STM32GPIO输入实战-key按键easy_button库移植
STM32GPIO输入实战-key按键easy_button库移植
- 一,ebtn介绍
- 二,ebtn移植
- 三,组件库的思想组成
- 1. 事件驱动 (Event-Driven) 🛎️ —— 像按门铃
- 2. 状态机 (State Machine) 🚦 —— 像红绿灯
- 3. 回调函数 (Callback Function) 📞 —— 留下你的电话
- 四,建立组件库应用层
- 1,建立应用层ebtn_app文件
- 2,定义KEY_ID、按键参数和按键数组
- 1. 定义按键参数defaul_ebtn_param
- 2. 定义按键ID
- 3,定义按键数组
- 3, 编写回调函数,完善按键驱动初始化
- 1,prv_btn_get_state解释
- 2,prv_btn_event解释
- 4,初始化 ebtn 库
- 5,周期性调用处理函数
一,ebtn介绍
前面介绍的按键 4 行代码虽然极致简洁,但是无法区分单击、双击、长按等常用操作。这时,就需要请出easy button库了。
ebtn
库是组件库,其的目标是让你只需关注按键"事件"本身,而把底层的抖动处理、状态判断、时间计算等繁琐细节交给它来完成。
据作者介绍ebtn有多处优势
打开github,搜索作者
解压后Typora打开说明文档
二,ebtn移植
打开工程文件,新建组件库文件
复制组件库ebtn,粘贴到Component
打开Keil,将组件库文件添加到Keil,编译检查错误
没有错误,框架好,就不需要修改
基于组件库建立组件库应用层文件ebtn_app.c和.h,添加到APP
三,组件库的思想组成
1. 事件驱动 (Event-Driven) 🛎️ —— 像按门铃
想象一下你家的门铃。你不需要每隔几秒就跑去门口问:“有人吗?”。你只需要待在屋里,当有人按下门铃时(这就是一个"事件"),铃声会响起,你再根据是谁(什么事件)决定开门(执行相应的操作)。
ebtn 就是这样工作的。它不会让你一直去检查按键状态,而是当按键发生了有意义的动作(比如按下、释放、单击、长按)时,它会主动"通知"你发生了什么"事件"。这种方式比你不停地去问"按键按下了吗?"(这叫轮询 Polling,如之前的四行代码)要高效得多,也更符合人类的直觉。
2. 状态机 (State Machine) 🚦 —— 像红绿灯
建议多了解状态机,后面出一个基于状态机的按键框架
红绿灯有几种状态:红灯、绿灯、黄灯。它不会一下子从红灯跳到黄灯,必须先变成绿灯。状态之间的切换是有固定规则和顺序的。
ebtn 内部也为每个按键维护了一个"状态机"。这个状态机记录了按键当前处于哪个阶段,比如:
空闲状态: 按键没被按下。
抖动检测状态: 刚检测到按下信号,需要等待一小段时间(去抖时间)确认不是干扰。
按下状态 (Pressed): 确认是有效按下,并且持续按着。
单击判断状态: 按下后很快松开了,正在等待看是否会有下一次点击(用于判断连击)。
释放状态 (Released): 确认有效松开。
ebtn 根据你按下的时间长短、松开的时间、以及连续点击的间隔,自动地在这些状态之间切换。你不需要关心这些复杂的状态转换,只需要等待 ebtn
告诉你最终的结果(事件)。
3. 回调函数 (Callback Function) 📞 —— 留下你的电话
想象你去餐厅点餐,服务员告诉你菜好了会叫你。你留下你的桌号(这就是"回调信息"),然后就可以做自己的事了。当菜准备好时,服务员会根据桌号找到你并通知你(这就是"回调")。
在 ebtn 中,你需要提供两个重要的"联系方式":
get_state_fn
(获取状态函数): 你需要告诉 ebtn 如何读取按键的物理状态(高电平还是低电平)。ebtn 在需要的时候会"打电话"给这个函数来获取信息。就像服务员问你:“你现在饿了吗?”。
evt_fn
(事件通知函数): 你需要告诉 ebtn,当有按键事件发生时,应该去执行哪个函数来处理。就像你告诉服务员:“菜好了,请到这个桌号通知我。”。当 ebtn
检测到单击、长按等事件时,就会"打电话"给你提供的这个函数,并告诉你哪个按键发生了什么事件。
通过这种方式,ebtn 负责检测和判断,而你只需要在 evt_fn
这个函数里编写处理逻辑(比如开关灯、发送消息等)。
驱动事件解耦,回调函数注册API
用事件驱动代替轮询,回调函数实现保留API,不同场景实现不同的API
四,建立组件库应用层
1,建立应用层ebtn_app文件
将组件库头文件引用到ebtn_app.c中
编译,有一个报错说找不到ebtn.h,因为没有添加组件库.h文件路径
2,定义KEY_ID、按键参数和按键数组
1. 定义按键参数defaul_ebtn_param
如下图
const是什么?
const 是 C/C++ 中的关键字,用于定义常量,表示该变量的值不可修改。(它会拒绝编译器对所定义的变量的修改)
ebtn_btn_param_t是组件库里定义的结构体类型,如下图。
结构体成员 | 说明 |
---|---|
time_debounce | 防抖处理,按下防抖超时,配置为0,代表不启动 |
time_debounce_release | 防抖处理,松开防抖超时,配置为0,代表不启动 |
time_click_pressed_min | 按键超时处理,按键最短时间,配置为0,代表不检查最小值 |
time_click_pressed_max | 按键超时处理,按键最长时间,配置为0xFFFF,代表不检查最大值,用于区分长按和按键事件。 |
time_click_multi_max | 多击处理,两个按键之间认为是连击的超时时间 |
time_keepalive_period | 长按处理,长按周期,每个周期增加keepalive_cnt计数 |
max_consecutive | 最大连击次数,配置为0,代表不进行连击检查。 |
原文链接:https://blog.csdn.net/wenbo13579/article/details/136268852
而defaul_ebtn_param是ebtn_btn_param_t类型的结构体defaul_ebtn_param
defaul:默认
ebtn:即easy_botton
param:参数
所以单从名字,我们可以知道,它是按键的默认参数
在后面需要用到,将按键与这个参数绑定
EBTN_PARAMS_INIT是组件库规定的一个宏
为什么使用宏定义呢?
因为宏定义可以把我们规定的参数赋值给结构体里的成员
如果不使用宏,直接在代码中对 ebtn_btn_param_t 结构体变量 defaul_ebtn_param 进行初始化,需要逐个写出成员变量的赋值语句,代码会显得较为零散。而使用宏定义,在初始化结构体变量时,能一眼看清各个关键参数的设置
2. 定义按键ID
使用枚举定义按键ID
typedef
为已有的数据类型(这里即enum枚举类型)创建一个新的别名,之后可直接用该枚举类型名称user_button_t定义
enum
代表这是枚举类型的变量,枚举具体可以看我关于宏和枚举的笔记
由于定义user_button_t时使用了typedef,
3,定义按键数组
static
关键字,设定该变量为局部静态变量
static 修饰的变量具有静态存储持续性,它在程序启动时就被创建,在整个程序的生命周期内都存在,不会因为代码块的结束而被销毁。(可持续储存)
其他文件无法通过外部链接(extern)来访问这个数组。(防止意外修改)
静态变量只会被初始化一次。(按键数据不会被多次初始化)
按键控制结构体说明-ebtn_btn_t
此结构体是组件库中定义的,用于记录按键当前状态,按键参数等信息。总之,它就像按键的身份证,包含了按键的基本信息
因为使用了变量别名typedef,所以struct ebtn_btn可以替换为 ebtn_btn_t
结构体成员 | 说明 |
---|---|
key_id | 用户定义的key_id信息,该值建议唯一 |
flags | 用于记录一些状态,目前只支持EBTN_FLAG_ONPRESS_SENT和EBTN_FLAG_IN_PROCESS |
time_change | 记录按键按下或者松开状态的时间点 |
time_state_change | 记录按键状态切换时间点(并不考虑防抖,单纯记录状态切换时间点) |
keepalive_last_time | 长按最后一次上报长按时间的时间点,用于管理keepalive_cnt |
click_last_time | 点击最后一次松开状态的时间点,用于管理click_cnt |
keepalive_cnt | 长按的KEEP_ALIVE次数 |
click_cnt | 多击的次数 |
param | 按键时间参数,指向ebtn_btn_param_t,方便节省RAM,并且多个按键可公用一组参数 |
原文链接:https://blog.csdn.net/wenbo13579/article/details/136268852
EBTN_BUTTON_INIT宏有什么用?
前面提到过,按键参数需要与按键ID关联,此宏可以实现
其作用是关联按键 ID 与参数:
将 user_button_t 枚举类型中定义的按键 ID(比如 USER_BUTTON_0 、USER_BUTTON_1 等 )与按键参数 defaul_ebtn_param 关联起来。在按键处理相关逻辑中,通过这种关联,程序能识别不同按键并应用对应的参数配置
结构体数组ebtns[]
以EBTN_BUTTON_INIT宏为成员关联按键ID与按键参数
3, 编写回调函数,完善按键驱动初始化
在ebtn.h中,有一个按键驱动初始化函数,ebtn_evt_fn 和 ebtn_get_state_fn 这两个回调函数作为其形参
回调函数指的是将函数指针当作参数传递给另一个函数,并且在合适的时机由接收到该指针的函数来调用
右键跳转到它们的定义
红框翻译:
转到 “ebtn_get_state_fn” 的定义
转到 “ebtn_get_state_fn” 的下一处引用
转到 “ebtn_get_state_fn” 的上一处引用
发现这两个函数定义了,但是没有具体内容
这需要我们在应用层再次定义一下
1,prv_btn_get_state解释
ebtn_get_state_fn定义为prv_btn_get_state
词段 | 含义 | 嵌入式领域惯例 |
---|---|---|
prv | private (私有) | 表示该函数是模块内部使用,不对外暴露 |
btn | button (按钮) | 缩写,指代按键相关功能 |
get | 获取 | 常见的操作动词 |
state | 状态 | 指按键的当前状态(按下/释放等) |
所以显而易见,这个函数名字代表它是用来获取按键实际状态的
怎么实现呢?
1,btn 是函数形参,它是一个指向 ebtn_btn 结构体的指针,通过这个指针,函数可以访问和操作 ebtn_btn 结构体中的成员(看上图,其实就是操作成员key_id)
2,prv_btn_get_state 函数会通过传入的指针 btn 来访问 ebtn_btn_t 结构体的成员
在函数中switch (btn->key_id):btn->key_id则是代表我们想访问结构体的某一个具体内容,也就是key_id
所以case是选择哪一个按键。毕竟是按键的id
之前讲到ebtn_btn是存储了按键参数的结构体,组件库定义时为其加了别名:struct ebtn_btn可以用ebtn_btn_t替代
所以函数prv_btn_get_state 的功能就很明确了:扫描按键ID,获取按键的高低电平并返回
按键高低电平的两种写法:
2,prv_btn_event解释
ebtn_evt_fn定义为prv_btn_event
第一个形参是ebtn_btn_t/struct ebtn_btn类型的结构体,和上一个函数一样,传入key_id
第二个形参是ebtn_evt_t 类型的枚举变量evt,代表按键事件
此函数接收组件库整理好的按键事件,留给我们去写事件对应的逻辑代码:
typedef enum
{EBTN_EVT_ONPRESS = 0x00,//按键被按下EBTN_EVT_ONRELEASE,//按键从按下变为释放 EBTN_EVT_ONCLICK, //按键被单击 EBTN_EVT_KEEPALIVE, //按键被长按
} ebtn_evt_t;
4,初始化 ebtn 库
在系统启动的初始化阶段(例如 main 函数开始处,或专门的初始化函数中),调用 ebtn_init 函数,将之前准备好的按键列表和回调函数通过ebtn_init 函数"注册"给 ebtn
库。
暂时没有用到组合按键
代码运行流程
[ System Start ]
↓
main()
├─ HAL_Init()
├─ SystemClock_Config()
├─ MX_GPIO_Init()
└─ ebtn_app_init() ← 注册 btns[ ]、prv_btn_get_state、prv_btn_event
[ SysTick 每 1ms ]
↓
HAL_SYSTICK_Callback()
└─ ebtn_task() ← 调用 ebtn_process(now_ms)
↓
ebtn_process(now_ms)
├─ 对每个按键 btn:
│ ├─ 调 prv_btn_get_state(btn) → 读 GPIO
│ ├─ 去抖 / 状态机推进
│ ├─ 若检测到单击/双击/长按 →
│ │ 调 prv_btn_event(btn, evt)
│ └─ 继续下一个 btn
└─ 结束
prv_btn_event(btn, EBTN_EVT_ONCLICK)
└─ switch(btn->key_id):
case USER_BUTTON_0:
if click_cnt=1 → ucLed[0]=1;
else if click_cnt=2 → ucLed[0]=0;
总结
prv_btn_get_state的职责:把 GPIO 读成 “0/1” 通知给组件库。
prv_btn_event的职责:接收组件库识别好的“单击/双击”等事件,做你的应用逻辑。
而ebtn_init的职责:btns[] 数组和prv_btn_get_state / prv_btn_event 注册到组件库里
5,周期性调用处理函数
将任务加入调度器
将初始化放在调度器之前
为什么周期性调用如此重要? ebtn 库内部的所有计时(去抖、单击超时、长按周期)都依赖于你通过 ebtn_process 传入的当前时间。如果调用间隔不规律或太长,会导致时间判断错误,按键事件也就无法被正确识别了。
相关文章:

STM32GPIO输入实战-key按键easy_button库移植
STM32GPIO输入实战-key按键easy_button库移植 一,ebtn介绍二,ebtn移植三,组件库的思想组成1. 事件驱动 (Event-Driven) 🛎️ —— 像按门铃2. 状态机 (State Machine) 🚦 —— 像红绿灯3. 回调函数 (Callback Function…...

【递归、搜索和回溯】递归、搜索和回溯介绍及递归类算法例题
个人主页 : zxctscl 专栏 【C】、 【C语言】、 【Linux】、 【数据结构】、 【算法】 如有转载请先通知 文章目录 递归、搜索和回溯递归搜索VS 深度优先遍历 VS 深度优先搜索 VS 宽度优先遍历 VS 宽度优先搜索 VS 暴搜回溯与剪枝 1 面试题 08.06. 汉诺塔问题1.1 分析…...

JDK8 HashMap红黑树退化为链表的机制解析
目录 1、数据结构: 2、Fail-Fast机制 2.1、核心作用 2.2、实现原理 2.3、触发场景 2.4、实现细节 2.5、对比 2.6、注意事项 3、核心结论 4、转化安全机制 4.1. 触发场景 4.2. 转换过程 4.3. 并发安全机制 5、设计原因 5.1. 性能权衡 5.2. 空间局部性…...

【基础】模型上下文协议(Model Context Protocol, MCP)根本原理与工作机制详解
一、MCP的根本原理 模型上下文协议(MCP)是一种标准化接口协议,旨在解决AI系统(尤其是大型语言模型,LLM)与外部工具、数据源之间的交互碎片化问题。其核心原理可以概括为以下三点: 统一接口抽象…...

霸王茶姬微信小程序自动化签到系统完整实现解析
霸王茶姬微信小程序自动化签到系统完整实现解析 技术栈:Node.js 微信小程序API MD5动态签名 一、脚本全景架构 功能模块图 #mermaid-svg-0vx5W2xo0IZWn6mH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-s…...
北斗导航 | RTKLib中重难点技术,公式,代码
Rtklib 一、抗差自适应卡尔曼滤波1. **核心难点**2. **公式与代码实现**二、模糊度固定与LAMBDA算法1. **核心难点**2. **LAMBDA算法实现**3. **部分模糊度固定技术**三、伪距单点定位与误差修正1. **多系统多频点修正**2. **接收机钟差与系统间偏差**四、动态模型与周跳处理1.…...

p2p虚拟服务器
ZeroTier Central ✅ 推荐工具:ZeroTier(免费、稳定、跨平台) ZeroTier 可以帮你把多台设备(无论是否跨网)加入一个虚拟局域网,彼此间可以像在同一个 LAN 中通信,UDP 视频、文件传输、SSH 等都…...
Python 爬虫基础入门教程(超详细)
一、什么是爬虫? 网络爬虫(Web Crawler),又称网页蜘蛛,是一种自动抓取互联网信息的程序。爬虫会模拟人的浏览行为,向网站发送请求,然后获取网页内容并提取有用的数据。 二、Python爬虫的基本原…...

python实现点餐系统
使用python实现点餐系统的增加菜品及价格,删除菜品,查询菜单,点菜以及会员折扣价等功能。 代码: 下面展示一些 内联代码片。 # coding utf-8menu {拍黄瓜: 6, 小炒肉: 28, 西红柿炒蛋: 18, 烤鱼: 30, 红烧肉: 38, 手撕鸡: 45,…...

(三)毛子整洁架构(Infrastructure层/DapperHelper/乐观锁)
文章目录 项目地址一、Infrastructure Layer1.1 创建Application层需要的服务1. Clock服务2. Email 服务3. 注册服务 1.2 数据库服务1. 表配置Configurations2. Respository实现3. 数据库链接Factory实现4. Dapper的DataOnly服务实现5. 所有数据库服务注册 1.3 基于RowVersion的…...

探索Stream流:高效数据处理的秘密武器
不可变集合 stream流 Stream流的使用步骤: 先得到一条Stream流(流水线),并把数据放上去 使用中间方法对流水线上的数据进行操作 使用终结方法对流水线上的数据进行操作 Stream流的中间方法 注意1:中间方法࿰…...
git高效杀器——cz-customizable 搭配 commitlint
What is cz-customizable and commitlint? cz-customizable 一款可定制化的Commitizen插件(也可作为独立工具),旨在帮助创建如约定式提交规范的一致性提交消息。commitlint commitlint 是一个用于检查 Git 提交信息的工具,它可以帮助开发者保持提交信息的规范性和一致性。…...

虚拟机ubantu20.04系统桥接模式下无法ping通外网,但可以ping通本机的解决方案
1.出现的问题: 虚拟机ubantu20.04系统桥接模式下无法ping通外网,但可以ping通本机。 2.解决方案: 如果 DHCP 未分配 IP 地址,可以手动配置静态 IP: 1.编辑网络配置文件: sudo nano /etc/netplan/01-netcfg.yaml 2…...

日常知识点之随手问题整理(思考单播,组播,广播哪个更省带宽)
新入职的公司在某些场景下无脑使用组播技术,自己突然就意识到一个问题:单播,组播,广播,哪个更省带宽? 有所收获,做点笔记,仅仅是个人理解~ 1:简单理解 单播࿱…...

qtcreater配置opencv
我配置opencv不管是按照网上的教程还是deep seek发现都有些问题,下面是我的配置方法以及实践成功的心得 电脑环境 windows平台qt6 下载 我这里直接提供官网下载地址:https://opencv.org/releases/ 我下载的是最新版,下载后是一个.exe文件…...
详解 c++17 重载类 overload的每一条语句,附实例.
author: hjjdebug date: 2025年 05月 09日 星期五 16:21:03 CST description: 详解 c17 重载类 overload的每一条语句 文章目录 1. template 模板类.2. class... Ts 是什么意思?3. template<class... Ts> 是什么意思?4. overload 是什么?5. Ts...…...

机器学习-数据集划分和特征工程
一.数据集划分 API函数: sklearn.model_selection.train_test_split(*arrays,**options) 参数: - arrays:多个数组,可以是列表,numpy数组,也可以是dataframe数据框等 - options:&…...

MySQL C API高效编程:C语言实现数据库操作的深入解析
知识点【MySQL C API】 1、头文件及MYSQL * 句柄 //头文件 #include <mysql/mysql.h>1、MYSQL MYSQL是一个结构体,封装了与数据库连接相关的所有状态,配置和数据。 2、MYSQL *的本质 类似于 FILE*,代表一个与数据库连接的通道&…...

MySQL初阶:数据库约束和表的设计
数据库约束 数据库约束是针对数据库中的表中的数据进行施加规则和条件,用于确保数据的准确性和可靠性。 数据库约束类型 1)not null 非空类型 :指定非空类型的列不能存储null,如果插入的数据是null便会报错。 2)de…...

LeetCode 解题思路 47(最长回文子串、最长公共子序列)
解题思路: dp 数组的含义: dp[i][j] 是否为回文子串。递推公式: dp[i][j] s.charAt(i) s.charAt(j) && dp[i 1][j - 1]。dp 数组初始化: 单字符 dp[i][i] true,双字符 dp[i][i 1] s.charAt(i) s.charA…...
左支座加工工艺与钻φ25孔专用夹具设计
1 零件结构与工艺分析 1.1 零件结构特征 本左支座为典型箱体类零件,采用HT200灰铸铁铸造毛坯。主体结构包含: 20015080mm安装基面 2φ12定位孔(公差H7) φ250.02主轴承孔(表面粗糙度Ra1.6) 4M10螺纹安…...
基于Qwen-14b的基础RAG实现及反思
1、概览 本文主要介绍RAG的基础实现过程,给初学者提供一些帮助,RAG即检索增强生成,主要是两个步骤:检索、生成,下面将基于这两部分进行介绍。 2、检索 检索的主要目的是在自定义的知识库kb中查询到与问题query相关的候…...

嵌入式培训之C语言学习完(十七)结构体、共用体、枚举、typedef关键字与位运算
目录 一、结构体(struct关键字) (一)声明一个结构体数据类型 (二)结构体的成员初始化与赋值 a、结构体变量赋值 b、结构体成员初始化 c、结构体的定义形式 (三)考点ÿ…...
极狐GitLab 命名空间的类型有哪些?
极狐GitLab 是 GitLab 在中国的发行版,关于中文参考文档和资料有: 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 命名空间 命名空间在极狐GitLab 中组织项目。因为每一个命名空间都是单独的,您可以在多个命名空间中使用相同的项…...
N6715C 基础型定制配置直流电源分析仪
N6715C 基础型定制配置直流电源分析仪 综述 N6715C 是一款可定制的直流电源分析仪系统,在装运之前已经过全面测试并组装完毕。 每台 N6715C 包括一个 N6705C 主机和 1 至 4 个模块。 模块作为 E6715C 的选件订购。 主要特点 ◆ ◆ 4 插槽主机最多可安装 4 个模块…...
4.1【LLaMA-Factory 实战】医疗领域大模型:从数据到部署的全流程实践
【LLaMA-Factory实战】医疗领域大模型:从数据到部署的全流程实践 一、引言 在医疗AI领域,构建专业的疾病诊断助手需要解决数据稀缺、知识专业性强、安全合规等多重挑战。本文基于LLaMA-Factory框架,详细介绍如何从0到1打造一个垂直领域的医…...

《软件项目经济性论证报告模板:全面解析与策略建议》
《软件项目经济性论证报告模板:全面解析与策略建议》 一、引言 1.1 项目背景阐述 在数字化浪潮席卷全球的当下,各行业对软件的依赖程度日益加深。[行业名称] 行业也不例外,随着业务规模的不断扩张、业务复杂度的持续提升以及市场竞争的愈发激烈,对高效、智能、定制化软件…...
腾讯云:数字世界的“量子熔炉”与硅基文明引擎
一、算力拓扑学:重新定义空间的计算密度 腾讯云的算力网络正在突破经典物理限制,其分布式架构通过“量子化”资源调度实现超维计算: 虚拟化跃迁:基于KVM的轻量级虚拟化技术,将单台物理服务器切割为百…...
Android 项目中配置了多个 maven 仓库,但依赖还是下载失败,除了使用代理,还有其他方法吗?
文章目录 前言解决方案gradlemaven 仓库 前言 我们在Android 开发的过程中,经常会遇到三方依赖下载不下来的问题。一般情况下我们会在项目的build.gradle文件中配置多个 maven 仓库来解决。 // Top-level build file where you can add configuration options com…...

关税冲击下,FBA国际物流企业如何靠智能拓客跑出增长“加速度”?
国际物流行业正迎来前所未有的增长机遇。据中研普华最新报告,2025年全球物流市场规模已突破6.27万亿美元,其中中国跨境物流市场预计达2.71万亿元。在全球化与数字化双轮驱动下,国际物流从“规模扩张”迈向“价值重构”。可以说,国…...