diff --git a/mrbgems/hw-pwm/README.md b/mrbgems/hw-pwm/README.md new file mode 100644 index 000000000..6a4ff01ce --- /dev/null +++ b/mrbgems/hw-pwm/README.md @@ -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//` +directory and implement the following C functions declared in +``: + +```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 diff --git a/mrbgems/hw-pwm/include/mruby/pwm.h b/mrbgems/hw-pwm/include/mruby/pwm.h new file mode 100644 index 000000000..f1f25d800 --- /dev/null +++ b/mrbgems/hw-pwm/include/mruby/pwm.h @@ -0,0 +1,20 @@ +#ifndef MRUBY_PWM_H +#define MRUBY_PWM_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* HAL functions - implemented in ports//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 */ diff --git a/mrbgems/hw-pwm/mrbgem.rake b/mrbgems/hw-pwm/mrbgem.rake new file mode 100644 index 000000000..bc3d0f049 --- /dev/null +++ b/mrbgems/hw-pwm/mrbgem.rake @@ -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 diff --git a/mrbgems/hw-pwm/mrblib/pwm.rb b/mrbgems/hw-pwm/mrblib/pwm.rb new file mode 100644 index 000000000..3e2cf8291 --- /dev/null +++ b/mrbgems/hw-pwm/mrblib/pwm.rb @@ -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 diff --git a/mrbgems/hw-pwm/ports/esp32/pwm.c b/mrbgems/hw-pwm/ports/esp32/pwm.c new file mode 100644 index 000000000..e91f280fc --- /dev/null +++ b/mrbgems/hw-pwm/ports/esp32/pwm.c @@ -0,0 +1,59 @@ +#include "driver/ledc.h" +#include + +#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); + } +} diff --git a/mrbgems/hw-pwm/ports/rp2040/pwm.c b/mrbgems/hw-pwm/ports/rp2040/pwm.c new file mode 100644 index 000000000..5800df3f4 --- /dev/null +++ b/mrbgems/hw-pwm/ports/rp2040/pwm.c @@ -0,0 +1,33 @@ +#include "pico/stdlib.h" +#include "hardware/pwm.h" +#include + +#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); +} diff --git a/mrbgems/hw-pwm/src/pwm.c b/mrbgems/hw-pwm/src/pwm.c new file mode 100644 index 000000000..6cae31555 --- /dev/null +++ b/mrbgems/hw-pwm/src/pwm.c @@ -0,0 +1,90 @@ +#include +#include +#include +#include + +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) +{ +}