diff --git a/mrbgems/hw-spi/README.md b/mrbgems/hw-spi/README.md new file mode 100644 index 000000000..d7ef90a30 --- /dev/null +++ b/mrbgems/hw-spi/README.md @@ -0,0 +1,113 @@ +# hw-spi - SPI peripheral interface for mruby + +This gem provides the `SPI` class for Serial Peripheral Interface +communication from mruby. It is designed for embedded platforms +such as ESP32 and RP2040. + +## Architecture + +Platform-specific HAL implementations are in `ports/` directories: + +- `ports/esp32/` - ESP32 using ESP-IDF SPI master driver +- `ports/rp2040/` - RP2040 using Pico SDK + +The build system automatically compiles matching port sources based +on `conf.ports` setting. + +## Build Configuration + +```ruby +MRuby::CrossBuild.new('esp32') do |conf| + conf.ports :esp32 + conf.gem core: 'hw-spi' +end +``` + +## Ruby API + +### SPI.new + +```ruby +spi = SPI.new( + unit: :RP2040_SPI0, # SPI unit name (required) + frequency: 100_000, # clock frequency in Hz (default: 100kHz) + sck_pin: -1, # SCK GPIO pin (default: platform default) + copi_pin: -1, # COPI/MOSI GPIO pin + cipo_pin: -1, # CIPO/MISO GPIO pin + cs_pin: -1, # CS GPIO pin (-1 for manual control) + mode: 0, # SPI mode 0-3 (default: 0) + first_bit: SPI::MSB_FIRST # bit order (default: MSB_FIRST) +) +``` + +#### Unit Names + +| Platform | Available Units | +|----------|-----------------------------------------------------| +| ESP32 | `:ESP32_SPI2_HOST`, `:ESP32_HSPI_HOST`, | +| | `:ESP32_SPI3_HOST`\*, `:ESP32_VSPI_HOST`\* | +| RP2040 | `:RP2040_SPI0`, `:RP2040_SPI1` | + +\*SPI3 availability depends on ESP32 variant. + +### SPI#write(*data) + +Write data to the SPI bus. Data can be Integer, Array, or String. + +```ruby +spi.write(0x01, [0x02, 0x03]) +``` + +### SPI#read(len, tx_value = 0) + +Read `len` bytes. Optionally specify the value to transmit during +read. + +```ruby +data = spi.read(4) # transmits 0x00 while reading +data = spi.read(4, 0xFF) # transmits 0xFF while reading +``` + +### SPI#transfer(*data, additional_read_bytes: 0) + +Full-duplex transfer. Sends data and returns received bytes. +Use `additional_read_bytes:` to append zero-filled read bytes. + +```ruby +# Send 1 byte command, read 4 bytes response +rx = spi.transfer(0x9F, additional_read_bytes: 4) +``` + +### SPI#select / SPI#deselect + +Manually control the CS pin (when using GPIO-based chip select). + +```ruby +spi.select do |s| + s.write(0x01) + data = s.read(4) +end # CS automatically deasserted +``` + +## HAL Interface + +To add support for a new platform, create a `ports//` +directory and implement the following C functions declared in +``: + +```c +int mrb_spi_unit_name_to_num(const char *name); +mrb_spi_status mrb_spi_init(mrb_spi_info *info); +int mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, + uint8_t tx_val); +int mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len); +int mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, + uint8_t *rx, size_t len); +``` + +The `mrb_spi_info` struct contains all configuration (unit, pins, +frequency, mode, bit order) and is passed to every HAL call. + +## License + +MIT diff --git a/mrbgems/hw-spi/include/mruby/spi.h b/mrbgems/hw-spi/include/mruby/spi.h new file mode 100644 index 000000000..9be4fc55b --- /dev/null +++ b/mrbgems/hw-spi/include/mruby/spi.h @@ -0,0 +1,44 @@ +#ifndef MRUBY_SPI_H +#define MRUBY_SPI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MRB_SPI_MSB_FIRST 1 +#define MRB_SPI_LSB_FIRST 0 + +typedef enum { + MRB_SPI_OK = 0, + MRB_SPI_ERROR_UNIT = -1, + MRB_SPI_ERROR_MODE = -2, + MRB_SPI_ERROR_FIRST_BIT = -3, + MRB_SPI_ERROR_INIT = -4, +} mrb_spi_status; + +typedef struct { + uint32_t frequency; + uint8_t unit_num; + int8_t sck_pin; + int8_t copi_pin; + int8_t cipo_pin; + int8_t cs_pin; + uint8_t mode; + uint8_t first_bit; +} mrb_spi_info; + +/* HAL functions - implemented in ports//spi.c */ +int mrb_spi_unit_name_to_num(const char *name); +mrb_spi_status mrb_spi_init(mrb_spi_info *info); +int mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val); +int mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len); +int mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* MRUBY_SPI_H */ diff --git a/mrbgems/hw-spi/mrbgem.rake b/mrbgems/hw-spi/mrbgem.rake new file mode 100644 index 000000000..f2f1149ad --- /dev/null +++ b/mrbgems/hw-spi/mrbgem.rake @@ -0,0 +1,5 @@ +MRuby::Gem::Specification.new('hw-spi') do |spec| + spec.license = 'MIT' + spec.authors = ['HASUMI Hitoshi', 'mruby developers'] + spec.summary = 'SPI peripheral interface' +end diff --git a/mrbgems/hw-spi/mrblib/spi.rb b/mrbgems/hw-spi/mrblib/spi.rb new file mode 100644 index 000000000..d0d6db519 --- /dev/null +++ b/mrbgems/hw-spi/mrblib/spi.rb @@ -0,0 +1,20 @@ +class SPI + MSB_FIRST = 1 + LSB_FIRST = 0 + DEFAULT_FREQUENCY = 100_000 + + def select + @cs&.write 0 + if block_given? + begin + yield self + ensure + deselect + end + end + end + + def deselect + @cs&.write 1 + end +end diff --git a/mrbgems/hw-spi/ports/esp32/spi.c b/mrbgems/hw-spi/ports/esp32/spi.c new file mode 100644 index 000000000..9557d6584 --- /dev/null +++ b/mrbgems/hw-spi/ports/esp32/spi.c @@ -0,0 +1,85 @@ +#include +#include "driver/spi_master.h" +#include + +static spi_device_handle_t handles[SPI_HOST_MAX]; + +int +mrb_spi_unit_name_to_num(const char *name) +{ + if (strcmp(name, "ESP32_SPI2_HOST") == 0) return SPI2_HOST; + if (strcmp(name, "ESP32_HSPI_HOST") == 0) return SPI2_HOST; +#if (SOC_SPI_PERIPH_NUM == 3) + if (strcmp(name, "ESP32_SPI3_HOST") == 0) return SPI3_HOST; + if (strcmp(name, "ESP32_VSPI_HOST") == 0) return SPI3_HOST; +#endif + return MRB_SPI_ERROR_UNIT; +} + +mrb_spi_status +mrb_spi_init(mrb_spi_info *info) +{ + if (handles[info->unit_num] != NULL) return MRB_SPI_OK; + + spi_bus_config_t buscfg = { + .mosi_io_num = info->copi_pin, + .miso_io_num = info->cipo_pin, + .sclk_io_num = info->sck_pin, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + }; + + esp_err_t err = spi_bus_initialize(info->unit_num, &buscfg, SPI_DMA_CH_AUTO); + if (err != ESP_OK) return MRB_SPI_ERROR_INIT; + + spi_device_interface_config_t devcfg = { + .clock_speed_hz = info->frequency, + .mode = info->mode, + .spics_io_num = info->cs_pin, + .queue_size = 7, + }; + + err = spi_bus_add_device(info->unit_num, &devcfg, &handles[info->unit_num]); + if (err != ESP_OK) { + spi_bus_free(info->unit_num); + handles[info->unit_num] = NULL; + return MRB_SPI_ERROR_INIT; + } + return MRB_SPI_OK; +} + +int +mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val) +{ + spi_transaction_t t = { + .length = len * 8, + .tx_buffer = NULL, + .rx_buffer = dst, + }; + esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t); + return (err == ESP_OK) ? (int)len : -1; +} + +int +mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len) +{ + spi_transaction_t t = { + .length = len * 8, + .tx_buffer = src, + .rx_buffer = NULL, + }; + esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t); + return (err == ESP_OK) ? (int)len : -1; +} + +int +mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len) +{ + spi_transaction_t t = { + .length = len * 8, + .tx_buffer = tx, + .rx_buffer = rx, + }; + esp_err_t err = spi_device_polling_transmit(handles[info->unit_num], &t); + return (err == ESP_OK) ? (int)len : -1; +} diff --git a/mrbgems/hw-spi/ports/rp2040/spi.c b/mrbgems/hw-spi/ports/rp2040/spi.c new file mode 100644 index 000000000..9210e1c2a --- /dev/null +++ b/mrbgems/hw-spi/ports/rp2040/spi.c @@ -0,0 +1,73 @@ +#include +#include "pico/stdlib.h" +#include "hardware/spi.h" +#include + +#define UNIT_SELECT(info) \ + spi_inst_t *inst; \ + switch ((info)->unit_num) { \ + case 0: inst = spi0; break; \ + case 1: inst = spi1; break; \ + default: return MRB_SPI_ERROR_UNIT; \ + } + +int +mrb_spi_unit_name_to_num(const char *name) +{ + if (strcmp(name, "RP2040_SPI0") == 0) return 0; + if (strcmp(name, "RP2040_SPI1") == 0) return 1; + return MRB_SPI_ERROR_UNIT; +} + +mrb_spi_status +mrb_spi_init(mrb_spi_info *info) +{ + UNIT_SELECT(info); + spi_init(inst, info->frequency); + + if (info->sck_pin < 0) info->sck_pin = PICO_DEFAULT_SPI_SCK_PIN; + if (info->cipo_pin < 0) info->cipo_pin = PICO_DEFAULT_SPI_RX_PIN; + if (info->copi_pin < 0) info->copi_pin = PICO_DEFAULT_SPI_TX_PIN; + + gpio_set_function(info->sck_pin, GPIO_FUNC_SPI); + gpio_set_function(info->cipo_pin, GPIO_FUNC_SPI); + gpio_set_function(info->copi_pin, GPIO_FUNC_SPI); + + if (info->first_bit != MRB_SPI_MSB_FIRST) { + return MRB_SPI_ERROR_FIRST_BIT; + } + + spi_cpol_t cpol; + spi_cpha_t cpha; + switch (info->mode) { + case 0: cpol = 0; cpha = 0; break; + case 1: cpol = 0; cpha = 1; break; + case 2: cpol = 1; cpha = 0; break; + case 3: cpol = 1; cpha = 1; break; + default: return MRB_SPI_ERROR_MODE; + } + spi_set_format(inst, 8, cpol, cpha, info->first_bit); + + return MRB_SPI_OK; +} + +int +mrb_spi_read(mrb_spi_info *info, uint8_t *dst, size_t len, uint8_t tx_val) +{ + UNIT_SELECT(info); + return spi_read_blocking(inst, tx_val, dst, len); +} + +int +mrb_spi_write(mrb_spi_info *info, const uint8_t *src, size_t len) +{ + UNIT_SELECT(info); + return spi_write_blocking(inst, src, len); +} + +int +mrb_spi_transfer(mrb_spi_info *info, const uint8_t *tx, uint8_t *rx, size_t len) +{ + UNIT_SELECT(info); + return spi_write_read_blocking(inst, tx, rx, len); +} diff --git a/mrbgems/hw-spi/src/spi.c b/mrbgems/hw-spi/src/spi.c new file mode 100644 index 000000000..2b75930c8 --- /dev/null +++ b/mrbgems/hw-spi/src/spi.c @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STACK_BUF_SIZE 256 +#define E_IO_ERROR mrb_exc_get_id(mrb, MRB_SYM(IOError)) + +static void +spi_info_free(mrb_state *mrb, void *ptr) +{ + mrb_free(mrb, ptr); +} + +static const struct mrb_data_type spi_info_type = { "SPI", spi_info_free }; + +#define SPI_INFO(self) \ + ((mrb_spi_info*)mrb_data_get_ptr(mrb, self, &spi_info_type)) + +static size_t +spi_calc_size(mrb_state *mrb, mrb_value *args, mrb_int argc) +{ + size_t total = 0; + for (mrb_int i = 0; i < argc; i++) { + switch (mrb_type(args[i])) { + case MRB_TT_ARRAY: + total += RARRAY_LEN(args[i]); + break; + case MRB_TT_INTEGER: + total += 1; + break; + case MRB_TT_STRING: + total += RSTRING_LEN(args[i]); + break; + default: + mrb_raise(mrb, E_TYPE_ERROR, "Integer, Array, or String expected"); + } + } + return total; +} + +static void +spi_fill_buf(mrb_state *mrb, uint8_t *buf, mrb_value *args, mrb_int argc, + size_t pad) +{ + size_t pos = 0; + for (mrb_int i = 0; i < argc; i++) { + switch (mrb_type(args[i])) { + case MRB_TT_ARRAY: { + mrb_int alen = RARRAY_LEN(args[i]); + const mrb_value *aptr = RARRAY_PTR(args[i]); + for (mrb_int j = 0; j < alen; j++) { + if (!mrb_integer_p(aptr[j])) { + mrb_raise(mrb, E_TYPE_ERROR, "array element must be Integer"); + } + buf[pos++] = (uint8_t)mrb_integer(aptr[j]); + } + break; + } + case MRB_TT_INTEGER: + buf[pos++] = (uint8_t)mrb_integer(args[i]); + break; + case MRB_TT_STRING: + memcpy(&buf[pos], RSTRING_PTR(args[i]), RSTRING_LEN(args[i])); + pos += RSTRING_LEN(args[i]); + break; + default: + break; + } + } + memset(&buf[pos], 0, pad); +} + +/* SPI.new(unit:, frequency:, sck_pin:, cipo_pin:, copi_pin:, + cs_pin:, mode:, first_bit:) */ +static mrb_value +mrb_spi_s_new(mrb_state *mrb, mrb_value klass) +{ + const char *unit_name; + mrb_int freq = 100000, sck = -1, cipo = -1, copi = -1, cs = -1; + mrb_int mode = 0, first_bit = MRB_SPI_MSB_FIRST; + + const mrb_sym kw_names[] = { + MRB_SYM(unit), MRB_SYM(frequency), MRB_SYM(sck_pin), + MRB_SYM(cipo_pin), MRB_SYM(copi_pin), MRB_SYM(cs_pin), + MRB_SYM(mode), MRB_SYM(first_bit) + }; + mrb_value kw_values[8]; + mrb_kwargs kwargs = { 8, 7, kw_names, kw_values, NULL }; + mrb_get_args(mrb, ":", &kwargs); + + /* unit is required */ + if (mrb_undef_p(kw_values[0])) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "unit: is required"); + } + unit_name = mrb_str_to_cstr(mrb, mrb_sym_str(mrb, mrb_symbol(kw_values[0]))); + + if (!mrb_undef_p(kw_values[1])) freq = mrb_integer(kw_values[1]); + if (!mrb_undef_p(kw_values[2])) sck = mrb_integer(kw_values[2]); + if (!mrb_undef_p(kw_values[3])) cipo = mrb_integer(kw_values[3]); + if (!mrb_undef_p(kw_values[4])) copi = mrb_integer(kw_values[4]); + if (!mrb_undef_p(kw_values[5])) cs = mrb_integer(kw_values[5]); + if (!mrb_undef_p(kw_values[6])) mode = mrb_integer(kw_values[6]); + if (!mrb_undef_p(kw_values[7])) first_bit = mrb_integer(kw_values[7]); + + int num = mrb_spi_unit_name_to_num(unit_name); + if (num < 0) { + mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown SPI unit: %s", unit_name); + } + + mrb_spi_info *info = (mrb_spi_info*)mrb_malloc(mrb, sizeof(mrb_spi_info)); + info->unit_num = (uint8_t)num; + info->frequency = (uint32_t)freq; + info->sck_pin = (int8_t)sck; + info->cipo_pin = (int8_t)cipo; + info->copi_pin = (int8_t)copi; + info->cs_pin = (int8_t)cs; + info->mode = (uint8_t)mode; + info->first_bit = (uint8_t)first_bit; + + mrb_value self = mrb_obj_value( + Data_Wrap_Struct(mrb, mrb_class_ptr(klass), &spi_info_type, info)); + + mrb_spi_status st = mrb_spi_init(info); + if (st != MRB_SPI_OK) { + mrb_raise(mrb, E_IO_ERROR, "SPI init failed"); + } + return self; +} + +/* SPI#write(*data) */ +static mrb_value +mrb_spi_m_write(mrb_state *mrb, mrb_value self) +{ + mrb_value *args; + mrb_int argc; + mrb_get_args(mrb, "*", &args, &argc); + + mrb_spi_info *info = SPI_INFO(self); + size_t total = spi_calc_size(mrb, args, argc); + if (total == 0) return mrb_fixnum_value(0); + + uint8_t sbuf[STACK_BUF_SIZE]; + uint8_t *buf = sbuf; + mrb_bool need_free = FALSE; + if (total > STACK_BUF_SIZE) { + buf = (uint8_t*)mrb_malloc(mrb, total); + need_free = TRUE; + } + spi_fill_buf(mrb, buf, args, argc, 0); + + int ret = mrb_spi_write(info, buf, total); + if (need_free) mrb_free(mrb, buf); + if (ret < 0) mrb_raise(mrb, E_IO_ERROR, "SPI write failed"); + return mrb_fixnum_value(ret); +} + +/* SPI#read(len, tx_value=0) */ +static mrb_value +mrb_spi_m_read(mrb_state *mrb, mrb_value self) +{ + mrb_int len, tx_val = 0; + mrb_get_args(mrb, "i|i", &len, &tx_val); + if (len <= 0) mrb_raise(mrb, E_ARGUMENT_ERROR, "length must be positive"); + + mrb_spi_info *info = SPI_INFO(self); + uint8_t *buf = (uint8_t*)mrb_malloc(mrb, len); + int ret = mrb_spi_read(info, buf, (size_t)len, (uint8_t)tx_val); + if (ret < 0) { + mrb_free(mrb, buf); + mrb_raise(mrb, E_IO_ERROR, "SPI read failed"); + } + mrb_value str = mrb_str_new(mrb, (const char*)buf, ret); + mrb_free(mrb, buf); + return str; +} + +/* SPI#transfer(*data, additional_read_bytes: 0) */ +static mrb_value +mrb_spi_m_transfer(mrb_state *mrb, mrb_value self) +{ + mrb_value *args; + mrb_int argc, extra = 0; + const mrb_sym kw_names[] = { MRB_SYM(additional_read_bytes) }; + mrb_value kw_values[1]; + mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL }; + mrb_get_args(mrb, "*:", &args, &argc, &kwargs); + + if (!mrb_undef_p(kw_values[0])) extra = mrb_integer(kw_values[0]); + + mrb_spi_info *info = SPI_INFO(self); + size_t total = spi_calc_size(mrb, args, argc) + (size_t)extra; + if (total == 0) return mrb_str_new(mrb, "", 0); + + uint8_t sbuf_tx[STACK_BUF_SIZE], sbuf_rx[STACK_BUF_SIZE]; + uint8_t *tx = sbuf_tx, *rx = sbuf_rx; + mrb_bool need_free = FALSE; + if (total > STACK_BUF_SIZE) { + tx = (uint8_t*)mrb_malloc(mrb, total * 2); + rx = tx + total; + need_free = TRUE; + } + spi_fill_buf(mrb, tx, args, argc, (size_t)extra); + + int ret = mrb_spi_transfer(info, tx, rx, total); + if (ret < 0) { + if (need_free) mrb_free(mrb, tx); + mrb_raise(mrb, E_IO_ERROR, "SPI transfer failed"); + } + mrb_value str = mrb_str_new(mrb, (const char*)rx, ret); + if (need_free) mrb_free(mrb, tx); + return str; +} + +void +mrb_hw_spi_gem_init(mrb_state *mrb) +{ + struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(SPI), mrb->object_class); + MRB_SET_INSTANCE_TT(cls, MRB_TT_CDATA); + + mrb_define_class_method_id(mrb, cls, MRB_SYM(new), mrb_spi_s_new, MRB_ARGS_KEY(8, 1)); + mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_spi_m_write, MRB_ARGS_REST()); + mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_spi_m_read, MRB_ARGS_ARG(1, 1)); + mrb_define_method_id(mrb, cls, MRB_SYM(transfer), mrb_spi_m_transfer, MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0)); +} + +void +mrb_hw_spi_gem_final(mrb_state *mrb) +{ +}