当前位置: 首页 > news >正文

STM32GPIO输入实战-按键key模板及移植

STM32GPIO输入实战-按键key模板及移植

  • 一,按键模板展示
  • 二,按键模板逻辑
    • 1,准备工作:头文件与全局变量
    • 2,读取硬件状态:key_read_raw()
    • 3,核心处理:`key_process_simple()` 的四行代码
  • 三,HAL_GPIO_ReadPin()解读
    • 1, 功能与作用
    • 2,参数拆解
    • 3,返回结果
    • 4,应用场景
    • 5,硬件抽象层
  • 四,按键模板移植
    • 1,模板移植
    • 2,移植修改

一,按键模板展示

“key_app.c”

#include "key_app.h"uint8_t key_val,key_old,key_down,key_up;uint8_t key_read()
{uint8_t temp = 0;if(HAL_GPIO_ReadPin(GPIOB, key1_Pin) == GPIO_PIN_RESET) temp = 1; if(HAL_GPIO_ReadPin(GPIOB, key2_Pin) == GPIO_PIN_RESET) temp = 2; if(HAL_GPIO_ReadPin(GPIOB, key3_Pin) == GPIO_PIN_RESET) temp = 3; if(HAL_GPIO_ReadPin(GPIOB, key4_Pin) == GPIO_PIN_RESET) temp = 4; if(HAL_GPIO_ReadPin(GPIOD, key5_Pin) == GPIO_PIN_RESET) temp = 5; if(HAL_GPIO_ReadPin(GPIOD, key6_Pin) == GPIO_PIN_RESET) temp = 6; return temp;
}void key_task()
{key_val = key_read();key_down = key_val & (key_old ^ key_val);key_up = ~key_val & (key_old ^ key_val);key_old = key_val;if(key_down == 1){ucLed[0] ^= 1;}}

“key_app.h”

#ifndef __KEY_APP_H__
#define __KEY_APP_H__#include "mydefine.h"void key_task(void);#endif

移植时需要修改引脚

二,按键模板逻辑

上面的模板是我在51单片机中使用的,它仅用四行核心代码实现的按键处理,它非常适合资源极其有限,或者只需要判断按键"按下"和"松开"两种状态的简单场景。虽然简洁,但牺牲了去抖动和复杂功能。

1,准备工作:头文件与全局变量

假设我们有一个 key_app.h 文件,包含了必要的 HAL 库定义。我们需要几个全局变量来存储按键的状态信息:

#include "key_app.h"uint8_t key_val,key_old,key_down,key_up;

或者

#include "key_app.h" // 包含必要的定义// 使用更明确的变量名
uint8_t g_key_current_state = 0; // 当前按键值 (哪个键按下? 0代表无)
uint8_t g_key_previous_state = 0; // 上一次循环检测到的按键值
uint8_t g_key_pressed_flag = 0; // 按键按下的标志 (哪个键刚被按下?)
uint8_t g_key_released_flag = 0; // 按键释放的标志 (哪个键刚被释放?)

2,读取硬件状态:key_read_raw()

这个函数直接与硬件打交道,读取引脚电平。假设按键按下时引脚为低电平 (GPIO_PIN_RESET)。我们给函数加上 “raw” 后缀,强调它读取的是未经处理的原始状态。

// 读取按键原始状态 (无去抖)
uint8_t key_read_raw(void) {uint8_t key_value = 0; // 初始化为0 (无按键)// 假设 KEY1-3 在 GPIOB, KEY4 在 GPIOAif(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) key_value = 1; // KEY1if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) key_value = 2; // KEY2if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) key_value = 3; // KEY3if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) key_value = 4; // KEY4 (WK_UP)return key_value; // 返回当前按下的按键编号 (1-4),或 0
}

警告: 这是最基础的读取,完全没有考虑按键抖动。在实际硬件上,按键按下和松开的瞬间,物理触点会快速通断多次,导致电平跳变。这个函数可能会在一次"按下"中读到多次0和1的交替。此方法仅适用于对实时性要求不高、能容忍偶尔误判或抖动不明显的场合。

3,核心处理:key_process_simple() 的四行代码

这四行代码利用位运算,巧妙地捕捉状态变化:

// 极简按键处理函数
void key_process_simple(void) {// 1. 读取当前原始状态g_key_current_state = key_read_raw();// 2. 检测哪些键【刚刚按下】(之前是0/未按,现在是1/按下)//    - (g_key_previous_state ^ g_key_current_state) 找出所有状态变化的位//    - & g_key_current_state 保留那些当前状态为1 (按下) 的变化位g_key_pressed_flag = g_key_current_state & (g_key_previous_state ^ g_key_current_state);// 3. 检测哪些键【刚刚释放】(之前是1/按下,现在是0/未按)//    - (g_key_previous_state ^ g_key_current_state) 同样找出所有状态变化的位//    - & ~g_key_current_state (或 & g_key_previous_state) 保留那些当前状态为0 (释放) 的变化位g_key_released_flag = ~g_key_current_state & (g_key_previous_state ^ g_key_current_state);// 等效写法: g_key_released_flag = g_key_previous_state & (g_key_previous_state ^ g_key_current_state);// 4. 更新上一次的状态,为下一次比较做准备g_key_previous_state = g_key_current_state;
}

如何使用?
在程序的初始化部分调用一次 key_process_simple() (或手动将 g_key_previous_state 初始化为 key_read_raw() 的结果)。
在主循环或定时器中断(推荐,如每 10-20ms)中周期性地调用 key_process_simple()。
在需要检查按键的地方,检查 g_key_pressed_flag 和 g_key_released_flag 的值。例如,if (g_key_pressed_flag == 1) { /* KEY1 被按下了 */ }。记得在处理完标志后将其清零,避免重复触发。
这种方法的优点是代码量极少,逻辑直接;缺点是抗干扰能力差,无法区分长按、短按、双击等复杂操作。

也可以

	key_val = key_read();key_down = key_val & (key_old ^ key_val);key_up = ~key_val & (key_old ^ key_val);key_old = key_val;

三,HAL_GPIO_ReadPin()解读

前面的 key_read_raw() 函数用到了 HAL_GPIO_ReadPin()。这是 STMicroelectronics 公司提供的 HAL (硬件抽象层) 库中的一个标准函数。理解它是进行 GPIO 输入操作的基础。

1, 功能与作用

一句话概括: 读取指定 GPIO 引脚当前的电平是高 (High / 1) 还是低 (Low / 0)。

官方定义 (函数原型):

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

这里的 GPIO_PinState 是一个枚举类型,定义了两种可能的状态。

2,参数拆解

GPIO_TypeDef *GPIOx: 这是指向 GPIO 端口寄存器结构体的指针。你需要告诉函数你想操作哪个"大门"(端口),比如 GPIOA, GPIOB 等。这些宏通常在芯片对应的 HAL 库头文件中定义。
uint16_t GPIO_Pin: 这是一个 16 位的无符号整数,用来指定你要读取这个"大门"上的哪个"小门"(具体的引脚)。例如 GPIO_PIN_0, GPIO_PIN_1, …, GPIO_PIN_15。这些也是预定义的宏,对应引脚编号。

3,返回结果

函数执行后,会返回一个 GPIO_PinState 类型的值,代表读取到的引脚状态:

GPIO_PIN_SET: 表示引脚当前是高电平。通常数值上等于 1。
GPIO_PIN_RESET: 表示引脚当前是低电平。通常数值上等于 0。
你可以直接用 == 来比较返回值,例如 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
下面是GPIO_PinState的定义

typedef enum
{GPIO_PIN_RESET = 0,GPIO_PIN_SET
}GPIO_PinState;

4,应用场景

作为读取引脚状态的基础,它无处不在:
按键检测: 判断按键是否被按下(检测是高还是低)。
传感器状态读取: 获取数字传感器(如霍尔传感器、红外对管)的输出信号。
状态指示灯确认: (较少见)读取某个引脚是否被外部拉高/拉低。
简单通信协议: 在软件模拟 I2C、SPI 或自定义协议时,读取数据线或状态线。

5,硬件抽象层

我们调用 HAL_GPIO_ReadPin() 时,实际上 HAL 库为我们做了这些事:

  1. 定位寄存器: 根据你传入的 GPIOx (如 GPIOA),找到对应端口的输入数据寄存器 (IDR - Input Data Register) 的内存地址。
  2. 读取与位操作: 读取整个 IDR 寄存器的值。然后,根据你传入的 GPIO_Pin (如 GPIO_PIN_5),通过位掩码 (Bit Masking) 和位移 (Bit Shifting) 操作,精确地提取出代表该引脚状态的那一位 (0 或 1)。
  3. 返回标准状态: 将提取出的 0 或 1 转换为标准的 GPIO_PIN_RESET 或 GPIO_PIN_SET 枚举值返回。

HAL 库的意义就在于此:它屏蔽了底层寄存器的复杂细节,提供了一套统一、易用的接口。即使未来 ST 公司推出了新的芯片,只要它们遵循 HAL 标准,你的代码(理论上)只需要很小的改动就能移植过去,这就是硬件抽象的好处。

四,按键模板移植

有了前面led的移植经验,移植按键将快很多

1,模板移植

打开要移植的工程APP文件,将上面展示的按键.c.h文件粘贴过来
在这里插入图片描述
打开CubeMX,根据原理图生成GPIO输入相关代码
在这里插入图片描述
我的原理图为:引脚被上拉,所以选上拉输入模式,引脚有6个即A0,A1,C0,C1,C2,C3
在这里插入图片描述
在CubeMX中选择引脚为输入模式
在这里插入图片描述
选择上拉输入,标签/宏定义随意
在这里插入图片描述
点击生成代码,生成完毕后,关闭(我的工程已经打开)
在这里插入图片描述
回到工程,重新加载
在这里插入图片描述
在这里插入图片描述
确定更换芯片
在这里插入图片描述
停止等待
在这里插入图片描述
更换芯片
在这里插入图片描述

每次重新生成代码时,都需要重新选择芯片

将桌面上的key文件添加的keil,移植完毕
在这里插入图片描述

2,移植修改

移植完毕后发现ucLed在key_app.c中没有定义,key_app.c引用key_app.h文件
在这里插入图片描述
而key_app.h只引用了mydefine.h(这里会包含所有自定义头文件)
在这里插入图片描述
跳转到mydefine.h,它调用了所有头文件,包括led相关的头文件,但是依旧包led相关变量未定义的错,
在这里插入图片描述
因为led_app.h只声明了led相关的函数,没有声明ucLed变量
在这里插入图片描述
只需将led_app.c中的ucLed引用过来即可(用extern)
在这里插入图片描述
此时编译不在报错,但是为了让其他底层使用key_app.c中的函数,需要将key的头文件也添加到mydefine.h中
在这里插入图片描述
接下来修改引脚
在main.h中可以看到CubeMX生成的引脚宏定义
在这里插入图片描述
但是直接将key模板移植过来时,模板的引脚和我们使用的引脚不匹配
在这里插入图片描述
修改模板引脚成为我们需要的引脚
在这里插入图片描述
最后在调度器中加上任务函数
在这里插入图片描述

修改完毕

http://www.xdnf.cn/news/278659.html

相关文章:

  • I.MX6U的GPIO配置和LED点灯实验。
  • leetcode:最小覆盖字符串
  • 【操作系统】吸烟者问题
  • NHANES指标推荐:LC9
  • Android第四次面试总结之Java基础篇(补充)
  • 【NTN 卫星通信】NTN关键问题的一些解决方法(一)
  • 55认知干货:深空产业
  • 2022年第十三届蓝桥杯省赛B组Java题解
  • 128. 最长连续序列
  • 【人工智能】大模型安全的深度剖析:DeepSeek漏洞分析与防护实践
  • 牛客周赛91 D题(数组4.0) 题解
  • 如何用更少的显存训练 PyTorch 模型
  • 【Java JUnit单元测试框架-60】深入理解JUnit:Java单元测试的艺术与实践
  • Spring AI 实战:第九章、Spring AI MCP之万站直通
  • HTML5实战指南:语义化标签与表单表格高级应用
  • AI日报 · 2025年5月04日|Hugging Face 启动 MCP 全球创新挑战赛
  • 《工业社会的诞生》章节
  • 相向双指针-16. 最接近的三数之和
  • 基于AWS Marketplace的快速解决方案:从选型到部署实战
  • OpenFAST 开源软件介绍
  • 大学之大:高丽大学2025.5.4
  • Java并发编程-多线程基础(三)
  • 在 Ubuntu 系统中,查看已安装程序的方法
  • Redis-----认识NoSQL
  • 网络开发基础(游戏)之 心跳机制
  • C++ 多态:原理、实现与应用
  • 科学养生,健康生活
  • Python容器与循环:数据处理的双剑合璧
  • 虚函数 vs 纯虚函数 vs 静态函数(C++)
  • 原型模式(Prototype Pattern)