xd87キーボードのマトリックススキャン

概要

xd87キーボードキットのキースイッチマトリックスの回路を確認するプログラムをArduino IDEで書いて組み込んでみた。全スイッチの状態を定期的にスキャンし、オンになったスイッチとオフになったスイッチの番号をシリアル出力する。

プログラムは、前回のLEDの動作確認と同様にArduino Leonardoと見立てて記述しているため、xd87と、LeonardoやPro Microとの相違点に配慮する必要があった。そのためにATmega32u4のヒューズビットの確認を行い、プログラムで対策した。

xd87 PCB

キースイッチマトリックス

調べてみた限りでは、xd87 PCBのキースイッチマトリックスは以下のような回路になっているようだ。

xd87 キースイッチマトリックス

横にのびる配線が行(ROW)の接続を、縦にのびる配線が列(COLumn)の接続を表しており、行と列の交点にスイッチとダイオードが配置されている。図中のスイッチ番号(SWnnn)は、行番号(1~6)と列番号(1~17)に基づいて勝手に決めたもので、
SW番号 = 17 × (行番号 – 1) + 列番号
としている。実際の回路では行と列の交点にスイッチがない部分もあるから抜け番もある。

このようなキーボード回路に対して、オンになっているスイッチ(つまり押されているキー)、オフになっているスイッチ(押されていないキー)をそれぞれ特定するための操作をマトリックススキャン(または単純にスキャン)と呼んでいる。

スキャンを定期的に行うことで、前回オフだったスイッチがオンになれば、それは新たにキーが押されたことになるし、オンだったものがオフになっていれば、そのキーは離されたことになる。今回のプログラムの目的は、状態に変化があったキーを検出し、その番号を出力することとした。

スイッチ状態の検出について

xd87 PCBでは、各スイッチ位置にあるダイオードの向きが列から行方向なので、以下のようにしてスイッチがオンのとき列から行に電流が流れるようにする。

  • 列側に接続されているポートは入力方向とし、内部プルアップ抵抗を有効にしておく(arduinoでのINPUT_PULLUP)。
  • 行側に接続されているポートは出力方向としておき、初期状態ではすべてHIGHを出力しておく。

プルアップ抵抗、スイッチ、入力ポートおよび出力ポートを乱暴な模式図にすると以下のような感じになるだろう。

スイッチ回路の模式図 スイッチがオフのとき

スイッチがオフならば、出力ポートの状態に関わらずプルアップ抵抗を介した電圧によって入力結果はHIGHになる。

スイッチ回路の模式図 スイッチがオンのとき

スイッチがオンならば、入力側のポート(列)と対応する出力側(行)のポートはダイオードを介して接続され、入力ポートが検出する値は出力ポートの状態で決まる。

  • 出力ポートがロジックHIGHを出力(図ではVCCにプルアップ)中なら、電位差が生じないので入力ポートはHIGHを検出。
  • ロジックLOWを出力(図ではGNDに接続)中ならば、プルアップ抵抗を介して電流が流れるので、入力ポートはLOWを検出する。

まとめると、行側ポートでLOWを出力しているタイミングで、列側ポートを読んだ結果がLOWならば、その交点にあるスイッチは押されているし、HIGHならば押されていないということになる。

図中のVIHとVILは、入力ポートがロジックHまたはロジックLと判定する電圧を表している。また、ATmega32u4の内部プルアップ抵抗RPUのMIN/MAX値は、20kΩ/50kΩとデータシートに規定されている。プルアップ抵抗の値が大きいこともあって、出力ポートがロジックLを出力したとき、入力ポートがVILに達するまでにはそれなりに時間がかかるから、行の選択と列状態の検出を行うまでのあいだには、待ち時間が必要になる。

プログラムについて

プログラム (スケッチ) 全体は以下のようになった。

最初に公開していたスケッチには、複数のキーを同時に押したとき、押していないキーのコードも出してしまうという恥ずかしいパグがあったので修正しました。

確認している様子

スイッチ番号の確認している様子を動画にもしてみた。

あらかじめシリアルポートを指定したTera Termを開いておき、 Atmel Flip でプログラムを開始するところから、いくつかのスイッチ位置をピンセットではさんでいるところ。

バウンス (チャタリング) を除く処理は入っていないので、ピンセットでの触り方によって、連続的に出力されたりしている。

Picture in Picture で画面キャプチャした動画と外部カメラで録った動画を組み合わせてみたが、わずかにタイミングがずれてしまった。

以下、スケッチ各部の説明など。

まず必要な設定

xd87 PCBは、ATmega32u4のほとんどのポートを使った回路構成になっているので、キースイッチマトリックスに関わるポート設定を行う前に、以下の三点をやっておく必要があった。

  • ヒューズ由来のクロックプリスケーラ(CLKPR)の変更 (LEDの話で触れた)。
  • ヒューズ由来のJTAG設定の禁止。
  • ArduinoライブラリにはTXLEDとRXLED用ポートを使わせない

JTAGの禁止

ATmega32u4のJTAGという機能が有効になっていると、PORTFの4つのピンがI/Oポートとして利用できないので、これを禁止した。ヒューズの調査と対策方法については、後半にまとめた。xd87関係の一連のはなしの中では、ヒューズの書き換えは行わないことにしている。

Arduino Leonardo特有の対策(TXLED, RXLED)

今回は、xd87 PCBをArduino Leonardoに見立ててプログラムを作っているので、Leonardo (および Pro Micro) に特有の対策が必要になる。これらの開発ボードでは、USB入出力に同期して点灯する2つのLEDが以下のように使われている。

  • PD5 = L でTXLED(送信)が点灯。
  • PB0 = L でRXLED(受信)が点灯。

PB0とPD5は、いずれも行を選択するための出力用ポートピンとして使うので、USB入出力の都合で勝手に状態を変更されては困る。Arduinoライブラリでは、これらのポートピンの初期設定やLED操作のためのコードは USBCore.cpp にまとめられているから、これを書き換えて使うことにした。変更の詳細や変更後のコードについては後半に

シリアル通信の開始

このプログラムでは、Arduinoのよくあるスケッチと同様に、setup() の終わりあたりで Serial.begin(); しているが、Arduino LeonardoやSparkFun Pro Microでやるのと同じく以下のようにした。

シリアル通信の相手方と接続できるまでここで待つ。つまり、PCなどでターミナルプログラム (Tera Termなど)を動かし、正しいポートをオープンしないと先に進まない。

LeonardoやPro Microでの Serial は、USBのCDC (Communication Device Class) を使って実装されているので、こういうコードを入れてCDCによる接続が完了し利用可能となるまで待つ必要がある。Serial.begin(); した直後に Serial.println(“test”); とか出してしまうと、その後のUSBを使った通信がうまくいかなかったりする。

スキャン用のポート設定

スイッチ状態読み取り用の設定は以下のようにした。

DDRではじまるレジスタはポートピンの入出力方向を設定するためのもので、1 をセットしたピンが出力方向になる (初期値はすべて入力方向)。行に接続している6つのピンの他に、RGBLED(PB7)、BGLED(PD0)、CapsLED(PE2)を出力としている。

次に、入力ピンのプルアップ抵抗を有効にするため、列に接続する17本のピンについて、PORTxレジスタに 1 をセットしている。

最後に、出力ポートピンの初期状態をロジックHとし、BGLED用のピンにもHを与えて消灯している。

スイッチ状態を読む

スイッチの状態の読み込みは、以下の繰り返しになる。

  1. ある一つの行のポートにLを出力
  2. すべての列の状態を読み込む
  3. Lにした行をHに戻す。

これを1行目から6行目まで行うことで、すべてのスイッチ状態が得られる。1つのスイッチの状態を1ビットで表現して記憶するので、全体で6 × 17ビット必要になる。このプログラムでは処理のしやすさから1行あたり3バイトずつ記憶するようにした。

行の選択と列状態の読み込みは以下のようなマクロにした。

portには対象行のポート名、pinにはピン番号を指定する。行ごとの17個のスイッチ状態の読み取りと格納は read_cols() で行う。

このマクロの中では、ポートの出力を変更したあと delayMicroseconds(5); によって遅延をおいている。これは、出力ポートの状態を変更してから入力ポートのロジックレベルがVILやVIHに達するまでに生じる遅延時間に対処するため (プルアップ抵抗と、ポートや配線の容量に起因する)。

列の状態の読取り

以下の read_cols() により、選択中の行のすべての列のスイッチ状態を読み取る。

回路図に合わせて17列の状態を取得し、スイッチ状態を記憶する key_pressed[] に格納 (1行あたり3バイト)。オンになっているスイッチの状態は実際には L ( == 0) となるが、格納にあたってビット反転し1で表現している。なお、3バイト目は最下位の1ビットしか使っていないので、LSB以外は0としている。

全行の状態の読み取り

scan() では、以下のように全行を順に読み取っている。

6つの行を上から順に読み取っている。READ_ROW() では1行につき3バイトずつ列の状態を格納していくので、 key_pressed[] に格納すべき位置を3ずつ増やして指定している。

最初と最後の PD0 の操作は、タイミングの観察のためのロジアナのトリガ信号とするために入れている。PD0 はBGLEDを点灯/消灯するためのポートで、ロジックLを書くと点灯するから行の読み込み中に点灯していることになる。

void loop() での処理

scan() によって key_pressed[] に得た全スイッチの状態を、前回スキャン時の状態 ( last_key_pressed[] )と比較することで、変化があったキーを検出している。

前回と今回とで同じスイッチから得たビットを XOR すると、スイッチ状態に変化があれば 1 となる。そして、XORして得たビットと今回得たビットの AND をとったとき、1ならばオンに、0ならばオフになったことになる。

そういった処理を key_pressed[] 内の全ビットに対して行い、変化のあったスイッチ番号と、オンになったのかオフになったのかをシリアル出力する。そして、変化のあったビットを含むバイトを、last_key_pressed[] にコピーすることで直前情報を更新している。

このプログラムを動かし、すべてのキースイッチポジションにピンセットの先を突っ込んで確認した結果が以下の図になる。

xd87スイッチ番号

ここに記載したスイッチ番号は、あくまでもこのプログラムで得られたものであり、実際の製品にこのような番号が振られているわけではない。

スキャンのタイミングチャート

USBロジアナを使って行の選択と読取りに要する時間や、全体のスキャンに要する時間を観察してみたとろ、下図のようになっていた。スケッチ内の scan() の最初と最後にあるPORTD0の操作をトリガとしてサンプリングしている(一番下のピンク色の線)。

キースキャンのタイミング図

各行のロジックを表す線には定期的に10μ秒ほどのL期間があるが、これがほぼREAD_ROW()マクロの実行期間になる。6行すべてをスキャンするのに要している時間(ほぼscan() の実行時間)は約62μ秒程度、その後のloop() 内の処理時間に約40μ秒ほどかかっている。前後のスキャン結果に相違がない場合のものなので、比較に要する最短時間ということになる。

図内のBGLEDのアノード側端子から得ているピンクの線のL期間は20.7μ秒となっており、scan() 以外の loop() 内の実行時間を表しているように思えるが、PD0をHにしてもアノード側がロジックLになるまでにはアナログ的な遅延があり、立ち下がりが遅れている。

READ_ROW()内には合計10μ秒の遅延を入れているから、行ごとのL期間についても実際にはもうちょっと長いはず。このあたりのところはロジアナではなく、オシロスコープで観察した方がよいのは間違いない。

なお、このタイミングを得るときには、void loop() の最後にある delay(10); をコメントとしている。

USBCore.cpp の修正

LeonardoやPro MicroのTXLEDおよびRXLED用のポートをArduinoライブラリに触らせないため、USBCore.cppを書き換えた
書換えは …\Arduino\hardware\arduino\avr\cores\arduino\USBCore.cpp  (および .h) を、今回作っているスケッチのあるフォルダにコピーしてきて、コピーしてきたものに対して行った。

具体的には以下のように追加して、定義済のポート情報を消した。

USBCore.cpp では、ここで内容を空にしたマクロを呼ぶことでポートの初期化やLEDの点灯/消灯を行っているので、マクロ定義さえ変更してしまえばコードの内容まで手を入れる必要がない。

ダウンロード

今回作成したスケッチ (scan_xd87-4.ino) および変更を加えたUSBCore.cpp は、scan_xd87-4.zip としてダウンロードできます。USBCore.h も含んでいますが、これについては変更はありません。また、light_ws2812.c および .h も含んでいます。

ヒューズビットの確認と対策

xd87のLEDをチカチカさせてみたとき、CLKPR ( クロックプリスケーラレジスタ )を操作してシステムクロックの分周比を1 (分周無し)にしたが、LeonardoやPro Microではあらかじめヒューズビットがプログラムされているので、こういった操作は不要になっている。では、他のヒューズの内容はどうなっているのかを調べて対策を行った。

ヒューズビットの確認には次のようなスケッチを使った。

setup() 内で ローバイトヒューズ (Fuse Low Byte)、ハイバイトヒューズ (Fuse High Byte)、拡張バイトヒューズ (Extended Fuse Byte) の内容を得ておき、Serial.println(); で出力している。

プログラムからヒューズの内容を得るためには、SPMCSR ( SPMコントロール・ステータスレジスタ) にアクセスし、Zレジスタに所定の値をセットしてLPM (Load Program Memory) 命令を実行する。詳しくは、 ATmega16U4/ATmega32U4 データシート 27.7.9 Reading the Fuse and Lock Bits from Software を参照。
このスケッチを逆アセンブルしてみると、ローバイトヒューズを読む部分は以下のようなコードになっていた。

このコードは、SPMCSR (I/Oアドレスの0x37)に所定のデータをセットしその3サイクル以内に0番地 (Z == 0) からlpmする、という規定を満たしている。実行すると、以下のように表示された。

これを、データシートの 28.2 Fuse Bits とにあるデフォルト値(出荷時)と照らし合わせて みると以下のようになる。

つまり、データシートに記載されているデフォルト値そのままということが分かった。

実はちゃんと読めていないのかもと思って、SparkFun Pro Micro (+5V/16MHz) でも同じコードを実行してみると以下のようになった。

xd87のヒューズビットとのおもな相違は、CLKDIV8が1、JTAGENが1、BOOTRSTが0、HWBEが1になっていること。不思議なのは、拡張バイトの上位4ビットがすべて1ではないところだが、以前にISPを使って読んだときもそのようになっていた。

上に載せたスキャン用のものもそうだが、外部リセットを使ってスケッチの入れ替えを行うようなときには、Arduino IDEのシリアルモニタではなく、あらかじめTera Termのようなターミナルプログラムを開始しておく方がよさそうである。
外部リセットによってブートローダーモードすると、ArduinoライブラリによるCDC接続が切れるので、PCから仮想シリアルポートがなくなる。そして、フラッシュ内に書き込んだArduinoスケッチの実行を開始すると消えていたシリアルポートが復活するが、このような対象シリアルポートが消えたり見えたりするような挙動に対して、Arduino IDEのシリアルモニタは不安定のようだ。

リセット時の挙動 (BOOTRSTおよびHWBE)

xd87では、HWBEビットが0であり、/HWB(PE2)ピンがプルダウンされているから、/RESETピンを使った外部リセット時にはブートローダーから実行を開始する。そして、BOOTRSTビットが1なので、パワーオンリセットやウォッチドッグリセット時には、プログラムの先頭 (0番地) のリセットベクタに書いてあるアドレス (一般的にはユーザープログラムの先頭) にジャンプする。

Arduino LeonardoやSparkFun Pro Microでは、HWBEが1なのでリセット時の挙動はBOOTRSTで決まる。BOOTRSTは0だから、外部リセット、パワーオンリセット、ウォッチドッグリセットともにブートローダーの先頭から実行が開始する。

JTAGについて

xd87 PCB (出荷時のATmega32u4)では、ハイバイトヒューズのJTAGENビットが0なので、デバイスレベルでのテストやデバッグのためのJTAGが有効になっている。JTAGが有効のとき、PF4~PF7の4つピンはJTAGデータの入出力専用になり、I/Oピンとしては利用できない。xd87 PCBではこれらのポートピンを使ってキースイッチマトリックスを構成しているから、JATGを無効にする必要がある。

JTAGを無効にするためには、MCUCR (MCUコントロールレジスタ) のJTDビットをセットするか、ヒューズのJTAGビットを1にしてやる。xd87の調査やプログラミングをするにあたって、ヒューズは変更しないことにしているので、CLKPRと同様にプログラムで行うことにした。

データシートの  26.5.1 MCU Control Register – MCUCR によれば、4サイクル以内にJTDビットにセットしたい値を2回書き込むことで変更できるとのことなので、以下のようなコードにした。

これをコンパイルすると以下のようなコードに落ちるので、意図通りに動くだろう。

ISPは使わずに済ませる

CLKDIV8とJTAGENについては、おそらくxd87用のあらゆるプログラムで処置が必要になるからヒューズを書き換えればいいのだけれど、数行のコードを追加すればいいことなので、出荷時設定のまま話を進めることにした。ヒューズ書き換えのためのISP装置 (装置といっても、Arduino Pro Miniとか Pro Microとかだが) を使わずに済むならば、その方が何かを壊してしまう機会も減る。

参考

ATmega32u4 の各種情報は、 ATmega16U4/ATmega32U4 データシート を参照のこと。

キースイッチマトリックスの構成や読み取り方法などについては、ルネサス社のKNOWLEDGEBASE (FAQ)にある、マイコンでのキー・スイッチ入力 が参考になります。

きょうのまとめ

xd87 PCBのキースイッチマトリックスをスキャンする機能はこれでほぼ完成。バウンス除去のための処理やスイッチ番号をHID Usage ID (USBキーボードでのスキャンコード) に置き換えて出力する処理などをいれれば、USBキーボードとして機能するだろう。

そろそろ動作確認は終えてもいいのだけど、集めた部品を眺めてるうちに気が変わって別のものを発注したり、それの輸送が1ヶ月以上滞ってしまって、また別のものを発注して待っていたりと泥沼化しつつある今日このごろ。とりあえず、キーの配置を決めてPCBにスタビライザーを着けるところから組立てを始める予定です。