diff --git a/components/modules/CMakeLists.txt b/components/modules/CMakeLists.txt index e1f68d5d..aff349f9 100644 --- a/components/modules/CMakeLists.txt +++ b/components/modules/CMakeLists.txt @@ -51,6 +51,7 @@ if(IDF_TARGET STREQUAL "esp32") "eth.c" "i2s.c" "pulsecnt.c" + "rmt.c" "sdmmc.c" "touch.c" ) diff --git a/components/modules/Kconfig b/components/modules/Kconfig index b07b6d0e..3758bc59 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -218,6 +218,13 @@ menu "NodeMCU modules" Includes the QR Code Generator from https://www.nayuki.io/page/qr-code-generator-library + config NODEMCU_CMODULE_RMT + bool "Remote Control pulse generator/receiver" + default "n" + help + Includes the rmt module to use the ESP32's built-in + remote control hardware. + config NODEMCU_CMODULE_SDMMC depends on IDF_TARGET_ESP32 bool "SD-MMC module" diff --git a/components/modules/rmt.c b/components/modules/rmt.c new file mode 100644 index 00000000..30577700 --- /dev/null +++ b/components/modules/rmt.c @@ -0,0 +1,381 @@ +// Module for working with the rmt driver + +#include +#include "module.h" +#include "lauxlib.h" +#include "platform.h" +#include "platform_rmt.h" +#include "task/task.h" + +#include "driver/rmt.h" + +#include "common.h" + +typedef struct { + bool tx; + int channel; + int cb_ref; + struct _lrmt_cb_params *rx_params; +} *lrmt_channel_t; + +typedef struct _lrmt_cb_params { + bool dont_call; + bool rx_shutting_down; + rmt_channel_t channel; + int cb_ref; + int data_ref; + int rc; + size_t len; + rmt_item32_t *data; +} lrmt_cb_params; + +static task_handle_t cb_task_id; + +static int get_divisor(lua_State *L, int index) { + int bittime = luaL_checkinteger(L, index); + + int divisor = bittime / 12500; // 80MHz clock + + luaL_argcheck(L, divisor >= 1 && divisor <= 255, index, "Bit time out of range"); + + return divisor; +} + +static int configure_channel(lua_State *L, rmt_config_t *config, rmt_mode_t mode) { + lrmt_channel_t ud = (lrmt_channel_t)lua_newuserdata(L, sizeof(*ud)); + if (!ud) return luaL_error(L, "not enough memory"); + memset(ud, 0, sizeof(*ud)); + luaL_getmetatable(L, "rmt.channel"); + lua_setmetatable(L, -2); + + // We have allocated the channel -- must free it if the rest of this method fails + int channel = platform_rmt_allocate(1, mode); + + if (channel < 0) { + return luaL_error(L, "no spare RMT channel"); + } + + config->channel = channel; + + ud->channel = channel; + ud->tx = mode == RMT_MODE_TX; + + esp_err_t rc = rmt_config(config); + if (rc) { + platform_rmt_release(config->channel); + return luaL_error(L, "Failed to configure RMT"); + } + + rc = rmt_driver_install(config->channel, 1000, 0); + if (rc) { + platform_rmt_release(config->channel); + return luaL_error(L, "Failed to install RMT driver"); + } + + return 1; +} + +static int lrmt_txsetup(lua_State *L) { + int gpio = luaL_checkinteger(L, 1); + int divisor = get_divisor(L, 2); + + // We will set the channel later + rmt_config_t config = RMT_DEFAULT_CONFIG_TX(gpio, 0); + config.clk_div = divisor; + + if (lua_type(L, 3) == LUA_TTABLE) { + lua_getfield(L, 3, "carrier_hz"); + int hz = lua_tointeger(L, -1); + if (hz) { + config.tx_config.carrier_freq_hz = hz; + config.tx_config.carrier_en = true; + } + lua_pop(L, 1); + + lua_getfield(L, 3, "carrier_duty"); + int duty = lua_tointeger(L, -1); + if (duty) { + config.tx_config.carrier_duty_percent = duty; + } + lua_pop(L, 1); + + lua_getfield(L, 3, "idle_level"); + if (!lua_isnil(L, -1)) { + int level = lua_tointeger(L, -1); + config.tx_config.idle_level = level; + config.tx_config.idle_output_en = true; + } + lua_pop(L, 1); + + lua_getfield(L, 3, "invert"); + if (lua_toboolean(L, -1)) { + config.flags |= RMT_CHANNEL_FLAGS_INVERT_SIG; + } + lua_pop(L, 1); + } + + configure_channel(L, &config, RMT_MODE_TX); + lua_pushinteger(L, divisor * 12500); + return 2; +} + +static int lrmt_rxsetup(lua_State *L) { + int gpio = luaL_checkinteger(L, 1); + int divisor = get_divisor(L, 2); + + // We will set the channel later + rmt_config_t config = RMT_DEFAULT_CONFIG_RX(gpio, 0); + config.clk_div = divisor; + config.rx_config.idle_threshold = 65535; + + if (lua_type(L, 3) == LUA_TTABLE) { + lua_getfield(L, 3, "invert"); + if (lua_toboolean(L, -1)) { + config.flags |= RMT_CHANNEL_FLAGS_INVERT_SIG; + } + lua_pop(L, 1); + + lua_getfield(L, 3, "filter_ticks"); + if (!lua_isnil(L, -1)) { + int ticks = lua_tointeger(L, -1); + if (ticks < 0 || ticks > 255) { + return luaL_error(L, "filter_ticks must be in the range 0 - 255"); + } + config.rx_config.filter_ticks_thresh = ticks; + config.rx_config.filter_en = true; + } + lua_pop(L, 1); + + lua_getfield(L, 3, "idle_threshold"); + if (!lua_isnil(L, -1)) { + int threshold = lua_tointeger(L, -1); + if (threshold < 0 || threshold > 65535) { + return luaL_error(L, "idle_threshold must be in the range 0 - 65535"); + } + config.rx_config.idle_threshold = threshold; + } + lua_pop(L, 1); + } + + configure_channel(L, &config, RMT_MODE_RX); + lua_pushinteger(L, divisor * 12500); + return 2; +} + +static void free_transmit_wait_params(lua_State *L, lrmt_cb_params *p) { + if (!p->data) { + luaL_unref(L, LUA_REGISTRYINDEX, p->cb_ref); + luaL_unref(L, LUA_REGISTRYINDEX, p->data_ref); + } + free(p); +} + +static void handle_receive(void *param) { + lrmt_cb_params *p = (lrmt_cb_params *) param; + + RingbufHandle_t rb = NULL; + + //get RMT RX ringbuffer + rmt_get_ringbuf_handle(p->channel, &rb); + // Start receive + rmt_rx_start(p->channel, true); + while (!p->rx_shutting_down) { + size_t length = 0; + rmt_item32_t *items = NULL; + + items = (rmt_item32_t *) xRingbufferReceive(rb, &length, 50 / portTICK_PERIOD_MS); + if (items && length) { + lrmt_cb_params *rx_params = malloc(sizeof(lrmt_cb_params) + length); + if (rx_params) { + memset(rx_params, 0, sizeof(*rx_params)); + memcpy(rx_params + 1, items, length); + rx_params->cb_ref = p->cb_ref; + rx_params->data = (void *) (rx_params + 1); + rx_params->len = length; + rx_params->channel = p->channel; + task_post_high(cb_task_id, (task_param_t) rx_params); + } else { + printf("Unable allocate receive data memory\n"); + } + } + if (items) { + vRingbufferReturnItem(rb, (void *) items); + } + } + + p->dont_call = true; + task_post_high(cb_task_id, (task_param_t) p); + + /* Destroy this task */ + vTaskDelete(NULL); +} + +static int lrmt_on(lua_State *L) { + lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel"); + if (ud->tx) { + return luaL_error(L, "Cannot receive on a TX channel"); + } + + luaL_argcheck(L, !strcmp(lua_tostring(L, 2), "data") , 2, "Must be 'data'"); + + luaL_argcheck(L, lua_type(L, 3) == LUA_TFUNCTION, 3, "Must be a function"); + + if (ud->rx_params) { + return luaL_error(L, "Can only call 'on' once"); + } + + // We have a callback + lrmt_cb_params *params = (lrmt_cb_params *) malloc(sizeof(*params)); + if (!params) { + return luaL_error(L, "Cannot allocate memory"); + } + memset(params, 0, sizeof(*params)); + + lua_pushvalue(L, 3); + params->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + ud->rx_params = params; + params->channel = ud->channel; + + xTaskCreate(handle_receive, "rmt-rx-receiver", 3000, params, 2, NULL); + + return 0; +} + +static void wait_for_transmit(void *param) { + lrmt_cb_params *p = (lrmt_cb_params *) param; + esp_err_t rc = rmt_wait_tx_done(p->channel, 10000 / portTICK_PERIOD_MS); + + p->rc = rc; + task_post_high(cb_task_id, (task_param_t) p); + + /* Destroy this task */ + vTaskDelete(NULL); +} + +static int lrmt_send(lua_State *L) { + lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel"); + if (!ud->tx) { + return luaL_error(L, "Cannot send on an RX channel"); + } + int string_index = 2; + + if (lua_type(L, 2) == LUA_TTABLE) { + lua_getfield(L, 2, "concat"); + lua_pushvalue(L, 2); + lua_pushstring(L, ""); + lua_call(L, 2, 1); + string_index = -1; + } + + size_t len; + const char *data = lua_tolstring(L, string_index, &len); + if (!data || !len) { + return 0; + } + + if (len & 1) { + return luaL_error(L, "Length must be a multiple of 2"); + } + + if (len & 3) { + // Just tack on a "\0\0" -- this is needed as the hardware can + // only deal with multiple of 4 bytes. + luaL_Buffer b; + luaL_buffinit(L, &b); + luaL_addlstring(&b, data, len); + luaL_addlstring(&b, "\0\0", 2); + luaL_pushresult(&b); + data = lua_tolstring(L, -1, &len); + string_index = -1; + } + + bool wait_for_done = true; + + if (lua_type(L, 3) == LUA_TFUNCTION) { + // We have a callback + lrmt_cb_params *params = (lrmt_cb_params *) malloc(sizeof(*params)); + if (!params) { + return luaL_error(L, "Cannot allocate memory"); + } + memset(params, 0, sizeof(*params)); + + params->channel = ud->channel; + + lua_pushvalue(L, 3); + params->cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, string_index); + params->data_ref = luaL_ref(L, LUA_REGISTRYINDEX); + xTaskCreate(wait_for_transmit, "rmt-tx-waiter", 1024, params, 2, NULL); + wait_for_done = false; + } + + // We want to transmit it + rmt_write_items(ud->channel, (rmt_item32_t *) data, len / sizeof(rmt_item32_t), wait_for_done); + return 0; +} + +static int lrmt_close(lua_State *L) { + lrmt_channel_t ud = (lrmt_channel_t)luaL_checkudata(L, 1, "rmt.channel"); + + if (ud->channel >= 0) { + if (ud->rx_params) { + // We need to stop the listening task + ud->rx_params->rx_shutting_down = true; + } else { + rmt_driver_uninstall(ud->channel); + platform_rmt_release(ud->channel); + } + ud->channel = -1; + } + + return 0; +} + +static void cb_task(task_param_t param, task_prio_t prio) { + lrmt_cb_params *p = (lrmt_cb_params *) param; + lua_State *L = lua_getstate(); + + if (!p->dont_call) { + lua_rawgeti (L, LUA_REGISTRYINDEX, p->cb_ref); + if (p->data) { + lua_pushlstring(L, (char *) p->data, p->len); + } else { + lua_pushinteger(L, p->rc); + } + + int res = luaL_pcallx(L, 1, 0); + if (res) { + printf("rmt callback threw an error\n"); + } + } + + if (p->rx_shutting_down) { + rmt_driver_uninstall(p->channel); + platform_rmt_release(p->channel); + } + + free_transmit_wait_params(L, p); +} + +// Module function map +LROT_BEGIN(rmt_channel, NULL, LROT_MASK_GC_INDEX) + LROT_FUNCENTRY( __gc, lrmt_close ) + LROT_TABENTRY ( __index, rmt_channel ) + LROT_FUNCENTRY( on, lrmt_on ) + LROT_FUNCENTRY( close, lrmt_close ) + LROT_FUNCENTRY( send, lrmt_send ) +LROT_END(rmt_channel, NULL, LROT_MASK_GC_INDEX) + +LROT_BEGIN(rmt, NULL, LROT_MASK_INDEX) + LROT_TABENTRY ( __index, rmt ) + LROT_FUNCENTRY( txsetup, lrmt_txsetup ) + LROT_FUNCENTRY( rxsetup, lrmt_rxsetup ) +LROT_END(rmt, NULL, LROT_MASK_INDEX) + +int luaopen_rmt(lua_State *L) { + luaL_rometatable(L, "rmt.channel", LROT_TABLEREF(rmt_channel)); // create metatable + cb_task_id = task_get_id(cb_task); + return 0; +} + +NODEMCU_MODULE(RMT, "rmt", rmt, luaopen_rmt); diff --git a/docs/modules/rmt.md b/docs/modules/rmt.md new file mode 100644 index 00000000..d3ab062d --- /dev/null +++ b/docs/modules/rmt.md @@ -0,0 +1,139 @@ +# Remote Control Driver +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2022-01-01 | [pjsg](https://github.com/pjsg) | [pjsg](https://github.com/pjsg) | [rmt.c](../../components/modules/rmt.c)| + +The RMT module provides a simple interface onto the ESP32 RMT peripheral. This allows the generation of +arbitrary pulse trains with good timing accuracy. This can be used to generate IR remote control signals, or +servo control pulses, or pretty much any high speed signalling system. It isn't good for low speed stuff as the maximum +pulse time is under 200ms -- though you can get longer by having multiple large values in a row. See the Data Encoding +below for more details. + +## rmt.txsetup(gpio, bitrate, options) + +This sets up a transmit channel on the specified gpio pin at the specified rate. Various options described below +can be specified in the `options`. The bit time is specified in picoseconds so that integer values can be used. + +An error will be thrown if the bit time cannot be approximated. + +#### Syntax +`channel = rmt.txsetup(gpio, bittime[, options])` + +#### Parameters +- `gpio` The GPIO pin number to use. +- `bittime` The bit time to use in picoseconds. Only certain times can be handled exactly. The actual set time will be returned. The actual range is limited -- probably using 100,000 (0.1µS) or 1,000,000 (1µS). The actual constraint is that the interval is 1 - 255 cycles of an 80MHz clock. +- `options` A table with the keys as defined below. + +##### Returns +- The `rmt.channel` object that can be used for sending data +- The actual bit time in picoseconds. + +#### Example +```lua +``` + +#### Options table + +This optional table consists of a number of keys that control various aspects of the RMT transmission. + +- `invert` if true, then the output is inverted. +- `carrier_hz` specifies that the signal is to modulate the carrier at the specified frequency. This is useful for IR transmissions. +- `carrier_duty` specifies the duty cycle of the carrier. Defaults to 50% +- `idle_level` specifies what value to send when the transmission completes. + +## rmt.rxsetup(gpio, bitrate, options) + +This sets up a receive channel on the specified gpio pin at the specified rate. Various options described below +can be specified in the `options`. The bit time is specified in picoseconds so that integer values can be used. + +An error will be thrown if the bit time cannot be approximated. + +#### Syntax +`channel = rmt.rxsetup(gpio, bittime[, options])` + +#### Parameters +- `gpio` The GPIO pin number to use. +- `bittime` The bit time to use in picoseconds. Only certain times can be handled exactly. The actual set time will be returned. The actual range is limited -- probably using 100,000 (0.1µS) or 1,000,000 (1µS). The actual constraint is that the interval is 1 - 255 cycles of an 80MHz clock. +- `options` A table with the keys as defined below. + +##### Returns +- The `rmt.channel` object that can be used for receiving data +- The actual bit time in picoseconds. + +#### Example +```lua +``` + +#### Options table + +This optional table consists of a number of keys that control various aspects of the RMT transmission. + +- `invert` if true, then the input is inverted. +- `filter_ticks` If specified, then any pulse shorter than this will be ignored. This is in units of the bit time. +- `idle_threshold` If specified, then any level longer than this will set the receiver as idle. The default is 65535 bit times. + + +## channel:on(event, callback) + +This is establishes a callback to use when data is received and it also starts the data reception process. It can only be called once per receive +channel. + +#### Syntax +`channel:on(event, callback)` + +#### Parameters +- `event` This must be the string 'data' and it sets the callback that gets invoked when data is received. +- `callback` This is invoked with a single argument that is a string that contains the data received in the format described for `send` below. `struct.unpack` is your friend. + +#### Returns +`nil` + +## channel:send(data, cb) + +This is a (default) blocking call that transmits the data using the parameters specified on the `txsetup` call. + +#### Syntax +`channel:send(data[, cb])` + +#### Parameters +- `data` This is either a string or a table of strings. +- `cb` This is an optional callback when the transmission is actually complete. If specified, then the `send` call is non-blocking, and the callback invoked when the transmission is complete. Otherwise the `send` call is synchronous and does not return until transmission is complete. + +#### Data Encoding + +If the `data` supplied is a table (really an array), then the elements of the table are concatenated together and sent. The elements of the table must be strings. + +If the item being sent is a string, then it contains 16 bit packed integers. The top bit of the integer controls the output level. +`struct.pack("H", value)` generates a suitable value to output a zero bit. `struct.pack("H", 32768 + value)` generates a one bit of the specified width. + +The widths are in units of the interval specified when the channel was setup. + + +#### Returns +`nil` + +#### Example + +This example sends a single R character at 19200 bps. You wouldn't actually want to do it this way.... In some applications this would be inverted. + +``` +channel = rmt.txsetup(25, 1000000000 / 19200, {idle_level=1}) +one = struct.pack("H", 32768 + 1000) +zero = struct.pack("H", 1000) +-- Send start bit, then R = 0x52 (reversed) then stop bit +channel:send(zero .. zero .. one .. zero .. zero .. one .. zero .. one .. zero .. one) +-- or using the table interface +channel:send({zero, zero, one, zero, zero, one, zero, one, zero, one}) +``` + +## channel:close() + +This shuts down the RMT channel and makes it available for other uses (e.g. ws2812). The channel cannot be used after this call returns. The channel +is also released when the garbage collector frees it up. However you should always `close` the channel explicitly as otherwise you can run out of RMT channels +before the garbage collector frees some up. + +#### Syntax +`channel:close()` + +#### Returns +`nil`