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

大家好,我是你们这门课的老朋友。做了十几年单片机开发,踩过的坑比吃过的盐还多。今天咱们开篇第一讲,不急着敲代码,先聊聊一个根本问题——为什么我们需要软件架构?

你可能会想:“我就一个跑裸机的MCU,总共几K的RAM,搞什么架构?那不是大材小用吗?”

嗯,我以前也这么想。直到有一次,我接手一个同事留下的项目——一个智能家居网关。代码写得那叫一个“自由奔放”:全局变量满天飞,中断服务函数里调延时,外设初始化散落在五个不同的.c文件里。客户要加一个温湿度传感器,我改了一周,还引入了三个新bug。从那以后,我彻底明白了:没有架构的项目,就是给自己挖坑。

1.1 MCU开发的典型痛点

咱们先来盘点一下,裸机开发中那些让人头疼的问题。我列了个表,你看看中了几条:

痛点 具体表现 后果
代码耦合严重 驱动代码和业务逻辑混在一起,改一个地方牵连一片 维护成本高,改bug容易引入新bug
全局变量泛滥 所有模块都能随意读写同一个变量,数据流不可控 调试困难,难以定位问题根源
中断处理臃肿 在ISR里做大量计算或调用阻塞函数 系统响应变慢,甚至死锁
外设驱动重复造轮子 每个项目都重新写I2C、SPI驱动,风格还不统一 开发效率低,代码质量参差不齐
内存管理混乱 静态数组大小拍脑袋定,动态分配不敢用 内存浪费或溢出,系统不稳定
移植困难 换个MCU型号,代码几乎要重写 产品迭代慢,错过市场窗口

说白了,这些问题归根结底就一句话:缺乏分层和抽象。 软件架构要解决的,就是把这些乱麻理清楚,让每一层各司其职。

核心观点: 软件架构不是给代码“穿衣服”,而是给系统“搭骨架”。骨架正了,肌肉(功能)才能长对地方。

1.2 课程知识体系总览

这门课一共30章,我把它分成四个大的模块。先给你一张图,心里有个谱:

课程知识体系结构图 《单片机软件架构》课程知识体系 模块一:基础与架构设计 第1-8章 • 课程导论与环境搭建 • 分层架构与模块化设计 • 状态机与事件驱动框架 • 接口抽象与回调机制 模块二:外设驱动框架实战 第9-18章 • GPIO/UART/I2C/SPI驱动框架 • 定时器与PWM抽象层 • ADC/DAC与传感器驱动 • DMA与中断统一管理 模块三:内存管理优化 第19-25章 • 静态内存与动态内存策略 • 内存池与伙伴算法 • 环形缓冲区与消息队列 • 内存泄漏检测与栈分析 模块四:工程实践与调试 第26-30章 • 单元测试与模拟框架 • 日志系统与断言机制 • 低功耗与实时性优化 • 综合项目:智能传感器节点 每个模块都配有完整工程模板和实战案例,代码可直接移植

你看,从基础架构到外设驱动,再到内存管理和工程实践,这是一个完整的闭环。我个人习惯是:先搭骨架,再填血肉。 所以前几章会有点“虚”,但请相信我,地基打牢了,后面盖楼才稳当。

1.3 开发环境选择与配置

工欲善其事,必先利其器。咱们这门课主要用三款IDE:Keil MDK、IAR EWARM 和 STM32CubeIDE。你可能会问:“到底选哪个?”

我的建议是:看项目,看团队,看个人习惯。 下面这个表格帮你快速对比:

特性 Keil MDK IAR EWARM STM32CubeIDE
厂商支持 ARM全系列 ARM全系列 STM32专属
编译器 ARMCC v6/v5 IAR C/C++ GCC (免费)
调试器 ULINK/J-Link I-Jet/J-Link ST-Link (内置)
代码大小限制 32KB (免费版) 无限制 (评估版有天数) 无限制 (完全免费)
工程模板 需手动搭建 需手动搭建 自动生成HAL/LL
适合场景 商业项目、老项目维护 高性能、代码密度要求高 学习、原型验证、STM32项目

我的个人经验: 如果你刚开始学,我建议用 STM32CubeIDE。免费、开箱即用、社区活跃。等你项目做大了,再考虑 Keil 或 IAR 的商业版。我自己平时做原型用 CubeIDE,量产项目用 Keil,因为 Keil 的调试工具链更成熟。

1.4 工程模板建立——以STM32CubeIDE为例

好,咱们动手实操一下。我以 STM32CubeIDE 为例,带你建一个干净的工程模板。这个模板会贯穿整个课程,所以每一步都别跳过。

步骤一:新建STM32项目

  1. 打开 STM32CubeIDE,点击 File → New → STM32 Project。
  2. 在芯片选择器里输入 STM32F103C8T6(咱们课程的主力芯片)。
  3. 选择好后,点击 Next,输入项目名称,比如 MCU_Arch_Template。
  4. 在 Project Settings 里,Targeted Language 选 C,Binary Type 选 Executable。
  5. 点击 Finish,IDE会自动生成初始化代码。

步骤二:配置时钟树

  1. 双击 .ioc 文件,打开图形化配置界面。
  2. 在 Clock Configuration 标签页,配置 HSE 为 8MHz 外部晶振。
  3. 设置系统时钟 SYSCLK 为 72MHz(F103的最高主频)。
  4. 确保 APB1 和 APB2 时钟分频正确(APB1=36MHz, APB2=72MHz)。

注意: 时钟配置是MCU开发的“第一道坎”。我曾经在一个项目里,因为时钟树配错了,导致UART波特率偏差了3%,通信时好时坏。排查了整整两天才发现是时钟源选错了。所以,每次改时钟配置后,一定要用示波器或逻辑分析仪验证一下。

步骤三:添加外设驱动框架目录

我个人习惯把工程文件组织成下面这样,清晰好找:

MCU_Arch_Template/
├── Core/
│   ├── Inc/          // 头文件
│   ├── Src/          // 主程序、中断、系统文件
│   └── Startup/      // 启动文件
├── Drivers/
│   ├── CMSIS/        // ARM CMSIS 标准库
│   └── STM32F1xx_HAL_Driver/  // HAL库
├── User/
│   ├── app/          // 应用层代码
│   ├── bsp/          // 板级支持包(外设驱动)
│   ├── framework/    // 架构框架(状态机、消息队列等)
│   └── utils/        // 通用工具(环形缓冲、CRC、日志等)
└── Doc/              // 项目文档

在 IDE 里,右键项目根目录 → New → Folder,按上面的结构建好。然后在 Project → Properties → C/C++ Build → Settings → Include paths 里,把 User/ 下的所有子目录都加进去。

步骤四:编写一个简单的“点亮LED”测试

模板建好了,咱们写个最简单的程序,验证环境是否正常:

// File: User/bsp/led.c
#include "led.h"
#include "stm32f1xx_hal.h"

void LED_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOC_CLK_ENABLE();   // 使能GPIOC时钟
    
    GPIO_InitStruct.Pin = GPIO_PIN_13;  // PC13 是板载LED
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}

void LED_Toggle(void)
{
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
// File: User/bsp/led.h
#ifndef __LED_H
#define __LED_H

void LED_Init(void);
void LED_Toggle(void);

#endif

然后在 main.c 里调用:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LED_Init();
    
    while (1)
    {
        LED_Toggle();
        HAL_Delay(500);  // 500ms 闪烁一次
    }
}

编译下载,如果板子上的LED开始有规律地闪烁,恭喜你,环境搭建成功了!

一个小建议: 从今天开始,养成给每个外设驱动单独建文件、单独写头文件的习惯。哪怕只是一个LED,也值得一个 led.c 和 led.h。这看起来有点“小题大做”,但项目一复杂,你就知道好处了——模块化是架构的起点。

1.5 避坑指南:环境搭建常见问题

最后,分享几个我踩过的坑,你遇到了可以少走弯路:

  • 问题1:编译报错“找不到头文件”
    八成是 Include paths 没加全。检查一下 User/ 下的子目录是否都添加了。
  • 问题2:下载后程序不运行
    先检查复位电路和BOOT引脚。我遇到过好几次,BOOT0没拉低,芯片一直停在系统存储器里。
  • 问题3:LED不亮但程序在跑
    用调试器单步执行,看GPIO的ODR寄存器有没有变化。如果有变化,检查硬件连接;如果没有,检查时钟使能。
  • 问题4:HAL_Delay 不准确
    通常是 SysTick 中断优先级被改了,或者被其他高优先级中断阻塞了。检查 HAL_InitTick() 的配置。