編集履歴一覧に戻る
uchanのアイコン画像

uchan が 2023年07月02日09時17分58秒 に編集

初版

タイトルの変更

+

CH32V003 マイコンの PA1 や PA2 が使えないときに確認すること

タグの変更

+

CH32V

+

CH32V003

+

MounRiver

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

セットアップや使用方法

本文の変更

+

# CH32V003 マイコンの PA1 や PA2 が使えないときに確認すること CH32V003J4M6 は[秋月電子でたった 40 円で買える](https://akizukidenshi.com/catalog/g/gI-18062/)、STM32 風の周辺機能を持ったマイコンです。このプログラムを開発していたところ、PA1 や PA2 から信号が出せないことに気付いたので、原因を探求しました。本記事では原因、回避策、原因調査の詳細を説明します。 前提として [MounRiver Studio](http://www.mounriver.com/) を使って開発しているとします。初期化を自動でやってくれる Arduino IDE などでの開発では問題にならないと思います。[cnlohr/ch32v003fun](https://github.com/cnlohr/ch32v003fun) などを使った、自身で初期化コードを書く必要がある開発環境では参考になるかもしれません。 ## 回避策 今回発見した原因に対する回避策を端的にまとめると次の通りです。 1. GPIOA へクロックを供給する - `RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);` などと、GPIOA にクロックを供給する必要があります。 2. 内蔵クロックを用いるように設定する - system_ch32v00x.c を編集し、`xxx_HSI` が `#define` されるようにします。 回避策 1 は気付きやすいですが、回避策 2 は自動生成コードの話なので気付きにくいでしょう。 ### 回避策 1 について CH32V は STM32 と周辺機能が非常に似ており、使いたい周辺機能に対してクロックを有効にする必要がある点も同じです。GPIOA や GPIOD、TIM1 など、周辺機能ごとに `RCC_xxx` という関数でクロックを供給します。 関数 `RCC_APB2PeriphClockCmd()` については [RCC_APB2PeriphClockCmd - STM32 Memo](https://www.minokasago.org/STM32wiki/?RCC_APB2PeriphClockCmd) に簡単な説明がありました。詳しい説明は、この記事に書いてある通り[「STM32マイコン徹底入門」](https://www.amazon.co.jp/STM32%E3%83%9E%E3%82%A4%E3%82%B3%E3%83%B3%E5%BE%B9%E5%BA%95%E5%85%A5%E9%96%80-TECH-Processor-%E5%B7%9D%E5%86%85-%E5%BA%B7%E9%9B%84/dp/4789849864?&linkCode=ll1&tag=uchannos-22&linkId=01a35d4f2d833c797115c3b3068e82a3&language=ja_JP&ref_=as_li_ss_tl)の p.72 に記載されています。筆者はこの書籍を買ってみましたが、詳しく書かれていて参考になりますね。 ### 回避策 2 について GPIOA にクロックを供給してもなお、PA1 や PA2 から信号が出てこないなあ、と思うときに確認すべきは、AFIO_PCFR1 レジスタの PA12_RM ビットの設定です。 CH32V003 において、PA1 と PA2 は外部クロックを供給するための端子と共有されています。それらの端子を GPIO として使うかどうかを決めるのが上記のビットです。たとえ **外部クロックを接続していない** としても、自動生成された初期化コードが、外部クロックを使おうとして設定を変えているかもしれません。 CH32V003 のリファレンスマニュアルによれば、このビットは PA1 と PA2 の有効/無効を設定します[^pa12rm]。0 にすると、PA1/2 ピンは GPIO や周辺機能(multiplexed function)で使えるようになります。1 にすると、PA1/2 ピンには機能は割り当てられず、外部クロック端子になります。リセット後は 0 に初期化されます。 [^pa12rm]: ビット名的には PA1&2 remapping なのですが、実態は remap ではない気がします。 リセット後は 0 に初期化されるはずのビットですが、main 関数の適当なところでブレークして値を確認すると、PA12_RM が 1 になっていることが分かりました。自分では何も設定変更していないはずなのに 1 になっているという場合、考えられるのは MounRiver Studio が自動生成した初期化コードが設定を変えていることです。筆者の環境では、自動生成されたコードはデフォルトで外部クロックを用いるようになっていて、初期化処理により PA12_RM=1 に設定されていました。 問題の自動生成コードは User/system_ch32v00x.c です。このファイルの先頭付近に次のような `#define` の羅列があります。 ``` //#define SYSCLK_FREQ_8MHz_HSI 8000000 //#define SYSCLK_FREQ_24MHZ_HSI HSI_VALUE //#define SYSCLK_FREQ_48MHZ_HSI 48000000 //#define SYSCLK_FREQ_8MHz_HSE 8000000 //#define SYSCLK_FREQ_24MHz_HSE HSE_VALUE #define SYSCLK_FREQ_48MHz_HSE 48000000 ``` 筆者の環境では、デフォルトで `SYSCLK_FREQ_48MHz_HSE` が有効になっていました。内蔵クロックを使う場合、コメントアウトする行を変更し、`xxx_HSI` の定義を有効化すると良いです。HSI は High Speed Internal の略で、高速内蔵クロックを表します。`xxx_HSE` はいずれも高速外部クロック(High Speed External)用の設定です。 ### 外部クロックが無いのに動作する理由 CH32V003 は、電源投入直後は内蔵 24MHz クロックで動作するようです。ソフトウェアからクロック設定を変更すると、それ以降はそのクロックで動作する仕組みです。筆者は外部クロックを繋がない状態で動作実験をしていました。 ここで疑問なのが、なぜこれで動いているのかです。デフォルトで `SYSCLK_FREQ_48MHz_HSE` が定義されているならば、外部クロックを使うように設定が変わった瞬間に動作しなくなると思うのです。この理由は初期化コードにありました。 `SYSCLK_FREQ_48MHz_HSE` が定義されている場合、クロックの初期化処理は関数 `SetSysClockTo_48MHz_HSE()` が担当します。この関数は多くの処理をやっていますが、本記事に関わるところの処理を書き出すと次の通りです。 1. PCFR1 レジスタの PA12_RM ビットをセット 2. HSE クロックを有効化する 3. HSE クロックが安定するまで待つ - タイムアウトまでに安定すれば、HSE クロックに切り替える - タイムアウトまでに安定しなければ、内蔵クロックのままにする 2 の時点では、外部クロックを有効化するだけで、マイコンが使うクロックは内蔵クロックのままです。PA1&2 に接続されているのが水晶発振子のようなものであれば、発振が安定するまでしばらく時間がかかります。 そこで 3 の処理があります。問答無用に外部クロックへ切り替えるのではなく、外部クロックが安定することを確認してから切り替える処理が組み込まれています。外部クロックが安定したかどうかは RCC_CTLR レジスタの HSERDY ビットで調べるようです。このビットが 0 の間は外部クロックが安定していないことを示します。 今回のように外部クロックを接続していない場合、HSERDY が 1 になることはありませんので、しばらくするとタイムアウトします。そうすると、HSE クロックへは切り替わらず、そのまま `SetSysClockTo_48MHz_HSE()` の処理が終わります。そのため、マイコンは内蔵クロックで動作を続けることになります。 以上が、外部クロックが無い状態で `SYSCLK_FREQ_48MHz_HSE` が定義されていても、マイコンがそれなりに動作を続けていた理由でした。ただし PCFR.PA12_RM やその他レジスタの設定変更はそのままの状態で関数が終わるため、本記事の主題である PA1 や PA2 が上手く動かないという状態になってしまうのですね。 ## 情報源 CH32V003 マイコンのメーカー(Nanjing Qinheng Microelectronics)は中国の会社で、データシートやリファレンスマニュアルの原本は中国語です。ただ、公式の英訳バージョンも出ていますので、中国語を読めない場合はそちらを読むのが良いでしょう(筆者も英語版しか読んでいない)。ただ、翻訳の品質はそれほど高くないようで、用語がばらばらだったりして、読むのに少し苦労します。 - 英語版データシート [CH32V003DS0](http://www.wch-ic.com/downloads/CH32V003DS0_PDF.html) - 英語版リファレンスマニュアル [CH32V003RM](http://www.wch-ic.com/downloads/CH32V003RM_PDF.html) CH32V003 の周辺機能(ペリフェラル)は、レジスタ構造やビット配置を含め STM32 と非常に似ています。そのため、公式のリファレンスマニュアルの説明がよく分からないときは、STM32 のリファレンスマニュアルを参照すると良いかもしれません。筆者は CH32V003 の開発をするとき、[RM0451](https://www.stmcu.jp/design/document/reference_manual/63378/) を一緒に開いておくことが多いです。 MounRiver Studio に含まれるライブラリ群は、STM32 の SPL(Standard Peripheral Library)とほぼ同じ関数名やマクロ名になっているようです。SPL は、レジスタを介した初期化処理や機能設定などを関数に包み、レジスタの設定方法に熟知していなくても正しく扱えるよう、サポートしてくれるライブラリです。軽く調べたところ、どうやら 2014 年に出た新しいライブラリ(HAL)が現在の主流で、SPL は古く、メーカーにより非推奨(将来的にサポートされなくなる)になっているようです。 ですが、CH32V の公式開発環境である MounRiver Studio は SPL 風のライブラリを内蔵しており、自動生成されるコードもその API を使用するコードになっています。ですので、STM32 を解説した新しい書籍や Web ページはあまり参考になりません。SPL を前提に書かれた古い情報源が欲しいところです。[「STM32マイコン徹底入門」](https://www.amazon.co.jp/STM32%E3%83%9E%E3%82%A4%E3%82%B3%E3%83%B3%E5%BE%B9%E5%BA%95%E5%85%A5%E9%96%80-TECH-Processor-%E5%B7%9D%E5%86%85-%E5%BA%B7%E9%9B%84/dp/4789849864?&linkCode=ll1&tag=uchannos-22&linkId=01a35d4f2d833c797115c3b3068e82a3&language=ja_JP&ref_=as_li_ss_tl)は HAL がまだ登場していない 2010 年に初版が出た本ですので、まさにうってつけの書籍です。幸い、本記事執筆時点で絶版になっておらず、簡単に買えました。 ## 原因究明の詳細 ここからは、筆者が原因を見つけるまでの軌跡を紹介します。回避策を知りたいだけであれば読む必要は全くありません。CH32V のデバッグ方法の参考になるかと思い、書きました。 ### 変な信号が出る! 始めに PA2 の動作がおかしいことに気付いたのは、とある開発プロジェクトで TIM1 と DMA を組み合わせた開発をしているときでした。DMA の割り込み処理時間が規定以下であるかを確認するため、割り込み処理の最初で PA2=1 にし、最後で PA2=0 にする処理を書きました。PA2=1 になっている時間が、ほぼ割り込み処理の時間と一致するはずです。 PA2 の出力をロジックアナライザで見てみたところ、まったく意図しない信号が出ていました。TIM1 の CH4 の出力が反転したような波形(相補信号)だったのです。可能性として、TIM1 の CH4N が PA2 の端子(ピン 3)から出ているかもしれません。しかし、それはあり得ません。PA2 の代替機能として TIM1 の CH2N は割り当たっていますが、CH4N は割り当たっていません。しかも、CH4N は出力しない設定にしているはずです。それに、完全に反転した波形かというとそうでもなさそうです。 ![PA2(CH1)から相補信号のような波形が出ている](https://camo.elchika.com/068e0573e14824bb5b4809e2216a7a2373bb2952/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f62346165393463662d643438612d346636372d613565332d346533363635623266343865/) ロジックアナライザの CH0 が TIM1 CH4 の信号、CH1 が PA2 です。時間軸ですが、CH0 のパルスの短い方が 335ns、長い方が 630ns、周期は 1.33μs 程度のかなり速い信号です。完全な相補信号かというとそうではなく、パルス幅などがかなり異なっています。 ### 検証用コード 原因を究明するために、MounRiver Studio で新規プロジェクトを作成し、最低限の動作確認コードを記述しました。 ```c #include "debug.h" #define PORT GPIOD #define PIN GPIO_Pin_6 void InitGPIO() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOD, ENABLE); GPIO_InitTypeDef gpio_init = { .GPIO_Pin = PIN, .GPIO_Mode = GPIO_Mode_Out_PP, .GPIO_Speed = GPIO_Speed_50MHz, }; GPIO_Init(PORT, &gpio_init); } void main() { Delay_Init(); InitGPIO(); while (1) { GPIO_WriteBit(PORT, PIN, 1); Delay_Us(50); GPIO_WriteBit(PORT, PIN, 0); Delay_Us(20); } } ``` このコードでは、PD6 を出力に設定しています。PA1 や PA2 はデフォルトのままなので、入力になっているはずです。これを動作させてみると、PD6 と PA2 はほぼ正確に反転したような信号が出ていることが分かりました。パルス幅が 50μs 程度で、先ほどと比べて遅い信号なので、信号の遅延が気にならない程度に綺麗な相補信号になったのでしょうか。この現象は PA2 を出力モードに設定しても発生しました。 ここで、次のような疑問が浮かびました。PD6 を入力に設定し、外部から信号を入力したら、PA2 から相補信号が出るのだろうか。検証してみたところ、見事(?)PA2 から相補信号が出てきました。 ### 波形をアナログ的に確認 このあたりで [@ciniml](https://twitter.com/ciniml) さんにアドバイスいただき、波形をオシロスコープで確認してみることになりました。そこで観測した波形が下図です。 ![PD6=Ch1に入力した信号の相補信号がPA2=Ch2から出る](https://camo.elchika.com/b47f7c342cc973ac68181926df291da846847230/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f66386364343539642d663266662d343739352d626430662d383731383766643832323634/) PD6 に 5KHz の矩形波を入力しています。Ch1(橙)が PD6 に入力された信号、Ch2(青)が PA2 から出力された信号の波形です。入力信号は 4Vp-p の矩形波ですが、PA2 から出力される信号は 1.5Vp-p 程度の信号になっています。PA2 から出る相補信号は GPIO の出力なので 5Vp-p だろうと思い込んでいたのですが、全く違うことが分かりました。 ### PA12_RMビット チップの故障を疑い、予備の CH32V003J4M6 でも実験しましたが、同じ現象が観測されました。したがって、チップの故障ではなく、設定に間違いがある可能性が高まりました。そこで、リファレンスマニュアルを「PA2」をキーワードによく調べたところ、AFIO_PCFR1 レジスタに PA12_RM ビットを発見しました(このビットの説明は本記事冒頭に書きました)。 このビットはリセット後に 0 になるはずなので、自分のプログラムでも当然 0 になっているだろうと考えていました。しかし、念のため実際の値を確認することにしました。デバッガ(WCH-LinkE)を用いて `main` 関数の最初の方でブレークし、このビットの値を確認しました。 すると、なんと 1 になっていることが分かりました。この時点で、原因はほぼこれだと確信しました。試しに `main` 関数の先頭付近で `AFIO->PCFR1 &= 0xFFFF7FFF;` を実行し、PA12_RM を 0 に設定する修正を加えてみました。すると、PA2 から相補信号は出なくなり、普通の GPIO の挙動になりました。 ### 真犯人 PA2 から相補信号が出る原因が PA12_RM ビットが 1 であることだと判明しました。そして、`main` 関数の先頭で PA12_RM を 0 にするというワークアラウンドの動作も確認できました。でもこのままでは不十分です。このビットが 1 になった原因は何なのか。1 にならないようにするには、どうすればいいのか。それを探して、今回の探求は一段落となります。 ヒントになるのは、`main` 関数の先頭でブレークした時点で PA12_RM ビットが 1 になっていることです。ということは、`main` 関数に来る前に原因があるはずです。そこで、ソースコードを PCRF1 というレジスタ名で検索してみると、User/system_ch32v00x.c の中で PA12_RM ビットを 1 にしている行を発見しました。当該部分のコードを掲載します。 ```c static void SetSysClockTo_48MHz_HSE(void) { __IO uint32_t StartUpCounter = 0, HSEStatus = 0; /* Close PA0-PA1 GPIO function */ RCC->APB2PCENR |= RCC_AFIOEN; AFIO->PCFR1 |= (1<<15); RCC->CTLR |= ((uint32_t)RCC_HSEON); ``` `SetSysClockTo_48MHz_HSE()` は、PA12_RM ビットをセットしている他に、RCC_CTRL の HSEON ビットもセットしていることも分かります。これは外部クロックを使うための設定です。今回は外部クロックは接続していないので、HSEON をセットする意味はありません。 この関数が原因でしょうか?どこからこの関数が呼ばれるかを辿っていくと、最終的に `SystemInit()` という関数から呼ばれていることが判明しました。`SystemInit()` は、Startup/startup_ch32v00x.S の中から `main` の前に呼ばれます。ということで、これが真犯人で間違いなさそうです。 最後に、問題の正しい対処方法を探します。幸いなことに、この関数が定義されたファイルの先頭付近にすぐヒントがありました。`xxx_HSI` や `xxx_HSE` というマクロの定義が並んでいたのです。デフォルトでは `#define SYSCLK_FREQ_48MHz_HSE` の行だけが有効になっており、他のマクロ定義はすべてコメントアウトされています。付近のコメントを見ると、希望するクロックに対応する定義のコメントを外せ、とあります。 そこで `#define SYSCLK_FREQ_48MHZ_HSI` のコメントを外したところ、`main` の先頭に加えた `AFIO->PCFR1 &= 0xFFFF7FFF;` を削除しても正しく動くようになりました。念のため、`SYSCLK_FREQ_48MHZ_HSI` が定義されたときに実行されるクロック設定コードを追いかけてみると、`AFIO_PCFR1` への書き込みは無く、PA12_RM ビットが変更されないコードであることが確認できました。めでたし、めでたし。