重新认识结构体:为什么 STM32 库函数喜欢接收一大包参数

经典三问

Q: C 语言结构体是什么?

A: 它是把多个有名称的成员组合成一个数据对象的类型,可以同时保存一组相关配置。

Q: 为什么库函数需要接收结构体?

A: 外设配置往往有多个彼此相关的字段,结构体能让调用接口保持稳定,也让参数含义更清楚。

Q: 结构体常见使用场景?

A: GPIO、定时器、串口等外设初始化,以及保存传感器数据、菜单项和控制器参数。


上一篇里,我把 GPIO_InitTypeDef 留在了这里:

c 复制代码
GPIO_InitTypeDef gpio;

gpio.GPIO_Pin = GPIO_Pin_13;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;

GPIO_Init(GPIOC, &gpio);

我第一次看到这几行,先冒出来的是一串问题:为什么会有这么多参数?每个参数到底管什么?它们应该放在哪里?为什么最后还要把 &gpio 交给另一个函数?

这次先不急着背 GPIO_InitTypeDef。我们退回普通 C,看看结构体到底在解决什么麻烦。

参数开始变多以后

假设要写一个函数,给一辆车设置速度、方向和刹车状态:

c 复制代码
void Car_Setup(uint8_t speed,
               uint8_t direction,
               uint8_t brake);

参数少时还能看懂。调用时,三个数字分别对应速度、方向和刹车:

c 复制代码
Car_Setup(100, 1, 0);

但如果后来又加上加速度限制、电机编号、是否允许反转,函数可能变成这样:

c 复制代码
void Car_Setup(uint8_t speed,
               uint8_t direction,
               uint8_t brake,
               uint8_t acceleration,
               uint8_t motor_id,
               uint8_t allow_reverse);

调用就会变成一串不容易读懂的数字:

c 复制代码
Car_Setup(100, 1, 0, 50, 2, 1);

只看这一行,很难立刻确认 50 是加速度,还是电机编号;最后两个 1、2 交换位置,编译器也不会提醒你,因为它们的类型都是 uint8_t。参数越多,函数调用就越像一张没有列名的表。

可以把属于"车辆配置"的东西放在一起:

c 复制代码
typedef struct
{
    uint8_t speed;
    uint8_t direction;
    uint8_t brake;
} CarConfig;

使用时先得到一个配置对象:

c 复制代码
CarConfig config;

config.speed = 40;
config.direction = 1;
config.brake = 0;

再把它交给处理配置的函数:

c 复制代码
Car_ApplyConfig(&config);

这里的结构体没有变成什么高级对象。它就是几块挨在一起的数据,只是这些数据现在有了一个共同名字:CarConfig。函数看到这张配置单,知道速度、方向和刹车属于同一件事。

typedef struct 到底写了什么

c 复制代码
typedef struct
{
    uint8_t speed;
    uint8_t direction;
    uint8_t brake;
} CarConfig;

struct { ... } 描述了一种结构,花括号里是它的成员;最后的 CarConfig 给这种结构起了一个更好用的类型名;typedef 让以后声明变量时可以直接写:

c 复制代码
CarConfig config;

不使用 typedef 时,声明变量会更长:

c 复制代码
struct CarConfigTag
{
    uint8_t speed;
    uint8_t direction;
    uint8_t brake;
};

struct CarConfigTag config;

STM32 标准库大量使用 typedef struct,只是因为外设配置成员很多,这样写起来短一些。看到一串很长的类型名时,先把它当成"某类配置对象",不必先把名字背下来。

回到 GPIO_InitTypeDef

c 复制代码
GPIO_InitTypeDef gpio;

gpio.GPIO_Pin = GPIO_Pin_13;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;

这三行不是随便挑了三个宏。它们分别回答三个问题:要安排哪根引脚?这根引脚作为输入、输出还是复用功能?输出时需要多快的边沿?

把 GPIO_Pin_13 换成 GPIO_Pin_12,你换的是哪根引脚;把 GPIO_Mode_Out_PP 换成输入模式,你是在换引脚的工作方式;把 GPIO_Speed_2MHz 换成 GPIO_Speed_50MHz,你只是在改变输出边沿能力。每一项都应该能在芯片手册或开发板接线中找到对应的东西。

为什么函数要拿到 &gpio

c 复制代码
GPIO_Init(GPIOC, &gpio);

gpio 是一个结构体变量,里面放着刚刚填写好的配置。&gpio 取出这个变量的地址,传给 GPIO_Init()。函数拿着这个地址,就能读取 GPIO_Pin、GPIO_Mode 和 GPIO_Speed,再去修改 GPIOC 的配置寄存器。

拿地址并不意味着函数获得了什么神秘能力。函数仍然只是在读一段内存;只是它读完之后,调用了标准库里对应的寄存器操作,于是 PC13 的工作方式发生了变化。

点号和箭头其实在访问同一组成员

手里是结构体变量时,用点号:

c 复制代码
gpio.GPIO_Pin = GPIO_Pin_13;

手里是指向结构体的指针时,用箭头:

c 复制代码
GPIO_InitTypeDef *config;

config = &gpio;
config->GPIO_Pin = GPIO_Pin_13;

config->GPIO_Pin 和 gpio.GPIO_Pin 访问的是同一个成员。箭头不是 STM32 专用语法,只是"先沿着指针找到结构体,再访问它的成员"的缩写写法。

配置单要不要先恢复默认值

GPIO 的配置成员不算多,逐项填写通常已经够清楚。定时器、串口和 I2C 的配置更长,漏掉一个成员时,问题可能要到下载后才露出来。标准库常提供恢复默认配置的函数:

c 复制代码
GPIO_InitTypeDef gpio;

GPIO_StructInit(&gpio);
gpio.GPIO_Pin = GPIO_Pin_13;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;

GPIO_Init(GPIOC, &gpio);

我一般先把这张配置单擦干净,再填这次真正需要的内容。不是每个函数都必须先调用 StructInit,但当结构体成员很多、代码还在调整时,我更愿意先恢复默认值,少给旧数据留下机会。

到了定时器、串口和 I2C,你还会看到 TIM_TimeBaseInitTypeDef、USART_InitTypeDef 和 I2C_InitTypeDef。名字换了,动作没有换:声明一个配置对象,填写成员,把它的地址交给初始化函数。

结构体能帮助你把"这一组参数属于同一件事"写得清楚。真正需要花时间的,是弄明白每个成员改变以后,哪一根线、哪一个计数器或哪一段通信会跟着变化。

这些类型和函数就存在于右键快捷添加的那个标准库中。顺着调用链往里走,会看见标准外设库正是把配置对象翻译成 GPIO、定时器和串口寄存器操作的那一层。后面我将先深入标准库带大家看看里面是什么牛鬼蛇神,不必句句理解,只需要留下大概印象就行。

相关推荐
Zwawa17 小时前
一只按键不只“按下”:短按、长按和连按如何共存
c语言·单片机·嵌入式
Zevalin爱灰灰1 天前
Linux入门第一部分(Ubuntu入门篇) 第三章 shell脚本入门(一)
linux·ubuntu·嵌入式
2035去旅行1 天前
U-Boot DM(Driver Model)驱动框架
嵌入式·arm·soc
硅基造物主1 天前
STM32 从 0 到 1 做智能家居(一):先想清楚要做什么
stm32·mqtt·嵌入式·智能家居·hal库·esp-12f
凉、介1 天前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
凉、介1 天前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm
LCG米1 天前
STM32 DMA 多通道链式传输实测:请求仲裁顺序错位根因、半满中断乒乓与高速并发搬运落地
stm32·单片机·嵌入式·1024程序员节
Sakura_j_h_2 天前
STM32F103 定时器的三种用法:PWM 输出、输入捕获与编码器接口
stm32·嵌入式·输入捕获·定时器·pwm·编码器·1024程序员节
pipixia200332 天前
C++ 面试高频:namespace 的 6 个知识点,从 using 到匿名命名空间
c语言·学习·算法·嵌入式·命名空间·c++面试·匿名命名空间
Zevalin爱灰灰2 天前
Linux入门第一部分(Ubuntu入门篇) 第二章 浅尝Ubuntu(二)
linux·嵌入式·1024程序员节