ch32v003funでCH32V003を便利に開発している話

tl;dr

  • ch32v003fun は、コミュニティの CH32V003 用の開発ライブラリ、および開発環境だよ。
  • 開発ライブラリと言っても、基本的にリファレンスマニュアル見ながらレジスタを操作するもので、各ペリフェラルを使いやすくするようなものではないよ。
  • でも examples が豊富で、やりたいことの example があればコピペで動かすことができるよ。
  • GPIO、NeoPixel用のライブラリもあるよ。
  • WCH-LinkE の SWDIO 経由でプリントデバッグできるのが割と便利だよ。

CH32V003 とは

CH32V は STM32 のペリフェラルRISC-V に移植したような MCU です。CH32V003 はその中でも 40 円から買えて安い上に、GPIO x18、ADC x8ch、Timer x4、UART、I2C、SPI など豊富なペリフェラルがあります。5V 電源でも使えるため、100n、1u の 2 つのコンデンサを組み込めば動いているように思っています。

akizukidenshi.com

私は Aliexpress 上の公式ショップで、50pcs 単位で購入しています。すると送料が USB 5 ほどかかりますが、1 個あたり USB 0.1~0.12 くらいになります。

50Pcs/Lot CH32V003 Industrial-grade MCU, RISC-V2A, Single-wire Serial Debug Interface, System Frequency 48MHz - AliExpress

https://www.aliexpress.com/item/1005005036714708.html

私にとっては、制御したいならとりあえず組み込んどけ、といえるマイコンになっています。既にいかに組み込んだことを記事にしました。

74th.hateblo.jp

74th.hateblo.jp

ch32v003fun とは

CH32V003 の開発環境、ライブラリには、現在 3 種類あります。

公式 SDK は、開発環境に MountReverStudio を使う必要があり、さらに容量が大きいため、16kB しかない CH32V003 には少し辛いです。Arduino はまだサポートされているペリフェラルが少ないです。

一方 ch32v003fun は、コミュニティでメンテナンスされている開発ライブラリ、開発環境です。以前の記事でも紹介しましたが、改めて書くと以下となります。

  • 基本的にマクロで実装されているため、プログラムの容量のオーバーヘッドが少ない。
  • ペリフェラルを抽象化するようなものではなく、直接レジスタを操作することが基本である。
  • ビルドするための Makefile、WCH-LinkE 経由で書き込むためのプログラム(minichlink)も提供されている。
  • 直接レジスタを操作する実装例が多く、ペリフェラルの操作自体はコピペで動くことが多い。
  • SWDIO 経由で print デバッグができたり、デバッグ実行できたり、開発を便利にする機能が用意されている。

つまり、MCU のリファレンスマニュアルから使い方を覚えて操作する、というのが基本です。リファレンスマニュアルは以下からダウンロードできます。

CH32V003リファレンスマニュアル

www.wch-ic.com

開発の始め方

ch32v003fun のための gcc などのツールチェインの準備方法はリポジトリwiki を参照してください。

https://github.com/cnlohr/ch32v003fun/wiki/Installation

WCH-LinkE を通して書き込むためのプログラム minichlink は、ch32v003fun リポジトリ内の minichlink ディレクトリでmakeこまんどを実行することで作成されます。

まず、ch32v003fun リポジトリ内にあるライブラリのディレクトリ(ch32v003fun)をワークスペースにコピーしてきます。

さらに Ch32v003fun リポジトリ内の examples/template の中身をコピーしてきます。

template.c、template.h のファイル名を作りたいプログラム名(例えば usb_rebooter.c)に変更します。

Makefile を編集します。

  • TARGET にプログラム名に変更する
  • CH32V003FUN を、ch32v003fun のコピーしたライブラリのディレクトリを指定する
  • MINICHLINK にビルドした minichlink のパスを指定する(私の場合は、PATH の通ったところにコピー済み)
  • includeをコピーした ch32v003fun 内の ch32v003fun.mk に変更する

変更前

all : flash

TARGET:=template
CH32V003FUN:=../../ch32v003fun

include ../../ch32v003fun/ch32v003fun.mk

flash : cv_flash
clean : cv_clean

変更後

all : flash

CH32V003FUN=./ch32v003fun
MINICHLINK=minichlink
TARGET:=usb_rebooter

include ./ch32v003fun/ch32v003fun.mk

flash : cv_flash
clean : cv_clean

なお、コードの作成を ch32v003fun の examples ディレクトリ内に新しくディレクトリを作って行った場合、TARGET だけを書き換えれば良いです。

これで、コマンドmakeを実行すると、プログラム名の bin ファイル(ファームウェアイメージ)が作成され、さらに minichlink を使って書き込みまで行われます。

ch32v003fun でいつも最初にすること

SystemInit()関数を呼ぶ

まず ch32v003fun の SystemInit()関数を呼びます。template ディレクトリをコピーしたのであれば、既に実装されていると思います。ここでは主にメインクロックの設定がされています。

#include "ch32v003fun.h"
#include <stdio.h>

int main()
{
    SystemInit();
}

ペリフェラルを有効にする

次に、各ペリフェラルへのクロックの設定を行います。CH32V003 等のマイコンでは、ペリフェラル毎にクロックを接続するか否かを設定して、使わないペリフェラルへのクロックを提供しないことで電力を節約しています。

この設定はRCC->APB1PCENRRCC->APB2PCENRに行います。examples にあるコードの最初の方に、RCC->APB2PCENR |= ...という行が含まれていると思いますが、それです。

I2C など利用したいペリフェラルの examples のコードを見てみて、そのコードをコピーして使うと良いです。

私が I2C と GPIOA、C、D を利用したい時には、以下のようにしていました。基本的に examples からコピーしています。

#include "ch32v003fun.h"
#include <stdio.h>

void init_rcc(void)
{
    RCC->APB2PCENR |= RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD;
    RCC->APB1PCENR |= RCC_APB1Periph_I2C1;
}

int main()
{
    SystemInit();
    init_rcc();
}

ペリフェラルの初期化をする

GPIO を含むペリフェラルの利用には、設定と、有効化が必要です。これらはレジスタに対して行います。

examples には、そのコード例が多く載っています。例えば、GPIO D0 をアウトプットにしたい場合、以下ようなコードをコピペします。

// GPIO D0 Push-Pull
GPIOD->CFGLR &= ~(0xf<<(4*0));
GPIOD->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP)<<(4*0);

上記にある GPIO->CFGLR などの仕様はリファレンスマニュアルを見るほかありません。

しかし、利用したいペリフェラルのサンプルが examples にあるならばそれをコピーしてこれば動くようになります。実際に動いたコードであるため、動く間に必要な設定が足りていないということがないのがありがたいです。さらに、コード中には多くのコメントが残されており、そこで何をしようとしているのか読み解きやすくなっています。

くわえて、レジスタを直接操作しているため、ch32v003fun のアップデートで使えなくなることが基本的にないといえるのもありがたいです。

以上が初期化で、あとは処理の実装

以上初期化の部分を説明してきました。

その後の GPIO などの操作も、レジスタを操作することを基本としているため、リファレンスマニュアルを読んで、examples から動くコードを理解して、機能を実装していきます。

最初は抽象化されていないので扱いにくく感じますが、処理自体は高速ですし、リファレンスマニュアル以上のブラックボックスはないので、気に入っています。

というか、公式 SDK のサンプルプログラムが若干質が低い(動かないことがあるし、PR を受け付ける体制ではなさそう)のに比べると、多くの人が触っているため安心感があり、これで良いなーと感じています。

ch32v003fun のよいところとして、printf で出力した内容を WCH-LinkE の SWDIO を通して見ることができる機能が含まれています。これはデフォルトで有効になっており、WCH-LinkE を接続の上、以下のコマンドで表示することができます。

minichlink -T

SOP-8 の CH32V003J4M6 では UART TX と SWDIO のピンが共通のため、UART TX でプリントデバッグしようとすると SWDIO で書き込めなくなる罠があるなか、SWDIO1 つでプリントデバッグできるのはありがたいですね。

なお、UART TX で printf で出力することもできるように funconfig.h を設定することもできます。

ch32v003fun の便利ライブラリ

ch32v003fun にはいくつかの便利なライブラリが含まれています。あくまで、ch32v003fun 本体ではなく、extralibs という位置づけです。

GPIO の操作

GPIO の初期化と、read、write を簡単にできるようにするライブラリがあります。

https://github.com/cnlohr/ch32v003fun/blob/master/extralibs/ch32v003_GPIO_branchless.h

このヘッダーファイルを ch32v003fun の中に入れておきます。

GPIO の初期化は以下のように記述できるようになります。

#include "ch32v003_GPIO_branchless.h"

#define LED_PIN GPIOv_from_PORT_PIN(GPIO_port_D, 4)
#define SELECT_U1_PIN GPIOv_from_PORT_PIN(GPIO_port_A, 1)
#define SELECT_U2_PIN GPIOv_from_PORT_PIN(GPIO_port_A, 2)
#define SELECT_U3_PIN GPIOv_from_PORT_PIN(GPIO_port_D, 0)
#define SELECT_U4_PIN GPIOv_from_PORT_PIN(GPIO_port_C, 0)
#define BTN1_PIN GPIOv_from_PORT_PIN(GPIO_port_D, 2)
#define BTN2_PIN GPIOv_from_PORT_PIN(GPIO_port_C, 7)
#define BTN3_PIN GPIOv_from_PORT_PIN(GPIO_port_C, 6)
#define BTN4_PIN GPIOv_from_PORT_PIN(GPIO_port_C, 5)

void init_gpio()
{
    // 各GPIOの有効化
    GPIO_port_enable(GPIO_port_A);
    GPIO_port_enable(GPIO_port_C);
    GPIO_port_enable(GPIO_port_D);
    // 各ピンの設定
    GPIO_pinMode(LED_PIN, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz);
    GPIO_pinMode(SELECT_U1_PIN, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz);
    GPIO_pinMode(SELECT_U2_PIN, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz);
    GPIO_pinMode(SELECT_U3_PIN, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz);
    GPIO_pinMode(SELECT_U4_PIN, GPIO_pinMode_O_pushPull, GPIO_Speed_10MHz);
    GPIO_pinMode(BTN1_PIN, GPIO_pinMode_I_pullDown, GPIO_Speed_10MHz);
    GPIO_pinMode(BTN2_PIN, GPIO_pinMode_I_pullDown, GPIO_Speed_10MHz);
    GPIO_pinMode(BTN3_PIN, GPIO_pinMode_I_pullDown, GPIO_Speed_10MHz);
    GPIO_pinMode(BTN4_PIN, GPIO_pinMode_I_pullDown, GPIO_Speed_10MHz);
}

そして、操作するコードは以下のようになります。

if (GPIO_digitalRead(BTN1_PIN) == high)
{
    GPIO_digitalWrite(SELECT_U1_PIN, high);
    GPIO_digitalWrite(SELECT_U2_PIN, low);
    GPIO_digitalWrite(SELECT_U3_PIN, low);
    GPIO_digitalWrite(SELECT_U4_PIN, low);
}

レジスタを扱わずに、Arduino と名称を同じようにしているため、簡単に操作できるようになります。

ただし、この関数自体は、関数ではなくマクロで実装されています。そのため、引数に変数を入れようとすると動かなかったりしますので、注意が必要です。

NeoPixel、WS2812B の点灯

NeoPixel は SPI を使うと簡単に制御できることが知られています。そのための、SPI および DMA の設定をまとめてあるのが、以下のヘッダーファイルになります。

https://github.com/cnlohr/ch32v003fun/blob/master/extralibs/ws2812b_dma_spi_led_driver.h

使い方は、example を参照してください。

https://github.com/cnlohr/ch32v003fun/tree/8b05efb3d8254084cc1eabf40d54b32319cf1219/examples/ws2812bdemo

ちなみに、私の場合は CPU を 100%使ってやってしまえば良いと思い、以下のようなコードを利用して WS2812B を点灯させています。

https://github.com/74th/relay-switch-usbhub/blob/main/main-firmware/gpio_neopixel.h

funconfig.h は何に使う?

ch32v003fun に設定を施すためのファイル funconfig.h があります。しかし、基本的にいじる必要はありません。私の場合は、以下のケースで利用しています。

  • UART TX を printf の出力にしたい
  • 内蔵発振器のクロックを下げて、電力を節約したい

私は主に UART TX で printf で出力するために使っていましたが、SWDIO 経由でできることがわかり、あまり使わなくなりました。

使いにくいですが、使いやすくて使っています!

抽象化されておらずレジスタを直接取り扱うのは、やることが多く難しいです。 ですが、一切のミドルレイヤを挟まないため、リファレンスマニュアルだけを頼りにすればよく、CH32v003 だけを使うのであれば十分なように見えます。

そして、実際に動かすことができたコードが examples に集まっているため、まず動くコードを見てから自分でできることを確認しつつ実装していく、マイコン開発にはちょうど良いように感じています。

今後、CH32V003 開発を Rust や TinyGo 等で実装したくなったとしても、最終的にはレジスタ操作をすることには変わらないので、レジスタの使い方を ch32v003fun から学ぶことにもなりそうです。

CH32V003 を使ったキットを販売中

CH32V003をProMicro化した開発ボードをboothで販売しています。

74th.booth.pm

また、Relay Switch USB 2.0 Hubには、リレーの制御と、リモート操作盤の制御にCH32V003を使っています。

74th.booth.pm