☰
GD32L233与GD32L235 Flash架构对比及OTA升级方案设计
2026/10/3 1:18:02 网站建设 项目流程

前段时间帮客户调一套基于GD32L235的远程OTA升级方案,代码是从以前GD32L233的项目里搬过来的。一开始我心想,都是L23x系列,flash操作能差到哪去?结果真调起来才发现,L233和L235在flash读写上的区别比想象中大得多,尤其是在OTA功能上,这两颗芯片的方案思路完全是两码事。

如果你正在用GD32L233或GD32L235做OTA,或者正在纠结选哪颗芯片做远程升级,这篇文章应该能帮你少走不少弯路。我会把两颗芯片在flash架构上的差异、读写操作的具体区别、以及围绕OTA功能怎么设计bootloader和升级流程,一次性讲清楚。

1. 先搞清楚:L233和L235的flash硬件架构差在哪

1.1 容量、bank、页:一张表讲完

很多人在选型时只看flash容量大小,觉得L235就是L233的“加大容量版”,这是个误区。两颗芯片的flash子系统架构有本质区别。

对比项GD32L233GD32L235
内核Cortex-M23Cortex-M23
最高主频64MHz64MHz
内置Flash容量最大256KB最大512KB(部分型号256KB)
Flash Bank结构单Bank双Bank
页擦除大小2KB2KB
编程最小宽度32位字32位字
读等待周期配置WS位可配WS位可配
典型SRAM32KB51.2KB(视型号)

单Bank和双Bank,才是这两颗芯片在OTA场景下最核心的分水岭。L233不管flash多大,整个主存区是一个整体,CPU取指、数据读写、擦写操作全都在同一块flash上进行。L235则把flash分成两个独立的bank,典型512KB型号是bank0和bank1各256KB,两个bank之间可以做到“一边擦写、一边运行”。

就这一个区别,直接决定了OTA升级策略的天壤之别。

1.2 单Bank和双Bank不是“多了个块”这么简单

拿生活里的例子类比,单Bank就像你在装修自己住的房子,装修期间你没法在屋里正常生活,得搬出去或者忍受不断停电停水施工噪音;双Bank则像一套双户型的房子,装修A户的时候人住在B户,两边互不干扰。

MCU内部flash是NOR型,特点是“能随机读取、能按页擦除、写入时需要先擦除”。因为代码可以直接在flash上XIP执行,所以CPU取指令的动作本质上就是读flash。问题就来了:当你对一块flash区域做擦除或编程操作时,如果CPU正好从这块区域取指令,Flash控制器会直接冲突,轻则操作失败,重则HardFault。这就是为什么很多人在OTA时,明明代码没问题,一旦开始擦写flash就死机。

L233由于只有一个bank,擦写flash期间,CPU不能从同一个bank取指。常见的处理办法是把擦写代码拷到SRAM里执行,或者暂停调度、等待擦写完成。L235就舒服得多,你可以在bank1里跑代码,同时去擦写bank0,完全不冲突。

1.3 MCU内部flash是用什么接口访问的

顺带回答一个很多新手会问的问题:MCU内部的flash是用什么接口访问的?它不像外部W25Q系列的SPI NOR flash那样挂在SPI总线上,也不像NAND flash那样走复杂的主控接口。内部flash直接挂在AHB总线上,由Flash控制器(GD32L23x上叫FMC,Flash Memory Controller)统一管理。CPU取指和数据访问都可以直接映射到flash地址空间,比如0x08000000就是主flash的起始地址。

反正记住一点:内部flash的地址空间和外部flash不一样,它和总线是紧耦合的,访问速度远比任何外部存储器快,但代价就是“擦写时不能同时取指”这类限制。OTA中最底层的内容,都是围绕这个限制做文章。

2. 读写flash的寄存器级操作到底哪里不一样

2.1 公共流程:解锁 → 操作 → 等待BSY → 清标志

先讲相同点。L233和L235的FMC操作流程基本一致,都是标准的一套:

  1. 解锁FMC寄存器,写入专用解锁序列;
  2. 配置操作模式(页擦除、字编程等);
  3. 发起操作并等待忙标志清除;
  4. 检查错误标志位;
  5. 重新加锁。

解锁序列是固定的,直接写寄存器操作大致如下:

/* 标准解锁序列 */ FMC->KEY = 0x45670123U; FMC->KEY = 0xCDEF89ABU;

写完两个KEY寄存器后,FMC的CTL寄存器就解锁了,可以配置擦写操作。做完操作后记得写锁定位把FMC重新锁上,防止程序跑飞时误操作flash。

厂家固件库一般封装好了这些步骤,直接调用fmc_unlock()、fmc_lock()就行。但你要理解背后发生了什么,才能解决OTA过程中的各类奇葩问题。

页擦除操作在L233和L235上一样:

fmc_unlock(); fmc_page_erase((uint32_t)page_addr); // 按2KB页擦除 while (fmc_busy_state_get()); fmc_lock();

擦除后flash内容全变成0xFF,如果写入时发现不是0xFF,必须先做擦除。这也是OTA中“先擦后写”的铁律。

2.2 编程宽度:按半字/字,不是按字节

L233/L235内部flash编程的固定宽度不是1字节。严格来说,按字(32位)编程是标准操作,部分型号也支持半字(16位)。你没法像写外部EEPROM那样直接往一个地址写一个字节。这在OTA固件合成和写入时特别容易踩坑。

比如你收到一包OTA数据,里面可能只有1个字节变了,但你把这一包写到flash时,如果按字节逻辑处理,底层会先读改写整个字。实际编程步骤是这样:

fmc_unlock(); fmc_word_program((uint32_t)addr, *(uint32_t *)buffer); while (fmc_busy_state_get()); fmc_lock();

所以工程上处理OTA数据包时,最好把数据整理成4字节对齐的块,一次写一个字。如果你的通信协议里包长不是4的倍数,最后多余的字节要拼接和处理,不能图省事直接逐字节写。实际踩坑经验是:很多“OTA后程序跑飞”“校验失败”的问题,根源就是在写flash时字节拼接没处理好。

另外,写入前必须确保目标地址所在页是擦除状态(0xFF)。对一个不为0xFF的地址再次写入,flash控制器会报编程错误(PGERR),这个错误状态如果不及时清除,会卡住后续的flash操作。

2.3 双Bank并发:L235独有玩法

L235的双Bank不只是容量大,它真正厉害的地方在于:允许一个bank进入擦写状态时,CPU从另一个bank正常取指执行。这意味着做OTA时,你完全可以让bootloader或者App A运行在bank1,同时去擦写bank0里的App B,整个过程系统不中断、不掉电、不进入“升级死区”。

实际操作上要注意,双Bank并发不是说你什么都不用管。CPU当前运行的代码位置,和你要擦写的区域必须在不同bank。如果程序在bank0跑,你去擦bank0,照样会冲突。所以L235上写flash函数时,要增加一个判断逻辑:确认当前PC程序计数器所在的bank和待操作地址不在同一个bank。

#define BANK0_START 0x08000000U #define BANK1_START 0x08040000U /* 示例写法,具体按选型手册 */ uint8_t check_remote_bank(uint32_t addr) { uint32_t pc = (uint32_t)__get_IPSR(); /* 精确判断用PC,可借助函数地址 */ uint32_t cur_bank = (pc >= BANK1_START) ? 1 : 0; uint32_t dst_bank = (addr >= BANK1_START) ? 1 : 0; return (cur_bank != dst_bank) ? 1 : 0; }

如果判断出来“当前代码所在bank和目标擦写bank相同”,那就得做特殊处理,比如临时跳转到另一个bank执行擦写动作,或者退回SRAM执行。

2.4 等待周期配置:频率一变,读写就出错

flash读等待周期(WS)是另一个容易被忽略的点。L233和L235都支持通过配置WS位来适应不同主频。频率较低时可以用0等待周期,跑在64MHz满频时通常需要配置为1个等待周期。

这个和OTA的关系在于:如果你在OTA升级过程中临时改变了时钟配置(比如先从64MHz降到32MHz省电,或者为了加速擦写临时超频),却没有同步调整WS位,flash读数据就会出错,程序直接跑飞。

我见过一个案例,升级过程中为了降低功耗把系统时钟从64MHz切换到32MHz,结果忘了改WS位,芯片每次执行到新固件的前几条指令就死掉,折腾了很久才发现是挂起等待周期不一致。

3. OTA场景下,单Bank和双Bank要用不同的思路

3.1 L233单Bank升级:三种主流做法

L233最大256KB的flash,单bank结构,意味着你在OTA时面对的核心问题永远是:擦写期间,代码从哪来?

先看一个经典的分区方案:

区域起始地址大小(示例)内容
Bootloader0x0800000016KB启动引导、flash驱动、升级入口
App参数区0x080040004KB启动计数、升级标志、固件状态
App A0x08005000剩余空间主应用固件

在这个方案里,App运行时如果需要OTA,它会接收完整的新固件到某个缓冲区(通常是外部flash/串口直写),然后重启进入bootloader,由bootloader执行“擦除App区→写入新固件→跳转App”流程。因为bootloader常驻在独立区域,擦写App时CPU在bootloader里取指,两个区域不冲突,这是单bank最简单可靠的做法。

第二种做法是“双副本A/B”方案,对容量要求高:

区域地址说明
Bootloader0x08000000引导和升级管理
App A0x08008000当前运行版本
App B0x08020000备用版本/升级目标
参数区0x0803F000切换标志

L233如果flash达到256KB,且App控制的在64KB以内,可以勉强做A/B双副本。升级时往不在运行的B区写入,写完校验,改标志位,重启后bootloader决定跳到A还是B。好处是升级失败还能回滚到旧版本,坏处是可用flash空间被砍掉一半,而且L233不支持并发操作,往B区写的时候如果代码还在A区运行,逻辑上没问题,因为A区没被擦写,CPU能正常取指。

第三种是“外部存储中转”方案,适合固件较大、flash较小的场景。OTA数据先下载到外部SPI NOR flash,再把内外flash配合使用,升级时从外部读取、擦写内部flash。这个方案成本高些,但容量瓶颈被打破了。

L233上我推荐的做法是什么?如果项目固件不大,直接选方案一(bootloader + 单App + 参数区),逻辑简单、稳定性最好。如果产品对可靠性要求很高,空间又充裕,再上A/B。但不管哪种,都要接受一个现实:单bank升级时,系统必然有一段时间是“半停机”状态——App切到bootloader,bootloader接管刷写,刷完重启。这段时间用户如果操作设备,一般是无响应的。

3.2 L235双Bank乒乓升级的正确姿势

L235的双bank给OTA带来的最大红利就是:真正实现无感升级,升级过程中系统照常运行,升级完成后只需做一次跳转重启。这才是很多产品想要的OTA体验。

具体分区可以这样设计(以512KB L235为例):

区域地址大小内容
Bootloader0x0800000032KB引导、恢复出厂、flash驱动
Bank0 App0x08008000224KBApp版本A
Bank1 App0x08040000224KBApp版本B
参数区0x0807E0008KB版本号、启动标志、升级状态

升级流程是:当前设备运行在Bank0 App,后台下载新固件。新固件按字对齐分包,直接写入Bank1的对应区域。Bank1目前没在执行代码,所以边下载边写flash完全没问题,写完了做一次CRC或者SHA256校验,校验通过后在参数区写“下次启动切到Bank1”,然后软件复位。Bootloader启动时读取参数区的标志,跳转到Bank1的App。

如果Bank1校验失败怎么办?参数区的标志还停留在“回退模式”,bootloader依然跳回Bank0旧固件,用户完全无感知。这就是双bank乒乓升级(也叫双bank交替升级)的核心逻辑。

L235的另一个好处是,操作flash时你甚至不用把擦写函数拷贝到SRAM,因为你在Bank1擦写Bank0时,CPU正稳稳地在Bank1取指,系统调度、看门狗喂狗、通信协议栈全都照常运转。这也是L235相比L233在OTA体验上最本质的差距。

3.3 跳转App的正确写法与中断向量表

不管L233还是L235,OTA最后一跳都是跳转到新固件,这里有个高频坑就是中断向量表。

Cortex-M23支持VTOR寄存器,可以重映射中断向量表。跳转前必须把App区的向量表地址写入VTOR,否则中断一进来,CPU还是从0x08000000读中断向量,如果App编译时的链接地址跟当前运行地址不一致,程序就乱了。

#define APP_START_ADDR 0x08008000U typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_address = *(volatile uint32_t *)(app_addr + 4); pFunction jump_to_app = (pFunction)jump_address; __disable_irq(); SCB->VTOR = app_addr; /* 重映射中断向量表 */ __set_MSP(*(volatile uint32_t *)app_addr); /* 设置主栈指针 */ jump_to_app(); /* 跳转 */ while (1); }

在L235双bank升级场景下,如果你在Bank1下载新固件后直接跳转,跳转目标地址是Bank0的App,要和Bootloader约定好升级标志,防止反复跳转。这个跳转代码本身要放在两个bank都能稳定执行的位置,实践里我一般放在Bootloader里做统一跳转,App只负责写标志和复位,干净利落。

3.4 OTA下载与flash校验的可靠性设计

OTA不是“收到数据→写进去”那么简单。我建议每个固件包至少带三样东西:固件版本号、包序号、CRC32校验值。

推荐流程:

  1. 设备上报当前版本号,后台下发新固件包;
  2. 设备按4字节对齐接收数据,逐字写入目标flash区域;
  3. 写入过程中记录已写偏移量,断点续传时可以接着写;
  4. 全部写完后,整片读出flash数据和固件包CRC比对;
  5. 校验通过后写启动标志,校验失败则保留旧标志、下次仍启动旧固件。

其中第4步尤其重要。因为Flash写入有底层的位翻转风险,通信传输也有偶发错误,任何一层漏了,都可能让“校验通过但运行即崩溃”的概率上升。CRC32虽然简单,但对OTA这个场景足够用。在L235双bank方案里,建议做整bank的CRC计算,开销可控。

4. 移植和调试中的高频坑与排查思路

4.1 擦写过程中程序跑飞、HardFault

这个是我的老客户踩得最多的问题。现象:App做OTA时,程序在擦除flash某个页时直接进入HardFault,或者擦写完一次就死机。

排查方向:

  1. 确认擦写地址和当前执行代码是否处于同一个bank。L233没有任何回旋余地,擦写前必须确保代码在SRAM或bootloader里;L235则确认PC所在的bank和目标地址不同。
  2. 确认擦写期间中断是关闭的,或者中断服务程序所在flash区域没有被擦写。擦写flash期间触发中断,中断服务程序里的代码若在同一区域,就会导致总线错误。
  3. 确认FMC状态错误标志有没有清除。PGERR、WPERR这些错误标志置位后,不清除的话后续操作经常会“卡死”。

那个“电脑访问SD卡重命名时拔卡”的坏习惯,放到设备里是行不通的,真不能“把flash改写当U盘”。

4.2 跳转不成功、启动失败

跳转不成功常见原因有两个。第一,跳转地址算错了。*(volatile uint32_t *)(app_addr + 4)读出的是复位向量,即App的初始PC地址,如果App编译时的起始地址和跳转地址不一致,这个值就是错的,跳转后第一行代码就跑飞。第二,VTOR设置后中断使能时机有问题。我一般建议跳转前关总中断,App的启动代码里再统一开中断,避免边界时刻中断错乱。

另外一个隐蔽问题是:App的编译链接地址和flash实际写入地址不一致。比如你把App编译在0x08040000,但固件下载时被错写到0x08008000,然后跳转目标还是0x08040000,这种问题纯靠调试器看值才能发现,代码逻辑上看不出来。

4.3 调试器下载报错:Error: Flash Download failed - Target DLL has been cancelled

开发期调试时,你可能会遇到“Error: Flash Download failed - Target DLL has been cancelled”这个报错。我第一次碰到时以为是调试器坏了,后来才排查清楚,大多时候跟flash状态有关系。

常见原因和解决办法:

  1. 芯片当前处于低功耗模式,调试器连接后无法稳定访问flash。解决办法:让芯片先以正常模式复位,再连接调试器。
  2. flash被写保护,下载器无法正常擦写。解决办法:解除写保护,或者在调试器里勾选“unlock chip”类选项。
  3. 看门狗在复位芯片,调试器刚连上又被踢下去。解决办法:禁用看门狗喂狗动作,或者按住复位在特定时机连接。
  4. 目标flash处于busy状态。如果你前面有代码卡在while (FMC->STAT & FMC_STAT_BUSY),调试器自然下载不进去。这时检查FMC状态寄存器,必要时手动硬件复位。

4.4 写保护导致无法擦除

GD32L23x系列支持flash写保护,通过选项字节配置。一旦写了保护,调试器或App代码都无法擦写对应区域。OTA调试时如果你不小心把写保护级别设高了,可能出现“程序能跑、就是不能升级”的诡异现象。

解决办法是回退选项字节配置,把写保护关掉。注意,GD32的读保护级别一旦提升到最高级,只能通过重新烧录选项字节恢复,而且会触发整片擦除,这个操作对量产设备是有严格规划的,不要让普通用户能轻易触发。

4.5 擦写期间看门狗、低功耗和通信协议相互打架

OTA升级是个耗时操作,尤其是擦除大量页时。GD32L235一次页擦除大概几十毫秒量级,如果你OTA要擦几十上百页,加上写数据,总时间可能好几秒。这几秒里如果看门狗一直在跑,没喂够,中途复位,升级直接失败。

经验做法:

  1. 每一批页擦写完成后主动喂狗;
  2. 升级期间业务不太重要的话临时把看门狗时钟源切换或配置成较长超时;
  3. 擦写期间进低功耗模式更要小心:flash擦写时系统时钟如果被关掉,FMC的擦写时序会乱。我一直建议OTA过程中不要进任何低功耗模式,哪怕是STOP、STANDBY都不建议,除非你非常清楚FMC在低功耗时钟下的行为。

通信协议也建议做拥塞处理。OTA下载时一般是网络模块一边收数据一边写flash,如果你的通信模块和MCU共用总线,写flash占用了总线,通信数据包就会丢。所以要么给通信留够缓冲,要么用DMA双缓冲转发,把下载和写flash流水线化。

5. 最后再说点实在的:怎么选芯片、怎么做驱动层设计

说了半天,最终落到实际问题:你是该用L233还是L235做OTA产品?

我的建议是:如果你的固件本身就大于100KB,或者产品对升级可靠性、升级体验要求高,直接上L235,双bank带来的“升级不打断业务”体验值回票价,而且不需要处理单bank下“代码拷到SRAM执行”这种复杂逻辑。

如果产品flash吃紧、成本敏感,固件又不怎么做频繁OTA,L233也完全够用。把它当成传统IAP来弄:bootloader独立、升级过程短、界面提示“正在升级请勿断电”即可。很多家电产品、低功耗传感器设备都是这么跑的,稳定得很。

在驱动层设计上,我强烈建议把flash驱动单独抽象一层:

typedef struct { uint8_t (*init)(void); uint8_t (*erase_page)(uint32_t addr); uint8_t (*program_word)(uint32_t addr, uint32_t data); uint8_t (*check_busy)(void); uint8_t (*lock)(void); uint8_t (*unlock)(void); } fmc_drv_t;

这样你在L233上写完一套OTA框架,换到L235时只需要替换驱动实现,上层升级状态机完全不用动。我在做移植时,两个芯片的差异全部收敛到这一层,后面调双bank时只改底层,省了非常多时间。

最后分享一个我的个人习惯:不管用哪颗芯片,拿到手第一件事,先打开用户手册的Flash章节,把bank划分、页大小、等待周期、写保护方式用便签贴在工位上。OTA这种功能,出问题往往不是算法有多难,而是底层硬件的“性格”没摸透。L233和L235在这方面风格差异真的很大,早一点看清,后面能少熬好几个夜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询