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友好) |
2.2 编译器与调试器配置
IDE 装好只是第一步。真正决定代码质量的,是编译器选项。我见过太多人直接点「编译」,默认选项跑到底,结果 Flash 不够用或者跑起来有 bug。
2.2.1 编译器优化等级
这里有个常见的坑:调试阶段不要开 -O2 或 -Os。为什么?因为优化器会重排代码、内联函数、删除变量,你单步调试的时候根本对不上源码行号。
- -O0:无优化,调试体验最好,适合开发阶段。
- -O1:轻度优化,代码量适中,我常在功能验证后期用。
- -O2:中度优化,性能优先,适合稳定后的发布版本。
- -Os:优化代码大小,Flash 紧张时的救命稻草。
2.2.2 调试器配置要点
调试器配置其实就三件事:接口、速度、复位策略。
- 接口选择:SWD 只需要 4 根线(VCC、GND、SWDIO、SWCLK),JTAG 要 5 根。我 90% 的项目都用 SWD,省引脚。
- 时钟速度:默认 4MHz 比较稳。如果板子走线长或者有干扰,降到 1MHz。别为了快那几秒把调试器搞掉线。
- 复位策略:选「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
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
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
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 就能干活;工程模板按我给的目录结构来,后面加模块会非常舒服。