**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.hMakefile 里用这个 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 的语法/约定差异**。按出现频率排,最常见的就是:
1. 多 target 工程选错宏(编译前先确认)
2. 头文件大小写(Linux 专属,批量建软链解决)
3. __weak / __packed / __nop 等 Keil 关键字(-D 预定义或改写)
4. .C 被当 C++(-x c)
5. VLA、__CC_ARM 宏
整个过程虽然繁琐,但每一步都有明确的报错指引——编译器告诉你缺什么、哪行不对,
顺着改就行。而且好处很实在:**之后改固件,命令行
make 一下就能编,不依赖 Windows和 Keil 授权**,CI 里也能跑。
最后提醒一句:自己重新编译的固件,刷之前确认有"刷回出厂固件"的退路(一键下载/SWD
都行),心里就踏实了。
---
*环境:STM32F407VE / FreeRTOS V9.0.0 / arm-none-eabi-gcc 10.3.1 / Ubuntu 22.04*