Fix ws2812 on later esp32 variants

By not relying on the internal details of the RMT registers. For example
on the esp32s2, the threshold event bits start at bit 12 not bit 24, and
on the esp32s3 the memory buffers are only 48 words not 64. Also the
esp-idf recommends against hooking the rmt ISR in the first place.

Instead, use the published rmt APIs (specifically rmt_write_sample and
rmt_translator_init) and the default rmt ISR, which should be much more
resilient against future esp32 SoC changes.
This commit is contained in:
Tom Sutcliffe 2023-04-30 18:46:07 +01:00 committed by Johny Mattsson
parent 32d03a21da
commit 1b38d8e6eb
1 changed files with 57 additions and 74 deletions

View File

@ -33,6 +33,11 @@
#undef WS2812_DEBUG #undef WS2812_DEBUG
// If either of these fails, the reset logic in ws2812_sample_to_rmt will need revisiting.
_Static_assert(SOC_RMT_MEM_WORDS_PER_CHANNEL % 8 == 0,
"SOC_RMT_MEM_WORDS_PER_CHANNEL is assumed to be a multiple of 8");
_Static_assert(SOC_RMT_MEM_WORDS_PER_CHANNEL >= 16,
"SOC_RMT_MEM_WORDS_PER_CHANNEL is assumed to be >= 16");
// divider to generate 100ns base period from 80MHz APB clock // divider to generate 100ns base period from 80MHz APB clock
#define WS2812_CLKDIV (100 * 80 /1000) #define WS2812_CLKDIV (100 * 80 /1000)
@ -58,84 +63,67 @@ const rmt_item32_t ws2812_rmt_bit1 = {
.duration1 = WS2812_DURATION_T1L .duration1 = WS2812_DURATION_T1L
}; };
#define ws2812_rmt_reset {.level0 = 0, .duration0 = 4, .level1 = 0, .duration1 = 4} // This is one eighth of 512 * 100ns, ie in total a bit above the requisite 50us
// reset signal, spans one complete buffer block const rmt_item32_t ws2812_rmt_reset = { .level0 = 0, .duration0 = 32, .level1 = 0, .duration1 = 32 };
const rmt_item32_t ws2812_rmt_reset_block[64] = { [0 ... 63] = ws2812_rmt_reset };
// descriptor for a ws2812 chain // descriptor for a ws2812 chain
typedef struct { typedef struct {
bool valid; bool valid;
bool needs_reset;
uint8_t gpio; uint8_t gpio;
const uint8_t *data; const uint8_t *data;
size_t len; size_t len;
size_t tx_offset;
} ws2812_chain_t; } ws2812_chain_t;
// chain descriptor array // chain descriptor array
static ws2812_chain_t ws2812_chains[RMT_CHANNEL_MAX]; static ws2812_chain_t ws2812_chains[RMT_CHANNEL_MAX];
// interrupt handler for ws2812 ISR #define MIN(a, b) ((a) < (b) ? (a) : (b))
static intr_handle_t ws2812_intr_handle;
static void ws2812_sample_to_rmt(const void *src, rmt_item32_t *dest, size_t src_size, size_t wanted_num, size_t *translated_size, size_t *item_num)
static void ws2812_fill_memory_encoded( rmt_channel_t channel, const uint8_t *data, size_t len, size_t tx_offset )
{ {
for (size_t idx = 0; idx < len; idx++) { // Note: enabling these commented-out logs will ruin the timing so nothing
// will actually work when they're enabled. But I've kept them in as comments
// because they were useful in debugging the buffer management.
// ESP_DRAM_LOGW("ws2812", "ws2812_sample_to_rmt wanted=%u src_size=%u", wanted_num, src_size);
void *ctx;
rmt_translator_get_context(item_num, &ctx);
ws2812_chain_t *chain = (ws2812_chain_t *)ctx;
size_t reset_num = 0;
if (chain->needs_reset) {
// Haven't sent reset yet
// We split the reset into 8 even though it would fit in a single
// rmt_item32_t, simply so that dest stays 8-item aligned which means we
// don't have to worry about having to split a byte of src across multiple
// blocks (assuming the static asserts at the top of this file are true).
for (int i = 0; i < 8; i++) {
dest[i] = ws2812_rmt_reset;
}
dest += 8;
wanted_num -= 8;
reset_num = 8;
chain->needs_reset = false;
}
// Now write the actual data from src
const uint8_t *data = (const uint8_t *)src;
size_t data_num = MIN(wanted_num, src_size * 8) / 8;
for (size_t idx = 0; idx < data_num; idx++) {
uint8_t byte = data[idx]; uint8_t byte = data[idx];
for (uint8_t i = 0; i < 8; i++) { for (uint8_t i = 0; i < 8; i++) {
RMTMEM.chan[channel].data32[tx_offset + idx*8 + i].val = byte & 0x80 ? ws2812_rmt_bit1.val : ws2812_rmt_bit0.val; dest[idx * 8 + i] = (byte & 0x80) ? ws2812_rmt_bit1 : ws2812_rmt_bit0;
byte <<= 1; byte <<= 1;
} }
} }
*translated_size = data_num;
*item_num = reset_num + data_num * 8;
// ESP_DRAM_LOGW("ws2812", "src bytes consumed: %u total rmt items: %u", *translated_size, *item_num);
} }
static void ws2812_isr(void *arg)
{
uint32_t intr_st = RMT.int_st.val;
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) {
if ((intr_st & (BIT(channel+24))) && (ws2812_chains[channel].valid)) {
RMT.int_clr.val = BIT(channel+24);
ws2812_chain_t *chain = &(ws2812_chains[channel]);
#if defined(CONFIG_IDF_TARGET_ESP32)
uint32_t data_sub_len = RMT.tx_lim_ch[channel].limit/8;
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
uint32_t data_sub_len = RMT.chn_tx_lim[channel].tx_lim_chn/8;
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
uint32_t data_sub_len = RMT.tx_lim_ch[channel].tx_lim/8;
#else
uint32_t data_sub_len = RMT.tx_lim[channel].limit/8;
#endif
if (chain->len >= data_sub_len) {
ws2812_fill_memory_encoded( channel, chain->data, data_sub_len, chain->tx_offset );
chain->data += data_sub_len;
chain->len -= data_sub_len;
} else if (chain->len == 0) {
RMTMEM.chan[channel].data32[chain->tx_offset].val = 0;
} else {
ws2812_fill_memory_encoded( channel, chain->data, chain->len, chain->tx_offset );
RMTMEM.chan[channel].data32[chain->tx_offset + chain->len*8].val = 0;
chain->data += chain->len;
chain->len = 0;
}
if (chain->tx_offset == 0) {
chain->tx_offset = data_sub_len * 8;
} else {
chain->tx_offset = 0;
}
}
}
}
int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *data, size_t len ) int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *data, size_t len )
{ {
int channel; int channel;
@ -147,7 +135,7 @@ int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *dat
chain->gpio = gpio_num; chain->gpio = gpio_num;
chain->len = len; chain->len = len;
chain->data = data; chain->data = data;
chain->tx_offset = 0; chain->needs_reset = true;
#ifdef WS2812_DEBUG #ifdef WS2812_DEBUG
ESP_LOGI("ws2812", "Setup done for gpio %d on RMT channel %d", gpio_num, channel); ESP_LOGI("ws2812", "Setup done for gpio %d on RMT channel %d", gpio_num, channel);
@ -209,23 +197,23 @@ int platform_ws2812_send( void )
res = PLATFORM_ERR; res = PLATFORM_ERR;
break; break;
} }
if (rmt_translator_init(channel, ws2812_sample_to_rmt) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
if (rmt_translator_set_context(channel, &ws2812_chains[channel]) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
} }
} }
// hook-in our shared ISR
esp_intr_alloc( ETS_RMT_INTR_SOURCE, PLATFORM_RMT_INTR_FLAGS, ws2812_isr, NULL, &ws2812_intr_handle );
// start selected channels one by one // start selected channels one by one
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) { for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) {
if (ws2812_chains[channel].valid) { if (ws2812_chains[channel].valid) {
// we just feed a single block for generating the reset to the rmt driver // ws2812_sample_to_rmt takes care of translating the data to rmt_item32_t
// the actual payload data is handled by our private shared ISR // format, as well as prepending the reset sequence.
if (rmt_write_items( channel, if (rmt_write_sample(channel, ws2812_chains[channel].data, ws2812_chains[channel].len, false) != ESP_OK) {
(rmt_item32_t *)ws2812_rmt_reset_block,
64,
false ) != ESP_OK) {
res = PLATFORM_ERR; res = PLATFORM_ERR;
break; break;
} }
@ -239,11 +227,6 @@ int platform_ws2812_send( void )
} }
} }
// un-hook our ISR
esp_intr_free( ws2812_intr_handle );
return res; return res;
} }