基于GD32的矩阵按键usb-hid设备,详细教程,完全模拟的电脑数字键盘的所有功能,包括长按、短按,多个按键识别。

目录

1.前言

2.电路原理图

3.键盘扫描法-行列扫描

4.实现usb-hid模拟键盘功能

5.源代码


1.前言

最近在做一个开发基于GD32F350芯片的数字键盘的项目,有一些心得体会跟大家分享一下,本文采用的是基于GD32F350的一个4×5的矩阵键盘键盘板。

2.电路原理图

矩阵键盘的电路原理图大致如下,由四个列引脚和五个行引脚来检测判断按键的按下。

本文四个列引脚分别是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}		 
}; 

3.键盘扫描法-行列扫描

本文采用的是矩阵键盘列扫描法,常规的列扫描法是先把列引脚设置为输出,把行引脚设置为输入,然后把列引脚和行引脚都初始化为低电平,再从第一列开始,先给第一列引脚输出高电平,然后依次检测行引脚,若哪一行为高电平则代表有按键按下,通过列和行引脚的标号从而判断出具体是哪个按键按下。下面是按键初始化代码。

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

但当我用这个思路把代码写好后却发现,当我同时按下同一行的两个按键时,通过串口打印出的信息显示两个按键都松开了,这明显是不对的,后面通过排查发现当同一行的两个按键同时按下时,会导致电平异常,解决方法是优化列扫描法,当某一列输出高电平时,把另外三列都设置为输入模式,这样就不会有电平干扰了。这个改进是参考的这篇文章,大家有时间可以去看一下按键扫描处理总结_按键行为输出列为输入-CSDN博客

下面是按键扫描代码。

//返回最新按下的键值,并且当有多个按键按下时,只返回最新按下的键值,当有多个按键按下时,并且最新按下的按键松开了,则函数一直输出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,直到有新的按键按下才开始输出,而除了最新按下的按键之外,其他的按键松开,则不会影响函数的输出,大家可以拿自己键盘的数字键盘对着记事本试一下,看看是不是这样的功能。

4.实现usb-hid模拟键盘功能

下面是讲如何把按键扫描函数返回的最新键值通过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;

5.源代码

好了,废话不多说,接下来是完整代码,如果是其他的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;
}

main.c

#include "drv_usb_hw.h"
#include "standard_hid_core.h"
#include <stdio.h>
#include "systick.h"
#include "timetest.h"
#include "newkey.h"

	

extern hid_fop_handler fop_handler;
usb_core_driver hid_keyboard;

void led_spark(void)
{
    static __IO uint32_t timingdelaylocal = 0U;

    if(timingdelaylocal){

        if(timingdelaylocal < 500U){
            gd_eval_led_on(LED1);
        }else{
            gd_eval_led_off(LED1);
        }

        timingdelaylocal--;
    }else{
        timingdelaylocal = 1000U;
    }
}



/*!
    \brief      main routine will construct a USB keyboard
    \param[in]  none
    \param[out] none
    \retval     none
*/
int main(void)
{ 
 //时钟初始化
	systick_config();
 
	//按键初始化
  init();

  //配置usb频率为48mhz
	usb_rcu_config();
	//定时器初始化
  usb_timer_init();

  hid_itfop_register (&hid_keyboard, &fop_handler);
	  
  usbd_init (&hid_keyboard, USB_CORE_ENUM_FS, &hid_desc, &usbd_hid_cb);
	
  usb_intr_config();

   /* check if USB device is enumerated successfully */
  while (USBD_CONFIGURED != hid_keyboard.dev.cur_status) 
	{	
  }   
  while(1) 
	{		
		key_proc();		
  }
}

好了,文章到这里就结束了,大家有什么见解或者问题可以在评论区里提出来,欢迎大家一起讨论。

  • 20
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值