2. 开发环境搭建与工程模板

好,咱们正式开始动手。这一章我带你把吃饭的家伙事儿先支棱起来。很多初学者喜欢一上来就写代码,结果编译报错找半天是环境没配好,或者工程结构乱成一锅粥,后面维护想死的心都有。

我个人的习惯是:先花半天把环境和模板搞定,后面能省下无数个半天。你想想看,是不是这个理儿?

2.1 IDE 选择与配置

目前主流的 MCU IDE 就三个:Keil、IAR、STM32CubeIDE。我三个都用过,说说我的感受。

特性 Keil MDK IAR EWARM STM32CubeIDE
厂商绑定 通用(ARM Cortex-M为主) 通用(ARM/AVR/RISC-V) ST 专用
编译器 ARMCC v6/v5 IAR C/C++ Compiler GCC(arm-none-eabi)
调试器 ULINK / J-Link / ST-Link I-Jet / J-Link / ST-Link ST-Link(内置)
许可证 商业付费(有代码大小限制版) 商业付费 免费(Eclipse 开源)
工程管理 .uvprojx 二进制 .ewp 二进制 .cproject 文本(Git友好)
我的建议: 如果你做 STM32 项目,直接用 STM32CubeIDE,免费且 Git 友好。如果是通用项目或客户指定,Keil 和 IAR 二选一。我个人在量产项目中用 IAR 多一些,它的优化选项更细,代码密度控制得更好。

2.2 编译器与调试器配置

IDE 装好只是第一步。真正决定代码质量的,是编译器选项。我见过太多人直接点「编译」,默认选项跑到底,结果 Flash 不够用或者跑起来有 bug。

2.2.1 编译器优化等级

这里有个常见的坑:调试阶段不要开 -O2 或 -Os。为什么?因为优化器会重排代码、内联函数、删除变量,你单步调试的时候根本对不上源码行号。

  • -O0:无优化,调试体验最好,适合开发阶段。
  • -O1:轻度优化,代码量适中,我常在功能验证后期用。
  • -O2:中度优化,性能优先,适合稳定后的发布版本。
  • -Os:优化代码大小,Flash 紧张时的救命稻草。
我曾经踩过的坑: 有一次量产前把优化从 -O0 切到 -Os,结果延时函数全不准了,串口波特率直接翻倍。查了两天才发现是优化器把循环里的 NOP 指令给吃了。从那以后,我每次切优化等级都会做一次全功能回归测试。

2.2.2 调试器配置要点

调试器配置其实就三件事:接口、速度、复位策略。

  1. 接口选择:SWD 只需要 4 根线(VCC、GND、SWDIO、SWCLK),JTAG 要 5 根。我 90% 的项目都用 SWD,省引脚。
  2. 时钟速度:默认 4MHz 比较稳。如果板子走线长或者有干扰,降到 1MHz。别为了快那几秒把调试器搞掉线。
  3. 复位策略:选「Connect under Reset」或者「Normal」。如果芯片休眠后 SWD 口被关了,必须用复位连接。
// STM32CubeIDE 调试配置示例(launch.json 片段)
{
    "name": "Debug STM32F407",
    "cwd": "${workspace_root}",
    "executable": "./build/firmware.elf",
    "request": "launch",
    "type": "cortex-debug",
    "servertype": "openocd",
    "device": "STM32F407VG",
    "interface": "swd",
    "speed": 4000,   // 4MHz
    "runToMain": true
}

2.3 版本控制(Git)入门

说到 Git,我见过太多嵌入式工程师还在用「项目_v1.0_final_改3_真的不改了.zip」这种命名方式。说实话,这习惯得改。

嵌入式项目用 Git,核心就三个操作:commit、branch、merge。别整那些花里胡哨的 rebase 和 cherry-pick,先把基础玩明白。

2.3.1 .gitignore 怎么写

嵌入式项目里有一堆编译中间产物,千万别提交到仓库。我见过有人把 200MB 的 .o 文件也 push 上去,整个仓库直接废了。

# 嵌入式项目 .gitignore 模板
# 编译输出
build/
Debug/
Release/
*.o
*.d
*.hex
*.bin
*.elf
*.map

# IDE 配置(不同人习惯不同,不要提交)
*.uvguix.*
*.uvoptx
*.ewt
*.ewd
.cproject
.project

# 系统文件
.DS_Store
Thumbs.db
核心原则: 只提交源码、头文件、链接脚本、工程描述文件。其他一切自动生成的东西,统统 ignore。

2.3.2 分支策略

小团队(1-3人)我推荐最简单的策略:

  • main:稳定版本,只合并不直接开发。
  • dev:日常开发分支,每天下班前 push 一次。
  • feature/xxx:新功能分支,做完后合并到 dev。
  • fix/xxx:紧急修复分支,修完合并到 main 和 dev。

别搞什么 Git Flow 那一套,嵌入式项目没那么复杂。你想想看,一个单片机程序,总共就几十个文件,搞五六个分支来回切,光合并冲突就够你喝一壶的。

2.4 创建标准化的 MCU 工程模板

这一节是重点。我做了十年嵌入式,换过七八家公司,每家工程结构都不一样。后来我自己总结了一套模板,用了五年,从 STM32 到 GD32 到 NXP,换芯片只改链接脚本和启动文件,其他结构完全不变。

2.4.1 目录结构

project_root/
├── app/                # 应用层代码(main.c 和业务逻辑)
│   ├── main.c
│   └── app_task.c
├── bsp/                # 板级支持包(外设驱动)
│   ├── bsp_uart.c
│   ├── bsp_i2c.c
│   └── bsp_spi.c
├── mcal/               # MCU 抽象层(寄存器操作)
│   ├── mcal_gpio.c
│   ├── mcal_timer.c
│   └── mcal_adc.c
├── middleware/          # 中间件(RTOS、协议栈、文件系统)
│   ├── FreeRTOS/
│   └── lwIP/
├── hal/                # 硬件抽象层(统一接口)
│   ├── hal_uart.h
│   └── hal_timer.h
├── startup/            # 启动文件和链接脚本
│   ├── startup_stm32f407xx.s
│   └── STM32F407VG_FLASH.ld
├── doc/                # 文档
│   └── readme.md
├── tools/              # 工具脚本
│   └── flash.py
├── build/              # 编译输出(不提交 Git)
├── .gitignore
├── Makefile            # 或者 CMakeLists.txt
└── README.md
为什么这么分? 因为分层架构的核心就是「依赖倒置」。app 层只调用 hal 层接口,不直接操作寄存器。哪天你要换 MCU,只需要重写 mcal 和 bsp,app 层一行代码都不用改。我在一个项目中用这个结构,把 STM32F103 换成了 GD32F303,只花了三天。

2.4.2 启动文件与链接脚本

启动文件一般芯片厂商会提供,但链接脚本我建议你自己手写一遍。为什么?因为只有你自己知道 Flash 和 RAM 怎么分、堆栈设多大、要不要加自定义段。

/* 链接脚本核心片段(STM32F407VG,1MB Flash,192KB RAM) */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
    CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}

SECTIONS
{
    .text : {
        *(.isr_vector)    /* 中断向量表 */
        *(.text)          /* 代码 */
        *(.rodata)        /* 只读数据 */
        _etext = .;
    } > FLASH

    .data : {
        _sdata = .;
        *(.data)
        _edata = .;
    } > RAM AT > FLASH

    .bss : {
        _sbss = .;
        *(.bss)
        _ebss = .;
    } > RAM
}

2.5 Makefile / CMake 基础

IDE 虽然方便,但自动化构建和 CI/CD 还是得靠命令行。我建议你至少掌握一种构建工具。

2.5.1 Makefile 快速上手

Makefile 的核心就三要素:目标、依赖、命令。别把它想得太复杂。

# 一个极简的 MCU Makefile
TARGET = firmware
MCU = cortex-m4

# 源文件
SRCS = $(wildcard app/*.c bsp/*.c mcal/*.c)
OBJS = $(SRCS:.c=.o)

# 编译器
CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld

# 编译选项
CFLAGS = -mcpu=$(MCU) -mthumb -O2 -Wall -Iapp -Ibsp -Imcal
LDFLAGS = -T startup/STM32F407VG_FLASH.ld

all: $(TARGET).hex

$(TARGET).elf: $(OBJS) startup/startup_stm32f407xx.o
    $(LD) $(LDFLAGS) $^ -o $@

$(TARGET).hex: $(TARGET).elf
    arm-none-eabi-objcopy -O ihex $< $@

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f $(OBJS) $(TARGET).elf $(TARGET).hex

.PHONY: all clean
注意: Makefile 里的缩进必须是 Tab,不能用空格。这个坑我当年踩了整整一个下午,死活编译不过,最后发现是编辑器自动把 Tab 转成空格了。

2.5.2 CMake 更适合现代项目

如果你的项目跨平台或者需要管理多个目标(比如同时编译 Bootloader 和 App),CMake 是更好的选择。它比 Makefile 更抽象,可读性也更好。

# CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.16)
project(Firmware C ASM)

set(MCU cortex-m4)
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/startup/STM32F407VG_FLASH.ld)

# 添加源文件
add_executable(firmware.elf
    app/main.c
    bsp/bsp_uart.c
    mcal/mcal_gpio.c
    startup/startup_stm32f407xx.s
)

# 编译选项
target_compile_options(firmware.elf PRIVATE
    -mcpu=${MCU} -mthumb -O2 -Wall
)

# 链接选项
target_link_options(firmware.elf PRIVATE
    -T ${LINKER_SCRIPT}
    -Wl,--gc-sections
)

# 生成 hex 文件
add_custom_command(TARGET firmware.elf POST_BUILD
    COMMAND arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
)

我个人现在新项目都优先用 CMake。原因很简单:它和 STM32CubeIDE 的 Eclipse 工程可以共存,而且 Git 对比时是纯文本,不会像 .uvprojx 那样动不动就冲突。

本章小结

环境搭建这事儿,说白了就是「磨刀不误砍柴工」。IDE 选一个顺手就行,别换来换去;Git 学会 commit、push、pull 就能干活;工程模板按我给的目录结构来,后面加模块会非常舒服。