Use a translator callback to convert GRB data to RMT pulses.

This commit is contained in:
Christoph Thelen 2022-02-23 18:25:31 +01:00
parent e5892a7286
commit b4af563bc3
4 changed files with 296 additions and 96 deletions

View File

@ -11,6 +11,13 @@
#define SHIFT_LOGICAL 0 #define SHIFT_LOGICAL 0
#define SHIFT_CIRCULAR 1 #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 500
typedef struct { typedef struct {
int size; int size;
@ -33,6 +40,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. // 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 ) static int ws2812_write( lua_State* L )
{ {
int type;
int top = lua_gettop( L ); int top = lua_gettop( L );
for (int stack = 1; stack <= top; stack++) { for (int stack = 1; stack <= top; stack++) {
@ -56,6 +64,106 @@ static int ws2812_write( lua_State* L )
int gpio_num = luaL_checkint( L, -1 ); int gpio_num = luaL_checkint( L, -1 );
lua_pop( 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 // retrieve data
// //
@ -83,7 +191,7 @@ static int ws2812_write( lua_State* L )
lua_pop( L, 1 ); lua_pop( L, 1 );
// prepare channel // 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 ); ws2812_cleanup( L, 0 );
return luaL_argerror( L, stack, "can't set up chain" ); return luaL_argerror( L, stack, "can't set up chain" );
} }

View File

@ -210,7 +210,7 @@ int platform_dht_read( uint8_t gpio_num, uint8_t wakeup_ms, uint8_t *data );
// WS2812 platform interface // WS2812 platform interface
void platform_ws2812_init( void ); 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_release( void );
int platform_ws2812_send( void ); int platform_ws2812_send( void );

View File

@ -30,122 +30,183 @@
#include "driver/gpio.h" #include "driver/gpio.h"
#include "esp_log.h" #include "esp_log.h"
#include "soc/periph_defs.h" #include "soc/periph_defs.h"
#include "soc/rmt_reg.h"
#undef WS2812_DEBUG #undef WS2812_DEBUG
// 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)
// 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
};
#define ws2812_rmt_reset {.level0 = 0, .duration0 = 4, .level1 = 0, .duration1 = 4}
// reset signal, spans one complete buffer block
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;
uint8_t gpio; uint8_t gpio;
bool sendreset;
rmt_item32_t reset;
rmt_item32_t bits[2];
const uint8_t *data; const uint8_t *data;
size_t len; size_t len;
size_t tx_offset; uint8_t bitpos;
} 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 // Callback function to convert GRB data to pulse lengths.
static intr_handle_t ws2812_intr_handle; static void ws2812_convert(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++) { size_t cnt_in;
uint8_t byte = data[idx]; 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;
for (uint8_t i = 0; i < 8; i++) { cnt_in = 0;
RMTMEM.chan[channel].data32[tx_offset + idx*8 + i].val = byte & 0x80 ? ws2812_rmt_bit1.val : ws2812_rmt_bit0.val; cnt_out = 0;
if( dest!=NULL && wanted_num>0 )
byte <<= 1;
}
}
}
static void ws2812_isr(void *arg)
{ {
uint32_t intr_st = RMT.int_st.val; tStatus = rmt_translator_get_context(item_num, &pvContext);
if( tStatus==ESP_OK )
{
ptContext = (ws2812_chain_t *)pvContext;
for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) { if( ptContext->sendreset==true )
{
if ((intr_st & (BIT(channel+24))) && (ws2812_chains[channel].valid)) { dest[cnt_out++] = ptContext->reset;
RMT.int_clr.val = BIT(channel+24); ptContext->sendreset = false;
ws2812_chain_t *chain = &(ws2812_chains[channel]);
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)
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;
#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;
} }
if( src!=NULL && src_size>0 )
{
ucBitPos = ptContext->bitpos;
/* Each bit of the input data is converted into one RMT item. */
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 = 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; 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]); ws2812_chain_t *chain = &(ws2812_chains[channel]);
rmt_item32_t tRmtItem;
uint32_t half;
chain->valid = true; chain->valid = true;
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->bitpos = 0;
// Send a reset if "reset" is not 0.
chain->sendreset = (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 #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);
@ -192,6 +253,7 @@ int platform_ws2812_send( void )
rmt_tx.tx_config.idle_level = 0; rmt_tx.tx_config.idle_level = 0;
rmt_tx.tx_config.idle_output_en = true; rmt_tx.tx_config.idle_output_en = true;
rmt_tx.rmt_mode = RMT_MODE_TX; rmt_tx.rmt_mode = RMT_MODE_TX;
rmt_tx.flags = 0;
// configure selected RMT channels // configure selected RMT channels
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++) {
@ -202,26 +264,38 @@ int platform_ws2812_send( void )
res = PLATFORM_ERR; res = PLATFORM_ERR;
break; break;
} }
if (rmt_driver_install( channel, 0, PLATFORM_RMT_INTR_FLAGS ) != ESP_OK) { if (rmt_driver_install( channel, 0, 0 /*PLATFORM_RMT_INTR_FLAGS*/ ) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
if (rmt_translator_init( channel, ws2812_convert) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
if (rmt_translator_set_context( channel, &(ws2812_chains[channel])) != ESP_OK) {
res = PLATFORM_ERR; res = PLATFORM_ERR;
break; break;
} }
} }
} }
#if SOC_RMT_SUPPORT_TX_SYNCHRO
// hook-in our shared ISR for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) {
esp_intr_alloc( ETS_RMT_INTR_SOURCE, PLATFORM_RMT_INTR_FLAGS, ws2812_isr, NULL, &ws2812_intr_handle ); 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 // 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 if (rmt_write_sample( channel,
// the actual payload data is handled by our private shared ISR ws2812_chains[channel].data,
if (rmt_write_items( channel, ws2812_chains[channel].len,
(rmt_item32_t *)ws2812_rmt_reset_block,
64,
false ) != ESP_OK) { false ) != ESP_OK) {
res = PLATFORM_ERR; res = PLATFORM_ERR;
break; break;
@ -236,10 +310,16 @@ int platform_ws2812_send( void )
} }
} }
#if SOC_RMT_SUPPORT_TX_SYNCHRO
// un-hook our ISR for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) {
esp_intr_free( ws2812_intr_handle ); if (ws2812_chains[channel].valid) {
if (rmt_remove_channel_from_group( channel ) != ESP_OK) {
res = PLATFORM_ERR;
break;
}
}
}
#endif
return res; return res;
} }

View File

@ -1,7 +1,7 @@
# WS2812 Module # WS2812 Module
| Since | Origin / Contributor | Maintainer | Source | | 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. ws2812 is a library to handle ws2812-like led strips.
It works at least on WS2812, WS2812b, APA104, SK6812 (RGB or RGBW). 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) - `pin` IO index, see [GPIO Overview](gpio.md#gpio-overview)
- `data` payload to be sent to one or more WS2812 like leds through GPIO2 - `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 500 which generates a reset of 50us.
- `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: Payload type could be:
- `string` representing bytes to send - `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 {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 RGB led on the ESP32-C3-DevKitM-1 to green
```
# Buffer module # Buffer module
For more advanced animations, it is useful to keep a "framebuffer" of the strip, For more advanced animations, it is useful to keep a "framebuffer" of the strip,
interact with it and flush it to the strip. interact with it and flush it to the strip.