// Copyright 2022 Stefan Kerkmann
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "quantum.h"
|
|
#include "ws2812.h"
|
|
#include "hardware/pio.h"
|
|
#include "hardware/clocks.h"
|
|
|
|
#if !defined(MCU_RP)
|
|
# error PIO Driver is only available for Raspberry Pi 2040 MCUs!
|
|
#endif
|
|
|
|
#if defined(WS2812_PIO_USE_PIO1)
|
|
static const PIO pio = pio1;
|
|
#else
|
|
static const PIO pio = pio0;
|
|
#endif
|
|
|
|
#if !defined(RP_DMA_PRIORITY_WS2812)
|
|
# define RP_DMA_PRIORITY_WS2812 12
|
|
#endif
|
|
|
|
static int state_machine = -1;
|
|
|
|
#define WS2812_WRAP_TARGET 0
|
|
#define WS2812_WRAP 3
|
|
|
|
#define WS2812_T1 2
|
|
#define WS2812_T2 5
|
|
#define WS2812_T3 3
|
|
|
|
#if defined(WS2812_EXTERNAL_PULLUP)
|
|
|
|
# pragma message "The GPIOs of the RP2040 are NOT 5V tolerant! Make sure to NOT apply any voltage over 3.3V to the RGB data pin."
|
|
|
|
// clang-format off
|
|
static const uint16_t ws2812_program_instructions[] = {
|
|
// .wrap_target
|
|
0x7221, // 0: out x, 1 side 1 [2]
|
|
0x0123, // 1: jmp !x, 3 side 0 [1]
|
|
0x0400, // 2: jmp 0 side 0 [4]
|
|
0xb442, // 3: nop side 1 [4]
|
|
// .wrap
|
|
};
|
|
|
|
#else
|
|
|
|
static const uint16_t ws2812_program_instructions[] = {
|
|
// .wrap_target
|
|
0x6221, // 0: out x, 1 side 0 [2]
|
|
0x1123, // 1: jmp !x, 3 side 1 [1]
|
|
0x1400, // 2: jmp 0 side 1 [4]
|
|
0xa442, // 3: nop side 0 [4]
|
|
// .wrap
|
|
};
|
|
// clang-format on
|
|
#endif
|
|
|
|
static const pio_program_t ws2812_program = {
|
|
.instructions = ws2812_program_instructions,
|
|
.length = 4,
|
|
.origin = -1,
|
|
};
|
|
|
|
static uint32_t WS2812_BUFFER[RGBLED_NUM];
|
|
static const rp_dma_channel_t* WS2812_DMA_CHANNEL;
|
|
|
|
bool ws2812_init(void) {
|
|
uint pio_idx = pio_get_index(pio);
|
|
/* Get PIOx peripheral out of reset state. */
|
|
hal_lld_peripheral_unreset(pio_idx == 0 ? RESETS_ALLREG_PIO0 : RESETS_ALLREG_PIO1);
|
|
|
|
// clang-format off
|
|
iomode_t rgb_pin_mode = PAL_RP_PAD_SLEWFAST |
|
|
PAL_RP_GPIO_OE |
|
|
(pio_idx == 0 ? PAL_MODE_ALTERNATE_PIO0 : PAL_MODE_ALTERNATE_PIO1);
|
|
// clang-format on
|
|
|
|
palSetLineMode(RGB_DI_PIN, rgb_pin_mode);
|
|
|
|
state_machine = pio_claim_unused_sm(pio, true);
|
|
if (state_machine < 0) {
|
|
dprintln("ERROR: Failed to acquire state machine for WS2812 output!");
|
|
return false;
|
|
}
|
|
|
|
uint offset = pio_add_program(pio, &ws2812_program);
|
|
|
|
pio_sm_set_consecutive_pindirs(pio, state_machine, RGB_DI_PIN, 1, true);
|
|
|
|
pio_sm_config config = pio_get_default_sm_config();
|
|
sm_config_set_wrap(&config, offset + WS2812_WRAP_TARGET, offset + WS2812_WRAP);
|
|
sm_config_set_sideset_pins(&config, RGB_DI_PIN);
|
|
sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX);
|
|
|
|
#if defined(WS2812_EXTERNAL_PULLUP)
|
|
/* Instruct side-set to change the pin-directions instead of outputting
|
|
* a logic level. We generate our levels the following way:
|
|
*
|
|
* 1: Set RGB data pin to high impedance input and let the pull-up drive the
|
|
* signal high.
|
|
*
|
|
* 0: Set RGB data pin to low impedance output and drive the pin low.
|
|
*/
|
|
sm_config_set_sideset(&config, 1, false, true);
|
|
#else
|
|
sm_config_set_sideset(&config, 1, false, false);
|
|
#endif
|
|
|
|
#if defined(RGBW)
|
|
sm_config_set_out_shift(&config, false, true, 32);
|
|
#else
|
|
sm_config_set_out_shift(&config, false, true, 24);
|
|
#endif
|
|
|
|
int cycles_per_bit = WS2812_T1 + WS2812_T2 + WS2812_T3;
|
|
float div = clock_get_hz(clk_sys) / (800.0f * KHZ * cycles_per_bit);
|
|
sm_config_set_clkdiv(&config, div);
|
|
|
|
pio_sm_init(pio, state_machine, offset, &config);
|
|
pio_sm_set_enabled(pio, state_machine, true);
|
|
|
|
WS2812_DMA_CHANNEL = dmaChannelAlloc(RP_DMA_CHANNEL_ID_ANY, RP_DMA_PRIORITY_WS2812, NULL, NULL);
|
|
|
|
// clang-format off
|
|
uint32_t mode = DMA_CTRL_TRIG_INCR_READ |
|
|
DMA_CTRL_TRIG_DATA_SIZE_WORD |
|
|
DMA_CTRL_TRIG_IRQ_QUIET |
|
|
DMA_CTRL_TRIG_TREQ_SEL(pio_idx == 0 ? state_machine : state_machine + 8);
|
|
// clang-format on
|
|
|
|
dmaChannelSetModeX(WS2812_DMA_CHANNEL, mode);
|
|
dmaChannelSetDestinationX(WS2812_DMA_CHANNEL, (uint32_t)&pio->txf[state_machine]);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Convert RGBW value into WS2812 compatible 32-bit data word.
|
|
*/
|
|
__always_inline static uint32_t rgbw8888_to_u32(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) {
|
|
#if (WS2812_BYTE_ORDER == WS2812_BYTE_ORDER_GRB)
|
|
return ((uint32_t)green << 24) | ((uint32_t)red << 16) | ((uint32_t)blue << 8) | ((uint32_t)white);
|
|
#elif (WS2812_BYTE_ORDER == WS2812_BYTE_ORDER_RGB)
|
|
return ((uint32_t)red << 24) | ((uint32_t)green << 16) | ((uint32_t)blue << 8) | ((uint32_t)white);
|
|
#elif (WS2812_BYTE_ORDER == WS2812_BYTE_ORDER_BGR)
|
|
return ((uint32_t)blue << 24) | ((uint32_t)green << 16) | ((uint32_t)red << 8) | ((uint32_t)white);
|
|
#endif
|
|
}
|
|
|
|
static inline void sync_ws2812_transfer(void) {
|
|
if (unlikely(dmaChannelIsBusyX(WS2812_DMA_CHANNEL) || !pio_sm_is_tx_fifo_empty(pio, state_machine))) {
|
|
fast_timer_t start = timer_read_fast();
|
|
do {
|
|
// Abort the synchronization if we have to wait longer than the total
|
|
// count of LEDs in millisecounds. This is safely much longer than it
|
|
// would take to push all the data out.
|
|
if (unlikely(timer_elapsed_fast(start) > RGBLED_NUM)) {
|
|
dprintln("ERROR: WS2812 DMA transfer has stalled, aborting!");
|
|
dmaChannelDisableX(WS2812_DMA_CHANNEL);
|
|
return;
|
|
}
|
|
|
|
} while (dmaChannelIsBusyX(WS2812_DMA_CHANNEL) || !pio_sm_is_tx_fifo_empty(pio, state_machine));
|
|
// We wait for the WS2812 chain to reset after all data has been pushed
|
|
// out.
|
|
wait_us(WS2812_TRST_US);
|
|
}
|
|
}
|
|
|
|
void ws2812_setleds(LED_TYPE* ledarray, uint16_t leds) {
|
|
static bool is_initialized = false;
|
|
if (unlikely(!is_initialized)) {
|
|
is_initialized = ws2812_init();
|
|
}
|
|
|
|
sync_ws2812_transfer();
|
|
|
|
for (int i = 0; i < leds; i++) {
|
|
#if defined(RGBW)
|
|
WS2812_BUFFER[i] = rgbw8888_to_u32(ledarray[i].r, ledarray[i].g, ledarray[i].b, ledarray[i].w);
|
|
#else
|
|
WS2812_BUFFER[i] = rgbw8888_to_u32(ledarray[i].r, ledarray[i].g, ledarray[i].b, 0);
|
|
#endif
|
|
}
|
|
|
|
dmaChannelSetSourceX(WS2812_DMA_CHANNEL, (uint32_t)WS2812_BUFFER);
|
|
dmaChannelSetCounterX(WS2812_DMA_CHANNEL, leds);
|
|
dmaChannelEnableX(WS2812_DMA_CHANNEL);
|
|
}
|