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 <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-27 10:46:18 +09:00
parent 1ed52461e8
commit 0ff41e4e28
9 changed files with 472 additions and 0 deletions
+7
View File
@@ -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
+55
View File
@@ -0,0 +1,55 @@
#include "driver/gpio.h"
#include <mruby/gpio.h>
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 <mruby.h>
void mrb_hw_esp32_gpio_gem_init(mrb_state *mrb) {}
void mrb_hw_esp32_gpio_gem_final(mrb_state *mrb) {}
+148
View File
@@ -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 `<mruby/gpio.h>`:
```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_<gemname>_gem_init()` and
`mrb_<gemname>_gem_final()` functions (with hyphens replaced by
underscores).
## License
MIT
+31
View File
@@ -0,0 +1,31 @@
#ifndef MRUBY_GPIO_H
#define MRUBY_GPIO_H
#include <stdint.h>
#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-<platform>-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 */
+5
View File
@@ -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
+44
View File
@@ -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
+119
View File
@@ -0,0 +1,119 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/gpio.h>
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)
{
}
+7
View File
@@ -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
+56
View File
@@ -0,0 +1,56 @@
#include <stdbool.h>
#include "hardware/gpio.h"
#include <mruby/gpio.h>
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 <mruby.h>
void mrb_hw_rp2040_gpio_gem_init(mrb_state *mrb) {}
void mrb_hw_rp2040_gpio_gem_final(mrb_state *mrb) {}