diff --git a/mrbgems/hw-esp32-i2c/mrbgem.rake b/mrbgems/hw-esp32-i2c/mrbgem.rake new file mode 100644 index 000000000..8c743bc01 --- /dev/null +++ b/mrbgems/hw-esp32-i2c/mrbgem.rake @@ -0,0 +1,7 @@ +MRuby::Gem::Specification.new('hw-esp32-i2c') do |spec| + spec.license = 'MIT' + spec.author = 'mruby developers' + spec.summary = 'I2C HAL for ESP32' + + spec.add_dependency 'hw-i2c' +end diff --git a/mrbgems/hw-esp32-i2c/src/i2c.c b/mrbgems/hw-esp32-i2c/src/i2c.c new file mode 100644 index 000000000..3384084ff --- /dev/null +++ b/mrbgems/hw-esp32-i2c/src/i2c.c @@ -0,0 +1,122 @@ +#include +#include "driver/i2c_master.h" +#include + +typedef struct { + i2c_master_bus_handle_t bus; + uint32_t freq; + bool initialized; +} i2c_ctx; + +/* ESP32 supports up to 2 I2C ports */ +static i2c_ctx ctx[2]; + +static bool +valid_unit(int unit) +{ + return unit >= 0 && unit <= 1 && ctx[unit].initialized; +} + +static uint32_t +us_to_ms(uint32_t timeout_us) +{ + uint32_t ms = (timeout_us + 999) / 1000; + return (ms < 10) ? 10 : ms; +} + +static i2c_master_dev_handle_t +add_device(int unit, uint8_t addr) +{ + i2c_device_config_t cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = ctx[unit].freq, + }; + i2c_master_dev_handle_t dev; + if (i2c_master_bus_add_device(ctx[unit].bus, &cfg, &dev) != ESP_OK) + return NULL; + return dev; +} + +int +mrb_i2c_unit_name_to_num(const char *name) +{ + if (strcmp(name, "ESP32_I2C0") == 0) return 0; + if (strcmp(name, "ESP32_I2C1") == 0) return 1; + return MRB_I2C_ERROR_UNIT; +} + +mrb_i2c_status +mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl) +{ + if (unit < 0 || unit > 1) return MRB_I2C_ERROR_UNIT; + + if (ctx[unit].initialized) { + i2c_del_master_bus(ctx[unit].bus); + ctx[unit].initialized = false; + } + + i2c_master_bus_config_t cfg = { + .clk_source = I2C_CLK_SRC_DEFAULT, + .i2c_port = unit, + .scl_io_num = scl, + .sda_io_num = sda, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + + esp_err_t err = i2c_new_master_bus(&cfg, &ctx[unit].bus); + if (err != ESP_OK) return MRB_I2C_ERROR_UNIT; + + ctx[unit].initialized = true; + ctx[unit].freq = freq; + return MRB_I2C_OK; +} + +int +mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len, + uint32_t timeout_us) +{ + if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT; + + i2c_master_dev_handle_t dev = add_device(unit, addr); + if (!dev) return -1; + + esp_err_t err = i2c_master_receive(dev, dst, len, us_to_ms(timeout_us)); + i2c_master_bus_rm_device(dev); + return (err == ESP_OK) ? (int)len : -1; +} + +int +mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len, + uint32_t timeout_us) +{ + if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT; + + i2c_master_dev_handle_t dev = add_device(unit, addr); + if (!dev) return -1; + + esp_err_t err = i2c_master_transmit(dev, src, len, us_to_ms(timeout_us)); + i2c_master_bus_rm_device(dev); + return (err == ESP_OK) ? (int)len : -1; +} + +int +mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen, + uint8_t *dst, size_t rlen, uint32_t timeout_us) +{ + if (!valid_unit(unit)) return MRB_I2C_ERROR_UNIT; + + i2c_master_dev_handle_t dev = add_device(unit, addr); + if (!dev) return -1; + + esp_err_t err = i2c_master_transmit_receive(dev, src, wlen, dst, rlen, + us_to_ms(timeout_us)); + i2c_master_bus_rm_device(dev); + return (err == ESP_OK) ? (int)rlen : -1; +} + +#include + +void mrb_hw_esp32_i2c_gem_init(mrb_state *mrb) {} +void mrb_hw_esp32_i2c_gem_final(mrb_state *mrb) {} diff --git a/mrbgems/hw-i2c/README.md b/mrbgems/hw-i2c/README.md new file mode 100644 index 000000000..bdfad87f1 --- /dev/null +++ b/mrbgems/hw-i2c/README.md @@ -0,0 +1,175 @@ +# hw-i2c - I2C peripheral interface for mruby + +This gem provides the `I2C` class for communicating with I2C devices from mruby. It is designed for embedded platforms such as ESP32 and RP2040. + +## Architecture + +The I2C support is split into a common gem and platform-specific HAL gems: + +- **hw-i2c** (this gem) - Ruby API, C bindings, and HAL function declarations +- **hw-esp32-i2c** - HAL implementation for ESP32 (using ESP-IDF I2C master driver) +- **hw-rp2040-i2c** - HAL implementation for RP2040 (using Pico SDK) + +The platform gems depend on hw-i2c, so you only need to specify the platform gem in your build configuration. + +## Build Configuration + +```ruby +# For ESP32 +MRuby::CrossBuild.new('esp32') do |conf| + # ... + conf.gem "#{root}/mrbgems/hw-esp32-i2c" +end + +# For RP2040 +MRuby::CrossBuild.new('rp2040') do |conf| + # ... + conf.gem "#{root}/mrbgems/hw-rp2040-i2c" +end +``` + +## Ruby API + +### I2C.new + +```ruby +i2c = I2C.new( + unit: :ESP32_I2C0, # I2C unit name (platform-specific, required) + frequency: 100_000, # bus frequency in Hz (default: 100kHz) + sda_pin: 21, # SDA GPIO pin number (default: -1 for platform default) + scl_pin: 22, # SCL GPIO pin number (default: -1 for platform default) + timeout: 500 # default timeout in ms (default: 500) +) +``` + +#### Unit Names + +| Platform | Available Units | +|----------|---------------------------------| +| ESP32 | `:ESP32_I2C0`, `:ESP32_I2C1` | +| RP2040 | `:RP2040_I2C0`, `:RP2040_I2C1` | + +On RP2040, if `sda_pin` or `scl_pin` is -1, the Pico SDK default pins are used. + +### I2C#write + +Write data to an I2C device. + +```ruby +i2c.write(addr, *data, timeout: 500) +``` + +- `addr` - 7-bit I2C device address (Integer) +- `data` - one or more data arguments, each can be: + - **Integer** - a single byte (0-255) + - **Array of Integer** - multiple bytes + - **String** - raw bytes +- `timeout:` - optional timeout in ms (overrides instance default) +- Returns the number of bytes written (Integer) +- Raises `IOError` on failure + +```ruby +# Write a single byte +i2c.write(0x3C, 0x00) + +# Write multiple bytes +i2c.write(0x3C, 0x00, [0xAE, 0xD5, 0x80]) + +# Write a string +i2c.write(0x3C, "hello") + +# Mix data types +i2c.write(0x3C, 0x40, [0x01, 0x02], "data") +``` + +### I2C#read + +Read data from an I2C device. Optionally write data before reading (repeated START). + +```ruby +i2c.read(addr, length, *write_data, timeout: 500) +``` + +- `addr` - 7-bit I2C device address (Integer) +- `length` - number of bytes to read (Integer, must be positive) +- `write_data` - optional data to write before reading (same format as `write`). When provided, the gem performs a write-then-read transaction using I2C repeated START condition. This is the standard way to read from a specific register. +- `timeout:` - optional timeout in ms (overrides instance default) +- Returns the data read (String) +- Raises `IOError` on failure, `ArgumentError` if length <= 0 + +```ruby +# Simple read (2 bytes from device) +data = i2c.read(0x50, 2) + +# Register read: write register address 0x00, then read 2 bytes +data = i2c.read(0x50, 2, 0x00) + +# Multi-byte register address +data = i2c.read(0x50, 4, [0x00, 0x10]) +``` + +### I2C#scan + +Scan the I2C bus for responsive devices. + +```ruby +i2c.scan(timeout: 500) +``` + +- `timeout:` - optional timeout per probe in ms +- Returns an Array of 7-bit addresses (Integer) that responded + +```ruby +found = i2c.scan +# => [0x3C, 0x50, 0x68] +``` + +## HAL Interface + +To add support for a new platform, create a gem (e.g., `hw-myboard-i2c`) that depends on `hw-i2c` and implements the following C functions declared in ``: + +```c +/* Convert platform-specific unit name string to unit number. + Return MRB_I2C_ERROR_UNIT for unknown names. */ +int mrb_i2c_unit_name_to_num(const char *name); + +/* Initialize an I2C bus unit. + sda/scl: GPIO pin numbers (-1 for platform default if available). + Return MRB_I2C_OK on success. */ +mrb_i2c_status mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl); + +/* Read len bytes from device at addr. + Return number of bytes read on success, negative on error. */ +int mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len, + uint32_t timeout_us); + +/* Write len bytes to device at addr. + Return number of bytes written on success, negative on error. */ +int mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len, + uint32_t timeout_us); + +/* Atomic write-then-read using repeated START condition. + Write wlen bytes from src, then read rlen bytes into dst. + Return number of bytes read on success, negative on error. */ +int mrb_i2c_write_read(int unit, uint8_t addr, + const uint8_t *src, size_t wlen, + uint8_t *dst, size_t rlen, + uint32_t timeout_us); +``` + +The gem must also provide empty `mrb__gem_init()` and `mrb__gem_final()` functions (with hyphens replaced by underscores). + +### Error Codes + +```c +typedef enum { + MRB_I2C_OK = 0, + MRB_I2C_ERROR_UNIT = -1, /* invalid or uninitialized unit */ + MRB_I2C_ERROR_TIMEOUT = -2, /* communication timeout */ + MRB_I2C_ERROR_NACK = -3, /* device did not acknowledge */ +} mrb_i2c_status; +``` + +## License + +MIT diff --git a/mrbgems/hw-i2c/include/mruby/i2c.h b/mrbgems/hw-i2c/include/mruby/i2c.h new file mode 100644 index 000000000..d49a55b07 --- /dev/null +++ b/mrbgems/hw-i2c/include/mruby/i2c.h @@ -0,0 +1,31 @@ +#ifndef MRUBY_I2C_H +#define MRUBY_I2C_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + MRB_I2C_OK = 0, + MRB_I2C_ERROR_UNIT = -1, + MRB_I2C_ERROR_TIMEOUT = -2, + MRB_I2C_ERROR_NACK = -3, +} mrb_i2c_status; + +/* HAL functions - implemented by hw--i2c gems */ +mrb_i2c_status mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl); +int mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len, uint32_t timeout_us); +int mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len, uint32_t timeout_us); +int mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen, + uint8_t *dst, size_t rlen, uint32_t timeout_us); +int mrb_i2c_unit_name_to_num(const char *name); + +#ifdef __cplusplus +} +#endif + +#endif /* MRUBY_I2C_H */ diff --git a/mrbgems/hw-i2c/mrbgem.rake b/mrbgems/hw-i2c/mrbgem.rake new file mode 100644 index 000000000..584e2b2a2 --- /dev/null +++ b/mrbgems/hw-i2c/mrbgem.rake @@ -0,0 +1,5 @@ +MRuby::Gem::Specification.new('hw-i2c') do |spec| + spec.license = 'MIT' + spec.authors = ['HASUMI Hitoshi', 'mruby developers'] + spec.summary = 'I2C peripheral interface' +end diff --git a/mrbgems/hw-i2c/mrblib/i2c.rb b/mrbgems/hw-i2c/mrblib/i2c.rb new file mode 100644 index 000000000..49e506c21 --- /dev/null +++ b/mrbgems/hw-i2c/mrblib/i2c.rb @@ -0,0 +1,21 @@ +class I2C + DEFAULT_FREQUENCY = 100_000 # Hz + DEFAULT_TIMEOUT = 500 # ms + + def initialize(unit:, frequency: DEFAULT_FREQUENCY, sda_pin: -1, scl_pin: -1, timeout: DEFAULT_TIMEOUT) + @timeout = timeout + @unit_num = __init(unit.to_s, frequency, sda_pin, scl_pin) + end + + def scan(timeout: @timeout) + found = [] + (0x08..0x77).each do |addr| + begin + read(addr, 1, timeout: timeout) + found << addr + rescue IOError + end + end + found + end +end diff --git a/mrbgems/hw-i2c/src/i2c.c b/mrbgems/hw-i2c/src/i2c.c new file mode 100644 index 000000000..46aab6554 --- /dev/null +++ b/mrbgems/hw-i2c/src/i2c.c @@ -0,0 +1,200 @@ +#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 size_t +i2c_fill_buf(mrb_state *mrb, uint8_t *buf, mrb_value *args, mrb_int argc) +{ + 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; + } + } + return pos; +} + +static size_t +i2c_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; +} + +/* Allocate write buffer, fill it, return pointer and size. + Caller must free if need_free is set. */ +static uint8_t* +i2c_build_buf(mrb_state *mrb, mrb_value *args, mrb_int argc, + size_t *out_len, uint8_t *sbuf, mrb_bool *need_free) +{ + size_t total = i2c_calc_size(mrb, args, argc); + uint8_t *buf; + if (total <= STACK_BUF_SIZE) { + buf = sbuf; + *need_free = FALSE; + } + else { + buf = (uint8_t*)mrb_malloc(mrb, total); + *need_free = TRUE; + } + i2c_fill_buf(mrb, buf, args, argc); + *out_len = total; + return buf; +} + +static mrb_int +get_timeout(mrb_state *mrb, mrb_value self, mrb_value kw) +{ + if (mrb_undef_p(kw)) { + return mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(timeout))); + } + return mrb_integer(kw); +} + +static mrb_value +mrb_i2c_m_write(mrb_state *mrb, mrb_value self) +{ + mrb_value *args; + mrb_int argc, addr; + const mrb_sym kw_names[] = { MRB_SYM(timeout) }; + mrb_value kw_values[1]; + mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL }; + + mrb_get_args(mrb, "i*:", &addr, &args, &argc, &kwargs); + + mrb_int timeout_ms = get_timeout(mrb, self, kw_values[0]); + mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num))); + + uint8_t sbuf[STACK_BUF_SIZE]; + size_t wlen; + mrb_bool need_free; + uint8_t *buf = i2c_build_buf(mrb, args, argc, &wlen, sbuf, &need_free); + + int ret = mrb_i2c_write((int)unit, (uint8_t)addr, buf, wlen, + (uint32_t)timeout_ms * 1000); + if (need_free) mrb_free(mrb, buf); + if (ret < 0) { + mrb_raise(mrb, E_IO_ERROR, "I2C write failed"); + } + return mrb_fixnum_value(ret); +} + +static mrb_value +mrb_i2c_m_read(mrb_state *mrb, mrb_value self) +{ + mrb_value *args; + mrb_int argc, addr, len; + const mrb_sym kw_names[] = { MRB_SYM(timeout) }; + mrb_value kw_values[1]; + mrb_kwargs kwargs = { 1, 0, kw_names, kw_values, NULL }; + + mrb_get_args(mrb, "ii*:", &addr, &len, &args, &argc, &kwargs); + + if (len <= 0) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "read length must be positive"); + } + + mrb_int timeout_ms = get_timeout(mrb, self, kw_values[0]); + mrb_int unit = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(unit_num))); + uint32_t timeout_us = (uint32_t)timeout_ms * 1000; + + uint8_t *rxbuf = (uint8_t*)mrb_malloc(mrb, len); + int ret; + + if (argc > 0) { + /* write-then-read (repeated START) */ + uint8_t sbuf[STACK_BUF_SIZE]; + size_t wlen; + mrb_bool need_free; + uint8_t *wbuf = i2c_build_buf(mrb, args, argc, &wlen, sbuf, &need_free); + + ret = mrb_i2c_write_read((int)unit, (uint8_t)addr, + wbuf, wlen, rxbuf, (size_t)len, timeout_us); + if (need_free) mrb_free(mrb, wbuf); + } + else { + ret = mrb_i2c_read((int)unit, (uint8_t)addr, rxbuf, (size_t)len, timeout_us); + } + + if (ret < 0) { + mrb_free(mrb, rxbuf); + mrb_raise(mrb, E_IO_ERROR, "I2C read failed"); + } + mrb_value str = mrb_str_new(mrb, (const char*)rxbuf, ret); + mrb_free(mrb, rxbuf); + return str; +} + +static mrb_value +mrb_i2c_m_init(mrb_state *mrb, mrb_value self) +{ + const char *unit; + mrb_int freq, sda, scl; + + mrb_get_args(mrb, "ziii", &unit, &freq, &sda, &scl); + + int num = mrb_i2c_unit_name_to_num(unit); + if (num < 0) { + mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown I2C unit: %s", unit); + } + mrb_i2c_status st = mrb_i2c_init(num, (uint32_t)freq, (int8_t)sda, (int8_t)scl); + if (st != MRB_I2C_OK) { + mrb_raise(mrb, E_IO_ERROR, "I2C init failed"); + } + return mrb_fixnum_value(num); +} + +void +mrb_hw_i2c_gem_init(mrb_state *mrb) +{ + struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(I2C), mrb->object_class); + mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_i2c_m_init, MRB_ARGS_REQ(4)); + mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_i2c_m_write, MRB_ARGS_REQ(1)|MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0)); + mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_i2c_m_read, MRB_ARGS_REQ(2)|MRB_ARGS_REST()|MRB_ARGS_KEY(1, 0)); +} + +void +mrb_hw_i2c_gem_final(mrb_state *mrb) +{ +} diff --git a/mrbgems/hw-rp2040-i2c/mrbgem.rake b/mrbgems/hw-rp2040-i2c/mrbgem.rake new file mode 100644 index 000000000..d637ec229 --- /dev/null +++ b/mrbgems/hw-rp2040-i2c/mrbgem.rake @@ -0,0 +1,7 @@ +MRuby::Gem::Specification.new('hw-rp2040-i2c') do |spec| + spec.license = 'MIT' + spec.author = 'mruby developers' + spec.summary = 'I2C HAL for RP2040' + + spec.add_dependency 'hw-i2c' +end diff --git a/mrbgems/hw-rp2040-i2c/src/i2c.c b/mrbgems/hw-rp2040-i2c/src/i2c.c new file mode 100644 index 000000000..98de36e95 --- /dev/null +++ b/mrbgems/hw-rp2040-i2c/src/i2c.c @@ -0,0 +1,69 @@ +#include +#include "pico/stdlib.h" +#include "hardware/i2c.h" +#include + +#define UNIT_SELECT(u) \ + i2c_inst_t *inst; \ + switch (u) { \ + case 0: inst = i2c0; break; \ + case 1: inst = i2c1; break; \ + default: return MRB_I2C_ERROR_UNIT; \ + } + +int +mrb_i2c_unit_name_to_num(const char *name) +{ + if (strcmp(name, "RP2040_I2C0") == 0) return 0; + if (strcmp(name, "RP2040_I2C1") == 0) return 1; + return MRB_I2C_ERROR_UNIT; +} + +mrb_i2c_status +mrb_i2c_init(int unit, uint32_t freq, int8_t sda, int8_t scl) +{ + UNIT_SELECT(unit); + i2c_init(inst, freq); + + if (sda < 0) sda = PICO_DEFAULT_I2C_SDA_PIN; + if (scl < 0) scl = PICO_DEFAULT_I2C_SCL_PIN; + gpio_set_function(sda, GPIO_FUNC_I2C); + gpio_set_function(scl, GPIO_FUNC_I2C); + gpio_pull_up(sda); + gpio_pull_up(scl); + + return MRB_I2C_OK; +} + +int +mrb_i2c_read(int unit, uint8_t addr, uint8_t *dst, size_t len, + uint32_t timeout_us) +{ + UNIT_SELECT(unit); + return i2c_read_timeout_us(inst, addr, dst, len, false, timeout_us); +} + +int +mrb_i2c_write(int unit, uint8_t addr, const uint8_t *src, size_t len, + uint32_t timeout_us) +{ + UNIT_SELECT(unit); + return i2c_write_timeout_us(inst, addr, src, len, false, timeout_us); +} + +int +mrb_i2c_write_read(int unit, uint8_t addr, const uint8_t *src, size_t wlen, + uint8_t *dst, size_t rlen, uint32_t timeout_us) +{ + UNIT_SELECT(unit); + /* write with nostop=true (no STOP, keeps bus for repeated START) */ + int ret = i2c_write_timeout_us(inst, addr, src, wlen, true, timeout_us); + if (ret < 0) return ret; + /* read with nostop=false (STOP after read) */ + return i2c_read_timeout_us(inst, addr, dst, rlen, false, timeout_us); +} + +#include + +void mrb_hw_rp2040_i2c_gem_init(mrb_state *mrb) {} +void mrb_hw_rp2040_i2c_gem_final(mrb_state *mrb) {}