ch32funのmruby/cでCH32X035のPWM


GPIOやADCは、ch32funでもArduinoと同じような感覚で、マイコンの種類を意識しないで使うことができる。 ch32funでmruby/cを使うときには、これを利用してmrubyc_arduinoのコードを少し変更するだけで、GPIOとADCを使うことができた。 また、USB CDCも予想していたよりは簡単に、マイコンの違いはあまり考慮する必要な無く、組み込むことに成功した。 しかし、ch32funでそれ以外の機能を使うときには、マイコンの細かな違いを考慮して、プログラムを作る必要がある。 これはそれなりに面倒なのだが、CH32X035用のmruby/cの機能を充実させるために、PWMを使えるようにしてみた。

CH32X035には、三つのタイマーがあり、これらを用いることによって、PWMを使うことができる。 すべてのピンでPWMが使えるわけでは無いが、 タイマーとセッティングとチャンネルの設定を駆使することによって、かなりのピンでPWMを使えるようにすることができる。 逆に、一つのピンでPWMを使う場合には、これらの選択の仕方が一通りでない場合もあるので、その時には他のピンとの相性などから、一つの設定を選ぶ。 CH32X035のPA0-15とPB0-15とPC0-15について、どのような設定でPWMを使うかを考えてみると、その約2/3でPWMが使えることが分かった。 ch32funでは、16番以降のピンを扱うのは面倒なので、今回はそれらは対象外とした。 timerとsettingとchを八進法で表すと8ビットに収まるので、配列を作ってピンと対応させた。 あとは、タイマー、セッティング、チャンネル毎に、適切なレジスタに必要な値を代入するようにすれば良い。

ESP8266用のPWMのプログラムを変更して作ったら、最初はfloatを使うようになっていて、そのせいでコンパイルしたときのサイズがかなり大きくなってしまった。 そこで、整数しか使わないようにしたら、ましにはなったけど、それでも2kはある。 素直にプログラムを書いたら、 タイマーで分岐して、セッティングを変更して、チャンネルでさらに分岐して、必要な操作をするようにしたので、 多くの条件分岐が必要で、レジスタへ定数を代入する箇所が多数あり、そのために大きくなってしまっているようである。 レジスタのビットの場所を調べて、レジスタ操作をまとめたら、1.7kぐらいにはなった。 さらに、 構造体のポインタを駆使して、レジスタの指定も条件分岐しないで指定できるようにしたところ、可読性はかなり悪くなったが、サイズは1.3kまで小さくなった。 まあ、このくらいが限界かな。

mrbc_pwm.hは、周波数とdutyを整数にしたので、以下のようにした。

#ifndef _MRBC_PWM_H
#define _MRBC_PWM_H

#include "ch32fun.h"
#include "mrubyc.h"

typedef struct PWM_HANDLE {
  uint8_t pin_num;
  uint16_t duty_num; // unit 1/10000
  uint32_t freq_num;
} PWM_HANDLE;

void mrbc_init_class_pwm(void);

#endif

ただし、floatを使わず整数だけで扱うようにしたことによって、dutyが整数のパーセントだと桁数が少なくなってしまうので、dutyの100倍の値を整数として保存するようにして、桁数を確保することにした。 パーセントの655.35倍にしても良いのだけど。

mrbc_pwm.cは、少し長くなるが、以下のような感じになった。 これでも、複雑な条件分岐無くなったので、かなり短かくなっている。

#include "mrbc_pwm.h"

static volatile uint8_t pin_timer[]={ // timer, map, ch
  0201, 0202, 0203, 0204, 0332, 0, 0301, 0302,
  0, 0, 0, 0, 0215, 0216, 0217, 0,
  0116, 0117, 0, 0223, 0224, 0312, 0125, 0126,
  0127, 0121, 0122, 0123, 0124, 0, 0, 0212,
  0131, 0132, 0133, 0134, 0, 0135, 0136, 0137,
  0, 0, 0, 0, 0, 0, 0262, 0263,
};
static volatile uint32_t base[]={TIM1_BASE,TIM2_BASE,TIM3_BASE};

void general_pwm(uint8_t pin, uint16_t limit, uint16_t val, uint16_t prescaler)
{
  if(pin>=48) return;
  uint8_t tmr=pin_timer[pin];
  if(tmr == 0) return;
  uint8_t ch=tmr%8;
  uint8_t alt=(tmr/8)%8; // alternate setting
  tmr>>=6;
  TIM_TypeDef *tim = ((TIM_TypeDef *) base[tmr-1]);
  volatile uint32_t *enadrs=&(RCC->APB2PCENR);
  *(enadrs+tmr/2) |= 1<<((tmr+10)%12); //RCC_APB2Periph_TIMx 0x800,1,2
  AFIO->PCFR1 &= ~( (7-tmr/3*4)<<(tmr*3+12) ); // AFIO_PCFR1_TIM1_REMAP 7,7,3
  AFIO->PCFR1 |= alt<<(tmr*3+12); // AFIO_PCFR1_TIMx_REMAP_0 15,18,21
  tim->PSC = prescaler;
  tim->CTLR1 |= TIM_ARPE;
  tim->ATRLR = limit;
  tim->CCER |= (TIM_CC1E | TIM_CC1P)<<(4*(ch-1)-ch/5*14); // 1,1N,2,2N,3,3N,4
  volatile uint16_t *chadrs=&(tim->CHCTLR1);
  *(chadrs+((ch-1)&2)) |= (TIM_OC1M_2 | TIM_OC1M_1 | TIM_OC1PE)<<((ch-1)%2*8);
  volatile uint32_t *vladrs=&(tim->CH1CVR);
  *(vladrs+(ch-1)%4) = val;
  tim->BDTR |= TIM_MOE;
  tim->SWEVGR |= TIM_UG;
  tim->CTLR1 |= TIM_CEN;
  funPinMode(pin, GPIO_CFGLR_OUT_50Mhz_AF_PP);
}

void c_pwm_sub(mrb_vm *vm, mrb_value *v){
  PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data;
  if(handle->freq_num==0){
    funDigitalWrite(handle->pin_num,0);
    funPinMode(handle->pin_num, GPIO_CFGLR_OUT_50Mhz_PP);
  }else{
    uint32_t limit=FUNCONF_SYSTEM_CORE_CLOCK/(handle->freq_num);
    uint32_t prescaler=limit>>16;
    limit/=(prescaler+1);
    uint16_t val=limit*(handle->duty_num)/10000;
    general_pwm(handle->pin_num, limit, val, prescaler);
  }
}

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);
  MRBC_KW_ARG(frequency, freq, duty);
  if( MRBC_ISNUMERIC(duty) ){
    handle->duty_num = MRBC_TO_INT(duty)*100;
  }else{
    handle->duty_num = 5000;
  }
  if( MRBC_ISNUMERIC(frequency) ){
    handle->freq_num = MRBC_TO_INT(frequency);
  }else if( MRBC_ISNUMERIC(freq) ){
    handle->freq_num = MRBC_TO_INT(freq);
  }else{
    handle->freq_num = 1000;
  }
  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_I(1)*100;
  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_I(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);
  uint32_t freq = ( us==0 ? 0 : 1000000 / 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)/100; //1e6/1e4
  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);
}

通常はレジスタ名を指定して、そこにbit情報の定義を使って、定数を代入するのだが、 レジスタはポインタを使ってタイマー毎にアドレスの調整をして、bitの場所も変数で変更して書き込んでいる。 マニュアルなどを参考にして、できるだけ単純にしようとしたが、 不規則な配置になっているときには、その処理に必要な数列をつくるための数式を考える必要があった。 タイマーやチャンネルは1から始まっているが、処理上は0から始まる数値の方が扱いやすい。 しかし、タイマーやチャンネルの名称と数字がずれると、人間が理解しにくくなるし、0のときにはPWMが使えないことを表すようにしている関係で、やはり1から始まる数字を使うようにしている。

PWMを動かすrubyプログラムはこんな感じである。

g=PWM.new(28,freq:5)
g.duty(30)
while true
  f=0
  while f<50
    g.frequency(f+=5)
    sleep 3
  end
end

注意点は、mrblibがないのでloopを使わすwhileで繰り返している点と、dutyやfrequencyを整数で指定することかな。

動作確認は、LEDのついているピンでしか動作確認していないので、バグが残っているかも知れない。 このピンはtimer1とtimer2で動かせるので、その二つの設定では動くことは確認はしたので、timer1とtimer2は大丈夫だと思うけど、 timer3の動作は確認できていない。 さて、次はI2Cかな。