1. 课程导论与开发环境搭建:为什么需要软件架构?

各位同学,大家好。我是你们这门课的老朋友。说实话,做了十几年嵌入式,踩过的坑比吃过的盐还多。今天咱们开篇第一讲,不急着敲代码,先聊聊一个看似“虚”但实则非常要命的问题——为什么我们需要软件架构?

很多人觉得,STM32裸机开发嘛,不就是写个while(1)循环,加点中断,调调外设就完事了?我以前也这么想。直到有一次,我接手一个同事离职留下的项目——一个智能家居网关。好家伙,main函数里两千多行,全局变量满天飞,中断服务函数里还调延时。改一个功能,三个地方出bug。那段时间我天天加班到凌晨,心里就一个念头:这代码要是有点架构,至于吗?

1.1 STM32裸机开发的痛点分析

咱们先来盘点一下,裸机开发到底有哪些“坑”。你想想看,是不是经常遇到这些问题?

痛点 具体表现 后果
代码耦合严重 LED闪烁逻辑写在按键中断里,定时器回调里又去操作串口 改一处,崩一片
全局变量泛滥 extern uint8_t flag; 满文件飞,不知道谁改的 调试像破案,心累
模块复用性差 这个项目的I2C驱动,换个芯片引脚就得重写 重复造轮子,效率低
可读性灾难 main函数里直接操作寄存器,没有函数封装 自己写的代码,两周后也看不懂
调试困难 逻辑和硬件初始化混在一起,不知道是配置错了还是逻辑错了 定位问题耗时占开发60%
⚠️ 我曾经踩过的坑: 有一次做电机控制,我把PID计算和PWM输出写在同一个函数里。结果电机转起来抖得厉害,查了两天,发现是PWM更新频率被PID计算里的浮点运算拖慢了。如果当时把控制逻辑和硬件驱动分层,一眼就能看出问题。

说白了,裸机开发不是不能做,而是没有章法地做,项目一复杂就崩。我见过太多人,写个流水灯没问题,但一做多任务、多外设协同,代码就成了一团乱麻。

1.2 为什么需要软件架构?

软件架构不是什么高大上的东西。它其实就是一套约定和规则,告诉你的代码该怎么组织、模块之间怎么通信、数据怎么流动。

我个人习惯把架构比作“盖房子的脚手架”。没有脚手架,你也能砌墙,但砌不高、砌不稳,风一吹就倒。有了架构,你才能一层一层往上盖,出了问题也知道是哪根柱子歪了。

咱们这门课要讲的模块化编程架构,核心就三个原则:

  • 高内聚:一个模块只做一件事,并且把它做好。比如LED模块只管亮灭,别管按键。
  • 低耦合:模块之间通过接口通信,不直接访问对方的内部变量。
  • 分层设计:硬件驱动层、中间件层、应用逻辑层,各司其职。

你想想看,如果代码按这个思路写,是不是改一个功能,其他模块不用动?是不是换个芯片平台,只需要重写底层驱动,上层逻辑直接复用?

💡 一句话总结: 软件架构不是为了炫技,而是为了让你少加班、少掉头发。

1.3 裸机模块化架构的核心逻辑

为了让大家更直观地理解,我画了一张图。这张图就是咱们整个课程要实现的目标架构。

STM32裸机模块化软件架构图 STM32 裸机模块化软件架构 应用逻辑层 (App) 中间件层 (Middleware) — 协议栈、算法、状态机 硬件抽象层 (HAL) — GPIO / UART / I2C / SPI / Timer MCU 底层 (寄存器 / 启动文件) 架构要点: • 上层只能调用相邻下层接口,禁止跨层访问(如App直接操作寄存器) • 每层内部模块高内聚,层与层之间通过结构体函数指针解耦

这张图就是咱们这门课的终极目标。从下往上,每一层都只依赖它的下一层。应用层不知道你用的是STM32F103还是F407,它只管调用中间件接口。中间件也不知道GPIO是怎么配置的,它只管调用HAL层的API。

这样做的好处,我在项目里体会太深了。有一次客户要求把主控从STM32F103换成GD32F303,我只需要重写最底层的HAL驱动,上面的中间件和应用层一行代码没改,两天就搞定了。要是以前那种“大泥球”代码,估计得折腾两周。

1.4 开发环境搭建与工程模板创建

好了,理念讲完了,咱们来点实际的。工欲善其事,必先利其器。咱们得先把开发环境搭好。

市面上主流的STM32开发环境有三个:Keil MDK、IAR EWARM 和 STM32CubeIDE。我个人习惯用Keil,因为生态成熟,第三方库支持好。但STM32CubeIDE免费且内置了CubeMX,对新手很友好。IAR的编译优化确实强,但价格劝退。

特性 Keil MDK IAR EWARM STM32CubeIDE
价格 收费(有免费版限制32KB) 收费(较贵) 免费
编译效率 中等 高 中等(基于GCC)
调试器支持 J-Link / ST-Link / ULINK J-Link / I-jet ST-Link(原生支持)
CubeMX集成 需手动导入 需手动导入 原生集成
适合人群 企业项目、老手 对代码大小/速度有极致要求 新手、学生、预算有限
💡 我的建议: 如果你是初学者,直接用STM32CubeIDE,省去配置的烦恼。如果你以后要去企业,Keil是绕不开的。这门课我会以Keil为例讲解,但架构思想完全通用,你用哪个IDE都一样。

1.5 创建你的第一个模块化工程模板

咱们不搞复杂的,先创建一个最简的工程模板。这个模板会贯穿整个课程,后续所有章节的代码都基于这个模板扩展。

步骤很简单:

  1. 新建文件夹结构:按架构分层创建目录
  2. 配置启动文件和链接脚本:从STM32Cube固件包复制
  3. 编写main.c框架:只包含初始化循环和主循环骨架
  4. 添加一个简单的LED模块:验证编译通过

文件夹结构我建议这样组织:

Project/
├── App/                # 应用逻辑层
│   └── app_main.c
├── Middleware/         # 中间件层
│   └── led/            # LED模块(后续会扩展)
│       ├── led.c
│       └── led.h
├── Hal/                # 硬件抽象层
│   ├── gpio/
│   │   ├── hal_gpio.c
│   │   └── hal_gpio.h
│   └── timer/
│       ├── hal_timer.c
│       └── hal_timer.h
├── Mcu/                # MCU底层
│   ├── startup_stm32f103xe.s
│   └── system_stm32f1xx.c
├── Drivers/            # 官方标准外设库或HAL库
│   └── CMSIS/
└── Project.uvprojx     # Keil工程文件

嗯,这里要注意:不要把所有的.c文件都扔在一个文件夹里。我见过太多人,一个Src文件夹里塞了50多个文件,找起来眼睛都花了。按模块分文件夹,这是架构的第一步。

main.c的骨架也很简单:

#include "hal_gpio.h"
#include "led.h"

int main(void)
{
    /* 硬件初始化 */
    HAL_GPIO_Init();
    
    /* 模块初始化 */
    LED_Init();
    
    /* 主循环 */
    while (1)
    {
        LED_Toggle();
        Delay_Ms(500);
    }
}

你看,main函数里只有三部分:初始化、主循环。具体怎么闪烁,那是LED模块内部的事。main函数只负责“调度”,不负责“实现”。这就是架构的雏形。

⚠️ 避坑指南: 我曾经见过有人把延时函数直接写在main.c里,结果后来要改成非阻塞延时,整个main.c都得重写。正确的做法是把延时封装成模块,main.c只管调用。这样改的时候,只改模块内部,main.c纹丝不动。

1.6 本章小结

这一章咱们聊了不少。从裸机开发的痛点,到为什么需要架构,再到环境搭建和工程模板。说白了,就是让你明白一个道理:写代码和写“好”代码,中间差的就是一个架构。

我刚开始做嵌入式的时候,也觉得架构是浪费时间。直到被项目毒打了几次,才明白“磨刀不误砍柴工”的道理。这门课,我会把我这些年积累的模块化架构经验,一点一点拆给你看。