hw-pwm: add PWM peripheral gem for embedded platforms

Add hw-pwm gem with pulse width modulation support:
- Common Ruby API with frequency, duty, period_us, pulse_width_us
- ports/esp32/ using LEDC peripheral
- ports/rp2040/ using Pico SDK PWM hardware

Based on picoruby-pwm by HASUMI Hitoshi.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-28 10:59:36 +09:00
parent 5931a96a17
commit ce4bc77aaf
7 changed files with 303 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# hw-pwm - PWM peripheral interface for mruby
This gem provides the `PWM` class for Pulse Width Modulation
output 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 LEDC peripheral
- `ports/rp2040/` - RP2040 using Pico SDK PWM hardware
## Build Configuration
```ruby
MRuby::CrossBuild.new('esp32') do |conf|
conf.ports :esp32
conf.gem core: 'hw-pwm'
end
```
## Ruby API
### PWM.new
```ruby
pwm = PWM.new(pin, frequency: 1000, duty: 50)
```
- `pin` - GPIO pin number (Integer)
- `frequency:` - frequency in Hz (default: 0, disabled)
- `duty:` - duty cycle in percent 0-100 (default: 50)
### PWM#frequency(freq)
Set frequency in Hz. Returns the frequency. Setting 0 disables
output.
```ruby
pwm.frequency(1000) # 1 kHz
pwm.frequency(0) # disable
```
### PWM#period_us(us)
Set period in microseconds. Returns the corresponding frequency.
```ruby
pwm.period_us(1000) # 1ms period = 1 kHz
```
### PWM#duty(pct)
Set duty cycle in percent (0.0-100.0). Clamped to range.
```ruby
pwm.duty(75.0)
```
### PWM#pulse_width_us(us)
Set pulse width in microseconds. Duty cycle is calculated from
current frequency.
```ruby
pwm.pulse_width_us(500) # 500us pulse width
```
## HAL Interface
To add support for a new platform, create a `ports/<name>/`
directory and implement the following C functions declared in
`<mruby/pwm.h>`:
```c
void mrb_pwm_init(uint32_t pin);
void mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty);
void mrb_pwm_set_enabled(uint32_t pin, bool enabled);
```
- `frequency` is in Hz, `duty` is in percent (0-100)
- `mrb_pwm_set_enabled` is called with `false` when frequency is 0
## License
MIT
+20
View File
@@ -0,0 +1,20 @@
#ifndef MRUBY_PWM_H
#define MRUBY_PWM_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* HAL functions - implemented in ports/<platform>/pwm.c */
void mrb_pwm_init(uint32_t pin);
void mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty);
void mrb_pwm_set_enabled(uint32_t pin, bool enabled);
#ifdef __cplusplus
}
#endif
#endif /* MRUBY_PWM_H */
+5
View File
@@ -0,0 +1,5 @@
MRuby::Gem::Specification.new('hw-pwm') do |spec|
spec.license = 'MIT'
spec.authors = ['HASUMI Hitoshi', 'mruby developers']
spec.summary = 'PWM peripheral interface'
end
+9
View File
@@ -0,0 +1,9 @@
class PWM
def initialize(pin, frequency: 0, duty: 50)
@pin = pin
__init(@pin)
@frequency = frequency.to_f
@duty = duty.to_f
frequency(@frequency)
end
end
+59
View File
@@ -0,0 +1,59 @@
#include "driver/ledc.h"
#include <mruby/pwm.h>
#define DUTY_RESOLUTION LEDC_TIMER_14_BIT
static int8_t channel_for_gpio[GPIO_NUM_MAX];
static int next_channel;
void
mrb_pwm_init(uint32_t gpio)
{
if (gpio >= GPIO_NUM_MAX) return;
if (next_channel >= LEDC_CHANNEL_MAX) return;
ledc_timer_config_t timer_cfg = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = DUTY_RESOLUTION,
.freq_hz = 1000,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer_cfg);
ledc_channel_config_t ch_cfg = {
.gpio_num = gpio,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = next_channel,
.timer_sel = LEDC_TIMER_0,
.intr_type = LEDC_INTR_DISABLE,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ch_cfg);
channel_for_gpio[gpio] = next_channel++;
}
void
mrb_pwm_set_freq_duty(uint32_t gpio, float frequency, float duty)
{
if (gpio >= GPIO_NUM_MAX) return;
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, (uint32_t)frequency);
int8_t ch = channel_for_gpio[gpio];
uint32_t max_duty = (1 << DUTY_RESOLUTION) - 1;
uint32_t d = (uint32_t)(duty * max_duty / 100.0f);
ledc_set_duty(LEDC_LOW_SPEED_MODE, ch, d);
ledc_update_duty(LEDC_LOW_SPEED_MODE, ch);
}
void
mrb_pwm_set_enabled(uint32_t gpio, bool enabled)
{
if (!enabled) {
int8_t ch = channel_for_gpio[gpio];
ledc_stop(LEDC_LOW_SPEED_MODE, ch, 0);
}
}
+33
View File
@@ -0,0 +1,33 @@
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include <mruby/pwm.h>
#define APB_CLK_FREQ 125000000
#define CLK_DIV 100.0f
void
mrb_pwm_init(uint32_t pin)
{
gpio_set_function(pin, GPIO_FUNC_PWM);
uint slice = pwm_gpio_to_slice_num(pin);
pwm_set_clkdiv(slice, CLK_DIV);
}
void
mrb_pwm_set_freq_duty(uint32_t pin, float frequency, float duty)
{
uint slice = pwm_gpio_to_slice_num(pin);
uint channel = pwm_gpio_to_channel(pin);
float period = 1.0f / frequency;
uint16_t wrap = (uint16_t)(period * APB_CLK_FREQ / CLK_DIV);
pwm_set_wrap(slice, wrap);
uint16_t level = (uint16_t)(wrap * duty / 100.0f);
pwm_set_chan_level(slice, channel, level);
}
void
mrb_pwm_set_enabled(uint32_t pin, bool enabled)
{
uint slice = pwm_gpio_to_slice_num(pin);
pwm_set_enabled(slice, enabled);
}
+90
View File
@@ -0,0 +1,90 @@
#include <mruby.h>
#include <mruby/presym.h>
#include <mruby/variable.h>
#include <mruby/pwm.h>
static mrb_value
mrb_pwm_m_init(mrb_state *mrb, mrb_value self)
{
mrb_int pin;
mrb_get_args(mrb, "i", &pin);
mrb_pwm_init((uint32_t)pin);
return mrb_nil_value();
}
static void
apply_freq_duty(mrb_state *mrb, mrb_value self)
{
uint32_t pin = (uint32_t)mrb_integer(mrb_iv_get(mrb, self, MRB_IVSYM(pin)));
mrb_float freq = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(frequency)));
mrb_float duty = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(duty)));
mrb_pwm_set_freq_duty(pin, (float)freq, (float)duty);
mrb_pwm_set_enabled(pin, freq > 0);
}
/* PWM#frequency(freq) */
static mrb_value
mrb_pwm_m_frequency(mrb_state *mrb, mrb_value self)
{
mrb_float freq;
mrb_get_args(mrb, "f", &freq);
mrb_iv_set(mrb, self, MRB_IVSYM(frequency), mrb_float_value(mrb, freq));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, freq);
}
/* PWM#period_us(us) */
static mrb_value
mrb_pwm_m_period_us(mrb_state *mrb, mrb_value self)
{
mrb_int us;
mrb_get_args(mrb, "i", &us);
mrb_float freq = 1000000.0 / us;
mrb_iv_set(mrb, self, MRB_IVSYM(frequency), mrb_float_value(mrb, freq));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, freq);
}
/* PWM#duty(pct) */
static mrb_value
mrb_pwm_m_duty(mrb_state *mrb, mrb_value self)
{
mrb_float duty;
mrb_get_args(mrb, "f", &duty);
if (duty < 0.0) duty = 0.0;
if (duty > 100.0) duty = 100.0;
mrb_iv_set(mrb, self, MRB_IVSYM(duty), mrb_float_value(mrb, duty));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, duty);
}
/* PWM#pulse_width_us(us) */
static mrb_value
mrb_pwm_m_pulse_width_us(mrb_state *mrb, mrb_value self)
{
mrb_int pw;
mrb_get_args(mrb, "i", &pw);
mrb_float freq = mrb_as_float(mrb, mrb_iv_get(mrb, self, MRB_IVSYM(frequency)));
mrb_float duty = (mrb_float)pw / 10000.0 * freq;
if (duty < 0.0) duty = 0.0;
if (duty > 100.0) duty = 100.0;
mrb_iv_set(mrb, self, MRB_IVSYM(duty), mrb_float_value(mrb, duty));
apply_freq_duty(mrb, self);
return mrb_float_value(mrb, duty);
}
void
mrb_hw_pwm_gem_init(mrb_state *mrb)
{
struct RClass *cls = mrb_define_class_id(mrb, MRB_SYM(PWM), mrb->object_class);
mrb_define_method_id(mrb, cls, MRB_SYM(__init), mrb_pwm_m_init, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(frequency), mrb_pwm_m_frequency, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(period_us), mrb_pwm_m_period_us, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(duty), mrb_pwm_m_duty, MRB_ARGS_REQ(1));
mrb_define_method_id(mrb, cls, MRB_SYM(pulse_width_us), mrb_pwm_m_pulse_width_us, MRB_ARGS_REQ(1));
}
void
mrb_hw_pwm_gem_final(mrb_state *mrb)
{
}