admin管理员组文章数量:1571949
第一节 USB协议及建立USB-HID工程
第二节 配置描述符及HID报文格式
第三节 PCB按键映射(基于稚晖君开源)
文章目录
- 系列文章目录
- 前言
- 一、相关描述符简述
- 二、映射原理
- 1.转载部分
- 2.原理分析
- 总结
前言
本节主要讲稚晖君键盘按键实现映射的原理。详细开源的资料链接:peng-zhihui/HelloWord-Keyboard (github)
文章较长,对比着原理图理解,比较容易懂。
关于键盘的实现在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等效,故上面的配置描述符中增加了输出端点。
之后的这一段好像是为了后续BOOTLOADER升级使用,笔者未深究,不作分析。
对应的键盘码报文使用的 报文page的第七页,故枚举体也是跟这个对应的,详细讲解看上一节。
(虽然定义了128个按键,但是不一定都用完了的,可选择其中任意的使用)
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...层都是自定义的,负责映射标准按键布局到任意布局。
举个例子:
考虑原理图中箭头指的那个按键,这个按键可以在PCB的任意位置,但是我们可以看到,它是从左到右(按74HC165的连接顺序,也即移位扫描顺序)的第10颗,因此它的编号为9(从0开始算).
如果我们在实际的PCB板上把它放在了右边Alt的位置,那么参考在下图代码红色框中的第1层映射(也就是标准布局)中的RIGHT_ALT
的序号是76,那么在第0层映射的76号变量就填入9(蓝色框).
这样依次把你PCB上所有按键都填入0层映射,就得到了一个映射好的标准键盘了。后续2、3、4、5...层需要怎么映射就随意修改添加即可,也不需要再使用数字编号,而是可以直接用枚举的按键名称很方便。
所以对于想修改键盘配列的人,只需要再原理图上添加或删减几个串联的74HC165,然后PCB随意走线,再将代码中0层映射删减或增加一些数字即可(比如在下面的例子中我的键盘是83键的);后面几层的修改就以此类推了。 代码中通过keyboard.Remap
函数来映射不同层,比如keyboard.Remap(2)
这一句是使用第2层映射。
2.原理分析
首先找到这个函数:这个函数即一个完整的扫描过程
对其分析,其大概顺序为先扫描一遍所有按键的状态,之后进入消抖函数(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定义的地方。
获取完状态后,按顺序该进入按键消抖函数: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表示按下,故取反,以便发送数据。
第二部分:
原理其实和第一部分一样,这里主要是将我们标准布局后的键盘,各个位重组成要发送的报文数据。看报文,第一部分是需要发送一个字节,这个字节表示ctrl等按键,第二部分是120个普通按键。(就对应上面定义的枚举体),发送是有顺序要求的。
所以第二部分功能:以第一个字节为例:
这里在枚举体中前八个字节给的负值,所以这里加了一个1。
而这里加1,是因为报文数据的第0字节是ID号,第1个字节开始才是数据。
此时remapBuffer的各个位代表 下图顺序的按键状态。
报文的第一个数据要表示下图 八个BIT的状态,所以用第一部分的办法,在remapBuffer中取出对应的位,组成对应按键的一个字节。即hidbuffer【1】的八位就对应这八个按键。
至此,分析结束。本质上结束去找出相应的bit然后重组成byte。
总结
部分写的可能描述不清楚,带一个byte进去尝试一下会通透很多。下一节更新键盘和控制切歌、调节音量的USB-HID复合设备实现
版权声明:本文标题:基于STM32的USB键盘制作(保姆级)(三) 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1727706912a1126449.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论