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

文章目录

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

1、函数指针应用

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

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

c 复制代码
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语言规定,函数名本身是一个指向该函数代码的指针常量。

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

或者显式取地址

c 复制代码
uint8_t (*func_ptr)(uint8_t) = &read_button_gpio; // 等价写法

两者效果相同。

1.1 传递函数指针的语法

调用时直接传递函数名:

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

1.2 按键函数的初始化

c 复制代码
typedef struct _Button Button;

struct _Button {

    uint16_t ticks;                     // tick counter

    uint8_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 level

    uint8_t  button_id;                 // button identifier

    uint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIO

    BtnCallback cb[BTN_EVENT_COUNT];    // callback function array

    Button* next;                       // next button in linked list

};

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

如果是NULL,就直接退出初始化

c 复制代码
    if (!handle || !pin_level) return;  // parameter validation
c 复制代码
- **`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(空指针)。

初始化思想:

c 复制代码
    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 level

    handle->active_level = active_level;

    handle->button_id = button_id;

    handle->state = BTN_STATE_IDLE;

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

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

1.3 按键相关变量

1.3.1 按键属性结构体

c 复制代码
typedef struct _Button Button;

struct _Button {

    uint16_t ticks;                     // tick counter

    uint8_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 level

    uint8_t  button_id;                 // button identifier

    uint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIO

    BtnCallback cb[BTN_EVENT_COUNT];    // callback function array

    Button* next;                       // next button in linked list

};
c 复制代码
static Button btn1, btn2;

需要说明的是:

c 复制代码
    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* 链表指针 指向下一个按键对象,支持无限扩展按键,形成全局链表统一处理。
c 复制代码
    uint8_t  button_id;                 // button identifier

    uint8_t  (*hal_button_level)(uint8_t button_id);  // HAL function to read GPIO

    BtnCallback cb[BTN_EVENT_COUNT];    // callback function array

    Button* next;                       // next button in linked list

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

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

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

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

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 种状态),驱动内部逻辑流转用。

c 复制代码
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口检测到的电平,

c 复制代码
    handle->button_level = !active_level;  // initialize to opposite of active level

    handle->active_level = active_level;

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

hal_button_level GPIO检测函数

c 复制代码
输入参数:uint8_t(*pin_level)(uint8_t)

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

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

cb 回调函数数组。

next 按键链表指针,

c 复制代码
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 初始化引起的思考

c 复制代码
static Button btn1, btn2;

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

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

在这里不得不引申一下:

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

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

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

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

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

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

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

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

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

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

这是整个RAM的空间:

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

c 复制代码
    Exec Addr    Load Addr    Size         Type   Attr      Idx    E Section Name        Object

    0x20000000   COMPRESSED   0x00000024   Data   RW           39    .data               main.o
    0x20000024   COMPRESSED   0x00000040   Data   RW          110    .data               modbus_app.o
    0x20000064   COMPRESSED   0x000000b5   Data   RW          183    .data               mb.o
    0x20000119   COMPRESSED   0x00000003   PAD
    0x2000011c   COMPRESSED   0x0000000c   Data   RW          267    .data               mbrtu.o
    0x20000128   COMPRESSED   0x00000008   Data   RW          372    .data               modbus_slave.o
    0x20000130   COMPRESSED   0x00000024   Data   RW          581    .data               key_drv.o
    0x20000154   COMPRESSED   0x00000024   Data   RW          618    .data               led_drv.o
    0x20000178   COMPRESSED   0x00000008   Data   RW          668    .data               ntc_drv.o
    0x20000180   COMPRESSED   0x00000006   Data   RW          810    .data               rh_drv.o
    0x20000186   COMPRESSED   0x00000002   PAD
    0x20000188   COMPRESSED   0x0000000c   Data   RW          964    .data               systick.o
    0x20000194   COMPRESSED   0x0000001c   Data   RW         1010    .data               usb2com_drv.o
    0x200001b0   COMPRESSED   0x00000002   Data   RW         1133    .data               portevent.o
    0x200001b2   COMPRESSED   0x00000002   PAD
    0x200001b4   COMPRESSED   0x00000018   Data   RW         1168    .data               portserial.o
    0x200001cc   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.o
    0x200002d4   COMPRESSED   0x00000004   PAD
    0x200002d8        -       0x00000030   Zero   RW          580    .bss                key_drv.o
    0x20000308        -       0x00000014   Zero   RW          666    .bss                ntc_drv.o
    0x2000031c   COMPRESSED   0x00000004   PAD
    0x20000320        -       0x00000400   Zero   RW         3383    STACK               startup_gd32f30x_hd.o

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

c 复制代码
    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

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

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

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

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

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

【版权声明】

本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:

署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。

相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

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

相关推荐
不想学习\??!5 小时前
STM32-DMA
stm32·单片机·嵌入式硬件
遇见尚硅谷5 小时前
C语言:20250712笔记
c语言·开发语言·数据结构
☞下凡☜5 小时前
C语言(20250711)
linux·c语言·开发语言
机器视觉知识推荐、就业指导6 小时前
51单片机基础知识讲解
嵌入式硬件·mongodb·51单片机
WD137298015577 小时前
WD5018 同步整流降压转换器核心特性与应用,电压12V降5V,2A电流输出
stm32·单片机·嵌入式硬件·51单片机
温暖的苹果7 小时前
【linux V0.11】init/main.c
linux·c语言
秋说7 小时前
【PTA数据结构 | C语言版】阶乘的递归实现
c语言·数据结构·算法
iCxhust8 小时前
一个用于在 Ubuntu 22.04.3 LTS 上显示文件系统超级块信息的 C 程序
linux·c语言·ubuntu