瀚文机械键盘固件开发详解:HWKeyboard.h文件解析与应用

发布于:2025-06-05 ⋅ 阅读:(26) ⋅ 点赞:(0)

【手把手教程】从零开始的机械键盘固件开发:HWKeyboard.h详解

前言

大家好,我是键盘DIY爱好者Despacito0o!今天想和大家分享我开发的机械键盘固件核心头文件HWKeyboard.h的设计思路和技术要点。这个项目是我多年来对键盘固件研究的心血结晶,希望能帮助更多对单片机开发和键盘DIY感兴趣的小伙伴入门!

本文将按模块详解每部分代码的具体作用和设计目的,让完全没有键盘开发经验的朋友也能一看就懂。后续文章会继续分享.cpp文件的实现细节,形成一个完整系列。

一、为什么要自己开发键盘固件?

在开始代码解析前,先聊聊为什么要自己写键盘固件:

  1. 学习目的:深入理解单片机编程和嵌入式系统
  2. 个性化需求:市面上的键盘功能很难完全满足个人需求
  3. DIY乐趣:自己设计的键盘、自己写的固件,用起来格外有成就感
  4. 开发能力提升:涉及SPI通信、USB协议、RGB驱动等多种技术

二、整体架构设计目的

我设计这个键盘固件的主要目标是:

  1. 模块化设计:核心功能独立封装,便于扩展和维护
  2. 高效率:采用SPI批量读取按键状态,降低扫描延迟
  3. 丰富功能:支持RGB灯效、多层按键映射、触控条等
  4. 可定制性:预留足够扩展接口,方便用户个性化配置

下面就正式开始代码详解!

三、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将数据发送到WS2812B
    bool 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层按键映射等,简化调用
  • 函数命名清晰表达功能,如ScanKeyStatesApplyDebounceFilter等,提高代码可读性

八、按键映射表模块

    // 按键映射表(多层)- 核心功能:实现按键多层定义
    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指示
  • 私有成员变量:封装内部数据结构,防止外部直接访问
    • 缓冲区设计遵循数据处理流程:spiBufferdebounceBufferremapBufferhidBuffer
    • 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});
    }
    
    // 同步灯光数据到WS2812B
    kb.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;
    }
}

实现要点解析

  1. 初始化流程

    • 创建键盘对象时只需传入SPI句柄,简化初始化
    • 构造函数自动完成硬件初始化,无需额外代码
  2. 按键处理流水线

    • 扫描原始按键状态 → 消抖处理 → 层选择 → 重映射 → 生成HID报告 → 发送USB数据
    • 整个流程清晰,每步对应一个函数调用
  3. 灯光效果实现

    • 示例中实现了简单的呼吸灯效果,适合入门学习
    • 使用isRgbTxBusy避免在DMA传输过程中修改灯光数据
    • 灯光更新频率比按键扫描低,避免过度占用CPU资源
  4. 并行任务处理

    • 按键扫描和灯光控制使用不同的更新周期,实现并行处理
    • 时间戳机制确保任务按照预定间隔执行

十一、开发中的技术难点与解决方案

在开发这个键盘固件的过程中,我遇到了几个关键技术难点:

1. 按键抖动处理

难点:机械开关按下或释放时会产生数毫秒的抖动,导致一次按键被识别为多次。

解决方案

  • 实现了时间窗口消抖算法,记录状态变化点并延迟确认
  • ApplyDebounceFilter()中,通过比较当前状态与上次稳定状态来判断变化
  • 当检测到变化时,等待指定时间(默认100μs)后再次确认,确保状态稳定

2. SPI模拟WS2812B时序

难点:WS2812B要求严格的时序,传统方法需要精确的延时控制,难以实现。

解决方案

  • 创新地使用SPI接口发送特定字节模式来模拟WS2812B时序
  • WS_HIGHWS_LOW两种字节模式在特定SPI频率下恰好满足时序要求
  • 通过DMA传输大量数据,避免CPU干预,实现稳定可靠的灯光控制

3. 多层按键映射实现

难点:如何高效地实现按键层切换,同时保证响应速度。

解决方案

  • 使用二维数组存储多层映射关系,第一维是层索引,第二维是按键索引
  • 通过判断Fn键状态动态选择当前激活层
  • Remap()函数实现从物理按键到逻辑按键的转换映射

十二、拓展知识:自制键盘的完整流程

想要完全自制一把机械键盘,整体流程大致如下:

  1. 设计键盘布局

    • 选择键盘尺寸(60%、75%、TKL、全尺寸等)
    • 设计键位布局(ANSI、ISO或自定义)
  2. 设计硬件电路

    • 选择单片机(本项目使用STM32F103)
    • 设计键盘矩阵电路(本项目使用74HC165方案)
    • 规划RGB灯珠布局(WS2812B)
  3. PCB设计与制作

    • 使用KiCad或Altium Designer设计PCB
    • 发送至PCB厂商制作
  4. 固件开发(本文重点)

    • 编写按键扫描代码
    • 实现消抖算法
    • 开发多层按键映射功能
    • 实现RGB灯效控制
    • 开发USB通信模块
  5. 外壳设计与3D打印

    • 使用Fusion 360等软件设计键盘外壳
    • 3D打印或CNC加工外壳
  6. 组装与调试

    • 焊接元器件
    • 安装轴体与键帽
    • 烧录固件并调试

总结

本文详细介绍了一个完整的机械键盘固件头文件设计,从硬件接口到功能实现,逐模块进行了解析。这个.h文件为后续.cpp文件的实现奠定了基础,定义了清晰的接口和数据结构。

通过这个项目,我们不仅实现了基本的键盘功能,还加入了RGB灯效、多层按键映射、触控条等高级特性。希望这篇教程能帮助更多对键盘DIY感兴趣的朋友入门,为你的定制键盘之旅提供参考。

在下一篇文章中,我将分享.cpp文件的实现细节,敬请期待!


关键词:机械键盘固件、单片机编程、SPI通信、WS2812B驱动、多层按键映射、HID协议、DIY机械键盘、STM32开发

本文作者:Despacito0o | 出处:CSDN | 原创文章,欢迎转载,请注明出处


网站公告

今日签到

点亮在社区的每一天
去签到