从 C 语言的 main 到 STM32 的 main:你的代码后来去了哪里
经典三问
Q: STM32 里的 main 是什么?
A: 它是上电后由启动代码交给用户程序的入口函数,之后通常一直运行主循环。
Q: 为什么需要理解 main 的启动过程?
A: 只有知道时钟、全局变量和外设初始化何时发生,才能判断程序到底从哪里开始、哪里出了问题。
Q: STM32 的 main 常见使用场景?
A: 初始化芯片资源、启动任务循环,以及组织 LED、按键、串口和电机等模块的业务流程。
我第一次看到 STM32 的 GPIO 初始化代码时,真正卡住我的不是某个拼写,而是它看起来根本不像我熟悉的 C:
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_InitTypeDef 是什么?为什么要先声明一个它,再往里面塞三个参数?这些参数是谁规定的?如果我把其中一个选错了,STM32 会不会直接完蛋?更让人不安的是,这些东西像是突然从工程里冒出来的,写完之后,代码居然就能控制芯片上的一根引脚。
后来我才发现,问题不在于 C 语言突然换了一套规则。还是变量、结构体、函数和地址,只是这些东西不再对着电脑屏幕工作,而是要绕一圈,落到芯片里的寄存器,再从寄存器落到 PC13 的电平上。
在电脑上,main 可以下班
普通 C 程序大概是这样:
c
int main(void)
{
int score = 100;
printf("score = %d\n", score);
return 0;
}
程序启动,变量有了值,printf 把文字送到终端,return 0 把结果交给操作系统。窗口关掉,进程结束,事情办完了。
STM32 的 main 通常没有这个结尾:
c
int main(void)
{
while (1)
{
}
}
第一次看到这段代码,我也会想:花括号里什么都没有,程序是不是卡住了?它确实一直停在这里,但这次停住不是漏写代码,而是单片机的工作方式。它没有桌面系统替你回收进程,也没有一个终端等着接收返回值。上电之后,它就一直留在现场,等按键、等定时器、等串口字节,或者把某根输出线再翻一次。
后面点亮 LED 时,真正的动作会被放进这个循环里。现在先记住这件事:在 STM32 里,while (1) 不是程序没写完,而是程序准备一直活着。
那些函数和类型从哪里来
main.c 其实没有直接填写 GPIO 参数:
c
#include "stm32f10x.h"
#include "led.h"
int main(void)
{
LED_Init();
while (1)
{
LED_On();
}
}
你看到的 LED_Init() 来自 led.h 的声明,真正的初始化代码在 led.c。再往下追,GPIO_InitTypeDef、GPIO_Mode_Out_PP 和 GPIO_Init() 又来自 STM32 标准外设库的头文件和源文件。
这几层不是魔法,也不是 Keil 临时替你生成的代码:头文件告诉编译器类型和函数存在,标准库源文件实现这些函数,工程文件把它们一起编译、链接进最终程序,芯片启动后执行 main(),函数最终改变的是硬件寄存器里的位。
所以 GPIO_Init() 并不是一个"让 GPIO 自己变好"的咒语。它只是一个已经有人写好的 C 函数,把你填进配置单里的选择翻译成寄存器操作。
先看一张配置单
在 LED 模块的初始化函数里,会看到一张交给标准库的配置单:
c
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_13;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC, &gpio);
现在只需要建立一个整体印象:先让 GPIOC 具备工作的条件,再写下引脚的配置,最后把配置交给 GPIO_Init()。gpio 是一组相关数据,&gpio 是把这组数据的地址交给函数。
至于每个成员具体表示什么、结构体为什么这样传递、时钟和 GPIO 初始化分别做了什么,后续文章会按顺序拆开。这里不要求先背下这些名字,也不要求现在就修改参数。
"选错了会不会完蛋"
我一开始也有这个担心。看到一串陌生的参数时,很容易把它们想成芯片的生死开关,好像 GPIO_Speed_2MHz 写错成 50MHz,整个工程就会烧掉。
实际情况没有这么吓人,但也不能什么都随便试:
- 把
GPIO_Pin_13改成GPIO_Pin_12,通常是代码和接线对不上,灯不亮; - 把
GPIO_ResetBits改成GPIO_SetBits,常见 Blue Pill 上通常只是亮灭关系反过来; - 忘记打开 GPIOC 时钟,外设可能不响应;
- 把一个外部设备的引脚配置成错误的输出模式,或者让两个输出互相硬顶,才可能带来真正的电气风险。
所以实验不能只靠"改一个参数看看"。先确认板子的原理图和接线,再一次只改一个配置;LED、串口或万用表会告诉你变化发生在哪里。参数不是神秘的禁区,但每个参数都应该有它对应的硬件对象。
最后橘猫说
你写下的仍然是变量:
c
uint32_t count = 0;
count++;
仍然是函数:
c
LED_On();
只是 LED_On() 最终不再负责打印一行文字,而是让 PC13 输出一个电平;while (1) 也不再等待一个程序退出,而是让芯片一直留在自己的工作循环里。
代码没有突然获得控制 STM32 的能力。编译器、标准库、启动文件、链接器和芯片寄存器一起,把普通 C 代码送到了硬件上。你现在看到的 GPIO_InitTypeDef,只是这条链路中负责描述配置的一张表。
先把这张表的样子记住:一组相关参数放在一起,交给一个接收地址的函数。结构体为什么这样组织、哪一项漏填会在板子上露出马脚,顺着这张表继续看,都会一一对上。