diff --git a/app/include/user_modules.h b/app/include/user_modules.h index 0b28dbe7..79853254 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -55,6 +55,7 @@ //#define LUA_USE_MODULES_SIGMA_DELTA //#define LUA_USE_MODULES_SJSON //#define LUA_USE_MODULES_SNTP +//#define LUA_USE_MODULES_SOFTUART //#define LUA_USE_MODULES_SOMFY #define LUA_USE_MODULES_SPI //#define LUA_USE_MODULES_SQLITE3 diff --git a/app/modules/softuart.c b/app/modules/softuart.c new file mode 100644 index 00000000..96db98e9 --- /dev/null +++ b/app/modules/softuart.c @@ -0,0 +1,412 @@ +#include "ets_sys.h" +#include "osapi.h" +#include "gpio.h" +#include "os_type.h" +#include "user_interface.h" +#include "module.h" +#include "lauxlib.h" +#include "task/task.h" +#include "platform.h" +#include +#include + +#define SOFTUART_MAX_RX_BUFF 128 +#define SOFTUART_GPIO_COUNT 13 + +typedef struct { + char receive_buffer[SOFTUART_MAX_RX_BUFF]; + uint8_t length; + uint8_t buffer_overflow; +} softuart_buffer_t; + +typedef struct { + uint8_t pin_rx; + uint8_t pin_tx; + volatile softuart_buffer_t buffer; + uint16_t bit_time; + uint16_t need_len; // Buffer length needed to run callback function + char end_char; // Used to run callback if last char in buffer will be the same + uint8_t armed; +} softuart_t; + +typedef struct { + softuart_t *softuart; +} softuart_userdata; + +// Array of pointers to SoftUART instances +softuart_t * softuart_gpio_instances[SOFTUART_GPIO_COUNT] = {NULL}; +// Array of callback reference to be able to find which callback is used to which rx pin +static int softuart_rx_cb_ref[SOFTUART_GPIO_COUNT]; +// Task for receiving data +static task_handle_t uart_recieve_task = NULL; +// Receiving buffer for callback usage +static char softuart_rx_buffer[SOFTUART_MAX_RX_BUFF]; + +static inline int32_t asm_ccount(void) { + int32_t r; + asm volatile ("rsr %0, ccount" : "=r"(r)); + return r; +} + +static inline uint8_t checkbit(uint8_t data, uint8_t bit) +{ + if ((data & bit) != 0) { + return 1; + } else { + return 0; + } +} + +uint32_t ICACHE_RAM_ATTR softuart_intr_handler(uint32_t ret_gpio_status) +{ + // Disable all interrupts + ets_intr_lock(); + int32_t start_time = asm_ccount(); + uint32_t gpio_status = GPIO_REG_READ(GPIO_STATUS_ADDRESS); + uint32_t gpio_bits = gpio_status; + for (uint8_t gpio_bit = 0; gpio_bits != 0; gpio_bit++, gpio_bits >>= 1) { + // Check all pins for interrupts + if (! (gpio_bits & 0x01)) continue; + // We got pin that was interrupted + // Load instance which has rx pin on interrupt pin attached + softuart_t *s = softuart_gpio_instances[pin_num_inv[gpio_bit]]; + if (s == NULL) continue; + if (softuart_rx_cb_ref[pin_num_inv[gpio_bit]] == LUA_NOREF) continue; + if (!s->armed) continue; + // There is SoftUART rx instance on that pin + // Clear interrupt status on that pin + GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, gpio_status & (1 << pin_num[s->pin_rx])); + ret_gpio_status &= ~(1 << pin_num[s->pin_rx]); + // Start listening to transmission + // TODO: inverted + if (! (GPIO_INPUT_GET(GPIO_ID_PIN(pin_num[s->pin_rx])))) { + //pin is low - therefore we have a start bit + unsigned byte = 0; + // Casting and using signed types to always be able to compute elapsed time even if there is a overflow + uint32_t elapsed_time = (uint32_t)(asm_ccount() - start_time); + + // Wait till start bit is half over so we can sample the next one in the center + if (elapsed_time < s->bit_time / 2) { + uint16_t wait_time = s->bit_time / 2 - elapsed_time; + while ((uint32_t)(asm_ccount() - start_time) < wait_time); + start_time += wait_time; + } + + // Sample bits + // TODO: How many bits? Add other configs to softuart + for (uint8_t i = 0; i < 8; i ++ ) { + while ((uint32_t)(asm_ccount() - start_time) < s->bit_time); + //shift d to the right + byte >>= 1; + + // Read bit + if(GPIO_INPUT_GET(GPIO_ID_PIN(pin_num[s->pin_rx]))) { + // If high, set msb of 8bit to 1 + byte |= 0x80; + } + // Recalculate start time for next bit + start_time += s->bit_time; + } + + // Wait for stop bit + // TODO: Add config for stop bits and parity bits + while ((uint32_t)(asm_ccount() - start_time) < s->bit_time); + + // Store byte in buffer + // If buffer full, set the overflow flag and return + uint8 next = s->buffer.length + 1 % SOFTUART_MAX_RX_BUFF; + if (next != 0) { + s->buffer.receive_buffer[s->buffer.length] = byte; // save new byte + s->buffer.length = next; + // Run callback when buffer is filled with enough data or last char is the triggering one + if (((s->need_len != 0) && (s->buffer.length >= s->need_len)) || \ + ((s->need_len == 0) && ((char)byte == s->end_char))) { + s->armed = 0; + task_post_low(uart_recieve_task, (task_param_t)s); // Send the pointer to task handler + } + } else { + //TODO: use this information somehow? + s->buffer.buffer_overflow = 1; + } + } + } + // re-enable all interrupts + ets_intr_unlock(); + return ret_gpio_status; +} + +static void softuart_putchar(softuart_t *s, char data) +{ + // Disable all interrupts + ets_intr_lock(); + int32_t start_time = asm_ccount(); + // Set start bit + GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[s->pin_tx]), 0); + for (uint32_t i = 0; i < 8; i++) { + while ((uint32_t)(asm_ccount() - start_time) < s->bit_time); + + GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[s->pin_tx]), checkbit(data, 1 << i)); + // Recalculate start time for next bit + start_time += s->bit_time; + } + + // Stop bit + while ((uint32_t)(asm_ccount() - start_time) < s->bit_time); + GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[s->pin_tx]), 1); + // Delay after byte, for new sync + os_delay_us(s->bit_time*6 / system_get_cpu_freq()); + // Re-enable all interrupts + ets_intr_unlock(); +} + +static void softuart_init(softuart_t *s) +{ + NODE_DBG("SoftUART initialize gpio\n"); + + if (s->pin_tx != 0xFF){ + // Init tx pin + platform_gpio_mode(s->pin_tx, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP); + platform_gpio_write(s->pin_tx, PLATFORM_GPIO_HIGH); + } + + // Init rx pin + if (s->pin_rx != 0xFF){ + platform_gpio_mode(s->pin_rx, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP); + uint32_t mask = 1 << pin_num[s->pin_rx]; + platform_gpio_register_intr_hook(mask, softuart_intr_handler); + + softuart_gpio_instances[s->pin_rx] = s; + // Enable interrupt for pin on falling edge + platform_gpio_intr_init(s->pin_rx, GPIO_PIN_INTR_NEGEDGE); + } +} + + +static int softuart_setup(lua_State *L) +{ + uint32_t baudrate; + uint8_t tx_gpio_id, rx_gpio_id; + uint8_t stack = 1; + softuart_userdata *suart = NULL; + + NODE_DBG("SoftUART setup called\n"); + + if(lua_isnumber(L, stack)) { + baudrate = (uint32_t)luaL_checkinteger( L, stack); + //230400 Is the max baudrate the author of Arduino-Esp8266-Software-UART tested + if (baudrate <= 0 || baudrate > 230400) { + return luaL_error(L, "Invalid baud rate" ); + } + stack++; + } else { + return luaL_error(L, "Invalid argument type"); + } + + if(lua_isnumber(L, stack)) { + tx_gpio_id = (uint8_t)luaL_checkinteger( L, stack); + if (!platform_gpio_exists(tx_gpio_id) || tx_gpio_id == 0) { + return luaL_error(L, "SoftUART tx GPIO not valid"); + } + stack++; + } else { + tx_gpio_id = 0xFF; + stack++; + } + if (lua_isnumber(L, stack)) { + rx_gpio_id = (uint8_t)luaL_checkinteger( L, stack); + if (!platform_gpio_exists(rx_gpio_id) || rx_gpio_id == 0) { + return luaL_error(L, "SoftUART rx GPIO not valid"); + } + if (softuart_gpio_instances[rx_gpio_id] != NULL) { + return luaL_error( L, "SoftUART rx already configured on the pin."); + } + } else { + rx_gpio_id = 0xFF; + } + + suart = (softuart_userdata*)lua_newuserdata(L, sizeof(softuart_userdata)); + suart->softuart = malloc(sizeof(softuart_t)); + if (!suart->softuart) { + free(suart->softuart); + suart->softuart = NULL; + return luaL_error(L, "Not enough memory"); + } + suart->softuart->pin_rx = rx_gpio_id; + suart->softuart->pin_tx = tx_gpio_id; + suart->softuart->need_len = RX_BUFF_SIZE; + suart->softuart->armed = 0; + //set bit time + suart->softuart->bit_time = system_get_cpu_freq() * 1000000 / baudrate; + + // Set metatable + luaL_getmetatable(L, "softuart.port"); + lua_setmetatable(L, -2); + // Init SoftUART + softuart_init(suart->softuart); + return 1; +} + +static void softuart_rx_callback(task_param_t arg) +{ + softuart_t *softuart = (softuart_t*)arg; //Receive pointer from ISR + lua_State *L = lua_getstate(); + lua_rawgeti(L, LUA_REGISTRYINDEX, softuart_rx_cb_ref[softuart->pin_rx]); + // Copy volatile data to static buffer + for (int i = 0; i < softuart->buffer.length; i++) { + softuart_rx_buffer[i] = softuart->buffer.receive_buffer[i]; + } + lua_pushlstring(L, softuart_rx_buffer, softuart->buffer.length); + softuart->buffer.length = 0; + softuart->armed = 1; + lua_call(L, 1, 0); +} + +// Arguments: event name, minimum buffer filled to run callback, callback function +static int softuart_on(lua_State *L) +{ + NODE_DBG("SoftUART on called\n"); + softuart_userdata *suart = NULL; + size_t name_len, arg_len; + uint8_t stack = 1; + + suart = (softuart_userdata *)luaL_checkudata(L, 1, "softuart.port"); + luaL_argcheck(L, suart, stack, "softuart.port expected"); + if (suart == NULL) { + NODE_DBG("Userdata is nil\n"); + return 0; + } + stack++; + + const char *method = luaL_checklstring(L, stack, &name_len); + if (method == NULL) + return luaL_error(L, "Wrong argument type"); + stack++; + + if (lua_type(L, stack) == LUA_TNUMBER) { + suart->softuart->need_len = (uint16_t)luaL_checkinteger( L, stack ); + stack++; + suart->softuart->end_char = 0; + if (suart->softuart->need_len > SOFTUART_MAX_RX_BUFF) { + suart->softuart->need_len = 0; + return luaL_error(L, "Argument bigger than SoftUART buffer"); + } + suart->softuart->armed = 1; + } else if (lua_isstring(L, stack)) { + const char *end = luaL_checklstring(L , stack, &arg_len); + stack++; + if ( arg_len != 1) { + return luaL_error(L, "Wrong end char length"); + } + suart->softuart->end_char = end[0]; + suart->softuart->need_len = 0; + suart->softuart->armed = 1; + } else { + return luaL_error(L, "Wrong argument type"); + } + + + if (lua_type(L, stack) == LUA_TFUNCTION || lua_type(L, stack) == LUA_TLIGHTFUNCTION) { + lua_pushvalue(L, stack); // Copy to top of the stack + } else { + lua_pushnil(L); + } + + if (name_len == 4 && strcmp(method, "data") == 0) { + + if(suart->softuart->pin_rx == 0xFF) { + return luaL_error(L, "Rx pin was not declared"); + } + + if (softuart_rx_cb_ref[suart->softuart->pin_rx] != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, softuart_rx_cb_ref[suart->softuart->pin_rx]); + softuart_rx_cb_ref[suart->softuart->pin_rx] = LUA_NOREF; + } + if (! lua_isnil(L, -1)) { + softuart_rx_cb_ref[suart->softuart->pin_rx] = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + lua_pop(L, 1); + } + } else { + lua_pop(L, 1); + return luaL_error(L, "Method not supported"); + } + return 0; +} + +static int softuart_write(lua_State *L) +{ + NODE_DBG("SoftUART write called\n"); + softuart_userdata *suart = NULL; + uint8_t stack = 1; + size_t str_len; + suart = (softuart_userdata *)luaL_checkudata(L, 1, "softuart.port"); + luaL_argcheck(L, suart, stack, "softuart.port expected"); + if (suart == NULL) { + NODE_DBG("Userdata is nil\n"); + return 0; + } + stack++; + if(suart->softuart->pin_tx == 0xFF) { + return luaL_error(L, "Tx pin was not declared"); + } + if (lua_type(L, stack) == LUA_TNUMBER) { + // Send byte + uint32_t byte = (uint32_t)luaL_checkinteger( L, stack ); + if (byte > 255) { + return luaL_error(L, "Integer too large for a byte"); + } + softuart_putchar(suart->softuart, (char)byte); + } else if (lua_isstring(L, stack)) { + // Send string + const char *string = luaL_checklstring(L , stack, &str_len); + for (size_t i = 0; i < str_len; i++) { + softuart_putchar(suart->softuart, string[i]); + } + } else { + return luaL_error(L, "Wrong argument type"); + } + return 0; +} + +static int softuart_gcdelete(lua_State *L) +{ + NODE_DBG("SoftUART GC called\n"); + softuart_userdata *suart = NULL; + suart = (softuart_userdata *)luaL_checkudata(L, 1, "softuart.port"); + luaL_argcheck(L, suart, 1, "softuart.port expected"); + if (suart == NULL) { + NODE_DBG("Userdata is nil\n"); + return 0; + } + softuart_gpio_instances[suart->softuart->pin_rx] = NULL; + luaL_unref(L, LUA_REGISTRYINDEX, softuart_rx_cb_ref[suart->softuart->pin_rx]); + softuart_rx_cb_ref[suart->softuart->pin_rx] = LUA_NOREF; + free(suart->softuart); + return 0; +} + +// Port function map +LROT_BEGIN(softuart_port) + LROT_FUNCENTRY( on, softuart_on) + LROT_TABENTRY( __index, softuart_port) + LROT_FUNCENTRY( write, softuart_write) + LROT_FUNCENTRY( __gc, softuart_gcdelete) +LROT_END(ads1115, softuart_port, LROT_MASK_GC_INDEX) + +// Module function map +LROT_BEGIN(softuart) + LROT_FUNCENTRY( setup, softuart_setup) + LROT_TABENTRY(__metatable, softuart_port) +LROT_END(softuart, NULL, 0 ) + +static int luaopen_softuart(lua_State *L) +{ + for(int i = 0; i < SOFTUART_GPIO_COUNT; i++) { + softuart_rx_cb_ref[i] = LUA_NOREF; + } + uart_recieve_task = task_get_id((task_callback_t) softuart_rx_callback); + luaL_rometatable(L, "softuart.port", (void *)softuart_port_map); + return 0; +} + +NODEMCU_MODULE(SOFTUART, "softuart", softuart, luaopen_softuart); diff --git a/docs/modules/softuart.md b/docs/modules/softuart.md new file mode 100644 index 00000000..d976ec35 --- /dev/null +++ b/docs/modules/softuart.md @@ -0,0 +1,78 @@ +# SoftUART Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +|2019-12-27 | [pleningerweb](https://github.com/plieningerweb/), [juancgalvez](https://github.com/juancgalvez/), [crasu](https://github.com/crasu/), [galjonsfigur](https://github.com/galjonsfigur/)| [galjonsfigur](https://github.com/galjonsfigur/) | [softuart.c](../../app/modules/softuart.c) | + +The SoftUART module provides access to multiple software-based UART ports. + +ESP8266 has only 1 full hardware UART port that is used to program the chip and communicate with NodeMCU firmware. The second port is transmit-only. More information can be found in [uart module documentation](uart/). This module provides access to more UART ports and can be used to communicate with devices like GSM or GPS modules. The code is based on [esp8266-software-uart](https://github.com/plieningerweb/esp8266-software-uart) and [Arduino-esp8266-Software-UART](https://github.com/juancgalvez/Arduino-esp8266-Software-UART) projects. Currently doesn't support inverted serial data logic or modes other than 8N1. It's important to notice that this is a software implementation of the serial protocol. There could be some interrupts that make the transmition or reception fail due to invalid timing. + +!!! note +SoftUART cannot be used on D0 pin. + +## softuart.setup() + +Creates new SoftUART instance. Note that rx pin cannot be shared between instances but tx pin can. + +#### Syntax +`softuart.setup(baudrate, txPin, rxPin)` + +#### Parameters +- `baudrate`: SoftUART baudrate. Maximum supported is 230400. +- `txPin`: SoftUART tx pin. If set to `nil` `write` method will not be supported. +- `rxPin`: SoftUART rx pin. If set to `nil` `on("data")` method will not be supported. + +#### Returns +`softuart` instance. + +#### Example +```lua +-- Create new software UART with baudrate of 9600, D2 as Tx pin and D3 as Rx pin +s = softuart.setup(9600, 2, 3) +``` + +# SoftUART port + + +## softuart.port:on() +Sets up the callback function to receive data. + +#### Syntax +`softuart.port:on(event, trigger, function(data))` + +#### Parameters +- `event`: Event name. Currently only `data` is supported. +- `trigger`: Can be a character or a number. If character is set, the callback function will only be run when that character gets received. When a number is set, the callback function will only be run when buffer will have as many characters as number. +- `function(data)`: Callback function. the `data` parameter is software UART receiving buffer. + +#### Returns +`nil` + +#### Example +```lua +-- Create new software UART with baudrate of 9600, D2 as Tx pin and D3 as Rx pin +s = softuart.setup(9600, 2, 3) +s:on("data", 10, function(data) + print("Lua handler called!") + print(data) +end) +``` + +## softuart.port:write() +Transmits a byte or sequence of them. + +#### Syntax +`softuart.port:write(data)` + +#### Parameters +- `data`: Can be a number or string. When a number is passed, only one byte will be sent. When a string is passed, whole sequence will be transmitted. + +#### Returns +`nil` + +#### Example +```lua +-- Create new software UART with baudrate of 9600, D2 as Tx pin and D3 as Rx pin +s = softuart.setup(9600, 2, 3) +s:write("Hello!") +``` diff --git a/mkdocs.yml b/mkdocs.yml index 4d704d17..11be8c88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -99,6 +99,7 @@ pages: - 'sigma delta': 'modules/sigma-delta.md' - 'sjson': 'modules/sjson.md' - 'sntp': 'modules/sntp.md' + - 'softuart': 'modules/softuart.md' - 'somfy': 'modules/somfy.md' - 'spi': 'modules/spi.md' - 'sqlite3': 'modules/sqlite3.md'