mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
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:
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user