Update rmt translator (#3629)

* Choose the number of RMT buffers in the ws2812 module.

The number of buffers required for optimal operation should be selected
by the ws2812 module, not the caller.

* Add parameters for RGB LED bit times.

This patch adds compatibility for different RGB LEDS besides the WS2812.
ESP evaluation boards like the ESP32-C3-DevKitM-1 use an SK68XXMINI-HS
RGB LED which does not respond to the timings of this module.
The patch adds optional parameters for the bit timings to the write
function. If the new parameters are not supplied, the old values are used.
An example for the SK68XXMINI-HS is provided in the documentation.

* Remove restrictions from RTM translator.

The old RMT translator was not able to split the bits of the source
data into the size requested by the RMT transmitter. Either all 8 bits
of an input byte were translated or none.
The new routine removes the restriction by delivering exactly the
requested amount of data to the transmitter, which results in a more
balanced buffering of translated data under load.

* Add a parameter for the RGB LED reset time.

This patch introduces a new optional parameter for the reset time
in the RGB LED communication. The default is 51.2 microseconds. A
value of 0 sends no reset signal, which allows a small optimisation
for consecutive write commands.

Please note that the reset time of the old code should be 50
microseconds, as the define WS2812_DURATION_RESET suggested. Due to the
restrictions of the old RMT translator routine, it was slightly
increased to 51.2 microseconds. This patch keeps the value of 51.2
microseconds to be as compatible as possible.

* Minimize the time drift between RMT channels.

Place all RMT channels in a group to minimize the time drift between
the signals. Please note that this feature is not available on all
platforms.

* Fix the description of the SK6812 LED in the example code.

The SK6812 expects the data for the green LED first, then red and
finally blue. It should be described as a GRB LED.
This commit is contained in:
Christoph Thelen 2024-02-07 23:56:17 +01:00 committed by GitHub
parent 10e3783948
commit 14cdff107f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 285 additions and 72 deletions

View File

@ -11,6 +11,14 @@
#define SHIFT_LOGICAL 0
#define SHIFT_CIRCULAR 1
// The default bit H & L durations in multiples of 100ns.
#define WS2812_DURATION_T0H 4
#define WS2812_DURATION_T0L 7
#define WS2812_DURATION_T1H 8
#define WS2812_DURATION_T1L 6
// The default reset duration in multiples of 100ns.
#define WS2812_DURATION_RESET 512
typedef struct {
int size;
@ -33,6 +41,7 @@ static void ws2812_cleanup( lua_State *L, int pop )
// ws2812.write({pin = 4, data = string.char(255, 0, 0, 255, 255, 255)}) first LED green, second LED white.
static int ws2812_write( lua_State* L )
{
int type;
int top = lua_gettop( L );
for (int stack = 1; stack <= top; stack++) {
@ -56,6 +65,106 @@ static int ws2812_write( lua_State* L )
int gpio_num = luaL_checkint( L, -1 );
lua_pop( L, 1 );
//
// retrieve reset
// This is an optional parameter which defaults to WS2812_DURATION_RESET.
//
int reset = WS2812_DURATION_RESET;
type = lua_getfield( L, stack, "reset" );
if (type!=LUA_TNIL )
{
if (!lua_isnumber( L, -1 )) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "invalid reset" );
}
reset = luaL_checkint( L, -1 );
if ((reset<0) || (reset>0xfffe)) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "reset must be 0<=reset<=65534" );
}
}
lua_pop( L, 1 );
//
// retrieve t0h
// This is an optional parameter which defaults to WS2812_DURATION_T0H.
//
int t0h = WS2812_DURATION_T0H;
type = lua_getfield( L, stack, "t0h" );
if (type!=LUA_TNIL )
{
if (!lua_isnumber( L, -1 )) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "invalid t0h" );
}
t0h = luaL_checkint( L, -1 );
if ((t0h<1) || (t0h>0x7fff)) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "t0h must be 1<=t0h<=32767" );
}
}
lua_pop( L, 1 );
//
// retrieve t0l
// This is an optional parameter which defaults to WS2812_DURATION_T0L.
//
int t0l = WS2812_DURATION_T0L;
type = lua_getfield( L, stack, "t0l" );
if (type!=LUA_TNIL )
{
if (!lua_isnumber( L, -1 )) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "invalid t0l" );
}
t0l = luaL_checkint( L, -1 );
if ((t0l<1) || (t0l>0x7fff)) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "t0l must be 1<=t0l<=32767" );
}
}
lua_pop( L, 1 );
//
// retrieve t1h
// This is an optional parameter which defaults to WS2812_DURATION_T1H.
//
int t1h = WS2812_DURATION_T1H;
type = lua_getfield( L, stack, "t1h" );
if (type!=LUA_TNIL )
{
if (!lua_isnumber( L, -1 )) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "invalid t1h" );
}
t1h = luaL_checkint( L, -1 );
if ((t1h<1) || (t1h>0x7fff)) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "t1h must be 1<=t1h<=32767" );
}
}
lua_pop( L, 1 );
//
// retrieve t1l
// This is an optional parameter which defaults to WS2812_DURATION_T1L.
//
int t1l = WS2812_DURATION_T1L;
type = lua_getfield( L, stack, "t1l" );
if (type!=LUA_TNIL )
{
if (!lua_isnumber( L, -1 )) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "invalid t1l" );
}
t1l = luaL_checkint( L, -1 );
if ((t1l<1) || (t1l>0x7fff)) {
ws2812_cleanup( L, 1 );
return luaL_argerror( L, stack, "t1l must be 1<=t1l<=32767" );
}
}
lua_pop( L, 1 );
//
// retrieve data
//
@ -83,7 +192,7 @@ static int ws2812_write( lua_State* L )
lua_pop( L, 1 );
// prepare channel
if (platform_ws2812_setup( gpio_num, 1, (const uint8_t *)data, length ) != PLATFORM_OK) {
if (platform_ws2812_setup( gpio_num, reset, t0h, t0l, t1h, t1l, (const uint8_t *)data, length ) != PLATFORM_OK) {
ws2812_cleanup( L, 0 );
return luaL_argerror( L, stack, "can't set up chain" );
}

View File

@ -209,7 +209,7 @@ int platform_dht_read( uint8_t gpio_num, uint8_t wakeup_ms, uint8_t *data );
// WS2812 platform interface
void platform_ws2812_init( void );
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, uint32_t reset, uint32_t bit0h, uint32_t bit0l, uint32_t bit1h, uint32_t bit1l, const uint8_t *data, size_t len );
int platform_ws2812_release( void );
int platform_ws2812_send( void );

View File

@ -32,112 +32,180 @@
#include "soc/periph_defs.h"
#include "rom/gpio.h" // for gpio_matrix_out()
#include "soc/gpio_periph.h"
#include "soc/rmt_reg.h"
#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
#define WS2812_CLKDIV (100 * 80 /1000)
// bit H & L durations in multiples of 100ns
#define WS2812_DURATION_T0H 4
#define WS2812_DURATION_T0L 7
#define WS2812_DURATION_T1H 8
#define WS2812_DURATION_T1L 6
#define WS2812_DURATION_RESET (50000 / 100)
// 0 bit in rmt encoding
const rmt_item32_t ws2812_rmt_bit0 = {
.level0 = 1,
.duration0 = WS2812_DURATION_T0H,
.level1 = 0,
.duration1 = WS2812_DURATION_T0L
};
// 1 bit in rmt encoding
const rmt_item32_t ws2812_rmt_bit1 = {
.level0 = 1,
.duration0 = WS2812_DURATION_T1H,
.level1 = 0,
.duration1 = WS2812_DURATION_T1L
};
// This is one eighth of 512 * 100ns, ie in total a bit above the requisite 50us
const rmt_item32_t ws2812_rmt_reset = { .level0 = 0, .duration0 = 32, .level1 = 0, .duration1 = 32 };
// descriptor for a ws2812 chain
typedef struct {
bool valid;
bool needs_reset;
uint8_t gpio;
rmt_item32_t reset;
rmt_item32_t bits[2];
const uint8_t *data;
size_t len;
uint8_t bitpos;
} ws2812_chain_t;
// chain descriptor array
static ws2812_chain_t ws2812_chains[RMT_CHANNEL_MAX];
#define MIN(a, b) ((a) < (b) ? (a) : (b))
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)
{
// 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);
size_t cnt_in;
size_t cnt_out;
const uint8_t *pucData;
uint8_t ucData;
uint8_t ucBitPos;
esp_err_t tStatus;
void *pvContext;
ws2812_chain_t *ptContext;
uint8_t ucBit;
void *ctx;
rmt_translator_get_context(item_num, &ctx);
ws2812_chain_t *chain = (ws2812_chain_t *)ctx;
cnt_in = 0;
cnt_out = 0;
if( dest!=NULL && wanted_num>0 )
{
tStatus = rmt_translator_get_context(item_num, &pvContext);
if( tStatus==ESP_OK )
{
ptContext = (ws2812_chain_t *)pvContext;
size_t reset_num = 0;
if (chain->needs_reset) {
// Haven't sent reset yet
if( ptContext->needs_reset==true )
{
dest[cnt_out++] = ptContext->reset;
ptContext->needs_reset = false;
}
if( src!=NULL && src_size>0 )
{
ucBitPos = ptContext->bitpos;
// 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;
}
/* Each bit of the input data is converted into one RMT item. */
// 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];
for (uint8_t i = 0; i < 8; i++) {
dest[idx * 8 + i] = (byte & 0x80) ? ws2812_rmt_bit1 : ws2812_rmt_bit0;
byte <<= 1;
pucData = (const uint8_t*)src;
/* Get the current byte. */
ucData = pucData[cnt_in] << ucBitPos;
while( cnt_in<src_size && cnt_out<wanted_num )
{
/* Get the current bit. */
ucBit = (ucData & 0x80U) >> 7U;
/* Translate the bit to a WS2812 input code. */
dest[cnt_out++] = ptContext->bits[ucBit];
/* Move to the next bit. */
++ucBitPos;
if( ucBitPos<8U )
{
ucData <<= 1;
}
else
{
ucBitPos = 0U;
++cnt_in;
ucData = pucData[cnt_in];
}
}
ptContext->bitpos = ucBitPos;
}
}
}
*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);
*translated_size = cnt_in;
*item_num = cnt_out;
}
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, uint32_t reset, uint32_t t0h, uint32_t t0l, uint32_t t1h, uint32_t t1l, const uint8_t *data, size_t len )
{
int channel;
if ((channel = platform_rmt_allocate( num_mem, RMT_MODE_TX )) >= 0) {
if ((channel = platform_rmt_allocate( 1, RMT_MODE_TX )) >= 0) {
ws2812_chain_t *chain = &(ws2812_chains[channel]);
rmt_item32_t tRmtItem;
uint32_t half;
chain->valid = true;
chain->gpio = gpio_num;
chain->len = len;
chain->data = data;
chain->needs_reset = true;
chain->bitpos = 0;
// Send a reset if "reset" is not 0.
chain->needs_reset = (reset != 0);
// Construct the RMT item for a reset.
tRmtItem.level0 = 0;
tRmtItem.level1 = 0;
// The reset duration must fit into one RMT item. This leaves 2*15 bit,
// which results in a maximum of 0xfffe .
if (reset>0xfffe)
{
reset = 0xfffe;
}
if (reset>0x7fff)
{
tRmtItem.duration0 = 0x7fff;
tRmtItem.duration1 = reset - 0x7fff;
}
else
{
half = reset >> 1U;
tRmtItem.duration0 = half;
tRmtItem.duration1 = reset - half;
}
chain->reset = tRmtItem;
// Limit the bit times to the available 15 bits.
// The values must not be 0.
if( t0h==0 )
{
t0h = 1;
}
else if( t0h>0x7fffU )
{
t0h = 0x7fffU;
}
if( t0l==0 )
{
t0l = 1;
}
else if( t0l>0x7fffU )
{
t0l = 0x7fffU;
}
if( t1h==0 )
{
t1h = 1;
}
else if( t1h>0x7fffU )
{
t1h = 0x7fffU;
}
if( t1l==0 )
{
t1l = 1;
}
else if( t1l>0x7fffU )
{
t1l = 0x7fffU;
}
// Construct the RMT item for a 0 bit.
tRmtItem.level0 = 1;
tRmtItem.duration0 = t0h;
tRmtItem.level1 = 0;
tRmtItem.duration1 = t0l;
chain->bits[0] = tRmtItem;
// Construct the RMT item for a 1 bit.
tRmtItem.level0 = 1;
tRmtItem.duration0 = t1h;
tRmtItem.level1 = 0;
tRmtItem.duration1 = t1l;
chain->bits[1] = tRmtItem;
#ifdef WS2812_DEBUG
ESP_LOGI("ws2812", "Setup done for gpio %d on RMT channel %d", gpio_num, channel);
@ -210,6 +278,19 @@ int platform_ws2812_send( void )
}
}
// Try to add all channels to a group. This moves the start of all RMT sequences closer
// together.
#if SOC_RMT_SUPPORT_TX_SYNCHRO
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) {
if (ws2812_chains[channel].valid) {
if (rmt_add_channel_to_group( channel ) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
}
}
#endif
// start selected channels one by one
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) {
if (ws2812_chains[channel].valid) {
@ -229,6 +310,17 @@ int platform_ws2812_send( void )
}
}
#if SOC_RMT_SUPPORT_TX_SYNCHRO
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) {
if (ws2812_chains[channel].valid) {
if (rmt_remove_channel_from_group( channel ) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
}
}
#endif
return res;
}

View File

@ -1,7 +1,7 @@
# WS2812 Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2015-02-05 | [Till Klocke](https://github.com/dereulenspiegel), [Thomas Soëte](https://github.com/Alkorin) | [Arnim Läuger](https://github.com/devsaurus) | [ws2812.c](../../components/modules/ws2812.c)|
| 2015-02-05 | [Till Klocke](https://github.com/dereulenspiegel), [Thomas Soëte](https://github.com/Alkorin), [Christoph Thelen](https://github.com/docbacardi) | [Arnim Läuger](https://github.com/devsaurus) | [ws2812.c](../../components/modules/ws2812.c)|
ws2812 is a library to handle ws2812-like led strips.
It works at least on WS2812, WS2812b, APA104, SK6812 (RGB or RGBW).
@ -22,6 +22,14 @@ Variable number of tables, each describing a single strip. Required elements are
- `pin` IO index, see [GPIO Overview](gpio.md#gpio-overview)
- `data` payload to be sent to one or more WS2812 like leds through GPIO2
Optional elements are:
- `reset` duration of the reset signal in multiples of 100ns. A duration of 0 generates no reset. The minimum possible value is 0. The maximum is 65534. The default value is 512 which generates a reset of 51.2us.
- `t0h` duration of the high period for a 0 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 4 which results in a high period of 400ns for each 0 code.
- `t0l` duration of the low period for a 0 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 7 which results in a low period of 700ns for each 0 code.
- `t1h` duration of the high period for a 1 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 8 which results in a high period of 800ns for each 1 code.
- `t1l` duration of the low period for a 1 code in multiples of 100ns. The minimum possible value is 1. The maximum is 32767. The default value is 6 which results in a low period of 600ns for each 1 code.
Payload type could be:
- `string` representing bytes to send
@ -44,6 +52,10 @@ ws2812.write({pin = 4, data = string.char(255, 0, 0, 255, 0, 0)},
{pin = 14, data = string.char(0, 255, 0, 0, 255, 0)}) -- turn the two first RGB leds to green on the first strip and red on the second strip
```
```lua
ws2812.write({pin = 8, reset = 800, t0h = 3, t0l = 9, t1h = 6, t1l = 6, data = string.char(1, 0, 0)}) -- turn the SK6812 GRB led on the ESP32-C3-DevKitM-1 to green
```
# Buffer module
For more advanced animations, it is useful to keep a "framebuffer" of the strip,
interact with it and flush it to the strip.