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

🔥 机械键盘固件开发从入门到精通:HWKeyboard模块全解析

作为一名嵌入式开发老司机,今天带大家拆解一个完整的机械键盘固件代码。即使你是单片机小白,看完这篇教程也能轻松理解机械键盘的工作原理,甚至自己动手复刻一个!

🚀 项目整体概览

这个hw_keyboard.cpp模块实现了一个完整的机械键盘固件,基于STM32单片机开发,主要功能包括:

  • 按键矩阵扫描与状态读取
  • 机械按键消抖处理
  • 键位映射与HID协议数据生成
  • RGB灯效控制(支持WS2812B灯带)
  • 特殊功能键(Fn键、触控条)处理

整个固件就像一个智能中转站,把物理按键的按下抬起动作,转换成电脑能理解的键盘信号,同时控制炫酷的RGB灯光效果。

🧩 核心模块拆解

一、延时函数:精确掌控时间

cpp 复制代码
inline void DelayUs(uint32_t _us)
{
    for (int i = 0; i < _us; i++)              // 外层循环,每次循环代表1微秒
        for (int j = 0; j < 8; j++)            // 内层循环,调整延时时长(不同芯片需调整)
            __NOP();                           // 空操作,单纯耗时
}
白话解析:
  • 这是一个微秒级的延时函数,精确控制程序暂停的时间
  • 双重循环结构,外层控制延迟多少微秒,内层循环次数需要根据芯片主频调整
  • __NOP()是"No Operation"的缩写,就是让CPU空转一个周期
  • 为什么需要它?键盘需要精确的时间控制,比如消抖、LED控制都需要

二、按键扫描模块:监听键盘上的每一次敲击

cpp 复制代码
uint8_t* HWKeyboard::ScanKeyStates()
{
    memset(spiBuffer, 0xFF, IO_NUMBER / 8 + 1);    // 将spiBuffer缓冲区全部置为0xFF,准备接收数据
    PL_GPIO_Port->BSRR = PL_Pin;                   // 设置锁存引脚为高电平,锁存当前按键状态

    spiHandle->pRxBuffPtr = (uint8_t*) spiBuffer;  // 设置SPI接收缓冲区指针
    spiHandle->RxXferCount = IO_NUMBER / 8 + 1;    // 设置SPI接收字节数
    __HAL_SPI_ENABLE(spiHandle);                   // 使能SPI外设
    while (spiHandle->RxXferCount > 0U)            // 循环直到所有数据接收完毕
    {
        if (__HAL_SPI_GET_FLAG(spiHandle, SPI_FLAG_RXNE)) // 检查SPI接收缓冲区非空标志
        {
            (*(uint8_t*) spiHandle->pRxBuffPtr) = *(__IO uint8_t*) &spiHandle->Instance->DR; // 从SPI数据寄存器读取数据到缓冲区
            spiHandle->pRxBuffPtr += sizeof(uint8_t); // 指针后移
            spiHandle->RxXferCount--;                 // 剩余接收字节数减一
        }
    }
    __HAL_SPI_DISABLE(spiHandle);                     // 禁用SPI外设

    PL_GPIO_Port->BRR = PL_Pin;                       // 设置锁存引脚为低电平,完成采样
    return scanBuffer;                                // 返回扫描缓冲区指针
}
白话解析:
  • 这个函数就像键盘的"耳朵",不断监听按键是否被按下
  • PL_Pin是一个特殊引脚,拉高时会把所有按键的状态"拍照"保存
  • SPI通信就像快递系统:
    • spiBuffer是装数据的袋子
    • pRxBuffPtr是指向袋子的手指
    • RxXferCount是需要接收的数据数量
  • 数据接收完成后,返回的scanBuffer里存着所有按键的当前状态(1表示未按下,0表示按下)

三、按键消抖模块:解决机械按键的"手抖"问题

cpp 复制代码
void HWKeyboard::ApplyDebounceFilter(uint32_t _filterTimeUs)
{
    memcpy(debounceBuffer, spiBuffer, IO_NUMBER / 8 + 1); // 备份当前SPI缓冲区到消抖缓冲区

    DelayUs(_filterTimeUs); // 延时一段时间,等待抖动消除
    ScanKeyStates();        // 再次扫描按键状态

    uint8_t mask;
    for (int i = 0; i < IO_NUMBER / 8 + 1; i++) // 遍历所有字节
    {
        mask = debounceBuffer[i] ^ spiBuffer[i]; // 计算两次扫描的不同位
        spiBuffer[i] |= mask;                    // 将有变化的位强制置为1(消除抖动影响)
    }
}
白话解析:
  • 机械按键按下时会像"手抖"一样产生短暂的多次通断,这就是抖动
  • 消抖处理就像拍照时的防抖功能:
    1. 先保存第一次拍的照片(按键状态)
    2. 等一小会儿(_filterTimeUs微秒)
    3. 再拍一张照片(再次扫描按键)
    4. 比较两张照片,如果有不同,就认为是"抖动",修正这些差异
  • 这个过程很重要,否则键盘会误判你按了多次键

四、键位映射模块:把物理按键变成电脑认识的键

cpp 复制代码
uint8_t* HWKeyboard::Remap(uint8_t _layer)
{
    int16_t index, bitIndex; // 定义索引变量

    memset(remapBuffer, 0, IO_NUMBER / 8); // 清空重映射缓冲区
    for (int16_t i = 0; i < IO_NUMBER / 8; i++) // 遍历每个字节
    {
        for (int16_t j = 0; j < 8; j++) // 遍历每个bit
        {
            index = (int16_t) (keyMap[0][i * 8 + j] / 8);         // 计算当前物理按键在scanBuffer中的字节索引
            bitIndex = (int16_t) (keyMap[0][i * 8 + j] % 8);      // 计算当前物理按键在该字节中的位索引
            if (scanBuffer[index] & (0x80 >> bitIndex))           // 检查该物理按键是否被按下(高电平为未按下,低电平为按下)
                remapBuffer[i] |= 0x80 >> j;                      // 如果按下,则在remapBuffer中对应位置标记为1
        }
        remapBuffer[i] = ~remapBuffer[i];                         // 取反,转换为"按下为1,未按下为0"
    }

    memset(hidBuffer, 0, KEY_REPORT_SIZE);                        // 清空HID报告缓冲区

    int i = 0, j = 0;
    while (8 * i + j < IO_NUMBER - 6)                             // 遍历所有可用按键(排除最后6个保留位)
    {
        for (j = 0; j < 8; j++)
        {
            index = (int16_t) (keyMap[_layer][i * 8 + j] / 8 + 1); // 计算映射后按键在hidBuffer中的字节索引(+1跳过修饰键)
            bitIndex = (int16_t) (keyMap[_layer][i * 8 + j] % 8);  // 计算映射后按键在该字节中的位索引
            if (bitIndex < 0)
            {
                index -= 1;                                        // 位索引为负时,向前借一字节
                bitIndex += 8;
            } else if (index > 100)
                continue;                                          // 越界保护

            if (remapBuffer[i] & (0x80 >> j))                      // 如果该按键被按下
                hidBuffer[index + 1] |= 1 << (bitIndex);           // 在hidBuffer中对应位置标记为1(+1跳过Report-ID)
        }
        i++;
        j = 0;
    }

    return hidBuffer;                                              // 返回HID报告缓冲区
}
白话解析:
  • 这个模块就像一个"翻译官",把物理按键的位置翻译成电脑认识的键码
  • 过程分两大步:
    1. 第一步:把原始扫描结果(scanBuffer)转换成中间格式(remapBuffer
      • 这一步是把"物理按键位置"变成"逻辑按键位置"
      • 使用keyMap[0]查表,找到每个物理按键对应的逻辑位置
    2. 第二步:把中间格式(remapBuffer)转换成USB-HID标准格式(hidBuffer
      • 这一步是把"逻辑按键位置"变成"标准键码"
      • 使用keyMap[_layer]查表,支持多层键位映射(比如Fn组合键)
  • 最终生成的hidBuffer就是可以直接发送给电脑的USB-HID报告

五、RGB灯效控制模块:让键盘"发光发热"

cpp 复制代码
void HWKeyboard::SetRgbBufferByID(uint8_t _keyId, HWKeyboard::Color_t _color, float _brightness)
{
    // 防止全0导致ws2812b协议错误
    if (_color.b < 1)_color.b = 1;                                 // 蓝色分量最小为1,避免全0

    for (int i = 0; i < 8; i++)                                   // 遍历8位
    {
        rgbBuffer[_keyId][0][i] =
            ((uint8_t) ((float) _color.g * _brightness) >> brightnessPreDiv) & (0x80 >> i) ? WS_HIGH : WS_LOW; // 绿色分量
        rgbBuffer[_keyId][1][i] =
            ((uint8_t) ((float) _color.r * _brightness) >> brightnessPreDiv) & (0x80 >> i) ? WS_HIGH : WS_LOW; // 红色分量
        rgbBuffer[_keyId][2][i] =
            ((uint8_t) ((float) _color.b * _brightness) >> brightnessPreDiv) & (0x80 >> i) ? WS_HIGH : WS_LOW; // 蓝色分量
    }
}

void HWKeyboard::SyncLights()
{
    while (isRgbTxBusy);                                           // 等待上一次DMA传输完成
    isRgbTxBusy = true;                                            // 标记DMA忙
    HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*) rgbBuffer, LED_NUMBER * 3 * 8); // 通过DMA发送RGB数据
    while (isRgbTxBusy);                                           // 等待DMA完成
    isRgbTxBusy = true;                                            // 再次标记DMA忙
    HAL_SPI_Transmit_DMA(&hspi2, wsCommit, 64);                    // 发送ws2812b协议结尾信号
}
白话解析:
  • 这个模块负责控制键盘上的RGB灯,让键盘变得炫酷
  • SetRgbBufferByID函数像是一支神奇的画笔:
    • _keyId:选择要涂色的灯珠
    • _color:选择RGB颜色(红、绿、蓝三原色)
    • _brightness:控制颜色的亮度(0.0-1.0)
  • WS2812B是一种智能LED灯珠,需要特殊的信号格式:
    • 每个灯珠需要24位数据(8位绿+8位红+8位蓝)
    • WS_HIGHWS_LOW是两种不同的电平时序,用来表示1和0
    • 所有灯珠串联在一起,数据像多米诺骨牌一样传递
  • SyncLights函数使用DMA(直接内存访问)技术快速发送数据:
    • DMA可以在不占用CPU的情况下传输数据
    • 发送完所有LED数据后,还要发送一个结束信号(wsCommit

六、特殊功能键处理模块:Fn键和触控条

cpp 复制代码
bool HWKeyboard::FnPressed()
{
    return remapBuffer[9] & 0x02;                                  // 检查remapBuffer第9字节的第2位(Fn键状态)
}

uint8_t HWKeyboard::GetTouchBarState(uint8_t _id)
{
    uint8_t tmp = (remapBuffer[10] & 0b00000001) << 5 |            // 取remapBuffer第10字节的各个位,重新排列组合
              (remapBuffer[10] & 0b00000010) << 3 |
              (remapBuffer[10] & 0b00000100) << 1 |
              (remapBuffer[10] & 0b00001000) >> 1 |
              (remapBuffer[10] & 0b00010000) >> 3 |
              (remapBuffer[10] & 0b00100000) >> 5;
    return _id == 0 ? tmp : (tmp & (1 << (_id - 1)));              // 返回全部状态或指定触控条状态
}
白话解析:
  • 这部分处理键盘上的特殊功能键:Fn键和触控条
  • FnPressed函数检查Fn键是否按下:
    • 简单查看remapBuffer中的特定位,1表示按下,0表示未按下
    • Fn键在这个键盘中位于第9字节的第2位(从0开始计数)
  • GetTouchBarState函数读取触控条状态:
    • 触控条有多个触摸点,每个点对应remapBuffer[10]的一位
    • 函数进行位重排,使触摸点按从左到右的顺序排列
    • 参数_id为0时返回所有触摸点状态,否则返回特定触摸点状态

七、HID报告处理模块:电脑与键盘的"对话"

cpp 复制代码
uint8_t* HWKeyboard::GetHidReportBuffer(uint8_t _reportId)
{
    switch (_reportId)
    {
        case 1:
            hidBuffer[0] = 1;                                      // 设置报告ID为1
            return hidBuffer;                                      // 返回主报告缓冲区
        case 2:
            hidBuffer[KEY_REPORT_SIZE] = 2;                        // 设置报告ID为2
            return hidBuffer + KEY_REPORT_SIZE;                    // 返回备用报告缓冲区
        default:
            return hidBuffer;                                      // 默认返回主报告缓冲区
    }
}

bool HWKeyboard::KeyPressed(KeyCode_t _key)
{
    int index, bitIndex;

    if (_key < RESERVED)                                           // 判断是否为保留键
    {
        index = _key / 8;                                          // 计算字节索引
        bitIndex = (_key + 8) % 8;                                 // 计算位索引
    } else
    {
        index = _key / 8 + 1;                                      // 计算字节索引(跳过修饰键)
        bitIndex = _key % 8;                                       // 计算位索引
    }

    return hidBuffer[index + 1] & (1 << bitIndex);                 // 检查对应位是否为1(按下)
}

void HWKeyboard::Press(HWKeyboard::KeyCode_t _key)
{
    int index, bitIndex;

    if (_key < RESERVED)
    {
        index = _key / 8;
        bitIndex = (_key + 8) % 8;
    } else
    {
        index = _key / 8 + 1;
        bitIndex = _key % 8;
    }

    hidBuffer[index + 1] |= (1 << bitIndex);                       // 设置对应位为1(按下)
}

void HWKeyboard::Release(HWKeyboard::KeyCode_t _key)
{
    int index, bitIndex;

    if (_key < RESERVED)
    {
        index = _key / 8;
        bitIndex = (_key + 8) % 8;
    } else
    {
        index = _key / 8 + 1;
        bitIndex = _key % 8;
    }

    hidBuffer[index + 1] &= ~(1 << bitIndex);                      // 清除对应位(释放)
}
白话解析:
  • 这部分处理键盘的HID报告,这是键盘与电脑通信的"官方语言"
  • GetHidReportBuffer函数准备不同类型的HID报告:
    • 报告ID 1:标准键盘报告
    • 报告ID 2:扩展功能报告(如多媒体键、自定义功能键)
  • KeyPressed函数检查某个键是否被按下:
    • 通过计算键码在HID报告中的位置(字节索引和位索引)
    • 特殊处理小于RESERVED的键(可能是修饰键如Ctrl、Shift等)
  • PressRelease函数模拟按键按下和释放:
    • 直接修改HID报告缓冲区中对应键的状态位
    • 这允许程序在不实际按键的情况下发送按键信号

📊 完整工作流程

一个按键从按下到被电脑识别的全过程:

  1. 硬件初始化

    • 设置SPI通信参数
    • 配置GPIO引脚
    • 初始化RGB灯为熄灭状态
  2. 按键扫描循环

    cpp 复制代码
    while(1) {
        ScanKeyStates();              // 扫描按键矩阵,读取原始状态
        ApplyDebounceFilter(5000);    // 应用5ms消抖滤波
        uint8_t layer = FnPressed() ? 1 : 0;  // 根据Fn键状态选择映射层
        Remap(layer);                 // 重映射键位,生成HID报告
        
        // 发送HID报告给电脑
        uint8_t* report = GetHidReportBuffer(1);
        USB_SendData(report, KEY_REPORT_SIZE);
        
        // 更新RGB灯效
        UpdateRgbEffects();
        SyncLights();
        
        HAL_Delay(10);  // 10ms扫描周期
    }
  3. 关键环节解析

    • 按键扫描:使用SPI读取74HC165移位寄存器中的按键状态
    • 消抖处理:比较两次扫描结果,忽略抖动引起的差异
    • 重映射处理:物理按键位置→逻辑按键位置→标准HID键码
    • RGB控制:设置每个LED的RGB值,通过DMA高速传输数据
    • USB通信:定期发送HID报告给电脑,告知当前按键状态

💡 小白开发指南

开发环境搭建

  1. 硬件准备

    • STM32F1/F4系列单片机(如STM32F103C8T6)
    • 74HC165移位寄存器(扩展输入IO)
    • WS2812B RGB灯珠
    • 机械键盘轴体和轴座
    • PCB电路板
  2. 软件工具

    • STM32CubeIDE或Keil MDK(代码编写和编译)
    • STM32CubeMX(单片机外设配置)
    • PCB设计软件(如立创EDA、Altium Designer)

从零开始的实现步骤

  1. 项目结构设计

    复制代码
    - main.c         // 主程序入口
    - hw_keyboard.h  // HWKeyboard类声明
    - hw_keyboard.cpp // HWKeyboard类实现
    - usb_device.c   // USB设备配置
    - key_map.h      // 键位映射表
  2. 关键硬件连接

    复制代码
    STM32 SPI1_MISO <- 74HC165 QH (串行数据输出)
    STM32 SPI1_CLK -> 74HC165 CLK (时钟信号)
    STM32 GPIO_PL -> 74HC165 PL (锁存信号)
    
    STM32 SPI2 -> WS2812B数据线(通过电平转换)
  3. 代码实现步骤

    • 实现ScanKeyStates函数,通过SPI读取按键状态
    • 添加ApplyDebounceFilter消抖处理
    • 实现Remap函数,完成键位映射
    • 添加RGB灯效控制函数
    • 最后实现USB通信部分
  4. 测试调试方法

    • 分阶段测试:先测试按键扫描,再测试灯效控制
    • 使用串口打印中间变量进行调试
    • 使用示波器观察SPI和WS2812B信号波形

📚 进阶知识点

1. 如何定制键位映射

键位映射是通过keyMap二维数组实现的:

cpp 复制代码
// 示例键位映射表(简化版)
const uint16_t keyMap[2][64] = {
    // Layer 0: 标准层
    {
        KEY_ESC, KEY_1, KEY_2, KEY_3, /* 更多键... */
    },
    // Layer 1: Fn层
    {
        KEY_GRAVE, KEY_F1, KEY_F2, KEY_F3, /* 更多键... */
    }
};

定制步骤:

  1. 测量物理按键矩阵位置
  2. 确定每个位置对应的标准键码(参考USB HID标准)
  3. 填写到keyMap数组中

2. RGB灯效编程技巧

cpp 复制代码
// 彩虹灯效示例
void RainbowEffect() {
    static uint8_t hue = 0;
    for(int i = 0; i < LED_NUMBER; i++) {
        // 创建彩虹色相滚动效果
        Color_t color = HsvToRgb(hue + i * 255 / LED_NUMBER, 255, 255);
        keyboard.SetRgbBufferByID(i, color, 0.5f); // 亮度50%
    }
    keyboard.SyncLights();
    hue++; // 颜色循环移动
}

// HSV转RGB颜色转换
Color_t HsvToRgb(uint8_t h, uint8_t s, uint8_t v) {
    Color_t rgb = {0, 0, 0};
    // 转换算法实现
    // ...
    return rgb;
}

3. 性能优化技巧

  1. 扫描频率优化

    • 降低扫描频率可节省CPU资源
    • 但过低会导致输入延迟
    • 推荐扫描频率:100Hz(10ms周期)
  2. DMA使用

    • 使用DMA传输RGB数据,释放CPU资源
    • 使用中断而非轮询等待DMA完成
  3. 内存优化

    • 使用位操作减少内存使用
    • 共用缓冲区减少RAM占用

🎯 实战项目:DIY全彩RGB机械键盘

完成这个教程后,你可以尝试以下项目:

  1. 简易版:61键迷你键盘

    • 标准QWERTY布局
    • 单色背光
    • 两层键位映射
  2. 进阶版:64键配置RGB

    • 增加方向键
    • 全RGB背光
    • 多种灯效模式
  3. 大师版:分体式人体工学键盘

    • 左右分离设计
    • 每键RGB可寻址
    • 支持无线蓝牙连接

通过本教程的学习,你已经掌握了机械键盘固件开发的核心技术。从简单的按键扫描到复杂的RGB控制,从底层硬件操作到高层次的用户体验,一步步揭开了机械键盘的神秘面纱。希望这份教程能帮助你开启DIY键盘的奇妙旅程!

相关推荐
一屉大大大花卷30 分钟前
初识Neo4j之入门介绍(一)
数据库·neo4j
周胡杰1 小时前
鸿蒙arkts使用关系型数据库,使用DB Browser for SQLite连接和查看数据库数据?使用TaskPool进行频繁数据库操作
前端·数据库·华为·harmonyos·鸿蒙·鸿蒙系统
wkj0011 小时前
navicate如何设置数据库引擎
数据库·mysql
赵渝强老师1 小时前
【赵渝强老师】Oracle RMAN的目录数据库
数据库·oracle
暖暖木头1 小时前
Oracle注释详解
数据库·oracle
御控工业物联网1 小时前
御控网关如何实现MQTT、MODBUS、OPCUA、SQL、HTTP之间协议转换
数据库·sql·http
GJCTYU3 小时前
spring中@Transactional注解和事务的实战理解附代码
数据库·spring boot·后端·spring·oracle·mybatis
MicroTech20253 小时前
微算法科技(NASDAQ: MLGO)探索Grover量子搜索算法,利用量子叠加和干涉原理,实现在无序数据库中快速定位目标信息的效果。
数据库·科技·算法
Code季风3 小时前
SQL关键字快速入门:CASE 实现条件逻辑
javascript·数据库·sql
weixin_478689763 小时前
操作系统【2】【内存管理】【虚拟内存】【参考小林code】
数据库·nosql