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

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

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

# 目標

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

# 今回の方針

* コネクタ8 (CN8) シルク表示、AN0からAN5の6入力を全て使う事ができるようにする
* ピンの指定は、STM32マイコンのポート名を文字列でも指定できるようにする。（例： "PA0"）
* C言語のみで実装する


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

当ボードに搭載されている STM32F401RET6 は、12bit A/Dコンバータ(ADC)を1ユニット、前段にマルチプレクサを搭載し16チャネルの測定ができる仕様です。
このボードでは、16チャネルのマルチプレクサ入力のうち、後述する6入力をアナログ入力ピンとしてコネクタ8に割り当てています。この記事では、このコネクタ8のピン番号を、便宜的に「ADピン番号」と記述することにします。

## HALライブラリ調査

ADCの機能として、タイマーやDMAと組み合わせて複雑な制御もできますが、ペリフェラル仕様書ではごく単純なソフトウェア制御による測定のみを仕様化しているので、使い方は簡単です。
メーカー製 HAL リファレンスマニュアル (UM1725) や、CubeIDEでのコード自動生成により、HALのA/Dライブラリ使用方法を調査します。その結果、以下の順番でコールすれば今回の用途には十分であることが分かりました。

* `HAL_ADC_ConfigChannel()` で、マルチプレクサのチャネル設定を行う
* `HAL_ADC_Start()` で、A/D 開始
* `HAL_ADC_PollForConversion()` で、A/D 変換終了を待つ
* `HAL_ADC_GetValue()` で、測定値を取得

チャネルの指定には、`ADC_CHANNEL_0` 等のHALライブラリで提供された定数を使う必要があります。そのため、ピン("PA0")とチャネル(ADC_CHANNEL0) の対応表が必要になりそうです。

## ピン割り当て

ボード上のシルク印刷（＝ADピン番号）と、GPIOチャネル、A/Dチャネルの対応は、以下の通りになっています。

| CN | pin | SILK | GPIO | ADC channel |
|-|-:|-|-|-|
| CN8 | 1 | A0 | PA0 | ADC_CHANNEL_0 |
| CN8 | 2 | A1 | PA1 | ADC_CHANNEL_1 |
| CN8 | 3 | A2 | PA4 | ADC_CHANNEL_4 |
| CN8 | 4 | A3 | PB0 | ADC_CHANNEL_8 |
| CN8 | 5 | A4 | PC1 | ADC_CHANNEL_11 |
| CN8 | 6 | A5 | PC0 | ADC_CHANNEL_10 |


# 作業手順

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

## 雛形の作成

[前回のGPIOクラス実装編](https://qiita.com/HirohitoHigashi/items/9be9124f01a180fb68b1) と同様に、雛形のファイルを用意します。

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

extern ADC_HandleTypeDef hadc1;

/*! constructor
*/
static void c_adc_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

/*! read voltage
*/
static void c_adc_read_voltage(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

/*! read raw
*/
static void c_adc_read_raw(mrbc_vm *vm, mrbc_value v[], int argc)
{
}

/*! set up the ADC class.
*/
void mrbc_init_class_adc( void )
{
  mrbc_class *cls = mrbc_define_class(0, "ADC", 0);

  // define methods
  mrbc_define_method(0, cls, "new", c_adc_new);
  mrbc_define_method(0, cls, "read_voltage", c_adc_read_voltage);
  mrbc_define_method(0, cls, "read", c_adc_read_voltage);
  mrbc_define_method(0, cls, "read_raw", c_adc_read_raw);
}
```

今回はメソッドも少ないので、必要になる4つのメソッド定義と、そのモックを既に書いています。その中でも readメソッドは、read_voltageと同じ（エイリアス）なので、同じ名前の関数をコールすることでそれを実現しています。
ADCは、今後作る他の機能へ提供する機能は無いと思われるので、簡略化のためCヘッダファイルは作らなくても良いです。一方、ピンアサインにGPIOモジュールの機能を使うために、`stm32f4_gpio.h` をインクルードしておきます。
また、HALによって用意されたADCを使うために、`hadc1` の参照を記述します。

```c
#include "stm32f4_gpio.h"

extern ADC_HandleTypeDef hadc1;
```

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

```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();             // 追加
```

この段階で、一度正しくビルドができるか確認します。
さらに注意深く確認するには、Rubyスクリプトでも ADC クラスが使えるようになったかを確認するコードを書いておくと良いと思います。

```ruby:Core/mrubyc/task1.rb
a0 = ADC.new("PA0")
p a0.read_voltage
p a0.read
p a0.read_raw

puts "DONE"
```
ベースに、記事「[【チュートリアル】mruby/cをSTM32マイコンで動かす Chapter04: halの仕上げ](https://qiita.com/HirohitoHigashi/items/4e29539416d3c8df332b)」でポーティングした環境を使っているので、USBシリアル変換によるコンソール出力が表示できます。
実行してみて、コンソールにエラーが表示されず、最後に `DONE` と表示されれば、成功です。


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

コンストラクタの仕様を確認します。

:::note info
コンストラクタ (new) 仕様
```
ADC.new( pin, *params )
```
 * pin で示す物理ピンを指定して、ADC オブジェクトを生成する。
 * pin は標準的には整数で指定するが、別な方法（例えばPICでは"B1"等）があっても良い。
 * ピンにアナログ・デジタルモードの切り替えがある機器の場合は、このタイミングでアナログモードに切り替える。
 * 基本的に追加パラメータ params の指定は無いが、機種によってサンプリング速度などの追加機能指定がある場合は、ここへ指定する。
:::

いくつか機種依存の箇所があります。ここでは、
 * ピンの指定は、ボード上のADピン番号（シルク印刷の番号　0から5)、もしくは、GPIOのポート番号 (例："PA0" 等）　で指定する
 * A/Dの仕様は、CubeIDEで作られたデフォルト値のみ（追加パラメータなし）

ということで進めます。

### 変換テーブル

ADピン番号と、GPIOポート番号、ADCチャネル番号の対応が必要なので、テーブル化します。

```c
/*!
  Pin assign vs ADC channel table.
*/
static struct ADC_HANDLE {
  PIN_HANDLE pin;	//!< Pin
  uint32_t channel;	//!< ADC channel.

} const TBL_ADC_CHANNELS[] = {
                            // GPIO  ADC ch.  silk
  {{1, 0}, ADC_CHANNEL_0},	// PA0   0        A0
  {{1, 1}, ADC_CHANNEL_1},	// PA1   1        A1
  {{1, 4}, ADC_CHANNEL_4},	// PA4   1        A2
  {{2, 0}, ADC_CHANNEL_8},	// PB0   8        A3
  {{3, 1}, ADC_CHANNEL_11},	// PC1   11       A4
  {{3, 0}, ADC_CHANNEL_10},	// PC0   10       A5
};
static const int NUM_TBL_ADC_CHANNELS = sizeof(TBL_ADC_CHANNELS)/sizeof(struct ADC_HANDLE);
```
コンストラクタでは、このテーブルを使って引数からADピン番号を割り出します。

### 引数の解析

コンストラクタでは、第1引数として、ADピン番号=整数型、GPIOポート番号=文字列型の2種類の場合に対応します。

```c
static void c_adc_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  if( argc != 1 ) goto ERROR_RETURN;

  switch( v[1].tt ) {
  case MRBC_TT_INTEGER:  // in case of ADC.new(0)
    ...

  case MRBC_TT_STRING:  // in case of ADC.new("PA1")
    ...

  default:
    goto ERROR_RETURN;
  }
```

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

コンストラクタでは、引数で与えられたADピン番号を、インスタンスに記憶しておく必要があります。GPIOクラスでとった方法と同様に、`mrbc_instance_new()` 関数でインスタンス用メモリを確保時に、ADピン番号を保存するための容量を一緒に確保します。今回の場合は、int 型で十分でしょう。

```c
v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(int));
```

### 実装

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

```c
static void c_adc_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
  if( argc != 1 ) goto ERROR_RETURN;
  int idx;
  PIN_HANDLE pin;

  switch( v[1].tt ) {
  // in case of ADC.new(0)
  case MRBC_TT_INTEGER:
    idx = mrbc_integer(v[1]);
    if( idx < 0 || idx >= NUM_TBL_ADC_CHANNELS ) goto ERROR_RETURN;
    pin = TBL_ADC_CHANNELS[idx].pin;
    break;

  // in case of ADC.new("PA1")
  case MRBC_TT_STRING:
    if( gpio_set_pin_handle( &pin, &v[1] ) != 0 ) goto ERROR_RETURN;

    // find ADC channel from TBL_PIN_TO_ADC_CHANNEL table.
    for( idx = 0; idx < NUM_TBL_ADC_CHANNELS; idx++ ) {
      if( (TBL_ADC_CHANNELS[idx].pin.port == pin.port) &&
	  (TBL_ADC_CHANNELS[idx].pin.num == pin.num) ) break;
    }
    if( idx >= NUM_TBL_ADC_CHANNELS ) goto ERROR_RETURN;
    break;

  default:
    goto ERROR_RETURN;
  }

  // allocate instance with table index.
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(int));
  *((int *)(v[0].instance->data)) = idx;

  // set pin to analog input
  gpio_setmode( &pin, GPIO_ANALOG|GPIO_IN );
  return;

 ERROR_RETURN:
  mrbc_raise(vm, MRBC_CLASS(ArgumentError), "ADC initialize.");
}
```

## readの実装

APIガイドラインでは、ADCのリードには以下の2パターンがあります。
 * 戻り値を浮動小数点の電圧値で取得する `read_voltage`, `read`
 * 戻り値を整数で取得する `read_raw`

どちらも、読み込み動作に違いは無いので、まずは両方共通で使えるサブルーチンを定義します。

```c
static uint32_t read_sub(mrbc_vm *vm, mrbc_value v[], int argc)
{
  int idx = *((int *)(v[0].instance->data));

  ADC_ChannelConfTypeDef sConfig = {
    .Channel = TBL_ADC_CHANNELS[idx].channel,
    .Rank = 1,
    .SamplingTime = ADC_SAMPLETIME_3CYCLES,
  };
  if( HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK ) return 0;

  HAL_ADC_Start(&hadc1);
  if( HAL_ADC_PollForConversion(&hadc1, 1000) != HAL_OK ) return 0;

  return HAL_ADC_GetValue(&hadc1);
}
```

HALライブラリの調査で得た結果をそのまま実装しています。
ポイントとしては、チャネル番号の指定には、インスタンスメモリに保存した ADピン番号（int値）を取り出し、先のテーブルを使って ADCチャネル番号を取得しています。

このサブルーチンを使って、2つのメソッドを実装します。

```c
/*! read_voltage
  adc1.read_voltage() -> Float
*/
static void c_adc_read_voltage(mrbc_vm *vm, mrbc_value v[], int argc)
{
  uint32_t raw_val = read_sub( vm, v, argc );

  SET_FLOAT_RETURN( raw_val * 3.3 / 4095 );
}


/*! read_raw
  adc1.read_raw() -> Integer
*/
static void c_adc_read_raw(mrbc_vm *vm, mrbc_value v[], int argc)
{
  uint32_t raw_val = read_sub( vm, v, argc );

  SET_INT_RETURN( raw_val );
}
```

以上で実装が完了です。


# テスト

task1.rb にテストコードを書いて、正しく動作するか確認します。

```ruby:Core/mrubyc/task1.rb
a0 = ADC.new(0)
a1 = ADC.new(1)

while true
  v0 = a0.read_voltage()
  v1 = a1.read_raw()

  printf("A0 = %.3fV, A1 = %d (raw)\n", v0, v1)
  sleep 1
end
```

A0端子にDC定電圧電源を、A1端子にCdS素子を接続してテストします。
![STM32mrbc03-Test1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/6aca7e16-076c-19c2-b74c-ddbc1d03eed7.jpeg)

画面には以下のように表示され、CdSにあたる光を遮るとA1の値が小さくなります。

```
A0 = 3.007V, A1 = 3587 (raw)
A0 = 3.007V, A1 = 3574 (raw)
A0 = 3.008V, A1 = 1904 (raw)
A0 = 3.004V, A1 = 1571 (raw)
A0 = 3.007V, A1 = 1500 (raw)
...
```

## 測定値の検証

A0端子に入れている電圧を、テスター（基準値）と比較してみます。
測定値は、多少ばらつきますので、おおよそセンター値と思われる値を採用した結果が以下の表です。値がばらつくのは恐らく商用電源の影響を受けているのだと思います。

| 基準値(V) | 測定値(V) |
|-:|-:|
| 0.0000 | 0.000 |
| 0.3069 | 0.306 |
| 3.0106 | 3.012 |

使用機器
　テスター　三和電気計器　PC7000　(DC確度 ±0.03%rdg + 2dgt)
　直流電源　菊水電子　PAB 32-1.2A
 
まあ、こんなものではないでしょうか。

# おわりに

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

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

今回は、ADCクラスを実装しました。測定値の確からしさも、簡易的な確認ですが致命的なエラーも無さそうです。
次回は、PWMクラスを実装します。