コロナ禍の半導体不足の影響で PIC マイコンに纏わる価格が高くなった. そんな折, 俗に言う 秋月の40円マイコン (CH32V003J4M6) と呼ばれるものを見つける。 ライタ (WCH-LinkE エミュレータ)も750円となかなかお手軽. 表面実装な点が若干とっつきにくいが安さにはかなわない。 今後はこれを主軸に使っていこういろいろ調べてみた.
必要なソフトウェアは以下の二点.
WCH-LinkUtility はライタ (エミュレータ) のドライバ関連が入っている. 初回起動時にそれらがインストールされる. またこのライタは様々な IC に対応できる汎用的な作りになっているようで, 一回「お前は今後 CH32V003 用として働いてもらうからよろしく」といった 指示を行う必要がある. 具体的には Active WCH-Link Mode を WCH-LinkRV にして Set ボタンを押す.
書き込みに必要なのは以下の 3 ピン. CH32V003 にある残り 4 ピンや, ライタ側の残り 7 ピンは何も繋げなくてよい.
大体 MPLAB-IDE 等の他の IDE と同じ.
といった感じ.
Project Explore に興したプロジェクトに関するファイル情報を出る. 編集すべきファイルは User の下の system_ch32v00x.c と main.c のみ. 後はよく分からない. これらのファイルはプロジェクトを興すと雛形が自動生成されるが, 正直あまりよろしくない. いきなり試しにとコンパイルして焼いてしまうと痛い目に会うので 雛形を直接使うのでなく少し書き換えた方がよい (焼いてしまった場合は「シリアル通信」の項を参照).
書き換える部分は二点. 一点目は sytem_ch32v00x.c の冒頭部分で, 雛形は SYSCLK_FREQ_48MHz_HSE を定義しているが, その行はコメントアウトして SYSCLK_FREQ_48MHZ_HSI を定義する. 二点目は main.c. ここの変更点はかなりにわたるので, 以下のコードにごっちょり変えてしまう.
#include "debug.h" void Port_Init(void) { GPIO_InitTypeDef GPIO_InitStructure = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); } int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); SystemCoreClockUpdate(); Port_Init(); Delay_Init(); for(;;) { GPIO_WriteBit(GPIOC, GPIO_Pin_1, 1); Delay_Ms(250); GPIO_WriteBit(GPIOC, GPIO_Pin_1, 0); Delay_Ms(250); } } |
このコードは 5 番ピンは 2Hz で High/Low する. LED を付けると点滅するはずだが, PIC の感覚で電流流しすぎるとリセットがかかる. ほどほどに.
40 円マイコンのポートは次のようになっている (単純化のため若干ウソあり).
先のコードの for 文内では C1 を GPIO_WriteBit() で 1/0 にしていたので 5 番ピンが High/Low になっていた. 6 番ピン (PC2) も High/Low できるようにするには, GPIO_WriteBit() の第二引数を GPIO_Pin_2 にすればいい (第一引数は GPIOC) が, そのためにはまず PC2 を出力モードにしなければならない. このコードでそれをやるには Port_Init() 内の
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); |
というフレーズの最初の行を GPIO_Pin_1 | GPIO_Pin2 と書けば, 5 番ピンと 6 番ピンが両方出力モードになる.
一方, 入力モードにする別の宣言が必要で, 例えば PC4 を入力モードにしたいのなら, このフレーズの後にでも
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOC, &GPIO_InitStructure); |
と書く. 該当するポートの状態を読み取るには GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_4) みたいな感じで使う.
もし 3 番ピン (PA5) も出力として使いたいのなら RCC_APB2PeriphClockCmd() の第一引数を RCC_APB2Periph_GPIOC も or するとともに 上記三行を GPIOC に対しても行えばよい.
40 円マイコンの 3 番ピンは PA5 というデジタル入出力として使えるが, A0 というアナログ入力としても使える. このポートをアナログ入力にするには RCC_APB2PeriphClockCmd() の第一引数で RCC_APB2Periph_ADC1 も or で enable にした後, 次のようなフレーズで初期化する. 現段階では, 試行錯誤した結果とにかくこんな感じでうまく動いた というだけで, 理由は不明.
RCC_ADCCLKConfig(RCC_PCLK2_Div8); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitTypeDef ADC_InitStructure = {0}; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = 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)); |
AD 値の読み込みはこんな感じの関数で実現できる.
int ReadADC(void){ ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } |
データシートによると 3 番ピンは A0, 7 番ピンは A2 とも書いているので, 上手くチャンネル設定すれば 7 番ピンからもアナログ入力できるはずである. いろいろ調べた結果, 上記 ////// 部分に次のフレーズを加えれば なんかうまくいく (もちろん GPIO_Init() で適切に設定は必要).
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 1, ADC_SampleTime_241Cycles ); |
これもよく分かってないけど, main() 冒頭に以下のフレーズを入れ,
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); |
Init() 最後にこんなフレーズを追加したら上手くいった.
NVIC_EnableIRQ(SysTicK_IRQn); SysTick->SR =0; SysTick->CMP = (SystemCoreClock/100000)-1;// 10us SysTick->CNT = 0; SysTick->CTLR=0x0f; |
/100000 という部分がタイマ割込み周期. この例だと 10us 毎に割込みがかかる. SystemCoreClock は何も手を加えないと 48000000 になってるはずなので, 割り切れない数値を指定すると時間がずれるはず.
割込み処理は次のような感じで書く.
void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void SysTick_Handler(void) { SysTick->SR = 0; // 処理 } |
タイマ割込み使って何か計測しようとすると微妙に精度が悪い. クロックを内部発信しているのが原因なので, 24 MHz の水晶を外部につけるしかない (2 番ピンが GND なので配線は結構楽).
使い方は, sytem_ch32v00x.c の定義を SYSCLK_FREQ_48MHz_HSE に戻すだけ. 保険で main() 冒頭で SystemCoreClockUpdate() も入れてるけど効いてるかどうかは不明.
1 番ピンを Rx, 8 番ピンを Tx とすることを考える. WCH-LinkEエミュレータ はシリアル通信機能もあるので, 次のような感じで PC と通信してできる.
シリアル通信を実現するためには RCC_APB2PeriphClockCmd() の第一引数で RCC_APB2Periph_USART1 も or で enable にした後, 次のようなフレーズで初期化する.
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOD, ∓GPIO_InitStructure); USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); |
以後次のような関数でシリアル通信ができる.
void SendUART(char p){ while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, p); } int RecvUART(){ if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) return -1; return USART_ReceiveData(USART1); } |
ところでこのように初期化して 8 番ピンを Tx に設定すると, MounRiver Studio 単体ではプログラムを更新できない. プログラムを更新するには WCH-LinkUtility を用いて 40 円マイコンをまっさらな状態に戻さないといけない. 具体的にはターゲットを CH32V00X にして (これは一回やれば設定を覚えてくれる)
メニューから「Clear All Code Flash-by Power off」を選ぶ.
冒頭でプロジェクトを興した時に生成される雛形はお勧めしないと書いたのは, 雛形は Tx を使うため, プログラムを書き換えるには 毎回「Clear All Code Flash-by Power off」しなくてはならず, それに気づくまでなんか書き込めなくなったと悩むから.
8 番ピンを Tx 設定にしてない場合は MounRiver Studio 単体で プログラム更新ができる. その場合電源は基板上にあるレギュレータ等から供給してもかまわない. WCH-LinkUtility とは 2 番ピンと 8 番ピンをつなげるだけでプログラムの 書き換えができる. Tx 設定した場合はそれができない。 電源を WCH-LinkE エミュレータ経由で供給するようにして WCH-LinkUtility で「Clear All Code Flash-by Power off」する必要がある. しかしそれには 40 円マイコンの電源供給の系統を上手く切り替えなくては ならずなかなか面倒である.
MounRiver Studio 単体でプログラム更新ができないのは Tx 設定したことが原因で, プログラム起動直後の先のフレーズで実現されている. 言い換えるとそのフレーズが実行され設定されるまではプログラム更新ができる. つまり起動直後 5 秒くらいは Tx 設定せずに過ごし, その後 Tx 設定すれば電源ボタンを入れてすぐ「書き込み」をすれば 電源ウンヌンに悩まずプログラムが更新できるようになる.
CH32V003 シリーズには, 今回の 8pin 以外に 16pin とか 20pin のものがある. これらは PD7 に対応するピンが NRST に使われており, 何もしなければ PD7 が使えない. PD7 を使えるようにするには WCH-LinkUtility で 「Disable mul-func. PD7 is used for IO function」を選ぶ.
とはよくあちこちの WEB ページ書いてあるけど選んだあとどうすればよいかはわからない. とりあえず「Open Firmware」で適当な hex ファイルを開き, それを焼き処理するとなんとかなってる.
PIC マイコンの感覚だと型番が違うと ID が違ってて 例えば F4P4 用で興したプロジェクトで作ったものを, 間違って J4M6 に書き込もうと すると「対象のデバイスじゃない」と怒られそうだけどそういうことは起こらない. 起こるものだ, 起こらないということはプロジェクトが間違ってる可能性は排除してよいと考えると失敗する.
なんか世の中にはchv003funなる組織があって, こっちの方がシンプルなコードができそうという噂 である. MounRiver Studio は ESP32 とコードをできるだけ合わせるためいろいろ無理しているらしいく, 逆に言えば ESP32 系の情報現をたどってもほぼほぼ流量できる. 当方は適当な段階で chv003fun に移住しようかなと考えつつ忙しさにかまけて実現できてない.
それと Arduino IDEを用いた開発環境もある. こっちは個人的にちょっと好みじゃないのであまり調べてないや, 結構頑張ってる人もいる.