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

各位同学好,我是你们这门课的老周。咱们直接开门见山——为什么我花了整整一章来讲环境搭建和分层理念?

因为我在带项目时见过太多这样的场景:一个工程师把外设驱动、业务逻辑、任务调度全写在一个文件里,刚开始跑得挺好,等加了三个外设、两个通信协议后,代码就成了一团乱麻。改一个定时器参数,结果把串口搞崩了。你说这锅该谁背?

所以这门课的第一件事,就是帮你建立正确的「架构思维」。别急着写代码,先把地基打牢。

1.1 为什么需要分层架构?

说白了,分层架构就是把代码按职责拆开。你想想看,一个嵌入式系统里,有硬件寄存器操作、有外设驱动逻辑、有业务处理流程、还有任务调度策略。这些东西混在一起,就像把衣服、袜子、工具全塞进一个抽屉——找东西时得翻半天。

我个人的习惯是,把代码分成三层:

层级 职责 典型内容
硬件抽象层(HAL) 封装MCU外设寄存器操作 STM32 HAL库、LL库
驱动封装层(Driver) 提供外设功能接口,屏蔽硬件细节 SPI Flash读写、I2C传感器驱动
应用层(App) 业务逻辑、任务调度、协议处理 FreeRTOS任务、状态机、数据解析

这样做的好处很明显:

  • 可移植性——换MCU时只改HAL层,上层代码基本不动
  • 可维护性——每个文件只干一件事,出bug了定位快
  • 可测试性——可以单独测试驱动层,不用等硬件就绪

核心观点:分层不是炫技,是为了让你在项目后期少熬夜。我在一个工业控制项目里,就因为没分层,改一个GPIO引脚定义导致三个模块报错,那晚的泡面味我现在还记得。

下面这张图,就是咱们这门课要讲的完整分层架构:

STM32 HAL库外设驱动分层架构 应用层(App) 业务逻辑 · 任务调度 · 协议处理 · 状态机 FreeRTOS任务 · 消息队列 · 软件定时器 驱动封装层(Driver) SPI Flash读写 · I2C传感器驱动 · UART通信封装 外设初始化/去初始化 · 中断回调封装 硬件抽象层(HAL) STM32 HAL库 · LL库 · 寄存器操作封装 GPIO · TIM · ADC · DMA · SPI · I2C · UART STM32 MCU 硬件外设 ↓ 上层调用下层接口,下层不依赖上层

1.2 STM32CubeMX 与 IDE 安装配置

好,理念讲完了,咱们动手搭环境。我建议你按这个顺序来:

  1. 安装 Java 运行环境(CubeMX 依赖)——去 Oracle 官网下 JDK 11 或更高版本
  2. 安装 STM32CubeMX——ST 官网免费下载,建议用最新稳定版
  3. 安装 IDE——Keil MDK 或 IAR EWARM,二选一
  4. 获取 HAL 库固件包——CubeMX 内在线下载,或手动导入

小技巧:我个人习惯把 CubeMX 生成的工程目录和 IDE 工程目录分开。CubeMX 只负责生成初始化代码,IDE 负责编译调试。这样你手动改代码时,不会被 CubeMX 重新生成覆盖掉。

1.3 Keil MDK 安装要点

Keil 的安装其实没什么难度,但有几点我踩过坑:

  • 安装路径不要有中文——Keil 对中文路径支持很差,编译报错时你根本想不到是路径问题
  • 先装 Keil,再装 Pack——Keil 安装完后,需要手动安装对应芯片的 Device Family Pack
  • License 激活——用管理员身份运行 Keil,否则激活可能失败

我曾经在一个项目里,因为路径带了「项目」两个字,编译死活过不去。折腾了两小时才发现是路径编码问题。嗯,从那以后我所有工具链都只用英文路径。

1.4 IAR EWARM 安装要点

IAR 和 Keil 的安装逻辑类似,但有几点不同:

对比项 Keil MDK IAR EWARM
芯片支持包 需单独下载 Pack 安装时可选,或后续添加
编译器 ARMCC v5/v6 IAR C/C++ Compiler
调试器 ULINK / J-Link / ST-Link I-Jet / J-Link / ST-Link
工程文件 .uvprojx .ewp / .eww

我个人更习惯用 Keil,因为它的 Pack 管理比较方便。但如果你做的是大项目,IAR 的编译优化确实更强一些。这个看个人喜好,不影响咱们课程的学习。

1.5 HAL 库固件包获取

HAL 库固件包有两种获取方式:

  • 在线下载——CubeMX 里打开 Help → Manage embedded software packages,选择对应系列下载
  • 手动导入——去 ST 官网下载 STM32Cube_FW_xxx 压缩包,解压后放到 CubeMX 的 repository 目录

注意:不同系列的 HAL 库版本可能不同。比如 STM32F4 和 STM32H7 的 HAL 库 API 有细微差异。你在创建工程时,CubeMX 会自动匹配对应系列的固件包。不要手动混用,否则编译会报一堆 undefined reference。

1.6 工程模板创建

好,环境搭完了,咱们创建一个干净的工程模板。这个模板会贯穿咱们整个课程。

我的做法是:

  1. 打开 CubeMX,选择芯片型号(比如 STM32F407ZGT6)
  2. 配置时钟源(HSE + LSE)和时钟树(主频 168MHz)
  3. 使能调试接口(SWD,PA13/PA14)
  4. 配置一个串口(USART1,用于打印调试信息)
  5. 配置一个定时器(TIM2,1ms 中断,用于系统滴答)
  6. 生成代码,选择 IDE 类型(MDK-ARM V5)

生成后的工程结构大概是这样的:

Project/
├── Core/
│   ├── Inc/          # 头文件
│   │   ├── main.h
│   │   ├── stm32f4xx_hal_conf.h
│   │   └── stm32f4xx_it.h
│   └── Src/          # 源文件
│       ├── main.c
│       ├── stm32f4xx_hal_msp.c
│       └── stm32f4xx_it.c
├── Drivers/
│   ├── STM32F4xx_HAL_Driver/  # HAL库源码
│   └── CMSIS/                 # CMSIS核心文件
├── MDK-ARM/          # Keil工程文件
└── .ioc              # CubeMX配置文件

避坑指南:我曾经在生成工程后,直接修改了 HAL 库源码。结果下次 CubeMX 重新生成时,我的修改全被覆盖了。正确的做法是:把需要修改的 HAL 函数复制到自己的驱动文件里,不要动原厂库。

1.7 验证环境是否正常

模板创建好了,咱们写个简单的测试代码,验证环境是否正常:

/* main.c */
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_TIM2_Init();

    /* 启动定时器 */
    HAL_TIM_Base_Start_IT(&htim2);

    while (1)
    {
        /* 主循环:每1秒翻转一次LED */
        HAL_Delay(1000);
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

/* 定时器中断回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        /* 1ms 系统滴答,用于任务调度 */
        uwTick++;
    }
}

编译通过,下载到板子,LED 以 1 秒间隔闪烁。好,环境没问题,咱们可以正式开始后面的课程了。

记住,一个好的工程模板,能让你后续的开发效率翻倍。别嫌麻烦,花半小时把模板搭好,后面省的是几天的调试时间。