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

基于按键开源MultiButton框架深入理解代码框架(一)(指针的深入理解与应用)

文章目录

  • 1、函数指针应用
    • 1.1 传递函数指针的语法
    • 1.2 按键函数的初始化
    • 1.3 按键相关变量
      • 1.3.1 按键属性结构体
      • 1.3.2 按键结构体参数意义
    • 1.4 初始化引起的思考


1、函数指针应用

前面讲解了函数指针,这里就是指针的实际应用。

深入理解C语言内存空间、函数指针(三)(重点是函数指针)-CSDN博客

void button_init(Button* handle, uint8_t(*pin_level)(uint8_t), 
uint8_t active_level, uint8_t button_id) 这是声明的函数, 
uint8_t(*pin_level)(uint8_t)表示函数指针变量 uint8_t read_button_gpio(uint8_t button_id) 
{ switch (button_id) { case 1: return btn1_state; case 2: return btn2_state; default: return 0; } 
} 但是在使用传递参数过程中,直接就传递了函数名 button_init(&btn1, read_button_gpio, 1, 1);

C语言规定,函数名本身是一个指向该函数代码的指针常量。

uint8_t (*func_ptr)(uint8_t) = read_button_gpio; // 正确,函数名隐式转换为地址

或者显式取地址

uint8_t (*func_ptr)(uint8_t) = &read_button_gpio; // 等价写法

两者效果相同。

1.1 传递函数指针的语法

调用时直接传递函数名:

button_init(&btn1, read_button_gpio, 1, 1);
应用场景关键技术点代表函数/库
回调函数事件驱动、异步通知GUI 事件处理、Node.js
策略模式算法可替换C 标准库 qsort()
分派表状态机、命令解析计算器、协议处理器
线程池任务并发任务抽象线程池框架(如 Pthread)
插件架构动态加载、热更新动态链接库(dlopen

1.2 按键函数的初始化


typedef struct _Button Button;struct _Button {uint16_t ticks;                     // tick counteruint8_t  repeat : 4;                // repeat counter (0-15)uint8_t  event : 4;                 // current event (0-15)uint8_t  state : 3;                 // state machine state (0-7)uint8_t  debounce_cnt : 3;          // debounce counter (0-7)uint8_t  active_level : 1;          // active GPIO level (0 or 1)uint8_t  button_level : 1;          // current button leveluint8_t  button_id;                 // button identifieruint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIOBtnCallback cb[BTN_EVENT_COUNT];    // callback function arrayButton* next;                       // next button in linked list};

这个地方可以给自己规定死,也就是只要出现传递的参数是指针的,那么最开始一定要做的一件事就判断地址的合法性,也就是不能为NULL。
如果是NULL,就直接退出初始化

    if (!handle || !pin_level) return;  // parameter validation
- **`ptr`**​:目标内存起始地址(此处 `handle` 需是 `Button*` 类型)。
-**`value`**​:填充值(仅低 8 位有效,故 `0` 或 `0xFF` 等单字节值安全)。
-**`num`**​:填充字节数(`sizeof(Button)` 确保覆盖整个结构体)。void* memset(void* ptr, int value, size_t num);memset(handle, 0, sizeof(Button));

清零内存区域

  • memset 按字节填充内存,此处 0 表示将 sizeof(Button) 字节的内存全部设为 0
  • 对于结构体 Button,其所有成员(包括整型、字符数组、指针等)的二进制值均被置零:
    • 数值类型(如 int)变为 0
    • 字符数组/字符串变为空字符串(\0 填充)。
    • 指针变为 NULL(空指针)。

初始化思想:

    memset(handle, 0, sizeof(Button));handle->event = (uint8_t)BTN_NONE_PRESS;handle->hal_button_level = pin_level;handle->button_level = !active_level;  // initialize to opposite of active levelhandle->active_level = active_level;handle->button_id = button_id;handle->state = BTN_STATE_IDLE;

我们第一步是使用memset函数将sizeof(Button) 字节的内存全部设为 0

但是有一部分还是需要一些默认值,所以后面的就是通过结构体指针特有的方式进行访问相关成员函数。这是结构体指针变量特有的方式。

1.3 按键相关变量

1.3.1 按键属性结构体


typedef struct _Button Button;struct _Button {uint16_t ticks;                     // tick counteruint8_t  repeat : 4;                // repeat counter (0-15)uint8_t  event : 4;                 // current event (0-15)uint8_t  state : 3;                 // state machine state (0-7)uint8_t  debounce_cnt : 3;          // debounce counter (0-7)uint8_t  active_level : 1;          // active GPIO level (0 or 1)uint8_t  button_level : 1;          // current button leveluint8_t  button_id;                 // button identifieruint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIOBtnCallback cb[BTN_EVENT_COUNT];    // callback function arrayButton* next;                       // next button in linked list};
static Button btn1, btn2;

需要说明的是:

    uint8_t  repeat : 4;                // repeat counter (0-15)uint8_t  event : 4;   

这里为什么我们使用后面的4?
这是因为这是一种 ​位段(Bit Field)​​ 的声明方式,其核心作用是精确控制成员变量占用的内存位数,以节省空间并高效表示小范围整数值。

  • uint8_t:基础类型(无符号8位整数),表示该位段基于1字节(8位)内存单元分配
  • repeat:成员变量名。
  • : 4:指定该成员占用 ​4个比特位(bit)​,而非完整的1字节(8位)。
  • 取值范围​:4位二进制数的范围是 0~15(24=16 种可能值),适合存储小范围整数(如计数器、状态标志)。
  • 节省内存​:若 repeat 只需表示0-15的值,使用完整uint8_t(8位)会浪费4位空间,位段将其压缩至4位。
  • 紧凑存储​:在嵌入式系统、网络协议等内存敏感场景中,位段能显著减少结构体总大小(如用户结构体中的repeateventstate等均为位段)。

1.3.2 按键结构体参数意义

变量名数据类型/位宽含义说明功能作用
​**ticks**​uint16_t时间计数器记录按键状态持续的毫秒数,用于计算单击、长按等事件的时间阈值。
​**repeat**​uint8_t : 4连击次数计数器记录连续快速按下的次数(如双击、三击),取值范围 0~15。
​**event**​uint8_t : 4当前事件标识存储按键触发的事件类型(如按下、松开、单击等),用枚举值表示。
​**state**​uint8_t : 3状态机当前状态标识按键在状态机中的位置(共 8 种状态),驱动内部逻辑流转用。
​**debounce_cnt**​uint8_t : 3消抖计数器记录按键电平稳定的持续周期数,用于消除机械抖动干扰(通常 5ms 周期)。
​**active_level**​uint8_t : 1有效触发电平定义按键按下时的有效电平(0=低电平有效,1=高电平有效)。
​**button_level**​uint8_t : 1当前实际电平存储通过 hal_button_level 读取的当前 GPIO 电平值。
​**button_id**​uint8_t按键标识符区分多个按键的唯一 ID,用于共享电平读取函数时识别不同按键。
​**hal_button_level**​函数指针硬件抽象层电平读取函数指向用户实现的 GPIO 读取函数,参数为 button_id,返回当前电平。
​**cb**​BtnCallback[]回调函数数组存储不同事件(如按下、长按)对应的回调函数指针,事件触发时调用。
​**next**​Button*链表指针指向下一个按键对象,支持无限扩展按键,形成全局链表统一处理。
    uint8_t  button_id;                 // button identifieruint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIOBtnCallback cb[BTN_EVENT_COUNT];    // callback function arrayButton* next;                       // next button in linked list

ticks 表示的按下按键的持续时长,用于计算单击、长按等事件的时间阈值。这个数据是核心,只有根据这个数据才能判断是长按、短按。并且一般单位都是ms,在初始化的时候默认是0。

repeat 表示的是单击、双击、三击等 如果是两次就表示是双击。注意我们在设计的时候没有把这个数设计的那么大,这是因为我们的连击是有常用的可能的,不可能说能连击255次,因此只需要设计合理的范围就行,这样还能节约空间。

event 表示的按键事件标志或者说是代表是按下、还是松开、还是怎么其他的可能。这是一个枚举变量,在使用枚举变量的时候其实是有一些细节的。

参考枚举文章C语言关键字—枚举

typedef enum {BTN_PRESS_DOWN = 0,     // 按键按下BTN_PRESS_UP,           // 按键抬起BTN_PRESS_REPEAT,       // 重复按下检测BTN_SINGLE_CLICK,       // 单击完成BTN_DOUBLE_CLICK,       // 双击完成BTN_LONG_PRESS_START,   // 长按开始BTN_LONG_PRESS_HOLD,    // 长按保持BTN_NONE_PRESS          // 无事件} ButtonEvent;

state 表示​状态机当前状态​ ,标识按键在状态机中的位置(共 8 种状态),驱动内部逻辑流转用。

typedef enum {BTN_STATE_IDLE = 0,     // idle state  空闲状态BTN_STATE_PRESS,        // pressed state  按下状态BTN_STATE_RELEASE,      // released state waiting for timeout  释放状态BTN_STATE_REPEAT,       // repeat press state  重复按下状态BTN_STATE_LONG_HOLD     // long press hold state  长按保持状态} ButtonState;

debounce_cnt 消抖计数器,这个时间和时间计数器是不是可以产生关联。

``
active_level 有效电平的触发,也就说在按键检测时候,有可能是高电平检测有效,也有可能是低电平检测有效。

button_level 实际有效的电平,也就是通过GPIO口检测到的电平,

    handle->button_level = !active_level;  // initialize to opposite of active levelhandle->active_level = active_level;

这个地方也是一个编程技巧,也就是我们初始化的实际电平一定是有效电平的相反数,不然可能会出现还没有按键,就导致按键检测的是有效的,避免系统出现紊乱。
开发的严谨性。

hal_button_level GPIO检测函数

输入参数:uint8_t(*pin_level)(uint8_t)可以看出我们传进去的是一个函数指针,相当于是这个函数的入口地址。handle->hal_button_level = pin_level;

button_id 按键的ID,因为一个项目可能出现多个按键。

cb 回调函数数组。

next 按键链表指针,

void button_init(Button* handle, uint8_t(*pin_level)(uint8_t), uint8_t active_level, uint8_t button_id)

综上所述,在初始化一个按键的时候,我们需要关注的传入参数:
1、按键的属性集合,也就是按键的结构体
2、按键的检测函数,检测GPIO电平的。
3、按键按下是高电平有效还是低电平有效
4、按键的ID,表示我正在初始的是那个按键

1.4 初始化引起的思考

static Button btn1, btn2;

首先是声明按键结构体:程序启动时分配,地址固定,​保证地址有效性&btn1永不为 NULL,并且​自动零初始化(成员为 0NULL)。

使用static关键字的目的是:
static 修饰的变量(无论全局或局部)存储在静态数据区​(全局/静态存储区),其内存在程序启动时已分配。

在这里不得不引申一下:

我们知道RAM里面有栈空间、堆空间、bss、data段。

  • 栈空间(Stack)​​:存储函数调用的局部变量、参数、返回地址等,由系统自动管理,从高地址向下生长。

  • 堆空间(Heap)​​:用于动态内存分配(如 malloc),由程序员手动管理,从低地址向上生长。

  • ​.bss 段​:存储未初始化的全局变量和静态变量,程序启动时由系统自动清零。

  • ​.data 段​:存储已初始化的全局变量和静态变量,程序启动时从 Flash 复制初始值到 RAM。

但是需要声明的是在裸机开发中一般不使用堆空间,

并且函数的执行都是在==栈空间==,那说到这里还记不记得有一个栈顶空间,对的,这个栈顶空间就是给一个上限,因此栈空间的特殊性,是从上到下的,也就是高字节到低字节分配,

  • 栈是一种线性数据结构,仅允许在栈顶(Top)​进行插入(入栈)和删除(出栈)操作。类似一摞盘子,最后放上的盘子最先被取走。

这是因为在_main函数到mainARM内核还有一段代码需要执行,因此留出来的是这一段空间,然后才是我们自己写的main函数栈顶地址,就这后面的栈顶空间就可以循环利用了。

我们首先需要知道栈顶地址是怎么得到的?

这是整个RAM的空间:

![[Pasted image 20250711161229.png]]

栈是RAM顶部的最后一个区域,符合典型设计。

    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object0x20000000   COMPRESSED   0x00000024   Data   RW           39    .data               main.o0x20000024   COMPRESSED   0x00000040   Data   RW          110    .data               modbus_app.o0x20000064   COMPRESSED   0x000000b5   Data   RW          183    .data               mb.o0x20000119   COMPRESSED   0x00000003   PAD0x2000011c   COMPRESSED   0x0000000c   Data   RW          267    .data               mbrtu.o0x20000128   COMPRESSED   0x00000008   Data   RW          372    .data               modbus_slave.o0x20000130   COMPRESSED   0x00000024   Data   RW          581    .data               key_drv.o0x20000154   COMPRESSED   0x00000024   Data   RW          618    .data               led_drv.o0x20000178   COMPRESSED   0x00000008   Data   RW          668    .data               ntc_drv.o0x20000180   COMPRESSED   0x00000006   Data   RW          810    .data               rh_drv.o0x20000186   COMPRESSED   0x00000002   PAD0x20000188   COMPRESSED   0x0000000c   Data   RW          964    .data               systick.o0x20000194   COMPRESSED   0x0000001c   Data   RW         1010    .data               usb2com_drv.o0x200001b0   COMPRESSED   0x00000002   Data   RW         1133    .data               portevent.o0x200001b2   COMPRESSED   0x00000002   PAD0x200001b4   COMPRESSED   0x00000018   Data   RW         1168    .data               portserial.o0x200001cc   COMPRESSED   0x00000004   Data   RW         3445    .data               mc_w.l(stderr.o)0x200001d0   COMPRESSED   0x00000004   Data   RW         3734    .data               mc_w.l(stdout.o)0x200001d4        -       0x00000100   Zero   RW          265    .bss                mbrtu.o0x200002d4   COMPRESSED   0x00000004   PAD0x200002d8        -       0x00000030   Zero   RW          580    .bss                key_drv.o0x20000308        -       0x00000014   Zero   RW          666    .bss                ntc_drv.o0x2000031c   COMPRESSED   0x00000004   PAD0x20000320        -       0x00000400   Zero   RW         3383    STACK               startup_gd32f30x_hd.o

通过工程的map文件可以看出在栈空间确定之前,首先确定的是data、bss数据占用的RAM空间,最后确定出栈空间的最低地址是多少。通过代码可以看出是0x20000320,大小是0x00000400,其中栈的大小是可以自己设定的。那么两者相加就是0x20000320 + 0x00000400 = 0x20000720

    pxMBFrameCBByteReceived                  0x2000007c   Data           4  mb.o(.data)pxMBFrameCBTransmitterEmpty              0x20000080   Data           4  mb.o(.data)pxMBPortCBTimerExpired                   0x20000084   Data           4  mb.o(.data)pxMBFrameCBReceiveFSMCur                 0x20000088   Data           4  mb.o(.data)pxMBFrameCBTransmitFSMCur                0x2000008c   Data           4  mb.o(.data)__stderr                                 0x200001cc   Data           4  stderr.o(.data)__stdout                                 0x200001d0   Data           4  stdout.o(.data)ucRTUBuf                                 0x200001d4   Data         256  mbrtu.o(.bss)__initial_sp                             0x20000720   Data           0  startup_gd32f30x_hd.o(STACK)

从最后一行代码也可以看出该工程的栈顶地址是0x20000720
![[Pasted image 20250711150317.png]]

bss和data不会释放的,会一直占用。

即使 static 变量地址有效,若函数通过参数接收外部指针(如 button_init(&btn1, ...)),仍需检查该参数是否为空:

因此初始化的时候首先要进行检测的就是判断地址的合法性。

void button_init(Button* handle, ...) {if (!handle) return;  // 必须检查,避免外部误传 NULL
}

文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。


文章转载自:
http://bespatter.zzyjnl.cn
http://brian.zzyjnl.cn
http://alice.zzyjnl.cn
http://candidature.zzyjnl.cn
http://calfskin.zzyjnl.cn
http://bailer.zzyjnl.cn
http://antihero.zzyjnl.cn
http://chorography.zzyjnl.cn
http://chichi.zzyjnl.cn
http://atomistics.zzyjnl.cn
http://blackness.zzyjnl.cn
http://ccpit.zzyjnl.cn
http://caracul.zzyjnl.cn
http://attar.zzyjnl.cn
http://bulletin.zzyjnl.cn
http://balliol.zzyjnl.cn
http://appendicectomy.zzyjnl.cn
http://budgie.zzyjnl.cn
http://brainworker.zzyjnl.cn
http://acta.zzyjnl.cn
http://acousma.zzyjnl.cn
http://braaivleis.zzyjnl.cn
http://bicolour.zzyjnl.cn
http://bessy.zzyjnl.cn
http://abbreviatory.zzyjnl.cn
http://barefooted.zzyjnl.cn
http://adduceable.zzyjnl.cn
http://airline.zzyjnl.cn
http://brooch.zzyjnl.cn
http://cem.zzyjnl.cn
http://www.dtcms.com/a/280031.html

相关文章:

  • 开源AI Agent开发平台Dify源码剖析系列(二)
  • HTTP 协议
  • 微信小程序进度条cavans
  • 【电脑】显卡(GPU)的基础知识
  • Golang Channel与协程的完美配合指南
  • CAU数据挖掘 第五章 聚类问题
  • vscode里面怎么配置ssh步骤
  • Python+Selenium自动化爬取携程动态加载游记
  • python实现自动化sql布尔盲注(二分查找)
  • js最简单的解密分析
  • 分支战略论:Git版本森林中的生存法则
  • document.documentElement详解
  • Webshell连接工具原理
  • 渗透笔记1-4
  • html js express 连接数据库mysql
  • 【算法训练营Day12】二叉树part2
  • 进程---基础知识+命令+函数(fork+getpid+exit+wait+exec)
  • 100道K8S面试题
  • LVS初步学习
  • google浏览器::-webkit-scrollbar-thumb设置容器滚动条滑块不生效
  • langflow搭建带记忆功能的机器人
  • 【React Native】环境变量和封装 fetch
  • Knife4j快速入门
  • 【深度学习:进阶篇】--4.4.集束搜索(Beam Search)
  • 深入探索ZYNQ网络通信:四大实现方案与创新应用
  • VMWare 使用 U 盘 PE 系统安装 Win 11 ESD 镜像
  • 日常--PyCharm清除attach记录
  • Linux进程优先级机制深度解析:从Nice值到实时调度
  • 详解从零开始实现循环神经网络(RNN)
  • 实现高效、可靠的基于骨骼的人体姿态建模(第二章 基于三维人体姿态回归的语义图卷积网络)