简介:面向嵌入式开发者和单片机学习者,这份资源以富士通 MB95F318 的 Flash 操作为核心,演示其编程、擦除与校验流程,并覆盖 ISP 与 IAP 应用场景,便于理解在线与离线固件更新方式。压缩包共 29 个文件,约 79KB,包含 C 源码、头文件、汇编启动文件、Keil 工程文件及 .obj/.mhx 等编译输出,便于对照源码、目标文件与工程配置理解 Flash 操作实现。已有 469 人学习下载,可作为 MB95F318 固件开发入门的参考实例。通过示例代码可掌握 Flash 分块管理、写保护、超时处理等细节,也能直接复用工程模板或移植到自有项目。 去年做一款变频器控制器的时候,MCU选的是富士通FM3系列(MB9BF568)。产品已经量产了一百多台,结果客户突然提需求:希望后续固件更新不用开壳、不用拆回来,现场用串口就能升级。富士通这颗料不像ST那么大众,网上资料少,片内也没有出厂预置的ISP引导程序,要支持远程升级就只能自己做,于是我把这颗单片机片内Flash的擦写逻辑彻底摸了一遍。这篇文章就记录从原理到落地、再到排查下载器报错的完整过程,给正在用富士通单片机做Bootloader或者需要操作片内Flash的朋友一个参考。
1. 富士通单片机Flash操作的应用场景和基本概念
1.1 从产品升级需求引出IAP
"Flash操作"这个词听着很底层,但如果做产品,你迟早得面对它。常见场景就这么几个:一是Bootloader引导跳转,也就是IAP(In Application Programming),产品出厂后通过UART、CAN或者USB升级应用固件;二是参数存储,把设备的校准值、序列号、运行状态写进Flash,断电不丢;三是OTA服务器下发固件包,本地校验后写入Flash。这几个场景背后涉及的都是同一套东西:片内Flash怎么擦、怎么写、擦写到一半掉电了怎么办。
富士通FM3系列基于ARM Cortex-M3/M4内核,片内Flash容量从128KB到512KB不等,具体看你选的型号。这类芯片定位偏工控和汽车,很多用在电机驱动、变频器、PLC和医疗设备上。实际开发中和Flash打交道最多的就是Bootloader。FM3没有单独划分出一块专门给Bootloader使用的隐藏ROM区,所以引导程序和应用代码都在同一块主Flash里,地址空间你自己规划。
规划Flash布局时,我的做法是:Bootloader占最低地址区,往上是App起始区,最后留一块独立的参数存储区。FM3的Flash擦除操作按扇区来,不能跨扇区擦,所以Bootloader的大小必须压到一个扇区内,或者用两个相邻扇区,否则升级逻辑会复杂很多。
1.2 Flash与EEPROM的区别:为什么不能按字节写
很多刚从51单片机转过来的朋友会有一个误区:Flash是不是和EEPROM一样,随时可以按字节改?不是。EEPROM可以单字节擦写,Flash不行。Nor Flash的物理特性决定了"写1"容易、"写0"也容易,但要从0变回1,就必须整块擦除。所以Flash编程的逻辑是先擦后写,而且是整扇区擦除。
这带来一个实用结论:如果你的产品需要用Flash保存一些经常更新的参数,比如PID参数、用户设置,不建议每次都整扇区擦除重写。因为Flash擦写次数有限,FM3的片内Flash数据保持寿命是10万次擦写以上,听起来很多,但如果你每秒存一次参数,几天就磨穿了。更合理的做法是"日志式写入":只在扇区末尾追加新值,读的时候从后往前找最新的,写满一个扇区再擦。
另外说一句外部Flash。很多人看到"Flash"会想到W25Q64这类SPI Nor Flash,它的操作思路和片内Flash类似,也是先擦后写、扇区管理,但是寻址方式和命令时序完全不同。片内Flash直接挂在芯片内部总线上,通过寄存器触发编程,而外部SPI Flash要通过SPI协议发命令。这篇重点讲片内Flash,外部Flash只在Bootloader做固件暂存时会顺带提一下。
2. FM3片内Flash的擦除与编程原理
2.1 扇区划分与地址映射
用FM3之前,我先翻了型号对应的硬件手册,找到Flash那一章。FM3的Flash扇区划分在不同子系列里差别挺大,以MB9BF5xx为例,大致是:起始地址从0x00000000开始,低地址区可能是几个8KB的小扇区,后面是64KB的大扇区,顶部还有独立的配置区域用来存放加密位、时钟配置等。具体每个扇区的起始地址和大小,手册里有一张表,写代码前一定要把这张表找到并确认。
地址映射有个容易被坑的地方:Flash的起始地址是0x00000000,但向量表也在0x00000000。你在做Bootloader的时候,Bootloader和App都有自己的向量表,Bootloader的程序入口是0x00000000,App程序入口是你定义的偏移地址。擦除操作千万不要拿着地址随便擦,比如想把App区擦掉,结果地址算错一位,把Bootloader自己的程序擦了一半,设备直接变砖。
片内Flash还有一个独立的配置区,富士通叫"Security"或者"Option",里面有关闭调试口的保护位。这个区域一般不在主Flash的普通用户地址范围内,要用专门的命令操作。后面我讲下载器报错的时候,还会提到它。
2.2 命令序列与状态位轮询
FM3的片内Flash编程不是直接往地址写数据就行的,而是通过一个Flash控制器来执行自动编程算法。你往地址总线写数据,只是把数据放到了缓冲区,真正落盘要靠控制器来"触发"。
标准流程大致是这样的:
- 解锁Flash寄存器写保护,让CPU可以访问Flash控制寄存器。
- 写目标地址到Flash地址寄存器。
- 按手册要求往控制寄存器写入一系列命令字,通常是"编程使能"再"编程执行"。
- 等待Flash状态寄存器中的忙标志变为空闲。
- 查询编程结果状态,确认成功后再锁回寄存器写保护。
擦除操作也是类似流程,只是命令字不同。下面这段代码是FM3系列Flash操作最核心的骨架,我注释掉了具体寄存器位,重点是整个流程的顺序:
static flash_status_t fm3_flash_execute_command(uint32_t cmd, uint32_t addr) { /* 1. 解锁Flash控制器 */ FLASH_Unlock(); /* 2. 设置目标地址 */ FLASH->FADR = addr; /* 3. 写入命令序列,不同命令的时序由芯片手册定义 */ FLASH->CS = FLASH_CS_CST | cmd; /* 4. 轮询Busy位,等待控制器执行完成 */ uint32_t timeout = 0xFFFFF; while ((FLASH->CS & FLASH_CS_BUSY) && timeout--) { /* wait for flash idle */ } /* 5. 读取状态并锁定 */ flash_status_t st = (FLASH->CS & FLASH_CS_ERR) ? FLASH_ERROR : FLASH_OK; FLASH_Lock(); return st; }有一点要特别提醒:命令序列的时序非常敏感,步骤之间不能加多余的语句或者延时代码。我一开始在"写地址"和"写命令字"之间插了一句调试用的串口打印,结果Flash操作时而成功时而失败,排查了半天才反应过来。调试接口的数据输出会打乱指令流水,Flash控制器对时序窗口的要求很严格,所以正式代码里这段逻辑必须是纯粹的寄存器操作。
2.3 代码必须从RAM执行的隐藏规则
这是FM3最容易被忽视的一个坑:执行Flash擦写操作的时候,CPU执行的那段代码不能放在同一个Flash里。因为Flash控制器做擦写编程时,整个Flash阵列处于忙状态,无法响应取指访问,你在Flash里跑的代码会直接卡死或者取到全FF。
解决办法有两种。第一种是中断向量表、Flash驱动代码全部放RAM,Bootloader启动后先把自己搬到RAM再执行。第二种是写Flash的时候关闭中断,然后把擦写函数放到RAM中执行。因为中断向量表如果在Flash里,擦写Flash时一旦有中断进来,CPU去Flash取中断向量就取不到了。
在Keil MDK里把函数放到RAM,可以这样声明:
/* 将函数放到RAM执行,避免Flash忙时取指失败 */ #if defined(__CC_ARM) __attribute__((section("RAMCODE"), noinline)) #elif defined(__ICCARM__) __ramfunc #endif flash_status_t fm3_flash_erase_sector(uint32_t addr) { return fm3_flash_execute_command(FLASH_ERASE_CMD, addr); }在IAR环境下更简单,直接加__ramfunc关键字。但不管用哪种方式,都要确认链接脚本里预留了足够的RAM空间给这段代码和它调用的子函数。FM3的Flash驱动代码不大,一般几KB就够,但对于RAM本身只有几十KB的小容量型号,这个细节要提前预留,免得后面开优化或者加功能时链接报溢出。
3. IAP Bootloader中Flash驱动的实现
3.1 封装一个可复用的Flash擦写模块
看手册原理都能懂,真正难的是落地成稳定的工程代码。我建议把Flash操作独立成一个模块,至少提供四个接口:擦除指定扇区、写入一段数据、读取一段数据、整片校验。不要直接在上层业务代码里到处写FLASH->CS寄存器操作,否则后期维护会想骂人。
接口设计方面,这是我用了很久的一个版本:
#define APP_BASE_ADDR 0x00010000u #define BOOT_SECTOR_SIZE 0x00002000u #define MAIN_FLASH_END 0x00080000u flash_status_t fm3_flash_erase_sectors(uint32_t start_addr, uint32_t end_addr); flash_status_t fm3_flash_write_data(uint32_t dst_addr, const uint8_t *src_buf, uint32_t len); flash_status_t fm3_flash_verify_data(uint32_t src_addr, const uint8_t *expect_buf, uint32_t len);写入数据时,需要注意FM3的自动编程算法要求地址按4字节对齐,长度也建议按16字节对齐,不然边界处理会很麻烦。上面代码里,我在写入前对地址做了范围检查,原因就是一次误操作把Bootloader区擦了,那真的是半夜抱着仿真器哭的心都有。安全校验宁可做厚一分,不要薄一分。
实际往App区写固件时,我还做了分块写入。UART接收一包(比如512字节),CRC校验OK后,先暂存到RAM缓冲区,攒够一块再写入Flash。边收边写的方案遇到串口丢包时,会在Flash里留下半截固件,可靠性太差。我现在的流程是:先通过串口把整个固件包接收完整,放在一个足够大的RAM数组里,或者放在外挂SPI Flash里,全部校验完毕后再开始擦写片内Flash。这样掉电风险也小,因为擦写窗口很短。
3.2 跳转到App前的关键操作
Bootloader擦写完成之后,就要跳转到App区执行。跳转不是一刀就完事,有几个动作必须做,否则App跑起来就是HardFault:
- 关掉全局中断,把SysTick、外设中断全部停掉。
- 把栈顶指针(MSP)设为App向量表中的第一个字。
- 把PC指针指向App复位处理函数,即向量表中的第二个字。
- 跳转前把数据缓存、指令缓存失效掉,防止执行了旧代码。
FM3基于Cortex-M内核,参考代码是这样:
void jump_to_app(uint32_t app_base) { uint32_t stack_top = *(volatile uint32_t *)app_base; uint32_t reset_handler = *(volatile uint32_t *)(app_base + 4); __disable_irq(); /* 构造一个函数指针并跳转 */ void (*app_reset)(void) = (void (*)(void))reset_handler; __set_MSP(stack_top); app_reset(); /* 跳转成功后不会返回 */ while (1) { } }这里需要注意,接收固件包时Bootloader用的串口中断、DMA等,如果跳转前没有正确关闭,App启动时可能会收到残留中断,导致程序行为异常。我当初遇到的问题是:Bootloader里的串口接收中断没有完全关闭,跳转到App后,App一旦初始化串口,马上收到一个历史遗留的字符,误以为来了命令,直接进了错误分支。
3.3 中断向量表重映射
跳转之后,App代码里的中断向量表默认还是放在0x00000000,也就是Bootloader的地址位置,这样App里任何中断触发(比如SysTick、串口中断)都会跳回Bootloader的向量表,找不到对应的App处理函数,程序立即崩溃。所以App工程里必须重新设置向量表偏移。
在Cortex-M3/M4内核上,有一个寄存器叫VTOR(Vector Table Offset Register),专门用来设置向量表基地址。FM3的App工程里,main函数最开头加这一行:
SCB->VTOR = APP_BASE_ADDR;注意,这行代码必须在任何中断使能之前执行。我习惯放在SystemInit之后、所有外设初始化之前的第一行代码位置。还有一个坑:如果你的App使用RTOS,RTOS的启动文件也可能重设VTOR,两者要协调,别让RTOS在你没看的地方又把向量表指回0x00000000。
另外,编译App工程时,需要把链接脚本里的Flash起始地址改为APP_BASE_ADDR,否则编译器生成的代码地址还是从0开始。这个不难,在IAR的链接器配置里改一下ROM起始地址即可,Keil里改Target页的IROM1起始地址。
4. 下载器写Flash失败的排查链路
4.1 保护位和读锁导致的"flash download failed"
用J-Link或者Keil MDK往FM3下载程序时,如果目标板本身能跑、仿真器也能连上,但一点下载按钮就报:
Error: Flash Download failed - Target DLL has been cancelledflash download faild cortex-m3flash download faild cortex-m4
这类错误,我踩过之后把排查顺序固定成了下面几条:
第一个怀疑对象是芯片的安全保护位。FM3有"Security"机制,一旦通过专用命令设置了加密位,调试口就被锁死,外部仿真器无法读写Flash。这时仿真器虽然能访问内核,但无法操作Flash编程算法,下载就报错。解决办法是使用仿真器配套的unlock命令做全片擦除,把安全位擦掉。注意这个操作会把整个Flash清空,如果你里面有量产固件,请先把二进制备份出来。
第二个常见原因是用错了Flash下载算法。Keil的Flash Download页面里有一堆Flash Algorithm选项,必须选对FM3对应系列的. FLM文件。选成其他Cortex-M3芯片的算法,下载器一执行就会报奇怪的错误。每次拿到一颗新型号的富士通MCU,第一件事就是确认Flash驱动文件是否匹配型号和Flash容量。
第三个原因是芯片供电或者复位问题。Flash编程时电流需求会突然加大,如果电源线过细、供电能力不足,内核电压跌落,下载算法瞬间跑飞,同样会报DLL cancelled。
4.2 探针速率、复位和时钟的干扰
J-Link连接FM3目标板时,还有一个常见坑是SWD速率设太高。FM3默认上电后使用的内部时钟,频率不高,如果SWD时钟设成几MHz,调试器和CPU握手就可能不稳定。表现为:第一次能连上,第二次就连不上,或者掉电再上电后连接失败。把SWD频率降到1MHz以下,通常就稳了。
再有一个是复位引脚。FM3的复位脚上一般会有一个10nF到100nF的下拉电容,起上电复位延时作用。电容太大,复位引脚的低电平时间太长,下载器等待复位完成的时间不够,单片机还没从复位状态完全起来,下载器已经放弃握手了。我遇到过一次,板子复位脚上焊接了一个100nF大电容,J-Link死活连不上,换小电容之后立刻正常。这个现象在低温下更明显,因为电容低温容值会偏大,所以批量产品的复位电容最好控制在手册推荐范围内。
最后,时钟源也要检查。FM3的Flash擦写时序有一部分依赖主时钟频率。如果你在调试时把时钟初始化改成外部高速晶振,而板子上根本没有这个晶振,那不仅系统跑不起来,Flash下载也会失败。所以排查下载问题的时候,我会先看启动配置引脚和时钟使能状态,排除硬件时钟源与软件配置不一致的问题。
5. Flash可靠性设计:掉电保护与擦写寿命管理
5.1 双Bank区和数据校验
Bootloader做得差不多了,就要考虑升级过程中掉电怎么办。一次固件升级可能需要擦写几十KB的Flash,窗口按秒算。如果用户在这个窗口内突然断电,App区和Bootloader区都可能是损坏状态,设备就彻底变砖了。
我的做法是给固件更新增加"AB区备份"机制。FM3主Flash容量如果够,可以把代码区划分为三个部分:Bootloader区、AppA区、AppB区。平时运行在AppA区,升级时往AppB区写新固件,写完后校验CRC32和版本号,全部通过后把启动标志写入参数区,再重启到AppB区。如果写入中途掉电,AppA区还是好的,下次启动Bootloader发现AppB区的固件不完整,就继续从AppA区引导。这套方案在工程上叫Dual Bank OTA,可靠性远高于单区升级。
如果Flash容量紧张,退而求其次,也至少要做到"先写新固件到临时扇区,全部校验通过后再擦除目标扇区并复制"的思路,给掉电留一个恢复的退路。
5.2 擦写均衡的实际考量
Flash的擦写寿命问题,前面提过,这里展开一下。FM3手册给的Flash擦写寿命通常是以万次为单位,但这个数据是基于"合理使用"的。频繁擦写同一扇区,会让该区域提前老化,而其他扇区还很健康。
所以我做参数存储时,用的是"循环写入"策略。比如分配8个扇区,每4个一组,每组内按顺序写入最新的参数记录。每次只写一条记录到当前扇区的下一个空位,不用动其他扇区。等这一组的4个扇区都写满了,才擦写其中最早的那一个。这样实际磨损被摊薄到4倍甚至更多。反正也不复杂,就是在参数头里加一个序列号,读取时遍历找序列号最大的那条记录。
另外,固件升级用的参数区,最好单独划分两个扇区交替使用。每次升级前写一个"更新中"标志,升级完成再写"更新完成"标志。启动时Bootloader读取这个标志判断是否需要恢复。不要小看这个标志位,它用的是最普通的单次写,但能帮你把升级掉电的判断逻辑变得非常简单。
还有一个细节是Flash写超时。FM3的Flash控制器执行擦除编程,单片时间理论上是固定的。但如果异常情况导致Flash一直Busy,你的代码会卡死在轮询循环里。之前我把超时判断放成常量0xFFFFF,后来改成根据主频动态计算超时时间,比如100毫秒还没结束,直接返回错误并尝试重新初始化Flash控制器,避免系统真死掉。
最后我分享一个自己的体会:富士通单片机的Flash操作,核心难点不在API调用上,而在于你是否理解Flash的物理限制和异常场景。建议拿到评估板后,不要急着写Bootloader,先用手册里的官方例程把"擦除一个扇区、写一段数据、回读校验"这个最小闭环跑通,再做掉电测试。把调试器拔掉,反复插拔电源,看看板子能不能稳定恢复。这一步做到位了,后面的IAP、OTA都只是水到渠成的事。
本文还有配套的精品资源,点击获取