40円マイコン+ArduinoでADCのアナログ値取得（analogRead）を使おうとしてハマった話

久しぶりにハマりました

## この記事は？
秋月電子で40円で販売されているRISC-Vマイコン CH32V003で、Arduinoの開発環境を利用してADCのアナログ測定値を読みだす方法を説明します。
公式から用意されたArduinoだし簡単にできるかと思ったら、意外とハマりどころが多く時間がかかってしまいました。

## 環境・動かすコード
* CH32V003J4M6 （40円 RISC-V 8ピンマイコン）
* Arduino IDE 2
* openwch/arduino_core_ch32
* 周辺光ライトセンサ GA1A2S

Arduinoのライブラリとして、WCHが評価ボード用に出している[arduino_core_ch32](https://github.com/openwch/arduino_core_ch32/tree/main)を使用します。  
一通りのライブラリ実装や書き込みプログラムなどが準備されていて、サクッと開発がコードを組んで動かすことができます。  

https://github.com/openwch/arduino_core_ch32/tree/main

開発環境は、下記サイトを参考にライタ（WCH LinkE）の設定変更とArduinoボードパッケージの用意をすればOKです。お手軽です。
[40円RISC-Vマイコン(CH32V003)をArduino IDEでLチカをしてみました - きょうのかんぱぱ](https://kanpapa.com/today/2023/06/blink-risc-v-ch32v003-arduino.html)

マイコンはSOP-8パッケージの表面実装用なので、SOP-8用の変換基板を使ったり無理やり線をはんだ付けしたりして、入出力ができるようにします。  
僕は以下のような簡単な基板を作りました。（汚いですが・・・）  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/52d5ee3b-5d74-0475-eb62-828ae7f8f7e3.png)


Arduinoでアナログ値を読みだす、以下のシンプルなコードでAD変換値を正しく取得できるようにします。

※値の出力にプログラム書き込み（SDIO）と同じ8ピンのUART出力を使用するので、`Serial.begin`してしまうと**プログラムが書き込めなくなります**。なので、下記コードでは起動時にプログラムを書き込む時間用の`delay`を設けています。

```cpp
#define ADC_PIN A2

void setup() {
// put your setup code here, to run once:
  delay(5000); // 注：UART初期化前に5秒待機（プログラム書き込みのため）
  Serial.begin(115200);
  pinMode(ADC_PIN,INPUT_ANALOG);
  Serial.println("Ready");
}

void loop() {
// put your main code here, to run repeatedly:
  uint16_t adc;
  adc = analogRead(ADC_PIN);
  Serial.println(adc);
  delay(100);
}
```

CH32V003J4M6は、6チャンネル 4ピンからAD値の取得が可能です。（下図：CH32V003 データシートより）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/de51f699-b587-f896-36b4-28aa1bbbcbe5.png)

今回はアナログ入力のチャンネル0（A0 = 3pin）を使用します。

ADCに入力するアナログ信号は、周辺光センサGA1A2Sの出力を使用します。  
アンプが内蔵されたフォトセンサで、3.3V電源を入力するとセンサ周辺の明るさに対応した**電流**を出力します。  
抵抗（数kΩ）の一端を周辺稿センサの出力に、もう一端をGNDに接続しで電流が流れるようにしたうえで、センサ出力側をマイコンの3ピンに接続します。

## 結論（正しく動かす方法）
以下のようにすると、妥当なAD変換値が読めるようになります。

* `variants\CH32V00x\CH32V003F4\variant_CH32V003F4.h`のコメントアウトされている`ADC_MODULE_ENABLED`の定義をアンコメントする
* 以下のいずれかをする
    * `system\CH32V00x\USER\system_ch32v00x.c`の`SYSCLK_FREQ_48MHz_HSE`をコメントアウトし、代わりに`SYSCLK_FREQ_xxMHZ_HSI`のいずれかをアンコメントする
    * アナログ入力チャネルとしてA0（3ピン）、A1（1ピン）ではなくA2（7ピン）を使用する

ここより以下では、なににハマったのか？なぜそうなったのか？を説明します。

## ハマり①：`analogRead`の戻り値が0固定になる
まず上記のコードを書き込んで実行すると、以下のように`analogRead`の戻り値が0だけになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/15fa1ad0-3341-ea0b-8c36-ea2f9296d63f.png)

### 原因と対策

これは、下記Issueで議論されている通り、ADCのモジュールを利用しない設定になっていることが原因です。  

https://github.com/openwch/arduino_core_ch32/issues/43

`analogRead`の実装を確認すると、`ADC_MODULE_ENABLED`が定義されていない場合、何もせず0を返すようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/a4796dc0-46e4-2cea-01bc-36117bba7b3b.png)


このため、issueの回答にもあるとおり、ボードパッケージ内の`variants\CH32V00x\CH32V003F4\variant_CH32V003F4.h`内で`ADC_MODULE_ENABLE`が定義されるよう、コメントアウトを外します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/b3a76b3d-a7cc-5b33-fdce-bd1d405e4c16.png)

なお、arduino IDEのボードパッケージは、Windowsの場合デフォルトでは`C:\Users\{ユーザ名}\AppData\Local\Arduino15\packages`にあります。

コメントアウトを外して再度ビルドすると、0以外の値を返すようになります。

## ハマり②：`analogRead`の戻り値が不安定な値になる。

`ADC_MODULE_ENABLED`を定義したうえでビルドして実行すると、出力は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/184cbcdb-1738-7f1a-aa65-913eb834b942.png)

一見正しく動いてそうですが、だいぶデタラメな出力になっています。
試しに、周辺稿センサにスマホのライトを近づけて出力を変化させ、それを測定して確認します。  
真値として、ラズピコを使った簡易オシロ（PicoProbe）でアナログ値を40円マイコンと同時に測定し、比較してみます。

↓40円マイコンの出力（シリアル出力をExcleでグラフ化しています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/1296b484-2dd5-19a7-87c0-f42e1d8fcb55.png)

↓PicoProbeの出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/8aeec59d-d1af-945f-f07a-04a95ef2963a.png)

このとおり、精度とか言うまでもなく、全くデタラメな値を吐いています。  
3.3Vを上限とする10bitのADCなので、測定波形（<1V）から考えると大体300前後の値が出るはずなのに、倍以上の値が出ています。また、ライトの移動に伴う入力値の変化に全く追従できていません。

### 原因と対策

関係ありそうなレジスタの設定値を出力して確認しました。  
（確認用のコードは本記事の末尾に掲載します。）  

出力結果は以下の通りでした。

```cpp
RCC_CTLR: 0x13783
RCC_CFGR0: 0x0
RCC_APB2PCENR: 0x4025
GPIOA_CFGLR: 0x40
ADC_CTLR1: 0x0
ADC_CTLR2: 0x0
ADC_RSQR3: 0x0
RCC_CTLR: 0x13783
RCC_CFGR0: 0x0
```

`ADC_CTLR2`（下図：CH32V003マニュアルより）の最下位ビット `ADON`が0なのが気になりましたが、ライブラリの実装を確認すると`analogRead`読み出し時に1にセットしていて、ここは問題ありませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/a5bf542d-5391-ed85-7395-77e53bca55c2.png)

気になったのはクロック設定である`RCC_CTLR`（下図：CH32V003マニュアルより）の設定値です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/cb7f28dd-c2e1-e71d-bf26-ea4b0c63c7e8.png)

設定値は`0x13783`なので、16bitの`HSEON`が1、17bitの`HSERDY`が0になっています。  
マニュアル（下図：CH32V003マニュアルより）によると、`HSEON`は外部入力クロック用のオシレータの設定ビット、`HSERDY`はそのオシレータの安定ビットです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/c8483944-6ef4-21f9-50d2-4ce4e614e4e4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/6da575d3-b947-1d4f-07cd-2880f9e2596e.png)

`OSC_OUT`（`OSCO`）はアナログ入力のチャンネル0（`A0`）と共通の3ピンを使っているので、関係がありそうです。  
`HSEON`のreset valueは0なので、ライブラリ側で設定しているはずだと考えて調べたところ、`system\CH32V00x\USER\system_ch32v00x.c`で以下のように設定を行っていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/c1f363ee-d472-c667-0a79-c80520248783.png)

同ファイル上部には、システムのクロックを設定できるマクロ定義があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/1752e149-b027-c561-4c6b-1806fbe9ca04.png)

システムクロックデフォルト設定が、外部クロックを使用する48MHzになっています。  
おそらく、このArduinoライブラリがCH32V003の評価ボード向けに開発されているため、このようになっているのでしょう。  
もちろん、外部クロックは搭載しないので、適切なクロック設定がなされるように`SYSCLK_FREQ_48MHz_HSE`をコメントアウトし、代わりに`SYSCLK_FREQ_xxMHZ_HSI`のいずれかのコメントアウトを外します。これで、ADCを正しく使えるようになります。

ADCの動作を確認するため、この状態で再度ビルドし、先ほどと同様にスマホのライトを当てて出力をみてみます。

↓40円マイコンの出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/2842335d-24f5-201f-6a3f-b1423306a3c2.png)

↓PicoProbeの出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/9fe2e740-14e9-a8c5-54ce-e032636a7ba1.png)

ラズピコオシロで取得した波形と、非常に近い形ので読めていることがわかります。  
    また、ラズピコオシロの測定上のVmaxが1.08Vなのに対し、40円マイコンではピークで300前後の値となっており、割と妥当な値になっているといえます。
アナログ値の読み出しくらい、サクッとできるかなと思っていたのですが、あまり思っていなかったところにはまりポイントがありました

### 補足1：ほかのチャンネルを使用する場合
アナログ入力のチャンネル0（A0）はHSE_Outと、チャンネル1（A1）はHSE_Inと機能がかぶっています。このため、A1（1ピン）をアナログ入力として使用しても、同じ現象が発生します。  
一方、チャンネル2（A2）は7ピンを使用しており、HSEの機能とはかぶっていません。このため、ハマりポイント②の変更を加えなくても、正しくAD変換した値を取得できます。

### 補足2：クロック設定について
システムクロックを48MHz HSEに設定する`SetSysClockTo_48MHz_HSE`をよく見ると、`HSEON`設定後にHSEの安定を待っており、`HSERDY`が経つまでループを回ります。  
ただし、このループにはタイムアウトが設定されており、一定時間経過してもフラグが経たない場合は次の処理に映るようになっています。

続く処理は以下のようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/bdb818f0-97db-de00-988a-b00f7dcc74f7.png)

タイムアウト時はL404の分岐がFalseになり、L406以降の処理が実施されません。  
L406以降ではクロックの設定（PLL onなど）を行っているのですが、すべてすっとばされてしまいます。つまり、想定された正しいクロック設定が一切なされていないままに`setup()`関数までたどり着くことになります。

他ペリフェラル等の振舞は見ていないのですが、ADC同様正しく動かない可能性が高いと思われます。  
ですので、補足1に書いたようにA2を使用する場合でも、このクロック設定は正しいものに変更しておいた方がいいでしょう。

### 補足3：レジスタ設定値出力用コード

```cpp
#define ADC_PIN A0

// クロック関係
#define ADDR_RCC_CTLR ((volatile uint32_t *)(0x40021000))
#define ADDR_RCC_CFGR0 ((volatile uint32_t *)(0x40021004))
#define ADDR_RCC_APB2PCENR ((volatile uint32_t *)(0x40021018))

// GPIO関係
#define ADDR_GPIOA_CFGLR ((volatile uint32_t *)(0x40010800))

#define ADDR_ADC_CTLR1 ((volatile uint32_t *)(0x40012404))
#define ADDR_ADC_CTLR2 ((volatile uint32_t *)(0x40012408))
#define ADDR_ADC_RSQR3 ((volatile uint32_t *)(0x40012434))

void setup() {
// put your setup code here, to run once:
  delay(5000);
  Serial.begin(115200);
  pinMode(ADC_PIN,INPUT_ANALOG);
  Serial.println("Ready");
}

void loop() {
// put your main code here, to run repeatedly:
  uint32_t hoge;
  char fuga[50];

  hoge = *ADDR_RCC_CTLR;
  sprintf(fuga, "RCC_CTLR: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_RCC_CFGR0;
  sprintf(fuga, "RCC_CFGR0: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_RCC_APB2PCENR;
  sprintf(fuga, "RCC_APB2PCENR: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_GPIOA_CFGLR;
  sprintf(fuga, "GPIOA_CFGLR: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_ADC_CTLR1;
  sprintf(fuga, "ADC_CTLR1: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_ADC_CTLR2;
  sprintf(fuga, "ADC_CTLR2: 0x%lx", hoge);
  Serial.println(fuga);          // debug value
  hoge = *ADDR_ADC_RSQR3;
  sprintf(fuga, "ADC_RSQR3: 0x%lx", hoge);
  Serial.println(fuga);          // debug value

  delay(1000);
}
```