重新认识结构体:为什么 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 是加速度,还是电机编号;最后两个 12 交换位置,编译器也不会提醒你,因为它们的类型都是 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_PinGPIO_ModeGPIO_Speed,再去修改 GPIOC 的配置寄存器。

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

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

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

c 复制代码
gpio.GPIO_Pin = GPIO_Pin_13;

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

c 复制代码
GPIO_InitTypeDef *config;

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

config->GPIO_Pingpio.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_TimeBaseInitTypeDefUSART_InitTypeDefI2C_InitTypeDef。名字换了,动作没有换:声明一个配置对象,填写成员,把它的地址交给初始化函数。

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

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

相关推荐
星源~2 小时前
Linux - Trae Code Linux 环境嵌入式开发指南
linux·mcu·嵌入式·trae code
Qt程序员5 小时前
做嵌入式必备|12 个 RTOS 核心机制,建议收藏
嵌入式·嵌入式开发·rtos
喵喵锤锤你小可爱7 小时前
SerialHub:把串口变成 WebSocket 字节管道,浏览器和脚本直接读写(开源工具 SerialHub 实战)
websocket·rust·嵌入式·串口调试·开源工具
susplus1 天前
【51单片机通信协议】串口
51单片机·串口·嵌入式·通信协议
嵌入式阿蔡3 天前
嵌入式工程师2026:你该会什么,以及会到什么程度
嵌入式
jianqiang.xue3 天前
ESP-IDF保姆级入门38|电源管理与锂电池充电全解:充电拓扑/电源路径控制/充放电保护/电量检测/低功耗策略,掌握嵌入式电池供电系统设计
单片机·嵌入式·esp32
阿钱真强道4 天前
01 嵌入式操作系统 | 课程导论与虚拟机安装 Ubuntu
linux·ubuntu·嵌入式·虚拟机
不悔哥4 天前
开源 OOMWOO:自己组一台扫地机
开源·嵌入式
异彩OneColor4 天前
地瓜RDK S100平台Ofilm ISX031相机内参读取方案
嵌入式