非阻塞实现高效键盘扫描功能(STM32F4XX)

目录

概述

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

运行代码后,可以看见:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/418614.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字后端——DEF文件格式

文章目录 MACRO的不同orientationDEF中在macro orientation定义前需要留空格 MACRO的不同orientation DEF中在macro orientation定义前需要留空格 像下图中这种方向和分号之间没有空格的情况&#xff0c;就是有问题的格式。

构建一个基于Node.js的文件存储服务

随着现代web应用程序变得越来越复杂和功能强大&#xff0c;文件存储服务成为了许多应用的重要组成部分。在本篇博客中&#xff0c;我们将探讨如何构建一个基于Node.js的文件存储服务&#xff0c;让您可以轻松地上传、下载和管理文件。我们将利用Node.js的强大功能和模块来构建这…

苍穹外卖知识点总结(一)

简介 技术选型 展示项目中使用到的技术框架和中间件。 用户层&#xff1a;node.js Vue.js ElementUI 微信小程序 apache echarts 网关层&#xff1a;nginx 应用层&#xff1a;Spring Boot Spring MVC Spring Task httpclie…

2.26 Qt day4+5 纯净窗口移动+绘画事件+Qt实现TCP连接服务+Qt实现连接数据库

思维导图 Qt实现TCP连接 服务器端&#xff1a; widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer>//服务器端类 #include<QTcpSocket>//客户端类 #include<QMessageBox>//消息对话框类 #include<QList>//链…

fordeal测评养号环境搭建:解决硬件、IP、浏览器等关键问题

Fordeal电商平台销售网点覆盖中东、欧美等多个国家和地区&#xff0c;其中中东市场是最重要的市场。 Fordeal主要为用户提供男女装、箱包及配饰、护肤彩妆、电子数码、运动用品等品类。 fordeal 支持多种语言、货币和支付方式。 1.点击Sign in进入登录界面。 2. 选择Register注…

第七篇:微信小程序的跳转页面

前提&#xff1a;建议还没学HTML、CSS、JavaScript、JSON、vue、Ajax的兄弟姐妹们&#xff0c;先去把这些基础补好过一遍&#xff0c;不然不好理解微信小程序 前面这一篇已经讲过一次<navigator>跳转页面的用法了&#xff0c;今天详细讲解一下 回顾&#xff1a; 小程序…

<网络安全>《60 概念讲解<第七课 网络模型OSI对应协议>》

1 OSI模型 OSI模型&#xff08;Open Systems Interconnection Model&#xff09;是一个由国际标准化组织&#xff08;ISO&#xff09;提出的概念模型&#xff0c;用于描述和标准化电信或计算系统的通信功能&#xff0c;以实现不同通信系统之间的互操作性。该模型将通信系统划分…

【笔记】:更方便的将一个List中的数据传入另一个List中,避免多重循环

这里是 simpleInfoList 集合&#xff0c;记为集合A&#xff08;传值对象&#xff09; List<CourseSimpleInfoDTO> simpleInfoList courseClient.getSimpleInfoList(courseIds);if(simpleInfoListnull){throw new BizIllegalException("当前课程不存在!");}这…

Ubuntu上Jenkins自动化部署Gitee上VUE项目

文章目录 1.安装NodeJS插件2.配置全局工具配置-NodeJS环境变量3.新建自由风格的软件项目任务4.配置General配置丢弃旧的构建配置参数化构建过程 5.配置源码管理6.构建触发器7.设置构建环境8.配置构建步骤9.配置构建后操作10测试构建 前文链接&#xff1a; Ubuntu上Jenkins自动…

使用 OpenCV 通过 SIFT 算法进行对象跟踪

本文介绍如何使用 SIFT 算法跟踪对象 在当今世界&#xff0c;当涉及到对象检测和跟踪时&#xff0c;深度学习模型是最常用的&#xff0c;但有时传统的计算机视觉技术也可能有效。在本文中&#xff0c;我将尝试使用 SIFT 算法创建一个对象跟踪器。 为什么人们会选择使用传统的计…

深入Linux内核(进程篇)—进程切换之ARM体系架构 简单总结

context_switch函数完成Arm架构Linux进程切换&#xff0c;调用两个函数&#xff1a; 调用switch_mm() 完成用户空间切换&#xff0c;刷新I-CACHE&#xff0c;处理ASID和TLB&#xff0c;页表转换基址切换&#xff08;即把TTBR0寄存器的值设置为新进程的PGD&#xff09;&#xf…

应用多元统计分析--多元数据的直观表示(R语言)

例1.2 为了研究全国31个省、市、自治区2018年城镇居民生活消费的分布规律&#xff0c;根据调查资料做区域消费类型划分。 指标&#xff1a; 食品x1&#xff1a;人均食品支出(元/人) 衣着x2&#xff1a;人均衣着商品支出(元/人) 居住x3&#xff1a;人均居住支出(元/人) 生活x4…

智能驾驶规划控制理论学习-基于采样的规划方法

目录 一、基于采样的规划方法概述 二、概率路图&#xff08;PRM&#xff09; 1、核心思想 2、实现流程 3、算法描述 4、节点连接处理 5、总结 三、快速搜索随机树&#xff08;RRT&#xff09; 1、核心思想 2、实现流程 3、总结 4、改进RRT算法 ①快速搜索随机图&a…

Newtonsoft.Json

目录 引言 1、简单使用 1.1、官方案例 1.2、JsonConvert 2、特性 2.1、默认模式[JsonObject(MemberSerialization.OptIn/OptOut)] 2.2、序列化为集合JsonArrayAttribute/JsonDictionaryAttribute 2.3、序列化该元素JsonProperty 2.4、忽略元素JsonIgnoreAttribute 2.5、…

来,和同频的人一起学习论文#理解技术趋势

学习新技术&#xff0c;慢慢也有了施展拳脚的地方。今天我们给ComfyUI中文爱好者社区成员提供了一个工作机会&#xff0c;有需要可以联系我们的小助手&#xff1a; 相信这几天大家都看到了我们更新了些论文笔记出来&#xff0c;阅读1篇英文论文我们需要花几个小时&#xff0c;如…

STM32串口DMA发送接收(1.5Mbps波特率)机制

数据拷贝过程中不需要CPU干预&#xff0c;数据拷贝结束则通知CPU处理。 以115200bps波特率&#xff0c;1s传输11520字节&#xff0c;大约69us需响应一次中断&#xff0c;如波特率再提高&#xff0c;将消耗更多CPU资源 高波特率场景下&#xff0c;串口非常有必要使用DMA。 关…

C#使用iText7将多个PDF文档合并为单个文档

使用HtmlAgilityPack抓取并分析网页内容&#xff0c;然后再调用PuppeteerSharp将网页生成PDF文件&#xff0c;最终的成果如下图所示&#xff0c;得到将近120个pdf文档。能看&#xff0c;但是不方便&#xff0c;需要逐个打开文档才能看到所需的内容&#xff0c;最好能将这些文档…

Ps:绘画对称功能

Photoshop 中的绘画对称 Paint Symmetry功能允许用户在画布上创建对称的绘画和设计&#xff0c;极大地提高了创作的效率和准确性&#xff0c;尤其适合于制作复杂的对称图形和图案。 可在使用画笔工具、铅笔工具或橡皮擦工具时启用“绘画对称"功能。 提示&#xff1a; 绘画…

【IO流系列】ObjectStream 序列化流与反序列化流

序列化流与反序列化流 1. 概述2. 作用3. 序列化流&#xff08;对象操作字节输出流&#xff09;3.1 构造方法3.2 成员方法3.3 代码示例 4. 反序列化流&#xff08;对象操作字节输入流&#xff09;4.1 构造方法4.2 成员方法4.3 代码示例 5. 细节6. 练习6.1 练习1&#xff1a;用对…

看待事物的层与次 | DBA与架构的一次对话交流

前言 在计算机软件业生涯中,想必行内人或多或少都能感受到系统架构设计与数据库系统工程的重要性,也能够清晰地认识到在计算机软件行业中技术工程师这个职业所需要的专业素养和必备技能! 背景 通过自研的数据库监控管理工具,发现 SQL Server 数据库连接数在1-2K之间,想…