概要
しばらく前に、XD64 VER 3 PCBを使った親指シフトキーボードを作っておおむね満足していたのだけど、英文混じりの日本語入力をもうちょっとやりやすくしてみたいと思っていた。そのために、文字種切替え用のキーを追加して同じ60%サイズのキーボードを回路基板 (PCB) から作ることにした。回路基板(PCB)のスイッチレイアウトは、USキーボードと日本語キーボードの両者に対応できるようにした。
hoboNicolaLibrary を利用することで、ホスト側やアダプターの支援なく、親指シフトキーボードとして利用できる。今のところ、Windowsでの動作しか確認していない。
PCB の外形寸法やネジ穴の位置はGH60の寸法に合わせて設計した。そうしておけば、安価な市販のケース(あるいはシェル)をAliexpress などで容易に入手できるから、PCBから上だけ考えればよい。
マイコンは Raspberry Pi 財団 のRP2040を利用した。XD64にはATmelのATMega32U4が載っており、さまざまな実装が公開されている。ただ自分で作るにあたって従来と同じでは面白みもないので、安価で高性能なRP2040を採用することにした。
回路およびPCBの設計は、KiCADの6.11版を利用し、KiCADで出力したgerberデータを製造用に使った。
PCBの製造は JLCPCB に依頼。RP2040も含めて小さな部品を多数実装する必要があって、とても手ハンダで作れる自信がなかったからPCB製造と同時にほとんどのデバイスのアセンブリも依頼した (PCBA)。5枚製造したときの費用は送料も含めて $65程度だった。ひどい円安じゃなければ、安さをもっと実感できただろう。
RP2040に書き込むソフトウェアは、hoboNicolaライブラリーに今回のハードウェアに対応するための修正を加えた1.7.2版を使った。ソースについてはGitHUBを参照のこと。
ハードウェア
回路
RP2040を使ったシンプルな構成に、5行 x 14列 (70キー)のスイッチマトリックスを組み合わせている。
RP2040周り
マイコンのRP2040 の周辺回路については、Raspberry Pi Ltd が公開している Hardware design with RP2040 Using RP2040 microcontrollers to build boards and products を参考に、デカップリングコンデンサ、LDO、フラッシュメモリ、12MHzのオシレーターなどを接続した。
回路基板のB面のUSBコネクタの近傍、EscキーとTabキーの間あたりにおおよそのデバイスを以下のように配置した。GH60の寸法に合わせるためにはキースイッチを隙間なく配置する必要があるので、TabキーやCapsLockキーのまわり、あるいは、スペースキーの周辺くらいしか、デバイスをうまく配置できそうな場所がない。
これはB面から見た図になっている。特に配線がたて込むCPU周辺のGNDは、F面の導体ゾーンを利用している。
フラッシュメモリには、WinBondのW25Q128JVSIQ (128Mbit, 16MBytes Quad SPI Flash) を使ったが、実際のところフラッシュメモリの容量は2MBytesもあれば十分だから、容量2MBのW25Q16JV でもよかった。ただデバイスの単価が0.5$も違わなかったので、RP2040がサポートできる最大サイズである16MBytesの方を選択した。
USBおよび電源
Type-CのUSB2.0用レセプタクル (USB_C_Receptacle_HRO_TYPE-C-31-M-12) をGH60の指定位置に取り付け、その近傍にRP2040の+3.3V電源用としてLDO (AP2112K-3.3) を配置した。
USB VBUSとLDOの間には、1A(@25℃)以上でトリップするはずのリセッタブルヒューズ (nSMD100-16V) を入れ、過大な電流からUSBのホスト側を保護するようにした。LEDを駆動するための+5Vも、当然リセッタブルヒューズの出力側から得るようにした。
AP2112K-3.3は、VBUSの+5Vを入力として+3.3Vを出力するLDOで常温ならば600mAまでは安定して流せるはず。RP2040と10個程度の高輝度LEDを駆動するのは余裕だろう。
スイッチマトリクス
5行14列の、最大70キーまでサポートできるキースイッチマトリクスとした。RP2040にはGPIOが30本でているので余裕。
キーボードスイッチは、安くあげるためにPCBに直接ハンダ付けすることにしたので、5ピンのMXスイッチを使う。今回のPCBにを使って最初にこしらえたテスト目的のキーボードがわりと良かったので、スイッチプレートは設計はしたものの発注しなかった。
各スイッチに配した電流方向を決めるダイオードは、列から行の方向としているので、プログラムを書くとき列側GPIOピンはプルアップ付きの入力、行側は出力(デフォルトHIGH、行選択時にLOW)とする必要がある。
LED
CapsLockインジケーターとして1個と、NICOLAモードインジケーターを1系統を実装できるようにした。
hoboNicolaライブラリは、NICOLA配列や同時打鍵処理を許可するモード(NICOLAモード)を明示するためのLED (NICOLA LED)を使うが、このキーボードではNICOLAモードであることを分かりやすく示すため、LEDのフットプリントと配線を10箇所用意した。LEDはスイッチの上(キーキャップの下)に顔を出すスルーホールタイプまたはPCB裏面に実装するSMDタイプのいずれかを選択できる。裏面にLEDを実装するのは、GH60用のスケルトンタイプのケースと組み合わせることを想定していたため。
LEDのVFに注意
配慮すべき点は、RP2040のGPIOのVOHが+3.3Vであることと、青や黄緑色のLEDのVFは+3V以上のものが多く、GPIOによる直接駆動では点灯できなかったり暗かったりする可能性があること。
たとえばVFがきっちり+3.0Vだとしても、LED用の直列抵抗が100Ω ならばLEDを流れる電流は (3300 – 3000) ÷ 100 = 3 (mA) ということになり、あまり明るさも期待できない。LEDのVFには誤差や個体差があるので、GPIOのVOHとの差が1V以下になることは避けるようにしている。
LED駆動用のハイサイドスイッチ
NICOLA LEDとしてVFが3.0V以上の青色LEDを使うことにしたので、これらについては+5Vで駆動してやることにした。+3.3VのGPIOで+5Vをオン/オフすることになるので、ロードスイッチまたはハイサイドスイッチというものを使った。 具体的なデバイスとしては、DIODES INC のAP2281-3WG-7 を採用した。
VINに+5V、ENにRP2040のGPIOを接続してやる。ENがLOWならばVOUTもLOW、HIGH(+1.6V以上)を与えるとVOUTに+5Vが出力される。VOUTから流せる電流は最大2Aなので、LEDを10個や20個つけてもまず問題ない。気にすべきなのは、VINに流れるUSB VBUSからの電流の方だろう。
実際に組み立てたキーボードでは、NICOLA LEDはVF=3.0~3.2V, IF=20mAの青色SMDLED、CapsLock LEDは、VF=2.0~2.2V, IF=20mAの赤色のスルーホールを使った。直列抵抗はいずれも330Ωとしたので、NICOLA LEDは1個あたり最大で6mA、CapsLock LEDはやはり最大で4mA流れることになる。
CapsLock LED について電流制限抵抗を220Ωか100Ωにすればよかったかもしれない。
なお、NICOLA LED, CAPS LEDともに製造時にはLEDを実装していない。SMD LEDについては3216サイズとしたので手ハンダでも装着できる。
RESETスイッチとBOOTスイッチ
RP2040を使っただいたいのArduinoボードはRESETスイッチとBOOTSELスイッチを備えているので、このPCBのB面にも2つのスイッチを用意した。スイッチは、一般的なGH60用ケースの底部にあるリセットボタン用の開口部から操作できる位置に配置した。スイッチの目的はマイコンをBOOTSELモードで開始させるためである。
ただ、リセットボタン用開口部の位置や大きさの標準を見つけられなかったので、手元にあった2種類のケースの開口部の位置や大きさから配置位置を決めた。正確な位置についてはGH60用のネジ穴の項を参照。
ケース1 |
ケース2 |
リセット用開口部の大きさはケースによってまちまちで、製品によっては開口部がない。
なお、CapsLockスイッチの横に、ピンセットなどでショートさせるとスイッチと同じになる、RESETとBOOTSELのスルーホールも用意した。
RESETスイッチ
RP2040の#26(RUNまたはRESET)ピンをGNDに接地してやるとリセットがかかる。データシートによれば、このピンはRP2040の内部で約50kΩでプルアップされているので、オンのときにRUNをGNDに接続すればよい。
BOOTSELスイッチ
RP2040のリセット時にBOOTSELモードで開始させるためのスイッチ。RP2040の#56 (QSPI_SS)ピンはフラッシュメモリ (W25Q128JVSIQ )のCSピンに接続するが、リセット時にこのピンがLOWレベルならばRP2040内部のBOOT ROMからプログラムが読まれてはBOOTSELモードで開始する。
BOOTSELモードでのRP2040は内部のbootromにしたがってUSB Mass Storage Device として機能し、PCなどのホストからアクセスできる以下のようなディスクドライブができる。
このドライブに、UF2(USB Flashing Format Data)形式のプログラムを書き込んでやると、その内容がフラッシュメモリに書き込まれる。
これらのスイッチは必要か?
Arduinoベースで開発している場合、Arduino IDEやVSCodeからのUSBシリアルによる操作によってBOOTSELモードが自動的に開始してUF2ファイルの書込みが行われる。また、特定のキー操作でpico sdkにある reset_usb_boot(0, 0); というAPIを読んでやるようにすると、やはりBOOTSELモードが開始する。
ということで、これらのスイッチは今のところほとんど使っていない。ではなぜ用意したのかというと、Arduino特有のUSB経由の仮想シリアルポートを作らない構成とし、なおかつキー入力が不能になるようなバグを出したとき、確実にBOOTSELモードとする手段がないと困るから、といったところ。
RESETスイッチはUSBケーブルの抜き差しで代用できるから、BOOTSELスイッチだけでもよかったかもしれない。
追記
なお、nk60用ファームウェアの1.7.4版からはArduino特有の仮想シリアル(CDC)を禁止するようにした。その後のファームウェア更新時には、特別なキー操作(Fn + 左Ctrl + 右Ctrl + B) によってBOOTSELモードとし、Windows Explorerに現れるRPI-RP2ドライブにUF2ファイルをドロップするようにしている。
PCBAでのデバイスの選定について
今回は当初からPCBの製造とデバイスのマウント(SMT)を同時に依頼する、PCBA (PCB and Assembly) を前提にデバイスを選択した。デバイスの選択時には必要なスペックを満たすことと同時に、PCBAを依頼する業者(今回はJLCPCB) が扱っていて在庫があるものを選ぶようにした。その方が時間の節約になるし、発注がスムーズに行える。
またJLCPCBで選択できる部品には、Basic components と Extended components の2種類があり、Basicに分類されている部品の方が全体の費用が安くすむ。JLCPCBにSMT用のBOMを送ると、適切な部品を選定してくれるのだが、その時点で Extended components が選ばれていたら、同サイズ、同スペックのBasic components が実はあるのかもと思って探すようにしている。ときには、その時点で回路やPCBを修正して発注をやり直すなんてことをしている。
GH60用のネジ穴
GH60用のケースはM2ネジを6本使ってPCBを固定するようになっている。ネジの位置を間違えるとせっかく作ったPCBがケースに固定できない。ネット検索で資料を探し、geekhackという掲示板にあった絵と、GitHUBのGH60プロジェクトのPCB図面 (revC_plain) に基づいて以下のように寸法を決めた。
この図は、KiCADの3Dビューワーの画像に寸法線を入れて作成。なお、左下(左Altキーのちょっと右側くらい)のネジ穴はこのPCBのために設置したもので、GH60ケースとは関係ない。
リセットスイッチなどの位置
B面のスイッチの位置は上記のようにした。これは、オリジナルのGH60の各図にある位置と異なっているのだが、手持ちの3つのケースではこのようになっていた。
USBコネクタについては、オリジナルの指定位置通りに穴が開いていた。
キーレイアウト
PCBは日本語レイアウトとUSレイアウトに両対応しているが、日本語キーボードとしてのレイアウトを優先した。
各キーが出力する文字や記号に相当する出力コード(HID Usage ID)はソフトウェア側でいかようにもセットできるので、あくまでも基本的なレイアウトとして示している。実際に使うときのレイアウトは、Windowsなどのホスト側のキーボードレイアウトにも依存する。
- 右側のShiftキーはなくても困らないだろうということで省略。
- 親指左キー(SW61)は2U、親指右キー(SW62)は2.25Uとした。2つの親指キーの境界は、Bキーの中央下ではなくGキーとHキーの中間線に揃っている。これは、左Shiftキー(SW43)のサイズが2Uのため、標準的なキーボードと比べるとZXCVB.. 行が0.25U分だけ左にずれているため。
- 日本語キーボードでのアンダーバー (SW54)は、USレイアウトでは、ティルダおよびアクサングラブ (あるいは右Shift) のためのキーとして使う。
- USキーボードには、無変換、変換、ひらがな といったDOS/Vで拡張されたキーは存在しない(ハードウェアキーボードレイアウトが英語キーボードのとき、Windowsに無視される)。そのため、F14, F15, ImeOn (HID LANG1 , 0x90)を発生させる必要がある。また、F14,F15 が無変換や変換と同様の機能を果たすように、日本語入力IMEを設定しておく必要がある。
レイアウトについては、次の投稿を参照のこと。
スタビライザー用の穴
EnterキーやBackspaceキーはいずれもスタビライザーを必要とするため、スイッチやスタビライザーを以下のように配置している。
スイッチ用の穴とスタビライザー用の穴が重なる箇所もあって、KiCADのDRC (Design Rule Checker) からドリル穴が近すぎるとか言われるが製造上の問題はなかった。
今回のPCBは、PCBマウントのスタビライザーを使うことを前提としている。スイッチプレートを作るのであれば、プレートマウントのスタビライザーも選択できる。ただそうした場合、日本語とUSの両レイアウトを1枚のスイッチプレートでカバーするのは無理だろう。
キースイッチ
日本語レイアウトとした方はGateronのちょっと古いサイレントスイッチ(赤軸5ピン)を採用。「ちょっと古い」と書いたのは、現在のサイレントスイッチ (KS-9シリーズ)の底面にはPCBに表面実装したSMD LEDが接触しないための窓が開いているのに対し、手元にあったのはスルーホールLED用の穴が4つ開いているものだった。なので、スイッチ下にSMDLEDは置かないことにした。
USレイアウトとした方は、OutemuのLinear Cream Pink というfactory lubrication を謳うリニアな5ピンスイッチを選択した。こちらはサイレントスイッチではないので、コトコトカタカタといった控えめな音がする。
親指シフトキーボードなので、2つの親指キーを頻繁に打鍵することになるので、日本語をばりばり入力しているとちょっとうるさく感じた。そのため、2つの親指キー用のスイッチには、やはりOutemuのサイレントスイッチに付け直して使っている。
ソフトウェア
このPCB用のファームウェアは、GitHUBにhoboNicolaLibrary 1.7.2版として公開している。詳細についてはそちらを参照のこと。
hoboNicolaLibraryをキーボードに組み込んで使う場合、各キーボード特有の実装は独立したファイルに収めるようにしている。このPCB用のプログラムは、examples/nk60_hobo_nicola 内に収めた。
トピックス的なことをいくつか。
必要なコアパッケージおよびライブラリ
nk60 hoboNicola は Arduinoベースで開発しており以下のコアパッケージやライブラリに依存している (2023年11月現在)。
hoboNicolaLibraryを除くコアパッケージおよびライブラリの導入は、Arduino IDEのボードマネージャやライブラリマネージャから実施している。
デュアルコア動作
RP2040はデュアルコアなので、core1は定期的なキースキャンの実行専用とし、その他の処理 (HIDキーボード、親指シフト同時打鍵など)はcore0で実行する。nk60_hobo_nicola.ino を参照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void setup1() { delay(10); // wait for core0 setup done. } static unsigned long scan_interval = 5; void loop1() { delay(scan_interval); matrix_scan(); } void setup() { init_nk60(); nk60Keyboard::init_hobo_nicola(&kbd, "nk60"); delay(300); } void loop() { bool pressed; ..... } |
コアパッケージのArduino-Picoは、スケッチ内に void setup1() および void loop1() が存在すると、デュアルコア動作を意識したスケッチだと解釈し、multicore_launch_core1() を使ってcore1を開始するようになっている。上記のような記述により、core1 側は scan_interval (msec) ごとに matrix_scan() を呼び出してスイッチマトリックスのスキャンを行い、状態に変化があったスイッチの検出を行う。
setup1() の先頭に delay(10); を置いているのは、Arduino-pico 実装として setup() よりもsetup1() を先に実行するから。10msecほどの遅延しているあいだに、setup() を完了させることを意図した。
キースキャン
5行 x 14列のスイッチマトリックスのスキャンは以下のようにした。nk60.cpp を参照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static const uint8_t ROWS[] = {29, 25, 26, 27, 28}; static const uint8_t COLS[] = {23, 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13}; static const uint8_t SW_COUNT = sizeof(ROWS) * sizeof(COLS); void matrix_scan() { uint32_t tmp_state[STATE_COUNT]; for(uint8_t row = 0; row < sizeof(ROWS); row++) { // scan all rows. gpio_put(ROWS[row], 0); delayMicroseconds(5); uint32_t cols = gpio_get_all(); tmp_state[row] = ~(((cols & 0x800000) >> 23) | ((cols & 0x7f) << 1) | (cols & 0x03f00)); gpio_put(ROWS[row], 1); } .... } |
配列 ROWS[]内の定数が行1~行5のGPIO番号、COLS[]は各列に接続したGPIOの番号をもつ。対象行を gpio_put(ROWS[row], 0); によって選択し、わずかに待ってから全列の状態を得て変数tmp_state[] に格納している。
列の全ビットを得るために、pico sdkに用意されている、gpio_get_all() を使っている。このAPIは、RP2040 が備えている30ビットのGPIOの状態を32ビットの値として返す。ビット0がGPIO0の状態、ビット29がGOIO29の状態となっている。
gpio_get_all() で得た列内容のうちの必要な15ビットを列1から列15までの順に並べ替えて temp_state[] に格納。そして、 gpio_put(ROWS[row], 1); で対象行の選択を終了する。
この処理を5行分実行すると、tmp_state[] にはすべてのスイッチの状態を表す70ビットのデータが格納される。ただし、各要素で有効なのはビット0~14の15ビットのみである。
このあとデバウンス処理(スイッチのチャタリングの防止)などを経て、状態に変化のあったスイッチを検出し、対応するコード (スキャンコード = スイッチ番号)と状態(オンまたはオフ)をキューを介してcore0に通知する。
コア間でのキー入力の受渡し
core1で動作する matrix_scan() では、状態に変化のあったスイッチを検出すると新たにオンになったキー、オフになったスイッチのコード (スキャンコード) をsend_key_event() に渡す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static queue_t key_event_queue; queue_init(&key_event_queue, sizeof(queue_entry_t), 16); typedef struct { uint8_t flag; uint8_t code; } queue_entry_t; void send_key_event(uint8_t code, uint8_t flag) { queue_entry_t key; key.flag = flag; key.code = code; queue_try_add(&key_event_queue, &key); // キューがフルなら入らないだけ。 } |
この関数では、pico sdkの queue_try_add(&key_event_queue, &key); を呼んでコードおよび状態を key_event_queue に格納する。 queue_try_add() はノンブロッキングでキューに要素を追加するAPIで、キューがフルであっても止まらない。フルならば要素が入らないだけである。初期化時に16エントリとしているが、今のところおかしなことを起きないようである。
core0側では、void loop() 内で定期的に nk60_get_key() を呼び出してキー入力を得る。
1 2 3 4 5 6 7 8 |
uint8_t nk60_get_key(bool& pressed, bool us_layout) { uint8_t k; queue_entry_t key; if (!queue_try_remove(&key_event_queue, &key)) return 0; if (key.code > 0 && key.code <= SW_COUNT) { uint8_t hid = scan_to_hid_table[hid_table_index][key.code - 1]; .... |
queue_try_remove(&key_event_queue, &key); は、指定のキューから要素を得るためのAPIで、キューが空でなければ先頭の要素を取得し、それをキューから削除する。キューが空ならば falseを返すので、上記のように処理はそこで終了。
キューから新たな要素(スイッチ番号およびオン/オフのフラグ)が得られればその値でテーブル scan_to_hid_table[] を引いて HID Usage IDを得てその後の処理に使う。
hoboNicolaアダプターとの類似性
キューを介してキーイベントを受け取ったあとは、例えばhoboNicolaアダプターが外部キーボードからの入力をUSB Hostコントローラーを介して得てから行う処理 (同時打鍵、配列変更、HID 入力レポートの出力など)と同じことになるので、ライブラリ側で特に気を使うこともない。言い換えると、アダプター用のライブラリ本体部分にほとんど手を加えることなく、キーボードの実装も可能ということになる。
LEDのPWM制御
今回のキーボードには複数のNICOLA LEDを配置したのだが、ただ点灯/消灯するだけでは面白くないと思ってPWMで明滅させてやることにした。今後も RP2040を使ったアダプターやキーボードを作る予定なので、PWM用の機能をまとめたファイル nk_rp_pwmled.cpp / .h )をライブラリに追加した。
PWMの詳細については、RP2040 Datasheet(2.2版) の 4.6 PWM を参照。このデータシートの更新は頻繁に行われていて、ちょっと古い版だと章番号が違ったりするので注意。
RP2040のPWMの特徴や今回の実装のトピックス的なことをまとめた。
- 合計で16個のPWMチャンネルを利用でき、30本あるGPIOをPWM出力用に割り当て可能。今回は、GPIO14 をNICOLA LED用に使っている。
- 内部のアップダウンカウンターは16ビット幅なので、TOP値(あるいはWRAP値)は16ビットで表現する必要があるし、カウンターの現在値と比較する CC (Counter Compare )値も16ビットとする。今回は、TOP値として10000、pwm_level(CC値)として 1~10000の範囲の値を与えている。
-
pwm_config_set_clkdiv_int(&pwmconf, F_CPU / 1000000); により、クロックディバイダとして F_CPU / 1000000 を与えているので、カウントアップ周波数は1MHzとなる。TOP値が10000なので、約10msecごとに内部アップダウンカウンターはラップする(0に戻る)。
-
irq_set_exclusive_handler(PWM_IRQ_WRAP, isr_rp_pwm_wrap); により、ラップと同時に割込みハンドラ isr_rp_pwm_wrap() が呼ばれる。isr_rp_pwm_wrap() 内でCC値 (pwm_level) を漸増、漸減させることで、LEDの明るさを決めるデューティー比が漸増、漸減し、結果としてLEDが明るくなったり暗くなったりする。
もうちょっと凝った? 明滅をさせたいところなのだけど、現状はこんな具合です。
きょうのまとめ
- このキーボードは半年以上前に作ってずっと使っているのだが、親指キー周りについては、左Altキー(SW59)の右隣から、ImeOff、親指左(無変換またはF14)、親指右(空白)、ImeOn (またはひらがな) という並びで使っている。今回レイアウトとして示した基本形は変更せず、hoboNicolaライブラリ側に機能を追加することで、そのように並べられるようにした。
- ほかに、左Altキー(SW59)の右隣から 無変換またはF14、親指左(空白)、親指右(変換またはF15)、ImeOn (またはひらがな) というの設定も可能。これは通常型の日本語キーボードとほぼ同じ並びということになる。無変換キーによる半確定操作を使わない場合、この方がよいだろう。
- 左Shiftキーを2Uサイズにした(そうせざる得ない)ため、キーキャップセットの選定に困ることがある。ちょうど2Uサイズでキーキャップの高さはさほど変わらない、テンキー側の 0 キーを使うことが多い。
- プログラムの公開はGitHUBで行うようにした。 プログラムの更新情報やhoboNicolaライブラリの概要や考え方については、以前からある ほぼNICOLA化プログラム hoboNicolaのページ に書くようにしたのでご参照のほど。
- ブログの投稿を書かないうちに、次のキーボードを作ったりしているので、そちらについてもおいおいやっていきます。この投稿についても必要に応じて追記や変更を行っていく予定です。
- 機会があってMac book PROにつないでちょっと動作を確認した。親指シフト同時打鍵入力で日本語がまずまずスムーズに入力できることは確認したものの、日本語入力ソフトの設定や文字種切り替えの方法がよく分かっていないので、英文混じりの日本語入力が快適にばりばりできた、いう感じではない。Mac mini でも持ってるといいんでしょうけど。