第1章:课程导论与开发环境搭建

各位同学,欢迎来到《STM32裸机模块化编程与状态机设计实战》。我是你们这门课的主讲。在嵌入式这行摸爬滚打了十几年,踩过的坑、烧过的板子,说实话,真不少。今天这第一讲,我们不急着写代码,先把地基打牢。

你可能会问:为什么第一节课不讲点炫酷的?我的回答是:方向错了,跑得再快也没用。选型选错、工具链没配好、库选得不对,后面全是泪。我见过太多新手,甚至一些老手,因为一开始没想清楚,项目做到一半发现资源不够,或者换芯片重写驱动,那叫一个痛苦。

所以,这节课我们就把三件事聊透:选什么芯片、用什么思想写代码、搭什么环境。

1.1 STM32选型指南:别让芯片成为你的瓶颈

STM32家族庞大得像个动物园。从Cortex-M0到M7,从几十KB Flash到几MB,从几十脚到两百多脚。怎么选?我个人的习惯是,先看需求,再看预算,最后看生态。

系列 内核 主频 典型Flash 适合场景
STM32F0 Cortex-M0 48 MHz 16-256 KB 简单控制、替代8位机
STM32F1 Cortex-M3 72 MHz 32-512 KB 通用工业控制、电机
STM32F4 Cortex-M4F 168-180 MHz 256-2048 KB DSP、图形、复杂算法
STM32G0 Cortex-M0+ 64 MHz 16-512 KB 低功耗、成本敏感
STM32H7 Cortex-M7 400-550 MHz 1-2 MB 高性能计算、AI边缘
我的建议: 如果你是初学者,或者做通用项目,STM32F103C8T6(也就是大家常说的“蓝 pill”)或者 STM32F401CCU6(黑 pill)是最稳妥的选择。资料多、例程多、淘宝上十几块钱一块板子,烧了也不心疼。我曾经带过一个实习生,上来就选了H7,结果光配时钟树就配了一周,项目进度直接崩了。

选型时,还有几个关键点你要想清楚:

  • 引脚数: 你需要的GPIO、外设接口(UART、SPI、I2C)够不够?别到时候发现少一个定时器通道,那就尴尬了。
  • Flash和RAM: 代码体积、堆栈大小、缓冲区。我做过一个项目,用了F103的48KB RAM,结果一个FFT缓冲区就占了32KB,剩下的连个任务栈都分不出来。嗯,后来换了F4。
  • 封装: LQFP好焊接,BGA需要打样。自己DIY就选LQFP。

1.2 模块化编程思想概述:让代码像乐高一样搭起来

很多新手写代码,喜欢把所有东西都塞进 main.c。一个文件几千行,全局变量满天飞。这种代码,我称之为“意大利面条式代码”。看着能跑,但改一个功能,可能牵出一堆bug。

模块化编程,说白了就是分而治之。把不同的功能拆成独立的模块,每个模块有自己的 .h 和 .c 文件。模块之间通过接口(函数、结构体)通信,不直接访问对方的内部变量。

核心原则:
  • 高内聚: 一个模块只做一件事,并且把它做好。
  • 低耦合: 模块之间依赖越少越好。改一个模块,不影响其他模块。
  • 接口清晰: 公开的函数和数据结构要稳定,内部实现可以随便改。

举个例子。你写一个LED闪烁程序。非模块化写法:直接在main里操作GPIO寄存器。模块化写法:先写一个 led.c,提供 LED_Init()、LED_On()、LED_Off()、LED_Toggle() 四个函数。main里只管调用。以后你想把LED从PA0换到PB5,只需要改 led.c 里的配置,main里一行代码都不用动。

你想想看,如果项目里有几十个外设,每个都这么搞,维护起来是不是轻松多了?

1.3 状态机设计思想概述:让逻辑不再混乱

嵌入式系统里,大部分行为都是事件驱动的。按键按下、定时器溢出、数据接收完成……这些事件发生后,系统要做出不同的反应。如果用一堆 if-else 或者 switch-case 来写,逻辑一复杂,代码就变成一团乱麻。

状态机(State Machine)就是用来解决这个问题的。它把系统抽象成有限个状态,每个状态下,系统只对特定的事件做出响应,并可能跳转到下一个状态。

一个简单的状态机模型:

  • 状态(State): 系统当前所处的模式。比如:空闲、运行、报警。
  • 事件(Event): 触发状态转移的条件。比如:按键按下、温度超限。
  • 动作(Action): 进入或离开状态时执行的操作。比如:点亮LED、启动电机。
  • 转移(Transition): 从当前状态到下一个状态的路径。
我曾经踩过的坑: 有一次做智能家居网关,状态逻辑用了三层嵌套的switch-case,总共十几个状态。后来客户要加一个“离家模式”,我改了三天,改出了五个新bug。最后痛定思痛,全部重构成状态机表驱动,一天搞定,再也没出过逻辑问题。

状态机的好处是:逻辑可视化、可测试、易扩展。你甚至可以先把状态图画出来,再照着图写代码。这就是我们这门课的核心思想之一。

状态机基本结构 空闲状态 运行状态 报警状态 启动事件 故障事件 复位事件 恢复事件 每个状态只处理特定事件,逻辑清晰,便于调试和扩展

1.4 开发环境安装与配置:工欲善其事,必先利其器

环境搭建这块,我推荐两种选择:Keil MDK 和 STM32CubeIDE。各有优劣,看你的习惯和项目需求。

特性 Keil MDK STM32CubeIDE
编译器 ARMCC (v5/v6) GCC (arm-none-eabi)
调试器 ULINK / J-Link / ST-Link ST-Link (内置)
代码生成 需配合CubeMX 内置CubeMX
许可证 商业收费(有代码大小限制的免费版) 免费开源
生态 老牌、工业界广泛使用 ST官方、更新快
我的个人习惯: 做商业项目,我倾向于Keil,因为ARMCC的优化确实好,而且很多老牌芯片厂只提供Keil的例程。但如果是学习、个人项目或者团队协作,我强烈推荐STM32CubeIDE。免费、开箱即用、CubeMX集成,省去很多配置的麻烦。我自己现在讲课和做原型,基本都用CubeIDE。

安装步骤其实很简单,但有几个坑要注意:

  1. Keil安装: 先装Keil,再装对应的芯片包(Pack)。别装反了。我记得有一次帮同事装,他先装了Pack,结果Keil死活认不到芯片。嗯,顺序很重要。
  2. STM32CubeIDE: 直接从ST官网下载,解压即用。注意安装路径不要有中文和空格。我见过有人装在“D:\程序\STM32”下面,结果编译报错,找了一下午原因。
  3. 驱动: 如果你用ST-Link,记得装驱动。Win10/11一般自动识别,但Win7需要手动装。插上调试器,设备管理器里看到“STMicroelectronics STLink dongle”就对了。
  4. 第一个工程: 新建工程,选好芯片,配置时钟(一般先选外部晶振HSE,然后配PLL到最高主频),然后写一个简单的GPIO翻转程序,下载进去,看LED亮不亮。亮了,环境就通了。
避坑指南: 我曾经在调试一个F4项目时,怎么都下载不进去。折腾了两天,最后发现是调试器的线太长了,信号衰减。换了根短屏蔽线,问题解决。所以,调试线不要超过20cm,尽量用带磁环的。

1.5 标准外设库与HAL库的选择:别纠结,看场景

这是新手问得最多的问题。标准外设库(SPL)和HAL库,到底学哪个?

我的看法很直接:学HAL库,但理解SPL的思想。

  • 标准外设库(SPL): 直接操作寄存器,代码执行效率高,体积小。但每个外设的初始化代码都要自己写,移植性差。ST已经停止更新了,只支持到F1/F4等老系列。
  • HAL库: 抽象层封装,API统一,跨系列移植方便。CubeMX可以直接生成初始化代码。缺点是代码体积大,执行效率略低(但大部分场景下你感觉不到)。

为什么我推荐学HAL库?因为现在的趋势就是HAL。新出的G0、G4、H5、H7系列,ST只提供HAL库。你学SPL,以后换芯片还得重新学。而且HAL库的代码可读性好,配合CubeMX,开发效率高很多。

但是! 这不意味着你可以完全不懂寄存器。HAL库底层还是操作寄存器。遇到性能瓶颈或者bug,你还是得去看寄存器手册。所以,我的建议是:用HAL库快速开发,用寄存器思维去调试和优化。

举个例子。你用HAL库的 HAL_GPIO_TogglePin() 翻转一个IO,频率可能只有几MHz。但如果你直接操作 GPIOA->ODR ^= (1 << 5),频率可以到几十MHz。这就是差距。但平时点个灯、读个按键,HAL库完全够用。