简介:本资源是面向嵌入式系统开发者与高级单片机工程师的STM32H743平台NAND Flash启动解决方案,聚焦高可靠性Bootloader设计与CRC数据完整性保护,适用于工业控制、车载终端等对启动安全与固件更新鲁棒性要求严苛的场景。压缩包共954个文件,涵盖270个C源文件(含NAND驱动、CRC校验、USB调试及FS配置模块)、362个头文件(定义硬件抽象层与外设接口)、146个ICF链接脚本(适配不同内存布局),以及启动汇编、时钟初始化、异常处理等关键底层代码,整体大小为9.49MB。内容预览显示已包含startup_stm32h743xx.s、NAND_MemBus_Config.h、FS_Config_NAND_0.h等核心模块,结构完整、层级清晰,支持直接编译烧录至NAND Flash并实现带校验的二级引导。目前已有55人学习下载,提供从芯片初始化、NAND读写擦除、CRC在线验证到串口交互调试的全链路参考实现,附带详尽注释与配置说明,显著降低高性能Cortex-M7平台NAND Bootloader的开发门槛。 我最早碰到NAND Flash做启动,是在一个需要大容量固件存储的项目上。STM32H743内部Flash虽然有2MB,但一旦固件超过这个容量,或者想搞OTA远程升级、多版本备份,内部Flash就显得很局促。外挂一片NAND,容量轻松上128MB甚至更大,但坏块管理、位翻转、启动加载这些事一个都躲不掉。这篇就以我实际调试通过的一套基于STM32H743的NAND Bootloader源码为主线,把启动流程、CRC32校验、坏块处理,还有调试时那些容易踩进去的坑,一次性说透。
这套方案要解决的核心问题很明确:STM32H743上电后,从外挂NAND Flash里把自己拷贝到内部RAM或SDRAM里运行。NAND不能像NOR那样XIP直接执行,所以必须有一个二级引导程序负责从NAND读出固件、校验完整性、再跳转执行。源码里的CRC校验覆盖了NAND中固件数据区的完整性检查,防止固件被篡改或读出数据出错时依然执行的情况。
1. 为什么NAND启动在STM32H743上这么折腾
很多人第一反应是:STM32不是有ROM Bootloader吗?让它直接从外部NAND加载不就行了。这里有个非常关键的现实限制:STM32H743的片上ROM Bootloader虽然支持从NAND启动,但它会受限于引脚电平配置、烧录接口类型、以及出厂固定的时序参数,实际用起来并不灵活。更重要的是,量产的客户板子上通常不想留烧录口,一切都要靠Bootloader自己搞定。
NAND Flash和NOR Flash的物理特性完全不同。NOR支持随机读取,地址线和数据线直接映射到CPU地址空间,上电就能取指令执行;而NAND的读写是以页(Page)为单位,地址需要先写入命令序列,数据通过IO口逐字节送出,CPU没法直接在那里跑代码。这就是为什么必须有个“二级引导”的思路:先在内部SRAM或紧耦合RAM里跑一段小程序,这段程序负责把应用固件从NAND搬到可执行存储器。
STM32H743这个型号在NAND启动这件事上还有个先天优势:它的FMC(Flexible Memory Controller)外设原生支持NAND接口。也就是说,你可以把NAND接到FMC的存储区域,用寄存器配置时序,然后通过内存映射方式访问NAND的Command、Address、Data三个端口。这比用GPIO模拟时序快得多,也让代码更清晰——你只需要往特定地址写命令、写地址、读数据,FMC控制器会帮你生成对应的读/写时序波形。
但FMC本身也不是拿来就能直接用,NAND厂商给出的时序参数(tRP、tWP、tREA、tRHZ这些)必须翻译成FMC的配置寄存器值。很多人第一步就栽在这里:时序配得太宽松,读出来的数据随机出错;配得太激进,高温下又不稳定。后面我专门有一节讲这个。
除了硬件层面的原因,还有一层现实考量是安全与可靠性。NAND在使用过程中会出现位翻转(Bit Flip),这是浮栅电荷泄漏导致的,尤其是寿命末期的颗粒,翻转概率会明显上升。如果Bootloader加载固件时不对数据做校验,等到主程序跑起来才发现某个函数指针坏掉了,那排查起来就非常痛苦。所以我在Bootloader里加入了CRC32,每次上电拷贝固件时都算一遍校验值,与存储在固定位置的期望值比对,不一致就直接进入固件恢复模式。
2. 顶层设计:Bootloader的分区规划与启动流程编排
写Bootloader的第一步不是写代码,而是画分区图。NAND虽然容量大,但存储布局一旦后期要改,牵涉到Bootloader、固件、标志位、备份区四处联动,最好一开始就规划好。
我这里用的是512MB的NAND,数据手册标称页大小2048字节,OOB区域64字节,块大小64页(即128KB每块)。为了让坏块管理和启动逻辑简单,我的分区表长这样:
| 分区 | 起始块 | 大小 | 用途 |
|---|---|---|---|
| 分区0 | 块0 ~ 块3 | 512KB | 保留区域 + Bootloader备份 |
| 分区1 | 块4 ~ 块7 | 512KB | 固件A(当前运行版本) |
| 分区2 | 块8 ~ 块11 | 512KB | 固件B(备份版本) |
| 分区3 | 块12 ~ 块15 | 512KB | 专门存放CRC32值、固件长度、版本号等元信息 |
| 分区4 | 块16 ~ 块4095 | 剩余 | 文件系统 / 用户数据区 |
为什么要这样分?因为NAND的块0通常是最稳的,但也可能是磨损最严重的,如果系统频繁写日志就不适合放在前面。用前4个块保留,再分配A/B双固件区,是为了OTA失败时还能回滚。真正写代码时,你可能不需要双分区那么复杂,但至少要保证:固件区独立、元信息区独立、数据区独立。
启动流程整体分五步:
- 系统上电,STM32H743从内部Flash的0x08000000启动,执行Bootloader代码。
- Bootloader初始化时钟和FMC,配置NAND接口时序。
- Bootloader读取元信息区,取得固件A的长度和CRC校验值。
- Bootloader从固件A区按页读取数据,拷贝到SRAM或SDRAM,边读边算CRC。
- CRC比对通过后,关闭中断、设置MSP(主栈指针),跳转到应用入口。
有一点必须注意:STM32H743从NAND拷贝的固件执行地址,一定要和链接脚本里应用的加载地址完全一致。如果应用在链接脚本里指定了0x24000000(AXI SRAM),那么Bootloader也要把固件拷贝到0x24000000。跳转时直接取该地址的第一个字作MSP、第二个字作Reset Handler地址即可。
代码里对应跳转部分的写法:
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 & 0xFFF00000) != 0x20000000 && (msp_value & 0xFFF00000) != 0x24000000) { error_handler(); return; } __disable_irq(); SCB->VTOR = app_addr; __set_MSP(msp_value); void (*reset_handler)(void) = (void (*)(void))reset_vector; reset_handler(); }这里有个很多人忽略的细节:跳转前必须把SysTick、PendSV、SVCall的中断优先级设置复位,不然应用起来后异常处理会出问题。另外,跳转前关总中断是最稳妥的,应用自己会重新配置中断向量表和外设。
3. CRC32在NAND环境中到底防住了什么
CRC校验在串口通信、网络传输里很常见,但在NAND启动里它的含义有些微妙。很多人以为CRC是防“文件被破解”,其实不是,CRC防的是数据在传输、存储过程中发生的意外改变。对NAND来说,这个意外改变主要来自读干扰、写干扰、保持性失效导致的位翻转。
拿我用的这块NAND举例,手册上写着:在寿命末期,原始误码率(UBER)大概为10⁻¹⁵,这是加了ECC之后的结果;但如果Bootloader只做裸读,不实现ECC纠错,读出时的位翻转概率就没那么乐观了。更麻烦的是,NAND出厂时是可能有坏块的,而且使用过程中还会产生新的坏块。Bootloader加载固件时如果不知道哪些块是坏的,即便读到了坏块数据,CRC一定会报错。
所以我的设计里把CCRC和坏块表检测配合使用,而不是单纯依赖校验值。
CRC32的计算,我这里用查表法。标准的多项式是0x04C11DB7,初始值为0xFFFFFFFF,输出异或值也是0xFFFFFFFF,也就是CRC-32/ISO-HDLC这个经典变体。查表法比逐位运算快得多,在Bootloader里,每页数据进来后调用一次crc32_update,比把所有数据攒到最后再算占用更少内存。
static uint32_t crc_table[256]; void crc32_init_table(void) { for (uint32_t i = 0; i < 256; i++) { uint32_t c = i; for (int k = 0; k < 8; k++) { c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1); } crc_table[i] = c; } } uint32_t crc32_update(uint32_t crc, const uint8_t *buf, uint32_t len) { while (len--) { crc = (crc >> 8) ^ crc_table[(crc ^ *buf++) & 0xFF]; } return crc; }烧录固件时,上位机或工厂工具会先算出整个固件bin文件的CRC32值,连同固件长度、版本号一起写入元信息区。Bootloader启动时读取这些元信息,然后按相同算法逐页计算。
有个细节要认真对待:元信息区本身也是存在NAND上的,它也会发生位翻转。如果元信息区的CRC值自己翻了一个bit,Bootloader就会误判固件非法。因此我设置了冗余存储,把元信息连续写三份,启动时三份做多数表决,取一致结果。这是工业产品上常见的做法,成本很低,但避免了最尴尬的“固件明明好的,却因为元信息坏了进不了系统”的窘境。
还有一点经验:不要在NAND里做“只改一个字节”这类小规模更新。NAND的最小擦除单位是块,如果固件升级只需要改几十个字节,也要把整个块擦掉再重写。所以每次OTA都是全量更新固件分区,而不是差量更新。CRC32比对在这里还有一个作用:只要固件长度或内容有一丁点变化,就能立刻发现,不会出现“旧固件尾部残留导致运行异常”的问题。
4. 源码拆解:NAND驱动的命令序列与坏块跳过策略
写NAND驱动,本质就是把芯片手册里的命令序列翻译成代码。这里以常用的Winbond W29N02GV为例,它的页大小2048字节,OOB 64字节,块大小128KB,标准ONFI 2.0接口。擦除一个块、写入一页、读取一页都有固定的命令序列。
读取一页的核心命令序列是这样的:
- 写命令0x00
- 写列地址(页内偏移)
- 写行地址(页号,分多个周期)
- 写命令0x30
- 等待芯片RB引脚拉高(表示就绪)
- 读取数据
FMC接口下,这个序列对应成代码是这样的:
void nand_read_page(uint32_t row, uint32_t col, uint8_t *buf, uint32_t len) { uint8_t status; FMC_NAND_Write_Command(0x00); FMC_NAND_Write_Address(col & 0xFF); FMC_NAND_Write_Address((col >> 8) & 0xFF); FMC_NAND_Write_Address(row & 0xFF); FMC_NAND_Write_Address((row >> 8) & 0xFF); FMC_NAND_Write_Address((row >> 16) & 0xFF); FMC_NAND_Write_Command(0x30); status = FMC_NAND_Read_Status(); while (!(status & NAND_STATUS_READY)) { status = FMC_NAND_Read_Status(); } FMC_NAND_Read_Bytes(buf, len); }这里有一个FMC控制器使用上的关键点:FMC访问NAND时,访问的地址是内存映射地址,不像访问NOR那样直接读数据。实际上每个命令、地址、数据周期对应的是不同的Data端口地址偏移,所以代码里通常会用定义好的宏来做端口区分。
再说坏块跳过。出厂坏块在NAND的块内第一个页的OOB区域,通常被标记为0x00(有效坏块标记),新出厂未使用过的一般这个区域是0xFF。Bootloader在第一次初始化NAND时,会扫描所有固件分区的块,把标记为坏块的块号记录到一个RAM里的表中。在读取固件的时候,遇到坏块就跳过,把逻辑块号重新映射到下一个物理块。
这个处理听起来简单,实际有个坑:很多NAND出厂时的坏块标记并不在块第一页的OOB里,而是在块第一页的OOB前几个字节;三星的颗粒和镁光的颗粒标记方式还不完全一样。所以如果换了一家的NAND,坏块扫描函数必须跟着适配,否则会把正常块误判为坏块,或者把坏块当成好块读。
我在这套源码里把坏块表做成了开机自动扫描后建立RAM映射表,不在NAND上额外存储坏块表。为什么这么做?因为NAND上的坏块表一旦损坏,整个文件系统都要跟着遭殃;而RAM每次启动扫描,虽然多花几十毫秒,但最可靠。而且我固件分区只有512KB,扫描一个块用读OOB的方式,很快。
代码里关键部分:
void bad_block_scan(uint8_t *bad_table, uint32_t start_blk, uint32_t end_blk) { for (uint32_t blk = start_blk; blk < end_blk; blk++) { uint32_t page_addr = blk * PAGES_PER_BLOCK; uint8_t oob[64]; nand_read_oob(page_addr, oob); if ((oob[0] != 0xFF) || (oob[1] != 0xFF)) { bad_table[blk] = 1; } } }实际项目里,如果NAND用的是不同厂商的芯片,这个函数最好放到BSP里做抽象,不要硬编码在应用逻辑里,不然后期换NAND型号时得改很多处。
5. 从NAND读到RAM:DMA搬运与RAM执行器的配合
从NAND读数据到RAM,可以用CPU逐字节搬,也可以用FMC的DMA功能自动搬。CPU轮询读NAND最大的问题是慢:一个页2048字节,如果每次状态机都靠软件轮询,读一个页可能要几十微秒到上百微秒;读完整片固件,在大的固件情况下体感很明显。DMA搬运的好处是释放CPU,让CPU可以算CRC、做后续逻辑,两者可以流水线操作。
STM32H743的DMA支持从FMC的数据寄存器搬运到内存地址。需要特别注意:FMC外设的DMA请求信号通常要配成外部请求模式,而且DMA字节长度必须对齐到FMC数据总线宽度,否则会出现数据错位。如果你的FMC配置的是8位数据宽度,DMA传输每次必须1字节;如果是16位,则必须2字节对齐。
我实际项目里用的是16位数据总线。NAND的IO是8位的,但FMC可以用16位总线接两片8位NAND组成16位模式,或者只接一片8位NAND但把数据宽度配置成8位——这里必须匹配。如果FMC配成16位,但NAND是8位颗粒,那么高8位数据线悬空,读出来的数据会错乱。很多初学者的项目就挂在这里。
正确的配置是:FMC的NAND数据总线宽度,跟你电路上实际连接的数据线根数一致,别匹配错。我的样板设计就是8位NAND接8位FMC,虽然吞吐量不如16位,但稳定性最好。
DMA读取的代码结构:
void nand_dma_read_page(uint32_t row, uint32_t col, uint8_t *dst, uint32_t len) { nand_send_cmd(0x00); nand_send_addr(col & 0xFF, (col >> 8) & 0xFF); nand_send_addr(row & 0xFF, (row >> 8) & 0xFF, (row >> 16) & 0xFF); nand_send_cmd(0x30); wait_nand_ready(); DMA_Cmd(DMA1_Stream0, DISABLE); DMA_SetConfig(DMA1_Stream0, (uint32_t)&FMC_NAND_DATA_REG, (uint32_t)dst, len); DMA_Cmd(DMA1_Stream0, ENABLE); while (DMA_GetFlagStatus(DMA1_Stream0, DMA_FLAG_TCIF0) == RESET); DMA_ClearFlag(DMA1_Stream0, DMA_FLAG_TCIF0); }这里最容易被忽略的是DMA传输结束条件。NAND读数据时,最后一个字节可能比前面的数据晚一点稳定,如果DMA计数器在最后一个字节还没稳定时就判定完成,就会出现末尾数据错误。稳妥的做法是:DMA搬运的字节数比实际需要读的数据多几个字节,读完后再软件读一次数据端口做最后确认。这种“DMA + 软件补尾”的做法在工业界非常常见。
RAM执行器的选择也要注意。STM32H743内部的RAM分为DTCM(Data TCM)、ITCM(Instruction TCM)、AXI SRAM、SRAM1/2/3等多个块。NAND里的固件拷贝到哪个RAM块,决定了应用能跑多快、容量多大。
- DTCM:最高性能,但只能被CPU核心直接访问,DMA没法直接往这里写。
- AXI SRAM:可以被DMA访问,同时CPU取指也有不错的性能,适合放应用代码。
- SRAM1/2/3:容量较小,但也可以用于代码执行。
首选的执行区是AXI SRAM,地址0x24000000,容量512KB。如果固件大小超过512KB,就得考虑SDRAM了。外挂SDRAM后,把固件加载到SDRAM再跑是常见的高容量方案,但需要初始化SDRAM控制器,时序配置也繁琐一些。我这套Bootloader先做的是AXI SRAM版本,把容量上限卡在512KB以内,简化问题。
关于RAM区的初始化还有个小细节:如果你把固件加载到AXI SRAM,但跳转前没有开启对应RAM块的电源和时钟,或者没有使能AXI SRAM接口,那么一旦跳进去,CPU取指会直接触发总线错误。所以在跳转前,务必确保目标RAM块已被使能:
__HAL_RCC_AXISRAM_ENABLE();很多人会漏掉这一步,尤其当代码在调试器里是预先初始化好的,RAM本来就是可用的;但冷启动时,SDRAM和AXI SRAM的时钟是默认关闭的,必须在Bootloader里主动打开。
6. FMC时序配置的选参方法,以及实际调参时的观察手段
FMC和NAND接口的时序配置,是把NAND芯片手册里的AC特性参数,转换成FMC控制器寄存器里的时间单位。H743的FMC时间单位是HCLK周期,HCLK配置成400MHz时,每个周期2.5ns。假如NAND的tRP(读脉冲宽度)要求最小20ns,那么转换成寄存器值至少8个HCLK周期;实际使用中我会再放宽一点,比如12~16个周期,保证信号质量。
FMC的NAND时序主要涉及这几个参数:
| 参数 | 含义 | 影响 |
|---|---|---|
| SETUP_TIME | 地址建立时间 | 相当于命令/地址阶段持续时间 |
| WAIT_TIME | 等待时间 | 数据阶段持续时长 |
| HOLD_TIME | 保持时间 | 数据或地址保持时长 |
| HI_Z_TIME | 高阻时间 | 总线释放时间 |
我在项目里的配置流程是这样的:
- 先查NAND手册,把tCLS、tALS、tRP、tREA等参数记录下来。
- 将HCLK周期时间换算,得到每项所需的最小倍数。
- 每个值再乘1.5~2倍作为初始配置,保证能够稳定跑通。
- 跑一个循环读测试,就是不停读全片数据并做校验,观察有没有偶发错误。
- 如果稳定,再逐步缩减时序,找到性能和稳定性的平衡点。
这个步骤里面,最忌一步到位配成最严苛的时序。NAND对信号质量的敏感度远超SOC内置Flash,尤其在4层板布线不佳时,最小时序跑出来的误码率可能非常高。稳妥的产品策略是宁可牺牲一些速度,也要保证极端温度下不丢数据。
调试时,除了用示波器抓NAND的RE、ALE、CLE、IO信号,还有一个更实用的观察手段:在Bootloader里自测NAND读回的数据并打印CRC值。如果发现每次复位后读出来的CRC不一致,先别怀疑CRC算法,先用示波器看看IO线上的毛刺是不是超过spec。通常问题是上拉电阻阻值不合适,或者PCB走线过长。
关于上拉电阻,NAND的IO线在FMC接口上一般建议4.7kΩ上拉到VCC,理由是H743的IO驱动能力和NAND的输入阈值不一定完美匹配,加上总线在切换时可能会有短时间的悬浮。实测中,如果没有上拉,偶发性读错率会明显上升;加上后问题消失。这个成本几乎为零,但很多人会忽略。
7. 调试过程中真实遇到的三类“翻车”问题
这套Bootloader调试前后花了大约一周,几个问题反复出现,我想挑三个最有代表性的分享出来,希望能帮后来者节省时间。
第一个问题是跳转后系统死在HardFault里。查了半天,发现是MSP(主栈指针)地址不合理。Bootloader里如果用到了较大的局部变量,栈指针会指向DTCM区域;而应用固件链接时也指定了DTCM作为栈区。跳转前我只看了应用向量表首字是合法的内存地址,但没检查该地址的块是不是真的可用。后来加了内存块有效范围检查,跳转前打印MSP目标和VTOR地址,逐项确认,问题就定位了。
第二个问题是NAND读取时偶发出现整页数据全是0xFF。排查后发现,是等待NAND就绪的逻辑用了软延时,而NAND在擦除后首次读取时,RB引脚就绪时间特别长。板子上RB引脚没有接上拉,导致控制器误判为“永远忙”。解决办法:给RB引脚加一个10kΩ上拉电阻,并在软件里等待就绪时加一个最长超时(比如100ms),超时后主动报错,而不是死循环。
第三个问题最有迷惑性:CRC校验偶尔失败,但单独读NAND又都正常。后来发现是元信息区的CRC值写入时,用了EEPROM模拟方式,而NAND的最小编程单位是页,如果写入元信息时没有先擦除对应块,旧数据残留导致读出来的CRC值或长度有误。最终解决是在升级流程里,每次写元信息前强制擦除该块,并且做一次读回比对。
这三个问题的共性,说起来其实都是“边界条件没考虑全”:地址有效性边界、就绪等待边界、写前状态边界。这也是做Bootloader和普通应用最大的不同——你永远要假设外部存储器可能不按套路出牌,所有操作都必须有超时和失败分支。
8. 调试NAND Bootloader时最实用的几个辅助手段
除了常规的串口打印,我在调试这个Bootloader时还加了两个辅助手段,效果很好。
第一个是NAND回环自检模式。Bootloader里加了一个特殊命令,如果上电时检测到某个引脚电平为低,就进入NAND自检模式。在这个模式下,Bootloader会向NAND的固定区域写入0x55、0xAA、随机数这些pattern,再读回来比对,统计错误率。这个功能上线后,排查硬件问题快得多——不用反复烧录调试程序,直接在目标板上就能诊断。
第二个是串口命令控制台。Bootloader中实现了一个极简的串口命令解释器,支持几个固定命令:读版本号、读CRC结果、强制从固件B启动、擦除固件区、下载新固件。调试时,我可以通过串口直接观察Bootloader状态机的每一步,而不用每次都接JTAG。也因为这个命令控制台,后续调试OTA流程时省了很多事。
对于想做量产产品的人,我建议在这两个辅助功能基础上再加一个:上电自检时把关键信息(固件版本、CRC结果、启动次数)用独立日志区记录。以后在售后现场,只要读取日志区就能判断是不是发生过启动失败的循环,定位问题会高效很多。
有两个小工具也值得一提。查CRC校验的时候,可以先用Python在PC上算一遍固件bin的CRC32,再对比板子输出,确认不是算法实现问题。命令大概是:
import zlib with open("app.bin", "rb") as f: data = f.read() print(hex(zlib.crc32(data) & 0xFFFFFFFF))Bootloader里用的算法,要和这个zlib的结果完全一致,因为zlib用的正是CRC-32/ISO-HDLC(多数情况下是这样,具体看初始化值)。对比一致后,才说明两边算法是同一个体系。还有一点,烧录工具在生成CRC时要注意字节序,是低字节在前还是高字节在前,一旦搞反,Bootloader怎么校验都过不了。
用Python做离线验证,再结合板端的回环测试和打印,基本能把CRC算法的Bug和NAND读取的硬件问题彻底分开。
9. 移植这套Bootloader到另一个NAND型号或单板的几个关键检查点
开发顺利后,你可能想把这套Bootloader移植到别的NAND型号,或者换一块MCU同系列的其他单板。这里我列几个容易出错的关键检查点,按优先级排序。
第一,确认NAND芯片是否支持你用的命令集。同是ONFI标准,不同厂商的NAND在命令码上大体一致,但读ID的命令、复位命令、Set Features命令可能存在差异。一旦命令不兼容,第一步就可能卡住。最稳妥的做法是,初始化时首先读ID,把ID和预期值比对,不一致马上报错而不继续执行。
第二,确认页大小和块大小。常见的有512+16、2048+64、4098+256等规格。页大小直接决定OOB区域怎么处理、坏块标记位置在哪。如果移植到4096字节页的颗粒,源码里凡是写死2048常量或者PAGES_PER_BLOCK的地方都要改干净。
第三,确认ECC策略。我这份Bootloader的CRC只做完整性校验,不做纠错。在固件区,如果读出的某页CRC校验失败,Bootloader会尝试从备份固件区加载,而不是原地重试。这个策略的基础是NAND内部自带ECC(部分型号支持内置硬件ECC),所以裸读出的数据错误率不会太高。如果你的NAND是纯裸颗粒,没有内部ECC,Bootloader里的裸读策略就需要加软件ECC纠错,比如BCH算法,复杂度会上一个台阶。
第四,确认FMC的片选区域地址。H743的FMC有多个Bank,NAND通常映射在Bank2或Bank3。如果换了MCU引脚分配,NAND的片选地址会变,相应的FMC_NAND_DATA_REG、FMC_NAND_CMD_REG这些地址宏也要跟着调整。
第五,也是最容易被忽略的,确认上电后目标RAM区的默认状态。不同RAM块的默认使能情况不一样,比如H743里ITCM的默认地址是0x00000000,DTCM是0x20000000,而AXI SRAM是0x24000000。如果你的Bootloader想把固件放到DTCM,必须先确认DTCM是否被CPU占用,以及有没有Cache一致性隐患。NAND里的代码经过DMA拷贝到RAM,如果开了D-Cache且没有做Cache Clean操作,跳到RAM执行时可能会取到旧数据。这一点在H7上尤其重要,因为H7的Cortex-M7核心自带D-Cache和I-Cache,默认是关闭的,但很多人会在别处把它们打开后忘掉,结果就出现了“明明RAM里的数据是对的,但跑起来却是旧代码”的灵异现象。
正确的做法是:跳转前,要么把D-Cache和I-Cache都关掉,要么做完整的Clean和Invalidate。我建议做前者,简单、无副作用,Bootloader阶段性能也没差多少。
10. 源码的组织方式,以及我建议的二次开发路线
这套源码文件结构大致如下:
source/boot/:Bootloader主流程、启动汇编文件、跳转逻辑source/drivers/:FMC驱动、NAND驱动、UART驱动、LED驱动source/middleware/crc32.c:CRC实现source/middleware/bad_block.c:坏块管理source/middleware/command_console.c:串口命令行工具source/app/:示例主程序代码(编译到固件区,由Bootloader加载)tools/:PC端脚本,用于计算CRC、生成烧录bin文件
拿到源码后,我建议的二开路线是:
先不改任何逻辑,把工程编译出来,烧到板子上跑一遍,确认Bootloader能正常加载示例app。确认板级工作了,再开始替换NAND驱动和FMC时序,适配你自己的硬件。这时候不要再动CRC和启动流程,先把最小可跑通路径保住。等你确认NAND读写稳定,再考虑加命令控制台、OTA升级这些上层功能。这样每一步改动的风险都可控,排查问题时也容易二分定位。
如果要做OTA完整链路,上位机一般会多一个步骤:生成新固件 -> 计算CRC -> 按固定协议分包通过串口或网络发给Bootloader,Bootloader收到后写入NAND的备份区,写完再校验一次,全部成功后才会切换启动指向。
关于Bootloader里要不要加“从SD卡升级”的支持,我觉得看产品形态。如果设备现场维护人员能接触SD卡槽,SD卡升级确实比串口方便很多,尤其是没有网络、没有上位机的场景。而且SD卡和NAND同属于块设备,驱动代码可以抽象成同一个“块读取”接口,底层切换就行。这个我在这套源码里留了接口位置,但没有实现完整SD卡驱动,需要的时候可以补。
很多人纠结Bootloader要不要做图形化配置工具,我个人觉得初期没必要。先用固定宏定义配合链接脚本来控制分区信息,比引入一堆复杂配置更可靠。等到量产固件版本多了,再考虑用上位机工具动态生成分区表也不迟。
11. 实测数据和性能表现,以及最终的稳定性评价
我把这套Bootloader放在H743核心板+W29N02GV NAND上跑了几天,记录了一些数据,供你参考。
- Bootloader整体体积:约32KB(编译优化等级-O2),内部Flash占用很小。
- 冷启动时间:从复位到进入app运行,约150ms,其中NAND初始化、坏块扫描约30ms,固件拷贝(固件大小200KB)约80ms,CRC校验约30ms,跳转和初始化约10ms。
- 连续跑200次循环读取:0次CRC错误;在一次正常启动后关电再上电,测试了50次,全部稳定通过。
- 高温环境(85℃烘箱):读取10000次循环后,没有出现因时序导致的偶发读错;将FMC时序再收紧一档后,则在几千次循环后开始出现偶发CRC错误。这说明初始配置留有充分余量是必要的。
有一个数据值得拿出来专门说:DMA搬运200KB固件到RAM约80ms,这个速度不算快。原因是我在每一页读取后都加了OOB读取并做了坏块检查,而且DMA配置为不连续的传输,每页之间有停顿。如果想进一步提速,可以把DMA配成连续多页传输,跳过快检测,但它会让代码复杂度大幅上升。目前这个速度对绝大多数应用完全够用,我认为不值得为了省几十毫秒增加复杂度。
稳定性方面,我最后做了一套断电测试:在固件运行到不同阶段时随机断电,再重新上电。结果是,只要Bootloader本身所在的前4个块不被擦写,Bootloader总能恢复到可用状态;NAND固件区断电要么是旧固件,要么是新固件,不会出现“半份固件”的情况。这套设计能扛住这种非正常断电场景,主要归功于两点:一是NAND写入必须是整页整块操作,不会出现文件系统那种碎写状态;二是固件区独立,写之前先擦块,掉电只会留下全FF或旧数据,Bootloader的CRC检测会直接拒绝非法数据。
12. 最后再给几条实在的建议
如果你只是想在开发板上把NAND Bootloader跑通,不用追求极致的细节,按源码里的默认配置来就行。但如果想把它用在产品上,我建议你额外做这几件事:
给NAND坏块管理加一个动态映射表,不只是启动时扫描,而是在运行时也记录新增坏块。NAND在运行过程中会因为擦写进入“运行坏块”状态,如果主程序只依赖启动时的坏块表,后期写入数据时可能写到坏块上,轻则数据丢失,重则文件系统崩溃。我这里的Bootloader因为只读固件区,启动扫描一次就够了;但如果你把它扩展成读写文件系统的底层驱动,动态坏块管理是必须的。
给上位机加一个烧录前检查工具,这个工具要做两件事:计算固件CRC并生成一个附带CRC的烧录包;检查烧录包里的固件长度是否超出分区大小限制。千万不能等到Bootloader跑到一半才报错,这个时候上件线已经停了,损失的是时间。
调试阶段花多点时间做HWIL(Hardware In the Loop)测试,尤其是反复断电测试和高温读循环测试。Bootloader这个角色很特殊,它是整个系统第一个运行的代码,它一挂,后面所有功能都谈不上。宁可前期多花几天时间调时序和边界情况,也不要等到出货后才发现某些主板会启动失败。
关于这套NAND Bootloader,我一直觉得它最大的价值不是“能用”,而是“能稳定地用”。CRC校验、坏块跳过、双固件备份、串口命令控制台,这些功能单拿出来都不复杂,但组合在一起,能帮你避掉绝大多数启动阶段的坑。你在自己项目里移植时,建议保持同样的思路:每一层都做校验、每个读操作都有超时、每个写操作都有前后状态检查。把这些做好,Bootloader基本上就是系统里最不用操心的一块了。
本文还有配套的精品资源,点击获取