概要
CH552 を使ったテンキーパッドを作った話の続きで、今回は、CH55xDuinoを使ったプログラミングやプログラムの中身の話など。
CH55xDuino について
CH55xDuinoは、CH55xシリーズマイコン向けのプログラム開発を、Arduino IDEを使ってArduinoのように行うためのコア・パッケージ (ボードサポートパッケージ) で、ArduinoのAPIの多くをカバーするcoreライブラリ、MCS51用のビルドツール (SDCC)、SDCC をArduino 開発環境でうまく扱うためのツールやスクリプト、USBデバイスとして使うときのサンプル (examples) などから構成されている。
CH55xDuino 0.0.18版を使ってみてArduinoとちょっと違うなと感じたのは、今のところC++ではないという点だけだった。
C++ではない
CH55xDuinoでのプログラミングと、他のAVRやARMコアのArduino プログラミングとの大きな違いは、C++ではなくC 言語で記述する必要があること。したがってclassやオブジェクトは使えない。同じ名前で引数の型や個数が異なる関数(オーバーロードされた関数)の呼び出しもできない。
たとえば、Arduinoでよく使う Serial.print(value); といった呼び出しは、まずSerialオブジェクトがないし、当然そのメンバ関数もないから、たとえば serial_print(value ); といった独立した関数API に置き換えたものを使う必要がある。
また、ふつうのArduinoでは、同じ名前の関数に与える引数の型(この例ではvalueの部分)が文字列だったり文字だったり数値だったりしても、オブジェクト側で適切に対応してくれるが、C言語では引数の型ごとに異なる関数APIが必要になる。たとえば serial_print_s (“string”)、 serial_print_c (charater) 、 serial_print_n(number) といった具合。
現在のCH55xDuinoでは、USBを使う仮想シリアルの USBSerial やハードウェアシリアル(Serial0, Serial1) への出力については、パラメータの型に応じた書き分けをしなくて済むように、generic selection という技法を導入している。これについては、テンキーパッドのキースキャンを試すのに書いた numkeyPad_test.ino で利用した。
導入
Arduino IDEへの導入は他のマイコン用のパッケージと同じで、ファイル/環境設定 にある 追加のボードマネージャのURL に以下のURLを入力する。
https://raw.githubusercontent.com/DeqingSun/ch55xduino/ch55xduino/package_ch55xduino_mcs51_index.json
そして、ツール/ボードマネージャで Ch55xduino を選択してインストールする。
上図のように、ボードマネージャの検索フィールドに ch と入力してやるとすぐに見つかる。
ターゲットの選択
今回のテンキーパッドはCH552Gを使っているので、ツール/ボード から CH55x Boards を選択し、更にCH552 Board を選択する。この時点でサポートされているマイコンは、CH552, CH551, CH559, CH554 (実験段階) の4種類だった。
ビルドオプション
ツールメニューのビルドオプションには以下の各項目がある。
USB Settings:
スケッチからUSB経由で文字や数字をシリアルモニタに出力するために仮想シリアルを使いたいときは Default CDC を指定する。”USBSerial_” で始まる仮想シリアル用APIを利用するためには、このオプションを選択しておく必要がある。Default CDC を指定した場合、ビルドに際して USER_USB_RAM マクロが定義されない。
USBをその他の用途(HIDなど)で使うときは、USBのEPバッファ用などのためにあらかじめ xRAMに確保しておくメモリサイズを指定するため USER CODE w/ 148B USB ram などを選択する。”USER CODE w/” で始まるオプションを選択しているときは、ビルド時に USER_USB_RAM マクロがオプションに含まれているバイト数を保持する (USER CODE w/ 148B USB ramならば、sdccのコマンドラインに -DUSER_USB_RAM=148 が追加される )。
xRAM内に配置されるスケッチ内のデータは指定バイト数の後ろから配置され、たとえば、 w/ 148B ならば 0x93 番地以降にスケッチ内のデータが配置されるようになっている。
Upload methd:
スケッチのアップロード方法として、USB または Serial を選択する。CDCによる仮想シリアル経由でアップロードする場合、Arduino IDEからはシリアルポートに見えていてもUSBを指定する。ハードウェアシリアル経由でアップロードするときには Serial を選択する(のだと思う)。
Clock Source:
内蔵クロックまたは外部クロック、および、おのおのでの動作周波数の指定。CH552Boardを選択して内蔵クロックを使う場合、24MHz, 16MHz, 12MHzが指定できる。
AVRのArduinoのときと同じく、選択したクロック周波数は sdccのコマンドラインに -DF_CPU=12000000L のように与えられる。
今回のビルドオプションと注意点など
USBSerialにスキャン結果を表示するテスト用スケッチ (numkeyPad_test.ino)では Default CDCを、HIDキーボードを作るための ch552_numkeyPad.inoでは USER CODE w/148B を指定してビルドした。
クロックについては、動作周波数が低いほど消費電流は減るがさほど劇的な効果はなかった。同じスケッチで、24MHz時に16mAだったのが12MHzのとき13mAとか。外部LDOを使ってCH552に+3.3Vを供給するようにしておくと話が違うのかも。
Default CDC オプションを指定したスケッチが動いているハードウェアについては、ふつうのArduinoのようにArduino IDEやVSCodeからアップロード操作を行うだけで、特にマイコン側を触ることなくプログラムを更新できる。
それに対して、USER CODE w/xxxx を指定したスケッチが動いている場合、アップロードの直前にマイコン側を操作 (USB DPを+3.3Vでプルアップして電源投入) することでブートローダーモードで立ち上げるてやる必要がある。最初のうちはブートローダーモードに入れるタイミングが分からず、ちょっと手間取った。
ブートローダーモードに入れる操作
今回作ったPCBでは、キーパッドの最北部にBOOT用やRESET用のスルーホールを用意し、ピンセットでショートさせて使うようにした。
まず、テンキーパッドのUSBケーブルをホスト側またはテンキー側から抜いておく。そしてこの写真のようにBOOTをショートさせると、回路上USB DP (UDP)は10kΩの抵抗を介して+3.3Vに接続される。
この状態でUSBケーブルを接続して電源を与えると、CH552はブートローダーモードで開始する。そのまま何もしないでいると、ブートローダーモードは5,6秒間で終了し、ユーザーが書いた(プログラムROM内の) プログラムあるいはスケッチが開始する。
Arduino IDEからスケッチをアップロードする際には、USBケーブルを抜き、ピンセットなどでBOOTスルーホールをショートさせ、あと数秒で vnproch55x によるHEXファイルの転送が始まるタイミングを見計らってUSBケーブルを接続して電源を与える (ブートローダーモードを開始する)。うまく転送できているかどうかは、ステータス画面で確認する。
BOOTのスルーホールをショートさせたままでも、弱いプルアップのためかスケッチのアップロードは問題なく進行する。
ブートローダーモードを視覚的に確認
CH552のGPIOピンはリセットによって内部でプルアップされ、ブートローダーモードのときもプルアップは維持されるようである。今回の回路では、P1.1ピンにNumLockインジケーター用のLEDを接続しているので、BOOTをショートさせた状態で電源を与えるとLEDが点灯し、数秒後に ブートローダーモードが終了してスケッチが走り出すとsetup()での処理でLEDが消灯する。これにより、ブートローダーモードの終了を視覚的にも確認することができている。LEDはロジックHIGHで点灯であること(回路については前回の投稿を参照)。
いちおう、裏面にもBOOTスイッチを実装できるようなパターンを用意したのだけど、ピンセットでつまんでやる方が操作しやすいのでハンダ付けするのをやめてしまった。
キースキャンしてコードを出力するスケッチ numkeyPad_test.ino
テンキーパッドの基板のテストのため、オンオフされたスイッチのコードを仮想シリアルに出力する、というスケッチを作った。
このスケッチでは、10msec程度の頻度でスイッチマトリックスをスキャンし、前回状態と変化があったスイッチを検出する。そして、そのスイッチに相当するHID Usage IDを USBSerial_print() を使ってシリアルモニタに出力する。スケッチ全体については、https://github.com/okiraku-camera/ch552_numkeyPad/tree/main/sketch/numkeyPad_test を参照のこと。
GPIO入出力について
pinMode()、digitalWrite()、 digitalRead() といったArduinoと同じAPIを使ってI/Oピンを指定するときは、ピン番号としてたとえばP1.1を対象にするときは11を、P3.2を対象とするときは32を指定する。たとえば、digitalWrite(11, HIGH); と書けば、それは P1ポートのビット1をセットすることになる。
または、CH552の内部レジスタを直接指定することもできて、P1ポートのビット1をセットするならば P1 |= 2; クリアするならば P1 &= 0xfd; と書くことができる。
二番目のポート直接指定の方がコードは小さくなり、実行速度もあがるとは思うが、さほど急ぐ必要のない場面では分かりやすさ優先で使い分けている。
スイッチマトリックスのスキャン
テンキーパットのスイッチマトリックスは以下のような回路になっている。
上図では省略したが、COL1~COL4はCH552Gの P1.4, P1.5,P1.6,P1.7 に接続し、ROW1~ROW5は、P3.0, P3.1,P3.2,P3.3,P3.4に接続している。このスケッチでは、回路にあわせてスイッチマトリックスを以下の配列で表現している。
1 2 3 |
const uint8_t cols[] = {14, 15, 16, 17}; const uint8_t rows[] = {30, 31, 32, 33, 34}; const uint8_t row_masks[] = {1, 2, 4, 8, 0x10}; |
cols[] はスイッチマトリックスの1列目~4列目に接続しているポートピンの番号を保持し、rows[] には1行目~5行目のポートピン番号を保持している。row_masks[] は、スキャンの高速化を意識してポート直接指定するため、P3ポートの各ビットの状態を操作するためのデータになっている。
準備
今回のスイッチマトリックス回路は、スイッチがオンになったとき列側(COL)のポートピンから行側(ROW)のポートピンに電流が流れることを想定しており、cols[] 内の各ポートピンを INPUT_PULLUP とし、rows[] の各ポートピンは OUTPUT として初期値 HIGHを出力する。
setup() において以下のようにポートピンを初期化している。
1 2 3 4 5 6 |
for(uint8_t i = 0; i < sizeof(cols); i++) pinMode(cols[i], INPUT_PULLUP); for(uint8_t i = 0; i < sizeof(rows); i++) { pinMode(rows[i], OUTPUT); digitalWrite(rows[i], 1); } |
スキャンの実行
スイッチ状態の読み取りは cols[] 内の各ポートピンで行う。スイッチが押されていない状態で読むと、INPUT_PULLUP なので結果は常にHIGHになるし、スイッチが押されていても行側ポートピンがHIGHのままならばやはりHIGHとなる。マトリックス回路のキースキャンでは、行側ポートピンを一定の期間LOWとし、その期間中に列側を読んだときの状態でスイッチが押されているかどうかを判断する。
関数 scan() 内でのスイッチマトリックスのスキャンは1行ずつ以下のような操作で行う。
- 対象行に対して LOW を出力して選択。
- 1行を構成する4つのスイッチ状態を読み出して保存。
- 対象行に対してHIGHを出力して選択解除。
すべての行に対する読出し操作が終わった時点で、行数×列数 個のスイッチ状態のデータが1ビットずつ順番に格納されていればよい。
スキャン方法その1
ふつうのAPIを使って、20個のスイッチ状態をビットマップ化して配列 keys[] に格納する場合、以下のようになるだろう。keys[0]にはSW1~SW7、keys[1]にはSW8~SW15、keys[2]にはSW16~SW20の状態が格納される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
uint8_t keys[3]; uint8_t key = 0; uint8_t n = 0; for(uint8_t row = 0; row < sizeof(rows); row++) { digitalWrite(rows[row], 0); // select a row n = 0; for (uint8_t col = 0; col < sizeof(cols); col++) // read all cols. n |= (digitalRead(cols[col]) << col); if (row & 1) keys[key++] |= (~(n << 4) & 0xf0); else keys[key] = ~n & 0x0f; digitalWrite(rows[row], 1); // unselect a row. } |
このキーボードには各列4つのスイッチがあるので、偶数行のとき下位4ビットに、奇数行のとき上位4ビットに状態を格納するようにしている。なおポートピンから読み取れるスイッチの状態はオンのとき0、オフのときに1なので、わかりやすさのため反転して格納している。
この方法に問題があるとすれば、digitalRead() や digitalWrite() といったAPIの呼び出しを繰り返していることで、素早く終わらせたい処理の実装としてはあまりよろしくないだろう。
スキャン方法その2
CH552 (8051)では、各ポートピンの状態をポートごとにまとめて取得したり設定したりできるので、1列ずつ digitalRead() するのではなく、P1 ポートからまとめて読み取るようにすることもできる。さらに、行の選択もポート指定すると以下のようになる。
1 2 3 4 5 6 7 8 9 10 |
uint8_t keys[3]; uint8_t key = 0; for(uint8_t row = 0; row < sizeof(rows); row++) { P3 &= ~row_masks[row]; // select row if (row & 1) keys[key++] |= (~P1 & 0xf0); else keys[key] = (~P1 >> 4) & 0x0f; P3 |= row_masks[row]; // unselect row } |
実行時間の相違
ロジアナを使って1行の選択から選択解除までに要する時間を測ってみると以下のようになった(24MHz動作)。
プログラム | 選択行がLOWの期間 (読出しと保存も含む) |
次の行がLOWになるまでの時間 |
スキャン方法その1 | 35usec | 7usec |
スキャン方法その2 | 3usec | 1usec |
実行時間には10倍以上の相違があった。
その1のプログラムは、1行を構成する4つのスイッチ状態の読出しに、digitalRead() を4回呼び出していること大きく影響していると思われる。また、次の行がLOWになるまでの時間に相違があるのは、行の選択を解除するループの最後の digitalWrite() と、次の行を選択するための digitalWrite() の呼び出しよるものだろう。
スキャン方法その2 を採用するならば、スイッチマトリックス全体のキー状態を配列 keys[] に格納するための時間が 20usec 程度だから、キーボード用のコードとしては十分だろう。
実行速度やコードサイズを気にする場合、 sdcc がArduinoのビルドディレクトリに作成するアセンブラリストファイル(sourcefile.asm, sourcefile.lst, sourcefile.rst) やメモリマップファイル(sourcefile.mem, sourcefile.map) などを参照し、自力でコードの最適化を行うことも可能と思われる。 アセンブラリストファイルは、ビルドディレクトリ直下のsketchディレクトリ内に作成される)。
HIDコードへの変換とシリアルモニタへの出力
keys[] に格納したスイッチ状態は、デバウンスのために直前データ last_scan[] との比較する。全スイッチの状態が一致していなければ、スイッチ状態が安定していないとみなしてコードの生成は行わない。今回使ったGateron KS-33の仕様上のバウンス時間は5msec (max) なので、直前データとの比較は5msec以上の時間をあけて行う。このスケッチでは、10msecごとに scan() を呼んでいる。
前回取得した二度読み一致データは配列 last_stable[] 内に記憶しているので、その内容と1ビットずつ比較し変化があったスイッチを検出する。そして変化のあったスイッチ番号を void key_event(uint8_t switch_num, uint8_t state) に渡し、その中で対応するHID Usage ID に変換して USBSerial_print() を呼び出すことで、コードと状態をシリアルモニタに出力している。
HID Usage IDへの変換は、以下の配列をスイッチ番号で引くことで行う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
static const uint8_t scan_to_hid[] = { // scan hid HID_KEYBOARD_SC_NUM_LOCK, // 1 0x53 HID_KEYBOARD_SC_KEYPAD_SLASH, // 2 0x54 HID_KEYBOARD_SC_KEYPAD_ASTERISK, // 3 0x55 HID_KEYBOARD_SC_KEYPAD_MINUS, // 4 0x56 HID_KEYBOARD_SC_KEYPAD_7_AND_HOME, // 5 0x5f HID_KEYBOARD_SC_KEYPAD_8_AND_UP_ARROW, // 6 0x60 HID_KEYBOARD_SC_KEYPAD_9_AND_PAGE_UP, // 7 0x61 HID_KEYBOARD_SC_KEYPAD_PLUS, // 8 0x57 HID_KEYBOARD_SC_KEYPAD_4_AND_LEFT_ARROW, // 9 0x5c HID_KEYBOARD_SC_KEYPAD_5, // 10 0x5d HID_KEYBOARD_SC_KEYPAD_6_AND_RIGHT_ARROW, // 11 0x5e 0, // 12 HID_KEYBOARD_SC_KEYPAD_1_AND_END, // 13 0x59 HID_KEYBOARD_SC_KEYPAD_2_AND_DOWN_ARROW, // 14 0x5a HID_KEYBOARD_SC_KEYPAD_3_AND_PAGE_DOWN, // 15 0x5b HID_KEYBOARD_SC_KEYPAD_ENTER, // 16 0x58 HID_KEYBOARD_SC_KEYPAD_0_AND_INSERT, // 17 0x62 0, // 18 HID_KEYBOARD_SC_KEYPAD_DOT_AND_DELETE, // 19 0x63 0 // 20 }; |
この配列に含まれている HID_KEYBOARD_SC_NUM_LOCK といったHID Usage IDの定義は、CH55xDuinoの …\cores\ch55xduino\usbCommonDescriptors\HIDClassCommon.h 内に書かれている。
HIDキーボードとして動作するスケッチ ch552_numkeyPad.ino
- numkeyPad_test.ino と同じやり方でキースイッチマトリックスをスキャンし、状態変化があったスイッチのコードをHIDキーボードとして送信する。
- ホスト側からの出力レポートに基づいてNumLock インジケーターの点灯/消灯を行う。
- CH55xDuinoに付属のスケッチ例 HidKeyboard を構成する USBhandler.c/.h や USBconstant.c/.h といったファイルを修正して利用している。
- ビルド時に、USB Settings: USER CODE w/148B を指定すること。
- ソースファイルについては、 https://github.com/okiraku-camera/ch552_numkeyPad/tree/main/sketch/ch552_numkeyPad を参照のこと。
ファイル構成
このスケッチのファイル構成とトピックス的なこと。
ch552_numkeyPad.ino
メインスケッチ。キースイッチマトリックスのスキャンと状態変化を検出したキーの HID Usage ID を ch552_usbhid.c 内の関数を使って入力レポートとして送信する。
ホストからの出力レポートの内容をたまに参照し、必要ならばキーボードのNumLockインジケーターLEDを点灯/消灯する。
CH552をスリープ状態とするための関数 USBStartSuspend() は、USBhandler.c でUIF_SUSPEND割り込みを検出したときに呼び出される。
ch552_usbhid.c / .h
HidKeyboard サンプルの USBHIDKeyboard.c / .h から必要な部分を抽出し、HID入力レポートを送信するための report_press() およびreport_release() を追加。HID の入出力を行うための関数を集めた。
USBconstant.c / .h
USBの各種ディスクリプタを表すいろいろな配列を保持する。HidKeyboard サンプルから流用。
HIDレポートディスクリプタについては、修飾キーを含む/含まないのどちらも対応できるようにした。
USBhandler.c / .h
CH552のUSB割り込みハンドラを含む、USB入出力のコア部分。HidKeyboard サンプルから流用。
USBバスのサスペンド時、またはウェイクアップ検出時に発生する UIF_SUSPEND割り込み検出した場合、サスペンド中でなければ USBStartSuspend() を呼び出す。スリープモードからの復帰は、USBイベントの検出時に自動的に行われるようにした。テンキーの操作によるスリープ解除は対応していない。
レポートディスクリプタの変更
USBconstant.c 内の uint8_t ReportDescriptor[] がこのキーボードのレポートディスクリプタで、ビルド時に WITH_MODIFIERS マクロが定義されているかどうかに応じて内容が変化するようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
__code uint8_t ReportDescriptor[] = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) #ifdef WITH_MODIFIERS 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x08, // REPORT_COUNT (8) 0x75, 0x01, // REPORT_SIZE (1) 0x81, 0x02, // INPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x03, // INPUT (Cnst,Var,Abs) #endif // Keyboard 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0xff, // LOGICAL_MAXIMUM (255) 0x95, 0x06, // REPORT_COUNT (6) 0x75, 0x08, // REPORT_SIZE (8) 0x81, 0x00, // INPUT (Data,Ary,Abs) // LED 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (Num Lock) 0x29, 0x05, // USAGE_MAXIMUM (Kana) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x05, // REPORT_COUNT (5) 0x75, 0x01, // REPORT_SIZE (1) 0x91, 0x02, // OUTPUT (Data,Var,Abs) 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 0x91, 0x03, // OUTPUT (Cnst,Var,Abs) 0xc0 // END_COLLECTION }; |
このレポートディスクリプタは、一般的なキーボードと同様に、修飾キー、キーボード、LEDからなる3つの USAGE_PAGEのコレクションになっている。 今回は修飾キーを持たないテンキー用なので、WITH_MODIFIERS が定義されていないときは修飾キーに関する記述を省略するようにした。
このようにした場合、入力レポート用のデータからも修飾キーを表す8ビット (11~13行目)と、しばしば reserved と表現される8ビット (14~16行目)も取り除いておく必要がある。そのため入力レポートデータの配列は ch552_usbhid.h 内で以下のように定義した。
1 2 3 4 5 6 7 8 |
#ifdef WITH_MODIFIERS #define report_size 8 #define report_start_index 2 #else #define report_size 6 #define report_start_index 0 #endif __data uint8_t hid_report[report_size]; |
WITH_MODIFIERS ならば、よくあるキーボードの入力レポートデータと同様にレポートのデータ長は8バイトで、キー入力データは2バイト目から格納する。そうでなければデータ長は6バイトで、キー入力データはレポート配列の先頭から格納する。
きょうのまとめ
CH552やCH55xDuinoをはじめて使ってみたが、特に戸惑うこともなく、すんなりとプログラミングできて、すんなりとキーボードは動作した。共通プラットフォームとしてのArduinoのありがたみを感じる。
テンキーパッドとしての動作はとても順調。ただ、前の投稿に書いたようにロープロファイルのキーキャップをスタビライザー無しで使うのは無謀だったようで、+ や Enterを使うときには意識して中央部を叩くようにした。
ロープロファイル以外の、たとえばXDAのキーキャップならばスタビライザーがないことによるデメリットはさほど感じない。
ただ、手元にあったXDAキーキャップとタクタイル感から選んだ茶軸スイッチの相性が最悪な感じで、せめてリニアな赤軸スイッチにすればよかったかも。この抹茶色のようなキーキャップセットをはめると、あらゆるキーボードの官能性が最悪になるので、自分との相性の問題なのかもしれないが。
まとめると、ソフトウェア面では満足、ハードウェア面ではやり直したい気分といったところです。
追記
CH55xDuino の0.20版でのビルドとターゲット動作は0.18版で行ったときと同様に問題なかった。
0.20版では、下図のようにビルドオプションが追加されていた。
今回のキーパッドでは、BOOTスイッチでP3.6をプルアップするようにしているので、初期値として表示される P3.6(D+) pull-up のままでよい。