CH32VのオレオレArduino環境を作ろう その6 ADC

概要

前回はTickタイマーを使って時間系の処理を作りました。今回はADCを作ってみたいと思います。

CH32VのADCについて

CH32VではESP32などとは違って、ADCが使えるピンが固定されています。CH32V003だと8チャンネル、それ以外は16チャンネルのピンが利用可能です。また、GPIOのピン以外にも内部にある温度計と、基準電圧のVREFに接続できるチップもありました。

CH32V103

上記がCH32V103ですが一番下にADCがあります。外部からAIN0から15までの16チャンネルあり、それ以外にVIN17のVREFとAIN16の温度計があります。

CH32V307

上記がCH32V307です。ADCが1と2の2つあるのがわかります。ESP32の場合には1ユニットに接続できる上限があり、それ以上のGPIOは2に接続されていましたがCH32Vだと違いました。同時に利用可能なADCが2つあるかになります。ADCの値で割り込みなどを設定可能ですのでスナップショットはADC1で、割り込みはADC2でみたいな利用なのかなと想定しています。

今回は標準的なArduino Core APIを実装していますのでADC1固定で作ってみたいと思います。

EVTを利用してADCの実験

CH32V103/EVT/EXAM/ADC/Temperature_External_channel/User/main.c at main · ch32-riscv-ug/CH32V103
Contribute to ch32-riscv-ug/CH32V103 development by creating an account on GitHub.

単純にADCから値を持ってくる例があればいいのですが、EVTにはありません。参考にしたのは上記の例になります。「Temperature_External_channel」で内部の温度計と外部のGPIOから値を取得しているものです。

温度計の部分を消すことで通常のADCとして動かすことが可能でした。

Arduino Coreへの実装

pinMode関数

CH32VではGPIOをGPIO_Mode_AINに設定することでADCモードに変更できます。ただし、ESP32は独自拡張されていたのでANALOGがありましたが、標準的なArduino Core APIのpinMode関数ではANALOGはありません。analogRead関数内で初期化されていない場合には初期化してから呼び出す設計になっています。

ただし、CH32VではGPIOの有効化が結構面倒なのでなるべくpinMode関数で初期化をしたいと思います。

#define CH32_PIN_MODE_ANALOG ((PinMode)100)

INPUTなどのenumはArduino Core APIの共通定義にあるので変更したくなく、上記のように無理やり拡張をしてみました。この値はユーザーには公開しなく、内部で利用する初期化用の専用値となります。

GPIOとADCチャンネルとの対応

/*
 *  ch32_pin_to_adc
 *  Description: Convert pin to ADC channel
 *  https://ch32-riscv-ug.github.io/CH32V003/datasheet_en/CH32V003DS0.PDF#page=15
 */
uint8_t ch32_pin_to_adc(uint8_t pin)
{
    switch (pin)
    {
    case PA2:
        return CH32_ADC1_0;
    case PA1:
        return CH32_ADC1_1;
    case PC4:
        return CH32_ADC1_2;
    case PD2:
        return CH32_ADC1_3;
    case PD3:
        return CH32_ADC1_4;
    case PD5:
        return CH32_ADC1_5;
    case PD6:
        return CH32_ADC1_6;
    case PD4:
        return CH32_ADC1_7;

    default:
        return 0;
    }
}

いろいろ悩んだのですが、チップによって差があるのでport.cに上記のような関数を追加しました。コメントのところに引用元が記載されています。今回はGPIOとADCの対応表なのでPDFになっています。このリンクが他のチップへの移植の際に地味に便利です。

初期化

/*
 *  ch32_adc_init
 *  Description: Initialize the ADC
 *  https://github.com/ch32-riscv-ug/CH32L103/blob/main/EVT/EXAM/ADC/ADC_DMA/User/main.c#L34
 *  ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // Enable to Disable
 */
void ch32_adc_init(uint8_t adc_unit)
{
    if (adc_unit != CH32_ADC1)
    {
        // only ADC1
        return;
    }

    ADC_InitTypeDef ADC_InitStructure = {};

    RCC_PB2PeriphClockCmd(RCC_PB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div8);

    ADC_DeInit(ADC1);

    extern int16_t ch32_adc1_calibrattion_val;
    ch32_adc1_calibrattion_val = Get_CalibrationValue(ADC1);

    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // Enable to Disable
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1))
        ;
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1))
        ;
}

ここが地味に苦労しました。まず検証では「Temperature_External_channel」をベースに使っていましたが、この例は温度計を内蔵していないチップではありません。そのためどのチップでも存在している例を探す必要があり「ADC_DMA」を採用しました。ただ、DMAではスナップショットの測定ではなく、継続測定をするモードになっていましたので、そこだけ毎回手で書き換える必要がありました。ADC_ContinuousConvModeのところです。

また、チップごとの差が結構あり、上記はCH32L103なのですがキャリブレーションのリセットをする前にキャリブレーション値を取得しています。他のチップと初期化手順が違ったので少し苦戦しました。

ただし、上記のような参考にしたコード例へのリンクがあるので開いてすぐに差分を確認できるので非常に便利でした。とりあえずベースとなるものを作ったら、そのコードをコピーして他のチップにはりつけてURLを更新することで移植しやすい作りにできました。とはいえ現在でも6種類もチップがあるので検証作業が一番面倒です。

Errata

  • Channel 3, channel 7, channel 11, channel 15 and I2C functions of the ADC are not available for products with a lot number with a penultimate 5 digit of 0;

CH32X035のデータシートを読むと上記のようなNoteがありました。ロット番号の5桁目が0だとADCの3(PA3)、7(PA7)、11(PC1)、15(VREFINT)とI2Cが使えないとあります。これは結構はまりそうなので動かないときにはロット番号も確認する必要があります。

EVTのサンプル実行

ADCの作業が終わってから作業したのですが、最新版の環境ではEVTのスケッチ例を取り込んで動かすことができるようにしました。仕組みとしてはEVTのフォルダ構造からスケッチ例の構造にソースを入れ替えて、ダミーのinoファイルを生成しています。noneOSでもArduino版でも既存のmain関数はweakで宣言しているので、EVTのmain関数で上書きしてnoneOS扱いの動きとなります。

上記のように最新EVTのスケッチ例が入っています。ただしFreeRTOSなどのRTOS系はフォルダ構成が複雑になっていて、Arduiono IDE上で動かすことができずに抜いてあります。

GitHub - ch32-riscv-ug/arduino_core_ch32_riscv_noneos: CH32 Risc-V noneOS for Arduino IDE (not ArduinoAPI)
CH32 Risc-V noneOS for Arduino IDE (not ArduinoAPI) - ch32-riscv-ug/arduino_core_ch32_riscv_noneos

noneOS版はArduino IDEでEVTをそのまま動かすための環境としては今一番おすすめかもしれません。Arduio IDEでボード定義を追加しただけでスケッチ例にEVT関連がずらっとはいっています。

GitHub - ch32-riscv-ug/arduino_core_ch32_riscv_arduino: CH32 Risc-V for Arduino IDE
CH32 Risc-V for Arduino IDE. Contribute to ch32-riscv-ug/arduino_core_ch32_riscv_arduino development by creating an acco...

上記のArduino版はまだまだArduino関数が足りないのですが、とりあえずEVTがそのまま動きます。noneOSにArduino系を足しただけのパッケージ違いなので統一してもよいのですが統合するとArduino関連でだけ変更があってバージョンアップされるのが邪魔かなと思い分離してあります。

Arduino版でもEVTをそのまま動かす場合にはmain関数から上書きして動いているのでnoneOSとの違いはないはずです。

まとめ

ADCは読み取り部分は共通化されていて素直なのですが、初期化部分はチップごとの差がでているので実機で実際に電圧を変化させながら正しい値が取得できるのかを確認しつつ実験をする必要がありました。

現在はI2Cを実験中ですがかなり苦戦しています。とりあえずスキャンまではできたのですが、実際の通信はこれからになります。

また、EVTのスケッチ例がArduino IDEでそのまま動くようになったのはかなり便利でして、なるべくMounRiverをつかない環境を準備していきたいと思っています。

コメント