rules.mk
に
SRC += matrix.c
CUSTOM_MATRIX = lite
を書いたときに書くコードについてのお話しです。
筆者は、このCUSTOM_MATRIX = lite
は、
既存のマトリクススキャンを取り入れながら、自分用のマトリクススキャンを書ける機能だと思っていたのですが、どうも違うようでした。
liteであっても、既存のマトリクススキャン機能(つまり、Pro MicroのGPIOピンを直接使って行う普通のスキャン)から、自前で書かなければならないようです。
動きを調べた感じからの印象です。違うよ! 何で車輪の再発明しているの!?という方は教えてください! お願いします!
筆者はあまり自分の書いたコードに納得していません。どうして、既に書かれているスキャンのコードをまた書かなきゃならないの?と不信を抱いています。
ということで、以降は、設計している自作キーボードのスキャンの仕組みが「既存のマトリクススキャン」+「カスタムマトリクススキャン」の合成である方に向けた、「既存のマトリクススキャン」の書き方です。
正しいかはわかりません。ひとまず動いたよ~というくらいの情報なので、ご参考まで、ということで。
普通の既存のマトリクススキャンを書くだけなので、基本はQMKファームウェアにあるソースコードのコピペです。
具体的にはhttps://github.com/qmk/qmk_firmware/blob/master/quantum/matrix.cからコピペします。
まるごとコピペしただけでも動くかも知れませんが、筆者は試していないのでわかりません。
必要な部分だけを切り貼りすると、自前のmatrix.cが以下の様になりました。
以下に全体を貼り付け、その後、それぞれの関数とかを説明を付け足していきます。
ただし、以下は自前のマトリクススキャンと既存のマトリクススキャンの両方を完成させてから、「既存のマトリクススキャン」だけを切り抜いたものなので、ビルドが通らないかもしれません。
#include "quantum.h"
#include "matrix.h"
#include "i2c_master.h"
#include <print.h>
#include <stdint.h>
#include <string.h>
#ifndef MATRIX_INPUT_PRESSED_STATE
# define MATRIX_INPUT_PRESSED_STATE 0
#endif
pin_t row_pins[MATRIX_ROWS] = MATRIX_ROW_PINS;
pin_t col_pins[MATRIX_COLS] = MATRIX_COL_PINS;
static inline void setPinOutput_writeLow(pin_t pin) {
ATOMIC_BLOCK_FORCEON {
setPinOutput(pin);
writePinLow(pin);
}
}
static inline void setPinInputHigh_atomic(pin_t pin) {
ATOMIC_BLOCK_FORCEON {
setPinInputHigh(pin);
}
}
static inline uint8_t readMatrixPin(pin_t pin) {
if (pin != NO_PIN) {
return (readPin(pin) == MATRIX_INPUT_PRESSED_STATE) ? 0 : 1;
} else {
return 1;
}
}
static bool select_row(uint8_t row) {
pin_t pin = row_pins[row];
if (pin != NO_PIN) {
setPinOutput_writeLow(pin);
return true;
}
return false;
}
static void unselect_row(uint8_t row) {
pin_t pin = row_pins[row];
if (pin != NO_PIN) {
setPinInputHigh_atomic(pin);
}
}
void matrix_read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row) {
// Start with a clear matrix row
matrix_row_t current_row_value = 0;
if (!select_row(current_row)) { // Select row
return; // skip NO_PIN row
}
matrix_output_select_delay();
// For each col...
matrix_row_t row_shifter = MATRIX_ROW_SHIFTER;
for (uint8_t col_index = 0; col_index < MATRIX_COLS; col_index++, row_shifter <<= 1) {
uint8_t pin_state = readMatrixPin(col_pins[col_index]);
// Populate the matrix row with the state of the col pin
current_row_value |= pin_state ? 0 : row_shifter;
}
// Unselect row
unselect_row(current_row);
matrix_output_unselect_delay(current_row, current_row_value != 0); // wait for all Col signals to go HIGH
// Update the matrix
current_matrix[current_row] = current_row_value;
}
void matrix_init_custom(void) {
{
for (uint8_t i = 0; i < MATRIX_COLS; i++) {
if (col_pins[i] != NO_PIN) {
ATOMIC_BLOCK_FORCEON {
setPinInputHigh(col_pins[i]);
}
}
}
for (uint8_t i = 0; i < MATRIX_ROWS; i++) {
if (row_pins[i] != NO_PIN) {
ATOMIC_BLOCK_FORCEON {
setPinOutput(row_pins[i]);
}
}
}
}
}
bool matrix_scan_custom(matrix_row_t* current_matrix) {
bool matrix_has_changed = false;
matrix_row_t tmp_matrix[MATRIX_ROWS] = {0};
{
for (uint8_t current_row = 0; current_row < MATRIX_ROWS; current_row++) {
matrix_read_cols_on_row(tmp_matrix, current_row);
}
matrix_has_changed = memcmp(current_matrix, tmp_matrix, sizeof(tmp_matrix)) != 0;
if (matrix_has_changed) memcpy(current_matrix, tmp_matrix, sizeof(tmp_matrix));
}
return matrix_has_changed;
}
基本的なこと
https://github.com/qmk/qmk_firmware/blob/master/docs/custom_matrix.mdにあるように、liteなカスタムマトリクススキャンの基本は以下の関数です。
void matrix_init_custom(void) {
// TODO: initialize hardware here
}
bool matrix_scan_custom(matrix_row_t current_matrix[]) {
bool matrix_has_changed = false;
// TODO: add matrix scanning routine here
return matrix_has_changed;
}
https://github.com/qmk/qmk_firmware/blob/master/docs/custom_matrix.mdより引用
matrix_init_custom
関数は、マトリクススキャン用の初期化関数で、https://github.com/qmk/qmk_firmware/blob/master/quantum/matrix_common.cのmatrix_init
関数から呼ばれるみたいです。
matrix_scan_custom
関数はスキャンで定期的に呼ばれることになるスキャン本体の関数で、https://github.com/qmk/qmk_firmware/blob/master/quantum/matrix_common.cのmatrix_scan
関数から呼ばれるみたいです。
筆者はスキャンの仕組みがなかなか理解できず、さらにさかのぼってコードを見ているのですが、今回は不要な知識なのでそういう情報は飛ばします。
ひとまず、matrix_init_custom
関数とmatrix_scan_custom
関数を実装するのが、さしあたっての目標です。
matrix_init_custom関数
先も書きましたとおり、こちらは初期化関数です。
主にハードウェアの初期化を行います。
この関数で、Pro MicroのGPIOピンの入出力を設定します。
info.json
には以下の様なピンの設定を書いているものと思いますので、この情報を使います。
"matrix_pins": {
"cols": ["F4", "F5", "F6", "F7", "B1", "B3", "B2"],
"rows": ["D4", "C6", "D7", "E6", "B4", "B5"]
},
後、たぶんconfig.h
に書いてある以下の情報も使います。
#define MATRIX_COLS 7
#define MATRIX_ROWS 6
情報が一元管理されていなくて気持ち悪いですね。
きっともっとまとめて書く方法があると思うのですが、筆者は今のところ知りませんので、今はこの気持ち悪さに目をつむります。
上記、ふたつの情報を元に、以下の様にハードウェアの初期化に使用する変数を定義します。
pin_t row_pins[MATRIX_ROWS] = MATRIX_ROW_PINS;
pin_t col_pins[MATRIX_COLS] = MATRIX_COL_PINS;
で、以下の様に初期化します。
void matrix_init_custom(void){
for(uint8_t i = 0; i < MATRIX_COLS; i++) {
if (col_pins[i] != NO_PIN) {
ATOMIC_BLOCK_FORCEON {
setPinInputHigh(col_pins[i]);
}
}
}
for(uint8_t i = 0; i < MATRIX_ROWS; i++) {
if (row_pins[i] != NO_PIN) {
ATOMIC_BLOCK_FORCEON {
setPinOutput(row_pins[i]);
}
}
}
}
COL2ROWの場合、COLピンが入力、ROWピンが出力です。
実はここで最初、筆者の勘違いがあり、COLピンが出力、ROWピンが入力だと思っていました。ですが、Discordでの親切な方の導きがあり、COLピンが入力、ROWピンが出力だとわかりました。原理は、筆者があらためて説明しようとすると間違ったことをいう可能性が高いため、説明は省きます。
ひとまず、COLピンが入力、ROWピンが出力であると、まるっと理解して、話を続けます。
見慣れないATOMIC_BLOCK_FORCEON
というものが出てきますが、これはくくった処理をまとめて実行するようにするためのおまじないです。くくった処理の最中に割り込みが入らないようにします。詳細はhttps://github.com/qmk/qmk_firmware/blob/master/docs/gpio_control.md#atomic-operationを見てください。
setPinInputHigh
はポートをプルアップ抵抗有り入力にする関数で、setPinOutput
はポートを出力にする関数です。
col_pins[]
とrow_pins[]
には以下で定義したピン名(ポート名)が書かれているため、これに基づいて初期設定を行います。
"matrix_pins": {
"cols": ["F4", "F5", "F6", "F7", "B1", "B3", "B2"],
"rows": ["D4", "C6", "D7", "E6", "B4", "B5"]
},
matrix_scan_custom関数
先に述べたとおり、スキャンの本体の関数です。
bool matrix_scan_custom(matrix_row_t* current_matrix) {
bool matrix_has_changed = false;
matrix_row_t tmp_matrix[MATRIX_ROWS] = {0};
{
for (uint8_t current_row = 0; current_row < MATRIX_ROWS; current_row++) {
matrix_read_cols_on_row(tmp_matrix, current_row);
}
matrix_has_changed = memcmp(current_matrix, tmp_matrix, sizeof(tmp_matrix)) != 0;
if (matrix_has_changed) memcpy(current_matrix, tmp_matrix, sizeof(tmp_matrix));
}
return matrix_has_changed;
}
スキャンでは、ROWピンをLowに走らせて、COLピンからの入力を確認します。
つまり、以下のコードにあるように、ROWピンの分だけ、COLピンを確認する作業をmatrix_read_cols_on_row
関数で実行します。
for (uint8_t current_row = 0; current_row < MATRIX_ROWS; current_row++) {
matrix_read_cols_on_row(tmp_matrix, current_row);
}
その後、マトリクスをスキャンした結果が前回のスキャン結果と異なっているかを、memcmp
関数で確認、違っていたら、マトリクスのスキャン結果を格納する配列current_matrix
へ最新のスキャン結果をコピーします。
matrix_has_changed = memcmp(current_matrix, tmp_matrix, sizeof(tmp_matrix)) != 0;
if (matrix_has_changed) memcpy(current_matrix, tmp_matrix, sizeof(tmp_matrix));
最後に上位関数へmatrix_has_changed
、つまり、マトリクスのスキャン結果が変わったかどうかを通知します。
return matrix_has_changed;
matrix_read_cols_on_row関数
では、さらに処理の本体を確認します。
void matrix_read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row) {
// Start with a clear matrix row
matrix_row_t current_row_value = 0;
if (!select_row(current_row)) { // Select row
return; // skip NO_PIN row
}
matrix_output_select_delay();
// For each col...
matrix_row_t row_shifter = MATRIX_ROW_SHIFTER;
for (uint8_t col_index = 0; col_index < MATRIX_COLS; col_index++, row_shifter <<= 1) {
uint8_t pin_state = readMatrixPin(col_pins[col_index]);
// Populate the matrix row with the state of the col pin
current_row_value |= pin_state ? 0 : row_shifter;
}
// Unselect row
unselect_row(current_row);
matrix_output_unselect_delay(current_row, current_row_value != 0); // wait for all Col signals to go HIGH
// Update the matrix
current_matrix[current_row] = current_row_value;
}
ここで行っていることは、ROWピンをセレクト――ROWピンをLowアクティブにし、全COLピンの反応を確認することです。
以下のコードのselect_row
関数がROWピンをLowアクティブにする関数です。
if (!select_row(current_row)) { // Select row
return; // skip NO_PIN row
}
その後、matrix_output_select_delay
関数で回路が反応するまでのディレイを挟み、全COLピンを読みとります。
matrix_output_select_delay();
for (uint8_t col_index = 0; col_index < MATRIX_COLS; col_index++, row_shifter <<= 1) {
uint8_t pin_state = readMatrixPin(col_pins[col_index]);
// Populate the matrix row with the state of the col pin
current_row_value |= pin_state ? 0 : row_shifter;
}
読み取り終わったら、ROWピンをアンセレクト――Highに戻し、matrix_output_unselect_delay
関数でHighに戻るのを少し待ちます。
// Unselect row
unselect_row(current_row);
matrix_output_unselect_delay(current_row, current_row_value != 0); // wait for all Col signals to go HIGH
最後に、スキャンの結果を格納します。
current_matrix[current_row] = current_row_value;
ここで、matrix_read_cols_on_row
関数で使用した関数select_row
、readMatrixPin
、unselect_row
も確認します。
select_row関数
info.json
のmatrix_pins
でNO_PIN
を指定していないROWピンをLowアクティブにします。
Lowアクティブの出力する処理が中断されないよう、setPinOutput_writeLow
はATOMIC_BLOCK_FORCEON
でくくっておきます。
static bool select_row(uint8_t row) {
pin_t pin = row_pins[row];
if (pin != NO_PIN) {
setPinOutput_writeLow(pin);
return true;
}
return false;
}
static inline void setPinOutput_writeLow(pin_t pin) {
ATOMIC_BLOCK_FORCEON {
setPinOutput(pin);
writePinLow(pin);
}
}
readMatrixPin関数
ROWピンを読みとった結果がMATRIX_INPUT_PRESSED_STATE
、つまり、0だったら、0をリターンし、そうでなければ1をリターンします。ピンの値はLowアクティブであることに注意してください。
static inline uint8_t readMatrixPin(pin_t pin) {
if (pin != NO_PIN) {
return (readPin(pin) == MATRIX_INPUT_PRESSED_STATE) ? 0 : 1;
} else {
return 1;
}
}
unselect_row関数
select_row
したROWピンを元に戻します。
static void unselect_row(uint8_t row) {
pin_t pin = row_pins[row];
if (pin != NO_PIN) {
setPinInputHigh_atomic(pin);
}
}
static inline void setPinInputHigh_atomic(pin_t pin) {
ATOMIC_BLOCK_FORCEON {
setPinInputHigh(pin);
}
}
まとめ
これで一通りのコードを確認しました。
基本はhttps://github.com/qmk/qmk_firmware/blob/master/quantum/matrix.cからコピペしたコードですので、そちらのソースコードも参考に見てください。
(ただ、プリプロセッサでソースを切り替えているため、ちょっと見づらいです)
基本は、
- 1番目のROWピンをLowにする
- すべてのCOLピンの状態を確認する
- 1番目のROWピンをHighに戻す
- 2番目のROWピンをLowにする
- すべてのCOLピンの状態を確認する
- 2番目のROWピンをHighに戻す
……
の繰り返しと理解してもらえれば、そんなに難しいコードではないと思います。
これがQMKで実装されている既存のマトリクススキャンの方法です。
次回の課題
これで、普通のマトリクススキャンが動いて、キースイッチの入力が取得されるようになりましたので、次は自前のマトリクススキャンのコードを紹介します。
自前のマトリクスはMCP23017というIOエキスパンダ(入出力ピンを増やすためのコントローラ)を経由して、マトリクススキャンします。
以降は別記事で紹介しますので、興味がある方は記事執筆をお待ちください。
以上、長々とありがとうございました。