[自製QMK鍵盤-番外] QMK的基本架構與運作方式

系列:自製QMK鍵盤 Posted on 2020-06-23

如果要深入修改 QMK 的話,最好還是要瞭解一下 QMK 的架構及其運作方式。這可能會有點無聊,但擁有足夠的知識總是可以讓我們少走一點冤枉路。

以下內容皆譯自 QMK 官方說明文件。我並沒有完全翻譯,只有翻譯其中比較重要的部分內容。如有翻譯錯誤還請指教,並以 QMK 官方文件為準。

鍵盤的運作

本節內容譯自 QMK 官方說明文件:How Keys Are Registered, and Interpreted by Computers

1.按下按鍵

當使用者按下一個按鍵時,鍵盤的韌體就會登記一個事件。該事件可以在按下(pressed)、按住(held)或釋放(released)時被登記。

這些事通常在定期的鍵盤掃描中發生,其速度通常受限於機械鍵軸的反映時間、傳輸按鍵的協定(在這裡指USB HID)和使用的軟體。

2.韌體傳送了什麼

HID規範了鍵盤可以透過USB發送、且有機會被正確識別的內容。這包含了掃描碼(scancodes)的預定義列表(pre-defined list),這些掃描碼是從0x000xE7(0到231)的簡單數字。韌體將掃描碼分配給鍵盤的每個鍵。

韌體並不會直接傳送實際的字母或字符,只會傳送掃描碼。所以修改韌體時,你只能修改通過USB傳送的按鍵掃描碼。

3.輸入事件/內核的作用

掃描碼映射(mapped)到依賴於 60-keyboard.hwdb的鍵碼(Keycode)。如果沒有此映射關係,作業系統無法收到有效的鍵碼,也無法對該按鍵執行任何有用的操作。

4.作業系統做了什麼

當鍵碼傳送到作業系統時,某個軟體會依照鍵盤佈局(Layout)來配對一個字符。例如QWERTY佈局的配對表如下:

鍵碼字符
0x04a/A
0x05b/B
0x06c/C
0x1Cy/y
0x1Dz/Z

鍵盤項目結構

本節內容譯自QMK官方說明文件:Introduction

keyboards資料夾裡,每個鍵盤項目都會有類似以下的架構:

  • keymaps資料夾: 可以建構的不同的按鍵映射(Keymap)。
    • keymap.c:按鍵映射的程式。必填。
    • config.h: 此按鍵映射的設定。
    • rules.mk: 啟用的QMK功能。
    • readme.md: 此按鍵映射的說明。
  • rules.mk: 設定預設的「make」設定。
  • config.h: 設定預設的編譯設定。
  • info.json: 給QMK Configurator用的鍵盤佈局(Layout)設定。更詳細的說明請看:Supporting Your Keyboard in QMK Configurator
  • readme.md: 該鍵盤的簡要說明。
  • <keyboardName>.h: 根據鍵盤的按鍵矩陣(Switch matrix)來定義鍵盤佈局位置。
  • <keyboardName>.c: 可以在此檔案中增加自定義的功能程式。

更詳細的結構介紹可以看QMK官方說明文件:QMK Keyboard Guidelines

瞭解QMK的程式碼

本節內容譯自QMK官方說明文件:Understanding QMK’s Code

開始

你可以認為QMK和任何其它的電腦程式沒有什麼不同。它開始、執行、然後結束。

和一般的C語言程式一樣,QMK的進入點是main()函數,但是對於不熟悉QMK的人來說可能會有點混亂,因為main()函數不只出現在一個地方,會不知道要看哪一個。

這是因為QMK支援的平台不同,最常見的平台是「lufa(Lightweight USB Framework for AVRs)」,它運作在如ATmega32U4這樣的AVR微控制器(處理機,Processors)上。其它還有「chibios」和「vusb」。

以下將重點放在使用「lufa」平台上的AVR微控制器。你可以在tmk_core/protocol/lufa/lufa.c中找到main()函數。瀏覽該函數可以發現它會初始化已配置的硬體(包含了主機的USB),然後在while(1)中開始核心的部分。這是所謂的「主迴圈(Main loop)」。

主迴圈

主迴圈的程式負責永遠地重複執行同一組指令。這是QMK分配令鍵盤執行它應該做的所有事情的地方。雖然它看起來包含了很多功能,但多數情況下它們會被#define給禁用(disable)。

keyboard_task()函數負責調度所有的鍵盤功能。其原始碼可在tmk_core / common / keyboard.c中找到,它負責檢測鍵盤矩陣的變化和各狀態LED燈的亮滅。

keyboard_task()函數中,你可以找到要處理的程式碼:

  • 矩陣掃描
  • 處理滑鼠
  • 串列連接
  • 可視化器
  • 鍵盤狀態LED燈(大寫鎖定、數字鎖定和滾動鎖定(Scroll Lock))

矩陣掃描

矩陣掃描(Matrix scanning)是鍵盤韌體的核心功能。這是檢測目前按下了那些按鍵的過程,鍵盤每秒鐘都會執行很多次此功能。不誇張地說,韌體有99%的CPU時間都在做矩陣掃描。

實際執行矩陣掃描有很多種不同的策略,但已超出本文的範圍,在此就當它是一個黑盒子就好。你會要求矩陣提供目前的狀態,並得到如以下的數據結構:

1{
2    {0,0,0,0},
3    {0,0,0,0},
4    {0,0,0,0},
5    {0,0,0,0},
6    {0,0,0,0}
7}

此數據結構是一個4×5(4 row by 5 column)的數字鍵盤矩陣。一個鍵按下後,該鍵在矩陣中的位置會回傳1而不是0

矩陣掃描的精確速度並不一定,但通常每秒至少執行10次,以避免明顯的延遲(lag)。

矩陣到物理佈局映射

一旦得知鍵盤上每個按鍵的狀態,就必須將其映射(map)到鍵碼(Keycode)。在QMK中,這是透過C語言的巨集(macro)來完成的,這讓我們可以將物理佈局和鍵碼定義分開來。如果你不是很熟係C語言的巨集功能,這篇文章或許可以幫助到你。

在鍵盤層次,我們定義一個C語言的巨集(通常命名為LAYOUT())以將鍵盤的矩陣映射到物理按鍵。有時矩陣中不是每一個位置都有按鍵,我們可以透過填上KC_NO來使鍵盤映射的定義更容易使用。

這是一個數字鍵盤的LAYOUT()巨集範例:

 1#define LAYOUT( \
 2    k00, k01, k02, k03, \
 3    k10, k11, k12, k13, \
 4    k20, k21, k22, \
 5    k30, k31, k32, k33, \
 6    k40,      k42 \
 7) { \
 8    { k00, k01, k02, k03, }, \
 9    { k10, k11, k12, k13, }, \
10    { k20, k21, k22, KC_NO, }, \
11    { k30, k31, k32, k33, }, \
12    { k40, KC_NO, k42, KC_NO } \
13}

請注意該LAYOUT()巨集的第二區塊(block)是如何與上面的「矩陣掃描」做匹配的?該巨集將矩陣掃描映射到鍵碼。但是數字鍵盤只有17鍵,會有3個鍵的位置在矩陣上可以有、但實際上並沒有按鍵。我們在其位置填上KC_NO,因為它們在鍵盤映射定義中不是必須要的。

你也可以利用此巨集來處理不常見的矩陣佈局,例如Clueboard rev2。在此不為其多做說明。

鍵碼分配

在鍵盤映射層次,我們利用上面的LAYOUT()巨集將鍵碼映射到物理位置,再映射到矩陣位置。像是這樣:

1const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
2[0] = LAYOUT(
3  KC_NLCK, KC_PSLS, KC_PAST, KC_PMNS, \
4  KC_P7,   KC_P8,   KC_P9,   KC_PPLS, \
5  KC_P4,   KC_P5,   KC_P6, \
6  KC_P1,   KC_P2,   KC_P3,   KC_PENT, \
7  KC_P0,            KC_PDOT)
8}

請注意這些引數(Argument)是如何與LAYOUT()巨集的前半部分匹配的?這就是取得鍵碼並映射到矩陣掃描的方法。

我們在const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS]呼叫了LAYOUT()巨集,並傳遞如以下的引數給LAYOUT()巨集:

KC_NLCKKC_PSLSKC_PASTKC_PMNS
KC_P7KC_P8KC_P9KC_PPLS
KC_P4KC_P5KC_P6
KC_P1KC_P2KC_P3KC_PENT
KC_P0KC_PDOT

以上這些引數傳遞進LAYOUT()巨集後,一一對應了LAYOUT()巨集的前半部分,也就是:

k00k01k02k03
k10k11k12k13
k20k21k22
k30k31k32k33
k40k42

接著LAYOUT()巨集開始動作,也就是進行「替換」。它將傳遞給它的引數對應其前半部分的參數(Parameter)名稱,並將LAYOUT()巨集的後半部分「替換」成各自對應的引數。

例如在LAYOUT()巨集的前半部分中,引數KC_NLCK的位置對應了參數名k00,那麼在LAYOUT()巨集後半部分的k00就會被替換成KC_NLCK

以下是LAYOUT()巨集的後半部分,請注意我特別標示成斜體的KC_NO在什麼位置。

k00k01k02k03
k10k11k12k13
k20k21k22KC_NO
k30k31k32k33
k40KC_NOk42KC_NO

LAYOUT()巨集完成所有的替換工作後,它會回傳如以下的內容:

KC_NLCKKC_PSLSKC_PASTKC_PMNS
KC_P7KC_P8KC_P9KC_PPLS
KC_P4KC_P5KC_P6KC_NO
KC_P1KC_P2KC_P3KC_PENT
KC_P0KC_NOKC_PDOTKC_NO

以上為了方便理解,所以我使用表格的方式表示,但實際情況會更加類似以下:

傳入LAYOUT()巨集的引數為:

以上的引數透過位置來對應到以下LAYOUT()巨集的前半部分。

k00k01k02k03k10k11k12k13k20k21k22k30k31k32k33k40k42

以上LAYOUT()巨集的前半部分透過名稱(如k00)來對應到以下的LAYOUT()巨集後半部分。

k00k01k02k03
k10k11k12k13
k20k21k22KC_NO
k30k31k32k33
k40KC_NOk42KC_NO

當巨集完成替換後會變成像這樣:

KC_NLCKKC_PSLSKC_PASTKC_PMNS
KC_P7KC_P8KC_P9KC_PPLS
KC_P4KC_P5KC_P6KC_NO
KC_P1KC_P2KC_P3KC_PENT
KC_P0KC_NOKC_PDOTKC_NO

檢測狀態變化

上面講述了矩陣掃描可以告訴我們某一時刻的矩陣狀態,但是電腦只想知道狀態的變化,而不是目前的狀態。QMK會儲存最後一次矩陣掃描的結果,並透過比較來確認何時按下或釋放了什麼按鍵。

以下舉例在鍵盤掃描中,之前的掃描看起來是這樣:

1{
2    {0,0,0,0},
3    {0,0,0,0},
4    {0,0,0,0},
5    {0,0,0,0},
6    {0,0,0,0}
7}

然後等目前的掃描結束後,新的掃描結果是這樣:

1{
2    {1,0,0,0},
3    {0,0,0,0},
4    {0,0,0,0},
5    {0,0,0,0},
6    {0,0,0,0}
7}

透過比對鍵盤映射,我們知道按下的按鍵是KC_NLCK。這裡我們調用process_record函數集。

Process Record

process_record() 是通往 QMK 各個層級功能的入口(gateway)。這裡列出了一系列的事件與詳細的介紹。

相關文章與資源



留言可能不會立即顯示。若過了幾天仍未出現,請 Email 聯繫:)

comments powered by Disqus