從頭開始:我如何構建開發人員的夢想鍵盤
已發表: 2022-03-112007 年 8 月的一天工作,我不禁意識到,我的常規 PC 鍵盤並沒有為我提供盡可能多的服務。 我不得不過度地在鍵盤的各個塊之間移動我的手,每天數百甚至數千次,而且我的手彼此靠近令人不舒服。 一定有更好的辦法,我想。
當我想到為開發人員定制最好的鍵盤時,我感到無比興奮——後來,我意識到,作為一名自由嵌入式軟件開發人員,我對硬件一無所知。
當時,我忙於其他項目,但沒有一天不考慮構建黑客鍵盤。 很快我就開始把空閒時間投入到這個項目上。 我設法學習了一套全新的技能,說服了我的一個朋友,傑出的機械工程師 Andras Volgyi 加入這個項目,召集了一些關鍵人物,並投入了足夠的時間來創建工作原型。 如今,終極黑客鍵盤已成為現實。 我們每天都在進步,眾籌活動的啟動觸手可及。
從對電子產品一無所知的軟件背景,到設計和構建功能強大、適銷對路的硬件設備,是一種有趣而引人入勝的體驗。 在本文中,我將介紹這款電子傑作的設計原理。 對電子電路圖有基本的了解可以幫助您跟進。
你是怎麼做鍵盤的?
在我為這個話題投入了數千小時的生命之後,要給出一個簡短的答案對我來說是一個巨大的挑戰,但有一種有趣的方式可以回答這個問題。 如果我們從簡單的東西開始,比如 Arduino 板,然後逐漸將其打造為終極黑客鍵盤,會怎樣? 它不僅應該更容易消化,而且應該極具教育意義。 因此,讓我們的鍵盤教程之旅開始吧!
第一步:沒有鍵的鍵盤
首先,讓我們製作一個每秒發出一次x
字符的 USB 鍵盤。 Arduino Micro 開發板是實現此目的的理想選擇,因為它具有 ATmega32U4 微控制器 - AVR 微控制器和與 UHK 大腦相同的處理器。
對於支持 USB 的 AVR 微控制器,適用於 AVR 的輕量級 USB 框架 (LUFA) 是首選庫。 它使這些處理器成為打印機、MIDI 設備、鍵盤或幾乎任何其他類型的 USB 設備的大腦。
將設備插入 USB 端口時,設備必須傳輸一些稱為 USB 描述符的特殊數據結構。 這些描述符告訴主機正在連接的設備的類型和屬性,並由樹結構表示。 更複雜的是,一個設備不僅可以實現一種功能,而且可以實現多種功能。 讓我們看看 UHK 的描述符結構:
- 設備描述符
- 配置描述符
- 接口描述符 0:GenericHID
- 端點描述符
- 接口描述符一:鍵盤
- 端點描述符
- 接口描述符2:鼠標
- 端點描述符
- 接口描述符 0:GenericHID
- 配置描述符
大多數標準鍵盤隻公開一個鍵盤接口描述符,這是有道理的。 但是,作為自定義編程鍵盤,UHK也暴露了鼠標接口描述符,因為用戶可以通過編程鍵盤的任意鍵來控制鼠標指針,所以鍵盤可以作為鼠標使用。 GenericHID 接口用作通信通道,用於交換鍵盤所有特殊功能的配置信息。 您可以在此處查看 LUFA 中 UHK 的設備和配置描述符的完整實現。
現在我們已經創建了描述符,是時候每秒發送x
字符了。
uint8_t isSecondElapsed = 0; int main(void) { while (1) { _delay_us(1000); isSecondElapsed = 1; } } bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo, uint8_t* const ReportID, const uint8_t ReportType, void* ReportData, uint16_t* const ReportSize) { USB_KeyboardReport_Data_t* KeyboardReport = (USB_KeyboardReport_Data_t*)ReportData; if (isSecondElapsed) { KeyboardReport->KeyCode[0] = HID_KEYBOARD_SC_X; isSecondElapsed = 0; } *ReportSize = sizeof(USB_KeyboardReport_Data_t); return false; }
USB 是一種輪詢協議,這意味著主機會定期(通常每秒 125 次)查詢設備,以確定是否有任何新數據要發送。 相關的回調是CALLBACK_HID_Device_CreateHIDReport()
函數,在這種情況下,只要isSecondElapsed
變量包含1
,它就會將x
字符的掃描碼發送到主機。 isSecondElapsed
從主循環設置為1
,並從回調設置為0
。
第二步:四鍵鍵盤
在這一點上,我們的鍵盤並不是非常有用。 如果我們真的能在上面打字就好了。 但是為此我們需要按鍵,並且按鍵必須排列成鍵盤矩陣。 一個全尺寸的 104 鍵鍵盤可以有 18 行和 6 列,但我們只需一個簡陋的 2x2 鍵盤矩陣即可啟動。 這是示意圖:
這就是它在麵包板上的樣子:
假設ROW1
連接到PINA0
, ROW2
連接到PINA1
, COL1
連接到PORTB0
並且COL2
連接到PORTB1
,掃描代碼如下所示:
/* A single pin of the microcontroller to which a row or column is connected. */ typedef struct { volatile uint8_t *Direction; volatile uint8_t *Name; uint8_t Number; } Pin_t; /* This part of the key matrix is stored in the Flash to save SRAM space. */ typedef struct { const uint8_t ColNum; const uint8_t RowNum; const Pin_t *ColPorts; const Pin_t *RowPins; } KeyMatrixInfo_t; /* This Part of the key matrix is stored in the SRAM. */ typedef struct { const __flash KeyMatrixInfo_t *Info; uint8_t *Matrix; } KeyMatrix_t; const __flash KeyMatrixInfo_t KeyMatrix = { .ColNum = 2, .RowNum = 2, .RowPins = (Pin_t[]) { { .Direction=&DDRA, .Name=&PINA, .Number=PINA0 }, { .Direction=&DDRA, .Name=&PINA, .Number=PINA1 } }, .ColPorts = (Pin_t[]) { { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB0 }, { .Direction=&DDRB, .Name=&PORTB, .Number=PORTB1 }, } }; void KeyMatrix_Scan(KeyMatrix_t *KeyMatrix) { for (uint8_t Col=0; Col<KeyMatrix->Info->ColNum; Col++) { const Pin_t *ColPort = KeyMatrix->Info->ColPorts + Col; for (uint8_t Row=0; Row<KeyMatrix->Info->RowNum; Row++) { const Pin_t *RowPin = KeyMatrix->Info->RowPins + Row; uint8_t IsKeyPressed = *RowPin->Name & 1<<RowPin->Number; KeyMatrix_SetElement(KeyMatrix, Row, Col, IsKeyPressed); } } }
代碼一次掃描一列,並在該列中讀取各個按鍵開關的狀態。 然後按鍵開關的狀態被保存到一個數組中。 在我們之前的CALLBACK_HID_Device_CreateHIDReport()
函數中,將根據該數組的狀態發送相關的掃描代碼。
第三步:一個有兩半的鍵盤
到目前為止,我們已經創建了普通鍵盤的開端。 但在本鍵盤教程中,我們的目標是先進的人體工程學設計,鑑於人們有兩隻手,我們最好在組合中添加另一半鍵盤。
另一半將具有另一個鍵盤矩陣,其工作方式與前一個相同。 令人興奮的新事物是鍵盤兩半之間的通信。 互連電子設備的三種最流行的協議是 SPI、I 2 C 和 UART。 出於實際目的,我們將在這種情況下使用 UART。
根據上圖,雙向通信向右流過 RX,向左流過 TX。 VCC 和 GND 是傳輸功率所必需的。 UART 需要對等體使用相同的波特率、數據位數和停止位數。 一旦雙方的 UART 收發器都建立起來,通信就可以開始流動了。
目前,左鍵盤通過 UART 向右鍵盤發送一個字節的消息,代表按鍵或按鍵釋放事件。 右半鍵盤處理這些消息並相應地操縱內存中全鍵盤矩陣陣列的狀態。 這是左鍵盤半發送消息的方式:
USART_SendByte(IsKeyPressed<<7 | Row*COLS_NUM + Col);
右半鍵盤接收消息的代碼如下所示:

void KeyboardRxCallback(void) { uint8_t Event = USART_ReceiveByte(); if (!MessageBuffer_IsFull(&KeyStateBuffer)) { MessageBuffer_Insert(&KeyStateBuffer, Event); } }
只要通過 UART 接收到一個字節,就會觸發KeyboardRxCallback()
中斷處理程序。 鑑於中斷處理程序應盡快執行,接收到的消息將被放入環形緩衝區以供以後處理。 環形緩衝區最終會在主循環中得到處理,鍵盤矩陣將根據消息進行更新。
以上是實現這一點的最簡單方法,但最終協議會稍微複雜一些。 必須處理多字節消息,並且必須使用 CRC-CCITT 校驗和檢查各個消息的完整性。
在這一點上,我們的麵包板原型看起來非常令人印象深刻:
第四步:認識LED顯示屏
我們與 UHK 的目標之一是讓用戶能夠定義多個特定於應用程序的鍵盤映射,以進一步提高生產力。 用戶需要通過某種方式來了解正在使用的實際鍵盤映射,因此鍵盤中內置了一個集成的 LED 顯示屏。 這是一個所有 LED 都亮起的原型顯示器:
LED 顯示屏由 8x6 LED 矩陣實現:
每兩行紅色 LED 符號代表 14 段 LED 顯示屏之一的段。 白色 LED 符號代表另外三個狀態指示燈。
為了驅動電流通過 LED 並將其點亮,對應的列設置為高電壓,對應的行設置為低電壓。 該系統的一個有趣結果是,在任何給定時刻,只能啟用一列(該列上應點亮的所有 LED 都將其相應的行設置為低電壓),而其餘列被禁用. 有人可能會認為該系統無法使用全套 LED,但實際上列和行更新得如此之快,以至於人眼看不到閃爍。
LED 矩陣由兩個集成電路 (IC) 驅動,一個驅動其行,另一個驅動其列。 驅動列的源 IC 是 PCA9634 I2C LED 驅動器:
驅動行的 LED 矩陣接收器 IC 是 TPIC6C595 功率移位寄存器:
我們來看看相關代碼:
uint8_t LedStates[LED_MATRIX_ROWS_NUM]; void LedMatrix_UpdateNextRow(bool IsKeyboardColEnabled) { TPIC6C595_Transmit(LedStates[ActiveLedMatrixRow]); PCA9634_Transmit(1 << ActiveLedMatrixRow); if (++ActiveLedMatrixRow == LED_MATRIX_ROWS_NUM) { ActiveLedMatrixRow = 0; } }
LedMatrix_UpdateNextRow()
大約每毫秒調用一次,更新一行 LED 矩陣。 LedStates
數組存儲各個 LED 的狀態,通過 UART 根據來自右半鍵盤的消息進行更新,與按鍵/按鍵釋放事件的方式幾乎相同。
大局
到目前為止,我們已經逐漸為我們的自定義黑客鍵盤構建了所有必要的組件,是時候看看大局了。 鍵盤的內部就像一個迷你計算機網絡:許多節點相互連接。 不同之處在於節點之間的距離不是以米或公里為單位,而是以厘米為單位,並且節點不是成熟的計算機,而是微型集成電路。
到目前為止,關於開發者鍵盤的設備端細節已經說了很多,但關於主機端軟件 UHK Agent 的說法並不多。 原因是,與硬件和固件不同,Agent 在這一點上非常初級。 但是,代理的高級架構已經確定,我想分享一下。
UHK Agent 是一個配置器應用程序,通過它可以定制鍵盤以滿足用戶的需求。 儘管是一個富客戶端,但 Agent 使用 Web 技術並在 node-webkit 平台之上運行。
代理通過發送特殊的、特定於設備的 USB 控制請求並處理其結果,使用 node-usb 庫與鍵盤進行通信。 它使用 Express.js 公開 REST API 以供第三方應用程序使用。 它還使用 Angular.js 來提供簡潔的用戶界面。
var enumerationModes = { 'keyboard' : 0, 'bootloader-right' : 1, 'bootloader-left' : 2 }; function sendReenumerateCommand(enumerationMode, callback) { var AGENT_COMMAND_REENUMERATE = 0; sendAgentCommand(AGENT_COMMAND_REENUMERATE, enumerationMode, callback); } function sendAgentCommand(command, arg, callback) { setReport(new Buffer([command, arg]), callback); } function setReport(message, callback) { device.controlTransfer( 0x21, // bmRequestType (constant for this control request) 0x09, // bmRequest (constant for this control request) 0, // wValue (MSB is report type, LSB is report number) interfaceNumber, // wIndex (interface number) message, // message to be sent callback ); }
每個命令都有一個 8 位標識符和一組特定於命令的參數。 目前,只實現了重新枚舉命令。 sendReenumerateCommand()
使設備重新枚舉為左引導加載程序或右引導加載程序,用於升級固件或作為鍵盤設備。
有人可能不知道這款軟件可以實現的高級功能,所以我僅舉幾例:代理將能夠可視化各個按鍵的磨損情況並通知用戶他們的預期壽命,因此用戶可以為即將進行的維修購買幾個新的鑰匙開關。 代理還將提供一個用戶界面,用於配置黑客鍵盤的各種鍵盤映射和圖層。 還可以設置鼠標指針的速度和加速度,以及許多其他超級功能。 天空是極限。
創建原型
創建定制的鍵盤原型需要做很多工作。 首先,機械設計必須最終確定,這本身就相當複雜,涉及定制設計的塑料部件、激光切割的不銹鋼板、精密銑削的鋼導軌和將兩個鍵盤半部固定在一起的釹磁鐵。 在製造開始之前,一切都在 CAD 中設計。
這是 3D 打印鍵盤盒的外觀:
根據機械設計和原理圖,必須設計印刷電路板。 KiCad 中右側的 PCB 如下所示:
然後 PCB 被製造出來,表面貼裝的元件必須手工焊接:
最後,在製作完所有部件之後,包括 3D 打印、拋光和塗漆塑料部件以及組裝一切,我們最終得到了一個可以工作的黑客鍵盤原型,如下所示:
結論
我喜歡將開發人員的鍵盤與音樂家的樂器進行比較。 如果您考慮一下,鍵盤是相當親密的對象。 畢竟,我們整天都在使用它們來逐個字符地製作明天的軟件。
可能是因為上述原因,我認為開發 Ultimate Hacking Keyboard 是一種特權,儘管困難重重,但它往往是一段非常激動人心的旅程和令人難以置信的緊張學習體驗。
這是一個廣泛的話題,我只能在這裡觸及表面。 我希望這篇文章很有趣,並且充滿了有趣的材料。 如果您有任何問題,請在評論中告訴我。
最後,歡迎您訪問 https://ultimatehackingkeyboard.com 了解更多信息,並在那裡訂閱我們的活動啟動通知。