本文采用的是基于GD32F350的一个4×5的矩阵键盘键盘板。
矩阵键盘的电路原理图大致如下,由四个列引脚和五个行引脚来检测判断按键的按下。
本文四个列引脚分别是PA15 PB8 PB9 PC13,五个行引脚分别是PB10 PB11 PB12 PB13 PB14。
typedef struct {
uint32_t GPIO_Group;
uint32_t GPIO_Pin; // 引脚号
} GPIO_Pin_t;
//列引脚
GPIO_Pin_t lie_pins[] = {
{GPIOA,GPIO_PIN_15}, // 第一个元素:GPIOA, GPIO_PIN_15
{GPIOB,GPIO_PIN_8}, // 第二个元素:GPIOB, GPIO_PIN_8
{GPIOB,GPIO_PIN_9}, // 第三个元素:GPIOB, GPIO_PIN_9
{GPIOC,GPIO_PIN_13} // 第四个元素:GPIOC, GPIO_PIN_13
};
//行引脚
GPIO_Pin_t hang_pins[] = {
{GPIOB,GPIO_PIN_10},
{GPIOB,GPIO_PIN_11},
{GPIOB,GPIO_PIN_12},
{GPIOB,GPIO_PIN_13},
{GPIOB,GPIO_PIN_14}
};
本文采用的是矩阵键盘列扫描法,常规的列扫描法是先把列引脚设置为输出,把行引脚设置为输入,然后把列引脚和行引脚都初始化为低电平,再从第一列开始,先给第一列引脚输出高电平,然后依次检测行引脚,若哪一行为高电平则代表有按键按下,通过列和行引脚的标号从而判断出具体是哪个按键按下。下面是按键初始化代码。
void init(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
//四列 PA15 PB8 PB9 PC13 作为输出
for(int l = 0;l<4;l++)
{
gpio_mode_set(lie_pins[l].GPIO_Group, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, lie_pins[l].GPIO_Pin);
gpio_output_options_set(lie_pins[l].GPIO_Group, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, lie_pins[l].GPIO_Pin);
}
//五行 PB10 PB11 PB12 PB13 PB14 下拉输入 默认为低电平
for(int h = 0;h<5;h++)
{
gpio_mode_set(hang_pins[h].GPIO_Group, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, hang_pins[h].GPIO_Pin);
}
//行列初始化为低电平
for(int hh = 0;hh<5;hh++)
{
gpio_bit_reset(hang_pins[hh].GPIO_Group, hang_pins[hh].GPIO_Pin);
}
for(int ll = 0;ll<4;ll++)
{
gpio_bit_reset(lie_pins[ll].GPIO_Group, lie_pins[ll].GPIO_Pin);
}
}
但当我用这个思路把代码写好后却发现,当我同时按下同一行的两个按键时,通过串口打印出的信息显示两个按键都松开了,这明显是不对的,后面通过排查发现当同一行的两个按键同时按下时,会导致电平异常,解决方法是优化列扫描法,当某一列输出高电平时,把另外三列都设置为输入模式,这样就不会有电平干扰了。下面是按键扫描代码。
//返回最新按下的键值,并且当有多个按键按下时,只返回最新按下的键值,当有多个按键按下时,并且最新按下的按键松开了,则函数一直输出num=0,直到有新的按键按下。
uint8_t key_sacn()
{
static int num = 0; //返回的最新键值
static int all = 0; //当前按下按键总数
static int last_all =0; //上一次按下的按键总数,用来判断有无按键松开
static int change = 0; //状态变化标志位
int i=1;
int j=0;
int sum=0;
for(i=1;i<col;i++) //遍历每一列
{
gpio_bit_set(lie_pins[i-1].GPIO_Group, lie_pins[i-1].GPIO_Pin); //拉高当前列引脚电平
lieshuru(i-1); //把当前列之外的另外三列设置为输入模式
for(j=0;j<row;j++) //遍历每一行
{
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==1) //检测到行有高电平
{
delay_1ms(10); //消抖
countkey[i+j*4]++;
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==1) //再次检测到行有高电平
{
key_states[i+j*4] = 1; //代表该按键按下
if(countkey[i+j*4] == 1)
{
num = key[i+j*4]; //当该按键计数器为1时,把键值赋值给num进行输出,并且按键每次按下只赋一次值,直到该按键松开,按键计数器清零才能再次赋值。
}
}
delay_1ms(10);
sum = add();
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==0&&sum==0) //检测到所有按键都松开时,num为0.
{
num = 0;
}
}
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==0) //检测到按键松开,按键状态标志位置0,把该按键计数器清零,方便下一次按下该按键时进行赋值。
{
key_states[i+j*4] = 0;
countkey[i+j*4] =0;
}
}
gpio_bit_reset(lie_pins[i-1].GPIO_Group, lie_pins[i-1].GPIO_Pin); //拉低当前列引脚电平
lieshuchu(i-1); //把另外三列重新设为输出模式
}
all = add(); //获得当前按下按键的总数
if(last_all>all&&key_states[num]==0) //如果上一次按下按键总数大于当前按下按键总数,判定为有按键松开,并且最后一次按下的按键松开了,使状态变化标志位置1.
{
last_all=all;
change = 1;
}
if(last_all!=all) //知道有按键重新按下,才使状态变化标志位置0.
{
change=0;
}
if(change==1) //当状态变化标志位置1时,使函数一直输出0.
{
num=0;
}
last_all=all; //记录上一次按下的按键总数
return num;
}
这个扫描函数能够做到,每当有新按键按下时,能够覆盖上一次的键值,使函数能够返回最新的键值,并且当有多个按键按下时,若最新按下的按键松开了,则函数一直输出0,直到有新的按键按下才开始输出,而除了最新按下的按键之外,其他的按键松开,则不会影响函数的输出,大家可以拿自己键盘的数字键盘对着记事本试一下,看看是不是这样的功能。
下面是讲如何把按键扫描函数返回的最新键值通过usb-hid协议发送到pc端从而做到模拟数字键盘的功能。
要做到这一点,我们只需要对官方固件库的demo做一点修改就行了,我们先从兆易创新官网下载好对应的固件和例程兆易创新GigaDevice-资料下载兆易创新GD32 MCU,然后打开他的hid_keyboard.uvprojx文件,然后把我们的系统时钟修改一下,在system_gd32f3x0.c这个文件这里更改我们的系统时钟,这里我们改成
#define __SYSTEM_CLOCK_96M_PLL_IRC8M_DIV2或者
#define __SYSTEM_CLOCK_72M_PLL_IRC8M_DIV2都可以。
然后下一步是把gd32f3x0_it.c中的EXTI0_1_IRQHandler函数和USBFS_WKUP_IRQHandler函数下的resume_mcu_clk函数屏蔽掉,不然会卡在设备枚举那里出不来。
然后再把standard_hid_core.h中的hid_fop_handler结构体修改如下。
typedef struct
{
void (*hid_itf_config) (void);
uint8_t (*hid_itf_data_process) (usb_dev *udev,int newnum);
} hid_fop_handler;
好了,废话不多说,接下来是完整代码,如果是其他的mcu或者行列引脚不同的话,只要稍作修改就行了,思路都是一样的
newkey.c
#include "newkey.h"
#include "gd32f350r_eval.h"
#include "systick.h"
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "drv_usb_hw.h"
#include "standard_hid_core.h"
#define col 5
#define row 5
typedef struct {
uint32_t GPIO_Group;
uint32_t GPIO_Pin; // 引脚号
} GPIO_Pin_t;
//列引脚
GPIO_Pin_t lie_pins[] = {
{GPIOA,GPIO_PIN_15}, // 第一个元素:GPIOA, GPIO_PIN_15
{GPIOB,GPIO_PIN_8}, // 第二个元素:GPIOB, GPIO_PIN_8
{GPIOB,GPIO_PIN_9}, // 第三个元素:GPIOB, GPIO_PIN_9
{GPIOC,GPIO_PIN_13} // 第四个元素:GPIOC, GPIO_PIN_13
};
//行引脚
GPIO_Pin_t hang_pins[] = {
{GPIOB,GPIO_PIN_10},
{GPIOB,GPIO_PIN_11},
{GPIOB,GPIO_PIN_12},
{GPIOB,GPIO_PIN_13},
{GPIOB,GPIO_PIN_14}
};
int key[20]={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}; //键值数组
static int key_states[20] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};//按键状态数组
static int countkey[20] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};//每个按键单独的计数器
extern hid_fop_handler fop_handler; //usb-hid设备
extern usb_core_driver hid_keyboard;//usb-hid设备
//求当前按下的按键总数
int add(void)
{
int i =0;
int sum = 0;
for(i=0;i<20;i++)
{
sum=sum+key_states[i];
}
return sum;
}
//把除了当前列的另外三列设置为输入模式
void lieshuru(int i)
{
int a = 0;
for(a=0;a<4;++a)
{
if(a==i)
{
continue;
}
gpio_mode_set(lie_pins[a].GPIO_Group, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, lie_pins[a].GPIO_Pin);
}
}
//把除了当前列的另外三列重新设置为输出模式,并初始化为低电平
void lieshuchu(int i)
{
int a = 0;
for(a=0;a<4;++a)
{
if(a==i)
{
continue;
}
gpio_mode_set(lie_pins[a].GPIO_Group, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, lie_pins[a].GPIO_Pin);
gpio_output_options_set(lie_pins[a].GPIO_Group, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, lie_pins[a].GPIO_Pin);
gpio_bit_reset(lie_pins[a].GPIO_Group, lie_pins[a].GPIO_Pin);
}
}
//矩阵键盘初始化
void init(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
//四列 PA15 PB8 PB9 PC13 作为输出
for(int l = 0;l<4;l++)
{
gpio_mode_set(lie_pins[l].GPIO_Group, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, lie_pins[l].GPIO_Pin);
gpio_output_options_set(lie_pins[l].GPIO_Group, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, lie_pins[l].GPIO_Pin);
}
//五行 PB10 PB11 PB12 PB13 PB14 下拉输入 默认为低电平
for(int h = 0;h<5;h++)
{
gpio_mode_set(hang_pins[h].GPIO_Group, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, hang_pins[h].GPIO_Pin);
}
//行列初始化为低电平
for(int hh = 0;hh<5;hh++)
{
gpio_bit_reset(hang_pins[hh].GPIO_Group, hang_pins[hh].GPIO_Pin);
}
for(int ll = 0;ll<4;ll++)
{
gpio_bit_reset(lie_pins[ll].GPIO_Group, lie_pins[ll].GPIO_Pin);
}
}
//返回最新按下的键值,并且当有多个按键按下时,只返回最新按下的键值,当有多个按键按下时,并且最新按下的按键松开了,则函数一直输出num=0,直到有新的按键按下。
uint8_t key_sacn()
{
static int num = 0; //返回的最新键值
static int all = 0; //当前按下按键总数
static int last_all =0; //上一次按下的按键总数,用来判断有无按键松开
static int change = 0; //状态变化标志位
int i=1;
int j=0;
int sum=0;
for(i=1;i<col;i++) //遍历每一列
{
gpio_bit_set(lie_pins[i-1].GPIO_Group, lie_pins[i-1].GPIO_Pin); //拉高当前列引脚电平
lieshuru(i-1); //把当前列之外的另外三列设置为输入模式
for(j=0;j<row;j++) //遍历每一行
{
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==1) //检测到行有高电平
{
delay_1ms(10); //消抖
countkey[i+j*4]++;
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==1) //再次检测到行有高电平
{
key_states[i+j*4] = 1; //代表该按键按下
if(countkey[i+j*4] == 1)
{
num = key[i+j*4]; //当该按键计数器为1时,把键值赋值给num进行输出,并且按键每次按下只赋一次值,直到该按键松开,按键计数器清零才能再次赋值。
}
}
delay_1ms(10);
sum = add();
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==0&&sum==0) //检测到所有按键都松开时,num为0.
{
num = 0;
}
}
if(gpio_input_bit_get(GPIOB, hang_pins[j].GPIO_Pin)==0) //检测到按键松开,按键状态标志位置0,把该按键计数器清零,方便下一次按下该按键时进行赋值。
{
key_states[i+j*4] = 0;
countkey[i+j*4] =0;
}
}
gpio_bit_reset(lie_pins[i-1].GPIO_Group, lie_pins[i-1].GPIO_Pin); //拉低当前列引脚电平
lieshuchu(i-1); //把另外三列重新设为输出模式
}
all = add(); //获得当前按下按键的总数
if(last_all>all&&key_states[num]==0) //如果上一次按下按键总数大于当前按下按键总数,判定为有按键松开,并且最后一次按下的按键松开了,使状态变化标志位置1.
{
last_all=all;
change = 1;
}
if(last_all!=all) //知道有按键重新按下,才使状态变化标志位置0.
{
change=0;
}
if(change==1) //当状态变化标志位置1时,使函数一直输出0.
{
num=0;
}
last_all=all; //记录上一次按下的按键总数
return num;
}
//处理要上报的键值以及按键类型
uint8_t key_proc(void)
{ static int timecount = 0; //按键计数器,用来判断长按短按
static int num = 0; //获取最新按下的键值
static int last_num=0; //获取上一次按下的键值
num = key_sacn(); //获取最新键值
if(num==0) //当num为0时,使计数器一直为0.
{
timecount=0;
}
if(num!=0) //当num不为0使,证明有按键按下。
{
if(last_num!=num&&last_num!=0) //当上一次键值和这一次键值不一样时,判断有新的按键按下,输出一次键值并且重置按键计数器。
{
if(fop_handler.hid_itf_data_process(&hid_keyboard,num)!=1)
{
fop_handler.hid_itf_data_process(&hid_keyboard,num);//向pc端发送当前键值的函数
}
timecount=15;
}
timecount++; //按键计数器开始加一
if(timecount==2)//按键计数器在2-25之间都判断为短按,只输出一次键值
{
fop_handler.hid_itf_data_process(&hid_keyboard,num);
}
if(timecount>=25)//当按键计数器大于等于25时,判断为长按,一直输出键值
{
fop_handler.hid_itf_data_process(&hid_keyboard,num);
}
}
last_num=num;//记录上一次键值
return 0;
}
newkey.h
#include "gd32f3x0.h"
#include "gd32f350r_eval.h"
uint8_t key_proc(void);
void init(void);
hid_keyboard.itf.c主要是实现通过按键扫描返回的键值向pc端发送对应的hid码值,从而实现数字键盘的功能。
hid_keyboard.itf.c
#include "standard_hid_core.h"
#include "drv_usb_hw.h"
#include <stdio.h>
#include "systick.h"
#include "newkey.h"
uint8_t pcnum[21]={0x53U,0x53U,0x54U,0x55U,0x56U,0x5FU,0x60U,0x61U,0x57U,0x5CU,0x5DU,0x5EU,0x57U,0x59U,0x5AU,0x5BU,0x58U,0x62U,0x62U,0x63U,0x00U};//数字键盘的hid码值数组
typedef enum
{
CHAR_A = 1,
CHAR_B,
CHAR_C
} key_char;
/* local function prototypes ('static') */
static void key_config (void);
uint8_t hid_key_data_send(usb_dev *udev,int newnum);
hid_fop_handler fop_handler = {
.hid_itf_config = key_config,
.hid_itf_data_process = hid_key_data_send
};
/*!
\brief send usb keyboard data
\param[in] none
\param[out] none
\retval the char
*/
static void key_config (void)
{
/* configure the wakeup key in EXTI mode to remote wakeup */
}
//向pc端发送数据函数
uint8_t hid_key_data_send(usb_dev *udev,int newnum)
{ static int send = 6;
standard_hid_handler *hid = (standard_hid_handler *)udev->dev.class_data[USBD_HID_INTERFACE];
delay_1ms(10);
if(hid->prev_transfer_complete)
{
hid->data[2] = pcnum[newnum];
if (0U != hid->data[2])
{
delay_1ms(10);
send = hid_report_send(udev, hid->data, HID_IN_PACKET);
}
}
return send;
}
好了,文章到这里就结束了,大家有什么见解或者问题可以在评论区里提出来,欢迎大家一起讨论。
标签:usb,PIN,数字键盘,按下,num,hid,按键,GPIO From: https://blog.csdn.net/windaqwe/article/details/140708564