diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 575c872a..c0d67eb4 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -135,4 +135,10 @@ config LUA_MODULE_WIFI help Includes the WiFi module (recommended). +config LUA_MODULE_WS2812 + bool "WS2812 module" + default "n" + help + Includes the ws2812 module. + endmenu diff --git a/components/modules/ws2812.c b/components/modules/ws2812.c new file mode 100644 index 00000000..ee86de79 --- /dev/null +++ b/components/modules/ws2812.c @@ -0,0 +1,533 @@ + +#include "module.h" +#include "lauxlib.h" +#include "lmem.h" +#include "platform.h" + +#include + +#define FADE_IN 1 +#define FADE_OUT 0 +#define SHIFT_LOGICAL 0 +#define SHIFT_CIRCULAR 1 + + +typedef struct { + int size; + uint8_t colorsPerLed; + uint8_t values[0]; +} ws2812_buffer; + +static void ws2812_cleanup( lua_State *L, int pop ) +{ + if (pop) + lua_pop( L, pop ); + platform_ws2812_release(); +} + +// Lua: ws2812.write("string") +// Byte triples in the string are interpreted as G R B values. +// +// ws2812.write({pin = 4, data = string.char(0, 255, 0)}) sets the first LED red. +// ws2812.write({pin = 4, data = string.char(0, 0, 255):rep(10)}) sets ten LEDs blue. +// 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 top = lua_gettop( L ); + + for (int stack = 1; stack <= top; stack++) { + if (lua_type( L, stack ) != LUA_TTABLE) { + ws2812_cleanup( L, 0 ); + luaL_checktype( L, stack, LUA_TTABLE ); // trigger error + return 0; + } + + // + // retrieve pin + // + lua_getfield( L, stack, "pin" ); + if (!lua_isnumber( L, -1 )) { + ws2812_cleanup( L, 1 ); + return luaL_argerror( L, stack, "invalid pin" ); + } + int gpio_num = luaL_checkint( L, -1 ); + lua_pop( L, 1 ); + + // + // retrieve data + // + lua_getfield( L, stack, "data" ); + + const char *data; + size_t length; + int type = lua_type( L, -1 ); + if (type == LUA_TSTRING) + { + data = lua_tolstring( L, -1, &length ); + } + else if (type == LUA_TUSERDATA) + { + ws2812_buffer *buffer = (ws2812_buffer*)luaL_checkudata( L, -1, "ws2812.buffer" ); + + data = (const char *)buffer->values; + length = buffer->colorsPerLed*buffer->size; + } + else + { + ws2812_cleanup( L, 1 ); + return luaL_argerror(L, stack, "ws2812.buffer or string expected"); + } + lua_pop( L, 1 ); + + // prepare channel + if (platform_ws2812_setup( gpio_num, 1, (const uint8_t *)data, length ) != PLATFORM_OK) { + ws2812_cleanup( L, 0 ); + return luaL_argerror( L, stack, "can't set up chain" ); + } + } + + // + // send all channels at once + // + if (platform_ws2812_send() != PLATFORM_OK) { + ws2812_cleanup( L, 0 ); + return luaL_error( L, "sending failed" ); + } + + ws2812_cleanup( L, 0 ); + + return 0; +} + +static ptrdiff_t posrelat (ptrdiff_t pos, size_t len) { + /* relative string position: negative means back from end */ + if (pos < 0) pos += (ptrdiff_t)len + 1; + return (pos >= 0) ? pos : 0; +} + +static ws2812_buffer *allocate_buffer(lua_State *L, int leds, int colorsPerLed) { + // Allocate memory + size_t size = sizeof(ws2812_buffer) + colorsPerLed*leds*sizeof(uint8_t); + ws2812_buffer * buffer = (ws2812_buffer*)lua_newuserdata(L, size); + + // Associate its metatable + luaL_getmetatable(L, "ws2812.buffer"); + lua_setmetatable(L, -2); + + // Save led strip size + buffer->size = leds; + buffer->colorsPerLed = colorsPerLed; + + return buffer; +} + +// Handle a buffer where we can store led values +static int ws2812_new_buffer(lua_State *L) { + const int leds = luaL_checkint(L, 1); + const int colorsPerLed = luaL_checkint(L, 2); + + luaL_argcheck(L, leds > 0, 1, "should be a positive integer"); + luaL_argcheck(L, colorsPerLed > 0, 2, "should be a positive integer"); + + ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed); + + memset(buffer->values, 0, colorsPerLed * leds); + + return 1; +} + +static int ws2812_buffer_fill(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + // Grab colors + int i, j; + int * colors = luaM_malloc(L, buffer->colorsPerLed * sizeof(int)); + + for (i = 0; i < buffer->colorsPerLed; i++) + { + colors[i] = luaL_checkinteger(L, 2+i); + } + + // Fill buffer + uint8_t * p = &buffer->values[0]; + for(i = 0; i < buffer->size; i++) + { + for (j = 0; j < buffer->colorsPerLed; j++) + { + *p++ = colors[j]; + } + } + + // Free memory + luaM_free(L, colors); + + return 0; +} + +static int ws2812_buffer_fade(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + const int fade = luaL_checkinteger(L, 2); + unsigned direction = luaL_optinteger( L, 3, FADE_OUT ); + + luaL_argcheck(L, fade > 0, 2, "fade value should be a strict positive number"); + + uint8_t * p = &buffer->values[0]; + int val = 0; + int i; + for(i = 0; i < buffer->size * buffer->colorsPerLed; i++) + { + if (direction == FADE_OUT) + { + *p++ /= fade; + } + else + { + // as fade in can result in value overflow, an int is used to perform the check afterwards + val = *p * fade; + if (val > 255) val = 255; + *p++ = val; + } + } + + return 0; +} + + +static int ws2812_buffer_shift(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + const int shiftValue = luaL_checkinteger(L, 2); + const unsigned shift_type = luaL_optinteger( L, 3, SHIFT_LOGICAL ); + + ptrdiff_t start = posrelat(luaL_optinteger(L, 4, 1), buffer->size); + ptrdiff_t end = posrelat(luaL_optinteger(L, 5, -1), buffer->size); + if (start < 1) start = 1; + if (end > (ptrdiff_t)buffer->size) end = (ptrdiff_t)buffer->size; + + start--; + int size = end - start; + size_t offset = start * buffer->colorsPerLed; + + luaL_argcheck(L, shiftValue > 0-size && shiftValue < size, 2, "shifting more elements than buffer size"); + + int shift = shiftValue >= 0 ? shiftValue : -shiftValue; + + // check if we want to shift at all + if (shift == 0 || size <= 0) + { + return 0; + } + + uint8_t * tmp_pixels = luaM_malloc(L, buffer->colorsPerLed * sizeof(uint8_t) * shift); + size_t shift_len, remaining_len; + // calculate length of shift section and remaining section + shift_len = shift*buffer->colorsPerLed; + remaining_len = (size-shift)*buffer->colorsPerLed; + + if (shiftValue > 0) + { + // Store the values which are moved out of the array (last n pixels) + memcpy(tmp_pixels, &buffer->values[offset + (size-shift)*buffer->colorsPerLed], shift_len); + // Move pixels to end + memmove(&buffer->values[offset + shift*buffer->colorsPerLed], &buffer->values[offset], remaining_len); + // Fill beginning with temp data + if (shift_type == SHIFT_LOGICAL) + { + memset(&buffer->values[offset], 0, shift_len); + } + else + { + memcpy(&buffer->values[offset], tmp_pixels, shift_len); + } + } + else + { + // Store the values which are moved out of the array (last n pixels) + memcpy(tmp_pixels, &buffer->values[offset], shift_len); + // Move pixels to end + memmove(&buffer->values[offset], &buffer->values[offset + shift*buffer->colorsPerLed], remaining_len); + // Fill beginning with temp data + if (shift_type == SHIFT_LOGICAL) + { + memset(&buffer->values[offset + (size-shift)*buffer->colorsPerLed], 0, shift_len); + } + else + { + memcpy(&buffer->values[offset + (size-shift)*buffer->colorsPerLed], tmp_pixels, shift_len); + } + } + // Free memory + luaM_free(L, tmp_pixels); + + return 0; +} + +static int ws2812_buffer_dump(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + lua_pushlstring(L, (char *)buffer->values, buffer->size * buffer->colorsPerLed); + + return 1; +} + +static int ws2812_buffer_replace(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + size_t l = buffer->size; + ptrdiff_t start = posrelat(luaL_optinteger(L, 3, 1), l); + + uint8_t *src; + size_t srcLen; + + if (lua_type(L, 2) == LUA_TSTRING) { + size_t length; + + src = (uint8_t *) lua_tolstring(L, 2, &length); + srcLen = length / buffer->colorsPerLed; + } else { + ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer"); + src = rhs->values; + srcLen = rhs->size; + luaL_argcheck(L, rhs->colorsPerLed == buffer->colorsPerLed, 2, "Buffers have different colors"); + } + + luaL_argcheck(L, srcLen + start - 1 <= buffer->size, 2, "Does not fit into destination"); + + memcpy(buffer->values + (start - 1) * buffer->colorsPerLed, src, srcLen * buffer->colorsPerLed); + + return 0; +} + +// buffer:mix(factor1, buffer1, ..) +// factor is 256 for 100% +// uses saturating arithmetic (one buffer at a time) +static int ws2812_buffer_mix(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + int pos = 2; + size_t cells = buffer->size * buffer->colorsPerLed; + + int n_sources = (lua_gettop(L) - 1) / 2; + + struct { + int factor; + const uint8_t *values; + } source[n_sources]; + + int src; + for (src = 0; src < n_sources; src++, pos += 2) { + int factor = luaL_checkinteger(L, pos); + ws2812_buffer *src_buffer = (ws2812_buffer*) luaL_checkudata(L, pos + 1, "ws2812.buffer"); + + luaL_argcheck(L, src_buffer->size == buffer->size && src_buffer->colorsPerLed == buffer->colorsPerLed, pos + 1, "Buffer not same shape"); + + source[src].factor = factor; + source[src].values = src_buffer->values; + } + + size_t i; + for (i = 0; i < cells; i++) { + int val = 0; + for (src = 0; src < n_sources; src++) { + val += ((int)(source[src].values[i] * source[src].factor) >> 8); + } + + if (val < 0) { + val = 0; + } else if (val > 255) { + val = 255; + } + buffer->values[i] = val; + } + + return 0; +} + +// Returns the total of all channels +static int ws2812_buffer_power(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + size_t cells = buffer->size * buffer->colorsPerLed; + + size_t i; + int total = 0; + for (i = 0; i < cells; i++) { + total += buffer->values[i]; + } + + lua_pushnumber(L, total); + + return 1; +} + +static int ws2812_buffer_get(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + const int led = luaL_checkinteger(L, 2) - 1; + + luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range"); + + int i; + for (i = 0; i < buffer->colorsPerLed; i++) + { + lua_pushnumber(L, buffer->values[buffer->colorsPerLed*led+i]); + } + + return buffer->colorsPerLed; +} + +static int ws2812_buffer_set(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + const int led = luaL_checkinteger(L, 2) - 1; + + luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range"); + + int type = lua_type(L, 3); + if(type == LUA_TTABLE) + { + int i; + for (i = 0; i < buffer->colorsPerLed; i++) + { + // Get value and push it on stack + lua_rawgeti(L, 3, i+1); + + // Convert it as int and store them in buffer + buffer->values[buffer->colorsPerLed*led+i] = lua_tonumber(L, -1); + } + + // Clean up the stack + lua_pop(L, buffer->colorsPerLed); + } + else if(type == LUA_TSTRING) + { + size_t len; + const char * buf = lua_tolstring(L, 3, &len); + + // Overflow check + if( buffer->colorsPerLed*led + len > buffer->colorsPerLed*buffer->size ) + { + return luaL_error(L, "string size will exceed strip length"); + } + + memcpy(&buffer->values[buffer->colorsPerLed*led], buf, len); + } + else + { + int i; + for (i = 0; i < buffer->colorsPerLed; i++) + { + buffer->values[buffer->colorsPerLed*led+i] = luaL_checkinteger(L, 3+i); + } + } + + return 0; +} + +static int ws2812_buffer_size(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + lua_pushnumber(L, buffer->size); + + return 1; +} + +static int ws2812_buffer_sub(lua_State* L) { + ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + size_t l = lhs->size; + ptrdiff_t start = posrelat(luaL_checkinteger(L, 2), l); + ptrdiff_t end = posrelat(luaL_optinteger(L, 3, -1), l); + if (start < 1) start = 1; + if (end > (ptrdiff_t)l) end = (ptrdiff_t)l; + if (start <= end) { + ws2812_buffer *result = allocate_buffer(L, end - start + 1, lhs->colorsPerLed); + memcpy(result->values, lhs->values + lhs->colorsPerLed * (start - 1), lhs->colorsPerLed * (end - start + 1)); + } else { + ws2812_buffer *result = allocate_buffer(L, 0, lhs->colorsPerLed); + (void)result; + } + return 1; +} + +static int ws2812_buffer_concat(lua_State* L) { + ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer"); + + luaL_argcheck(L, lhs->colorsPerLed == rhs->colorsPerLed, 1, "Can only concatenate buffers with same colors"); + + int colorsPerLed = lhs->colorsPerLed; + int leds = lhs->size + rhs->size; + + ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed); + + memcpy(buffer->values, lhs->values, lhs->colorsPerLed * lhs->size); + memcpy(buffer->values + lhs->colorsPerLed * lhs->size, rhs->values, rhs->colorsPerLed * rhs->size); + + return 1; +} + +static int ws2812_buffer_tostring(lua_State* L) { + ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); + + luaL_Buffer result; + luaL_buffinit(L, &result); + + luaL_addchar(&result, '['); + int i; + int p = 0; + for (i = 0; i < buffer->size; i++) { + int j; + if (i > 0) { + luaL_addchar(&result, ','); + } + luaL_addchar(&result, '('); + for (j = 0; j < buffer->colorsPerLed; j++, p++) { + if (j > 0) { + luaL_addchar(&result, ','); + } + char numbuf[5]; + sprintf(numbuf, "%d", buffer->values[p]); + luaL_addstring(&result, numbuf); + } + luaL_addchar(&result, ')'); + } + + luaL_addchar(&result, ']'); + luaL_pushresult(&result); + + return 1; +} + +static const LUA_REG_TYPE ws2812_buffer_map[] = +{ + { LSTRKEY( "dump" ), LFUNCVAL( ws2812_buffer_dump )}, + { LSTRKEY( "fade" ), LFUNCVAL( ws2812_buffer_fade )}, + { LSTRKEY( "fill" ), LFUNCVAL( ws2812_buffer_fill )}, + { LSTRKEY( "get" ), LFUNCVAL( ws2812_buffer_get )}, + { LSTRKEY( "replace" ), LFUNCVAL( ws2812_buffer_replace )}, + { LSTRKEY( "mix" ), LFUNCVAL( ws2812_buffer_mix )}, + { LSTRKEY( "power" ), LFUNCVAL( ws2812_buffer_power )}, + { LSTRKEY( "set" ), LFUNCVAL( ws2812_buffer_set )}, + { LSTRKEY( "shift" ), LFUNCVAL( ws2812_buffer_shift )}, + { LSTRKEY( "size" ), LFUNCVAL( ws2812_buffer_size )}, + { LSTRKEY( "sub" ), LFUNCVAL( ws2812_buffer_sub )}, + { LSTRKEY( "__concat" ),LFUNCVAL( ws2812_buffer_concat )}, + { LSTRKEY( "__index" ), LROVAL( ws2812_buffer_map )}, + { LSTRKEY( "__tostring" ), LFUNCVAL( ws2812_buffer_tostring )}, + { LNILKEY, LNILVAL} +}; + +static const LUA_REG_TYPE ws2812_map[] = +{ + { LSTRKEY( "newBuffer" ), LFUNCVAL( ws2812_new_buffer )}, + { LSTRKEY( "write" ), LFUNCVAL( ws2812_write )}, + { LSTRKEY( "FADE_IN" ), LNUMVAL( FADE_IN ) }, + { LSTRKEY( "FADE_OUT" ), LNUMVAL( FADE_OUT ) }, + { LSTRKEY( "SHIFT_LOGICAL" ), LNUMVAL( SHIFT_LOGICAL ) }, + { LSTRKEY( "SHIFT_CIRCULAR" ), LNUMVAL( SHIFT_CIRCULAR ) }, + { LNILKEY, LNILVAL} +}; + +int luaopen_ws2812(lua_State *L) { + // TODO: Make sure that the GPIO system is initialized + luaL_rometatable(L, "ws2812.buffer", (void *)ws2812_buffer_map); // create metatable for ws2812.buffer + return 0; +} + +NODEMCU_MODULE(WS2812, "ws2812", ws2812_map, luaopen_ws2812); diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index cf5883fb..2ba8b076 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -172,6 +172,14 @@ uint16_t platform_onewire_crc16( const uint8_t* input, uint16_t len, uint16_t cr int platform_dht_read( uint8_t gpio_num, uint8_t wakeup_ms, uint8_t *data ); +// ***************************************************************************** +// WS2812 platform interface + +int platform_ws2812_setup( uint8_t gpio_num, uint8_t num_mem, const uint8_t *data, size_t len ); +int platform_ws2812_release( void ); +int platform_ws2812_send( void ); + + // Internal flash erase/write functions uint32_t platform_flash_get_sector_of_address( uint32_t addr ); diff --git a/components/platform/ws2812.c b/components/platform/ws2812.c new file mode 100644 index 00000000..f0ee4d77 --- /dev/null +++ b/components/platform/ws2812.c @@ -0,0 +1,237 @@ +/* **************************************************************************** + * + * ESP32 platform interface for WS2812 LEDs. + * + * Copyright (c) 2017, Arnim Laeuger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * ****************************************************************************/ + +#include "platform.h" + +#include "driver/rmt.h" +#include "driver/gpio.h" +#include "esp_log.h" + + +#undef WS2812_DEBUG + + +// 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 +}; + +#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 +typedef struct { + bool valid; + uint8_t gpio; + const uint8_t *data; + size_t len; + size_t tx_offset; +} ws2812_chain_t; + + +// chain descriptor array +static ws2812_chain_t ws2812_chains[RMT_CHANNEL_MAX]; + +// interrupt handler for ws2812 ISR +static intr_handle_t ws2812_intr_handle; + + +static void IRAM_ATTR 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++) { + uint8_t byte = data[idx]; + + 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; + + byte <<= 1; + } + } +} + + +static void IRAM_ATTR 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]); + uint32_t data_sub_len = RMT.tx_lim_ch[channel].limit/8; + + 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].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 channel; + + if ((channel = platform_rmt_allocate( num_mem )) >= 0) { + ws2812_chain_t *chain = &(ws2812_chains[channel]); + + chain->valid = true; + chain->gpio = gpio_num; + chain->len = len; + chain->data = data; + chain->tx_offset = 0; + +#ifdef WS2812_DEBUG + ESP_LOGI("ws2812", "Setup done for gpio %d on RMT channel %d", gpio_num, channel); +#endif + + return PLATFORM_OK; + } + + return PLATFORM_ERR; +} + +int platform_ws2812_release( void ) +{ + for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) { + ws2812_chain_t *chain = &(ws2812_chains[channel]); + + if (chain->valid) { + rmt_driver_uninstall( channel ); + + platform_rmt_release( channel ); + + // attach GPIO to pin, driving 0 + gpio_set_level( chain->gpio, 0 ); + gpio_set_direction( chain->gpio, GPIO_MODE_DEF_OUTPUT ); + gpio_matrix_out( chain->gpio, SIG_GPIO_OUT_IDX, 0, 0 ); + } + + } + + return PLATFORM_OK; +} + +int platform_ws2812_send( void ) +{ + rmt_config_t rmt_tx; + int res = PLATFORM_OK; + + // common settings + rmt_tx.mem_block_num = 1; + rmt_tx.clk_div = WS2812_CLKDIV; + rmt_tx.tx_config.loop_en = false; + rmt_tx.tx_config.carrier_en = false; + rmt_tx.tx_config.idle_level = 0; + rmt_tx.tx_config.idle_output_en = true; + rmt_tx.rmt_mode = RMT_MODE_TX; + + // configure selected RMT channels + for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX && res == PLATFORM_OK; channel++) { + if (ws2812_chains[channel].valid) { + rmt_tx.channel = channel; + rmt_tx.gpio_num = ws2812_chains[channel].gpio; + if (rmt_config( &rmt_tx ) != ESP_OK) { + res = PLATFORM_ERR; + break; + } + if (rmt_driver_install( channel, 0, ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_SHARED ) != ESP_OK) { + res = PLATFORM_ERR; + break; + } + } + } + + + // hook-in our shared ISR + esp_intr_alloc( ETS_RMT_INTR_SOURCE, ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_SHARED, ws2812_isr, NULL, &ws2812_intr_handle ); + + + // 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) { + // we just feed a single block for generating the reset to the rmt driver + // the actual payload data is handled by our private shared ISR + if (rmt_write_items( channel, + (rmt_item32_t *)ws2812_rmt_reset_block, + 64, + false ) != ESP_OK) { + res = PLATFORM_ERR; + break; + } + } + } + + // wait for all channels to finish + for (rmt_channel_t channel = 0; channel < RMT_CHANNEL_MAX; channel++) { + if (ws2812_chains[channel].valid) { + rmt_wait_tx_done( channel ); + } + } + + + // un-hook our ISR + esp_intr_free( ws2812_intr_handle ); + + + return res; +} diff --git a/docs/en/modules/ws2812.md b/docs/en/modules/ws2812.md new file mode 100644 index 00000000..e6c66807 --- /dev/null +++ b/docs/en/modules/ws2812.md @@ -0,0 +1,317 @@ +# 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)| + +ws2812 is a library to handle ws2812-like led strips. +It works at least on WS2812, WS2812b, APA104, SK6812 (RGB or RGBW). + +## ws2812.write() +Send data to one or two led strip using its native format which is generally Green,Red,Blue for RGB strips +and Green,Red,Blue,White for RGBW strips. + +#### Syntax +`ws2812.write(array)` + +#### Parameters +Array containing an element for each chain (maximum of 8 elements): +- `pin` 0 ~ 33, I/O index +- `data` payload to be sent to one or more WS2812 like leds through GPIO2 + +Payload type could be: +- `string` representing bytes to send +- `ws2812.buffer` see [Buffer module](#buffer-module) + +#### Returns +`nil` + +#### Example +```lua +ws2812.write({pin = 4, data = string.char(255, 0, 0, 255, 0, 0)}) -- turn the two first RGB leds to green +``` + +```lua +ws2812.write({pin = 4, string.char(0, 0, 0, 255, 0, 0, 0, 255)}) -- turn the two first RGBW leds to white +``` + +```lua +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 +``` + +# 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. + +For this purpose, the ws2812 library offers a read/write buffer. This buffer has a `__tostring` method so that it can be printed. This is useful for debugging. + +#### Example +Led chaser with a RGBW strip +```lua +local i, buffer = 0, ws2812.newBuffer(300, 4); buffer:fill(0, 0, 0, 0); tmr.create():alarm(50, 1, function() + i = i + 1 + buffer:fade(2) + buffer:set(i % buffer:size() + 1, 0, 0, 0, 255) + ws2812.write({pin = 4, data = buffer}) +end) +``` + +## ws2812.newBuffer() +Allocate a new memory buffer to store led values. + +#### Syntax +`ws2812.newBuffer(numberOfLeds, bytesPerLed)` + +#### Parameters + - `numberOfLeds` length of the led strip + - `bytesPerLed` 3 for RGB strips and 4 for RGBW strips + +#### Returns +`ws2812.buffer` + +## ws2812.buffer:get() +Return the value at the given position + +#### Syntax +`buffer:get(index)` + +#### Parameters + - `index` position in the buffer (1 for first led) + +#### Returns +`(color)` + +#### Example +```lua +buffer = ws2812.newBuffer(32, 4) +print(buffer:get(1)) +0 0 0 0 +``` + +## ws2812.buffer:set() +Set the value at the given position + +#### Syntax +`buffer:set(index, color)` + +#### Parameters + - `index` position in the buffer (1 for the first led) + - `color` payload of the color + +Payload could be: +- `number, number, ...` you should pass as many arguments as `bytesPerLed` +- `table` should contains `bytesPerLed` numbers +- `string` should contains `bytesPerLed` bytes + +#### Returns +`nil` + +#### Example +```lua +buffer = ws2812.newBuffer(32, 3) +buffer:set(1, 255, 0, 0) -- set the first led green for a RGB strip +``` + +```lua +buffer = ws2812.newBuffer(32, 4) +buffer:set(1, {0, 0, 0, 255}) -- set the first led white for a RGBW strip +``` + +```lua +buffer = ws2812.newBuffer(32, 3) +buffer:set(1, string.char(255, 0, 0)) -- set the first led green for a RGB strip +``` + +## ws2812.buffer:size() +Return the size of the buffer in number of leds + +#### Syntax +`buffer:size()` + +#### Parameters +none + +#### Returns +`int` + +## ws2812.buffer:fill() +Fill the buffer with the given color. +The number of given bytes must match the number of bytesPerLed of the buffer + +#### Syntax +`buffer:fill(color)` + +#### Parameters + - `color` bytes of the color, you should pass as many arguments as `bytesPerLed` + +#### Returns +`nil` + +#### Example +```lua +buffer:fill(0, 0, 0) -- fill the buffer with black for a RGB strip +``` + +## ws2812.buffer:dump() +Returns the contents of the buffer (the pixel values) as a string. This can then be saved to a file or sent over a network. + +#### Syntax +`buffer:dump()` + +#### Returns +A string containing the pixel values. + +#### Example +```lua +local s = buffer:dump() +``` + +## ws2812.buffer:replace() +Inserts a string (or a buffer) into another buffer with an offset. +The buffer must have the same number of colors per led or an error will be thrown. + +#### Syntax +`buffer:replace(source[, offset])` + +#### Parameters + - `source` the pixel values to be set into the buffer. This is either a string or a buffer. + - `offset` the offset where the source is to be placed in the buffer. Default is 1. Negative values can be used. + +#### Returns +`nil` + +#### Example +```lua +buffer:replace(anotherbuffer:dump()) -- copy one buffer into another via a string +buffer:replace(anotherbuffer) -- copy one buffer into another +newbuffer = buffer.sub(1) -- make a copy of a buffer into a new buffer +``` + +## ws2812.buffer:mix() +This is a general method that loads data into a buffer that is a linear combination of data from other buffers. It can be used to copy a buffer or, +more usefully, do a cross fade. The pixel values are computed as integers and then range limited to [0, 255]. This means that negative +factors work as expected, and that the order of combining buffers does not matter. + +#### Syntax +`buffer:mix(factor1, buffer1, ...)` + +#### Parameters + - `factor1` This is the factor that the contents of `buffer1` are multiplied by. This factor is scaled by a factor of 256. Thus `factor1` value of 256 is a factor of 1.0. + - `buffer1` This is the source buffer. It must be of the same shape as the destination buffer. + +There can be any number of factor/buffer pairs. + +#### Returns +`nil` + +#### Example +```lua +-- loads buffer with a crossfade between buffer1 and buffer2 +buffer:mix(256 - crossmix, buffer1, crossmix, buffer2) + +-- multiplies all values in buffer by 0.75 +-- This can be used in place of buffer:fade +buffer:mix(192, buffer) +``` + +## ws2812.buffer:power() +Computes the total energy requirement for the buffer. This is merely the total sum of all the pixel values (which assumes that each color in each +pixel consumes the same amount of power). A real WS2812 (or WS2811) has three constant current drivers of 20mA -- one for each of R, G and B. The +pulse width modulation will cause the *average* current to scale linearly with pixel value. + +#### Syntax +`buffer:power()` + +#### Returns +An integer which is the sum of all the pixel values. + +#### Example +```lua +-- Dim the buffer to no more than the PSU can provide +local psu_current_ma = 1000 +local led_current_ma = 20 +local led_sum = psu_current_ma * 255 / led_current_ma + +local p = buffer:power() +if p > led_sum then + buffer:mix(256 * led_sum / p, buffer) -- power is now limited +end +``` + +## ws2812.buffer:fade() +Fade in or out. Defaults to out. Multiply or divide each byte of each led with/by the given value. Useful for a fading effect. + +#### Syntax +`buffer:fade(value [, direction])` + +#### Parameters + - `value` value by which to divide or multiply each byte + - `direction` ws2812.FADE\_IN or ws2812.FADE\_OUT. Defaults to ws2812.FADE\_OUT + +#### Returns +`nil` + +#### Example +```lua +buffer:fade(2) +buffer:fade(2, ws2812.FADE_IN) +``` +## ws2812.buffer:shift() +Shift the content of (a piece of) the buffer in positive or negative direction. This allows simple animation effects. A slice of the buffer can be specified by using the +standard start and end offset Lua notation. Negative values count backwards from the end of the buffer. + +#### Syntax +`buffer:shift(value [, mode[, i[, j]]])` + +#### Parameters + - `value` number of pixels by which to rotate the buffer. Positive values rotate forwards, negative values backwards. + - `mode` is the shift mode to use. Can be one of `ws2812.SHIFT_LOGICAL` or `ws2812.SHIFT_CIRCULAR`. In case of SHIFT\_LOGICAL, the freed pixels are set to 0 (off). In case of SHIFT\_CIRCULAR, the buffer is treated like a ring buffer, inserting the pixels falling out on one end again on the other end. Defaults to SHIFT\_LOGICAL. + - `i` is the first offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is 1. + - `j` is the last offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is -1. + +#### Returns +`nil` + +#### Example +```lua +buffer:shift(3) +``` + +## ws2812.buffer:sub() +This implements the extraction function like `string.sub`. The indexes are in leds and all the same rules apply. + +#### Syntax +`buffer1:sub(i[, j])` + +#### Parameters + - `i` This is the start of the extracted data. Negative values can be used. + - `j` this is the end of the extracted data. Negative values can be used. The default is -1. + +#### Returns +A buffer containing the extracted piece. + +#### Example +``` +b = buffer:sub(1,10) +``` + +## ws2812.buffer:__concat() +This implements the `..` operator to concatenate two buffers. They must have the same number of colors per led. + +#### Syntax +`buffer1 .. buffer2` + +#### Parameters + - `buffer1` this is the start of the resulting buffer + - `buffer2` this is the end of the resulting buffer + +#### Returns +The concatenated buffer. + +#### Example +``` +ws2812.write({pin = 4, data = buffer1 .. buffer2}) +``` + + + diff --git a/mkdocs.yml b/mkdocs.yml index ba6dcb32..054801f3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,4 +43,5 @@ pages: - 'tmr': 'en/modules/tmr.md' - 'uart': 'en/modules/uart.md' - 'wifi': 'en/modules/wifi.md' + - 'ws2812': 'en/modules/ws2812.md'