一、项目介绍
本项目是一个基于STM32F103为核心主控,加上一个组合的PCB设计的机械键盘,加上更改为60键位的电子琴键位,结合PC的音源软件,设计了一个简易的60键位电子琴。可以使用这个键盘去进行一定的演奏和练习曲子,音阶为2、3、4、5、6,五个刚好组成60个键位。
(成品图)
二、市场应用介绍
本次项目可作为常见电子琴的平替,总共成本算下来非常低,不超过50,加上3D外壳估计也不超过100,而且后续可以通过更改太空轴,使得更加贴合电子琴键位,pcb总计12,市面上的stm32f103价格也不高,热插拔座也只有20,唯一的成本只有轴体,而这个可以通过拆卸旧的键盘或者购入二手的低成本键盘即可。其次,为了方便些,后续可以改为蓝牙,也可以在pcb上加一下其他的设计,以实现智能化的交互。
三、项目设计思路
- 由于pc音源软件的存在,完全可以自己diy一个非常规配列的矩阵键盘。
- 主控使用最常见的stm32的开发版即可,同时使用较为便宜的f1也非常合适
- 键盘部分,考虑到一个80cm长的pcb成本会非常高,所以采取分段式的设计
- 由于3,所以使用五行12列的配置,且考虑到尺寸问题,使用小尺寸二极管更合适,所以阶段一的方案三淘汰掉,使用常规方案2
- stm32部分使用正常的usb hid协议去实现键盘功能
- 具体功能如下
- 使用矩阵键盘加机械轴体的pcb设计,作为输入设备
- 使用stm32作为主控,接受到键盘输入的数据并且进行处理
- 使用串口和USB向电脑输入数据,使得键盘按下的时候,电脑可以通过串口调试助手接受到键盘按下的数据,同时通过usb hid协议,实现键盘功能。
- 结合上述基本功能,在结合freepiano软件的使用,即可实现一个简易的电子琴配列
四、项目方案框图和原理图解
1、主要包括外部矩阵键盘、stm32主控、连接pc的音源软件,具体细节详见图片
(方案框图)
2、实现流程图如下,也就是代码顺序(流程图)
- 初始化配置,初始化串口GPIO等苦库
- 加载存放usbhid的数据数组,存放扫描键值的变量
- 进入循环,此时加载扫描函数
- 扫描函数开始初始化
- 扫描函数行扫描
- 对应行值基础上列扫描
- 获得值并且返回函数
- 此时根据扫描值进入对应的usb函数
- 向串口打印键值,同时向pc设备,通过usb协议发送数据
- pc获得键值后,进行一个低延时
- 延时完毕对发送函数进行清零,再重新发送数据。
- 回到循环初始的扫描函数
五、设计中用到规定厂商的元器件介绍
1、STM32F103RCT6
STM32F103RCT6是一款由STMicroelectronics生产的32位ARM Cortex-M3内核微控制器芯片。它具有丰富的外设集成,适用于广泛的应用领域,如工业控制、嵌入式系统和自动化。以下是该芯片的主要特点:
- 核心架构: ARM Cortex-M3
- 时钟频率: 可达72 MHz
- 存储器: Flash存储器最大为256 KB,SRAM最大为48 KB
- 通信接口: 支持多种通信接口,包括SPI、I2C、USART等
- 外设: 包括定时器、ADC、DAC等多个外设,适用于多种应用场景
- 低功耗: 支持多种低功耗模式,延长电池寿命
2、1N4148
1N4148是一种常见的高速开关二极管,广泛用于电子电路中的整流、开关和保护应用。以下是1N4148二极管的主要特性:
正向电压降: 典型值为0.7 V
反向峰值电压: 100 V
正向持续电流: 300 mA
封装: DO-35封装,适合常见的电子元器件安装
1N4148通常用于信号整流、快速开关和保护电路,其快速响应时间和小封装使其成为电子设计中常见的元器件之一。
六、PCB绘制打板介绍及遇到的问题和解决方法
(1)介绍
1、PCB绘制使用KiCad,原理图比较简单,这边没有用比较形象的库,直接用开关,比较简单
2、PCB的布线按照正常的就行,注意的地方只有模型要改一下,详细的导入就不说了
3、最后外框的尺寸可以小一点,特别是跳线部分改一下尺寸就行
4、正面是轴体,背面是热插拔座和二极管。
5、PCB的正面和背面图如下
总体
(2)问题+解决方法
1、由于矩阵键盘的原理(见前面),这里为了避免鬼键的问题,所以必须使用二极管,但是由于5*12的配列,以及考虑到代码的效率,所以行列数是严格规定的,所以PCB的设计中,二极管的方向要对,第一版本中就是把二极管方向搞混了,虽然焊接时只要交换位置就行,但是因为行列的规定,所以接线会反过来,如果是正常的一块PCB是没问题的,但是因为拼接设计,所以这里没办法解决。
2、热插拔的布局,由于轴体除了两个金属连接口之外,还有一个固定底座,我最初开始画PCB的时候,就把热插拔放反了,所以会挡住底座的口,这个也要注意
3、焊接的时候,二极管的方向也要对,如果反了的话,测试的时候,串口应该是不会接受到数据的,这个比较容易修改
4、测试的时候只需要万用表就可以了,这个比较简单就不详细说了,只需要两个表笔对准焊盘或者焊上去的器件的两端,按下轴体,看看蜂鸣器响不响就行了,如果没有声音,大部分是焊接虚焊了或者锡不够,这个热插拔底座的锡要的还是挺多的。
七、关键代码及说明
(1)关键代码
1、这部分是usbhid的配置
/* USER CODE BEGIN 2 */
printf("begin test");
/*
buffer[0] -
bit0:left ctrl
bit1:left shift
bit2:left ali
bit3:left gui
bit4:right ctrl
bit5:right shift
bit6:right ali
bit7:right gui
buffer[1]-padding = always 0x00
buffer[2]-key1
...
bufer[7]-key6
*/
uint8_t buffer[8] = {0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00};
/* USER CODE END 2 */
2、gpio的配置
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4
|GPIO_PIN_5, GPIO_PIN_RESET);
/*Configure GPIO pins : PA1 PA2 PA3 PA4
PA5 */
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4
|GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pins : PB10 PB11 PB12 PB13
PB14 PB15 PB4 PB5
PB6 PB7 PB8 PB9 */
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
3、gpio扫描的函数
/* USER CODE BEGIN 4 */
uint8_t Key_Scan(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
//PA5行,PB12列
//cubemx配置错了,现在应该是5行12列,input是列,output是行
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET);
//PA是L列,也就是竖着的,5列
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
//PB的4-15,12行H
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_Delay(10);
//第一行PA1
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1)==GPIO_PIN_RESET)//读取第1行
{
/*后4个端口输出低电平*/
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5, GPIO_PIN_RESET);
//后4个端口推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//前4个端口上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4)==GPIO_PIN_RESET)return 1;//此时列引脚,对应一行的1、2、3、4列
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5)==GPIO_PIN_RESET)return 2;//所以返回四个值
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_6)==GPIO_PIN_RESET)return 3;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)==GPIO_PIN_RESET)return 4;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)==GPIO_PIN_RESET)return 5;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_9)==GPIO_PIN_RESET)return 6;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10)==GPIO_PIN_RESET)return 7;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11)==GPIO_PIN_RESET)return 8;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_12)==GPIO_PIN_RESET)return 9;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_13)==GPIO_PIN_RESET)return 10;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_14)==GPIO_PIN_RESET)return 11;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_15)==GPIO_PIN_RESET)return 12;
}
//PA2第二行
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_2)==GPIO_PIN_RESET)//读取第1行
{
/*后4个端口输出低电平*/
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5, GPIO_PIN_RESET);
//后4个端口推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//前4个端口上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4)==GPIO_PIN_RESET)return 13;//此时列引脚,对应一行的1、2、3、4列
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5)==GPIO_PIN_RESET)return 14;//所以返回四个值
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_6)==GPIO_PIN_RESET)return 15;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)==GPIO_PIN_RESET)return 16;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)==GPIO_PIN_RESET)return 17;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_9)==GPIO_PIN_RESET)return 18;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10)==GPIO_PIN_RESET)return 19;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11)==GPIO_PIN_RESET)return 20;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_12)==GPIO_PIN_RESET)return 21;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_13)==GPIO_PIN_RESET)return 22;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_14)==GPIO_PIN_RESET)return 23;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_15)==GPIO_PIN_RESET)return 24;
}
//第三行PA3
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_3)==GPIO_PIN_RESET)//读取第1行
{
/*后4个端口输出低电平*/
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5, GPIO_PIN_RESET);
//后4个端口推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//前4个端口上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4)==GPIO_PIN_RESET)return 25;//此时列引脚,对应一行的1、2、3、4列
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5)==GPIO_PIN_RESET)return 26;//所以返回四个值
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_6)==GPIO_PIN_RESET)return 27;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)==GPIO_PIN_RESET)return 28;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)==GPIO_PIN_RESET)return 29;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_9)==GPIO_PIN_RESET)return 30;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10)==GPIO_PIN_RESET)return 31;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11)==GPIO_PIN_RESET)return 32;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_12)==GPIO_PIN_RESET)return 33;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_13)==GPIO_PIN_RESET)return 34;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_14)==GPIO_PIN_RESET)return 35;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_15)==GPIO_PIN_RESET)return 36;
}
//第四行PA4
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4)==GPIO_PIN_RESET)//读取第1行
{
/*后4个端口输出低电平*/
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5, GPIO_PIN_RESET);
//后4个端口推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//前4个端口上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4)==GPIO_PIN_RESET)return 37;//此时列引脚,对应一行的1、2、3、4列
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5)==GPIO_PIN_RESET)return 38;//所以返回四个值
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_6)==GPIO_PIN_RESET)return 39;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)==GPIO_PIN_RESET)return 40;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)==GPIO_PIN_RESET)return 41;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_9)==GPIO_PIN_RESET)return 42;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10)==GPIO_PIN_RESET)return 43;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11)==GPIO_PIN_RESET)return 44;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_12)==GPIO_PIN_RESET)return 45;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_13)==GPIO_PIN_RESET)return 46;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_14)==GPIO_PIN_RESET)return 47;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_15)==GPIO_PIN_RESET)return 48;
}
//第五行//PA5
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_5)==GPIO_PIN_RESET)//读取第1行
{
/*后4个端口输出低电平*/
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5, GPIO_PIN_RESET);
//后4个端口推挽输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//前4个端口上拉输入
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13
|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_4|GPIO_PIN_5
|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
HAL_Delay(10);
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_4)==GPIO_PIN_RESET)return 49;//此时列引脚,对应一行的1、2、3、4列
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_5)==GPIO_PIN_RESET)return 50;//所以返回四个值
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_6)==GPIO_PIN_RESET)return 51;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7)==GPIO_PIN_RESET)return 52;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_8)==GPIO_PIN_RESET)return 53;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_9)==GPIO_PIN_RESET)return 54;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_10)==GPIO_PIN_RESET)return 55;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_11)==GPIO_PIN_RESET)return 56;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_12)==GPIO_PIN_RESET)return 57;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_13)==GPIO_PIN_RESET)return 58;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_14)==GPIO_PIN_RESET)return 59;
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_15)==GPIO_PIN_RESET)return 60;
}
return 0;
}
/* USER CODE END 4 */
4、发送串口和键盘数据的函数
key_val = Key_Scan();
//这个只是一部分,总共60个,详情见附件代码,这个只是一个具体展示,其他的一样的
if(key_val == 3)
{
printf("03");
buffer[2] = 0x1D;
USBD_HID_SendReport(&hUsbDeviceFS,buffer,8);
HAL_Delay(15);
buffer[2] = 0x00;
USBD_HID_SendReport(&hUsbDeviceFS,buffer,8);
HAL_Delay (500);
}
5、键盘描述符的改动
__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[HID_MOUSE_REPORT_DESC_SIZE] __ALIGN_END =
{
0x05,0x01, //USAGE_PAGE (Generic Desktop)
0x09,0x06, //USAGE (Keyboard)
0xA1,0x01, //COLLECTION (Application)
//以下为输入报告描述符,用于中断输入端点
//输入报告描述符2字节,第一个字节各位代表的是控制键,第二个字为0,后面跟6个数组,用于普通按键键码
0x05,0x07, //USAGE_PAGE (Keyboard)
0x19,0xE0, //USAGE_MINIMUM (Keyboard LeftControl)
0x29,0xE7, //USAGE_MAXIMUM (Keyboard Right GUI)
0x15,0x00, //LOGICAL_MINIMUM (0)
0x25,0x01, //LOGICAL_MAXIMUM (1)
0x75,0x01, // REPORT_SIZE (1)
0x95,0x08, // REPORT_COUNT (8)
0x81,0x02, // INPUT (Data,Var,Abs)
0x95,0x01, // REPORT_COUNT (1)
0x75,0x08, // REPORT_SIZE (8)
0x81,0x03, // INPUT (Cnst,Var,Abs)
0x95,0x06, // REPORT_COUNT (6)
0x75,0x08, // REPORT_SIZE (8)
0x15,0x00, // LOGICAL_MINIMUM (0)
0x25,0x65, // LOGICAL_MAXIMUM (101)
0x05,0x07, // USAGE_PAGE (Keyboard)
0x19,0x00, // USAGE_MINIMUM (Reserved (no event indicated))
0x29,0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81,0x00, // INPUT (Data,Ary,Abs)
//输出报告描述符,用于控制键盘灯
//以下为中断输出报告描述符,用于中断输出端点
//第一节字的低5位表示相应的指示灯,高3位为0
0x95,0x05, // REPORT_COUNT (5)
0x75,0x01, // REPORT_SIZE (1)
0x05,0x08, // USAGE_PAGE (LEDs)
0x19,0x01, // USAGE_MINIMUM (Num Lock)
0x29,0x05, // USAGE_MAXIMUM (Kana)
0x91,0x02, // OUTPUT (Data,Var,Abs)
0x95,0x01, // REPORT_COUNT (1)
0x75,0x03, // REPORT_SIZE (3)
0x91,0x03, // OUTPUT (Cnst,Var,Abs)
0xC0 //END_COLLECTION
};
(2)说明
1、usbhid主要是看这个8位的数组--buffer,其中0、1是已经规定了数值的,要发送这些数据,必须得要从这里面发送,而2、3、4、5、6、7是对应六个键,也就是说,是完全可以使用它去做六键无冲的。
buffer[0] -
bit0:left ctrl
bit1:left shift
bit2:left ali
bit3:left gui
bit4:right ctrl
bit5:right shift
bit6:right ali
bit7:right gui
buffer[1]-padding = always 0x00
buffer[2]-key1
...
bufer[7]-key6
2、gpio的配置无非就是五个行和12个列配置,这个比较简单,看cubemx和keil的函数就知道了。
(cubemx放一张图)
3、扫描函数主要还是因为矩阵键盘的原理,这里简单说明一下,矩阵键盘大概就是这么一个图,随便画的,以2*2为例,为什么叫矩阵,看看图就知道了,如果使用如图的方式去连接,那么以行为H0、H1;列为L0、L1;(H0,L0)为A,为了方便我使用abcd表示四个位置,如图,也就是按下第一个按键时,行列检测到,其实简单来说就是一个按键有两个信息,而主控根据这两个信息判断是哪个按键按下。于是引申4
(矩阵键盘图)
4、一个开关,一般是一边是gnd,一边是单片机的IO,但是矩阵键盘这边连接两个IO,单片机要如何操作呢,其实就是一个轮询过程,比如以H为高电平,L为低电平,若A按下,此时H0监测低电平,其实B按下的话H0也是低电平,则行信息确认,反之,L为高电平,H低电平,A按下,则L0检测低电平,L0为列信息确认,所以判断成功
5、综上,具体扫描过程为,先初始配置IO,然后开始扫描,从行开始,扫描到行之后,再开始扫描列,从而得到对应的值。
6、鬼键问题,一般来说一个一个按键正常按下时是没问题的,但是当你出现按下多个时,可能会导致导线联通到别的IO口,此时单片机会检测到新的值,也就是鬼键,为了避免这个问题,加一个单向的二极管,限定方向,即可解决该方法。
7、串口发送不说了略过,usb描述的值详见这个链接(去找官方的最好),每个按键都有对应的一个值:USB协议中HID设备描述符以及键盘按键值对应编码表_usb键盘编码表-CSDN博客
8、改动部分,其实cubex初始配置是鼠标的,为了改成键盘的,只需要按下图改这几个就行了,具体为:
//hid.h部分
#define HID_EPIN_SIZE 0x08U
#define HID_MOUSE_REPORT_DESC_SIZE 63U
//hid.c部分
0x01, /*nInterfaceProtocol : 0=none, 1=keyboard, 2=mouse*/
- 04改成08
- 74改成63
- 函数有解释,照着改
- 描述符改动(见前面)
八、功能展示及说明
- 全部按键演示见视频,这里单独演示第五音阶,也就是第四个部分,这部分分别按下SW1、2、6、7、8,对应的值为37、38、42、43、44,此时为黑键,可以看到串口接受到的信息
- 同1,此时若打开freepiano软件,可以看到对应的五个黑键被按下
- 若按下3、4、5、9、10、11、12,对应的是39,40,41,45,46,47,48;串口接收到对应的数值
- 同3,此时打开软件,可以看到对应的7个白键被按下
- 打开键盘测试链接,以第四阶为例,七个白键对应键盘的qwertyu,此时按下第四阶的白键,可以看到网页检测到对应的按键,左为轨迹,右为顺序
- 连接到PC端口后可以在设备处看到stm32human的键盘指示,在设备管理器也可以看到。
九、整个设计过程遇到的难点和解决方法
1、PCB设计部分没有考虑到设计需求,因为设计需求对于二极管的方向是固定的,而二极管方向在初始设计的时候没有调整好,后续重新更改了一遍
2、实际焊接的时候,热插拔上锡上的太少,测试是否短路的时候明显没有短路,上锡给多点即可解决
3、二极管焊接方向反了,在结合代码测试的时候发现的,具体现象为按下键盘时,串口没有收到数据,说明是二极管的问题,用一根线按住焊盘两侧测试一下就知道了。
4、测试USB HID的时候,发现按下安静后,测试网页的按键会一直响应,说明是usb的发送函数在持续不断向pc发送数据,经过验证,需要加一个延时,在发送完数据之后,要给他清零,这样子即可避免。
5、cubex初始配置时忘记给io上拉了,这个检查矩阵键盘代码的时候可以查出来,因为若是别的按键没什么问题,检查代码也没什么问题,一般会看usart和gpio的文件,此时检查gpio.h即可发现问题。
十、对本大赛的心得体会(包括意见或建议)。
1、本次大赛我对我的成品不太满意,近期比较忙导致没办法去完善成品,我在10月底就已经完成所有的代码和基础功能的测试了,但是问题在于这个成品物理结构不稳定,而且辅助功能也没加,最难绷的还是尺寸没量好,甚至现在那个跳线连接还是很畸形的。
2、至于难点和解决方法,前面基本上都说了,就不在这里讲了,大部分还是硬件结合代码的调试,而且多看看别人的源码,有助于自己的代码开发。
3、本次设计过程主要点还是USB部分,这个我确实没怎么接触过,光是学习协议的一些基本原理和一些配置,就已经画了好几天时间了,不过最后确实发现,因为这个项目要求不是很多,所以基本上配置hid的时候也不是很麻烦。(USB的一个问题见代码部分)
4、本次感谢电子森林提供这么一个机会,让广大用户可以从其中锻炼自己独立完成产品的过程,不仅仅是硬件的设计和代码的学习,主要还是综合调试能力的提升,对个人的实践能力和动手能力非常有效的提升。