2. 裸机开发基础
大家好,我是你们的老朋友。今天咱们聊聊裸机编程的根基——单片机怎么从复位跑到你的 main(),以及那个看似简单却极其重要的超级循环架构。
很多初学者上来就写 main(),觉得单片机一上电就直奔那里。其实不然。中间有一段“看不见的旅程”,搞不懂它,你后面做任务调度时就会踩坑。我当年刚入行时,就因为不清楚启动流程,在 bootloader 里折腾了整整三天。
2.1 单片机启动流程:从复位到main()
单片机复位后,CPU 会从 复位向量表 取出第一条指令的地址,然后跳转执行。这个过程大致分三步:
- 硬件初始化:CPU 内部寄存器复位到默认值,PC 指针指向复位向量。
- 启动代码执行:由芯片厂商提供的汇编代码完成堆栈指针设置、中断向量表拷贝、全局变量初始化(BSS 段清零、数据段加载)。
- 跳转到 main():启动代码最后调用
main(),从此进入 C 语言世界。
核心要点:启动代码是芯片厂商写好的,你一般不需要改。但你要知道它干了什么——尤其是全局变量初始化这一步。如果你的项目有大量全局变量,启动时间会变长,这在低功耗唤醒场景下是个大问题。
我建议你打开编译器的 map 文件,看看 .text、.data、.bss 各段的大小。有一次我在一个 STM32 项目里发现 .bss 段占了 8KB,启动时间接近 2ms,这在电池供电的设备里完全不可接受。后来改成局部变量加动态分配,才把启动时间压到 200μs 以内。
2.2 main()函数:C世界的入口
main() 是 C 语言的标准入口,但在嵌入式世界里,它和 PC 上的 main() 有本质区别:
- PC 上的
main()会返回给操作系统。 - 嵌入式里的
main()永远不能返回——返回了,程序就飞了。
所以,嵌入式 main() 的典型结构是:
int main(void)
{
// 1. 硬件初始化
SystemClock_Config();
GPIO_Init();
UART_Init();
Timer_Init();
// 2. 应用初始化
App_Init();
// 3. 超级循环
while (1)
{
// 任务轮询
Task_Process();
}
}
你看,while(1) 是必须的。没有它,程序跑完初始化就不知道去哪了。我见过新手写的代码,main() 里没有死循环,结果程序跑飞后反复复位,整个系统像抽风一样。
注意:main() 里的初始化顺序很重要。比如你先初始化了外设,再初始化时钟,外设可能因为时钟没配好而工作异常。我习惯的规则是:时钟 → 中断控制器 → GPIO → 外设模块 → 应用层。
2.3 超级循环架构:裸机编程的基石
超级循环(Super Loop),说白了就是 while(1) 里不断轮询各个任务。它是裸机编程最基础、最直观的架构。
它的核心逻辑是:
while (1)
{
Task_ReadSensor(); // 读传感器
Task_ProcessData(); // 处理数据
Task_UpdateDisplay();// 更新显示
Task_CheckButton(); // 检测按键
// ... 更多任务
}
每个任务按顺序执行,执行完一轮再从头开始。简单吧?但简单背后有陷阱。
2.3.1 超级循环的优缺点
| 优点 | 缺点 |
|---|---|
| 代码结构清晰,容易理解 | 实时性差——一个任务卡住,后面全等 |
| 没有任务切换开销 | CPU 利用率低——没事干也在空转 |
| 资源占用少(无 RTOS 内核) | 任务间耦合高——改一个可能影响全局 |
| 调试简单,单步执行直观 | 难以处理突发性事件——轮询周期决定响应速度 |
我在一个智能家居项目里用过超级循环。当时控制 5 个继电器、读 3 个传感器、驱动一个 LCD 屏。任务不多,超级循环完全够用。但后来加了 WiFi 模块,轮询周期从 5ms 涨到 50ms,按键响应明显变慢。这就是超级循环的瓶颈——任务多了,轮询周期就长了。
2.3.2 如何优化超级循环
既然超级循环有缺点,那怎么优化?我分享几个实战经验:
- 任务分优先级:紧急任务(如按键检测)放前面,非紧急任务(如显示刷新)放后面。
- 使用状态机:把长任务拆成多个短状态,每次循环只执行一个状态,避免阻塞。
- 定时触发:用定时器中断设置标志位,主循环只处理有标志的任务,避免空轮询。
小技巧:我常用一个“心跳计数器”来分配任务执行频率。比如按键检测每 5ms 一次,显示刷新每 50ms 一次。这样既保证了实时性,又降低了 CPU 负载。
2.4 超级循环 vs 前后台系统
很多人把超级循环和前后台系统混为一谈。其实它们有区别:
- 超级循环:所有任务都在
while(1)里轮询,没有中断参与。 - 前后台系统:前台是中断服务程序(ISR),处理紧急事件;后台是超级循环,处理常规任务。
前后台系统是超级循环的升级版。它用中断解决了实时性问题,但引入了新的挑战——中断和主循环的共享数据保护。这个我们后面章节会详细讲。
2.5 知识体系总览
下面这张图帮你理清本章的核心逻辑:
2.6 避坑指南
最后,分享几个我踩过的坑:
- 全局变量未初始化:启动代码默认把 BSS 段清零,但如果你在
main()之前就用了全局变量(比如在构造函数里),值可能是随机的。我曾经因为这个原因,一个电机控制项目在启动瞬间会乱转一下。 - 中断优先级配置:超级循环里如果开了中断,一定要配好优先级。否则中断嵌套可能导致堆栈溢出。我建议裸机项目至少保留 128 字节的堆栈余量。
- 看门狗喂狗位置:如果你用了看门狗,喂狗要放在超级循环末尾,而不是每个任务里。否则某个任务卡死,看门狗也被“骗”过去了。
曾经有一次,我在一个工业控制项目里,把喂狗放在了第一个任务里。结果第二个任务因为传感器故障卡死了,看门狗却一直没复位。设备就这么“死”在现场,直到用户断电重启。从那以后,我坚持把喂狗放在循环末尾,并且加一个超时计数器做双重保护。