Arduino 64キー プロトタイピング キーボードマトリックス: シフトレジスタで自作キーボード
部品と材料
| 部品 | 数量 |
|---|---|
| Arduino UNO | 1 |
| 1N4148 高速スイッチングダイオード | 64 |
| タクトスイッチ 6×6×5mm (トップアクチュエート) | 64 |
| ピンヘッダー (1×8) | 2 |
| 74HC595 シフトレジスタ | 1 |
必要な工具
- はんだごて (汎用)
アプリとオンラインサービス
- Arduino IDE
プロジェクト概要
更新: コードを改良しました (文末の「Update」参照)。
統合キーボードを必要とするプロジェクトで、開発ボード上にキーボードをどう実装するかという課題に直面しました。USBキーボードや既存のArduinoベースキーボードは使えません。実際のプロジェクトではキーボードがメインマイコンに直接接続されるためです。そこで、この PCBベースの64キー プロトタイピング用キーボードマトリックス を設計しました。
このPCBにはICを一切搭載していません。キーマトリックスの行と列が直接ピンヘッダーに接続されているため、Arduinoや任意のマイコンに接続可能。統合キーボードを搭載するプロジェクトのプロトタイピングに最適です。
詳細なコメント付きコードを同梱。I/Oピン11本 (行8 + 列3 for シフトレジスタ制御? 実際は列8本直接入力 + シフトレジスタ制御3ピン = 11ピン) で動作するあらゆるArduino互換ボードで使用可能。64キーにはシフト、キャップス、Ctrl、Alt、Fn、「スペシャル」キーの修飾キーに加え、用途自由な6キーを含む。全キーの機能を修飾キーとの組み合わせ含めて個別に定義可能 で、既存のキーボードコードより 大幅に柔軟 です。
提供コードはシリアル出力しますが、表示先をOLED等に変更するのは容易 (Arduino IDEの「すべて置換」で Serial.print を display.print 等に一括変換)。
プログラムサイズについて
ライブラリを一切使わずスクラッチで書いたためコードは大きめです。Arduino UNOではプログラム領域 9100 バイト (28%)、グローバル変数 394 バイト (19%) を使用。より効率的な書き方やライブラリは存在しますが、全キー×全修飾キーの完全なカスタマイズと、実用的なキーボード挙動 (例: CapsLock有効時のShiftで小文字になる等) を両立させるにはこのアプローチが必要でした。
ステップ1: キーボードマトリックスの仕組み
なぜマトリックスが必要か? 64キーを直接接続すると64本のI/Oピンが必要。マトリックス化すれば ピン数 = キー数の平方根 (切り上げ) で済みます。行ごとにスイッチを共通接続、列ごとにも共通接続し、行を順次アクティブ (LOW) にして列をスキャンします。
スキャン方法: マイコンの内蔵 プルアップ抵抗 を列ピンに使用。通常時は全行 HIGH。スキャン対象の行のみ LOW にし、押下されたキーの列が LOW になるのを検知。ATmega328Pにはプルダウンがないためプルアップを使用。この処理を高速に繰り返し (コードでは200回/秒に制限)、全キーをスキャンします。
ダイオード、ゴースト現象、Nキーロールオーバー: ダイオードは特定キー同時押し時に意図しないキーが検知される「ゴースト」を防ぎます。ダイオードは一方向のみ導通するため、隣接スイッチ経由の迂回電流を遮断し、Nキーロールオーバー (任意のキー組み合わせ同時押し対応) を実現します。
シフトレジスタでピン節約: 理論上8行×8列=16ピン必要ですが、74HC595シフトレジスタ で行側 (出力) を制御し、Arduino側は3ピン (Data, Latch, Clock) で済みます。入力側 (列) にもシフトレジスタ (並列入力シリアル出力型) を使えばさらに節約可能ですが、ノイズ/チャタリング対策が複雑になるため今回は見送りました。
ステップ2: PCB設計
KiCADで設計 (Eagle派の方ごめんなさい)。回路図: スイッチ+ダイオードシンボルを64個配置、1×8ピンヘッダー2つ (行用・列用) を追加。フットプリント割り当て時、KiCADのダイオードシンボルとフットプリントでピン番号が合わない問題に遭遇 (初版基板は廃棄)。修正版ではカスタムフットプリントでピン番号を入れ替え対処。基板外形はFusion 360で設計しDXFでインポート。配線は2層基板でビアも使用。ガーバーファイル一式をZIPで提供 (Google Driveリンク)。JLCPCB等に直接アップロード可能。
ステップ3: PCB実装
最も単調だが簡単な工程。スルーホール部品のみ。ダイオードの向き (マーク合わせ) に注意。サードハンドで基板固定 → ダイオード全挿入 → 裏返してはんだ付け → リードカット → タクトスイッチ全挿入・はんだ付け → ピンヘッダーはんだ付け。ピンヘッダーはオス/メスどちらでも可。オスを基板裏に付ければブレッドボード直挿し可能。
ステップ4: Arduinoとの接続
列ピン (8本) → Arduino直結:
- Column 1 → A0, Column 2 → A1, Column 3 → A2, Column 4 → A3
- Column 5 → A4, Column 6 → A5, Column 7 → D5, Column 8 → D6
74HC595 配線 (ブレッドボード中央溝にまたがって配置、ピン1は刻印側):
- VCC: ピン16, 10 → 5V
- GND: ピン8, 13 → GND
- Shift Register 制御: Data(14)→D2, Latch(12)→D3, Clock(11)→D4
- 出力ピン (行側) は並び順注意: Q0(15)→Row1, Q1(1)→Row2, Q2(2)→Row3, Q3(3)→Row4, Q4(4)→Row5, Q5(5)→Row6, Q6(6)→Row7, Q7(7)→Row8
Arduino D0, D1 (シリアル用) は未使用。
ステップ5: Arduinoコード書き込み
提供コードを通常通りアップロード。詳細コメント付き。メインループはタイマー関数で、5msごとにキーボードスキャン関数を呼出し。スキャン関数は行ごとにシフトレジスタ更新 → 8列チェック。押下キーをシリアル出力。
キー機能の変更: 各キー・修飾キー条件に対応する Serial.print("_"); を書き換えるだけ。例: Fn+N 押下時の出力等、全組み合わせカスタマイズ可能。バックスペース等はシリアルモニターでは効かないが、独自実装で対応可能。
自作プロジェクトへの組み込み: Serial.print を対象デバイスの出力関数に置換するだけで統合可能。
2021/1/30 更新版コード (大幅リライト)
オリジナルコードは「最後に押されたキー」を追跡する方式で、複数キー同時押しや連打 (「bummer」の 'mm' 等) で文字化けや入力漏れが発生していました。新版は キーボード全体の状態を前回ループと比較 する方式に変更。これにより:
- 複数キー同時押しでも正常動作
- 同一キー高速連打対応
- ループ高速化 (500μs間隔、8ループで全スキャン = 4ms = 250Hz)
- 全キーコードを配列化 (修飾キー別:
key[],shiftKey[],capsKey[],capsShiftKey[],ctrlKey[],altKey[],fnKey[],spclKey[]) で編集容易 - コード短縮化
- ARM Cortex-M4等高速MCUでも同性能 (インターバルはマイクロ秒指定)
トレードオフ: ダイナミックメモリ使用量増 (605バイト, 29%) だが、プログラム領域は大幅削減 (3532バイト, 10%)。
コード (更新版 ProtoKeyboardV1-Bits.ino)
/* Sketch for Prototyping Keyboard V1.2
* by Cameron Coward 1/30/21
* Tested on Arduino Uno. Requires custom PCB and 74HC595.
*/
const int rowData = 2, rowLatch = 3, rowClock = 4;
const int colA = A0, colB = A1, colC = A2, colD = A3;
const int colE = A4, colF = A5, colG = 5, colH = 6;
const byte shiftRow[] = {B01111111, B10111111, B11011111, B11101111,
B11110111, B11111011, B11111101, B11111110};
byte rowState[8] = {0}, prevRowState[8] = {0};
// キーコード配列 (ASCII): 無修飾, Shift+Caps, Shift, Ctrl, Spcl, Alt, Fn, Caps
const char key[] = { ... 64要素 ... };
const char capsShiftKey[] = { ... };
const char shiftKey[] = { ... };
const char ctrlKey[] = { ... };
const char spclKey[] = { ... };
const char altKey[] = { ... };
const char fnKey[] = { ... };
const char capsKey[] = { ... };
long previousKeyboardMicros = 0;
long keyboardInterval = 500; // μs
int rowToCheck = 0;
bool caps=false, shift=false, capsShift=false, ctrl=false, spcl=false, alt=false, fn=false;
void setup() {
Serial.begin(9600);
pinMode(colA, INPUT_PULLUP); ... pinMode(colH, INPUT_PULLUP);
pinMode(rowLatch, OUTPUT); pinMode(rowClock, OUTPUT); pinMode(rowData, OUTPUT);
updateShiftRegister(B11111111);
}
void loop() { mainTimer(); }
void mainTimer() {
unsigned long currentMicros = micros();
if(currentMicros - previousKeyboardMicros > keyboardInterval) {
previousKeyboardMicros = currentMicros;
checkKeyboard();
}
}
void updateShiftRegister(byte row) {
digitalWrite(rowLatch, LOW);
shiftOut(rowData, rowClock, MSBFIRST, row);
digitalWrite(rowLatch, HIGH);
}
void checkKeyboard() {
updateShiftRegister(shiftRow[rowToCheck]);
// 8列チェック → rowState[rowToCheck] の各ビット設定
updateShiftRegister(B11111111); // リーク防止
rowToCheck = (rowToCheck + 1) % 8;
if (rowToCheck == 0) checkPressedKeys(); // 8行完了でキー処理
}
void checkPressedKeys() {
// 修飾キー状態取得 (行/列ビット位置で判定)
shift = bitRead(rowState[5],1) | bitRead(rowState[6],4);
ctrl = bitRead(rowState[6],5) | bitRead(rowState[7],3);
spcl = bitRead(rowState[6],6) | bitRead(rowState[7],2);
alt = bitRead(rowState[6],7) | bitRead(rowState[7],1);
fn = bitRead(rowState[7],4);
capsShift = (shift && caps);
for(int i=7;i>=0;i--) for(int j=7;j>=0;j--) {
bool newBit = bitRead(rowState[i],j);
bool prevBit = bitRead(prevRowState[i],j);
if(newBit && !prevBit) { // 立ち上がりエッジのみ処理
int thisChar = i*8 + j;
if(capsShift) processKey(capsShiftKey[thisChar]);
else if(shift) processKey(shiftKey[thisChar]);
else if(ctrl) processKey(ctrlKey[thisChar]);
else if(alt) processKey(altKey[thisChar]);
else if(spcl) processKey(spclKey[thisChar]);
else if(fn) processKey(fnKey[thisChar]);
else if(caps) processKey(capsKey[thisChar]);
else processKey(key[thisChar]);
}
if(newBit) bitSet(prevRowState[i],j); else bitClear(prevRowState[i],j);
}
}
void processKey(char receivedKey) {
if(receivedKey == 7) caps = !caps; // CapsLockトグル (ASCII 7 = BEL)
else Serial.print(receivedKey);
}
カスタムパーツとエンクロージャ
PCB製造用ガーバーファイル一式 (keyplots_4xipOSLHzg.zip) を製造サービスにアップロード。
回路図
回路図PDF (schematic.pdf) を参照。
製造プロセス
- Arduino Nano 33 IoT製 フライトシミュレーター2020用コックピットパネル - LCD&CDI表示
- Microsoft Flight Simulator 2020用Arduinoスイッチ&LEDパネル
- MIDIキーボードをArduino・LED・MuseScoreでスマートピアノチューターに変身
- Arduino Unoで8×8 LEDマトリックスを制御:ステップバイステップガイド
- 学校向けスマート体温監視システム: 正確、リアルタイム、安全
- Arduino用8ビットI/Oポートライブラリ:デジタル制御を拡張
- Arduino Nano用 高性能2.8インチTFTシールド - 320×240 SPIディスプレイ
- Arduino用絶縁アナログ入力回路の設計:LM331 V-F変換+フォトカプラでノイズ耐性を確保
- Arduinoで作る電卓 - 16x2 LCDと4x4キーパッドで四則演算
- DCモーター制御の極意:PID制御とPWM出力による速度・方向制御のエキスパートガイド
- Arduino向けスマート屋内ナビゲーションロボットキット