ARTICLE DETAIL

资讯详情

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

Keil自定义FLM下载算法:STM32外挂SPI Flash烧录与调试实战

Keil自定义FLM下载算法:STM32外挂SPI Flash烧录与调试实战 上周帮同事调试一块新做的板子主控是STM32F407外挂一颗W25Q64的SPI Nor Flash用来存固件和参数。程序写完准备烧录结果Keil5直接弹了个让人头疼的报错No Flash Algorithm found for sector at address。我一看就明白了这颗外挂的SPI Flash根本不在Keil默认的算法列表里标准下载算法里只有芯片厂预置的几颗型号遇到这种非标Flash或者新板子上的外部存储就必须自己动手生成FLM文件。这篇文章先帮你把FLM文件的运行原理讲透然后带你在Keil5里从模板工程改出一份可用的自定义下载算法再结合我给W25Q64做SPI Nor Flash下载算法的实战过程把FlashDev.c参数配置、FlashOS.c底层驱动、分散加载文件和部署调试这些环节完整过一遍。适合那些已经在用Keil5做STM32或Cortex-M系列开发、对烧录流程有基本概念但还没接触过下载算法开发的工程师。看完之后你再遇到“没有匹配算法”“烧录失败”“Verify失败”这类问题就不会只会上网搜驱动包了自己动手就能解决。1. FLM文件到底是什么一个会被“塞进”RAM里运行的小程序很多做嵌入式开发的同学用了很久Keil但对烧录时发生的事情其实是一知半解的。我一开始也以为下载算法是Keil或者调试器内部的某个工具后来自己写了一套才彻底搞明白FLM文件本质上就是一个普通的ELF格式可执行程序里面装着一组函数和一张描述Flash特性的参数表。1.1 Keil烧录的完整过程先捋一下Keil烧录代码时的完整链路。当你在Keil里点下载按钮调试器J-Link、ST-Link、ULINK等会通过SWD或者JTAG接口连接目标芯片。在真正往Flash里写数据之前调试器要做两件事情第一把FLM文件里的下载算法代码加载到目标MCU的RAM中第二通过调试接口调用算法函数完成擦除、编程、校验等操作。整个过程可以类比成你手里有一堆货物固件程序但仓库Flash的钥匙只有仓库管理员下载算法有。管理员不在仓库里你没法直接开门放货所以你得先让管理员跑进仓库把算法代码加载到RAM然后让管理员帮你开门、搬货、整理货架。这里的关键点在于管理员的工作场所是仓库内部RAM而不是仓库外面调试器内部。为什么下载算法必须在RAM里运行而不是直接在Flash里运行原因很简单当你擦除或者编程Flash的时候CPU从Flash取指令会直接失败因为Flash正被擦写操作占用总线读到的全是无效数据。所以下载算法只能放在RAM里执行这就是为什么FLM工程的所有代码段都要定位到RAM地址空间而不是传统的0x08000000这类Flash地址。1.2 FLM内部到底装了什么东西说完运行机制再来拆开FLM文件看内部结构。FLM文件内部分为两部分一部分是描述Flash芯片特性的数据结构对应工程里的FlashDev.c另一部分是操作Flash的底层函数实现对应FlashOS.c。FlashDev.c里定义了一个名为FlashDevice的全局结构体里面包含了设备名称、Flash容量、起始地址、编程页大小、擦除扇区大小、编程和擦除的超时时间等参数。这些参数会被Keil的下载界面读取生成你在Options for Target - Utilities - Settings - Flash Download里看到的算法列表名称以及“Programming Algorithm”区域的地址范围。FlashOS.c里则实现了Init、UnInit、EraseChip、EraseSector、ProgramPage、Verify这几个函数。它们会被烧录器按照固定的时序调用初始化Flash控制器或外部Flash的通信接口、整片擦除或扇区擦除、按页写入数据、校验写入结果。整片擦除、整片编程、扇区擦除、扇区编程这些功能彼此独立下载器会根据用户在Keil烧录选项卡里的勾选项组合调用。2. 生成FLM前必须吃透的三样东西真正动手改FLM工程之前有三个东西必须搞清楚FlashDevice结构体每个字段的含义、FlashOS.c函数接口的调用约定、分散加载文件的RAM布局规则。这三个东西搞不明白哪怕你照着模板改出来一个FLM烧录的时候也会一头雾水出了问题根本无从排查。2.1 FlashDevice结构体给上位机看的“名片”FlashDevice结构体是整个FLM文件中最重要的数据结构相当于下载算法对外的一张名片。Keil的烧录组件会读取这个结构体把设备名称、地址范围、容量信息显示在Flash Download界面上。你在这个结构体里填什么Keil烧录时就会按什么参数来组织下载流程。struct FlashDevice const FlashDevice { FLASH_DRV_VERS, // 驱动版本号固定使用 FLASH_DRV_VERS W25Q64 SPI NOR, // 设备名称会显示在Keil算法列表里 EXTSPI, // 设备类型外部SPI Flash填写 EXTSPI 0x90000000, // Flash起始地址 0x00800000, // 设备容量单位字节W25Q64为8Mbit即1MB 4096, // 编程页大小 4, // 编程数据对齐 0xFF, // 擦除后的填充值 20, // 页编程超时时间ms 5000, // 扇区擦除超时时间ms 0, 0x000000, 0xFFFF, // 扇区划分 SECTOR_END };这里有个容易混淆的点设备类型字段EXTSPI对应的起始地址0x90000000并不是说这颗Flash真的被映射到了CPU的0x90000000地址空间。外部SPI Nor Flash是通过SPI接口访问的并不在MCU内部总线地址空间里。这个地址只是Keil烧录组件用来组织地址参数的一个逻辑地址。调试器在调用ProgramPage函数时会把目标地址作为参数传进去你在函数内部就要根据这个逻辑地址来决定写哪个SPI命令、操作哪个Flash内部地址。扇区划分表用0作为起始表示扇区尺寸为0x10000字节64KB扇区数量和总容量匹配最后以SECTOR_END结束。W25Q64每512KB一个扇区块但标准擦除单位是4KB扇区如果你把扇区大小定义成64KBKeil做扇区擦除时精度会很差。所以我一般会把扇区大小定义成4KB擦除效率更高也能避免误擦除相邻数据。2.2 FlashOS.c里六个关键函数FlashOS.c是整个下载算法的核心实现。模板工程自带了一套完整的函数骨架你只需要把针对特定Flash芯片的操作代码填入对应的函数体即可。六个函数分别是Init初始化接口和芯片。在每次下载流程开始前调用一次负责配置引脚、初始化SPI控制器、读取芯片ID、判断芯片是否存在。UnInit下载流程结束后调用负责释放引脚、停止SPI控制器、进入低功耗状态。不是必须的但建议实现。EraseChip整片擦除。将整个Flash芯片擦成0xFF状态通常操作时间较长需要等待芯片内部擦除完成。EraseSector扇区擦除。按FlashDevice结构体中定义的扇区大小擦除指定地址所在的扇区。ProgramPage页编程。按FlashDevice结构体中定义的编程页大小将数据写入Flash。这是被调用最多的函数性能优化主要看这里。Verify校验。写完数据后逐字节比对确认写入的数据和源数据完全一致。这些函数的声明位于模板工程的FlashOS.h头文件中都有固定的函数签名。比如Init函数uint32_t Init(uint32_t adr, uint32_t clk, uint32_t fnc)其中adr表示起始编程地址clk表示内核时钟频率fnc表示编程功能代码1代表擦除、2代表编程。三个参数在函数体内可能用不到但签名不能改烧录组件是靠固定的函数入口地址来调用这些函数的。2.3 分散加载文件决定下载算法跑在哪块RAMFLM工程的分散加载文件非常关键它决定下载算法代码会被加载到目标芯片的哪个RAM区域。我把模板中的分散加载文件内容稍微调整了一下变成下面这样LR_IROM1 0x20000000 0x00008000 { ER_IROM1 0x20000000 0x00008000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20008000 0x00008000 { .ANY (RW ZI) } }这里的0x20000000是Cortex-M系列芯片内部SRAM的起始地址0x00008000是分配的长度也就是32KB。第一段ER_IROM1放代码和只读数据第二段RW_IRAM1放可读写数据和零初始化数据。目标芯片不同RAM起始地址和容量也不同比如STM32F103系列SRAM是0x20000000但总容量只有20KB这时候你的算法分配区域就不能超过20KB否则下载时会直接放不下。为什么代码段要放在RAM而不是Flash前面已经说过因为擦写Flash期间CPU没法从Flash取指令所以整个下载算法必须在RAM中运行。这一点和普通应用程序的分散加载文件有本质区别很多新手第一次看FLM工程的Target选项卡时看到IROM1地址写的是0x20000000而不是0x08000000都会愣一下。3. 实战给W25Q64生成一个SPI Nor Flash下载算法理论说完直接上实战。我用的目标板是STM32F407ZG外挂W25Q64SPI接口用的是GPIO模拟四根线分别接在PB0CLK、PB1MOSI、PB2MISO、PB3CS。之所以不用硬件SPI控制器是因为在早期验证阶段GPIO模拟的方式更可控出现问题时逻辑分析仪一看波形就能定位不用怀疑寄存器配置。3.1 从模板工程开始改掉Device打开Keil安装目录下的ARM/Flash/_Template文件夹找一个和你目标芯片相近的模板工程。Keil自带的模板支持多种设备类型专注做Cortex-M系列的话直接找带有Cortex-M字样的模板即可比如Template_Flash_Cortex-M4。复制一份到自己的工作目录重命名为W25Q64_StdAlgo然后双击打开工程文件。打开工程后第一步在Options for Target - Device选项卡里选择你的目标芯片型号比如我这里选择的是STM32F407ZG。这样Keil就能根据芯片型号自动配置编译器选项和头文件路径确保你的FlashOS.c里能用到正确的寄存器定义。如果你用的是标准库或者HAL库模板工程里已经包含了必要的启动文件和系统初始化不用自己额外添加。需要特别注意的是模板工程默认的编译目标是一个空壳程序没有任何main函数也没有异常处理逻辑。启动文件会直接进入Reset_Handler并跳转到__main但__main最终会调用什么模板工程里贴心地放了一个空实现你在FlashOS.c里写的Init、ProgramPage这些函数并不是被__main调用的而是被调试器通过RAM中的函数地址直接调用的。所以你不要在文件里写main函数也不要加入任何可能阻塞代码执行的事件循环。3.2 编写FlashDev.c把W25Q64参数填进去把FlashDev.c中原有的示例芯片参数改成W25Q64的实际参数。注意看结构体定义里面的每一项都和Keil的烧录行为直接相关不能拍脑袋乱填。struct FlashDevice const FlashDevice { FLASH_DRV_VERS, // 驱动版本号 W25Q64 SPI NOR 0x90000000, // 算法名称建议带地址后缀方便区分 EXTSPI, // 设备类型外部SPI Flash 0x90000000, // 逻辑起始地址 0x00800000, // 容量8MBit 1MB 4096, // 编程页大小Keil按 4096 字节调用 ProgramPage 4, // 数据对齐 0xFF, // 擦除后的填充值 20, // 页编程超时20ms 1000, // 扇区擦除超时1000ms 0, 0x1000, 0xFFFF, // 扇区大小4KB共256个扇区 SECTOR_END };有两处要特别说明。第一处编程页大小我写的是4096但W25Q64硬件编程页只有256字节。这里的4096只是告诉Keil每次ProgramPage调用最多传入4096字节的数据真正的页边界拆分逻辑必须在ProgramPage函数内部自己处理。如果你直接按4096字节往里写一颗Flash的256字节页边界会被写穿数据直接错乱。第二处扇区大小写成0x10004KB这对应W25Q64的真实擦除粒度扇区擦除命令一次只能擦4KB你如果填64KBKeil的擦除操作打印会正常显示但Flash内部可能只擦了4KB导致校验失败。3.3 编写FlashOS.c用GPIO模拟SPI实现底层驱动接下来是重头戏FlashOS.c。我直接把自己的实现精简后贴出来去掉了一些调试用的延时和打印保留了最核心的逻辑。#include FlashOS.h #include stdint.h #define FLASH_CS_LOW() GPIOB-BSRR (uint32_t)GPIO_PIN_3 16 #define FLASH_CS_HIGH() GPIOB-BSRR (uint32_t)GPIO_PIN_3 #define FLASH_CLK_LOW() GPIOB-BSRR (uint32_t)GPIO_PIN_0 16 #define FLASH_CLK_HIGH() GPIOB-BSRR (uint32_t)GPIO_PIN_0 #define CMD_WRITE_ENABLE 0x06 #define CMD_READ_STATUS 0x05 #define CMD_PAGE_PROGRAM 0x02 #define CMD_SECTOR_ERASE 0x20 #define CMD_CHIP_ERASE 0xC7 #define CMD_READ_ID 0x9F static uint8_t spi_xfer(uint8_t byte) { uint8_t rx 0; for (int i 7; i 0; i--) { if (byte (1 i)) GPIOB-BSRR (uint32_t)GPIO_PIN_1; else GPIOB-BSRR (uint32_t)GPIO_PIN_1 16; FLASH_CLK_HIGH(); if (GPIOB-IDR GPIO_PIN_2) rx | (1 i); FLASH_CLK_LOW(); } return rx; } static void flash_wait_busy(void) { FLASH_CS_LOW(); spi_xfer(CMD_READ_STATUS); while (spi_xfer(0xFF) 0x01); FLASH_CS_HIGH(); } static void flash_write_enable(void) { FLASH_CS_LOW(); spi_xfer(CMD_WRITE_ENABLE); FLASH_CS_HIGH(); } uint32_t Init(uint32_t adr, uint32_t clk, uint32_t fnc) { // 配置 PB0/PB1/PB3 为推挽输出PB2 为浮空输入 RCC-AHB1ENR | RCC_AHB1ENR_GPIOBEN; GPIOB-MODER ~(GPIO_MODER_MODER0 | GPIO_MODER_MODER1 | GPIO_MODER_MODER2 | GPIO_MODER_MODER3); GPIOB-MODER | (GPIO_MODER_MODER0_0 | GPIO_MODER_MODER1_0 | GPIO_MODER_MODER2_0 | GPIO_MODER_MODER3_0); GPIOB-OSPEEDR | (GPIO_OSPEEDER_OSPEEDR0 | GPIO_OSPEEDER_OSPEEDR1 | GPIO_OSPEEDER_OSPEEDR2 | GPIO_OSPEEDER_OSPEEDR3); FLASH_CS_HIGH(); FLASH_CLK_LOW(); FLASH_CS_LOW(); spi_xfer(CMD_READ_ID); uint8_t mfr spi_xfer(0xFF); uint8_t type spi_xfer(0xFF); uint8_t cap spi_xfer(0xFF); FLASH_CS_HIGH(); if (mfr ! 0xEF || type ! 0x40 || cap ! 0x40) return 1; // ID 不匹配返回失败 return 0; } uint32_t UnInit(uint32_t fnc) { FLASH_CS_HIGH(); return 0; } uint32_t EraseChip(void) { flash_write_enable(); FLASH_CS_LOW(); spi_xfer(CMD_CHIP_ERASE); FLASH_CS_HIGH(); flash_wait_busy(); return 0; } uint32_t EraseSector(uint32_t adr) { uint32_t flash_addr adr 0x007FFFFF; // 去掉逻辑地址高位 flash_write_enable(); FLASH_CS_LOW(); spi_xfer(CMD_SECTOR_ERASE); spi_xfer((flash_addr 16) 0xFF); spi_xfer((flash_addr 8) 0xFF); spi_xfer(flash_addr 0xFF); FLASH_CS_HIGH(); flash_wait_busy(); return 0; } uint32_t ProgramPage(uint32_t adr, uint32_t sz, uint8_t *buf) { uint32_t flash_addr adr 0x007FFFFF; while (sz 0) { uint32_t page_remain 256 - (flash_addr % 256); uint32_t write_len sz page_remain ? sz : page_remain; flash_write_enable(); FLASH_CS_LOW(); spi_xfer(CMD_PAGE_PROGRAM); spi_xfer((flash_addr 16) 0xFF); spi_xfer((flash_addr 8) 0xFF); spi_xfer(flash_addr 0xFF); for (uint32_t i 0; i write_len; i) spi_xfer(buf[i]); FLASH_CS_HIGH(); flash_wait_busy(); sz - write_len; buf write_len; flash_addr write_len; } return 0; } uint32_t Verify(uint32_t adr, uint32_t sz, uint8_t *buf) { return 0; // 交给 Keil 读取回读校验 }这段代码有几个地方值得展开讲。spi_xfer函数是最基础的位操作SPI收发时序。在CLK上升沿发送一位数据、下降沿读取一位数据从最高位开始逐位收发。GPIO模拟的好处是不依赖任何外设时钟配置只要GPIO初始化正确就能工作坏处是速度受限。我这个板子跑168MHz主频循环里加一点临界条件实测一个字节大约需要1.5us左右写1MB数据大概要几秒钟比硬件SPI慢不少。如果你用硬件SPI1Init里只需要配置SPI1的时钟、模式、极性和相位速度能拉高一个数量级。ProgramPage函数里的页边界拆分是这段代码最核心的逻辑。W25Q64的硬件页编程命令最多只能一次写256字节且不能跨256字节边界。Keil调用ProgramPage时传入的sz是FlashDevice里定义的编程页大小4096字节如果函数内部不处理边界直接把4096字节连续发过去芯片会拒绝写入或者数据错乱。所以我在while循环里先计算当前地址到页边界的剩余长度取较小值作为本次写入长度分多次调用页编程命令每次写完等待内部忙状态结束再写下一次。这个模式是所有Nor Flash扇区编程的标准写法建议直接背下来。Verify函数我这里直接返回0让Keil使用默认的回读校验机制。Keil在编程完成后会从目标地址重新读回数据和源数据逐字节比较。这种方式最简单可靠也不需要额外实现读取Flash数据的命令。如果你想自己实现校验读取函数内部用一个读数组、逐字节比较、返回值0表示通过1表示失败也能用。3.4 编译、转换、部署三步走写完代码后在Keil里按F7编译正常情况下会生成一个.axf文件。但此时这个.axf还不能直接用需要转换为.flm格式。转换方式有两种。第一种是在工程选项里自动转换。打开Options for Target - Output选项卡勾选Create Flash Loader Output重新编译一次Keil会自动调用fromelf工具把.axf转换成.flm文件生成位置和.axf在同一个输出目录。第二种是手动转换。打开命令行进入Keil安装目录的ARM/ARMCC/bin目录MDK5.37以前或者ARM/ARMCLANG/bin目录MDK5.37及以后执行fromelf.exe --output W25Q64.flm --bin ./W25Q64.axf操作完成后把生成的W25Q64.flm复制到Keil安装目录的ARM/Flash目录下。然后回到你的应用程序工程打开Options for Target - Utilities - Settings - Flash Download点击Add按钮在列表里往下翻找到W25Q64 SPI NOR 0x90000000选中并添加。添加后要把Programming Algorithm区域的Start和Size设为0x90000000和0x00800000和FlashDevice结构体里的参数保持一致。这里有一个特别容易出问题的误区有些同学复制FLM文件时只复制了.flm本体没注意Keil还需要读写权限和芯片型号匹配。如果你添加算法后Keil提示Could not load file先检查是不是flash目录权限不足或者文件里有一段无效数据导致加载失败。实在不行把文件放到工程目录下手动在Algorithm列表里Browse选择这个文件也能生效。4. 调试下载算法的三个坑位我在调试这个SPI Nor Flash下载算法时踩了三个坑每一个都让我卡了不少时间写出来给你排雷。4.1 算法代码不能和用户代码共用同一块RAM我第一次调试的时候直接把模板工程的下载算法区域设置成0x20000000长度32KB没有仔细核对目标芯片的总RAM容量和用户程序的RAM使用情况。结果就是烧录时算法代码加载到RAM后和用户程序的变量区重叠导致程序在下载完成后第一次运行就HardFault。后来我在分发加载文件里把算法区域改成0x20000000开始、长度设为16KB把数据区域也单独划出去确保下载算法占用的RAM区域和用户程序实际使用区域完全隔离。这里要特别提醒如果你用的是带有DMA功能的外设RAM的分区也要考虑DMA缓冲区的对齐要求。最好的办法是在烧录完成后在调试状态下查看RAM窗口确认算法区域的末尾地址和用户程序的起始地址之间留了足够的安全间隙。4.2 擦除超时与状态轮询W25Q64的扇区擦除时间典型值是45ms但最坏情况可能到400ms。我在FlashDevice里写的扇区擦除超时是1000ms覆盖了极端情况。但如果你把超时时间改得太小烧录时就会出现“Timeout waiting for flash operation to complete”的报错。系统编程中最稳妥的做法是在EraseSector和ProgramPage函数内部实现完整的忙状态轮询也就是前面代码里的flash_wait_busy函数。每次发完命令后不断读状态寄存器的BIT0为1表示芯片还在忙为0表示操作完成。这样把超时控制在函数内部不用过分依赖Keil设置的超时参数。4.3 Verify校验不能省有段时间我想省时间在Flash Download配置里把Verify选项去掉了结果烧完程序板子跑起来偶尔会出现数据异常。后来查了半天才发现罪魁祸首是SPI时序问题GPIO模拟SPI的时钟极性设置不对导致写入的数据在某些位序上和源数据不一致但因为没开 VerifyKeil根本不知道数据写错了。之后我每次都留着VerifyKeil会在编程完成后回读Flash数据逐一比较任何写入错误都会直接报“Verify failed at address 0x......”。还有一个可以提的更隐蔽的点如果ProgramPage函数内部强制把数据拆分成256字节页写入但某一个分配段的地址偏移计算错误Verify也会报“Mismatch at address”。这时候不要急着改SPI时序先把地址计算逻辑和页边界拆分逻辑跑一遍单步调试往往问题出在地址对齐上。5. FLM常见报错排查实录做FLM开发过程中报错信息五花八门但归纳起来主要就下面这几种。我做了一个速查表你遇到问题可以先对照一下。报错现象可能原因排查思路No Flash Algorithm found for sector at address算法列表里没有匹配当前地址范围的FLM检查FlashDevice结构体的起始地址和大小是否覆盖目标地址Could not load file xxx.flmFLM文件损坏、路径不对或权限不足确认.flm文件在ARM/Flash目录下尝试重新编译生成Erase Failed at address扇区擦除超时或芯片忙状态轮询逻辑有误在EraseSector里加延时确认状态寄存器读取正确Program Failed at address页编程跨页、SPI时序或写使能命令遗漏检查ProgramPage的页边界拆分确认每次写前都发送写使能Verify failed at address写入数据和源数据不一致用逻辑分析仪抓SPI波形对比每个字节的时序Cannot access memoryRAM区域地址越界或调试器连接不稳定检查分散加载文件RAM地址范围确认目标芯片RAM容量每个现象背后还有更细分的场景。比如我有一次遇到了“Erase Failed at address 0x90000000”但仔细看错误提示会发现后面还带了一串内存地址。当时我判断是芯片扇区表定义错误但实际排查下来发现是FlashDevice结构体里的扇区划分表最后一行没有正确写SECTOR_END。Keil在解析扇区表时会遍历到SECTOR_END才停止如果你少写了SECTOR_END它可能把后续内存数据当作扇区表继续解析轻则算法列表里显示异常重则直接导致擦除流程错乱。所以写完结构体后对照模板多检查三遍扇区表结尾。还有一个经常被忽略的问题FLM文件里的代码不能在下载时使用操作系统功能。很多裸机工程依赖RTOS或者复杂的中断管理这些代码在下载算法阶段统统不能出现。你自己写FlashOS.c时只用最基础的寄存器操作就够了不要调用任何库函数。我在调试时曾经在Init里调用了一个HAL库函数结果下载器加载算法后直接死机换成寄存器操作后立刻正常。这就是下载算法和普通固件最大的差别运行环境极其受限没有内核、没有外设驱动库甚至没有完整的中断向量表只给你一组裸函数。6. 最后再分享几条工程经验这个FLM下载算法项目做完之后再回看整个调试过程有几个实践体会想留给你。第一FLM文件里的代码越精简越好。Keil在烧录前要把FLM中的代码和数据加载到RAM里代码越多、加载时间越长同时RAM占用也越大。我见过有人直接在FlashOS.c里引入了整个HAL库编译出来的FLM文件上百KB下载一次要等半天。合理的方式是只写必要的GPIO/SPI操作其他统统不引入。第二算法名称尽量加上芯片型号和地址后缀。比如我写的“W25Q64 SPI NOR 0x90000000”这样在Flash Download界面里一眼就能分辨哪些算法对应哪颗芯片。项目多了之后你会发现这是一条能救命的命名习惯。第三如果烧录总是失败先别急着怀疑代码逻辑。先用逻辑分析仪或者示波器抓一下SPI的波形确认CS、CLK、MOSI、MISO四条线的电平时序是否符合预期。我之前排查了一个下午的ProgramPage问题最后发现是GPIO的复用配置没改对MISO引脚一直被配置成了输出模式读回来全是0。这种问题看波形一眼就能定位闷头读代码反而找不到。最后再补一个实用小技巧如果你要调试的Flash不是SPI接口而是挂在外部总线上的Nor Flash或NAND Flash比如STM32的FMC接口外挂NOR方法完全一样。只需要把FlashOS.c里的底层接口从SPI操作换成FMC总线的读写操作其他逻辑、结构体、分散加载文件的配置方式全部不变。掌握了一套FLM生成方法等于掌握了给所有非标准Flash设备做下载算法的能力以后遇到再冷门的存储芯片你都能自己搞定。
返回列表