hw-adc: add ADC peripheral gem for embedded platforms

Add hw-adc gem with analog-to-digital conversion support:
- Common Ruby API with read, read_raw, and read_voltage
- ports/esp32/ using ESP-IDF ADC oneshot driver
- ports/rp2040/ using Pico SDK ADC hardware

Based on picoruby-adc by HASUMI Hitoshi.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-28 11:01:15 +09:00
parent ce4bc77aaf
commit 909f5a070f
7 changed files with 343 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# hw-adc - ADC peripheral interface for mruby
This gem provides the `ADC` class for reading analog input 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 ADC oneshot driver
- `ports/rp2040/` - RP2040 using Pico SDK ADC hardware
## Build Configuration
```ruby
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-adc'
end
```
## Ruby API
### ADC.new
```ruby
adc = ADC.new(pin)
```
- `pin` - ADC-capable GPIO pin number (Integer)
- Raises `ArgumentError` if the pin is not valid for ADC
### ADC#read / ADC#read_voltage
Read the analog value as voltage (Float).
```ruby
voltage = adc.read # => 1.65
voltage = adc.read_voltage # same
```
### ADC#read_raw
Read the raw ADC value (Integer, typically 0-4095 for 12-bit).
```ruby
raw = adc.read_raw # => 2048
```
### ADC#input
Returns the ADC input channel number assigned during initialization.
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/adc.h>`:
```c
int mrb_adc_init(uint8_t pin);
uint32_t mrb_adc_read_raw(uint8_t input);
float mrb_adc_read_voltage(uint8_t input);
```
- `mrb_adc_init` returns the input channel number (>= 0) on
success, negative on error
- `input` parameter for read functions is the value returned by
`mrb_adc_init`
## License
MIT
+19
View File
@@ -0,0 +1,19 @@
#ifndef MRUBY_ADC_H
#define MRUBY_ADC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* HAL functions - implemented in ports/<platform>/adc.c */
int mrb_adc_init(uint8_t pin);
uint32_t mrb_adc_read_raw(uint8_t input);
float mrb_adc_read_voltage(uint8_t input);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_ADC_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-adc') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'ADC peripheral interface'
end
+7
View File
@@ -0,0 +1,7 @@
class ADC
def initialize(pin)
@input = __init(pin)
end
attr_reader :input
end
+147
View File
@@ -0,0 +1,147 @@
#include <stdbool.h>
#include "esp_adc/adc_oneshot.h"
#include <mruby/adc.h>
#define VOLTAGE_MAX 3.3f
#define RESOLUTION 4095
#define UNIT_NUM 2
static adc_oneshot_unit_handle_t adc_handles[UNIT_NUM];
static bool adc_initialized;
static adc_unit_t
pin_to_unit(uint8_t pin)
{
switch (pin) {
#if CONFIG_IDF_TARGET_ESP32C3
case 0: case 1: case 2: case 3: case 4:
return ADC_UNIT_1;
case 5:
return ADC_UNIT_2;
#elif CONFIG_IDF_TARGET_ESP32
case 32: case 33: case 34: case 35: case 36: case 39:
return ADC_UNIT_1;
case 0: case 2: case 4: case 12: case 13: case 14: case 15:
case 25: case 26: case 27:
return ADC_UNIT_2;
#elif (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2)
case 1: case 2: case 3: case 4: case 5:
case 6: case 7: case 8: case 9: case 10:
return ADC_UNIT_1;
case 11: case 12: case 13: case 14: case 15:
case 16: case 17: case 18: case 19: case 20:
return ADC_UNIT_2;
#endif
}
return -1;
}
static adc_channel_t
pin_to_channel(uint8_t pin)
{
switch (pin) {
#if CONFIG_IDF_TARGET_ESP32C3
case 0: return ADC_CHANNEL_0;
case 1: return ADC_CHANNEL_1;
case 2: return ADC_CHANNEL_2;
case 3: return ADC_CHANNEL_3;
case 4: return ADC_CHANNEL_4;
case 5: return ADC_CHANNEL_0;
#elif CONFIG_IDF_TARGET_ESP32
case 36: return ADC_CHANNEL_0;
case 39: return ADC_CHANNEL_3;
case 32: return ADC_CHANNEL_4;
case 33: return ADC_CHANNEL_5;
case 34: return ADC_CHANNEL_6;
case 35: return ADC_CHANNEL_7;
case 4: return ADC_CHANNEL_0;
case 0: return ADC_CHANNEL_1;
case 2: return ADC_CHANNEL_2;
case 15: return ADC_CHANNEL_3;
case 13: return ADC_CHANNEL_4;
case 12: return ADC_CHANNEL_5;
case 14: return ADC_CHANNEL_6;
case 27: return ADC_CHANNEL_7;
case 25: return ADC_CHANNEL_8;
case 26: return ADC_CHANNEL_9;
#elif (CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2)
case 1: return ADC_CHANNEL_0;
case 2: return ADC_CHANNEL_1;
case 3: return ADC_CHANNEL_2;
case 4: return ADC_CHANNEL_3;
case 5: return ADC_CHANNEL_4;
case 6: return ADC_CHANNEL_5;
case 7: return ADC_CHANNEL_6;
case 8: return ADC_CHANNEL_7;
case 9: return ADC_CHANNEL_8;
case 10: return ADC_CHANNEL_9;
case 11: return ADC_CHANNEL_0;
case 12: return ADC_CHANNEL_1;
case 13: return ADC_CHANNEL_2;
case 14: return ADC_CHANNEL_3;
case 15: return ADC_CHANNEL_4;
case 16: return ADC_CHANNEL_5;
case 17: return ADC_CHANNEL_6;
case 18: return ADC_CHANNEL_7;
case 19: return ADC_CHANNEL_8;
case 20: return ADC_CHANNEL_9;
#endif
}
return -1;
}
static int
init_units(void)
{
adc_unit_t units[] = { ADC_UNIT_1, ADC_UNIT_2 };
for (int i = 0; i < UNIT_NUM; i++) {
adc_oneshot_unit_init_cfg_t cfg = {
.unit_id = units[i],
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
if (adc_oneshot_new_unit(&cfg, &adc_handles[i]) != ESP_OK)
return -1;
}
return 0;
}
int
mrb_adc_init(uint8_t pin)
{
if (!adc_initialized) {
if (init_units() != 0) return -1;
adc_initialized = true;
}
int ch = pin_to_channel(pin);
if (ch < 0) return -1;
adc_unit_t unit = pin_to_unit(pin);
if (unit < 0 || unit >= UNIT_NUM) return -1;
adc_oneshot_chan_cfg_t cfg = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
if (adc_oneshot_config_channel(adc_handles[unit], ch, &cfg) != ESP_OK)
return -1;
return (int)pin;
}
uint32_t
mrb_adc_read_raw(uint8_t input)
{
adc_unit_t unit = pin_to_unit(input);
int ch = pin_to_channel(input);
int raw = 0;
if (unit >= 0 && unit < UNIT_NUM)
adc_oneshot_read(adc_handles[unit], ch, &raw);
return (uint32_t)raw;
}
float
mrb_adc_read_voltage(uint8_t input)
{
return (float)mrb_adc_read_raw(input) * VOLTAGE_MAX / RESOLUTION;
}
+43
View File
@@ -0,0 +1,43 @@
#include <stdbool.h>
#include "hardware/adc.h"
#include <mruby/adc.h>
#define VOLTAGE_MAX 3.3f
#define RESOLUTION 4095
#define TEMP_INPUT 4
static bool adc_initialized;
int
mrb_adc_init(uint8_t pin)
{
if (!adc_initialized) {
adc_init();
adc_initialized = true;
}
uint input;
switch (pin) {
case 26: input = 0; break;
case 27: input = 1; break;
case 28: input = 2; break;
case 29: input = 3; break;
default: return -1;
}
adc_gpio_init(pin);
return (int)input;
}
uint32_t
mrb_adc_read_raw(uint8_t input)
{
adc_select_input(input);
return (uint32_t)adc_read();
}
float
mrb_adc_read_voltage(uint8_t input)
{
adc_select_input(input);
return (float)adc_read() * VOLTAGE_MAX / RESOLUTION;
}
+48
View File
@@ -0,0 +1,48 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/adc.h>
/* ADC#__init(pin) */
static mrb_value
mrb_adc_m_init(mrb_state *mrb, mrb_value self)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
int input = mrb_adc_init((uint8_t)pin);
if (input < 0) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "invalid ADC pin");
}
return mrb_fixnum_value(input);
}
/* ADC#read_raw */
static mrb_value
mrb_adc_m_read_raw(mrb_state *mrb, mrb_value self)
{
mrb_int input = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(input)));
return mrb_fixnum_value(mrb_adc_read_raw((uint8_t)input));
}
/* ADC#read_voltage (also aliased as read) */
static mrb_value
mrb_adc_m_read_voltage(mrb_state *mrb, mrb_value self)
{
mrb_int input = mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(input)));
return mrb_float_value(mrb, (mrb_float)mrb_adc_read_voltage((uint8_t)input));
}
void
mrb_hw_adc_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(ADC), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_adc_m_init, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(read_raw), mrb_adc_m_read_raw, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(read_voltage), mrb_adc_m_read_voltage, MRB_ARGS_NONE());
mrb_define_method_id(mrb, cls, MRB_SYM(read), mrb_adc_m_read_voltage, MRB_ARGS_NONE());
}
void
mrb_hw_adc_gem_final(mrb_state *mrb)
{
}