最近在折腾一台 ROS 小车,底盘是买的成品(STM32F407 主控板),厂商提供了
Keil µVision 工程形式的固件源码。因为是自己组装的底盘,开机自检永远过不了,
板子就主动把电机失能了——车纹丝不动。厂商的建议是:把自检代码去掉、重新编译固件。
问题来了:我手头没有 Windows,也不想为了改几行代码去装一整套 Keil(而且 Keil
免费版有 32KB 代码限制,这个带 FreeRTOS 的固件根本编不过)。于是我决定把这个
Keil 工程改用开源的 arm-none-eabi-gcc 工具链,在 Linux 上用命令行编译。
这篇就完整记录这个过程,以及踩到的所有坑。如果你也遇到「只有 Keil 工程、却想用
GCC/Makefile 编译」的情况,应该能少走不少弯路。
适用对象:STM32(本文是 F407)+ 标准外设库(StdPeriph)+ FreeRTOS 的 Keil 工程。
思路通用,CubeMX/HAL 工程同理。
一、整体思路
Keil 工程和 GCC 工程的差别,本质是四样东西要替换:
| Keil 提供的 | GCC 需要的 |
|---|---|
.uvprojx 工程文件(记录源文件、宏、include) |
一个 Makefile |
| 分散文件(scatter file)或默认内存布局 | 一个 链接脚本 .ld |
ARMCC 语法的启动文件 startup_xxx.s |
GCC 语法的启动文件 |
| 编译器内置的 FreeRTOS 移植层(RVDS) | FreeRTOS 的 GCC 移植层 |
把这四样准备好,再处理一批 ARMCC 与 GCC 的语法差异,就能编译了。
二、第一步:从 .uvprojx 里挖出工程配置
.uvprojx 是 XML,关键信息可以直接 grep 出来。
芯片型号 / 内存布局:
grep -aoE "<Device>[^<]+|<Cpu>[^<]*" USER/xxx.uvprojx
# <Device>STM32F407VE
# <Cpu>IROM(0x08000000,0x80000) IRAM(0x20000000,0x20000) ...
F407VE = 512K Flash @0x08000000、128K RAM @0x20000000(外加 64K CCM @0x10000000)。
编译宏定义:
grep -aoE "<Define>[^<]*</Define>" USER/xxx.uvprojx
⚠️ 第一个大坑——多 target 工程。我的工程有 5 个 target(小车的不同车型),
每个 target 定义不同的宏(AKM_CAR / _4WD_CAR / …)。grep 默认抓到的是第一个
(阿克曼),但我的车是四驱,必须用 _4WD_CAR。用错宏会导致一堆"未定义"错误,
因为对应车型的参数宏被 #if 条件挡掉了。务必确认你要的是哪个 target 的宏。
参与编译的源文件清单:
grep -aoE "<FilePath>[^<]+.c</FilePath>" USER/xxx.uvprojx | sort -u
三、第二步:写链接脚本(.ld)
按芯片的内存布局写。F407VE 的关键段:
_estack = 0x20020000; /* 栈顶 = 128K RAM 末尾 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (xrw): ORIGIN = 0x10000000, LENGTH = 64K
}
.isr_vector/.text/.rodata 放 FLASH,.data(>RAM AT> FLASH)和 .bss 放 RAM。
网上标准 STM32F4 GCC 链接脚本很多,套用即可。
小提醒:别手贱往脚本里加
/DISCARD/ { libc.a(*) }之类——会把标准库丢掉导致链接失败。
四、第三步:GCC 语法的启动文件
Keil 的 startup_stm32f40_41xxx.s 是 ARM 汇编语法,GCC 用的是 GNU AS 语法,不能直接用。
但中断向量表必须和原工程一致,否则中断会乱。
做法:从 Keil 启动文件里把向量表顺序提取出来,照搬到 GCC 模板里。
grep -aoE "DCD[[:space:]]+[A-Za-z0-9_]+_(Handler|IRQHandler)" CORE/startup_xxx.s
| sed -E 's/DCD[[:space:]]+//'
把这串 handler 名按顺序填进 GCC 启动文件的 .word ... 向量表,
再用弱符号把没实现的中断都指向 Default_Handler:
.macro IRQ handler
.weak handler
.thumb_set handler, Default_Handler
.endm
IRQ USART3_IRQHandler
...
Reset_Handler 里做标准三件事:拷 .data → 清 .bss → 调 SystemInit / __libc_init_array / main。
五、第四步:FreeRTOS 移植层换成 GCC 版
Keil 工程用的是 FreeRTOS/portable/RVDS/ARM_CM4F/port.c——RVDS 是 ARM 编译器的内联
汇编,GCC 编不了。别手写(汇编移植很容易错),直接下载官方对应版本的 GCC 移植文件。
先确认 FreeRTOS 版本(tasks.c 头部或 task.h),然后:
BASE="https://raw.githubusercontent.com/FreeRTOS/FreeRTOS/V9.0.0/FreeRTOS/Source/portable/GCC/ARM_CM4F"
curl -fsSL "$BASE/port.c" -o FreeRTOS/portable/GCC/ARM_CM4F/port.c
curl -fsSL "$BASE/portmacro.h" -o FreeRTOS/portable/GCC/ARM_CM4F/portmacro.h
Makefile 里用这个 GCC 版的 port.c,include 路径指向 GCC/ARM_CM4F。FreeRTOSConfig.h 里通常已经有 #define xPortPendSVHandler PendSV_Handler 之类的
映射,保持不动即可。
六、第五步:写 Makefile + 收拾语法差异
Makefile 本身不难(CPU flags、include、源文件列表、链接),重点是 ARMCC 与 GCC 的
语法差异,这是最耗时的部分。下面是我实际撞到的,基本覆盖了常见情况:
1. 头文件大小写(Linux 专属坑)
Windows/macOS 文件系统默认不区分大小写,Keil 工程里 #include "LED.h" 而真实文件
叫 LED.H、#include "MPU6050.h" 也对不上——在 Linux 上全都报 No such file。
挨个改不现实。我写了个脚本:扫描所有 #include 的拼写,给真实头文件按引用的拼写
建软链接:
# 伪代码:对每个 #include "X",若同目录存在大小写不同的真实文件,建一个 X 的软链
for inc in all_includes:
real = find_case_insensitive(inc)
if real and not exists(inc):
os.symlink(basename(real), inc)
一把解决(我这工程建了 ~130 个软链)。
2. .C 大写扩展名被当成 C++
有个文件叫 LED.C(大写扩展名)。两个问题:
- 提取文件列表时用
.c正则漏掉了它 → 链接报函数未定义 - 加进来后,GCC 默认把
.C当 C++ 编译,函数名被 name mangling
(LED_SetColor变成_Z12LED_SetColor...),C 文件链接时又找不到
解决:编译规则里对 .C 强制按 C 语言编:
$(BUILD)/%.o: %.C
$(CC) -c -x c $(CFLAGS) $< -o $@
(nm obj/LED.o 看到 mangled 名字是定位这个问题的关键。)
3. Keil 专用关键字 / 内建
| Keil 写法 | GCC 解决 |
|---|---|
__weak void Foo() |
-D'__weak=__attribute__((weak))' |
__packed struct |
-D'__packed=__attribute__((__packed__))' |
__nop() |
-D'__nop()=__NOP()'(映射到 CMSIS) |
__asm void WFI_SET(){ WFI; } 等 |
改写成 GCC 内联汇编 __asm volatile("wfi") |
__asm 函数手动改,例如设置主栈指针:
void MSR_MSP(u32 addr) { __asm volatile ("msr msp, %0" : : "r" (addr)); }
4. 去掉 ARMCC 专用宏
工程宏里的 __CC_ARM、__TARGET_FPU_VFP 是 ARM 编译器专用,GCC 下必须去掉,
否则 CMSIS 头文件会走进 ARMCC 的分支,编译出错。
5. 变长数组(VLA)
static const uint8_t len = sizeof(some_struct);
static uint8_t buffer[len]; // Keil 接受;GCC 报 "variably modified at file scope"
const 变量在 C 里不是编译期常量。改成 buffer[sizeof(some_struct)] 即可。
七、编译命令
工具链用 GCC 的 ARM 版。Linux 上 apt 直接装最省事(国内建议先把 apt 换国内镜像):
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi libnewlib-arm-none-eabi make
我一开始想在 macOS 上用
brew install --cask gcc-arm-embedded,结果工具链是从developer.arm.com下载的,国内网络反复中断装不上。换到一台配了清华 apt 源的
Linux 机器,一条命令几分钟搞定——这也是本文最后在 Linux 上编译的原因。
编译:
make clean && make
arm-none-eabi-size obj/firmware.elf # 确认体积没超 Flash/RAM
成功后会得到 .elf / .hex / .bin。看一眼 size 输出,确认 text(Flash 占用)和bss(RAM 占用)都在芯片容量内,基本就稳了。
烧录可以用 st-flash、OpenOCD,或厂商的一键下载软件(我这块板子用的是它配套的
FlyMcu,烧 .hex)。
八、小结
把一个 Keil 工程搬到 GCC,真正的工作量不在"写 Makefile",而在那一长串
ARMCC ↔ GCC 的语法/约定差异。按出现频率排,最常见的就是:
- 多 target 工程选错宏(编译前先确认)
- 头文件大小写(Linux 专属,批量建软链解决)
__weak/__packed/__nop等 Keil 关键字(-D预定义或改写).C被当 C++(-x c)- VLA、
__CC_ARM宏
整个过程虽然繁琐,但每一步都有明确的报错指引——编译器告诉你缺什么、哪行不对,
顺着改就行。而且好处很实在:之后改固件,命令行 make 一下就能编,不依赖 Windows
和 Keil 授权,CI 里也能跑。
最后提醒一句:自己重新编译的固件,刷之前确认有"刷回出厂固件"的退路(一键下载/SWD
都行),心里就踏实了。
环境:STM32F407VE / FreeRTOS V9.0.0 / arm-none-eabi-gcc 10.3.1 / Ubuntu 22.04