diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 796e5d1f..d7c5e654 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -135,6 +135,12 @@ config LUA_MODULE_SIGMA_DELTA Includes the sigma_delta module. This module provides access to the sigma-delta hardware. +config LUA_MODULE_SPI + bool "SPI module" + default "n" + help + Includes the spi module. + config LUA_MODULE_STRUCT bool "Struct module" default "n" diff --git a/components/modules/spi.c b/components/modules/spi.c new file mode 100644 index 00000000..58f2cc88 --- /dev/null +++ b/components/modules/spi.c @@ -0,0 +1,24 @@ +// Module for interfacing with the SPI interface + +#include "module.h" +#include "lauxlib.h" + +#include "spi_common.h" +#include "driver/spi_common.h" + + +static const LUA_REG_TYPE lspi_map[] = { + { LSTRKEY( "master" ), LFUNCVAL( lspi_master ) }, +// { LSTRKEY( "slave" ), LFUNCVAL( lspi_slave ) }, + { LSTRKEY( "SPI" ), LNUMVAL( SPI_HOST ) }, + { LSTRKEY( "HSPI" ), LNUMVAL( HSPI_HOST ) }, + { LSTRKEY( "VSPI" ), LNUMVAL( VSPI_HOST ) }, + {LNILKEY, LNILVAL} +}; + +int luaopen_spi( lua_State *L ) { + luaopen_spi_master( L ); + return 0; +} + +NODEMCU_MODULE(SPI, "spi", lspi_map, luaopen_spi); diff --git a/components/modules/spi_common.h b/components/modules/spi_common.h new file mode 100644 index 00000000..fae886e5 --- /dev/null +++ b/components/modules/spi_common.h @@ -0,0 +1,14 @@ + +#ifndef _NODEMCU_SPI_COMMON_H_ +#define _NODEMCU_SPI_COMMON_H_ + +#include "lauxlib.h" + +// *************************************************************************** +// SPI master +// +int luaopen_spi_master( lua_State *L ); +int lspi_master( lua_State *L ); + + +#endif /*_NODEMCU_SPI_COMMON_H_*/ diff --git a/components/modules/spi_master.c b/components/modules/spi_master.c new file mode 100644 index 00000000..46ed08cf --- /dev/null +++ b/components/modules/spi_master.c @@ -0,0 +1,337 @@ +// Module for interfacing with the SPI master hardware + +#include +#include "module.h" +#include "lauxlib.h" +#include "lextra.h" + +#include "driver/spi_master.h" +#include "esp_heap_alloc_caps.h" + +#include "esp_log.h" + +#define SPI_MASTER_TAG "spi.master" + +#define UD_HOST_STR "spi.master" +#define UD_DEVICE_STR "spi.device" + + +static int no_err( esp_err_t err ) +{ + switch (err) + { + default: + ESP_LOGI(SPI_MASTER_TAG, "unknown error"); + return 0; + case ESP_ERR_INVALID_ARG: + ESP_LOGI(SPI_MASTER_TAG, "invalid argument"); + return 0; + case ESP_ERR_INVALID_STATE: + ESP_LOGI(SPI_MASTER_TAG, "internal logic error"); + return 0; + case ESP_ERR_NO_MEM: + ESP_LOGI(SPI_MASTER_TAG, "no memory"); + return 0; + case ESP_OK: + return 1; + } +} + +// **************************************************************************** +// Device related functions +// +typedef struct { + spi_device_handle_t device; + int host_ref, host; +} lspi_device_t; + +#define GET_UD_DEVICE \ + lspi_device_t *ud = (lspi_device_t *)luaL_checkudata( L, 1, UD_DEVICE_STR ); + +#define CONFIG_TRANS_FROM_FIELD(field) \ + lua_getfield( L, stack, #field ); \ + trans.field = luaL_optint( L, -1, 0 ); + +static int lspi_device_free( lua_State *L ) +{ + GET_UD_DEVICE; + + if (ud->device) { + spi_bus_remove_device( ud->device ); + ud->device = NULL; + + // unref host to unblock automatic gc from this object + luaL_unref( L, LUA_REGISTRYINDEX, ud->host_ref ); + ud->host_ref = LUA_NOREF; + } + + return 0; +} + +// Lua: +// recv = dev:transfer(trans_desc) +// recv = dev:transfer(data) +static int lspi_device_transfer( lua_State *L ) +{ + GET_UD_DEVICE; + int stack = 1; + + luaL_argcheck( L, ud->device, stack, "no device" ); + + spi_transaction_t trans; + memset( &trans, 0, sizeof( trans ) ); + size_t data_len, rx_len; + const char *data; + + int type = lua_type( L, ++stack ); + luaL_argcheck( L, type == LUA_TSTRING || type == LUA_TTABLE, stack, "string or table expected" ); + + if (type == LUA_TSTRING) { + + data = luaL_checklstring( L, stack, &data_len ); + rx_len = data_len; + + } else { + const char * const options[] = {"std", "dio", "qio"}; + const uint32_t options_flags[] = {0, SPI_TRANS_MODE_DIO, SPI_TRANS_MODE_QIO}; + + CONFIG_TRANS_FROM_FIELD(command); + CONFIG_TRANS_FROM_FIELD(address); + // + lua_getfield( L, stack, "rxlen" ); + rx_len = luaL_optint( L, -1, 0 ); + // + lua_getfield( L, stack, "addr_mode" ); + trans.flags |= luaL_optbool( L, -1, false ) ? SPI_TRANS_MODE_DIOQIO_ADDR : 0; + // + lua_getfield( L, stack, "mode" ); + trans.flags |= options_flags[ luaL_checkoption( L, -1, options[0], options ) ]; + // + lua_getfield( L, stack, "txdata" ); + data = luaL_optlstring( L, -1, "", &data_len ); + + lua_settop( L, stack ); + } + + const char *msg = NULL; + + trans.length = data_len * 8; + if (data_len == 0) { + //no MOSI phase requested + trans.tx_buffer = NULL; + } else if (data_len <=4 ) { + // use local tx data buffer + trans.flags |= SPI_TRANS_USE_TXDATA; + memcpy( trans.tx_data, data, data_len ); + + } else { + // use DMA'able buffer + if ((trans.tx_buffer = pvPortMallocCaps( data_len, MALLOC_CAP_DMA ))) { + memcpy( (void *)trans.tx_buffer, data, data_len ); + } else { + msg = "no memory"; + goto free_mem; + } + } + + trans.rxlength = rx_len * 8; + if (rx_len == 0) { + // no MISO phase requested + trans.rx_buffer = NULL; + + } else if (rx_len <= 4) { + // use local rx data buffer + trans.flags |= SPI_TRANS_USE_RXDATA; + + } else { + // use DMA'able buffer + if (!(trans.rx_buffer = pvPortMallocCaps( rx_len, MALLOC_CAP_DMA ))) { + msg = "no mem"; + goto free_mem; + } + } + + // finally perform the transaction + if (no_err( spi_device_transmit( ud->device, &trans ) )) { + // evaluate receive data + if (trans.flags & SPI_TRANS_USE_RXDATA) { + lua_pushlstring( L, (const char *)&(trans.rx_data[0]), rx_len ); + } else { + lua_pushlstring( L, trans.rx_buffer, rx_len ); + } + + } else + msg = "transfer failed"; + +free_mem: + if (!(trans.flags & SPI_TRANS_USE_TXDATA) && trans.tx_buffer) + free( (void *)trans.tx_buffer ); + if (!(trans.flags & SPI_TRANS_USE_RXDATA) && trans.rx_buffer) + free( (void *)trans.rx_buffer ); + + if (msg) + return luaL_error( L, msg ); + + return 1; +} + + +static const LUA_REG_TYPE lspi_device_map[] = { + { LSTRKEY( "transfer" ), LFUNCVAL( lspi_device_transfer ) }, + { LSTRKEY( "remove" ), LFUNCVAL( lspi_device_free ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( lspi_device_free ) }, + { LSTRKEY( "__index" ), LROVAL( lspi_device_map ) }, + {LNILKEY, LNILVAL} +}; + + +// **************************************************************************** +// Host related functions +// +typedef struct { + int host; +} lspi_host_t; + +#define GET_UD_HOST \ + lspi_host_t *ud = (lspi_host_t *)luaL_checkudata( L, 1, UD_HOST_STR ); +// +#define CONFIG_BUS_PIN_FROM_FIELD(pin) \ + lua_getfield( L, stack, #pin ); \ + config.pin ## _io_num = luaL_optint( L, -1, -1 ); +// +#define CONFIG_DEVICE_FROM_INT_FIELD(field) \ + lua_getfield( L, stack, #field ); \ + config.field = luaL_optint( L, -1, 0 ); +#define CONFIG_DEVICE_FROM_BOOL_FIELD(field, mask) \ + lua_getfield( L, stack, #field ); \ + config.flags |= luaL_optbool( L, -1, false ) ? mask : 0; + +static int lspi_host_free( lua_State *L ) +{ + GET_UD_HOST; + + if (ud->host >= 0) { + spi_bus_free( ud->host ); + ud->host = -1; + } + + return 0; +} + +// Lua: master = spi.master(host, config) +int lspi_master( lua_State *L ) +{ + int stack = 0; + + int host = luaL_checkint( L, ++stack ); + luaL_argcheck( L, + host == SPI_HOST || host == HSPI_HOST || host == VSPI_HOST, + stack, + "invalid host" ); + + luaL_checktype( L, ++stack, LUA_TTABLE ); + + spi_bus_config_t config; + memset( &config, 0, sizeof( config ) ); + // + CONFIG_BUS_PIN_FROM_FIELD(sclk); + CONFIG_BUS_PIN_FROM_FIELD(mosi); + CONFIG_BUS_PIN_FROM_FIELD(miso); + CONFIG_BUS_PIN_FROM_FIELD(quadwp); + CONFIG_BUS_PIN_FROM_FIELD(quadhd); + lua_settop( L, stack ); + // + if (no_err( spi_bus_initialize( host, &config, 1 ) )) { + lspi_host_t *ud = (lspi_host_t *)lua_newuserdata( L, sizeof( lspi_host_t ) ); + luaL_getmetatable( L, UD_HOST_STR ); + lua_setmetatable( L, -2 ); + ud->host = host; + + return 1; + } + + return luaL_error( L, "bus init failed" ); +} + +// Lua: dev = master:device(config) +static int lspi_host_device( lua_State *L ) +{ + GET_UD_HOST; + int stack = 1; + + luaL_argcheck( L, ud->host >= 0, stack, "no active bus host" ); + + luaL_checktype( L, ++stack, LUA_TTABLE ); + + spi_device_interface_config_t config; + memset( &config, 0, sizeof( config ) ); + + // mandatory fields + lua_getfield( L, stack, "cs" ); + config.spics_io_num = luaL_optint( L, -1, -1 ); + // + lua_getfield( L, stack, "mode" ); + int mode = luaL_optint( L, -1, -1 ); + luaL_argcheck( L, mode >= 0, stack, "mode setting missing" ); + config.mode = (uint8_t)mode; + // + lua_getfield( L, stack, "freq" ); + int freq = luaL_optint( L, -1, -1 ); + luaL_argcheck( L, freq >= 0, stack, "freq setting missing" ); + config.clock_speed_hz = freq; + // + // optional fields + CONFIG_DEVICE_FROM_INT_FIELD(command_bits); + CONFIG_DEVICE_FROM_INT_FIELD(address_bits); + CONFIG_DEVICE_FROM_INT_FIELD(dummy_bits); + CONFIG_DEVICE_FROM_INT_FIELD(cs_ena_pretrans); + CONFIG_DEVICE_FROM_INT_FIELD(cs_ena_posttrans); + CONFIG_DEVICE_FROM_INT_FIELD(duty_cycle_pos); + CONFIG_DEVICE_FROM_BOOL_FIELD(tx_lsb_first, SPI_DEVICE_TXBIT_LSBFIRST); + CONFIG_DEVICE_FROM_BOOL_FIELD(rx_lsb_first, SPI_DEVICE_RXBIT_LSBFIRST); + CONFIG_DEVICE_FROM_BOOL_FIELD(wire3, SPI_DEVICE_3WIRE); + CONFIG_DEVICE_FROM_BOOL_FIELD(positive_cs, SPI_DEVICE_POSITIVE_CS); + CONFIG_DEVICE_FROM_BOOL_FIELD(halfduplex, SPI_DEVICE_HALFDUPLEX); + CONFIG_DEVICE_FROM_BOOL_FIELD(clk_as_cs, SPI_DEVICE_CLK_AS_CS); + lua_settop( L, stack ); + // + // fill remaining config entries + config.queue_size = 1; + + lspi_device_t *dev = (lspi_device_t *)lua_newuserdata( L, sizeof( lspi_device_t ) ); + dev->device = NULL; + if (no_err( spi_bus_add_device( ud->host, &config, &(dev->device) ) )) { + luaL_getmetatable( L, UD_DEVICE_STR ); + lua_setmetatable( L, -2 ); + + // reference host object to avoid automatic gc + lua_pushvalue( L, 1 ); + dev->host_ref = luaL_ref( L, LUA_REGISTRYINDEX ); + dev->host = ud->host; + + return 1; + } + + lua_pop( L, 1 ); + + return luaL_error( L, "failed to add device" ); +} + + +static const LUA_REG_TYPE lspi_master_map[] = { + { LSTRKEY( "device" ), LFUNCVAL( lspi_host_device ) }, + { LSTRKEY( "close" ), LFUNCVAL( lspi_host_free ) }, + { LSTRKEY( "__gc" ), LFUNCVAL( lspi_host_free ) }, + { LSTRKEY( "__index" ), LROVAL( lspi_master_map ) }, + {LNILKEY, LNILVAL} +}; + + +// **************************************************************************** +// Generic +// +int luaopen_spi_master( lua_State *L ) { + luaL_rometatable(L, UD_HOST_STR, (void *)lspi_master_map); + luaL_rometatable(L, UD_DEVICE_STR, (void *)lspi_device_map); + return 0; +} diff --git a/docs/en/modules/spi.md b/docs/en/modules/spi.md new file mode 100644 index 00000000..2a586bc3 --- /dev/null +++ b/docs/en/modules/spi.md @@ -0,0 +1,157 @@ +# SPI Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2017-05-04 | [Arnim Läuger](https://github.com/devsaurus) | [Arnim Läuger](https://github.com/devsaurus) | [spi.c](../../../components/modules/spi.c)| + +# SPI Bus +The ESP32 contains 4 SPI bus hosts called `SPI`, `SPI1`, `HSPI`, and `VSPI`. `SPI` is locked to flash communication and is not available for the application. `SPI1` is currently also tied to flash support, but might be available in the future. Applications can currently only use the `HSPI` and `VSPI` hosts. + +The host signals can be mapped to any suitable GPIO pins. + +!!! note + + The API on ESP32 differs from the API on ESP8266. For backwards compatibility please refer to [`lua_compat/spi_compat.lua`](../../../lua_compat/spi_compat.lua`). + + +## spi.master() +Initializes a bus in master mode and returns a bus master object. + +#### Syntax +`spi.master(host, config)` + +#### Parameters +- `host` id, one of + - `spi.SPI1`. not supported yet + - `spi.HSPI` + - `spi.VSPI` +- `config` table listing the assigned GPIOs. All signal assignment are optional. + - `sclk` + - `mosi` + - `miso` + - `quadwp` + - `quadhd` + +#### Returns +SPI bus master object + +#### Example +```lua +busmaster_config = {sclk = 19, mosi = 23, miso = 25} +busmaster = spi.master(spi.HSPI, busmaster_config) +``` + +# Bus Master Object + +## spi.master:close() +Close the bus host. This fails if there are still devices registered on this bus. + +!!! caution + + The bus is also closed when the bus master object is automatically destroyed during garbage collection. Registered devices inherently prevent garbage collection of the bus master object. + +#### Syntax +`busmaster:close()` + +#### Parameters +none + +#### Returns +`nil` + +## spi.master:device() +Adds a device on the given master bus. Up to three devices per bus are supported. + +#### Syntax +`busmaster:device(config)` + +#### Parameters +`config` table describing the device parameters: + +- `cs` GPIO connected to device's chip-select pin, optional +- `mode` SPI mode used for this device (0-3), mandatory +- `freq` clock frequency used for this device [Hz], mandatory +- `command_bits` amount of bits in command phase (0-16), defaults to 0 if omitted +- `address_bits` amount of bits in address phase (0-64), defaults to 0 if omitted +- `dummy_bits` amount of dummy bits to insert address and data phase, defaults to 0 if omitted +- `cs_ena_pretrans`, optional +- `cs_ena_posttrans`, optional +- `duty_cycle_pos`, optional +- `tx_lsb_first` transmit command/address/data LSB first if `true`, MSB first otherwise +- `rx_lsb_first` receive data LSB first if `true`, MSB first otherwise +- `wire3` use spiq for both transmit and receive if `true`, use mosi and miso otherwise +- `positive_cs` chip-select is active high during a transaction if `true`, cs is active low otherwise +- `halfduplex` transmit data before receiving data if `true`, transmit and receive simultaneously otherwise +- `clk_as_cs` output clock on cs line when cs is active if `true` + +#### Returns +SPI device object + +#### Example +```lua +device_config = {mode = 0, freq = 1000000} +device_config.cs = 22 +dev1 = busmaster:device(device_config) + +device_config.cs = 4 +dev2 = busmaster:device(device_config) +``` + +# Device Object + +## spi.master:device:remove() +Removes a device from the related bus master. + +!!! caution + + The device is also removed when the device object is automatically destroyed during garbage collection. + +#### Syntax +`device:remove()` + +#### Parameters +none + +#### Returns +`nil` + +## spi.master:device:transfer() +Initiate an SPI transaction consisting of + +1. assertion of cs signal +2. optional sending of command bits +3. optional sending of address bits +4. optional sending of dummy bits +5. sending of tx data +6. concurrent or appended reception of rx data, optional +7. de-assertion of cs signal + +The function returns after the transaction is completed. + +#### Syntax +```lua +device:transfer(trans) +device:transfer(txdata) +``` + +#### Parameters +`trans` table containing the elements of the transaction: + +- `command` data for command phase, amount of bits was defined during device creation, optional +- `address` data for address phase, amount of bits was defined during device creation, optional +- `txdata` string of data to be sent to the device, optional +- `rxlen` number of bytes to be received, optional +- `mode` optional, one of + - `sio` transmit in SIO mode, default if omitted + - `dio` transmit in DIO mode + - `qio` transmit in QIO mode +- `addr_mode` transmit address also in selected `mode` if `true`, transmit address in SIO otherwise. + +`txdata` string of data to be sent to the device + +#### Returns +String of `rxlen` length, or `#txdata` length if `rxlen` is omitted. + + +## spi.slave() +Initializes a bus in slave mode and returns a slave object. +Not yet supported. diff --git a/lua_compat/spi_compat.lua b/lua_compat/spi_compat.lua new file mode 100644 index 00000000..865c578d --- /dev/null +++ b/lua_compat/spi_compat.lua @@ -0,0 +1,107 @@ +-- **************************************************************************** +-- +-- Compatability wrapper for mapping ESP8266's spi module API to ESP32 +-- +-- Usage: +-- +-- spi = require("spi_compat")([pin_sclk], [pin_mosi], [pin_miso], [pin_cs]) +-- +-- pin_sclk: GPIO pin for SCLK, optional +-- pin_mosi: GPIO pin for MOSI, optional +-- pin_miso: GPIO pin for MISO, optional +-- pin_cs: GPIO pin for CS, optional +-- +-- **************************************************************************** + +local M = {} + +local _pin_sclk, _pin_mosi, _pin_miso, _pin_cs + +local _spi + +local _duplex +local _device + + +-- **************************************************************************** +-- Implement esp8266 compatability API +-- +function M.setup(id, mode, cpol, cpha, databits, clock_div, duplex_mode) + if databits ~= 8 then + error("only 8 bits per item supported") + end + + local bus_master = _spi.master(_spi.HSPI, {sclk = _pin_sclk, mosi = _pin_mosi, miso = _pin_miso}) + + local dev_config = {} + dev_config.cs = _pin_cs + dev_config.mode = cpol * 2 + cpha + dev_config.freq = 80000000 / clock_div + + _device = bus_master:device(dev_config) + + _duplex = duplex_mode or M.HALFDUPLEX + +end + +function M.send(id, ...) + local results = {} + local wrote = 0 + + for idx = 1, select("#", ...) do + local arg = select(idx, ...) + if type(arg) == "number" then + table.insert(results, _device:transfer(string.char(arg)):byte(1)) + wrote = wrote + 1 + elseif type(arg) == "string" then + table.insert(results, _device:transfer(arg)) + wrote = wrote + #arg + elseif type(arg) == "table" then + local rtab = {} + for i, data in ipairs(arg) do + table.insert(rtab, _device:transfer(string.char(data)):byte(1)) + wrote = wrote + 1 + end + table.insert(results, rtab) + else + error("wrong argument type") + end + end + + if _duplex == M.FULLDUPLEX then + return wrote, unpack(results) + else + return wrote + end +end + +function M.recv(id, size, default_data) + local def = default_data or 0xff + return _device:transfer(string.char(def):rep(size)) +end + + +return function (pin_sclk, pin_mosi, pin_miso, pin_cs) + -- cache built-in module + _spi = spi + -- invalidate built-in module + spi = nil + + -- forward unchanged functions + + -- forward constant definitions + M.MASTER = 0 + M.CPOL_LOW = 0 + M.CPOL_HIGH = 1 + M.CPHA_LOW = 0 + M.CPHA_HIGH = 1 + M.HALFDUPLEX = 0 + M.FULLDUPLEX = 1 + + _pin_sclk = pin_sclk + _pin_mosi = pin_mosi + _pin_miso = pin_miso + _pin_cs = pin_cs + + return M +end diff --git a/mkdocs.yml b/mkdocs.yml index f19da2e4..8fad4279 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -44,6 +44,7 @@ pages: - 'ow (1-Wire)': 'en/modules/ow.md' - 'sdmmc': 'en/modules/sdmmc.md' - 'sigma delta': 'en/modules/sigma-delta.md' + - 'spi': 'en/modules/spi.md' - 'struct': 'en/modules/struct.md' - 'tmr': 'en/modules/tmr.md' - 'uart': 'en/modules/uart.md'