本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

我用过的几种 32 位单片机:
特性、用途与编程差异

这不是一篇选型指南,也不评比优劣。我只是把自己实际用过、写过程序的几个系列摆在一起, 从“能用什么”和“写起来有什么不一样”两个角度做对照:内核档位决定了哪些运算要自己想办法, 存储档位决定了架构能摆多大,库和时钟树的写法决定了移植时要重写多少东西。 如果你也在多个平台之间来回切换,希望这篇能让你少踩几个我已经踩过的坑。

32 位 MCU Cortex-M0+ Cortex-M4F 平台移植 2017 – 2026 已发布

一、先说明这篇笔记不做什么

先说清楚边界,免得读的人误会。这篇笔记不做排名,也不给推荐结论, 更不能替代任何一份数据手册。我写了这么多年固件,越来越不敢说“某某芯片更好”—— 同一颗芯片,用在只需要两个串口和一路 ADC 的小板上是够用的,用在要跑以太网加四路总线的板子上就是不够的; 同一颗芯片,配一套顺手的库能两天跑通,配一套不熟的库能卡一周。“好不好用”很大程度上取决于你要做的事和你会用的库, 而不是芯片本身的绝对高低。

这篇笔记里出现的所有型号,都只有一个用途:说明某一类技术方案长什么样。 文中型号仅用于说明技术方案,与相关厂商无隶属或授权关系,所有参数请以各厂商最新数据手册为准。 我也不会写任何关于选型结论与商务信息的判断——这些不在我的笔记范围内,我也没有能力给出可靠结论。

时间跨度上,这篇笔记对应的是 2017 到 2026 这九年多里我陆续接触过的平台:从早期的 8 位机和 STM32F1 起步,中间用过 STM32F0、F4、G0,也用过小华的 HC32 系列和兆易的 GD32 系列。 下面的内容全部来自我自己写过的工程、读过的工程文件与源码,凡是我没确认的,我会直接写“未确认”。

二、我实际用过的平台一览

先上表。这张表里的“典型资源档”来自各工程的工程文件与链接脚本, 不是我抄的选型手册;主频一栏是我在源码里核对过时钟配置之后写的。

厂商系列内核典型资源档(Flash / RAM)主频我用在哪类项目
意法半导体(ST)STM32F103Cortex-M3 128 KB / 20 KB72 MHz 档 早期原型主板,HAL 库 + CubeMX 分层工程
意法半导体(ST)STM32F030Cortex-M0 32 KB / 4 KB、64 KB / 8 KB48 MHz 档 读卡器、段码液晶键盘板、三相变频驱动板
意法半导体(ST)STM32G030Cortex-M0+ 64 KB / 8 KB 16 MHz 直通;另一个工程跑 32 MHz 电池检测单元、总压总流采集单元、带 Bootloader 的从站节点
意法半导体(ST)STM32F407Cortex-M4F 512 KB / 192 KB 两个工程分别是 168 MHz 与 72 MHz 电池管理主控、产线自动化检测工装
国产 F407 兼容型号器件名与厂商字段都是兼容命名Cortex-M4F 1 MB / 192 KB(112 KB + 80 KB 两段)168 MHz 档 板间以太网 + 四路 RS485 的集控板与面板板
小华半导体HC32F460Cortex-M4F 256 KB / 192 KB、512 KB / 188 KB(工程声明) 200 MHz(12 MHz 晶振经 MPLL) FreeRTOS 多任务主板、产线工装、LED 屏控制卡、手持监测仪
小华半导体HC32F030 / HC32L136Cortex-M0+ 64 KB / 8 KB 48 MHz 档(L136 用内部高速 RC 经 PLL) 液位采集小板、Modbus 从站板、读卡器、面板主控
小华半导体HC32F005 / HC32F003Cortex-M0+ 32 KB / 4 KB、16 KB / 2 KB24 MHz 扫码转接仲裁板、双通道 PWM 脉冲发生小板
兆易创新GD32E103Cortex-M4F 64 KB / 20 KB、128 KB / 32 KB120 MHz 档 IC 卡收费主板,以及 8 KB 的引导程序
兆易创新GD32F303Cortex-M4F 256 KB / 48 KB120 MHz 档 加注机主板(裸机版与 FreeRTOS 版各一套,同源不同架构)
兆易创新GD32F425Cortex-M4F 512 KB / 192 KB + 64 KB TCM 外部 12 MHz 晶振(倍频结果我没在源码里逐行核对,未确认) 工业变送器 + RS485/CAN 双通道 OTA 引导

怎么确认一颗芯片到底是多少资源

这件事听起来很傻,但我确实在这上面栽过。工程文件里能表达“这颗芯片有多大”的地方至少有三处: 器件型号字段、IROM/IRAM 的地址与长度、以及实际链接的启动文件。 这三处经常不一致,而且不一致的时候编译照样通过。

我遇到过的两种:一种是工程声明的器件是 F003C4PA(按命名是 16 KB Flash / 2 KB RAM), 但工程里实际列出的启动文件和系统文件却是同族 F005 的,最终型号我只能标“未确认”; 另一种是器件字段写的是一个封装型号,输出文件名却写着同系列另一个容量的型号, 属于模板留下来的痕迹。这两种情况都不会报错,但如果你按错的容量去规划缓冲区, 问题会在链接阶段——或者更糟,在跑起来之后——才暴露。

我后来养成的习惯是:先看链接脚本或 IROM/IRAM 的地址长度,再看启动文件叫什么,最后才看型号字段。 三处一致才敢按那个容量写代码。这条我放进了本文最后一节的核对清单里。

按内核档位横向排列:M0、M0+、M3、M4、M4F,每个档位下列出用过的系列,标注典型资源档(Flash/RAM)与主
图 1 · 平台谱系图

三、内核档位带来的真实差别

外壳、引脚、Flash 容量都可以换,内核档位是换不掉的。我用的平台正好横跨两档: Cortex-M0/M0+(ARMv6-M)和 Cortex-M3/M4/M4F(ARMv7-M)。 这两档的差别不是“快一点慢一点”,而是有些事在低档位上根本做不到,只能绕。

能力Cortex-M0 / M0+Cortex-M3 / M4 / M4F在代码里表现成什么
硬件除法 没有除法指令,除法进软件库 有,单次约几个到十几个周期 M0+ 上一次除法抵几十条普通指令,热路径里的除法要改成移位、查表或先乘后除
单精度浮点 没有 FPU,所有浮点走软件浮点库 M4F 有单精度 FPU(M4 不带 F) M0+ 工程里我基本不用浮点做运算,改成放大整数;M4F 上也要工程显式使能 FPU 才会真的用上
位带 不支持,没有位带别名区 支持,可对单个比特做原子读改写 M4F 上能用位带做“不关中断也能改标志位”;M0+ 只能读-改-写,改共享标志就得关中断
中断优先级 2 位,只有 4 级 常见 4 位,16 级且支持抢占/子优先级分组 M0+ 上写“设置优先级分组”的代码实际是空操作,按 M4 习惯设计的嵌套关系要重新评估
指令集 ARMv6-M,Thumb 的一个子集 ARMv7-M,指令更全 按位操作、内联汇编、编译器的位操作内建函数,换核之后可能直接编不过

M4F 不等于“浮点一定在用”

这一条我单独拎出来说,因为我自己被它骗过。M4F 只是“有 FPU 这个硬件”, 编译器要不要用它,取决于三件事:工程里有没有定义表示“本芯片有 FPU”的宏、 启动后有没有把协处理器使能位打开、以及编译选项里的浮点 ABI 选的是什么。 我见过的一个 M4F 工程在 `main` 里显式去写协处理器访问控制寄存器来使能 FPU, 另一个工程则是在编译宏里定义 `__FPU_PRESENT`——两种做法的目的是一样的: 不显式打开,编出来的仍然是软件浮点,你会在“明明有 FPU”的芯片上得到软件浮点的速度和体积。

反过来,M0+ 上没有 FPU 也不是不能做小数运算。我的做法是把量纲放大成整数: 金额用分、升数用 0.01 升、角度用 0.1 度,全链路整数运算,只在显示或上报时插入小数点。 这套做法在一个双枪加注机的工程里从头用到尾,连取整都是“先放大到千分位再四舍五入”的整数写法。 代价是每引入一个新物理量,都要先想清楚它的最小单位。

/* 没有 FPU、也没有硬件除法时的定点惯用法(按个人理解重写的最小片段)
   单位由工程自己定义,这里用“放大 k 倍”的形式表示,不写具体量纲 */
static uint32_t scaled_mul(uint32_t a, uint32_t b, uint32_t k)
{
    /* 先乘后除:中间结果提到 64 位,避免先除造成的精度丢失与溢出 */
    return (uint32_t)(((uint64_t)a * b) / k);
}

主频高不等于“什么都能做”

一颗 200 MHz 的 M4F 和一颗 24 MHz 的 M0+,主频差了八倍多,但这八倍并不能直接换算成“能做的事多八倍”。 主频上去之后,Flash 等待周期、SRAM 等待周期、总线分频都要跟着配: 我用过的 200 MHz 工程里,Flash 控制器设了 5 个等待周期,SRAM 也设了 2 个等待周期, 同时把不同总线的分频分别配成 1、2、4 分频。 这些配置错一个,现象通常是“偶发跑飞”或者“某一路外设时好时坏”,而不是干脆不启动—— 这也是为什么我在换平台时把时钟配置放在第一位核对。

反过来说,48 MHz 的 M0 也完全够做硬实时的活。我有一个三相变频驱动的工程就是 48 MHz 的 M0, PWM 载波中断周期是 111 µs,一次中断大约 5300 个周期——只要中断里全是整数运算、 没有除法没有浮点,这点时间足够把电流环算完。 决定能不能做实时的是“中断里有多少条指令”,不是主频的数字。

位带这件事值得多说一句。我在一个 LED 屏控制卡上见过“位带式”的写法: 直接用寄存器基址加偏移去置位/复位某一位,再用“偏移乘以 10”的方式选不同的端口。 这类写法在 M4F 上没问题,甚至可以借助位带区拿到原子性;但搬到 M0+ 上, 同样的意图只能退回读-改-写,而读-改-写一旦和中断共用同一组寄存器,就会出现“设置了但没生效”。 如果你在 M0+ 上要共享标志位,就老老实实关中断改,或者把标志集中到自己的变量里。

/* Cortex-M3/M4 的位带别名:把“某个字的某一位”映射成一个可原子访问的 32 位字。
   别名地址 = 别名区基址 + (目标地址 - 位带区基址) * 32 + 位号 * 4
   这里只写形状,两个基址都由工程自己的宏给出(按个人理解重写的最小片段) */
#define BITBAND_ALIAS(alias_base, region_base, addr, bit)   \
    ((alias_base) + (((uint32_t)(addr) - (region_base)) << 5) + ((bit) << 2))
横轴为 RAM 容量(对数刻度),纵轴为“适合的软件框架”,在图上标出裸机分时、状态机、RTOS 三段的适用范围
图 2 · 资源档位与框架选择关系图

四、存储器与资源档位如何决定架构

上一节讲的是“内核能做什么”,这一节讲“资源够摆什么”。我的经验是: Flash 决定功能能装多少,RAM 决定架构能摆多大, 而 RAM 这一项,往往是在项目一开始就被低估、到中期才发现不够的那一项。

资源档我用过的型号我的默认架构为什么
16~32 KB Flash / 2~4 KB RAM HC32F003、HC32F005 纯前后台 + 一个定时器做全部计时 一块扫码转接板要在 4 KB RAM 里放四个 256 字节缓冲(两个串口各一收一发外加暂存),刚好塞下;放不下 RTOS 的内核对象和任务栈
64 KB / 8 KB HC32F030、HC32L136、STM32G030 时间片轮询 + 若干状态变量 够放 20 点采样缓冲、参数副本与状态机;不够放内核、定时器任务和多份任务栈
128~256 KB / 20~48 KB GD32E103、GD32F303 时间片,或 FreeRTOS(堆给到 20 KB 上下) 可以开 RTOS,但优先级档数(我见过只用 6 档的)、堆大小与任务栈都要一项项算
512 KB / 188~192 KB HC32F460、GD32F425、STM32F407、国产 F407 兼容型号 FreeRTOS 多任务 多任务、通信缓冲、字库/点阵缓存、日志缓冲可以同时放;也有余量做运行时间统计这类调试设施

RAM 是先被算光的,不是先被用光的

点阵显示那次给我的印象最深。当时算的是 64×64 点阵、128 个字,一页缓存就是 64 KB, 想开双页缓冲就是 128 KB——这个数字直接超过了 8 KB 档芯片的整片 SRAM,也超过了 192 KB 档 扣掉协议栈、任务栈、堆以后的可用余量。最后的结论不是“优化算法”, 而是把显示规格降到 48×48 点、128 个字,让一页缓存落到几十 KB 的量级。 结论是:内存预算要在画架构之前算,而不是在写完功能之后调。

这件事和“什么时候上 RTOS”是同一个问题的两面。我统计过自己手上两批工程: 一批 7 个工程里,FreeRTOS 只出现在 4 个 M4F 大容量工程里, 而且都是同一套骨架(1 kHz 节拍、最小任务栈相同、栈溢出检测开到第 2 级、 定时器任务栈是最小栈的两倍、静态与动态分配同时开、系统节拍中断里把厂商的节拍处理与内核的节拍处理串起来), 差别只在堆大小(10、16、20、30 KB 四档)和优先级档数(16 或 32); 另外 7 个工程里,只有业务代码超过一万行(其中一个 Application 目录接近三万五千行) 或者需要严格优先级调度的那两个上了 FreeRTOS,8 KB RAM 这一档全部是裸机。

所以我的分界线不是“功能多不多”,而是三个问题: 有没有会互相阻塞的慢操作(等模组应答、写外部 Flash、等总线仲裁); 有没有必须在一个节拍内响应的硬实时任务; 代码量是否已经超过一个人能记住的规模。 三个都是“否”,我就不上 RTOS——多一个内核,就多一层“现象在 A 处、根因在 B 处”的可能。

五、库与代码风格的三套体系

换平台真正花时间的,往往不是内核差异,而是库。我实际长期用过三套: 意法的标准外设库、意法的 HAL + CubeMX,以及小华的 DDL。 加上“不用库、直接写寄存器”这一派,实际上有四种写法。

体系我在哪些工程里见到典型写法换库时要改什么
标准外设库 STM32F0 系列、GD32E10x、GD32F30x 先开外设时钟 → 填一个初始化结构体 → 调 Init;中断入口自己写在中断文件里 时钟使能函数名、结构体字段名、位定义宏名
HAL + CubeMX STM32F103 主板、STM32G030 从站、STM32F407 工装 配置文件生成初始化函数;中断里调库的中断处理函数,业务写在回调里 句柄结构、回调函数名、生成代码的目录分层
厂商自研 DDL 小华全系(HC32F460 / F030 / L136 / F005) 用一个配置头文件里的开关裁剪要编译的外设模块;驱动按目录分门别类;函数命名是三段式 驱动目录结构、裁剪表、以及“同一个功能在 DDL 里叫什么”
寄存器直写 与上面三种混用(LED 屏扫描、移位寄存器驱动) 算出寄存器基址加偏移,直接读写 32 位寄存器 基址、偏移、位定义全部要重查一遍数据手册

这三套体系做同一件事的“形状”完全不同。开一个 GPIO 输出: 标准库是“开时钟、填结构体、初始化”三步;HAL 是“句柄 + 生成的初始化函数”; DDL 是“填一个 GPIO 初始化结构体,然后用置位/复位函数操作”。 这些差异查手册就能解决,真正会卡住的是中断入口与回调的写法: 标准库要求你自己在中断文件里写中断服务函数;HAL 要求中断服务函数里调用库的中断处理函数、 业务放到回调里;DDL 则是中断服务函数里调用厂商提供的回调注册点。 三种写法混在一个工程里时,最容易出现的现象是“中断确实触发了,但我的业务代码没进去”。

/* 外设访问骨架:只保留形状,地址与位定义都由工程宏给出(按个人理解重写) */
#define REG32(addr)   (*(volatile uint32_t *)(addr))

static inline void reg_set_bits(volatile uint32_t *reg, uint32_t mask)
{
    *reg |= mask;              /* 读-改-写:本身不是原子操作 */
}

static inline void reg_clear_bits(volatile uint32_t *reg, uint32_t mask)
{
    *reg &= ~mask;
}

寄存器直写和库函数混用,是我踩得最实在的一个坑。LED 屏扫描要的速度, 库函数一次调用几十个周期,所以那部分代码是直接算地址翻寄存器的; 但同一个文件里另一些地方又调用库的置位/复位函数。 问题出在库函数内部也是“读-改-写”:如果它读进来的是直写之前的值, 回写的时候就把直写的结果覆盖掉了。表现是“某些位偶尔不生效”,而且只在特定时序下复现。 我的处理办法很简单:同一组寄存器,要么全用库,要么全直写,不混着来。

六、时钟树:三种起点

时钟配置是换平台时最容易“看起来能跑、实际上全错”的一环。我把自己配过的时钟归纳成三种起点: 外部晶振经 PLL、内部高速 RC 经 PLL、外部晶振直通或低倍频。

起点我配过的例子好处要注意什么
外部晶振经 PLL 12 MHz 晶振经三级 MPLL(先分频、再倍频、再二分频)得到 200 MHz 精度高、温漂小,适合做串口和长周期计时 高主频下要同时配 Flash 与 SRAM 的等待周期、总线分频;晶振不起振就什么都不对外
内部高速 RC 经 PLL 一颗 M0+ 用内部高速 RC 直接倍频到 48 MHz,完全不依赖外部晶振 少一颗料、少两个引脚,小板上很实惠 频率精度与温漂都比晶振差,高波特率下要核算误差;不同批次的 RC 频率也有分散
外部晶振直通或低倍频 16 MHz 外部晶振直通;8 MHz 晶振乘 6 得到 48 MHz 简单,不用配多级 PLL PLL 的能力上限未必用满(有的系列 PLL 能上到 64 MHz,工程里只跑 16 MHz);APB 定时器时钟与 APB 时钟是否同频会直接影响 PWM 分辨率

还有一种情况是时钟配置根本不在你的源码里:CubeMX 生成的工程把时钟写在配置文件里, 而实际的 PLL 参数由生成代码决定;GD32 的库则把主频选项写在系统源文件里, 用一句“选择 120 MHz PLL + 外部晶振”的宏来切换,你自己的 `main` 里看不到倍频系数。 换平台时如果只在 `main` 里找时钟,就会漏掉这两个地方。

有一个细节我专门写下来提醒自己:那个 48 MHz 的变频工程,配置文件里只记了“PLL 源是外部晶振、倍频 6 倍、系统与 APB1 同为 48 MHz”, 但没有直接记晶振频率,我是按“乘 6 得 48 MHz”反推晶振是 8 MHz 的—— 这类反推在写笔记时必须标注清楚是推断,不能当成手册事实。

外部晶振失效了怎么办

兜底逻辑本身不难:启动时检查时钟就绪标志,超时就退回内部 RC,让系统先活下来。 难的是让外面看得出来。如果只是默默切到内部 RC 继续跑, 现场看到的现象会是“串口波特率全错”“定时偏慢”“通信一直 CRC 错”, 排查方向会被完全带偏。所以我现在的做法是两条:一是切换后在参数区(或某个只读寄存器) 留下“当前时钟源”和“是否发生过切换”的记录,上位机读得到; 二是初始化时不要写“死等标志位”的循环——晶振坏了就永远卡在那里, 连兜底的机会都没有。

另外提醒一句:改主频之后,所有软件延时都要重算。一个用空循环实现的毫秒延时函数, 在 48 MHz 上调好的循环次数,换到 24 MHz 的板子上就是两倍时长。 我见过最典型的后果是 RS485 方向切换的延时变长,通信本身还能用,只是整体节奏变慢, 很难被联想到主频上。关于时间与单位这一类坑,我在 《定时器与单位换算的坑:为什么注释不能信》 里专门整理过。

七、Flash 编程:宽度、页大小与保护

Flash 编程看起来是最枯燥的一节,但它直接决定了参数存储的设计, 而参数存储出问题往往意味着“现场要重新标定一遍”。我按三个维度记这件事: 最小写入宽度、擦除单位、以及保护位。

维度我遇到过的几种对设计的影响
最小写入宽度 按字节/半字/字多种宽度可选;也有系列要求一次写双字(8 字节),不足要补齐 写入函数不能只按“结构体多大”循环,尾部不足一个写入单元时必须先拼满再写,否则最后一个字段永远写不进去
擦除单位 512 字节、1 KB、2 KB 三种页大小都遇到过 页越大,改一个小参数要搬的数据越多;参数区尺寸不是页的整数倍时,必须做读-改-写,否则会把相邻数据擦掉
空片判断 用擦除后的全 1 状态判断“从未写过” 这是最省事的首次上电初始化:读到全 1 就写一份默认参数表,不需要额外的标志位
磨损寿命 有的系列手册给的最低擦写次数是 1000 次 结论很直接:不要把运行日志写片内 Flash;参数只在一部分场景落盘(用户确认保存、正常关机前),并且写之前先比较,值没变就不写
读写保护 读保护打开后调试器连不上;有的系列解除读保护会整片擦除 只能放在量产的最后一步,而且要写进流程文档,否则返修时连不上板子

边界错误我踩过一次,印象很深:批量写入的循环条件写成了“小于等于长度”, 结果多写了一个元素。单看代码很难发现,现象是“写完最后一个字节之后,CRC 校验总是不对”。 在 RAM 里多写一个字节常常无所谓,在 Flash 上就是实打实的错误数据。

参数区的三条加固

我读过的一批工程里,参数区的默认做法相当朴素:把一个结构体整块擦写进片内 Flash 的末页, 没有 CRC、没有版本号、没有备份。最极端的一处是引导程序与应用对同一块 Flash 的参数结构体 定义不一致——应用侧比引导侧多了两个字段,于是引导程序会按自己的布局去解读同一块 Flash。 这在“结构体没变”的版本上没问题,一旦版本错配就是隐性故障。

我现在给参数区加的三条是:双区备份 + 递增序号(写入时交替写两个区,读的时候取序号大的有效区)、 结构体头带 CRC 与版本号(校验失败就回退默认值,版本不符就走一次迁移)、 以及写前关中断(避免写到一半被打断)。这三条不复杂,但要在项目第一天就定下来, 后面补的成本会高很多。具体做法我在 《嵌入式参数存储设计:Flash 分区、双区备份与 CRC 校验》 里展开写过。

八、唯一 ID 与读保护:只讲机制差异

芯片唯一 ID 这类东西,我第一次用的时候以为“全世界的 MCU 都一样,读一个固定地址就行”。 换了平台才发现,它在四个维度上都不一样,而且没有一个是能靠猜解决的。 下面只讲机制差异,不写具体地址、长度与校验式子——那属于实现细节,也属于我不想公开的部分。

  • 位置不同:有的放在厂商保留的系统存储区,有的在出厂信息区,都不在主 Flash 空间里,也不是每个系列都能在任意读保护等级下访问。
  • 长度不同:有 96 位(12 字节)的,也有更短的;有的系列还会同时提供容量标识、批次信息一类的出厂字,位数各不相同。
  • 命名不同:有的头文件里叫唯一 ID,有的拆成三个 32 位字分别命名,有的干脆叫芯片 ID 或 Flash ID,读代码时得先确认这三个名字指的是不是同一件事。
  • 读取方式不同:多数是直接按地址读几个字,个别系列要求先解锁或在特定时钟条件下读;也有系列在打开读保护后,读取本身就会失败。

用法上各家思路倒是接近:把几个出厂字做一次交叉运算,得到一个校验值,存到外部存储器里, 开机时重新算一遍并比对。不匹配时我有见过三种反应:直接死循环、 不锁死而是把目标输出置零(停机而不是报警,现场更不容易发现)、 以及跳到错误界面提示。三种做法的“强度”不同,代价也不同。

这套机制的代价我认为比收益更值得记住:换芯片或换批次要重新授权; 一旦读保护打开,调试链路断开,返修很麻烦;产线工装必须能写授权码,否则量产会卡住; 而且它终究只是“提高顺手抄的成本”,对有心破解的人作用有限—— 校验点是可以被绕过的。我在 《芯片唯一 ID 与程序绑定:一个实用但容易反噬的做法》 里写过这个取舍。

九、中断与向量表

中断这块,换平台时最需要重新核对的是优先级分组和向量表偏移的写法。

优先级分组在 M0+ 上是个空概念。ARMv6-M 的 NVIC 只有两位优先级,共 4 级, 没有“抢占优先级 + 子优先级”的分组机制。所以从 M4 迁到 M0+ 时, 那句“设置优先级分组”的调用编译得过、跑起来也是空操作,但你会以为自己设计了 8 级嵌套。 我现在的做法是:在 M0+ 上只区分“必须快”和“可以等”两类中断,不去精细设计嵌套。

向量表偏移的写法我在哪些平台上见到容易漏的地方
直接写内核寄存器 基于 CMSIS 的工程 写完要加一次数据同步屏障;偏移值必须与 Flash 分区对齐
厂商封装的设置函数 GD32 系列(参数是“表在 Flash 还是 RAM”加偏移量) 有的工程用的是拼错的常量名,编译过得去但语义要靠人核
链接脚本 + 系统初始化两处 GCC 工具链的工程 两处都要改:只改链接脚本,程序能跳过去但中断进不来;只改系统初始化,链接地址又不对

偏移量本身没有玄机,它等于引导程序占用的大小。我遇到过的取值有 8 KB、10 KB 和 64 KB 三档, 对应的就是三个不同容量的引导程序。这里有一个很典型的误区:“程序能跑”不代表“偏移配对了”—— 如果主循环不依赖任何中断,偏移错了照样跑得欢,一旦打开串口接收中断就死机。

十、外设的“同名不同物”

这是我认为换平台时最费时间的一节。同一个名字——串口、定时器、DMA、GPIO—— 在不同厂商的芯片上行为差异可以很大,而数据手册里这些差异散落在不同章节。

串口

  • 波特率寄存器的算法不一样:有的系列有一个“双倍波特率”开关,打开之后重载值的算法整个变掉。移植时漏掉这一位,现象是“波特率正好差一倍”,而且收发都是乱的。
  • 有的系列用定时器当波特率发生器:波特率设置函数返回的不是分频值,而是定时器的重载值。我见过把重载值当分频值填进去的写法,结果就是完全对不上。
  • 空闲中断不是都有:有硬件空闲中断的可以用它判帧;没有的只能靠“收到字节就重置计时器、超时即判帧”。同一个产品的两块板子,一块用硬件空闲中断,另一块用定时器累计静默时间,两种实现我都写过,见 《串口通信的帧同步:四种“怎么知道一帧收完了”的做法》。

定时器

“给我一个 1 ms 中断”这句话,在不同系列上要写的东西完全不同: 有的是自动重装模式配一个分频和一个重载值(我用过 24 MHz、256 分频、重载值给到接近一百,得到约 1 ms); 有的是 PWM 模式配比较值;还有的系列定时器时钟源取自某条 APB 总线, 总线分频一改,定时周期跟着变。这一节的标题我一度想写成“注释不可信”, 因为我核对过的定时器配置里,注释与实际计算值不一致的至少有三处。

DMA

循环模式与普通模式的区别、外设与 DMA 通道的绑定关系,在两个系列之间基本不能照搬。 我见过一个工程因为“某一路串口接收和另一路串口发送的 DMA 冲突”, 干脆整路不用,并在注释里写明原因——这是一个很实在的取舍: 引脚多、外设多的平台上,DMA 通道是比引脚更稀缺的资源。 另外,串口发送用 DMA 时,“发送前清全部标志位”这一步不能省, 否则第一次发完之后,后面的发送会一直“以为上一次还没发完”。

GPIO 与调试口

复用功能编号同一个系列里也常常不同:同一个引脚,在这颗芯片上要配成第 2 号复用, 换一颗就要配第 6 号。上下拉与开漏的默认状态也不同,有的系列上电后所有 IO 默认处于模拟输入, 不配置就“测不到电平”。还有一个我差点栽进去的点:为了省引脚,把调试口复用成普通 IO。 我见过一个量产工程就把 SWD 之外的调试脚全部释放成 IO,这在量产板上是常见手法, 但这一步做错,或者配置顺序不对,板子就再也连不上调试器了。

看门狗也是同名不同物:名字有 WDT、SWDT、独立看门狗几种, 超时时间的算法是“计数除以(时钟除以分频)”,我见过算出 2.68 秒的,也见过直接定 1 秒的。 换平台时我一般会把看门狗复位时间当成一个需要重新核对的参数,而不是照抄。

十一、换平台时要重新核对什么

这一节是我自己的核对清单。每次换平台,我按这个顺序过一遍, 比“编译通过了就先烧进去看”要省时间。表格第三列是具体表现,都是我自己遇到过的。

类别要核对的内容我遇到过的具体表现
器件与资源 器件型号字段、IROM/IRAM 地址长度、实际链接的启动文件,三者是否一致 器件声明是 16 KB/2 KB 的型号,链接的却是同族另一个系列的启动文件;输出文件名与器件字段属于不同系列
内核能力 有没有硬件除法、有没有 FPU、有没有位带、NVIC 优先级几位 从 M4F 挪到 M0+ 后代码体积明显变大;原来的位带写法编不过;按 M4 设计的优先级分组在 M0+ 上失效
Flash 编程 最小写入宽度、是否要求成对写入、擦除页大小、结构体是否跨页 写入循环条件写成“小于等于”,多写一个元素;参数结构体尺寸不是页的整数倍,改一个参数要读-改-写整页
时钟 时钟源、PLL 参数写在哪、总线分频、改频后所有软件延时 注释写着 48 MHz 的延时函数跑在 24 MHz 的板子上,RS485 方向切换延时整体翻倍
中断 向量表偏移的写法(是否两处都要改)、优先级分组、中断入口与回调约定 只改了链接脚本没改系统初始化,程序能跑但中断进不来;中断触发了但业务回调没进去
外设 复用功能编号、双倍波特率位、是否用定时器产生波特率、DMA 通道绑定、有无空闲中断 波特率差一倍;DMA 通道冲突;发送前没清标志位导致第二次发送卡住
库 时钟使能函数、GPIO 写法、回调机制、寄存器直写与库函数是否混用 同一组寄存器上库函数与直写互相覆盖,某些位偶尔不生效
存储 参数区偏移与布局、引导程序与应用对同一块 Flash 的解读是否一致、有无 CRC 与版本号 引导程序与应用的参数结构体差两个字段,共用一块 Flash
保护 读保护等级、唯一 ID 的读取条件、打开保护的时机、产线工装是否支持写授权 读保护打开后调试器连不上;返修时要先擦片
工程配置 优化等级、FPU 相关宏、堆栈大小、是否启用 FPU 的编译 ABI 优化等级从最低档换到较高档后,靠空循环实现的延时全部变了

读陌生的工程时,这份清单还能反过来用:拿到一份没写文档的固件工程, 先按这十项去问“它跑在哪颗芯片上、时钟从哪来、参数区在哪”, 比从头读业务代码快得多。我把这个流程写在 《拿到一份陌生固件工程的前 30 分钟》里。

十二、几条我反复用到的经验

  1. 先确认内核能力,再决定代码风格。M0+ 上尽量把浮点和除法从热路径里拿掉,改成放大整数与查表;这一步在写第一行代码前做,比事后优化便宜。
  2. 先算 RAM,再画架构。2~4 KB、8 KB、几十 KB、近 200 KB 是四种完全不同的写法,不是同一套代码配不同的任务数。
  3. 库可以混用,但不要在同一组寄存器上混用。要么全用库函数,要么全直写,混着来会出现“写得进、读得出、就是不生效”的怪现象。
  4. 换平台第一件事是量延时。翻一个 IO 用示波器看一次,比读十分钟注释靠谱;注释里的主频与延时都应该当成线索而不是事实。
  5. 参数区从第一天就带 CRC 和版本号。“结构体整块擦写 + 无校验”在样机上永远不出问题,在批量之后一定会出问题。
  6. 把“哪些层不用改”列出来。我见过同一套三函数状态机加事件表从 2021 年一路用到 2023 年,四次换平台只改驱动层——协议、状态机、存储格式是可以沉淀的资产,时钟与 GPIO 不是。
  7. 读保护与唯一 ID 的开关时机要写进流程。它们不是代码问题,是流程问题;顺手打开一次,可能要花半天才能连上板子。
  8. 不要在换平台的同一版里同时换框架。两层一起动,出了问题无法判断是哪一层的责任。关于框架选择,我在另一篇笔记里单独写:《固件框架的几种写法:前后台、时间片、状态机与 RTOS 任务》。

参考资料与说明

  • ARM 官方公开的 Cortex-M 系列处理器技术参考手册中,关于 ARMv6-M 与 ARMv7-M 指令集、位带别名、NVIC 优先级位数与向量表偏移寄存器的描述,属于公开架构资料。
  • 各芯片厂商公开发布的数据手册、参考手册与固件库用户手册(包括标准外设库、HAL 库与各家的自研驱动库文档),是我核对寄存器行为与 Flash 编程规格的依据。
  • 文中提到的所有资源档位、主频与工程现象,均来自我自己写过的工程与读过的工程文件;凡是我没有逐行核对的项,文中已标注“未确认”。具体参数请以各厂商最新数据手册为准。
  • 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。
  • 文中出现的芯片型号仅用于说明技术方案,与相关厂商无隶属或授权关系;本文不涉及任何报价、供货或推荐结论。示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码。
  • 站内相关笔记:固件框架的几种写法、定时器与单位换算的坑、嵌入式参数存储设计、芯片唯一 ID 与程序绑定、Bootloader 跳转与 OTA、学习笔记目录、个人实验。