ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GD32L233与GD32L235 Flash架构对比及OTA升级方案设计

GD32L233与GD32L235 Flash架构对比及OTA升级方案设计 前段时间帮客户调一套基于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部分型号256KBFlash 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上叫FMCFlash Memory Controller统一管理。CPU取指和数据访问都可以直接映射到flash地址空间比如0x08000000就是主flash的起始地址。反正记住一点内部flash的地址空间和外部flash不一样它和总线是紧耦合的访问速度远比任何外部存储器快但代价就是“擦写时不能同时取指”这类限制。OTA中最底层的内容都是围绕这个限制做文章。2. 读写flash的寄存器级操作到底哪里不一样2.1 公共流程解锁 → 操作 → 等待BSY → 清标志先讲相同点。L233和L235的FMC操作流程基本一致都是标准的一套解锁FMC寄存器写入专用解锁序列配置操作模式页擦除、字编程等发起操作并等待忙标志清除检查错误标志位重新加锁。解锁序列是固定的直接写寄存器操作大致如下/* 标准解锁序列 */ 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切到bootloaderbootloader接管刷写刷完重启。这段时间用户如果操作设备一般是无响应的。3.2 L235双Bank乒乓升级的正确姿势L235的双bank给OTA带来的最大红利就是真正实现无感升级升级过程中系统照常运行升级完成后只需做一次跳转重启。这才是很多产品想要的OTA体验。具体分区可以这样设计以512KB L235为例区域地址大小内容Bootloader0x0800000032KB引导、恢复出厂、flash驱动Bank0 App0x08008000224KBApp版本ABank1 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还是L235OTA最后一跳都是跳转到新固件这里有个高频坑就是中断向量表。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校验值。推荐流程设备上报当前版本号后台下发新固件包设备按4字节对齐接收数据逐字写入目标flash区域写入过程中记录已写偏移量断点续传时可以接着写全部写完后整片读出flash数据和固件包CRC比对校验通过后写启动标志校验失败则保留旧标志、下次仍启动旧固件。其中第4步尤其重要。因为Flash写入有底层的位翻转风险通信传输也有偶发错误任何一层漏了都可能让“校验通过但运行即崩溃”的概率上升。CRC32虽然简单但对OTA这个场景足够用。在L235双bank方案里建议做整bank的CRC计算开销可控。4. 移植和调试中的高频坑与排查思路4.1 擦写过程中程序跑飞、HardFault这个是我的老客户踩得最多的问题。现象App做OTA时程序在擦除flash某个页时直接进入HardFault或者擦写完一次就死机。排查方向确认擦写地址和当前执行代码是否处于同一个bank。L233没有任何回旋余地擦写前必须确保代码在SRAM或bootloader里L235则确认PC所在的bank和目标地址不同。确认擦写期间中断是关闭的或者中断服务程序所在flash区域没有被擦写。擦写flash期间触发中断中断服务程序里的代码若在同一区域就会导致总线错误。确认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状态有关系。常见原因和解决办法芯片当前处于低功耗模式调试器连接后无法稳定访问flash。解决办法让芯片先以正常模式复位再连接调试器。flash被写保护下载器无法正常擦写。解决办法解除写保护或者在调试器里勾选“unlock chip”类选项。看门狗在复位芯片调试器刚连上又被踢下去。解决办法禁用看门狗喂狗动作或者按住复位在特定时机连接。目标flash处于busy状态。如果你前面有代码卡在while (FMC-STAT FMC_STAT_BUSY)调试器自然下载不进去。这时检查FMC状态寄存器必要时手动硬件复位。4.4 写保护导致无法擦除GD32L23x系列支持flash写保护通过选项字节配置。一旦写了保护调试器或App代码都无法擦写对应区域。OTA调试时如果你不小心把写保护级别设高了可能出现“程序能跑、就是不能升级”的诡异现象。解决办法是回退选项字节配置把写保护关掉。注意GD32的读保护级别一旦提升到最高级只能通过重新烧录选项字节恢复而且会触发整片擦除这个操作对量产设备是有严格规划的不要让普通用户能轻易触发。4.5 擦写期间看门狗、低功耗和通信协议相互打架OTA升级是个耗时操作尤其是擦除大量页时。GD32L235一次页擦除大概几十毫秒量级如果你OTA要擦几十上百页加上写数据总时间可能好几秒。这几秒里如果看门狗一直在跑没喂够中途复位升级直接失败。经验做法每一批页擦写完成后主动喂狗升级期间业务不太重要的话临时把看门狗时钟源切换或配置成较长超时擦写期间进低功耗模式更要小心flash擦写时系统时钟如果被关掉FMC的擦写时序会乱。我一直建议OTA过程中不要进任何低功耗模式哪怕是STOP、STANDBY都不建议除非你非常清楚FMC在低功耗时钟下的行为。通信协议也建议做拥塞处理。OTA下载时一般是网络模块一边收数据一边写flash如果你的通信模块和MCU共用总线写flash占用了总线通信数据包就会丢。所以要么给通信留够缓冲要么用DMA双缓冲转发把下载和写flash流水线化。5. 最后再说点实在的怎么选芯片、怎么做驱动层设计说了半天最终落到实际问题你是该用L233还是L235做OTA产品我的建议是如果你的固件本身就大于100KB或者产品对升级可靠性、升级体验要求高直接上L235双bank带来的“升级不打断业务”体验值回票价而且不需要处理单bank下“代码拷到SRAM执行”这种复杂逻辑。如果产品flash吃紧、成本敏感固件又不怎么做频繁OTAL233也完全够用。把它当成传统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在这方面风格差异真的很大早一点看清后面能少熬好几个夜。
返回列表