目录
概述
1 原理分析
1.1 技术背景
1.2 系统硬件
1.3 STM32 IO(输入模式)寄存器分析
1.3.1 输入IO的功能描述
1.3.2 输入配置
1.3.3 GPIO 寄存器(输入模式相关)
1.3.3.1 GPIO 端口模式寄存器
1.3.3.2 GPIO 端口上拉/下拉寄存器
1.3.3.3 GPIO 端口输入数据寄存器
1.4 外设时钟使能寄存器
2 软件实现
2.1 使用STM32CubeMX创建工程
2.2 认识Hal库中和IO相关的函数
2.3 实现代码
2.3.1 定义一个Key相关的数据结构
2.3.2 初始化函数
2.3.3 按键扫描函数
2.3.4 使用键值
3 测试
3.1 编写测试代码
3.2 测试
源代码下载地址:
使用SM32-F4实现非阻塞方式,读取按键值资源-CSDN文库
概述
本文主要介绍如何使用非阻塞方式,实现多个按键扫描功能,能准确判断按键的状态。还详细介绍STM32 F4系列芯片IO相关的寄存器,已经Hal库中和IO相关的接口函数。重点讲解非阻塞高效键盘扫描功能的代码实现逻辑。
1 原理分析
1.1 技术背景
在一个系统程序中,一般希望程序运行尽可能的快,这样MCU才可能经可能多的执行逻辑,或者处理数据。Windows /Linux系统中引入线程和进程来解决这个问题。在单线程系统(单片机程序:主程序main中一个while循环)中,也可以模拟多线程的方式,把阻塞执行的代码,使用时间片来轮询来执行。以提高代码运行的效率。
扫描键盘功能就明显有这类任务的特征,本文就是采用系统定时器产生时间片,实现一个非阻塞任务方式,扫描键盘中的按键,并判断键值是否有效。
1.2 系统硬件
电路分析:
系统有8个独立按键,每个独立按键一个端口与一个MCU的一个IO相连,且与MCU IO相连的这个端口,接了一个上拉电阻,其目的,保持IO输入口电平的稳定性。按键的另一个端口与GND连接,当按键按下之后,MCU IO会检测到低电平信号。
1.3 STM32 IO(输入模式)寄存器分析
要使用MCU IO控制外围设备,就需要对IO模块有一个清晰的认识,这样才能正确的使用它,下面来分析STM32 IO模块的特性。笔者使用的芯片型号是STM32F407IGT6,所以,本文以STM32F4xx用户手册为例来介绍其IO的使用方法。由于,STM32 IO的功能比较复杂,这里只介绍将其配置为输入IO时,该如何使用。
1.3.1 输入IO的功能描述
根据数据手册中列出的每个 I/O 端口的特性,可通过软件将通用 I/O (GPIO) 端口的各个端口位分别配置输入模式时,有如下3种方式配置: ● 输入浮空 ● 输入上拉 ● 输入下拉
1.3.2 输入配置
对 I/O 端口进行编程作为输入时: ● 输出缓冲器被关闭 ● 施密特触发器输入被打开 ● 根据 GPIOx_PUPDR 寄存器中的值决定是否打开上拉和下拉电阻 ● 输入数据寄存器每隔 1 个 AHB1 时钟周期对 I/O 引脚上的数据进行一次采样 ● 对输入数据寄存器的读访问可获取 I/O 状
1.3.3 GPIO 寄存器(输入模式相关)
1.3.3.1 GPIO 端口模式寄存器
每一组GPIO有个 GPIOx_MODER 端口模式寄存器, 该寄存器一共有32个bit, 每两个bit控制一组IO下的一个pin引脚的模式状态。
MODERy[1:0]: 端口 x 配置位 (Port x configuration bits) (y = 0..15)。这些位通过软件写入,用于配置 I/O 方向模式。
00:输入(复位状态) 01:通用输出模式 10:复用功能模式 11:模拟模式
举个例子:配置GPIOI_PIN7为输入引脚,需要写MODER7[1:0] = 00
1.3.3.2 GPIO 端口上拉/下拉寄存器
每一组GPIO有个 (GPIOx_PUPDR) 上拉/下拉寄存器, 该寄存器一共有32个bit, 每2个bit控制一组IO下的一个pin引脚的上拉/下拉状态。
PUPDRy[1:0]: 端口 x 配置位 (Port x configuration bits) (y = 0..15),这些位通过软件写入,用于配置 I/O 上拉或下拉。
00:无上拉或下拉 01:上拉 10:下拉 11:保留
1.3.3.3 GPIO 端口输入数据寄存器
每一组GPIO有个 (GPIOx_IDR) 输入数据寄存器 , 该寄存器一共有32个bit, 每1个bit表示对应端口输入的值。因为stm32每一组IO有16个端口,所以,使用bit0~bit15,存储输入的bit值,bit-16~bit-31保留
IDRy[15:0]: 端口输入数据 (Port input data) (y = 0..15) 这些位为只读形式,只能在字模式下访问。它们包含相应 I/O 端口的输入值。
1.4 外设时钟使能寄存器
这个寄存器主要用来打开或者关闭所使用的外设功能,STM32F407有3个外设时钟使能寄存器 ,本文仅介绍和IO相关的 RCC_AHB1ENR ,其定义如下:
在上图中可以看见,和IO相关的时钟使能bit位分布在bit0~bit8,要使用那个端口,只需将对应的位置1,就可以使能该对应位的时钟。
2 软件实现
2.1 使用STM32CubeMX创建工程
1)打开STM32CubeMX创建工程,然后在GPIO选项卡中,配置和KEY-IO相关的参数。
2)完成参数配置后,点击GENERATE CODE,生成工程文件。
2.2 认识Hal库中和IO相关的函数
1) HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
功能: 用于初始化GPIO的属性,在STM32CubeMX中配置IO属性后,该函数会在IO初始代码中调用。这部分代码会由STM32CubeMX自动生成。
2) PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
功能:读取IO_PIN的值
函数参数:
GPIOx: GPIO组(A,B,C...)
GPIO_Pin: GPIO组下的那个引脚(0~15)
返回值: 读到IO pin 的值
3) HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
功能:写IO_PIN的值
函数参数:
GPIOx: GPIO组(A,B,C...)
GPIO_Pin: GPIO组下的那个引脚(0~15)
PinState: 要写的状态(0 or 1)
4) HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
功能: 触发IO_Pin电平变化
函数参数:
GPIOx: GPIO组(A,B,C...)
GPIO_Pin: GPIO组下的那个引脚(0~15)
5)HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
功能: 锁存当前IO_Pin的值,reset时,该IO的电平不会发生变化
函数参数:
GPIOx: GPIO组(A,B,C...)
GPIO_Pin: GPIO组下的那个引脚(0~15)
6)HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
功能: 当前IO_Pin的中断函数
函数参数:
GPIO_Pin: GPIO组下的那个引脚(0~15)
7)HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
功能: 中断函数的回调函数,HAL_GPIO_EXTI_IRQHandler中会调用该函数,且这个函数函数为weak类型,用户可重新写它。
2.3 实现代码
源代码下载地址: 使用SM32-F4实现非阻塞方式,读取按键值资源-CSDN文库
2.3.1 定义一个Key相关的数据结构
定义一个数据结构,由于操作和key相关的状态控制。
typedef struct { uint8 (*KeyActFunc)(void); uint8 Count; uint8 State; uint8 DownState; uint8 ReleaseState; }KeyAct_Stru;
2.3.2 初始化函数
1)在该段函数中,21~29 行,实现IO状态读取函数,当按键被按下后,返回值为1, 否则返回值为0
2)44~52行, 注册按键触发函数
源代码
static KeyAct_Stru s_tBtn[KEY_TOTAL];
static unsigned char IsKey_1_Down(void) {if ((KEY_1_GPIO_Port->IDR & KEY_1_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_2_Down(void) {if ((KEY_2_GPIO_Port->IDR & KEY_2_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_3_Down(void) {if ((KEY_3_GPIO_Port->IDR & KEY_3_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_up_Down(void) {if ((KEY_UP_GPIO_Port->IDR & KEY_UP_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_down_Down(void) {if ((KEY_DOWN_GPIO_Port->IDR & KEY_DOWN_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_left_Down(void) {if ((KEY_LEFT_GPIO_Port->IDR & KEY_LEFT_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_right_Down(void) {if ((KEY_RIGHT_GPIO_Port->IDR & KEY_RIGHT_Pin) == 0) return 1;else return 0;}
static unsigned char IsKey_ok_Down(void) {if ((KEY_OK_GPIO_Port->IDR & KEY_OK_Pin) == 0) return 1;else return 0;}
void bsp_KeyInit( void )
{
int i = 0;
KeyAct_Stru *pBtn;
for( i =0; i < KEY_TOTAL; i++ )
{
pBtn = &s_tBtn[i];
memset( pBtn, sizeof(KeyAct_Stru), 0 );
}
s_tBtn[0].KeyActFunc = IsKey_1_Down;
s_tBtn[1].KeyActFunc = IsKey_2_Down;
s_tBtn[2].KeyActFunc = IsKey_3_Down;
s_tBtn[3].KeyActFunc = IsKey_up_Down;
s_tBtn[4].KeyActFunc = IsKey_down_Down;
s_tBtn[5].KeyActFunc = IsKey_left_Down;
s_tBtn[6].KeyActFunc = IsKey_right_Down;
s_tBtn[7].KeyActFunc = IsKey_ok_Down;
}
2.3.3 按键扫描函数
实现逻辑如下: 1) 当按键被按下后,检测计数器开始工作(63~75行),bsp_KeyScan()的运行周期是1ms,当count值累加到门限值时,按键按下的状态没有改变,说明按键值有效。
2)检测按键弹起:
step-1: 检测按下标记为是否有效,如果该位有效,说明按键被按下过。
step-2:计数器开始工作,当计数器的值到达门限值后,弹起状态有效,置位弹起标记。
3)键值位(102~108行)
检测按下确认状态位和弹起确认状态位。当二者都有效时,存储的键值有效。
源代码
void bsp_KeyScan( unsigned char index )
{
KeyAct_Stru *pBtn;
pBtn = &s_tBtn[index];
if( pBtn->KeyActFunc() )
{
// key first pressed
if( pBtn->Count < KEY_FILTER_MAX_TIME ){
pBtn->Count = KEY_FILTER_MAX_TIME;
}
else if( pBtn->Count < KEY_FILTER_NEXT_TIME ) {
pBtn->Count++;
}
else{
// confirm: key is presssed
if( !pBtn->DownState )
{
pBtn->DownState = 1;
}
pBtn->Count = 0;
}
}
else
{
if( pBtn->DownState )
{
if( pBtn->Count > KEY_FILTER_MAX_TIME )
{
pBtn->Count = KEY_FILTER_MAX_TIME;
}
else if( pBtn->Count > 0)
{
pBtn->Count--;
}
else
{
//confirm: key is released
if( !pBtn->ReleaseState )
{
pBtn->ReleaseState = 1;
}
}
}
}
// confirm key press action
if( pBtn->ReleaseState && pBtn->DownState )
{
printf(" KEY-%d is pressed!\r\n", index);
pBtn->State = 1;
pBtn->ReleaseState = 0;
pBtn->DownState = 0;
}
}
2.3.4 使用键值
1)bsp_KeyMonitor
按键扫描函数,该函数必须放在一个以1ms为间隙扫描的任务里,其会周期性的扫描所有注册的按键状态
2)bsp_KeyGetValue
获取键值函数,通过传入key所对应ID的值,就能得到该键值
3 测试
3.1 编写测试代码
1) 第104行, 调用系统Tick函数,实现1ms Tick功能,
2)第117行,实现键盘扫描功能,其执行周期为1ms
3) 第127行,读取键值
源代码
void bsp_KeyMonitor( void )
{
unsigned char index ;
for( index = 0; index < KEY_TOTAL; index ++ )
{
bsp_KeyScan( index );
}
}
unsigned char bsp_KeyGetValue(KEY_ID keyID)
{
unsigned char value;
value = s_tBtn[keyID].State;
s_tBtn[keyID].State = 0; // clear key status
return value;
}
3.2 测试
编译程序,下载到板卡中,按下不同的按键,会打印不同的键值
源代码
void tick_action( void )
{
static bool flag_1s = 0;
static unsigned int tick_cnt = 0;
static unsigned int beforTick = 0;
unsigned int currentTick;
unsigned char val;
currentTick = HAL_GetTick();
if(beforTick != currentTick )
{
beforTick = currentTick;
tick_cnt++;
// 1s action
if( (tick_cnt % 1000) == 0)
{
flag_1s = true;;
}
//1ms action
bsp_KeyMonitor();
}
if( flag_1s )
{
flag_1s = false;
HAL_GPIO_TogglePin(SYS_RUN_LED_GPIO_Port, SYS_RUN_LED_Pin);
}
val = bsp_KeyGetValue( KEY_1 );
if( val )
{
test_can1_send();
}
val = bsp_KeyGetValue( KEY_2 );
if( val )
{
test_can2_send();
}
}
运行代码后,可以看见: