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

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

时间:2024-07-26 10:59:27浏览次数:18  
标签:usb PIN 数字键盘 按下 num hid 按键 GPIO

本文采用的是基于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

相关文章

  • mstar 方案遥控器和按键修改方法
    mstar方案遥控器和按键修改方法:一、kernel中遥控器码值修改路径:\vendor\mstar\kernel\linaro\mstar2\drv\ir_mirc\keymaps\keymap-mstar-tv.c二、kernel中修改遥控器头码路径:vendor\mstar\kernel\linaro\mstar2\drv\ir_mirc\ir_common.h三、3.1)遥控器IR码值:kernel中......
  • VirtualBox 虚拟机识别主机接入的USB
    ###环境我当前的主机系统是Ubuntu20.04.6LTS,由于鸿蒙开发工具只有win和mac,我选择开个win的虚拟机。我使用的虚拟机是VirtualBox7.0.18,系统是win10,前面已经安装完成。鸿蒙开发环境已经在VBox中配置完成,创建项目后无法连接开发者手机,发现VBox没有启用USB。 ###解决方法......
  • 《安富莱嵌入式周报》第340期:开源便携RF信号发生器,六自由度3D鼠标,开源USB PD Sniffer,C
    周报汇总地址:http://www.armbbs.cn/forum.php?mod=forumdisplay&fid=12&filter=typeid&typeid=104 视频版:https://www.bilibili.com/video/BV18w4m1k7NF/目录1、开源便携RF信号发生器,支持12.5MHz-6.4GHz2、关于ThreadX被移交Eclipse后,使用FileX的exFAT功能版权问题3、......
  • windows USB 设备驱动开发-USB Type-C 手动互操作性测试过程
    驱动开发中,可能需要测试已启用USBType-C的系统和Windows的互操作性。本文为设备和系统制造商提供了指南,用于对公开USBType-C连接器的系统和设备执行各种功能和压力测试。它假定读者熟悉官方USB规范和xHCI互操作性测试过程,可从USB.ORG下载。可以使用USBType-C......
  • Python 检测 USB 设备 - IDLE 和 CMD 解释器之间的不同结果
    我正在尝试解决VDI解决方案中智能卡设备的USB重定向问题。我正在使用pyscard模块作为智能卡。对于进一步的上下文,主要问题是当浏览器插件调用用于处理智能卡的python脚本时,未检测到读卡器。关于问题,当我从CMD解释器运行此代码片段时,我收到空列表,表示系统上未找......
  • 带 USB的双独立通道超强回音消除 A-59U
    一,产品概述:   A-59U是一款高性能的数字语音处理模块,针对所有免提全双工通话设备中的回音问题进行消除(AEC),并具环境噪音压制(ENC)功能,及定向拾音(BF)功能,让通话设备获得更好的语音品质。   A-59U的优异处理性能,可以在消除大音量喇叭声音的同时,并最大保持全双工......
  • 嵌入式学习之USB协议(二)
    上一节我们讲了USB的电气信号,今天我们讲帧的组成内容;请注意,USB通信中的“帧”相当于串口通信中的“字节”。在常见的串口,I2C,SPI等等,均以字节为单位,通过分析字节组成的数据得到信息;USB以帧为单位,通过帧组成的数据得到信息,不完整的帧组成的信息没有任何意思,直接丢弃。帧的组......
  • 1323、基于51单片机按键发送GPS时间定位信息 GSM短信收LCD12864显示报警(程序+原理图+
    毕设帮助、开题指导、技术解答(有偿)见文未  目录方案选择单片机的选择一、设计功能二、实物图单片机模块设计三、原理图四、程序源码五、PCB图资料包括:需要完整的资料可以点击下面的名片加下我,找我要资源压缩包的百度网盘下载地址及提取码。方案选择单片机的......
  • QT 之 USB SCSI指令0x2A对USB设备进行写有问题
     摘要:使用QT进行SCSI指令操作时遇到问题,0x28读取正常,但0x2A写入失败,原因是系统对0x2A命令的写入权限控制严格。解决方法是通过FSCTL_LOCK_VOLUME实现独占访问,实现对USB设备的写操作。 问题参考:https://blog.csdn.net/kifea/article/details/1036960990x2A命令参考: https:/......
  • 如何在没有 USB 的情况下将文件从手机传输到笔记本电脑
    我们经常需要在不同设备间传输文件,而不再局限于使用传统的USB线连接。无线技术的进步为手机与笔记本电脑之间的文件传输提供了便捷、高效的解决方案。无论是跨越房间还是位于网络覆盖的不同位置,以下方法将指导您如何在没有USB线的情况下,轻松实现手机与笔记本电脑间的数据传输。......