Press "Enter" to skip to content

把厂商的 Keil STM32 固件改用 GCC 在 Linux 上编译:一次完整的移植实录

最近在折腾一台 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 的语法/约定差异。按出现频率排,最常见的就是:

  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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注