From 0ff41e4e28381d10403842d21e084dc2c10cc479 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Fri, 27 Mar 2026 10:46:18 +0900 Subject: [PATCH] hw-gpio: add GPIO peripheral gems for embedded platforms Add three new gems for GPIO pin control: - hw-gpio: common Ruby API, C bindings, and HAL header - hw-esp32-gpio: ESP32 HAL using ESP-IDF GPIO driver - hw-rp2040-gpio: RP2040 HAL using Pico SDK Co-authored-by: Claude --- mrbgems/hw-esp32-gpio/mrbgem.rake | 7 ++ mrbgems/hw-esp32-gpio/src/gpio.c | 55 ++++++++++ mrbgems/hw-gpio/README.md | 148 +++++++++++++++++++++++++++ mrbgems/hw-gpio/include/mruby/gpio.h | 31 ++++++ mrbgems/hw-gpio/mrbgem.rake | 5 + mrbgems/hw-gpio/mrblib/gpio.rb | 44 ++++++++ mrbgems/hw-gpio/src/gpio.c | 119 +++++++++++++++++++++ mrbgems/hw-rp2040-gpio/mrbgem.rake | 7 ++ mrbgems/hw-rp2040-gpio/src/gpio.c | 56 ++++++++++ 9 files changed, 472 insertions(+) create mode 100644 mrbgems/hw-esp32-gpio/mrbgem.rake create mode 100644 mrbgems/hw-esp32-gpio/src/gpio.c create mode 100644 mrbgems/hw-gpio/README.md create mode 100644 mrbgems/hw-gpio/include/mruby/gpio.h create mode 100644 mrbgems/hw-gpio/mrbgem.rake create mode 100644 mrbgems/hw-gpio/mrblib/gpio.rb create mode 100644 mrbgems/hw-gpio/src/gpio.c create mode 100644 mrbgems/hw-rp2040-gpio/mrbgem.rake create mode 100644 mrbgems/hw-rp2040-gpio/src/gpio.c diff --git a/mrbgems/hw-esp32-gpio/mrbgem.rake b/mrbgems/hw-esp32-gpio/mrbgem.rake new file mode 100644 index 000000000..f9b80f055 --- /dev/null +++ b/mrbgems/hw-esp32-gpio/mrbgem.rake @@ -0,0 +1,7 @@ +MRuby::Gem::Specification.new('hw-esp32-gpio') do |spec| + spec.license = 'MIT' + spec.author = 'mruby developers' + spec.summary = 'GPIO HAL for ESP32' + + spec.add_dependency 'hw-gpio' +end diff --git a/mrbgems/hw-esp32-gpio/src/gpio.c b/mrbgems/hw-esp32-gpio/src/gpio.c new file mode 100644 index 000000000..fbebeaaed --- /dev/null +++ b/mrbgems/hw-esp32-gpio/src/gpio.c @@ -0,0 +1,55 @@ +#include "driver/gpio.h" +#include + +void +mrb_gpio_init(uint8_t pin) +{ + gpio_reset_pin(pin); +} + +void +mrb_gpio_set_dir(uint8_t pin, uint8_t flags) +{ + if (flags & MRB_GPIO_IN) { + gpio_set_direction(pin, GPIO_MODE_INPUT); + } + else if (flags & MRB_GPIO_OUT) { + gpio_set_direction(pin, GPIO_MODE_OUTPUT); + } + /* HIGH_Z: not yet implemented */ +} + +void +mrb_gpio_pull_up(uint8_t pin) +{ + gpio_pullup_en(pin); +} + +void +mrb_gpio_pull_down(uint8_t pin) +{ + gpio_pulldown_en(pin); +} + +void +mrb_gpio_open_drain(uint8_t pin) +{ + /* not yet implemented */ +} + +int +mrb_gpio_read(uint8_t pin) +{ + return gpio_get_level(pin); +} + +void +mrb_gpio_write(uint8_t pin, uint8_t val) +{ + gpio_set_level(pin, val); +} + +#include + +void mrb_hw_esp32_gpio_gem_init(mrb_state *mrb) {} +void mrb_hw_esp32_gpio_gem_final(mrb_state *mrb) {} diff --git a/mrbgems/hw-gpio/README.md b/mrbgems/hw-gpio/README.md new file mode 100644 index 000000000..c780a353b --- /dev/null +++ b/mrbgems/hw-gpio/README.md @@ -0,0 +1,148 @@ +# hw-gpio - GPIO peripheral interface for mruby + +This gem provides the `GPIO` class for controlling general-purpose +input/output pins from mruby. +It is designed for embedded platforms such as ESP32 and RP2040. + +## Architecture + +- **hw-gpio** (this gem) - Ruby API, C bindings, and HAL function declarations +- **hw-esp32-gpio** - HAL implementation for ESP32 (using ESP-IDF GPIO driver) +- **hw-rp2040-gpio** - HAL implementation for RP2040 (using Pico SDK) + +The platform gems depend on hw-gpio, 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-gpio" +end + +# For RP2040 +MRuby::CrossBuild.new('rp2040') do |conf| + # ... + conf.gem "#{root}/mrbgems/hw-rp2040-gpio" +end +``` + +## Ruby API + +### Constants (direction/mode flags) + +These flags can be combined with bitwise OR. + +| Flag | Value | Description | +|---------------------|--------|----------------------------| +| `GPIO::IN` | `0x01` | Input mode | +| `GPIO::OUT` | `0x02` | Output mode | +| `GPIO::HIGH_Z` | `0x04` | High-impedance (tri-state) | +| `GPIO::PULL_UP` | `0x08` | Enable pull-up resistor | +| `GPIO::PULL_DOWN` | `0x10` | Enable pull-down resistor | +| `GPIO::OPEN_DRAIN` | `0x20` | Enable open-drain output | + +### GPIO.new + +```ruby +gpio = GPIO.new(pin, flags) +``` + +- `pin` - GPIO pin number (Integer) +- `flags` - direction and mode flags (combined with `|`) +- Exactly one of `IN`, `OUT`, or `HIGH_Z` must be specified +- `PULL_UP` and `PULL_DOWN` are mutually exclusive +- Raises `ArgumentError` on invalid flag combinations + +```ruby +# Input with pull-up +button = GPIO.new(2, GPIO::IN | GPIO::PULL_UP) + +# Output +led = GPIO.new(25, GPIO::OUT) +``` + +### Instance Methods + +#### GPIO#read + +Read the current pin value. + +```ruby +val = gpio.read # => 0 or 1 +``` + +#### GPIO#write + +Set the pin output value. + +```ruby +gpio.write(1) # set high +gpio.write(0) # set low +``` + +- Raises `ArgumentError` if value is not 0 or 1 + +#### GPIO#high? / GPIO#low? + +```ruby +gpio.high? # => true if read != 0 +gpio.low? # => true if read == 0 +``` + +#### GPIO#pin + +```ruby +gpio.pin # => pin number passed to new +``` + +#### GPIO#setmode + +Reconfigure pin direction and mode flags after initialization. + +```ruby +gpio.setmode(GPIO::OUT) +``` + +### Class Methods + +These operate directly on pin numbers without creating an instance. + +```ruby +GPIO.read_at(pin) # => 0 or 1 +GPIO.write_at(pin, val) # set pin output (0 or 1) +GPIO.set_dir_at(pin, flags) # set direction (IN/OUT/HIGH_Z) +GPIO.pull_up_at(pin) # enable pull-up +GPIO.pull_down_at(pin) # enable pull-down +GPIO.open_drain_at(pin) # enable open-drain +``` + +## HAL Interface + +To add support for a new platform, create a gem (e.g., `hw-myboard-gpio`) +that depends on `hw-gpio` and implements the following C functions +declared in ``: + +```c +void mrb_gpio_init(uint8_t pin); +void mrb_gpio_set_dir(uint8_t pin, uint8_t flags); +void mrb_gpio_pull_up(uint8_t pin); +void mrb_gpio_pull_down(uint8_t pin); +void mrb_gpio_open_drain(uint8_t pin); +int mrb_gpio_read(uint8_t pin); +void mrb_gpio_write(uint8_t pin, uint8_t val); +``` + +The `flags` parameter for `mrb_gpio_set_dir()` uses the bitmask +constants defined in the header (`MRB_GPIO_IN`, `MRB_GPIO_OUT`, +`MRB_GPIO_HIGH_Z`). + +The gem must also provide empty `mrb__gem_init()` and +`mrb__gem_final()` functions (with hyphens replaced by +underscores). + +## License + +MIT diff --git a/mrbgems/hw-gpio/include/mruby/gpio.h b/mrbgems/hw-gpio/include/mruby/gpio.h new file mode 100644 index 000000000..ed1e7c10b --- /dev/null +++ b/mrbgems/hw-gpio/include/mruby/gpio.h @@ -0,0 +1,31 @@ +#ifndef MRUBY_GPIO_H +#define MRUBY_GPIO_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* direction/mode flags (bitmask) */ +#define MRB_GPIO_IN 0x01 +#define MRB_GPIO_OUT 0x02 +#define MRB_GPIO_HIGH_Z 0x04 +#define MRB_GPIO_PULL_UP 0x08 +#define MRB_GPIO_PULL_DOWN 0x10 +#define MRB_GPIO_OPEN_DRAIN 0x20 + +/* HAL functions - implemented by hw--gpio gems */ +void mrb_gpio_init(uint8_t pin); +void mrb_gpio_set_dir(uint8_t pin, uint8_t flags); +void mrb_gpio_pull_up(uint8_t pin); +void mrb_gpio_pull_down(uint8_t pin); +void mrb_gpio_open_drain(uint8_t pin); +int mrb_gpio_read(uint8_t pin); +void mrb_gpio_write(uint8_t pin, uint8_t val); + +#ifdef __cplusplus +} +#endif + +#endif /* MRUBY_GPIO_H */ diff --git a/mrbgems/hw-gpio/mrbgem.rake b/mrbgems/hw-gpio/mrbgem.rake new file mode 100644 index 000000000..f117381d7 --- /dev/null +++ b/mrbgems/hw-gpio/mrbgem.rake @@ -0,0 +1,5 @@ +MRuby::Gem::Specification.new('hw-gpio') do |spec| + spec.license = 'MIT' + spec.authors = ['HASUMI Hitoshi', 'mruby developers'] + spec.summary = 'GPIO peripheral interface' +end diff --git a/mrbgems/hw-gpio/mrblib/gpio.rb b/mrbgems/hw-gpio/mrblib/gpio.rb new file mode 100644 index 000000000..a9fd46418 --- /dev/null +++ b/mrbgems/hw-gpio/mrblib/gpio.rb @@ -0,0 +1,44 @@ +class GPIO + IN = 0x01 + OUT = 0x02 + HIGH_Z = 0x04 + PULL_UP = 0x08 + PULL_DOWN = 0x10 + OPEN_DRAIN = 0x20 + + attr_reader :pin + + def initialize(pin, flags) + @pin = pin + __init(pin) + setmode(flags) + end + + def setmode(flags) + dir = flags & (IN | OUT | HIGH_Z) + n = (flags & IN != 0 ? 1 : 0) + (flags & OUT != 0 ? 1 : 0) + (flags & HIGH_Z != 0 ? 1 : 0) + if n == 0 + raise ArgumentError, "specify one of IN, OUT, or HIGH_Z" + elsif n > 1 + raise ArgumentError, "IN, OUT, and HIGH_Z are exclusive" + end + GPIO.set_dir_at(@pin, dir) + + pull = flags & (PULL_UP | PULL_DOWN) + if pull == (PULL_UP | PULL_DOWN) + raise ArgumentError, "PULL_UP and PULL_DOWN are exclusive" + end + GPIO.pull_up_at(@pin) if pull == PULL_UP + GPIO.pull_down_at(@pin) if pull == PULL_DOWN + GPIO.open_drain_at(@pin) if flags & OPEN_DRAIN != 0 + nil + end + + def high? + read != 0 + end + + def low? + read == 0 + end +end diff --git a/mrbgems/hw-gpio/src/gpio.c b/mrbgems/hw-gpio/src/gpio.c new file mode 100644 index 000000000..92661926d --- /dev/null +++ b/mrbgems/hw-gpio/src/gpio.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include + +static mrb_value +mrb_gpio_m_init(mrb_state *mrb, mrb_value self) +{ + mrb_int pin; + mrb_get_args(mrb, "i", &pin); + mrb_gpio_init((uint8_t)pin); + return mrb_nil_value(); +} + +/* GPIO.set_dir_at(pin, flags) */ +static mrb_value +mrb_gpio_s_set_dir_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin, flags; + mrb_get_args(mrb, "ii", &pin, &flags); + mrb_gpio_set_dir((uint8_t)pin, (uint8_t)flags); + return mrb_nil_value(); +} + +/* GPIO.pull_up_at(pin) */ +static mrb_value +mrb_gpio_s_pull_up_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin; + mrb_get_args(mrb, "i", &pin); + mrb_gpio_pull_up((uint8_t)pin); + return mrb_nil_value(); +} + +/* GPIO.pull_down_at(pin) */ +static mrb_value +mrb_gpio_s_pull_down_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin; + mrb_get_args(mrb, "i", &pin); + mrb_gpio_pull_down((uint8_t)pin); + return mrb_nil_value(); +} + +/* GPIO.open_drain_at(pin) */ +static mrb_value +mrb_gpio_s_open_drain_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin; + mrb_get_args(mrb, "i", &pin); + mrb_gpio_open_drain((uint8_t)pin); + return mrb_nil_value(); +} + +/* GPIO.read_at(pin) */ +static mrb_value +mrb_gpio_s_read_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin; + mrb_get_args(mrb, "i", &pin); + return mrb_fixnum_value(mrb_gpio_read((uint8_t)pin)); +} + +/* GPIO.write_at(pin, val) */ +static mrb_value +mrb_gpio_s_write_at(mrb_state *mrb, mrb_value klass) +{ + mrb_int pin, val; + mrb_get_args(mrb, "ii", &pin, &val); + if (val != 0 && val != 1) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "value must be 0 or 1"); + } + mrb_gpio_write((uint8_t)pin, (uint8_t)val); + return mrb_nil_value(); +} + +/* GPIO#read */ +static mrb_value +mrb_gpio_m_read(mrb_state *mrb, mrb_value self) +{ + mrb_int pin = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin))); + return mrb_fixnum_value(mrb_gpio_read((uint8_t)pin)); +} + +/* GPIO#write(val) */ +static mrb_value +mrb_gpio_m_write(mrb_state *mrb, mrb_value self) +{ + mrb_int val; + mrb_get_args(mrb, "i", &val); + if (val != 0 && val != 1) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "value must be 0 or 1"); + } + mrb_int pin = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin))); + mrb_gpio_write((uint8_t)pin, (uint8_t)val); + return mrb_nil_value(); +} + +void +mrb_hw_gpio_gem_init(mrb_state *mrb) +{ + struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(GPIO), mrb->object_class); + + mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_gpio_m_init, MRB_ARGS_REQ(1)); + mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_gpio_m_read, MRB_ARGS_NONE()); + mrb_define_method_id(mrb, cls, MRB_SYM(write), mrb_gpio_m_write, MRB_ARGS_REQ(1)); + + mrb_define_class_method_id(mrb, cls, MRB_SYM(set_dir_at), mrb_gpio_s_set_dir_at, MRB_ARGS_REQ(2)); + mrb_define_class_method_id(mrb, cls, MRB_SYM(pull_up_at), mrb_gpio_s_pull_up_at, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, cls, MRB_SYM(pull_down_at), mrb_gpio_s_pull_down_at, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, cls, MRB_SYM(open_drain_at), mrb_gpio_s_open_drain_at, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, cls, MRB_SYM(read_at), mrb_gpio_s_read_at, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, cls, MRB_SYM(write_at), mrb_gpio_s_write_at, MRB_ARGS_REQ(2)); +} + +void +mrb_hw_gpio_gem_final(mrb_state *mrb) +{ +} diff --git a/mrbgems/hw-rp2040-gpio/mrbgem.rake b/mrbgems/hw-rp2040-gpio/mrbgem.rake new file mode 100644 index 000000000..d27b6b25f --- /dev/null +++ b/mrbgems/hw-rp2040-gpio/mrbgem.rake @@ -0,0 +1,7 @@ +MRuby::Gem::Specification.new('hw-rp2040-gpio') do |spec| + spec.license = 'MIT' + spec.author = 'mruby developers' + spec.summary = 'GPIO HAL for RP2040' + + spec.add_dependency 'hw-gpio' +end diff --git a/mrbgems/hw-rp2040-gpio/src/gpio.c b/mrbgems/hw-rp2040-gpio/src/gpio.c new file mode 100644 index 000000000..a01cc4958 --- /dev/null +++ b/mrbgems/hw-rp2040-gpio/src/gpio.c @@ -0,0 +1,56 @@ +#include +#include "hardware/gpio.h" +#include + +void +mrb_gpio_init(uint8_t pin) +{ + gpio_init(pin); +} + +void +mrb_gpio_set_dir(uint8_t pin, uint8_t flags) +{ + if (flags & MRB_GPIO_IN) { + gpio_set_dir(pin, false); + } + else if (flags & MRB_GPIO_OUT) { + gpio_set_dir(pin, true); + } + /* HIGH_Z: not yet implemented */ +} + +void +mrb_gpio_pull_up(uint8_t pin) +{ + gpio_pull_up(pin); +} + +void +mrb_gpio_pull_down(uint8_t pin) +{ + gpio_pull_down(pin); +} + +void +mrb_gpio_open_drain(uint8_t pin) +{ + /* not yet implemented */ +} + +int +mrb_gpio_read(uint8_t pin) +{ + return gpio_get(pin); +} + +void +mrb_gpio_write(uint8_t pin, uint8_t val) +{ + gpio_put(pin, val == 1); +} + +#include + +void mrb_hw_rp2040_gpio_gem_init(mrb_state *mrb) {} +void mrb_hw_rp2040_gpio_gem_final(mrb_state *mrb) {}