极海 APM32 使用 Nimmake 编译指南

极海 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

  1. 访问极海半导体官网(www.geehy.com)
  2. 进入对应芯片页面(如 APM32F103 / APM32F407)
  3. 下载 SDK (如 APM32F10x_SDK_V1.8.zip 或 APM32F4xx_SDK)
  4. 解压后得到:
    • Libraries/ --- 标准外设库、CMSIS、Device
    • Examples/ --- 示例工程(含 MDK/IAR/Eclipse 三平台)
    • Middlewares/ --- FreeRTOS、FatFs、LwIP 等中间件

方式二:从 DFP 包中提取

  1. 下载 Geehy.APM32F10x_DFP.x.x.x.pack 或 Geehy.APM32F4xx_DFP.pack
  2. 用解压软件打开 .pack 文件(本质是 ZIP)
  3. 提取:
    • Device/Include/apm32f10x.h
    • Device/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. 烧录方案

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

需先安装极海 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",
]

踩坑点:

  1. 中断向量重定向 :port.c 中 xPortPendSVHandler / xPortSysTickHandler 必须与启动文件中的 PendSV_Handler / SysTick_Handler 向量名一致。若启动文件用的是 xPortPendSVHandler 命名,则无需改;否则需在 App 中断入口里转发。
  2. SysTick 时钟源:见 7.4 第 2 点,APM32 必须强制内核时钟。
  3. 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

  1. 装好 arm-none-eabi-gcc 与 pip install nimmake
  2. 拿到 GCC 版启动文件、system_*.c、设备头文件、标准外设库
  3. 编写/提取 apm32f10x_flash.ld,确认 _estack 与 Flash 起始 0x08000000
  4. 写好 Nimmake.py,设置正确的 Chip 预设与 MACROS
  5. nimmake --dry-run 检查编译/链接命令是否符合预期
  6. nimmake 编译,生成 firmware.elf / .bin / .hex
  7. 确认 Option Byte 已开启 SWD
  8. nimmake flash 烧录(或 J-Link / 官方 ISP)
  9. 复位运行,LED 闪 / 串口打印即成功

本指南与《灵动微 MM32》《雅特力 AT32》《国民技术 N32》《华大 HC32》四篇系列保持一致结构,可直接横向对比迁移。

相关推荐
朝朝辞暮i37 分钟前
C++ 第 26 课:对象、指针与 ->
java·开发语言·c++
慢云智慧空间1 小时前
从智能终端到空间AI,慢云科技如何重新定义智慧建筑的核心能力?
人工智能·python·科技
2601_962885721 小时前
如何用 Python 做股价历史分位选股?(筛出处于历史低位的股票)
开发语言·python
倔强的石头1061 小时前
【Linux指南】动静态库系列(十一):PLT 与延迟绑定:第一次调用动态库函数时发生了什么
linux·人工智能·python
梦帮科技1 小时前
vLLM / TensorRT-LLM 极限推理:PagedAttention 细粒度物理页表管理与连续批处理(Continuous Batching)实战
数据结构·人工智能·分布式·python·深度学习·算法·vllm
殷色玫瑰2 小时前
C++入门基础复习:从命名空间到引用与nullptr,一篇重新捡回C++基础
java·开发语言·c++
小神兵2 小时前
对电压源与电流源的的简单理解
单片机·嵌入式硬件·物联网
(Charon)2 小时前
【C++面试】手写线程池:任务队列、工作线程与优雅退出
开发语言·c++
言乐62 小时前
Python概括后端原理
开发语言·python·django·virtualenv·pygame