1. 课程导论与开发环境搭建:为什么需要软件架构?
大家好,我是你们这门课的老朋友。做了十几年单片机开发,踩过的坑比吃过的盐还多。今天咱们开篇第一讲,不急着敲代码,先聊聊一个根本问题——为什么我们需要软件架构?
你可能会想:“我就一个跑裸机的MCU,总共几K的RAM,搞什么架构?那不是大材小用吗?”
嗯,我以前也这么想。直到有一次,我接手一个同事留下的项目——一个智能家居网关。代码写得那叫一个“自由奔放”:全局变量满天飞,中断服务函数里调延时,外设初始化散落在五个不同的.c文件里。客户要加一个温湿度传感器,我改了一周,还引入了三个新bug。从那以后,我彻底明白了:没有架构的项目,就是给自己挖坑。
1.1 MCU开发的典型痛点
咱们先来盘点一下,裸机开发中那些让人头疼的问题。我列了个表,你看看中了几条:
| 痛点 | 具体表现 | 后果 |
|---|---|---|
| 代码耦合严重 | 驱动代码和业务逻辑混在一起,改一个地方牵连一片 | 维护成本高,改bug容易引入新bug |
| 全局变量泛滥 | 所有模块都能随意读写同一个变量,数据流不可控 | 调试困难,难以定位问题根源 |
| 中断处理臃肿 | 在ISR里做大量计算或调用阻塞函数 | 系统响应变慢,甚至死锁 |
| 外设驱动重复造轮子 | 每个项目都重新写I2C、SPI驱动,风格还不统一 | 开发效率低,代码质量参差不齐 |
| 内存管理混乱 | 静态数组大小拍脑袋定,动态分配不敢用 | 内存浪费或溢出,系统不稳定 |
| 移植困难 | 换个MCU型号,代码几乎要重写 | 产品迭代慢,错过市场窗口 |
说白了,这些问题归根结底就一句话:缺乏分层和抽象。 软件架构要解决的,就是把这些乱麻理清楚,让每一层各司其职。
核心观点: 软件架构不是给代码“穿衣服”,而是给系统“搭骨架”。骨架正了,肌肉(功能)才能长对地方。
1.2 课程知识体系总览
这门课一共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项目
- 打开 STM32CubeIDE,点击 File → New → STM32 Project。
- 在芯片选择器里输入 STM32F103C8T6(咱们课程的主力芯片)。
- 选择好后,点击 Next,输入项目名称,比如
MCU_Arch_Template。 - 在 Project Settings 里,Targeted Language 选 C,Binary Type 选 Executable。
- 点击 Finish,IDE会自动生成初始化代码。
步骤二:配置时钟树
- 双击 .ioc 文件,打开图形化配置界面。
- 在 Clock Configuration 标签页,配置 HSE 为 8MHz 外部晶振。
- 设置系统时钟 SYSCLK 为 72MHz(F103的最高主频)。
- 确保 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()的配置。