极海 APM32 使用 Nimmake 编译指南(详细版)
主线型号:APM32F103 / Cortex-M3 ,并给出 F4xx(Cortex-M4F)适配
构建方式:
arm-none-eabi-gcc+ Nimmake(Python 构建脚本)适用系列:APM32F10x、APM32F4xx、APM32F0xx、APM32A1xx、APM32E1xx 等支持 GCC/Makefile 的系列
1. 可行性结论
极海半导体(Geehy)APM32 系列基于 Arm Cortex-M0+/M3/M4F 内核,官方提供完整的 SDK、标准外设库、CMSIS 支持、启动文件、系统初始化代码,以及 Keil DFP 设备支持包(内含启动文件、头文件、Flash 算法等)。SDK 中 Eclipse 工程底层即使用 GNU Arm Embedded Toolchain,因此配合 xPack ARM GCC 工具链和 Nimmake 构建脚本,完全可以实现命令行裸机编译。
官方已提供的底层素材:
| 素材 | 来源 | 说明 |
|---|---|---|
| 启动文件 | SDK / DFP 包 | startup_apm32f10x_md.s / startup_apm32f4xx.s(ARM 汇编,GCC 可用) |
| 设备头文件 | SDK / DFP 包 | apm32f10x.h / apm32f4xx.h,寄存器定义 |
| 系统初始化 | SDK | system_apm32f10x.c / system_apm32f4xx.c,含 SystemInit() |
| CMSIS 核心 | SDK | core_cm3.h / core_cm4.h 等 |
| 标准外设库 | SDK | GPIO、USART、SPI、I2C、TIM、ADC 等 |
| 中间件 | SDK | FreeRTOS、FatFs、LwIP、USB 等 |
| Flash 算法 | DFP 包 | .flm 文件,用于 J-Link / 烧录器 |
需要自行补充的素材:
| 素材 | 说明 |
|---|---|
GCC 链接脚本(.ld) |
官方 DFP 主要提供 Keil .sct 格式,需自行编写或从 Eclipse 工程中提取 .ld |
| GCC 版启动文件 | DFP 中的 .s 为 ARM 汇编语法,GCC 可直接使用,但需确认伪指令兼容 |
| 编译宏 | 需根据型号定义,如 APM32F10X_MD、USE_STDPERIPH_DRIVER |
2. 为什么用 Nimmake
| 痛点 | Makefile 写法 | Nimmake 写法 |
|---|---|---|
| 芯片参数 | 手写 -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard |
CORTEX_M4F_CFG.clone() 一行预设 |
| 标准库源文件列表 | 手动 glob 或逐行写,换型号就改 | Parties 自动扫描 Libraries/**/src/*.c |
| 换型号 | 改 CPU、链接脚本、启动文件、宏,多处同步 | 只改 Chip 预设和 Parties 路径 |
| 头文件路径 | -I 逐条写,漏一个就报错 |
Parties 自动收集 inc/ |
| 多目标 | 每个 target 写一套规则 | 多个 Program 对象即可 |
| 缓存 | 需手动配 ccache |
--cache 内置支持 |
| 跳转补全 | 无 | --compiledb 生成 compile_commands.json |
| 烧录命令 | 手写 openocd -f ... |
Phony 目标封装,nimmake flash |
3. 环境准备
3.1 安装 Nimmake
bash
pip install nimmake
nimmake --version
3.2 安装 ARM GCC 工具链
推荐使用 xPack ARM Embedded GCC:
bash
# macOS
brew install xpack-arm-none-eabi-gcc
# Windows (Scoop)
scoop install xpack-arm-none-eabi-gcc
验证:
bash
arm-none-eabi-gcc --version
arm-none-eabi-size --version
arm-none-eabi-objcopy --version
3.3 获取 APM32 SDK 素材
方式一:从极海官网下载 SDK
- 访问极海半导体官网(www.geehy.com)
- 进入对应芯片页面(如 APM32F103 / APM32F407)
- 下载 SDK (如
APM32F10x_SDK_V1.8.zip或APM32F4xx_SDK) - 解压后得到:
Libraries/--- 标准外设库、CMSIS、DeviceExamples/--- 示例工程(含 MDK/IAR/Eclipse 三平台)Middlewares/--- FreeRTOS、FatFs、LwIP 等中间件
方式二:从 DFP 包中提取
- 下载
Geehy.APM32F10x_DFP.x.x.x.pack或Geehy.APM32F4xx_DFP.pack - 用解压软件打开
.pack文件(本质是 ZIP) - 提取:
Device/Include/apm32f10x.hDevice/Source/GCC/startup_apm32f10x_md.s(如有 GCC 目录)Device/Source/system_apm32f10x.c
方式三:从 Eclipse 工程中提取 GCC 配置(最省事)
极海 SDK 的 Eclipse 工程基于 GCC 工具链,可直接从中提取 .ld 链接脚本和编译参数,是最省事的 GCC 兼容来源。
3.4 素材自检清单
-
startup_apm32f10x_md.s--- 启动文件(ARM 汇编,GCC 可用) -
apm32f10x.h--- 设备头文件 -
system_apm32f10x.c--- 系统初始化 -
core_cm3.h/core_cm4.h--- CMSIS 核心头文件 - 标准外设库
Libraries/APM32F10x_StdPeriphDriver/目录 - 链接脚本
apm32f10x_flash.ld(需自行编写,见第 5 章) -
arm-none-eabi-gcc可执行
4. 工程目录结构
推荐按重编频率分层:
apm32-nimmake/
|-- Nimmake.py # 构建脚本(唯一需要维护的文件)
|-- App/ # 应用代码(最常改)
| |-- inc/main.h
| |-- src/main.c
|-- Device/ # 启动 + 系统初始化(几乎不改)
| |-- startup_apm32f10x_md.s
| |-- system_apm32f10x.c
|-- Libraries/ # 标准外设库(几乎不改)
| |-- APM32F10x_StdPeriphDriver/
| |-- inc/ (apm32f10x.h / _gpio.h / _usart.h ...)
| |-- src/ (apm32f10x_gpio.c / _usart.c ...)
|-- CMSIS/ # CMSIS 核心(几乎不改)
| |-- Include/core_cm3.h
| |-- Device/apm32f10x.h
|-- ld/ # 链接脚本
| |-- apm32f10x_flash.ld
|-- tools/ # 烧录脚本(可选)
|-- openocd_apm32.cfg
5. 链接脚本编写
以下为 APM32F103RCT6(256 KB Flash + 48 KB SRAM)示例:
ld
/* ld/apm32f10x_flash.ld */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 48K
}
_estack = ORIGIN(SRAM) + LENGTH(SRAM);
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
. = ALIGN(4);
_etext = .;
} > FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data.*)
. = ALIGN(4);
_edata = .;
} > SRAM AT > FLASH
_sidata = LOADADDR(.data);
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > SRAM
. = ALIGN(4);
_end = .;
}
注意: APM32 Flash 起始地址为
0x08000000(与 STM32 一致),不同型号 Flash/SRAM 大小不同,需根据具体芯片调整LENGTH。
6. 完整 Nimmake.py
python
# Nimmake.py
from nimmake import *
# --- 1. 芯片预设 ---
Chip = CORTEX_M3_CFG.clone()
Chip.CPU = "cortex-m3"
Chip.FPU = None
Chip.ABI = "soft"
Chip.MACROS = [
"APM32F10X_MD", # 型号宏,决定头文件中寄存器定义
"USE_STDPERIPH_DRIVER", # 使用标准外设库
]
# --- 2. 工具链 ---
Toolchain = Update(Chip)
Refresh()
# --- 3. 编译选项 ---
CFLAGS = [
"-O2", "-g",
"-ffunction-sections", "-fdata-sections",
"-Wall", "-Wno-unused-function",
]
LDFLAGS = [
"-T" + ROOT + "/ld/apm32f10x_flash.ld",
"-Wl,--gc-sections",
"-Wl,-Map=" + BUILD + "/firmware.map",
"--specs=nano.specs",
"--specs=nosys.specs",
]
# --- 4. Party:标准外设库(静态库) ---
StdPeriph = Party("StdPeriph")
StdPeriph.Sources = Glob("Libraries/APM32F10x_StdPeriphDriver/src/*.c")
StdPeriph.Includes = [
"Libraries/APM32F10x_StdPeriphDriver/inc",
"CMSIS/Include",
"CMSIS/Device",
]
StdPeriph.BuildType = "staticlib"
# --- 5. Party:Device(启动文件 + 系统初始化) ---
Device = Party("Device")
Device.Sources = [
"Device/startup_apm32f10x_md.s",
"Device/system_apm32f10x.c",
]
Device.Includes = [
"Libraries/APM32F10x_StdPeriphDriver/inc",
"CMSIS/Include",
"CMSIS/Device",
]
Device.Dependencies = [StdPeriph]
# --- 6. Party:App(应用代码) ---
App = Party("App")
App.Sources = Glob("App/src/*.c")
App.Includes = [
"App/inc",
"Libraries/APM32F10x_StdPeriphDriver/inc",
"CMSIS/Include",
"CMSIS/Device",
]
App.Dependencies = [Device]
# --- 7. Program ---
Program = Prog("firmware")
Program.Parties = [App]
Program.CFLAGS = CFLAGS
Program.LDFLAGS = LDFLAGS
# --- 8. 后处理:生成 bin + 打印体积 ---
@AfterBuild
def post_build(ctx):
elf = ctx.Output
Run(f"arm-none-eabi-objcopy -O binary {elf} {elf.replace('.elf','.bin')}")
Run(f"arm-none-eabi-objcopy -O ihex {elf} {elf.replace('.elf','.hex')}")
Run(f"arm-none-eabi-size {elf}")
# --- 9. 烧录目标 ---
@Phony
def flash(ctx):
elf = ctx.Program.Output
Run("openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg "
"-c init -c 'reset halt' "
f"-c 'flash write_image erase {elf}' "
"-c 'reset run' -c exit")
@Phony
def gdb(ctx):
Run(f"arm-none-eabi-gdb {ctx.Program.Output}")
@Phony
def erase(ctx):
Run("openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg "
"-c init -c 'reset halt' -c 'stm32f1x mass_erase 0' -c exit")
DefaultTarget(Program)
7. 逐段解析
7.1 芯片预设
python
Chip = CORTEX_M3_CFG.clone()
Chip.CPU = "cortex-m3"
Chip.FPU = None
Chip.ABI = "soft"
APM32F103 是 Cortex-M3,无 FPU。若使用 APM32F407(M4F),则改为 CORTEX_M4F_CFG,FPU = "fpv4-sp-d16",ABI = "hard"。
7.2 编译宏
python
Chip.MACROS = [
"APM32F10X_MD", # 型号宏,决定 Flash/SRAM 大小
"USE_STDPERIPH_DRIVER", # 启用标准外设库
]
APM32F10X_MD表示 Medium Density(128~256KB Flash),小容量用APM32F10X_LD,大容量改为APM32F10X_HD。
7.3 链接脚本
python
LDFLAGS = [
"-T" + ROOT + "/ld/apm32f10x_flash.ld",
"-Wl,--gc-sections",
"--specs=nano.specs",
"--specs=nosys.specs",
]
nano.specs:使用 newlib-nano,显著减小体积。nosys.specs:提供_sbrk/_write等系统调用的空实现。
7.4 APM32 专属注意点
1. SWD 调试接口默认关闭
APM32F103 上电复位后 SWD 口默认关闭,必须通过 Option Byte 编程手动开启,否则首次无法连接调试器。解锁方式:
- 使用 Keil Flash Download,在
Settings -> Flash Download -> Program/erase options中勾选 "Enable SWD" 并点击 "Update" - 或使用 J-Link Commander 执行
exec SetTargetInterface SWD后再烧录
2. SysTick 时钟源默认不同
APM32 的 SysTick->CTRL 复位后 CLKSOURCE 位默认为 0(外部时钟源),而 STM32 默认为 1(内核时钟源)。使用 FreeRTOS 时需在 SysTick_Config() 之后强制设置:
c
SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // 强制选择内核时钟
3. NVIC 优先级分组
APM32F103 的 NVIC 优先级分组策略默认为 Group 4(仅抢占优先级,无子优先级),而 STM32F103 默认为 Group 2。若未在 SystemInit() 中调用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4),则 NVIC_SetPriority() 设置的优先级值会被截断。
4. _estack 与链接脚本
链接脚本中必须定义 _estack,启动文件中通过它初始化栈指针。如果 SRAM 大小与实际芯片不符,上电即 HardFault。
5. VTOR 向量表偏移
做 Bootloader + App 时,App 中需重新设置 VTOR:
c
SCB->VTOR = APP_FLASH_OFFSET;
8. 常用命令
| 命令 | 说明 |
|---|---|
nimmake |
编译默认目标 |
nimmake --dry-run |
预览命令,不实际执行 |
nimmake --compiledb |
生成 compile_commands.json |
nimmake --cache |
启用编译缓存 |
nimmake -n |
仅编译,不链接 |
nimmake -c |
清理构建目录 |
nimmake flash |
烧录固件 |
nimmake erase |
擦除芯片 |
nimmake gdb |
启动 GDB |
9. 烧录方案
9.1 OpenOCD + CMSIS-DAP / J-Link
APM32F103 与 STM32F103 内存布局一致,可直接使用 stm32f1x.cfg:
bash
openocd -f interface/cmsis-dap.cfg \
-f target/stm32f1x.cfg \
-c "program firmware.elf verify reset exit"
APM32F407 则使用 stm32f4x.cfg。
9.2 PyOCD
APM32F411 Tiny 等板载 CMSIS-DAP 调试器,可使用 PyOCD:
bash
pyocd flash -t apm32f411xx firmware.hex
9.3 J-Link
需先安装极海 Flash 算法(.flm)到 J-Link 安装目录:
bash
JLinkExe -device APM32F103RC -if SWD -speed 4000 -CommanderScript flash.jlink
flash.jlink 内容:
h
loadbin firmware.bin 0x08000000
r
g
q
9.4 官方 ISP 工具
极海提供 GeehyProg 烧录工具和 APM32 MCU ISP 串口下载工具,支持 UART/USB ISP 模式。将 BOOT0 拉高后上电即可进入 ISP。
10. printf 重定向
使用 nosys.specs 时,需重写 _write 才能通过 USART 输出:
c
// App/src/uart_printf.c
#include <stdio.h>
#include <unistd.h>
#include "apm32f10x.h"
#include "apm32f10x_usart.h"
int _write(int fd, char *ptr, int len) {
if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
for (int i = 0; i < len; i++) {
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET);
USART_TxData(USART1, ptr[i]);
}
return len;
}
return -1;
}
若使用
nosys.specs且不重写_write,printf 会静默丢弃输出,不会报错。
11. 型号迁移
| 型号 | 内核 | 预设 | 宏 | Flash | SRAM | 备注 |
|---|---|---|---|---|---|---|
| APM32F030 | M0+ | CORTEX_M0PLUS_CFG |
APM32F030 |
32K~64K | 4K | 无 FPU,ABI=soft,FPU=None |
| APM32F035 | M0+ | CORTEX_M0PLUS_CFG |
APM32F035 |
64K | 8K | 无 FPU |
| APM32F103 LD | M3 | CORTEX_M3_CFG |
APM32F10X_LD |
16K~32K | 10K | 小容量 |
| APM32F103 MD | M3 | CORTEX_M3_CFG |
APM32F10X_MD |
64K~256K | 48K | 中容量,主力型号 |
| APM32F103 HD | M3 | CORTEX_M3_CFG |
APM32F10X_HD |
384K~512K | 64K | 大容量 |
| APM32F405 | M4F | CORTEX_M4F_CFG |
APM32F405 |
512K~1M | 192K | FPU=fpv4-sp-d16,ABI=hard |
| APM32F407 | M4F | CORTEX_M4F_CFG |
APM32F407 |
512K~1M | 192K~256K | 同上 |
| APM32F411 | M4F | CORTEX_M4F_CFG |
APM32F411 |
128K~256K | 96K | 小封装高性价比 |
| APM32A103 | M3 | CORTEX_M3_CFG |
APM32A103 |
128K~256K | 48K | 车规,引脚兼容 F103 |
迁移时只需修改 Nimmake.py 中的三处:
python
Chip = CORTEX_M4F_CFG.clone() # 1. 换内核预设
Chip.CPU = "cortex-m4"; Chip.FPU = "fpv4-sp-d16"; Chip.ABI = "hard"
Chip.MACROS = ["APM32F407", "USE_STDPERIPH_DRIVER"] # 2. 换宏
LDFLAGS = ["-T" + ROOT + "/ld/apm32f407_flash.ld", ...] # 3. 换链接脚本
M0+ 系列特别注意 :无 FPU,必须把
FPU = None、ABI = "soft",且链接脚本 Flash/SRAM 很小,注意代码裁剪。
12. Bootloader + App
12.1 Flash 分区示例
| 区域 | 地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16 KB | 引导程序 |
| App | 0x08004000 | 剩余 | 应用程序 |
12.2 App 链接脚本修改
ld
/* ld/apm32f103_app.ld */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 240K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 48K
}
12.3 App 中设置 VTOR 与跳转
c
// App/main.c 最前
int main(void) {
SCB->VTOR = 0x08004000; // 偏移向量表
SystemInit();
// ...
}
// Bootloader 中跳转到 App
void jump_to_app(uint32_t app_addr) {
uint32_t app_sp = *(__IO uint32_t *)app_addr;
uint32_t app_pc = *(__IO uint32_t *)(app_addr + 4);
void (*app_entry)(void) = (void (*)(void))app_pc;
__set_MSP(app_sp);
app_entry();
}
12.4 Nimmake 多目标
python
Bootloader = Prog("bootloader")
Bootloader.Parties = [App_BL]
Bootloader.LDFLAGS = ["-T" + ROOT + "/ld/apm32f103_boot.ld"]
AppFW = Prog("app")
AppFW.Parties = [App_Main]
AppFW.LDFLAGS = ["-T" + ROOT + "/ld/apm32f103_app.ld"]
DefaultTarget(Bootloader) # 也可 DefaultTarget([Bootloader, AppFW]) 同时编译
13. FreeRTOS 集成
将 FreeRTOS 作为独立 Party:
python
FreeRTOS = Party("FreeRTOS")
FreeRTOS.Sources = [
"Middlewares/FreeRTOS/source/croutine.c",
"Middlewares/FreeRTOS/source/list.c",
"Middlewares/FreeRTOS/source/queue.c",
"Middlewares/FreeRTOS/source/tasks.c",
"Middlewares/FreeRTOS/source/timers.c",
"Middlewares/FreeRTOS/portable/GCC/ARM_CM3/port.c",
"Middlewares/FreeRTOS/portable/MemMang/heap_4.c",
]
FreeRTOS.Includes = [
"Middlewares/FreeRTOS/include",
"Middlewares/FreeRTOS/portable/GCC/ARM_CM3",
]
踩坑点:
- 中断向量重定向 :
port.c中xPortPendSVHandler/xPortSysTickHandler必须与启动文件中的PendSV_Handler/SysTick_Handler向量名一致。若启动文件用的是xPortPendSVHandler命名,则无需改;否则需在 App 中断入口里转发。 - SysTick 时钟源:见 7.4 第 2 点,APM32 必须强制内核时钟。
- heap_4 的
_sbrk:需在链接脚本里定义_heap_start/_heap_end,或提供_sbrk实现。
14. FAQ
| 问题 | 原因 / 解决 |
|---|---|
启动文件报 Error: bad register name |
拷到的是 Keil/IAR 版启动文件,换成 GCC 版 startup_*_gcc.s 或 Eclipse 工程里的 .s |
undefined reference to _sbrk |
用了 nano.specs 但没加 --specs=nosys.specs,或用了 heap_4 未实现 _sbrk |
| printf 无输出但不报错 | 未重写 _write(见第 10 章) |
| 上电即 HardFault | 链接脚本 SRAM _estack 与实际芯片不符 |
-T 没生效 / Flash 地址错 |
链接脚本路径用了相对路径且没在工程根目录执行,改用 ROOT 绝对拼接 |
| 调试器连不上(SWD 无法识别) | APM32 SWD 默认关闭,需先通过 Option Byte 开启(见 7.4) |
| 用 STM32 cfg 烧录失败 / ID 不认 | APM32 Device ID 与 STM32 不同,OpenOCD 可能报 unknown id,可加 -c "set CHIPID ..." 或用官方工具 |
| ABI mismatch 链接告警 | 库和 App 用了不同 -mfloat-abi,全局统一 ABI |
| FreeRTOS 任务不切换 | SysTick 时钟源未设内核时钟;或 PendSV 优先级未设为最低 |
| 换 F4xx 后链接报 FPU 相关错 | 忘记改 FPU / ABI 为 hard |
| map 文件没生成 | LDFLAGS 里加 -Wl,-Map=...(注意逗号非空格) |
15. 体积分析
bash
arm-none-eabi-size firmware.elf # text / data / bss 三段
arm-none-eabi-nm -S --size-sort firmware.elf | tail -20 # 最大符号
裁剪手段:
-ffunction-sections -fdata-sections+-Wl,--gc-sections--specs=nano.specs(newlib-nano)- 标准外设库按需编译,只把用到的
.c加进Sources -Os优化体积- 通过
firmware.map定位大对象
16. 最小可跑 Checklist
- 装好
arm-none-eabi-gcc与pip install nimmake - 拿到 GCC 版启动文件、
system_*.c、设备头文件、标准外设库 - 编写/提取
apm32f10x_flash.ld,确认_estack与 Flash 起始0x08000000 - 写好
Nimmake.py,设置正确的Chip预设与MACROS nimmake --dry-run检查编译/链接命令是否符合预期nimmake编译,生成firmware.elf/.bin/.hex- 确认 Option Byte 已开启 SWD
nimmake flash烧录(或 J-Link / 官方 ISP)- 复位运行,LED 闪 / 串口打印即成功
本指南与《灵动微 MM32》《雅特力 AT32》《国民技术 N32》《华大 HC32》四篇系列保持一致结构,可直接横向对比迁移。