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:
parent
10e3783948
commit
14cdff107f
|
@ -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" );
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue