news 2026/10/3 7:51:01

GD32L233到L235 OTA迁移:Flash页大小与擦除粒度差异全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32L233到L235 OTA迁移:Flash页大小与擦除粒度差异全解析

上个月把一套基于GD32L233的OTA方案搬到GD32L235上,原本想着同系列芯片,Bootloader和App包直接换个型号就能跑,结果联调测试时连续踩了几个跟Flash读写强相关的坑。最直观的一个:L233上按2KB页对齐的分区表搬过去后,App跳转反复失败;还有个升级中途掉电恢复的用例,L233能正常回滚,L235却卡在启动阶段。排查到最后,问题都出在内部Flash的页大小、擦除粒度、读保护行为和等待周期配置上。

这篇内容适合正在用GD32L233/235做OTA功能的人,尤其是打算从L233往L235迁移、或者同系列里不同型号间复用OTA工程的同学。我会把两款芯片在Flash读写上的关键差异点逐一拆开讲,包括分区规划、读操作、写擦除操作、OTA执行链路里的坑,以及最后一张可以直接抄作业的迁移检查清单。

1. 为什么同样的OTA流程,换个型号就要重新审查Flash读写

1.1 OTA对Flash读写的依赖远超“能擦能写”

OTA这件事表面上只是“下载新固件、写入Flash、重启跳转”,但实际健壮性几乎全部压在Flash读写的细节上。比如接收升级包时,你是边收边写还是收完再写?边收边写就涉及页对齐、擦除时机、写失败回滚;收完再写就需要足够的外部存储或RAM缓冲。再比如写入完成后,你要在Flash里记录一个“固件有效”的标记,这个标记的写入位置、写入方式、掉电安全性都取决于Flash的最小擦除单位。还有启动校验,Bootloader要从Flash里读出版本号、CRC、升级状态,不同芯片的读取时序如果没处理好,上电瞬间读到的可能是旧值或者被预取缓冲误导的值。这些在单芯片上调试时不容易暴露,一换型号就全来了。

很多开发者觉得Flash操作就是调几个标准库函数,写之前擦一下,写之后等一下,能跑就行。但OTA场景里,Flash读写会被放在一个长时间、多步骤、可能掉电的流程里反复执行,任何一步对Flash特性的假设不成立,最后都会变成现场偶发故障。比如你对页大小的假设是2KB,换到4KB页的芯片后,分区表里一大半地址都失去意义。

1.2 同系列不同型号,FMC控制器未必完全一样

GD32L233和GD32L235都叫GD32L系列,内核也都是Cortex-M23,很多人潜意识里会把它们当成同一个东西。但Flash控制器不是光看内核就能确定的。芯片的设计目标、Flash容量、工艺调整,都会让FMC(Flash Memory Controller)的寄存器和行为产生细节差异。最典型的就是页大小:L233的页大小是2KB,L235样片是4KB页,这直接导致页擦除的最小单位变大,地址必须按新的页边界对齐。另外等待周期数、预取缓冲大小、读保护级别、写保护寄存器每bit对应的页数,都可能有变化。把这些差异全部忽略,直接复制工程,大概率会出现我开头说的现象:编译下载都正常,跑OTA就出问题。

这里有个经验:同一个系列的不同型号,外设寄存器通常“长得像”但不一定完全一样。你在L233上用的是FMC_KEY、FMC_CTL、FMC_WS这套命名,到L235上大概率还是这套命名,但寄存器位定义、某些保留位、状态位的位置可能挪过。所以迁移时不能只查数据手册的容量表,要把参考手册里的FMC章节从头到尾过一遍。

1.3 迁移项目中最容易忽视的三个变量

我自己排查下来,最容易埋雷的是容量、页大小和保护机制这三项。容量决定了你能不能做双备份,页大小决定了分区怎么对齐,保护机制决定了调试器和Bootloader的交互方式。它们不像主频和外设信息那样醒目,往往藏在参考手册的“Memory Organization”和“Flash Memory”章节里。做迁移审查时,这三个变量必须逐项核对,而不是只看Flash总容量够不够。后面我会把每一项展开讲,并给出一张可以直接对照的检查表。

2. 分区规划差异:页大小与擦除粒度决定OTA分区怎么画

2.1 先分清page、sector和bank,再谈分区

在做OTA之前,必须确认FMC支持的最小擦除单位到底是什么。有些芯片叫page,有些叫sector,有些还有bank的概念。很多人把这三个混在一起,去论坛提问说“我这芯片Flash擦除怎么不按扇区来”,十有八九是没看参考手册的存储组织结构。page一般是擦除的最小单位,bank是一组扇区的组合,Flash控制器本身还可能分主存储区和信息区(如选项字节区)。对于GD32L233/L235,你要关心的是主存储区的页大小、页数量、各bank起始地址。这些信息在参考手册的Flash Memory一章通常有表格,一行一行列清楚。

一个很容易犯的错:直接把编译器里的“Flash Size”当成页大小的依据。编译器只关心总容量和地址范围,它不会告诉你擦除一页要多久、写保护寄存器一个bit管几页。这些信息只有从参考手册或寄存器描述里拿。我建议拿到新芯片后,先把下面这个表自己填一遍:

  • 主Flash基址:通常0x08000000
  • 最大容量:xxKB
  • 页大小:xxKB
  • 页擦除时间:xx ms
  • 全片擦除时间:xx ms
  • 编程最小宽度:半字/字/双字
  • 写保护最小粒度:每bit对应几页

2.2 两款芯片在页大小上的实际差异

以我手头样板为例,具体数值请以你手里的数据手册为准:L233的主Flash页大小是2KB,整片容量从64KB到256KB不等;L235样片页大小是4KB,容量最大到512KB。乍一看L235容量更大是好事,但页变大也有代价:分区的最小粒度变大,App镜像尾部可能要填充更多无效字节。更关键的是,页擦除一个4KB页和擦除一个2KB页的时间不同,这个后面会讲。编程操作上,两款都支持按32位字编程,但擦除命令、忙状态位位置如果有差异,代码里的等待循环需要调整。

项目GD32L233GD32L235(样片)影响
主Flash最大容量256KB512KB是否支持双Slot
页大小2KB4KB分区对齐、擦除粒度
页擦除典型时间约20ms-30ms约30ms-40ms升级进度和超时
编程最小宽度32位字32位字缓冲区写入逻辑
写保护最小粒度每bit对应1页(2KB)每bit对应1页(4KB)临时解锁范围

这个表只是思路参考,不同批次、不同封装可能有差异。关键不是记住具体数值,而是知道“这两个型号就是不一样”,迁移时所有和页大小相关的计算都要重新推。

2.3 Bootloader区、App区、升级暂存区怎么画

容量足够时,比较稳的OTA分区做法是Bootloader区、App当前区、App下载暂存区(或A/B双区)、参数区。分区时所有地址都必须是页大小的整数倍。比如L233页大小2KB,Bootloader如果不超过16KB,可以占0x08000000到0x08003FFF,也就是8页;App从0x08004000开始。L235页大小4KB时,Bootloader 16KB就只占4页,下一区从0x08004000开始也一样。但如果Bootloader压缩到12KB以内,L233上可以只占6页,L235上因为最小擦除单位是4KB,12KB仍然要占满4页(16KB),空间利用率就会差一些。

分区表一旦因为页大小变化需要移动,App镜像里所有绝对地址都会变,包括中断向量表偏移、链接脚本里的FLASH起始地址、Bootloader里的跳转地址和版本标记地址。我这次迁移就踩了坑:L233工程里App起始地址是0x08004000,链接脚本里直接写死,换到L235后没有同步改,结果Bootloader从0x08008000跳过去,App自己却按0x08004000的向量表执行,中断全部错位。所以分区规划不是画一张图就完事,必须把链接脚本、OTA包头、Bootloader跳转函数三个地方的地址统一。

// 分区配置示例,放在一个公共头文件里 #define APP_FLASH_START_ADDR 0x08004000u #define APP_FLASH_SIZE 0x0000C000u // 48KB #if defined(GD32L235) #define FLASH_PAGE_SIZE 0x1000u // 4KB #define BOOTLOADER_SIZE 0x4000u // 16KB #elif defined(GD32L233) #define FLASH_PAGE_SIZE 0x0800u // 2KB #define BOOTLOADER_SIZE 0x4000u // 16KB #endif #define APP_START_PAGE_ALIGNED (APP_FLASH_START_ADDR / FLASH_PAGE_SIZE)

3. 读操作差异:预取、等待周期与读保护对OTA的影响

3.1 从Flash取指与预取缓冲:跳转App时卡死的隐藏元凶

很多人在Bootloader跳转App时遇到过“复位后卡死”或“随机进入HardFault”,排查半天发现不是App代码问题,而是Flash读取相关的配置残留。CPU访问Flash的等待周期数和预取缓冲使能位是在时钟初始化时配置的,L233和L235支持的等待周期策略不一定相同。如果Bootloader初始化时按L233的Flash频率配置了预取和等待周期,跳到L235后时钟频率不同,Flash读取时序就可能不对。

比较稳妥的做法是:跳转前不依赖原有的预取配置,先把SysTick、外设中断都关掉,执行__DSB()和__ISB(),再重新设置向量表和MSP。如果两款芯片的等待周期寄存器位定义不同,跳转后App的SystemInit会重新初始化时钟,一般能覆盖回来。但你要保证跳转前这段代码本身是稳定的,不要在关中断后还访问容易出问题的外设。

static void jump_to_app(uint32_t app_addr) { uint32_t msp_value = *(volatile uint32_t *)app_addr; uint32_t reset_vector = *(volatile uint32_t *)(app_addr + 4); // 基础合法性检查 if ((msp_value & 0x2FFE0000) != 0x20000000) { return; // MSP不在RAM区,放弃跳转 } if ((reset_vector & 0x1FF00000) != 0x08000000) { return; // 复位向量不在Flash区,放弃跳转 } __disable_irq(); SysTick->CTRL = 0; __DSB(); __ISB(); SCB->VTOR = app_addr & 0xFFFFF800u; __set_MSP(msp_value); __enable_irq(); ((void (*)(void))reset_vector)(); }

3.2 读保护级别不同,对OTA的限制也完全不同

读保护一直是OTA项目里容易被忽略的一环。L233的读保护级别我印象里主要在Level0和Level1之间切换,Level1下通过调试器读Flash会被限制,但用户代码自己读还是允许的。L235如果支持Level2,那一旦使能基本上就是永久保护,任何方式都读不出来,也没有办法通过软件解除。这个差异对OTA最直接的影响是出厂固件烧录流程:产线可能要先用调试器烧Bootloader和首次App,再开启读保护,如果开成Level2,后期想通过OTA把App降级回调试版本就做不到了。

这里还要区分读保护和写保护:读保护限制的是“读出”,写保护限制的是“擦除/改写”。OTA需要Bootloader能擦写App区,所以App区不能被写保护;同时如果Bootloader区允许运行期改写,还得小心别被异常程序把Bootloader擦掉。实际调试时,如果IDE报flash download failed - target dll has been cancelled,很多人第一反应是连接问题,其实常见原因之一就是芯片处于Level1读保护状态,调试器无法重新写入Flash。处理办法通常是先用串口或厂家的工具解除读保护,或者设置IDE在连接前先做unprotect。这个坑在L233和L235上都会遇到,但错误的解除方式可能导致整片Flash被擦除。

3.3 写入后读回与缓存一致性的注意点

内部Flash一般没有复杂的cache,但像GD32L233/235这类低功耗MCU,为了提高取指速度可能会带预取缓冲或指令缓存。如果带缓存,Flash写入后刚写入的数据不一定立刻从数据总线读到最新值。官方库里面通常会在编程操作完成后做一次缓存无效化或者重新读取确认。如果你在OTA代码里边写边校验,比如写完一个page后立刻读回比对,遇到和写入值不一致的情况,先不要急着怀疑Flash坏了,检查一下是否需要对缓存做invalidate。

我处理过不少“写进去读出来全是0xFF”的问题,最后都是缓存或等待时间不够导致的,而不是颗粒问题。尤其是边写边校验的场景,写入后最好加一条__DSB()再读回,确保CPU侧的写缓冲已经flush。如果是带指令cache的型号,可能还需要调用库提供的cache reset接口。迁移到新型号时,这个动作容易漏,因为L233上不调用也能读对,L235上就可能偶发不对。

4. 写和擦除操作差异:状态轮询、写保护与时间预算

4.1 解锁、编程和擦除的命令流程

GD32L系列的内部Flash编程和擦除需要先向FMC_KEY寄存器依次写入两个解锁键值,然后操作控制寄存器里对应的位。L233和L235的基本思路一致,但寄存器位定义、状态位的位置可能有变化。写一个word数据前,先确认目标地址所在页是擦除状态,没有擦除的Flash写操作不会得到预期值;擦除后再把FMC_CTL的PG位置1,执行一次32位写入。擦除页时,把PER位置1,写入目标页地址,再置START位触发。整个过程要轮询BSY位,直到操作完成。

这类底层操作我不建议自己在业务逻辑里重新实现,直接用官方固件库提供的函数,然后确认库版本对应的是哪个型号。有些项目从L233迁移到L235时,固件库文件还是老的,FMC驱动编译能过但行为不对,就是因为库函数内部使用的寄存器基址或者宏定义不匹配。

4.2 擦写时间差异对升级进度和超时策略的影响

页擦除时间看起来只有几十毫秒,但在OTA里会被放大。假设App固件64KB,L233每页2KB,需要擦除32页,如果每页30ms,单擦除阶段就要约1秒;L235每页4KB,同样64KB只要擦除16页,但单页40ms的话也要约0.64秒。关键是升级时通常还要写入,写入时间按页算也要逐个等BSY。如果你的升级超时设得太紧,比如从网上抄来的代码里写死“等待BSY最多100ms”或者“每包超时200ms”,在页擦除时间不同的芯片上就可能偶发超时失败。

我建议把Flash操作的超时设计成可配置项,最好基于实际测量值再加3到5倍余量。进度条的计算也要根据页大小来:不要写死“总数=容量/2048”,而要用PARTITION_SIZE / FLASH_PAGE_SIZE。同时擦写期间的看门狗要记得喂,擦除操作虽然不长,但整个升级过程中如果反复擦写几十页,喂狗时机不对会直接被复位。

// 等待BSY超时,按实际测量值余量放大 static uint32_t flash_wait_busy(uint32_t timeout_ms) { uint32_t start = get_tick_ms(); while (fmc_flag_get(FMC_FLAG_BSY) == SET) { if (get_tick_ms() - start > timeout_ms) { return 1; // 超时 } } return 0; }

4.3 写保护的粒度与临时解除策略

写保护是把双刃剑。OTA要求Bootloader能改写App区,所以App区不能长期处于写保护状态,否则IAP会失败。但产品出厂时又想防止App区被随意改写,至少想防止调试器乱搞。这就需要在Bootloader进入升级模式后,先临时解除目标区域的写保护,升级完成后再重新使能。L233和L235写保护寄存器里每个bit对应的页数如果不同,解除保护时的掩码计算就会不同。假设L233是每bit一页(2KB),你要解除32KB区域就得写16个bit;L235如果每bit一页(4KB),同样32KB只要写8个bit。如果迁移后沿用旧的保护掩码,可能出现“想保护的区域没保护上”或者“误保护了别的区域”。

另外,很多芯片在修改写保护选项字节时,并不是改了寄存器立刻生效,而往往需要系统复位,甚至把整片Flash都擦掉。我个人的习惯是不要在正常业务运行时不必要地开关写保护,只在进入明确的OTA升级状态时再做,并且要在升级界面提示用户不要断电。

5. OTA执行链路上最容易翻车的三个Flash细节

5.1 擦写期间代码执行位置的选择

内部Flash在擦写时,Flash控制器忙,同一时刻从该Flash取指可能被挂起或引入不确定延迟。如果擦写函数本身放在Flash上,执行页擦除时指令预取可能会卡住,轻则擦写时间变长,重则看门狗复位。更稳的做法是把Flash驱动函数放到RAM里执行,或者至少把擦除过程中最关键的循环体放到RAM。GD32L233/235的RAM不大,但放一个几百字节的Flash驱动没问题。很多SDK里已经提供了类似的RAM执行函数,如果没有,就自己在链接脚本里加一个section,把对应的函数声明到RAM段。

这里有一点要注意:如果RAM区域和Flash区域之间有总线仲裁,擦写Flash时从RAM取指也可能等待,但通常比从Flash取指好得多。我做过一个实测:纯粹从Flash执行擦页函数,偶发耗时是RAM执行的2到3倍;在OTA超时临界状态下,这种偶发就会变成升级失败。

5.2 中断向量表重映射在L233和L235上的实现差异

Cortex-M23内核提供VTOR寄存器,用来设置中断向量表地址。Bootloader跳转App之前,应该把VTOR设置为App区起始地址。需要注意两点。第一是对齐,VTOR要求向量表地址按中断向量数对齐,通常至少要按64或128字节对齐,具体看参考手册,分区时最好直接把App起始地址定在页边界上,顺便满足VTOR对齐。第二是向量表首两个字,分别是初始MSP和复位向量,跳转时先关闭全局中断,然后取App首字的MSP赋给主栈指针,再取复位向量地址,确认地址合法后跳转。

L233和L235在VTOR的行为上基本一致,但如果你同时开了读保护或者Bootloader区和App区有特殊映射,实际跳转会受影响。比如某些型号Bootloader区可以用选项字节映射到0x00000000,这个映射会改变地址解析方式,跳到App前需要把映射改回来。这一部分最容易出现“在L233上能跳,在L235上跳过去就跑飞”的情况。排查时在跳转函数里加几个断言:检查App首字是否为合法RAM地址范围,检查复位向量是否落在App区范围内,检查当前MSP是否8字节对齐。

5.3 掉电恢复与备份区写入顺序设计

OTA升级最怕的是写固件写到一半掉电。很多人的方案是:先在Flash最后找一个固定参数区,写一个“升级中”标志,固件写完后,把标志改成“升级完成”。这个思路是对的,但实现上有个坑:很多MCU的Flash只能整页擦除,如果你把标志位和别的参数混在同一个页里,每次更新标志都要擦掉整页,一旦中途掉电,其他参数也跟着丢。

我建议把OTA标志单独放到一个独立页,或者放到两个备份页交替使用。写入顺序上,优先写数据区,数据全部校验完成后,最后写“新固件就绪”标志。为什么要最后写?因为标志本身就是一个原子提交点:只要标志没写成功,Bootloader就认为旧固件仍然有效,可以继续启动旧App,或者自动重试下载。反过来如果先写标志再写数据,掉电后Bootloader认为有新固件,实际数据是残缺的,就得靠额外的回滚机制去恢复。这个提交点思想,在做L233/L235迁移时也要保持一致,只是写标志时要注意页大小变化,保证标志页地址按页对齐。

6. 从L233迁到L235的Flash读写代码检查清单

6.1 十项自查表

为了这次迁移,我整理了一张检查表,每次换芯片型号都拿出来对照一遍。这张表不局限于GD32L233/235,任何同一系列不同子型号的迁移都能用。

检查项需要确认的内容我踩过的对应问题
页大小读手册Memory Organization章节,确认最小擦除单位分区表对齐错误,跳转失败
Flash起始地址和大小确认主Flash基址与容量上限链接脚本写死旧容量,编译越界
编程最小宽度确认支持8/16/32位写入中的哪些缓冲区按16位写到32位芯片,校验失败
页擦除时间实测或查手册,确认超时余量超时设太短,OTA偶发失败
等待周期/预取确认系统时钟下Flash等待周期配置跳转后随机卡死
读保护级别确认Level0/1/2支持情况调试器连不上,报flash download失败
写保护粒度确认每bit对应几个页保护掩码算错,误保护App区
缓存一致性确认FMC是否带cache,写入后是否需要invalidate写后读回全0xFF
跳转前外设清理确认需要关闭的外设和中断跳转App后外设状态冲突
掉电标志页确认标志页地址按新页大小对齐标志和数据混页,掉电丢参数

6.2 一套驱动适配两款芯片的封装思路

如果不想在L233和L235之间维护两套Flash驱动,可以做一个小的适配层。把差异点集中在几个宏或者配置结构体里,比如FLASH_PAGE_SIZE、FLASH_PAGE_COUNT、FLASH_APP_START_ADDR、FLASH_FLAG_ADDR、等待周期配置函数指针。分区表用统一的结构体来表示,Bootloader和App都引用同一份头文件。这样在迁移到L235时,理论上只需要替换配置头文件里的几个宏定义,再重新验证一遍Flash底层测试用例。

typedef struct { uint32_t page_size; uint32_t app_start_addr; uint32_t app_max_size; uint32_t flag_addr; uint32_t (*wait_busy)(uint32_t timeout_ms); } fmc_cfg_t; const fmc_cfg_t *fmc_get_config(void) { #if defined(GD32L235) static const fmc_cfg_t cfg = { 0x1000, 0x08004000u, 0x40000u, 0x0807F800u, flash_wait_busy }; return &cfg; #else static const fmc_cfg_t cfg = { 0x0800, 0x08004000u, 0x20000u, 0x0803F800u, flash_wait_busy }; return &cfg; #endif }

我建议至少写三个底层测试用例:页擦除回读、跨页写入回读、擦写100次稳定性测试。迁移完成后先在开发板上跑完这三个用例,再去做OTA联调。特别是稳定性测试,能暴露写保护解除失败、缓存一致性这些偶发问题。

6.3 最后一点实际体会

这次从L233迁到L235的整个过程下来,我的体会是:换个芯片型号看着是小改动,但Flash这类基础模块的差异影响的是整个OTA架构的判断。就算两款芯片来自同一系列、同一个厂商,也要抱着“重新读一遍参考手册”的心态去处理。不要偷懒直接复用旧工程,否则排查的时间会比重新适配长得多。

最后再分享一个小技巧:在适配层的配置头文件里放一个版本宏,例如FLASH_ADAPTER_VERSION "L235_1.0",Bootloader和App启动时都把当前Flash参数通过串口日志打出来。这样现场拿到一台设备,先看日志里的页大小和分区信息,就能立刻确认固件到底跑在哪个适配参数下,排查OTA问题会省很多事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 7:50:25

STM32G474 HRTIM触发ADC采样:实现PWM中间时刻采样的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:50:20

工厂冷却水引入分布式能源站:余量利用与方案比选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:50:11

SpringBoot WebSocket STOMP企业IM实战:群聊、@提醒与消息回执

在接手企业IM(即时通讯)这个需求之前,我一直以为SpringBoot里加个WebSocket就是调个API的事,真正动手做才发现,从“能连上”到“能稳定支持群聊、提醒、消息回执”之间隔着一条巨大的鸿沟。这篇文章把我在开发一个包含…

作者头像 李华
网站建设 2026/10/3 7:49:53

DRV8818PWPR+ATmega32工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:48:15

Hadoop生态核心拆解:从HDFS、MapReduce到HA高可用实战指南

做了这么多年大数据相关的项目,每次被问到“想入行大数据,第一步该学什么”,我的答案几乎没变过:先把Hadoop吃透。这不是因为它最时髦,恰恰相反,Hadoop的生态组件在当下已经不算“新潮”了,但它…

作者头像 李华