First attempt at adding support for the RMT device. (#3493)

* Adding the first version of the rmt documentation.

* Stub RMT module compiles.

* This version seems to work in (at least) simple cases.

* CLean up the docs

* Minor fixes

* Give the SPI module a chance of working...

* Update to the released version of idf4.4

* Try to get the CI Build to work in all cases

* Try to get the CI Build to work in all cases

* FIx a ringbuffer return issue

* Remove bogus comment

* Review comments

* Better example of transmission

* Review comments

* Add table send example

* Improved documentation

* Documentation comments

* Install the driver correctly.

* A couple of doc updates

* Fix typo
This commit is contained in:
Philip Gladstone 2022-03-05 17:27:11 -05:00 committed by GitHub
parent cb434811ca
commit ceb62993da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 528 additions and 0 deletions

View File

@ -51,6 +51,7 @@ if(IDF_TARGET STREQUAL "esp32")
"eth.c"
"i2s.c"
"pulsecnt.c"
"rmt.c"
"sdmmc.c"
"touch.c"
)

View File

@ -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"

381
components/modules/rmt.c Normal file
View File

@ -0,0 +1,381 @@
// Module for working with the rmt driver
#include <string.h>
#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);

139
docs/modules/rmt.md Normal file
View File

@ -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&micro;S) or 1,000,000 (1&micro;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&micro;S) or 1,000,000 (1&micro;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`