STM32 学习路线

「从入门到精通」的目录网上到处都是,但没人告诉你学到哪一步算过关。我按五个阶段排,每段只给一个标准:做不出来,就还没过,别急着往下走。

PHASE 0

先确认你够不够格开始

大部分「学不会 STM32」的人,问题不在 STM32,在 C 语言。指针、数组退化、结构体布局没搞明白,看寄存器手册就是看天书。

必须掌握:指针与指针的指针、数组与指针的区别、结构体与位域、函数指针与回调、const / static / volatile、位运算与宏。

建议练习:不看资料写一个环形缓冲区(ring buffer),再用函数指针表实现一个状态机。

验收:能解释 *(uint32_t *)(0x40020014) 这一行在做什么,以及为什么这里必须加 volatile。

PHASE 1

裸机外设,一个一个啃

顺序很重要:GPIO → EXTI → TIM → UART → I2C → SPI → ADC → DMA → Flash。每个外设都要做三遍:读时序图、用寄存器写一遍、再看 HAL 怎么封装。

为什么先写寄存器:不是让你以后都手写寄存器,是让你在 HAL 出问题时知道往哪看。只会调库的人,遇到库不工作就只能等别人救。

每个外设的通关三问:它的时钟从哪来?它的中断标志怎么清?它的错误标志分别代表什么?

验收:用寄存器(不用 HAL)写一个 UART 中断收发,能算出波特率寄存器的值并说明误差来源。

PHASE 2

工程化:从「能跑」到「能改」

这一步是分水岭。绝大多数教程停在 Phase 1,所以你看到的所有代码都是「一个 main.c 里三千行」。

分层:bsp(板级:引脚、外设初始化)/ drv(器件驱动:传感器、屏、Flash)/ app(业务逻辑)。规则只有一条:不允许跨层调用。

非阻塞:业务代码里禁止 HAL_Delay()。用状态机 + 时间戳轮询,或者定时器驱动的 tick。

日志:分级(ERROR / WARN / INFO / DEBUG)+ 环形缓冲 + DMA 空闲中断发送,编译期可整段裁掉。

参数存储:Flash 双备份 + CRC 校验 + 版本号。单备份的产品,掉一次电就有一批板子返修。

验收:加一个新传感器,只新增文件、不改动已有模块的任何一行代码。

PHASE 3

接 RTOS:先想清楚为什么

不是所有项目都需要 RTOS,但「需要」的时候往往已经晚了。判断标准:有多个「同时」发生的需求,或者有等待时间不确定的操作(通信、存储、人机交互)。

任务划分三原则:按触发源分而不是按功能分;一个任务只阻塞在一个地方;任务间只通过队列 / 信号量 / 任务通知通信,不共享裸变量。

别做的事:给每个函数开一个任务(几十个任务互相等,调度开销吃掉一半 CPU)。

验收:把 Phase 2 的项目改成 4 个左右任务的 RTOS 版本,每个任务的栈都用 HighWaterMark 量过并留 30% 余量。

详细内容见 FreeRTOS 学习攻略。

PHASE 4

从开发板到产品

开发板上跑通,只完成了整个工作量的一小半。剩下的部分没人教,但决定产品能不能出货。

电源与复位:上电时序、复位 RC 参数、欠压检测阈值、复位脚走线远离干扰源。

时钟:外部晶振的负载电容要按晶体规格算,起振时间留余量,HSE 起振失败要能回退到内部 RC 并上报。

看门狗:独立看门狗 + 任务心跳汇总,别在定时器中断里喂。

升级:Bootloader + 双区 OTA + 断电续传 + 版本回滚 + 完整性校验。这四项缺一项,现场升级就会变砖。

低功耗:每个外设在睡眠前要关时钟,唤醒源要清理干净,实测待机电流而不是看手册典型值。

验收:断电拔电 500 次参数不丢;OTA 到 60% 断电,重新上电能接着升或回滚;待机电流达标且重复 20 次一致。

一个能直接抄的目录结构

Phase 2 说的分层,落到文件夹就是下面这样。新建项目直接照抄,改改模块名即可。

project/ ├── app/ 业务:状态机、协议、业务规则 │ ├── app_main.c 唯一入口,main 里只调它 │ ├── app_state.c │ └── app_proto.c ├── drv/ 器件驱动:换块板子也能用 │ ├── drv_sht30.c 温湿度 │ ├── drv_w25q.c SPI Flash │ └── drv_oled.c ├── bsp/ 板级:这块板子特有的 │ ├── bsp_gpio.c 引脚、上下拉、初始电平 │ ├── bsp_clock.c 时钟树,把频率假设写死在这 │ └── bsp_uart.c DMA + 空闲中断收发 ├── mid/ 中间件:不碰硬件的通用件 │ ├── ringbuf.c │ ├── logger.c │ └── soft_timer.c ├── cfg/ 参数默认值、编译开关、版本号 │ ├── cfg_default.h │ └── version.h ├── test/ 能单独编译的测试 │ └── test_ringbuf.c └── main.c 只做初始化顺序和启动调度
配套三条规矩,比目录本身重要:
1. 依赖只能往下,app 可以调 drv,drv 不能反过来调 app。
2. 头文件只放接口。结构体用 typedef struct xxx_s xxx_t,成员不对外暴露。
3. 每个 .c 都能单独编译,不依赖 main.c 里的全局变量。做到这条,才谈得上写单元测试。

这十一个坑,我基本都踩过

  1. 中断里调 HAL_Delay() —— 它依赖 SysTick 中断,同优先级下进不去,直接卡死。要延时用 DWT 计数器或空循环。
  2. CubeMX 重新生成,手写代码没了 —— 所有自己写的代码必须在 /* USER CODE BEGIN */ 和 /* USER CODE END */ 之间,否则下次生成就覆盖。
  3. SysTick 优先级设成最低 —— 高优先级中断里调 HAL 的延时或超时函数,会永远等不到 tick。
  4. 外部晶振不起振 —— 检查负载电容是否按晶体规格(不是随便 20pF)、起振时间是否留够、HSE 失败后代码有没有回退到 HSI。
  5. SWD 引脚被复用成 GPIO —— 下次连不上。用 connect under reset 或拉高 BOOT0 救回来,救回来之后记得留调试口。
  6. BOOT0 悬空 —— 上电偶尔进不了 app。这个故障看起来像「程序时好时坏」,其实是启动模式在飘。
  7. 调试时看门狗一直复位 —— 用 DBGMCU 冻结,但别在产品代码里写「检测到调试器就关狗」。
  8. Flash 写完不校验 —— 没解锁、没擦除、写到写保护区,写操作静默失败。写完必须回读比对。
  9. 中断里调 printf / malloc —— 见 面试题库第 4 题。表现是随机死机,堆坏了之后故障点跑到几百毫秒以外。
  10. 共享变量忘了 volatile —— 优化等级一开,while(flag) 就变成死循环,Debug 版本还好好的。
  11. 供电纹波大 —— ADC 抖动、随机复位、通信误码,全都能是它。先量电源,别急着改代码。
资料怎么用:参考手册(RM)查寄存器,数据手册(DS)查电气参数,Cortex-M 权威指南理解内核行为,HAL 源码看 ST 怎么处理错误分支。
顺序是:先看时序图想清楚「硬件要什么」,再决定「软件怎么写」。反过来做,就是在试错中浪费两周。

留言 / 提问

写你卡住的地方:什么芯片、什么现象、已经试过什么。频率高的问题我会写进页面。昵称不填也能发。