【手把手教程】從零開始的機械鍵盤固件開發:HWKeyboard.h詳解
前言
大家好,我是鍵盤DIY愛好者Despacito0o!今天想和大家分享我開發的機械鍵盤固件核心頭文件HWKeyboard.h
的設計思路和技術要點。這個項目是我多年來對鍵盤固件研究的心血結晶,希望能幫助更多對單片機開發和鍵盤DIY感興趣的小伙伴入門!
本文將按模塊詳解每部分代碼的具體作用和設計目的,讓完全沒有鍵盤開發經驗的朋友也能一看就懂。后續文章會繼續分享.cpp
文件的實現細節,形成一個完整系列。
一、為什么要自己開發鍵盤固件?
在開始代碼解析前,先聊聊為什么要自己寫鍵盤固件:
- 學習目的:深入理解單片機編程和嵌入式系統
- 個性化需求:市面上的鍵盤功能很難完全滿足個人需求
- DIY樂趣:自己設計的鍵盤、自己寫的固件,用起來格外有成就感
- 開發能力提升:涉及SPI通信、USB協議、RGB驅動等多種技術
二、整體架構設計目的
我設計這個鍵盤固件的主要目標是:
- 模塊化設計:核心功能獨立封裝,便于擴展和維護
- 高效率:采用SPI批量讀取按鍵狀態,降低掃描延遲
- 豐富功能:支持RGB燈效、多層按鍵映射、觸控條等
- 可定制性:預留足夠擴展接口,方便用戶個性化配置
下面就正式開始代碼詳解!
三、HWKeyboard類定義與初始化
#ifndef HELLO_WORD_KEYBOARD_FW_HW_KEYBOARD_H
#define HELLO_WORD_KEYBOARD_FW_HW_KEYBOARD_H#include "spi.h" // 引入SPI相關頭文件,用于與74HC165和WS2812B通信// 硬件鍵盤類定義 - 整合鍵盤所有硬件控制功能
class HWKeyboard
{
public:// 構造函數,傳入已初始化的SPI句柄explicit HWKeyboard(SPI_HandleTypeDef* _spi) :spiHandle(_spi) // 將SPI句柄存儲到類成員變量{scanBuffer = &spiBuffer[1]; // scanBuffer指向spiBuffer的第2個字節,第1個字節用于SPI命令// 使能74HC165芯片(拉低CE引腳激活芯片)HAL_GPIO_WritePin(CE_GPIO_Port,CE_Pin,GPIO_PIN_RESET);// 初始化所有RGB燈為關閉狀態for (uint8_t i = 0; i < HWKeyboard::LED_NUMBER; i++)SetRgbBufferByID(i, HWKeyboard::Color_t{0, 0, 0});}
模塊設計目的:
- 構造函數設計初衷是簡化鍵盤初始化流程,只需傳入一個SPI句柄,就能完成所有硬件初始化
- SPI句柄傳遞意在將底層硬件控制與鍵盤邏輯分離,提高代碼可移植性
- scanBuffer偏移設計是因為SPI傳輸需要命令字節,實際有效數據從第2個字節開始
- CE引腳控制用于激活74HC165移位寄存器,是掃描電路的核心控制信號
- RGB燈初始化為關閉是一個安全設計,避免上電瞬間燈光異常
四、常量定義模塊
// 常量定義區 - 配置鍵盤硬件參數static const uint8_t IO_NUMBER = 11 * 8; // IO總數:11片74HC165,每片8位,共88個IO點static const uint8_t KEY_NUMBER = 82; // 按鍵總數:82個物理按鍵static const uint8_t TOUCHPAD_NUMBER = 6; // 觸控條數量:6個電容觸摸點static const uint8_t LED_NUMBER = 104; // RGB燈數量:104顆WS2812B可編程燈珠static const uint16_t KEY_REPORT_SIZE = 1 + 16; // 鍵盤HID報告長度:1字節報告ID + 16字節鍵盤數據static const uint16_t RAW_REPORT_SIZE = 1 + 32; // 原始報告長度:1字節報告ID + 32字節原始掃描數據static const uint16_t HID_REPORT_SIZE = KEY_REPORT_SIZE + RAW_REPORT_SIZE; // 完整HID報告總長度
模塊設計目的:
- 使用靜態常量明確定義硬件規格,方便后續修改適配不同的鍵盤布局
- IO_NUMBER設為88是為了預留足夠的IO口,實際使用82個物理按鍵
- 分離KEY_NUMBER和IO_NUMBER是考慮到部分IO可能用于特殊功能而非按鍵
- TOUCHPAD_NUMBER定義觸控點數量,用于后續觸控條功能的實現
- HID報告大小嚴格按照USB標準制定,確保與操作系統兼容
五、鍵碼枚舉模塊
// 鍵碼枚舉定義 - 遵循USB HID標準,方便進行按鍵映射enum KeyCode_t : int16_t{/*------------------------- HID報告數據定義 -------------------------*/LEFT_CTRL = -8,LEFT_SHIFT = -7,LEFT_ALT = -6,LEFT_GUI = -5, // 左側修飾鍵(負值方便識別)RIGHT_CTRL = -4,RIGHT_SHIFT = -3,RIGHT_ALT = -2,RIGHT_GUI = -1, // 右側修飾鍵(Windows/Command鍵)RESERVED = 0,ERROR_ROLL_OVER,POST_FAIL,ERROR_UNDEFINED, // 保留鍵值和錯誤碼(0-3)A,B,C,D,E,F,G,H,I,J,K,L,M, // 字母鍵A-M(4-16)N,O,P,Q,R,S,T,U,V,W,X,Y,Z, // 字母鍵N-Z(17-29)NUM_1/*1!*/,NUM_2/*2@*/,NUM_3/*3#*/,NUM_4/*4$*/,NUM_5/*5%*/, // 數字鍵1-5(30-34)NUM_6/*6^*/,NUM_7/*7&*/,NUM_8/*8**/,NUM_9/*9(*/,NUM_0/*0)*/, // 數字鍵6-0(35-39)ENTER,ESC,BACKSPACE,TAB,SPACE, // 常用功能鍵(40-44)MINUS/*-_*/,EQUAL/*=+*/,LEFT_U_BRACE/*[{*/,RIGHT_U_BRACE/*]}*/, // 符號鍵(45-48)BACKSLASH/*\|*/,NONE_US/**/,SEMI_COLON/*;:*/,QUOTE/*'"*/, // 符號鍵(49-52)GRAVE_ACCENT/*`~*/,COMMA/*,<*/,PERIOD/*.>*/,SLASH/*/?*/, // 符號鍵(53-56)// ...(省略部分鍵碼定義以簡化顯示)FN = 1000 // Fn功能鍵,使用1000作為特殊值(超出標準HID范圍)/*------------------------- HID報告數據定義結束 -------------------------*/};
模塊設計目的:
- 用枚舉類型定義所有鍵碼,使代碼更易讀,避免直接使用數字常量
- 修飾鍵使用負值,普通鍵使用正值,便于程序判斷鍵的類型
- 嚴格遵循USB HID標準鍵碼順序,確保與操作系統完全兼容
- FN鍵使用1000這個特殊值,因為它是自定義功能鍵,不屬于標準USB HID鍵碼
- 注釋中標明每個鍵的實際符號,提高代碼可讀性
六、顏色結構體與WS2812B協議定義
// RGB顏色結構體定義 - 存儲單個燈珠的RGB值struct Color_t{uint8_t r; // 紅色分量 (0-255)uint8_t g; // 綠色分量 (0-255)uint8_t b; // 藍色分量 (0-255)};// WS2812B協議字節定義 - SPI模擬WS2812B時序關鍵enum SpiWs2812Byte_t : uint8_t{WS_HIGH = 0xFE, // 表示WS2812B協議中的"1"位 (二進制: 11111110)WS_LOW = 0xE0 // 表示WS2812B協議中的"0"位 (二進制: 11100000)};
模塊設計目的:
- Color_t結構體簡化RGB顏色處理,使設置燈光效果代碼更加直觀
- SpiWs2812Byte_t枚舉是本固件的一個創新點,用SPI模擬WS2812B協議
- 0xFE和0xE0這兩個特殊值經過精確計算,在特定SPI時鐘頻率下恰好滿足WS2812B的時序要求
- 使用枚舉而非直接使用數值,增強代碼可讀性和可維護性
技術拓展:為什么選擇0xFE和0xE0作為WS2812B協議的高低位表示?
WS2812B要求"1"位的高電平持續時間約為800ns,低電平約為450ns;"0"位的高電平約為400ns,低電平約為850ns。按8MHz SPI時鐘計算,一位傳輸需要125ns,因此0xFE(11111110)提供了7位高電平(875ns)和1位低電平(125ns),而0xE0(11100000)提供了3位高電平(375ns)和5位低電平(625ns),非常接近WS2812B的時序要求。
七、功能函數聲明模塊
// 功能函數聲明區 - 鍵盤核心功能接口uint8_t* ScanKeyStates(); // 掃描按鍵狀態,通過SPI讀取74HC165數據void ApplyDebounceFilter(uint32_t _filterTimeUs = 100); // 應用按鍵消抖,消除機械開關抖動uint8_t* Remap(uint8_t _layer = 1); // 按鍵重映射,將物理按鍵轉換為邏輯鍵碼void SyncLights(); // 同步RGB燈光,通過SPI將數據發送到WS2812Bbool FnPressed(); // 檢測Fn鍵是否按下,用于層切換bool KeyPressed(KeyCode_t _key); // 檢測指定鍵碼是否按下,用于組合鍵判斷void Press(KeyCode_t _key); // 模擬按下某鍵,用于宏功能void Release(KeyCode_t _key); // 模擬釋放某鍵,配合Press使用uint8_t* GetHidReportBuffer(uint8_t _reportId); // 獲取HID報告緩沖區,用于USB通信uint8_t GetTouchBarState(uint8_t _id = 0); // 獲取觸控條狀態,實現觸控功能void SetRgbBufferByID(uint8_t _keyId, Color_t _color, float _brightness = 1); // 設置RGB燈顏色和亮度
模塊設計目的:
- 提供完整的功能接口集,將復雜的底層操作封裝成簡單易用的函數
- 遵循單一職責原則,每個函數只負責一個明確的功能,便于調試和維護
- 參數默認值設計,如默認消抖時間100μs、默認使用第1層按鍵映射等,簡化調用
- 函數命名清晰表達功能,如
ScanKeyStates
、ApplyDebounceFilter
等,提高代碼可讀性
八、按鍵映射表模塊
// 按鍵映射表(多層)- 核心功能:實現按鍵多層定義int16_t keyMap[5][IO_NUMBER] = {// 物理按鍵到邏輯鍵的映射(0層,物理布局,標識PCB上按鍵的實際位置索引){67,61,60,58,59,52,55,51,50,49,48,47,46,3,80,81,64,57,62,63,53,54,45,44,40,31,26,18,2,19,70,71,66,65,56,36,37,38,39,43,42,41,28,1,15,74,73,72,68,69,29,30,35,34,33,32,24,0,14,76,77,78,79,16,20,21,22,23,27,25,17,4,13,12,8,75,9,10,7,11,6,5,86,84,82,87,85,83}, // TouchBar索引位置(最后6個值)// 第一層映射(標準QWERTY鍵盤布局,日常使用的基礎層){ESC,F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12,PAUSE,GRAVE_ACCENT,NUM_1,NUM_2,NUM_3,NUM_4,NUM_5,NUM_6,NUM_7,NUM_8,NUM_9,NUM_0,MINUS,EQUAL,BACKSPACE,INSERT,TAB,Q,W,E,R,T,Y,U,I,O,P,LEFT_U_BRACE,RIGHT_U_BRACE,BACKSLASH,DELETE,CAP_LOCK,A,S,D,F,G,H,J,K,L,SEMI_COLON,QUOTE,ENTER,PAGE_UP,LEFT_SHIFT,Z,X,C,V,B,N,M,COMMA,PERIOD,SLASH,RIGHT_SHIFT,UP_ARROW,PAGE_DOWN,LEFT_CTRL,LEFT_GUI,LEFT_ALT,SPACE,RIGHT_ALT,FN,RIGHT_CTRL,LEFT_ARROW,DOWN_ARROW,RIGHT_ARROW},// 第二層映射(自定義功能層,按下Fn鍵時激活){ESC,F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12,PAUSE,GRAVE_ACCENT,NUM_1,NUM_2,NUM_3,NUM_4,NUM_5,NUM_6,NUM_7,NUM_8,NUM_9,NUM_0,MINUS,EQUAL,BACKSPACE,INSERT,TAB,A,B,C,D,E,F,G,H,I,J,LEFT_U_BRACE,RIGHT_U_BRACE,BACKSLASH,DELETE,CAP_LOCK,K,L,M,N,O,P,Q,R,S,SEMI_COLON,QUOTE,ENTER,PAGE_UP,LEFT_SHIFT,T,U,V,W,X,Y,Z,COMMA,PERIOD,SLASH,RIGHT_SHIFT,A,PAGE_DOWN,LEFT_CTRL,LEFT_GUI,LEFT_ALT,SPACE,RIGHT_ALT,FN,RIGHT_CTRL,LEFT_ARROW,DOWN_ARROW,RIGHT_ARROW}};
模塊設計目的:
- 設計多層按鍵映射機制,實現一鍵多功能,大大提高鍵盤的可用性
- 第0層(物理層)存儲每個按鍵在電路中的實際位置索引,不是功能映射
- 第1層是標準QWERTY鍵盤布局,作為默認使用層
- 第2層是演示用的自定義層,將字母區重新排列為ABCDEF順序
- 預留5層空間(
keyMap[5][IO_NUMBER]
),為將來擴展更多功能層提供可能 - 使用前面定義的鍵碼枚舉值,使映射表更加清晰易讀
知識拓展:多層按鍵映射的實際應用
多層按鍵映射是現代機械鍵盤的重要功能,允許在不增加物理按鍵的情況下實現更多功能:
- 媒體控制層:在Fn+F1~F12可以映射為音量控制、播放/暫停等多媒體功能
- 鼠標控制層:將WASD鍵映射為鼠標移動,實現無鼠標操作
- 宏功能層:將常用的按鍵組合映射到單個按鍵,提高工作效率
- 游戲專用層:為不同游戲定制專用按鍵布局
九、狀態標志與私有成員變量
volatile bool isRgbTxBusy; // RGB燈DMA傳輸忙標志,用于中斷同步bool isCapsLocked = false; // 大寫鎖定狀態標志,用于CapsLock LED控制private:SPI_HandleTypeDef* spiHandle; // SPI句柄指針,用于底層硬件通信uint8_t spiBuffer[IO_NUMBER / 8 + 1]{}; // SPI接收緩沖區(每8個IO點占用1字節,外加1字節命令)uint8_t* scanBuffer; // 掃描緩沖區指針,指向spiBuffer中的有效數據部分uint8_t debounceBuffer[IO_NUMBER / 8 + 1]{}; // 按鍵消抖緩沖區,存儲上一次穩定的按鍵狀態uint8_t hidBuffer[HID_REPORT_SIZE]{}; // HID報告緩沖區,用于USB通信uint8_t remapBuffer[IO_NUMBER / 8]{}; // 按鍵重映射緩沖區,存儲邏輯按鍵狀態uint8_t rgbBuffer[LED_NUMBER][3][8]{}; // RGB燈數據緩沖區,3色各8位,存儲WS2812B時序數據uint8_t wsCommit[64] = {0}; // WS2812B協議復位信號緩沖區(至少50μs低電平)uint8_t brightnessPreDiv = 2; // RGB亮度預分頻(值為2表示亮度為1/4)
};#endif
模塊設計目的:
- 公有標志變量:提供給外部訪問的狀態標志
isRgbTxBusy
設計為volatile
是因為它會在中斷中被修改,避免編譯器優化導致的問題isCapsLocked
用于跟蹤大寫鎖定狀態,便于實現CapsLock LED指示
- 私有成員變量:封裝內部數據結構,防止外部直接訪問
- 緩沖區設計遵循數據處理流程:
spiBuffer
→debounceBuffer
→remapBuffer
→hidBuffer
rgbBuffer
特殊設計為三維數組,精確映射WS2812B的時序要求wsCommit
是WS2812B協議結束信號,確保所有LED能正確鎖存數據
- 緩沖區設計遵循數據處理流程:
十、實際應用詳解
下面通過一個具體例子,演示這個鍵盤類的完整工作流程:
// 1. 包含必要頭文件
#include "hw_keyboard.h"
#include "spi.h" // STM32 HAL庫
#include "usbd_hid.h" // USB設備HID庫// 2. 全局變量定義
extern SPI_HandleTypeDef hspi1; // 假設在CubeMX中已配置SPI1
extern USBD_HandleTypeDef hUsbDeviceFS; // USB設備句柄
HWKeyboard myKeyboard(&hspi1); // 創建鍵盤對象// 3. 自定義RGB燈效 - 呼吸燈效果
void breathingEffect(HWKeyboard &kb, uint32_t timeMs) {// 計算亮度值(0-255之間呼吸變化)uint8_t brightness = (sin(timeMs * 0.001f) + 1.0f) * 127.5f;// 設置所有按鍵為相同顏色,但亮度隨時間變化for (uint8_t i = 0; i < HWKeyboard::LED_NUMBER; i++) {// 使用藍色作為基礎顏色,亮度隨時間變化kb.SetRgbBufferByID(i, HWKeyboard::Color_t{0, 0, brightness});}// 同步燈光數據到WS2812Bkb.SyncLights();
}// 4. 主程序循環
void mainLoop() {uint32_t currentTime = HAL_GetTick(); // 獲取當前時間(毫秒)static uint32_t lastReportTime = 0; // 上次發送HID報告的時間static uint32_t lastLightTime = 0; // 上次更新燈光的時間// 4.1 按鍵掃描和處理(1ms周期)if (currentTime - lastReportTime >= 1) {// 掃描按鍵狀態myKeyboard.ScanKeyStates();// 應用消抖濾波myKeyboard.ApplyDebounceFilter();// 檢測Fn鍵狀態,確定當前使用的映射層uint8_t currentLayer = myKeyboard.FnPressed() ? 2 : 1;// 執行按鍵重映射,生成邏輯按鍵狀態myKeyboard.Remap(currentLayer);// 獲取鍵盤HID報告并通過USB發送uint8_t* keyReport = myKeyboard.GetHidReportBuffer(1);USBD_HID_SendReport(&hUsbDeviceFS, keyReport, HWKeyboard::KEY_REPORT_SIZE);// 更新上次發送時間lastReportTime = currentTime;}// 4.2 燈光效果更新(20ms周期,避免頻繁更新造成閃爍)if (currentTime - lastLightTime >= 20 && !myKeyboard.isRgbTxBusy) {// 調用呼吸燈效果函數breathingEffect(myKeyboard, currentTime);// 更新上次燈光更新時間lastLightTime = currentTime;}
}
實現要點解析:
-
初始化流程
- 創建鍵盤對象時只需傳入SPI句柄,簡化初始化
- 構造函數自動完成硬件初始化,無需額外代碼
-
按鍵處理流水線
- 掃描原始按鍵狀態 → 消抖處理 → 層選擇 → 重映射 → 生成HID報告 → 發送USB數據
- 整個流程清晰,每步對應一個函數調用
-
燈光效果實現
- 示例中實現了簡單的呼吸燈效果,適合入門學習
- 使用
isRgbTxBusy
避免在DMA傳輸過程中修改燈光數據 - 燈光更新頻率比按鍵掃描低,避免過度占用CPU資源
-
并行任務處理
- 按鍵掃描和燈光控制使用不同的更新周期,實現并行處理
- 時間戳機制確保任務按照預定間隔執行
十一、開發中的技術難點與解決方案
在開發這個鍵盤固件的過程中,我遇到了幾個關鍵技術難點:
1. 按鍵抖動處理
難點:機械開關按下或釋放時會產生數毫秒的抖動,導致一次按鍵被識別為多次。
解決方案:
- 實現了時間窗口消抖算法,記錄狀態變化點并延遲確認
- 在
ApplyDebounceFilter()
中,通過比較當前狀態與上次穩定狀態來判斷變化 - 當檢測到變化時,等待指定時間(默認100μs)后再次確認,確保狀態穩定
2. SPI模擬WS2812B時序
難點:WS2812B要求嚴格的時序,傳統方法需要精確的延時控制,難以實現。
解決方案:
- 創新地使用SPI接口發送特定字節模式來模擬WS2812B時序
WS_HIGH
和WS_LOW
兩種字節模式在特定SPI頻率下恰好滿足時序要求- 通過DMA傳輸大量數據,避免CPU干預,實現穩定可靠的燈光控制
3. 多層按鍵映射實現
難點:如何高效地實現按鍵層切換,同時保證響應速度。
解決方案:
- 使用二維數組存儲多層映射關系,第一維是層索引,第二維是按鍵索引
- 通過判斷Fn鍵狀態動態選擇當前激活層
Remap()
函數實現從物理按鍵到邏輯按鍵的轉換映射
十二、拓展知識:自制鍵盤的完整流程
想要完全自制一把機械鍵盤,整體流程大致如下:
-
設計鍵盤布局
- 選擇鍵盤尺寸(60%、75%、TKL、全尺寸等)
- 設計鍵位布局(ANSI、ISO或自定義)
-
設計硬件電路
- 選擇單片機(本項目使用STM32F103)
- 設計鍵盤矩陣電路(本項目使用74HC165方案)
- 規劃RGB燈珠布局(WS2812B)
-
PCB設計與制作
- 使用KiCad或Altium Designer設計PCB
- 發送至PCB廠商制作
-
固件開發(本文重點)
- 編寫按鍵掃描代碼
- 實現消抖算法
- 開發多層按鍵映射功能
- 實現RGB燈效控制
- 開發USB通信模塊
-
外殼設計與3D打印
- 使用Fusion 360等軟件設計鍵盤外殼
- 3D打印或CNC加工外殼
-
組裝與調試
- 焊接元器件
- 安裝軸體與鍵帽
- 燒錄固件并調試
總結
本文詳細介紹了一個完整的機械鍵盤固件頭文件設計,從硬件接口到功能實現,逐模塊進行了解析。這個.h
文件為后續.cpp
文件的實現奠定了基礎,定義了清晰的接口和數據結構。
通過這個項目,我們不僅實現了基本的鍵盤功能,還加入了RGB燈效、多層按鍵映射、觸控條等高級特性。希望這篇教程能幫助更多對鍵盤DIY感興趣的朋友入門,為你的定制鍵盤之旅提供參考。
在下一篇文章中,我將分享.cpp
文件的實現細節,敬請期待!
關鍵詞:機械鍵盤固件、單片機編程、SPI通信、WS2812B驅動、多層按鍵映射、HID協議、DIY機械鍵盤、STM32開發
本文作者:Despacito0o | 出處:CSDN | 原創文章,歡迎轉載,請注明出處