mruby/cペリフェラルライブラリのSTM32マイコンへの実装 Chapter04: PWMクラス実装編

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

[mruby/cペリフェラルライブラリのSTM32マイコンへの実装の記事](https://qiita.com/HirohitoHigashi/items/4a88aad1523455ae9857)、今回はその第4回、PWMクラスを実装します。

# 目標

[PWMクラスのAPIガイドライン](https://github.com/mruby/microcontroller-peripheral-interface-guide/blob/main/mruby_io_PWM_ja.md) に従って、STM32マイコン（Nucleo F401RE) 向けの実装を完了させる。

# 今回の方針

* ボード上のシルク PWM と書かれたピンだけではなく、ハード的に対応可能なピンはPWM出力を可能とする
* ピンの指定は、STM32マイコンのポート名を文字列で指定する。（例： "PA8"）
* C言語のみで実装する


# PWMユニットの仕様と、ピン割り当て調査

当ボードに搭載されている STM32F401RET6 は、PWM出力をするためのタイマーユニットを、合計8ユニット搭載しています。これらを、上記方針を考慮して、[第一回で調査、ピン割り付け](https://qiita.com/HirohitoHigashi/items/4a88aad1523455ae9857#%E3%83%9A%E3%83%AA%E3%83%95%E3%82%A7%E3%83%A9%E3%83%AB%E5%8F%8A%E3%81%B3%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%83%94%E3%83%B3%E3%81%AE%E5%89%B2%E3%82%8A%E4%BB%98%E3%81%91) しています。

（ピン割り付けのPWM（タイマー）部分のみ抜き出して再掲）

| CN | pin | SILK | GPIO | Usage | (PWM) |
|-|-:|-|-|-|-|
| CN5 | 5 | MISO/D12 | PA6 |  | TIM3_CH1 |
| CN5 | 4 | PWM/MOSI/D11 | PA7 |  | TIM3_CH2 |
| CN5 | 3 | PWM/CS/D10 | PB6 |  | TIM4_CH1 |
| CN5 | 2 | PWM/D9 | PC7 |  | TIM3_CH2 |
| CN9 | 8 | D7 | PA8 |  | TIM1_CH1 |
| CN9 | 7 | PWM/D6 | PB10 |  | TIM2_CH3 |
| CN9 | 6 | PMW/D5 | PB4 |  | TIM3_CH1 |
| CN9 | 5 | D4 | PB5 |  | TIM3_CH2 |
| CN8 | 1 | A0 | PA0 | AD0 | TIM2_CH1 |
| CN8 | 2 | A1 | PA1 | AD1 | TIM2_CH2 |
| CN8 | 4 | A3 | PB0 | AD8 | TIM3_CH3 |

この通り、利用できるタイマーユニットは、TIM1, TIM2, TIM3, TIM4 の4ユニットです。また、1ユニットに複数チャネルをもち、チャネルごとにデューティー比を設定できます。
まとめると、以下のようになります。

* タイマーは、TIM1からTIM4までの4ユニット使用する
* タイマーごとに複数チャネルを持つ
* タイマーごとに周波数の設定ができる
* チャネルごとにデューティー比を設定できる

たとえば、PA6とPA7ピンは、同じTIM3に属しますので、両者は同じ周波数の出力しかできません。一方、PA7とPB6は、TIM3とTIM4でユニットが違いますので、違う周波数が出力できます。


## HALライブラリ調査

メーカー製 HAL リファレンスマニュアル (UM1725) や、CubeIDEでのコード自動生成により、HALでタイマーをPWMとして使う方法を調査します。その結果、他のライブラリとは違って、レジスタ直接操作に近い方法しか用意されていないことが分かりました。

### 周波数の設定

```
__HAL_TIM_SET_PRESCALER( TIM handle, Prescaler value );
__HAL_TIM_SET_AUTORELOAD(　TIM handle, Autoload value );
```

タイマーのクロックには、前段に16bitのプリスケーラ(Prescaler value)と、タイムアップのカウンタ（Autoload value)と組み合わせで周波数を決定します。それらを、この2つのマクロで設定します。

```math
F_{out} = \frac{F_{APB}}{((psc+1) \cdot (arr+1)}
```
* F<sub>out</sub>　出力周波数
* F<sub>APB</sub>　ペリフェラルクロック
* psc　Prescaler register value
* arr　Autoload register value

mruby/c の APIでは出力周波数を指定する仕様なので、pscとarrに分解して計算する必要がありそうです。


### デューティー比（ON時間）の設定

以下のマクロで設定します。
```
__HAL_TIM_SET_COMPARE( TIM handle, TIM Channel, Compare value );
```

これは、前述のarr に対しての値です。
周波数設定に関してある程度の誤差を許せば、psc, arr の組み合わせはいくつもありますが、デューティー比の誤差を少なくしたいなら、できるだけ arr の値を大きくするほうが有利だということです。


# 作業手順

では、実際の作業に入ります。

## 雛形の作成

[GPIOクラス実装編](https://qiita.com/HirohitoHigashi/items/9be9124f01a180fb68b1) と同様に、雛形のファイルを用意してから、これに肉付けしていきます。

```c:Core/mrubyc/stm32f4_pwm.c
#include "main.h"
#include "../mrubyc_src/mrubyc.h"
#include "stm32f4_gpio.h"

static const uint32_t PWM_TIMER_FREQ = 84000000;	// 84MHz

extern TIM_HandleTypeDef htim1;
extern TIM_HandleTypeDef htim2;
extern TIM_HandleTypeDef htim3;
extern TIM_HandleTypeDef htim4;

static void c_pwm_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

static void c_pwm_frequency(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

static void c_pwm_period_us(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

static void c_pwm_duty(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

static void c_pwm_pulse_width_us(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

void mrbc_init_class_pwm(void)
{
  mrbc_class *cls = mrbc_define_class(0, "PWM", 0);

  mrbc_define_method(0, cls, "new", c_pwm_new);
  mrbc_define_method(0, cls, "frequency", c_pwm_frequency);
  mrbc_define_method(0, cls, "period_us", c_pwm_period_us);
  mrbc_define_method(0, cls, "duty", c_pwm_duty);
  mrbc_define_method(0, cls, "pulse_width_us", c_pwm_pulse_width_us);
}

```

PWMも、今後作る他の機能へ提供する機能は無いので、簡略化のためにCヘッダファイルを作りません。一方、GPIOのピン割り当て機能を使いたいので、`stm32f4_gpio.h` を include しておきます。
今回のポイントは、HALによって用意されたタイマーライブラリを使うためのグローバル変数を、extern宣言しておくことと、ペリフェラルクロックを定数定義していることです。ペリフェラルクロックの取得は HAL関数をうまく組み合わせれば動的に取得できそうではありましたが、簡単ではなさそうだったので、今回は単純に定数定義で済ませています。

```c
#include "stm32f4_gpio.h"

extern TIM_HandleTypeDef htim1;
extern TIM_HandleTypeDef htim2;
extern TIM_HandleTypeDef htim3;
extern TIM_HandleTypeDef htim4;

static const uint32_t PWM_TIMER_FREQ = 84000000;	// 84MHz
```

`start_mrubyc()` 関数へ、今回作成したPWM初期化用関数 mrbc_init_class_pwm() をコールするよう書き足します。

```c:Core/mrubyc/start_mrubyc.c
/*! mruby/c プログラムの実行開始
*/
void start_mrubyc( void )
{
  mrbc_init(memory_pool, MRBC_MEMORY_SIZE);

  // 各クラスの初期化
  void mrbc_init_class_gpio(void);
  mrbc_init_class_gpio();
  void mrbc_init_class_adc(void);
  mrbc_init_class_adc();
  void mrbc_init_class_pwm(void);    // 追加
  mrbc_init_class_pwm();             // 追加
```
この段階で、一度正しくビルドができるか確認します。
さらに注意深く確認するには、Rubyスクリプトでも PWM クラスが使えるようになったかを確認するコードを書いておくと良いと思います。

```ruby:Core/mrubyc/task1.rb
pwm = PWM.new("PA8", frequency: 1000)

puts "DONE"
```

## 変換テーブル

どのピンがどのタイマーユニット、チャネルを使うかの対応をプログラム側で知識化する必要がありますので、変換テーブルを作ります。

```c
/*
  PWM pin assign table
*/
static struct PWM_PIN_ASSIGN {
  PIN_HANDLE pin;	//!< Pin
  uint8_t unit_num;	//!< Timer unit number. (1..)
  uint8_t channel;	//!< Timer channel. (1..4)
} const PWM_PIN_ASSIGN[] =
{
  {{ 1,	6}, 3, 1 },	// PA6	TIM3_CH1
  {{ 1,	7}, 3, 2 },	// PA7	TIM3_CH2
  {{ 2,	6}, 4, 1 },	// PB6	TIM4_CH1
  {{ 3,	7}, 3, 2 },	// PC7	TIM3_CH2
  {{ 1,	8}, 1, 1 },	// PA8	TIM1_CH1
  {{ 2,10}, 2, 3 },	// PB10	TIM2_CH3
  {{ 2,	4}, 3, 1 },	// PB4	TIM3_CH1
  {{ 2,	5}, 3, 2 },	// PB5	TIM3_CH2
  {{ 1,	0}, 2, 1 },	// PA0	TIM2_CH1
  {{ 1,	1}, 2, 2 },	// PA1	TIM2_CH2
  {{ 2,	0}, 3, 3 },	// PB0	TIM3_CH3
};
```

HALライブラリの利用には、TIM_HandleTypeDef　構造体へのポインタと　TIM_CHANNEL_n 定数の利用が必要です。上記 PWM_PIN_ASSIGN テーブルに含めるデザインも考えられますが、今回は別な変換テーブルを使ってみようと思います。

```c
static TIM_HandleTypeDef * const TBL_UNIT_TO_HAL_HANDLE[/* unit */] = {
  0, &htim1, &htim2, &htim3, &htim4,
};

static uint32_t const TBL_CHANNEL_TO_HAL_CHANNEL[/* channel */] = {
  0, TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3, TIM_CHANNEL_4,
};
```

## コンストラクタの実装

[コンストラクタの仕様](https://github.com/mruby/microcontroller-peripheral-interface-guide/blob/main/mruby_io_PWM_ja.md#%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF) を確認すると、以下の通りです。

:::note info
コンストラクタ (new) 仕様
```
PWM.new( pin, *params )
```

  * pin で示す物理ピンを指定して、PWM オブジェクトを生成する。
  * pin は標準的には整数で指定するが、別な方法（例えばPICでは"B1"等）があっても良い。
  * パラメータ frequency を指定すると、デューティー比50%で出力を開始する。
  * デューティー比50%以外で出力を開始したい場合には、パラメータ duty を同時に指定する。

オプションパラメータ

| param | type | description|
|-|-|-|
| frequency | Integer,Float | 周波数の指定 |
| freq | 同上 | |	
| duty | Integer,Float | デューティー比の指定 |
:::

ここでは、当初方針どおりピンの指定は、GPIOのポート番号 (例："PA8" 等）　で指定することにします。
また、オプションパラメータは、mruby2系列よりキーワード引数として解釈されるので、その対応も必要です。

### 引数の解析

キーワード引数の解析が必要です。mruby/c のキーワード引数の解析には、`MRBC_KW_` で始まるいくつかのマクロを用意しています。

```c
static void c_pwm_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  MRBC_KW_ARG(frequency, freq, duty);
  if( !MRBC_KW_END() ) goto RETURN;
  if( argc == 0 ) goto ERROR_RETURN;
  ...

 ERROR_RETURN:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), "PWM initialize.");

 RETURN:
  MRBC_KW_DELETE(frequency, freq, duty);
}
```

<dl>
<dt>MRBC_KW_ARG　マクロ
<dd>キーワード引数を宣言する。マクロによって、引数と同名の mrbc_value 変数が用意される。
<dt>MRBC_KW_END　マクロ
<dd>他の（MRBC_KW_ARGで宣言した以外の）キーワード変数があるか確認し、結果をbool値で返す。
<dt>MRBC_KW_DELETE マクロ
<dd>MRBC_KW_ARGで用意したキーワード引数用変数の使い終わりを宣言する。必ず MRBC_KW_ARG とペアで使用する。
</dl>

mruby/c は、ガベージコレクションの方法にリファレンスカウンタを使用しているため、少し面倒ですが、MRBC_KW_DELETE で明示的に使い終わりの宣言が必要です。

次に、第1引数で指定されたピン番号（例 "PA8" 等）を、gpioに用意した関数 `gpio_set_pin_handle()` によって解析し、PWM_PIN_ASSIGNテーブルを引いて問題ないかを確認します。

```c
  PIN_HANDLE pin;
  if( gpio_set_pin_handle( &pin, &v[1] ) != 0 ) goto ERROR_RETURN;

  // find from PWM_PIN_ASSIGN table
  static const int NUM = sizeof(PWM_PIN_ASSIGN)/sizeof(PWM_PIN_ASSIGN[0]);
  int i;
  for( i = 0; i < NUM; i++ ) {
    if( (PWM_PIN_ASSIGN[i].pin.port == pin.port) &&
	(PWM_PIN_ASSIGN[i].pin.num  == pin.num) ) break;
  }
  if( i == NUM ) goto ERROR_RETURN;
```

### インスタンス用メモリの確保

インスタンスの管理方法もいくつか考えられますが、今回は、PWM_HANDLE構造体を定義し、インスタンスにPWM_HANDLE構造体を含めて管理する方針にします。

```c
/*!
  PWM handle
*/
typedef struct PWM_HANDLE {
  PIN_HANDLE pin;	//!< pin
  uint8_t unit_num;	//!< timer unit number.
  uint8_t channel;	//!< timer channel number.
  uint16_t psc;     //!< value in the PSC register.
  uint16_t period;	//!< value in the ARR register.
  uint16_t duty;	//!< percent but stretch 100% to UINT16_MAX
} PWM_HANDLE;
```

```c
  // allocate instance with PWM_HANDLE.
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(PWM_HANDLE));
  PWM_HANDLE *hndl = (PWM_HANDLE *)(v[0].instance->data);

  hndl->pin = pin;
  hndl->unit_num = PWM_PIN_ASSIGN[i].unit_num;
  hndl->channel  = PWM_PIN_ASSIGN[i].channel;
  hndl->duty = UINT16_MAX / 2;
```

### 周波数とデューティー比の反映

`MRBC_KW_ARG` マクロによって用意された各変数を使って周波数とデューティー比をセットします。実際の設定はサブルーチンを作って委譲します（後述）。

```c
  // set frequency and duty
  if( MRBC_ISNUMERIC(frequency) ) {
    pwm_set_frequency( hndl, MRBC_TO_FLOAT(frequency));
  }
  if( MRBC_ISNUMERIC(freq) ) {
    pwm_set_frequency( hndl, MRBC_TO_FLOAT(freq));
  }
  if( MRBC_ISNUMERIC(duty) ) {
    pwm_set_duty( hndl, MRBC_TO_FLOAT(duty));
  }
```

### 指定のピンを PWM 出力用に設定変更する

GPIOクラスファイルに、`gpio_setmode()` という、ピンを任意のモードに設定する関数があります。同様の考え方で、ピンを PWM出力モードに切り替える関数 `gpio_setmode_pwm()` を作って実際の設定を委譲します。もちろん、`gpio_setmode()` 関数を機能拡張して、PWM設定もできるようパラメータの追加等で対応することもできますが、今回は関数を分けた方が簡単でした。

```c
  // set GPIO pin.
  gpio_setmode_pwm( &pin, hndl->unit_num );
```

### PWM動作開始

`HAL_TIM_PWM_Start()` 関数によって、PWM出力を開始します。

```c
  // start!
  if( hndl->period != 0 ) {
    HAL_TIM_PWM_Start( TBL_UNIT_TO_HAL_HANDLE[hndl->unit_num],
                       TBL_CHANNEL_TO_HAL_CHANNEL[hndl->channel] );
  }
```

### 実装

最終的なコンストラクタの実装は、こうなりました。

```c
static void c_pwm_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  MRBC_KW_ARG(frequency, freq, duty);
  if( !MRBC_KW_END() ) goto RETURN;
  if( argc == 0 ) goto ERROR_RETURN;

  PIN_HANDLE pin;
  if( gpio_set_pin_handle( &pin, &v[1] ) != 0 ) goto ERROR_RETURN;

  // find from PWM_PIN_ASSIGN table
  static const int NUM = sizeof(PWM_PIN_ASSIGN)/sizeof(PWM_PIN_ASSIGN[0]);
  int i;
  for( i = 0; i < NUM; i++ ) {
    if( (PWM_PIN_ASSIGN[i].pin.port == pin.port) &&
	(PWM_PIN_ASSIGN[i].pin.num  == pin.num) ) break;
  }
  if( i == NUM ) goto ERROR_RETURN;

  // allocate instance with PWM_HANDLE.
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(PWM_HANDLE));
  PWM_HANDLE *hndl = (PWM_HANDLE *)(v[0].instance->data);

  hndl->pin = pin;
  hndl->unit_num = PWM_PIN_ASSIGN[i].unit_num;
  hndl->channel  = PWM_PIN_ASSIGN[i].channel;
  hndl->duty = UINT16_MAX / 2;

  // set frequency and duty
  if( MRBC_ISNUMERIC(frequency) ) {
    pwm_set_frequency( hndl, MRBC_TO_FLOAT(frequency));
  }
  if( MRBC_ISNUMERIC(freq) ) {
    pwm_set_frequency( hndl, MRBC_TO_FLOAT(freq));
  }
  if( MRBC_ISNUMERIC(duty) ) {
    pwm_set_duty( hndl, MRBC_TO_FLOAT(duty));
  }

  // set GPIO pin.
  gpio_setmode_pwm( &pin, hndl->unit_num );

  // start!
  if( hndl->period != 0 ) {
    HAL_TIM_PWM_Start( TBL_UNIT_TO_HAL_HANDLE[hndl->unit_num],
                       TBL_CHANNEL_TO_HAL_CHANNEL[hndl->channel] );
  }
  goto RETURN;


 ERROR_RETURN:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), "PWM initialize.");

 RETURN:
  MRBC_KW_DELETE(frequency, freq, duty);
}
```

## 周波数設定サブルーチン

周波数設定は、コンストラクタからも`frequency`メソッドからも使われるので、サブルーチン化しておきます。

```c
static int pwm_set_frequency( PWM_HANDLE *hndl, double freq )
{
  TIM_HandleTypeDef *htim = TBL_UNIT_TO_HAL_HANDLE[ hndl->unit_num ];

  if( freq == 0 ) {
    hndl->period = 0;
    __HAL_TIM_SET_COMPARE(htim, TBL_CHANNEL_TO_HAL_CHANNEL[ hndl->channel ], 0);
    return 0;
  }

  uint32_t ps_ar = PWM_TIMER_FREQ / freq;
  uint16_t psc = ps_ar >> 16;
  uint16_t arr = ps_ar / (psc+1) - 1;

  __HAL_TIM_SET_PRESCALER(htim, psc);
  __HAL_TIM_SET_AUTORELOAD(htim, arr);
  __HAL_TIM_SET_COMPARE(htim, TBL_CHANNEL_TO_HAL_CHANNEL[ hndl->channel ],
                        (uint32_t)arr * hndl->duty / UINT16_MAX);
  hndl->psc = psc;
  hndl->period = arr;

  return 0;
}
```

ここでは、HALの使い方で調べた各種マクロを使って、要求された周波数に近い設定となるよう各レジスタ値を計算、設定しています。戦略として、デューティー比の解像度を上げるため、できるだけ arr の値が大きくなるよう計算しています。代わりに周波数の正しさが多少犠牲になっているケースがありますが、トレードオフです。

## デューティー比設定サブルーチン

デューティー比の設定は、以下の通り `__HAL_TIM_SET_COMPARE` マクロによって設定します。
また、設定値の保存は、double 値を直接保存するのではなく、UINT16_MAX を100%とするスケーリングを行ってから PWM_HANDLE 構造体へ保存しています。

```c
//================================================================
/*! set duty cycle in percentage.
*/
static int pwm_set_duty( PWM_HANDLE *hndl, double duty )
{
  TIM_HandleTypeDef *htim = TBL_UNIT_TO_HAL_HANDLE[ hndl->unit_num ];

  hndl->duty = duty / 100 * UINT16_MAX;
  __HAL_TIM_SET_COMPARE(htim, TBL_CHANNEL_TO_HAL_CHANNEL[ hndl->channel ],
                        (uint32_t)hndl->period * duty / 100);
  return 0;
}
```

## ピンをPWM出力モードに変更するサブルーチン

この関数は、GPIOに関連するということで、`stm32_gpio.c` へ実装しています。
GPIO_InitTypeDef 構造体の Alternate メンバへ、GPIO_AF1_TIM1等のマクロ定数の指定が必要なので、ここでもテーブルを作って参照しています。
  
```c
int gpio_setmode_pwm( const PIN_HANDLE *pin, int unit_num )
{
  static uint8_t const PWM_GPIO_ALT[/* unit */] = {
    0, GPIO_AF1_TIM1, GPIO_AF1_TIM2, GPIO_AF2_TIM3, GPIO_AF2_TIM4,
  };

  GPIO_InitTypeDef GPIO_InitStruct = {
    .Pin = TBL_NUM_TO_STM32PIN[pin->num],
    .Mode = GPIO_MODE_AF_PP,
    .Pull = GPIO_NOPULL,
    .Speed = GPIO_SPEED_FREQ_LOW,
    .Alternate = PWM_GPIO_ALT[unit_num],
  };
  HAL_GPIO_Init( TBL_PORT_TO_STM32GPIO[pin->port], &GPIO_InitStruct);

  return 0;
}
```

## その他サブルーチン

この PWM クラスガイドラインの特徴として、周波数、デューティー比を時間（マイクロ秒）で指定する方法が用意されていることです。それをサポートする関数はごく単純な計算式によって実装できます。

```c
static int pwm_set_period_us( PWM_HANDLE *hndl, unsigned int us )
{
  double freq = (us == 0 ? 0 : 1e6 / us);
  return pwm_set_frequency( hndl, freq );
}

static int pwm_set_pulse_width_us( PWM_HANDLE *hndl, unsigned int us )
{
  TIM_HandleTypeDef *htim = TBL_UNIT_TO_HAL_HANDLE[ hndl->unit_num ];
  uint16_t pw_cnt = (us * (PWM_TIMER_FREQ / 1000000)) / (hndl->psc + 1) - 1;

  __HAL_TIM_SET_COMPARE(htim, TBL_CHANNEL_TO_HAL_CHANNEL[ hndl->channel ],
			pw_cnt);
  return 0;
}
```

## frequency メソッドの実装

mruby/c の frequency メソッドの実装をします。
先に作ったサブルーチンがあるので、それを呼び出すだけです。

```c
static void c_pwm_frequency(mrbc_vm *vm, mrbc_value v[], int argc)
{
  PWM_HANDLE *hndl = (PWM_HANDLE *)(v[0].instance->data);

  if( MRBC_ISNUMERIC(v[1]) ) {
    pwm_set_frequency( hndl, MRBC_TO_FLOAT(v[1]) );
  }
}
```

引数エラーがあった時に、メッセージなど表示していませんが、した方が親切しれませんね。


## その他メソッドの実装

いずれも、既にサブルーチンがあるので、それを呼ぶだけになります。詳しくは github リポジトリをご覧ください。


# テスト

## ElChika

task1.rb にテストコードを書いて、正しく動作するか確認します。プリスケーラのおかげで、非常に低い周波数でも出すことができるので、PWMを2つ使ったエルチカをしてみます。

```ruby:Core/mrubyc/task1.rb
pwm1 = PWM.new("PA8",  frequency: 1)
pwm2 = PWM.new("PB10", frequency: 2, duty: 10)
```
![STM32mrbc04-TestResult1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/b3a98cd9-a45b-d5e3-5a95-a2048c3d4fa2.gif)

2つの端子で、異なる周波数、異なるデューティー比でPWM出力ができている様子が確認できます。

## 周波数確認

| 設定周波数(Hz) | 測定値(Hz) |
|-:|-:|
|1k|1.0000k|
|2k|2.0000k|
|10k|10.000k|
|100k|100.00k|

使用機器
　テスター　三和電気計器　PC7000　（周波数確度 ±0.002%rdg +4dgt）

周波数カウンタが用意できなかったので、簡易測定です。マイコン側のクロックソースは、プログラマ基板付属のクリスタルですので、この程度の計測器では誤差が見えないのは当然でしたね。

## 模型用サーボモータ

模型用のサーボモータは、PWMでコントロールできるものが多く販売されており、当ボードでも使ってみます。

![STM32mrbc04-ServoMotor.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/7a36f87e-7c62-c090-1378-0351ec4e4a0a.jpeg)

使用機種　双葉電子工業 [RS304MD](https://www.futaba.co.jp/product/industrial_servo/command_type_servos/rs304md)

### 仕様確認

取扱説明書から抜粋します。

:::note info
RS303MR／RS304MD を PWM 方式で制御する場合は、一定周期（4ms～50ms）のパルスの幅を変化させて動作させます。（中略）　
パルス幅と動作角度（位置）の関係は下記となります。

|パルス幅|角度（位置）|
|-:|-:|
|560µs|+144度|
|1520µs|0度|
|2480µs|-144度|
:::

### デモプログラム

[ADCクラス実装編](https://qiita.com/HirohitoHigashi/items/d1827ce1830e79821f67) で、既にアナログ電圧値を扱えるようになっていますので、可変抵抗器をつないで、可変抵抗器のノブの角度とサーボモーターの回転角度が同じになるようマスタースレーブ動作をさせてみます。

![STM32mrbc04-Test_Overall.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/d14d09b9-391b-87bd-1e4c-5cc27d865ed0.jpeg)

![STM32mrbc04-TestResult2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/3fbce61c-6e20-c925-9ac3-9b4b24bc3542.gif)


```ruby:Core/mrubyc/task2.rb
PULSE_WIDTH_1 = 560 + 30
PULSE_WIDTH_2 = 2480 - 30
VR_MAX = 4095
DATA_MAX = 10

vr = ADC.new("PA0")             # 可変抵抗器 (VR)
servo = PWM.new("PB4")          # サーボモーター

servo.period_us( 10_000 )       # 10ms
servo.pulse_width_us( 0 )

# データ配列の準備
data = []
DATA_MAX.times {
  data << vr.read_raw
  sleep_ms 10
}

while true
  # VRから値取得しデータ配列へ
  data.shift
  data << vr.read_raw

  # 移動平均の計算
  avg = 0
  data.each {|d1| avg += d1 }
  avg /= data.size

  # Pulse width に換算して出力
  pw = PULSE_WIDTH_2 - (PULSE_WIDTH_2 - PULSE_WIDTH_1) * avg / VR_MAX
  servo.pulse_width_us( pw )

  sleep_ms 8
end
```

### 簡単な解説

まず、使用するピンを決めて、それぞれインスタンスを作ります。
```ruby
vr = ADC.new("PA0")             # 可変抵抗器 (VR)
servo = PWM.new("PB4")          # サーボモーター
```

周期は、サーボモーターの仕様により　4ms～50ms 範囲と決められているので、ここでは 10ms とします。
```ruby
servo.period_us( 10_000 )       # 10ms
```

可変抵抗器はノイズを拾うことも多いので、10個の移動平均をとって制御値を決定することにします。
まず、初期データを連続して取得して用意します。
```ruby
# データ配列の準備
data = []
DATA_MAX.times {
  data << vr.read_raw
  sleep_ms 10
}
```

メインループ中ではサンプリング1回として、データ配列には常に最新の10サンプリング分のデータが入っているようにします。
```ruby
  data.shift
  data << vr.read_raw
```

平均を計算します。
```ruby
  avg = 0
  data.each {|d1| avg += d1 }
  avg /= data.size
```

角度に相当するパルス幅を比率計算によって求め、サーボ (PWM) へ出力します。
```ruby
  pw = PULSE_WIDTH_2 - (PULSE_WIDTH_2 - PULSE_WIDTH_1) * avg / VR_MAX
  servo.pulse_width_us( pw )
```

周期やパルス幅が、時間の単位 (マイクロ秒）で直接指示できるので、プログラムが書きやすいですね。


# おわりに

ファイル全体は、github リポジトリにありますので、そちらをご覧ください。

https://github.com/HirohitoHigashi/mrubyc-nucleo-F401RE/tree/chapter04_pwm

今回は、PWMクラスを実装しました。プリスケーラのおかげで非常に幅広い周波数に対応ができますが、周波数からパラメータを逆算するプログラムは、少し面倒でした。
また、mruby/c のキーワード引数使用例も示すことができました。

次回は、I2Cクラスを実装します。