ESP8266のArduinoでmruby/cのPWM


これまでに、mrubyc_arduino用にいくつかの機能を組み込むためのプログラムを書いて来た。 しかし、PWMについては、不十分な点があった。 その時には、Arduinoの機能のみを使うことにしたので、APIのガイドラインのすべての機能を実装できていないかったのである。 dutyとfrequencyは、それぞれAnalogWriteとToneを使って変化させることが出来るのだが、これらを同時に変えることが出来無いので、それを実現するのは諦めていた。

マイコン毎に別々にプログラムを書くのは非効率なので、mrubyc_arduinoを用いることによって、共通のプログラムで様々なマイコンが使えるようにしてきた。 しかしESP8266では、WiFiを扱うために、専用のプログラムを作ったが、そこではArduinoから使える関数を使った。 ESP8266専用のプログラムが一つあるのだから、PWMも専用のプログラムを書いても良いのでは無いかと思うようになった。 それでもハードに近い部分までいじるのは避けたい。 少し調べると、ESP8266用のArduinoでは、 AnalogWriteやToneを実現するためには、startWaveformという関数が使われており、この関数を使えば、dutyとfrequencyを同時に変えることができることが分かった。

mrbc_pwm.hは、pin番号以外に、dutyとfrequencyを保存するために少しだけ書き換えた。

#ifndef _MRBC_PWM_H
#define _MRBC_PWM_H

#include <Arduino.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "mrubyc.h"

typedef struct PWM_HANDLE {
  uint8_t pin_num;
  float duty_num;
  float freq_num;
} PWM_HANDLE;

void mrbc_init_class_pwm(void);

#ifdef __cplusplus
}
#endif

#endif

mrbc_pwm.cppは、dutyとfrequencyの値から、highの時間とlowの時間を計算して、startWaveformに渡すようなサブルーチンを作って、それを他の関数から呼び出すようにした。 少し長くなったが、ガイドラインに沿った機能が使えるようになった。

#include "mrbc_pwm.h"
#include <core_esp8266_waveform.h>

extern "C" {

void c_pwm_sub(mrb_vm *vm, mrb_value *v){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  if(handle->freq_num==0){
    stopWaveform(handle->pin_num);
  }else{
    uint32_t timeLow=(uint32_t)(1e6/(handle->freq_num));
    uint32_t timeHigh=(uint32_t)(timeLow*(handle->duty_num)/100);
    timeLow = timeHigh>timeLow ? 0 : timeLow-timeHigh;
    pinMode(handle->pin_num, OUTPUT);
    startWaveform(handle->pin_num, timeHigh, timeLow);
  }
}

void c_pwm_new(mrb_vm *vm, mrb_value *v, int argc){
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(PWM_HANDLE));
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  handle->pin_num = GET_INT_ARG(1);
  handle->freq_num = 1000;
  handle->duty_num = 50.0;
  MRBC_KW_ARG(frequency, freq, duty);
  if( MRBC_ISNUMERIC(duty) )
    handle->duty_num = MRBC_TO_FLOAT(duty);
  if( MRBC_ISNUMERIC(frequency) )
    handle->freq_num = MRBC_TO_FLOAT(frequency);
  if( MRBC_ISNUMERIC(freq) )
    handle->freq_num = MRBC_TO_FLOAT(freq);
  MRBC_KW_DELETE(frequency, freq, duty);
  c_pwm_sub(vm,v);
}

static void c_pwm_duty(mrbc_vm *vm, mrbc_value *v, int argc){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  handle->duty_num = MRBC_ARG_F(1);
  c_pwm_sub(vm,v);
}

static void c_pwm_frequency(mrbc_vm *vm, mrbc_value *v, int argc){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  handle->freq_num = MRBC_ARG_F(1);
  c_pwm_sub(vm,v);
}

static void c_pwm_period_us(mrbc_vm *vm, mrbc_value *v, int argc){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  uint16_t us = MRBC_ARG_I(1);
  float freq = (us == 0 ? 0 : 1e6 / us);
  handle->freq_num = freq;
  c_pwm_sub(vm,v);
}

static void c_pwm_pulse_width_us(mrbc_vm *vm, mrbc_value *v, int argc){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  handle->duty_num = MRBC_ARG_I(1)*(handle->freq_num)/1e6*100;
  c_pwm_sub(vm,v);
}

void mrbc_init_class_pwm(void){
  mrbc_class *pwm = mrbc_define_class(0, "PWM", mrbc_class_object);
  mrbc_define_method(0, pwm, "new", c_pwm_new);
  mrbc_define_method(0, pwm, "duty", c_pwm_duty);
  mrbc_define_method(0, pwm, "frequency", c_pwm_frequency);
  mrbc_define_method(0, pwm, "period_us", c_pwm_period_us);
  mrbc_define_method(0, pwm, "pulse_width_us", c_pwm_pulse_width_us);
}

} //extern

引数を取り込むときに、 GET_INT_ARGとMRBC_ARG_Iのどちらを使うべきか、迷ったが前者の方が単純らしいのだが、思い通りに動かないことがあったので、後者を主に用いた。 前者は整数型の値を取り出すだけのマクロで、別の型が使われていた場合には、動作が不安定になるが、後者は型の判定などが行われるので、安全らしい。

PWMについては、CH32でも対応したプログラムを作るかどうか迷うところである。 マイコンの種類毎にプログラムを作ってしまったら、 Arduino用の関数を使っているとは言え、 mrubyc_arduinoを使っている利点が減ってしまうので、悩ましいところである。