基于STM32的USB键盘制作(保姆级)(三)

 

第一节  USB协议及建立USB-HID工程

第二节  配置描述符及HID报文格式

第三节  PCB按键映射(基于稚晖君开源)


 

 

文章目录

 


前言

本节主要讲稚晖君键盘按键实现映射的原理。详细开源的资料链接:peng-zhihui/HelloWord-Keyboard (github.com)

文章较长,对比着原理图理解,比较容易懂。

关于键盘的实现在Firmware文件夹中,映射算法在hw_keyboard.cpp中。

在看此文前,建议先看一遍74HC165的工作原理以及稚晖君的键盘原理图。

推荐看这位大佬:74HC165基础篇(一)_壹二叁的博客-CSDN博客

74HC165级联电路篇(三)_壹二叁的博客-CSDN博客


 

一、相关描述符简述

稚晖君的配置符(usbd_.customhid.c中),这里设置的最大输出电流为500ma,设置了输入输出两个端点(输入输出端点大小分别由宏CUSTOM_HID_EPOUT_SIZE,CUSTOM_HID_EPIN_SIZE定义)。因为在HID报文描述符中,使用了LED报文,由电脑输出,故定义了一个输出端点。其他定义基本默认。

__ALIGN_BEGIN static uint8_t USBD_CUSTOM_HID_CfgFSDesc[USB_CUSTOM_HID_CONFIG_DESC_SIZ] __ALIGN_END =
{
  0x09, /* bLength: Configuration Descriptor size */
  USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
  USB_CUSTOM_HID_CONFIG_DESC_SIZ,  /* wTotalLength: Bytes returned */
  0x00,
  0x01,         /*bNumInterfaces: 1 interface*/
  0x01,         /*bConfigurationValue: Configuration value*/
  0x00,         /*iConfiguration: Index of string descriptor describing
  the configuration*/
  0xC0,         /*bmAttributes: bus powered */
  0xFA,         /*MaxPower 500 mA: this current is used for detecting Vbus*/

  /************** Descriptor of CUSTOM HID interface ****************/
  /* 09 */
  0x09,         /*bLength: Interface Descriptor size*/
  USB_DESC_TYPE_INTERFACE,/*bDescriptorType: Interface descriptor type*/
  0x00,         /*bInterfaceNumber: Number of Interface*/
  0x00,         /*bAlternateSetting: Alternate setting*/
  0x02,         /*bNumEndpoints*/
  0x03,         /*bInterfaceClass: CUSTOM_HID*/
  0x01,         /*bInterfaceSubClass : 1=BOOT, 0=no boot*/
  0x01,         /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
  0,            /*iInterface: Index of string descriptor*/
  /******************** Descriptor of CUSTOM_HID *************************/
  /* 18 */
  0x09,         /*bLength: CUSTOM_HID Descriptor size*/
  CUSTOM_HID_DESCRIPTOR_TYPE, /*bDescriptorType: CUSTOM_HID*/
  0x11,         /*bCUSTOM_HIDUSTOM_HID: CUSTOM_HID Class Spec release number*/
  0x01,
  0x21,         /*bCountryCode: Hardware target country*/
  0x01,         /*bNumDescriptors: Number of CUSTOM_HID class descriptors to follow*/
  0x22,         /*bDescriptorType*/
  USBD_CUSTOM_HID_REPORT_DESC_SIZE,/*wItemLength: Total length of Report descriptor*/
  0x00,
  /******************** Descriptor of Custom HID endpoints ********************/
  /* 27 */
  0x07,          /*bLength: Endpoint Descriptor size*/
  USB_DESC_TYPE_ENDPOINT, /*bDescriptorType:*/

  CUSTOM_HID_EPIN_ADDR,     /*bEndpointAddress: Endpoint Address (IN)*/
  0x03,          /*bmAttributes: Interrupt endpoint*/
  CUSTOM_HID_EPIN_SIZE, /*wMaxPacketSize: 2 Byte max */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,          /*bInterval: Polling Interval */
  /* 34 */

  0x07,          /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT, /* bDescriptorType: */
  CUSTOM_HID_EPOUT_ADDR,  /*bEndpointAddress: Endpoint Address (OUT)*/
  0x03, /* bmAttributes: Interrupt endpoint */
  CUSTOM_HID_EPOUT_SIZE,  /* wMaxPacketSize: 2 Bytes max  */
  0x00,
  CUSTOM_HID_FS_BINTERVAL,  /* bInterval: Polling Interval */
  /* 41 */
};

HID报文描述符:(在customhid_if.c文件中)CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END

可以看见这里用了LED输出报文,主要是控制RGB等效,故上面的配置描述符中增加了输出端点。

4a4684635b724924bf37536bea01c071.png

之后的这一段好像是为了后续BOOTLOADER升级使用,笔者未深究,不作分析。

5f559fed3f6149d1863556269fac86f7.png

对应的键盘码报文使用的 报文page的第七页,故枚举体也是跟这个对应的,详细讲解看上一节。

(虽然定义了128个按键,但是不一定都用完了的,可选择其中任意的使用)

08134aa4fa2746a7a496fb5afb67bd75.png

enum KeyCode_t : int16_t
    {
        /*------------------------- HID report data -------------------------*/
        LEFT_CTRL = -8,LEFT_SHIFT = -7,LEFT_ALT = -6,LEFT_GUI = -5,
        RIGHT_CTRL = -4,RIGHT_SHIFT = -3,RIGHT_ALT = -2,RIGHT_GUI = -1,

        RESERVED = 0,ERROR_ROLL_OVER,POST_FAIL,ERROR_UNDEFINED,
        A,B,C,D,E,F,G,H,I,J,K,L,M,
        N,O,P,Q,R,S,T,U,V,W,X,Y,Z,
        NUM_1/*1!*/,NUM_2/*2@*/,NUM_3/*3#*/,NUM_4/*4$*/,NUM_5/*5%*/,
        NUM_6/*6^*/,NUM_7/*7&*/,NUM_8/*8**/,NUM_9/*9(*/,NUM_0/*0)*/,
        ENTER,ESC,BACKSPACE,TAB,SPACE,
        MINUS/*-_*/,EQUAL/*=+*/,LEFT_U_BRACE/*[{*/,RIGHT_U_BRACE/*]}*/,
        BACKSLASH/*\|*/,NONE_US/**/,SEMI_COLON/*;:*/,QUOTE/*'"*/,
        GRAVE_ACCENT/*`~*/,COMMA/*,<*/,PERIOD/*.>*/,SLASH/*/?*/,
        CAP_LOCK,F1,F2,F3,F4,F5,F6,F7,F8,F9,F10,F11,F12,
        PRINT,SCROLL_LOCK,PAUSE,INSERT,HOME,PAGE_UP,DELETE,END,PAGE_DOWN,
        RIGHT_ARROW,LEFT_ARROW,DOWN_ARROW,UP_ARROW,PAD_NUM_LOCK,
        PAD_SLASH,PAD_ASTERISK,PAD_MINUS,PAD_PLUS,PAD_ENTER,
        PAD_NUM_1,PAD_NUM_2,PAD_NUM_3,PAD_NUM_4,PAD_NUM_5,
        PAD_NUM_6,PAD_NUM_7,PAD_NUM_8,PAD_NUM_9,PAD_NUM_0,
        PAD_PERIOD , NONUS_BACKSLASH,APPLICATION,POWER,PAD_EQUAL,
        F13,F14,F15,F16,F17,F18,F19,F20,F21,F22,F23,F24, EXECUTE,
        HELP,MENU,SELECT,STOP,AGAIN,UNDO,CUT,COPY,PASTE,FIND,MUTE,VOLUME_UP,VOLUME_DOWN,
        FN = 1000
        /*------------------------- HID report data -------------------------*/
    };

 

二、映射原理

1.转载部分:(怎么用这个映射)

关于键盘固件的按键映射方式?

为了充分发挥视频中提到的移位寄存器扫描方案的优势,固件代码中将PCB Layout走线和按键扫描顺序解耦,通过软件进行重映射。也就是说PCB中按键的连接可以是任意的,走完线之后可以在hw_keyboard.h文件中的keyMap[KEYMAP_NUM][IO_NUMBER]中指定映射方式。

这是一个二维数组,代表有KEYMAP_NUM层键位映射,每一层有IO_NUMBER个按键(也就是你的键盘按键数目);其中第0层是特殊的,负责映射PCB按键的随机布局到键盘标准按键布局,后续的1、2、3、4...层都是自定义的,负责映射标准按键布局到任意布局。

755a9796e8746e6b710e70434f460106.jpeg

举个例子:

考虑原理图中箭头指的那个按键,这个按键可以在PCB的任意位置,但是我们可以看到,它是从左到右(按74HC165的连接顺序,也即移位扫描顺序)的第10颗,因此它的编号为9(从0开始算).

如果我们在实际的PCB板上把它放在了右边Alt的位置,那么参考在下图代码红色框中的第1层映射(也就是标准布局)中的RIGHT_ALT的序号是76,那么在第0层映射的76号变量就填入9(蓝色框).

这样依次把你PCB上所有按键都填入0层映射,就得到了一个映射好的标准键盘了。后续2、3、4、5...层需要怎么映射就随意修改添加即可,也不需要再使用数字编号,而是可以直接用枚举的按键名称很方便。

ca78596c40236b2e0933ce7b29156bf9.jpeg

所以对于想修改键盘配列的人,只需要再原理图上添加或删减几个串联的74HC165,然后PCB随意走线,再将代码中0层映射删减或增加一些数字即可(比如在下面的例子中我的键盘是83键的);后面几层的修改就以此类推了。
代码中通过keyboard.Remap函数来映射不同层,比如keyboard.Remap(2)这一句是使用第2层映射。

2.原理分析

首先找到这个函数:这个函数即一个完整的扫描过程

86be21086d7c462da6c7ec7c832cb5d7.png

对其分析,其大概顺序为先扫描一遍所有按键的状态,之后进入消抖函数(Apply..),然后进行按键重映射(因为PCB布线是随机布的,故需要映射到标准按键)。重映射后,发送HID报文,汇报按下了哪些键,即一个扫描过程实现完成。

 

首先看获取按键状态的函数,spibuffer即是存储各按键的状态,此时的顺序对应74HC165连接按键的顺序,第一个bit就是第一个按键的状态(0为按下,1为弹起)。IO_NUMBER表示共接了多少个IO口按键,大小为74HC165的片数*8.

uint8_t* HWKeyboard::ScanKeyStates()
{
    memset(spiBuffer, 0xFF, IO_NUMBER / 8 + 1);
    PL_GPIO_Port->BSRR = PL_Pin; // Latch

    spiHandle->pRxBuffPtr = (uint8_t*) spiBuffer;
    spiHandle->RxXferCount = IO_NUMBER / 8 + 1;
    __HAL_SPI_ENABLE(spiHandle);
    while (spiHandle->RxXferCount > 0U)
    {
        if (__HAL_SPI_GET_FLAG(spiHandle, SPI_FLAG_RXNE))
        {
            /* read the received data */
            (*(uint8_t*) spiHandle->pRxBuffPtr) = *(__IO uint8_t*) &spiHandle->Instance->DR;
            spiHandle->pRxBuffPtr += sizeof(uint8_t);
            spiHandle->RxXferCount--;
        }
    }
    __HAL_SPI_DISABLE(spiHandle);

    PL_GPIO_Port->BRR = PL_Pin; // Sample
    return scanBuffer;
}

这是一个指针函数,返回的scanBuffer这个指针,而这个指针指向的就是spiBuffer这个数组的首地址。下图为scanBuffer定义的地方。

d0fc81cae5f9435583ac267c169b9bde.png

 

获取完状态后,按顺序该进入按键消抖函数:ApplyDebounceFilter。

void HWKeyboard::ApplyDebounceFilter(uint32_t _filterTimeUs)
{
    memcpy(debounceBuffer, spiBuffer, IO_NUMBER / 8 + 1);

    DelayUs(_filterTimeUs);
    ScanKeyStates();

    uint8_t mask;
    for (int i = 0; i < IO_NUMBER / 8 + 1; i++)
    {
        mask = debounceBuffer[i] ^ spiBuffer[i];
        spiBuffer[i] |= mask;
    }
}

先将spiBuffer的数据拷贝到debounceBuffer(为了对比抖动前和抖动后),延时100us(延时消抖),再获取一遍状态,再利用一个For循环,将滤波前后数据整合在一起(第二次数据若和第一次不同,则说明有抖动就忽略这一次数据)。(整合具体实现过程,简单代入一个值分析一下即可,口头不好描述)。

 

重点来了,映射的实现

HWKeyboard::Remap(uint8_t _layer)找到这个函数

第一部分:功能:实现PCB上的按键号到标准键盘键码的映射。

    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++)
        {
            index = (int16_t) (keyMap[0][i * 8 + j] / 8);
            bitIndex = (int16_t) (keyMap[0][i * 8 + j] % 8);
            if (scanBuffer[index] & (0x80 >> bitIndex))
                remapBuffer[i] |= 0x80 >> j;
        }
        remapBuffer[i] = ~remapBuffer[i];
    }

index用来表示第几组(可理解为第几片74HC165),bitIndex用来表示第几位(可理解为第index号74HC165上的第bitIndex位)。

找到这个映射数组:第一组数组即是PCB上各按键对应的位置,其数字的含义对应PCB上的第多少个按键(例子:第一组的第一个数字67,即表示67/8=8 余 3,第八片74HC165上的第三个按键(74HC165由编号由0开始,按键个数为原理图上从左往右数))

    int16_t keyMap[5][IO_NUMBER] = {
        {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 index

        {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 },

        {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 }
    };

对第一部分的For循环分析:

因为一个字节为八位,且一个74HC165上挂着八个按键,一个字节的八个位代表对应八个按键的是否闭合状态。每循环一次,相当于对一个字节(即八个按键映射了一次)。

Tips:这里有个小知识点,由前面可知scanBuffer是一个地址,scanBuffer【x】,这个就代表scanBuffer首地址增加x个字节后对应的值,如a【5】=“abcde”,b指向a首地址,则b【4】=e。

注意上面spiBuffer这个数组记录的是74HC165每一个按键的状态(编号即原理图从左往右数)。

以一个例子来分析:i=0时,则index=0,对应第0组,j=0时,bitIndex=3,

通过scanBuffer[index] & (0x80 >> bitIndex)这条指令,我们就将第63个按键的状态取了出来,

再通过 remapBuffer[i] |= 0x80 >> j 这条指令就放在了remapBuffer这个数组的第0个字节的最高位

循环执行 一直到j=7,就得到了remapBuffer【0】的八个位依次对应PCB上的第67号,61号,60,58,59,52,55,51按键的状态。

如果我们选择用第二层映射,那就把这个remapBuffer【0】对应第二层第0组数据的前八个按键,即remapBuffer的第七位对应ESC按键状态(也就是原理图上的第67号按键(也就是第8片74HC165上的第三个按键(从左往右编号)))

依次循环IO_NUMBER/8次,就映射完成了。

为什么要 remapBuffer[i] = ~remapBuffer[i]; 取反一次呢

因为我们发送电脑报文的数据1表示按下,0表示没按。而硬件上1表示没按,0表示按下,故取反,以便发送数据。

第二部分:

c844d699c70948afa43bc58e2916a234.png

原理其实和第一部分一样,这里主要是将我们标准布局后的键盘,各个位重组成要发送的报文数据。看报文,第一部分是需要发送一个字节,这个字节表示ctrl等按键,第二部分是120个普通按键。(就对应上面定义的枚举体),发送是有顺序要求的。

bf310ad917214e4e9d1b4be79bf9a603.png

所以第二部分功能:以第一个字节为例:

d77049e41cd343b2b14afd8c6d806861.png

这里在枚举体中前八个字节给的负值,所以这里加了一个1。

89029848da484451bce8924a331b9ba2.png

而这里加1,是因为报文数据的第0字节是ID号,第1个字节开始才是数据。

bf2064bc37624deda683d143994963f5.png

此时remapBuffer的各个位代表 下图顺序的按键状态。

a4af82601e9d484895cb28467ab2d367.png

报文的第一个数据要表示下图 八个BIT的状态,所以用第一部分的办法,在remapBuffer中取出对应的位,组成对应按键的一个字节。即hidbuffer【1】的八位就对应这八个按键。

d3db7a0930f743b2be422cd31695b96a.png

至此,分析结束。本质上结束去找出相应的bit然后重组成byte。

 


 

总结

    部分写的可能描述不清楚,带一个byte进去尝试一下会通透很多。下一节更新键盘和控制切歌、调节音量的USB-HID复合设备实现

 

 

 

 

 

 

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大颜u

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值