ARTICLE DETAIL

资讯详情

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

告别串口升级:用MDK下载算法一键烧写STM32H750外部QSPI Flash

告别串口升级:用MDK下载算法一键烧写STM32H750外部QSPI Flash 搞嵌入式这些年我一直觉得给MCU烧程序就该是“打开MDK点一下Download完事”。直到遇见STM32H750VBT6这颗芯片它内置Flash只有128KB跑复杂点的应用分分钟爆满。为了把程序和资源塞到外部W25Q324MB QSPI Flash里我以前只能先烧一个Bootloader再用串口工具一帧一帧发固件。那段时间每发一次固件都要经历一次“串口掉线重连、校验失败重发、版本号记错”的折磨。后来我花了两三天时间把STM32H750的MDK下载算法FLM彻底搞明白了终于能在MDK里像烧内置Flash一样直接Download外部QSPI Flash还能用调试器在外部Flash上打断点。这篇就把完整过程和核心代码分享出来给同样被串口升级折磨的人指条路。1. 先搞清楚这四件事Flash困境、芯片选型、下载算法和硬件连接1.1 STM32H750VBT6的128KB困局STM32H750VBT6这颗芯片主频可以跑到480MHz内置DSP和双精度FPU还带TFT-LCD控制器、LTDC、DCMI等丰富外设性能在M7里算是很能打的。但唯独内置Flash只有128KB这对于稍微上点规模的应用来说完全不够用——你随便放个LVGL界面、放点字库图片、加个轻量级文件系统128KB就没了。所以实际产品里H750几乎都是外挂一片SPI/QSPI Flash来扩展存储。而扩展Flash最常见有两种玩法一种是把资源数据字库、图片、音频放在外部Flash程序本身仍从内置Flash运行另一种是干脆把代码也放到外部Flash里运行XIP这样内置Flash就只放一个极小的Bootloader。第二种玩法对烧写工具的要求更高因为你没法再用传统方式“把程序下载到内置Flash”了。1.2 W25Q32是一块什么样的FlashW25Q32是华邦Winbond出品的一颗32Mbit4MBSPI NOR Flash工作电压2.7V-3.6V支持标准SPI、双线SPI、四线SPIQSPI模式。4MB容量对于中小规模的MCU应用刚好合适而且价格便宜、供货稳定、资料丰富是我个人比较常用的扩展Flash型号。这颗Flash的几个关键参数页大小256字节扇区大小4KB块大小32KB/64KB可配擦除后数据0xFF最大时钟频率133MHz四线快速读JEDEC ID0xEF4015W25Q32的擦写寿命典型值是10万次数据保持能力在常温下可以到20年对于绝大多数嵌入式产品场景完全够用。1.3 MDK下载算法是什么为什么能解决串口升级的痛点MDK下载算法在Keil里叫“Flash Programming Algorithm”编译产物是.FLM文件。它的本质是一个极小的独立程序会被MDK通过调试器ST-Link/J-Link等临时加载到目标芯片的RAM里运行由这个RAM里的程序去操控外部Flash的擦除和写入。为什么有了它就能告别串口因为串口升级的链路是“APP/Bootloader用串口协议接收数据 → 写入Flash”你需要先在芯片里驻留一个Bootloader还要配合PC端的串口工具。而MDK下载算法走的是SWD/JTAG调试接口经过调试器直接控制芯片RAM里的算法程序来烧写Flash不需要Bootloader不需要串口工具也不占用任何通信外设。更关键的是下载算法的运行由MDK统一管理你可以在MDK里直接对0x90000000地址空间QSPI映射区执行擦除、下载、校验甚至调试。整个体验和烧内置Flash完全一致。1.4 本文实验环境的硬件连接方案在做下载算法之前先得确定QSPI Flash接在STM32H750的哪些引脚上。我使用STM32H750的QSPI1外设通过四线QSPI方式连接W25Q32具体引脚分配如下功能QSPI1信号引脚复用功能AF时钟QSPI1_CLKPB2AF9片选QSPI1_BK1_CSPG6AF10数据0QSPI1_BK1_IO0PC1AF9数据1QSPI1_BK1_IO1PC2AF9数据2QSPI1_BK1_IO2PE2AF9数据3QSPI1_BK1_IO3PD13AF9这套引脚也是很多H750开发板的标准接法如果你手里的板子是其他引脚方案只需调整本文代码里的GPIO配置即可不影响整体逻辑。2. FLM算法在MDK中的角色设备描述、函数接口和执行流程2.1 FlashDevice结构体算法的“身份证”MDK加载FLM文件后第一件事是读取里面FlashDevice结构体获取这颗Flash的“身份信息”。这个结构体定义了算法支持的Flash芯片名称、基地址、容量、页大小、超时时间等关键参数。#include FlashOS.h struct FlashDevice const FlashDevice { FLASH_DRV_VERS, // 驱动版本固定用这个宏 W25Q32_QSPI_4MB, // 设备名称会显示在MDK Flash Download列表中 EXTSPI, // 设备类型外部SPI Flash用EXTSPI 0x90000000, // Flash起始地址QSPI1映射地址 0x00400000, // Flash大小4MB 4096, // 页大小编程时一次最多多少字节 0, // 预留字段 0xFF, // 擦除后填充值 100, // 编程超时100ms 3000, // 擦除超时3000ms { {0x1000, 0x000000}, // 4KB扇区擦除 {0x8000, 0x000000}, // 32KB块擦除可选 {0x10000, 0x000000}, // 64KB块擦除可选 {0x00400000, 0x000000}, // 整片擦除 {0x000000, 0x000000} // 结束标志 } };这里有几个字段值得解释一下设备地址0x90000000这是STM32H750 QSPI1的存储映射区域起始地址。H750的QSPI1映射地址是0x90000000-0x9FFFFFFFQSPI2映射到0x70000000-0x7FFFFFFF。页大小设为4096W25Q32实际页编程单位是256字节但这里设成4096可以减小MDK调用ProgramPage函数的次数从而提高编程速度。我们的ProgramPage函数内部会按256字节自动分块编程。设备类型EXTSPI在MDK的FlashOS.h头文件中EXTSPI的值为5。如果这里写错MDK会按内置Flash的算法逻辑来处理外部SPI Flash导致报错。最后那个扇区擦除表就是给MDK选择擦除策略用的。MDK在执行“擦除扇区”操作时会根据下载地址范围和这个表来决定调用哪个擦除函数。比如下载地址落在4KB边界就调用4KB扇区擦除如果落在64KB边界就调用64KB块擦除。这里我保留了32KB和64KB块擦除项实际调试时更灵活。2.2 六大核心回调函数MDK下载算法需要实现以下六个函数这些函数的原型定义在FlashOS.h中函数名功能说明Init初始化Flash控制器和GPIO返回0表示成功UnInit下载结束后做清理工作比如关闭外设或保持状态EraseChip整片擦除FlashEraseSector擦除指定地址所在的扇区ProgramPage往指定地址写入一页数据大小由FlashDevice中的页大小决定Verify校验写入的数据返回0表示正确非0表示第一个错误地址这六个函数就是MDK与下载算法之间的“协议”。MDK在需要擦除时调用EraseSector/EraseChip在需要写数据时调用ProgramPage在写完后调用Verify校验。Init和UnInit在每次下载操作开始/结束时被调用。2.3 MDK调用FLM的完整流程理解调用链对排查问题很有帮助。当你点击MDK的Download按钮后会发生以下事情调试器ST-Link/J-Link通过SWD接口连接目标芯片配置芯片的调试端口。MDK从FLM文件中解析出代码段和数据段把整个算法镜像加载到RAM中RAM地址由Flash Download配置页面里的“RAM for Algorithm”决定。MDK设置程序计数器PC跳转到算法入口调用Init函数传入目标地址、时钟频率和功能码。Init函数完成QSPI外设和GPIO的初始化返回0告诉MDK“一切OK”。MDK根据Flash Download配置的编程选项比如“擦除扇区”或“整片擦除”调用对应的擦除函数。MDK把要烧写的数据通过调试接口传输到RAM中的临时缓冲区然后调用ProgramPage函数将缓冲区里的数据编程到Flash的指定地址。编程完成后MDK调用Verify函数把Flash里的数据和原数据做对比。最后调用UnInit函数如果配置了“Reset and Run”还会复位并运行目标程序。流程看着不复杂但每一步都可能埋着坑。比如第2步中RAM for Algorithm设置不当算法镜像就加载失败第5步擦除函数实现不对编程时就会报“verify failed at address xxx”。3. 搭建FLM算法工程目标配置、源文件与编译环境3.1 新建工程和源文件制作FLM算法不需要从零搭建HAL库环境我们只需要一个精简工程包含目标芯片启动文件、FlashDev.c和FlashPrg.c三个核心文件。具体步骤打开Keil MDKProject - New µVision Project给工程起名比如W25Q32_QSPI_FLM。选择设备搜索并选中STM32H750VBTx。在Manage Run-Time Environment弹窗里不用勾选任何组件直接OK。在创建好的工程里手动添加三个文件startup_stm32h750xx.s这个启动文件可以从MDK的Pack安装目录下找到路径一般是C:\Keil_v5\ARM\PACK\Keil\STM32H7xx_DFP\版本号\Device\Source\ARM\startup_stm32h750xx.sFlashDev.c放置FlashDevice结构体FlashPrg.c放置算法实现函数把工程默认生成的main.c删除或者不添加FLM算法工程不需要main函数。3.2 Target页面和分散加载文件的配置FLM算法本身要在RAM里运行所以Target页面的内存配置很关键。我使用的是AXI SRAM区域0x24000000起始原因后面会提到。在Options for Target - Target页面中勾选“Use Memory Layout from Target Dialog”IROM1Start0x24000000Size0x400016KB代码空间IRAM1Start0x24004000Size0x400016KB数据空间Xtal保持默认即可FLM算法不依赖这个值这里的IROM1虽然名字叫ROM但因为我们配置的是RAM起始地址所以Keil编译器会把代码生成到RAM地址上。链接器会根据Target对话框自动生成分散加载文件把只读代码段放进0x24000000起始的RAM空间。为什么不把算法放在DTCM0x20000000H750有128KB DTCM速度最快但也最容易和应用程序的RAM配置冲突。AXI SRAM有512KB空间充足而且即使App也用了AXI SRAM只要RAM for Algorithm设置合理在下载流程结束后App复位启动时会重新初始化这块区域问题不大。在Options for Target - C/C页面中注意以下几点Define里填STM32H750xx确保CMSIS头文件包含正确的外设定义。ARM Compiler建议选V6或者V5.06两个版本我都试过能编译通过。Optimization选择-O2或-O3别选-Os避免链接时因为函数合并导致问题。不勾选“One ELF Section per Function”保持默认即可。Output页面里Name of Executable改成W25Q32_QSPI这个名称就是最终生成的FLM文件名。勾选“Create HEX File”可以顺便生成HEX文件方便调试。3.3 编译生成FLM文件直接按F7编译如果没有报错工程目录的.\Flash文件夹下会生成一个W25Q32_QSPI.FLM文件。MDK编译FLM工程时会自动输出FLM格式这是MDK的特殊能力普通工程输出的是AXF文件。如果编译报找不到FlashOS.h确认一下MDK安装路径下的ARM\Flash\_Template目录是否在头文件包含路径里。一般安装MDK后C:\Keil_v5\ARM\INC\FlashOS.h这个路径会自动加入如果没有就手动加一下。4. 核心函数实现QSPI初始化、擦除、编程与校验4.1 QSPI引脚与时钟初始化在Init函数被MDK调用时第一件事就是初始化QSPI外设和GPIO。这里我采用寄存器直接操作的方式不依赖HAL库好处是ELF文件体积小、加载快、和MDK的FLM加载器配合更稳定。#include FlashOS.h #include stm32h7xx.h #define QSPI_FLASH_BASE 0x90000000UL #define W25Q32_JEDEC_ID 0xEF4015UL static void QSPI_InitHardware(void) { // 使能QSPI外设时钟 RCC-AHB1ENR | RCC_AHB1ENR_QSPIEN; // 使能相关GPIO端口时钟 RCC-AHB4ENR | RCC_AHB4ENR_GPIOBEN | RCC_AHB4ENR_GPIOCEN | RCC_AHB4ENR_GPIODEN | RCC_AHB4ENR_GPIOEEN | RCC_AHB4ENR_GPIOGEN; // PB2 - QSPI_CLK (AF9) GPIOB-MODER ~GPIO_MODER_MODE2; GPIOB-MODER | GPIO_MODER_MODE2_1; // Alternate Function GPIOB-OSPEEDR | GPIO_OSPEEDR_OSPEED2; // High speed GPIOB-AFR[0] ~GPIO_AFRL_AFSEL2; GPIOB-AFR[0] | (9U GPIO_AFRL_AFSEL2_Pos); // PC1 - QSPI_IO0 (AF9), PC2 - QSPI_IO1 (AF9) GPIOC-MODER ~(GPIO_MODER_MODE1 | GPIO_MODER_MODE2); GPIOC-MODER | (GPIO_MODER_MODE1_1 | GPIO_MODER_MODE2_1); GPIOC-OSPEEDR | (GPIO_OSPEEDR_OSPEED1 | GPIO_OSPEEDR_OSPEED2); GPIOC-AFR[0] ~(GPIO_AFRL_AFSEL1 | GPIO_AFRL_AFSEL2); GPIOC-AFR[0] | (9U GPIO_AFRL_AFSEL1_Pos) | (9U GPIO_AFRL_AFSEL2_Pos); // PD13 - QSPI_IO3 (AF9) GPIOD-MODER ~GPIO_MODER_MODE13; GPIOD-MODER | GPIO_MODER_MODE13_1; GPIOD-OSPEEDR | GPIO_OSPEEDR_OSPEED13; GPIOD-AFR[1] ~GPIO_AFRH_AFSEL13; GPIOD-AFR[1] | (9U GPIO_AFRH_AFSEL13_Pos); // PE2 - QSPI_IO2 (AF9) GPIOE-MODER ~GPIO_MODER_MODE2; GPIOE-MODER | GPIO_MODER_MODE2_1; GPIOE-OSPEEDR | GPIO_OSPEEDR_OSPEED2; GPIOE-AFR[0] ~GPIO_AFRL_AFSEL2; GPIOE-AFR[0] | (9U GPIO_AFRL_AFSEL2_Pos); // PG6 - QSPI_CS (AF10) GPIOG-MODER ~GPIO_MODER_MODE6; GPIOG-MODER | GPIO_MODER_MODE6_1; GPIOG-OSPEEDR | GPIO_OSPEEDR_OSPEED6; GPIOG-AFR[0] ~GPIO_AFRL_AFSEL6; GPIOG-AFR[0] | (10U GPIO_AFRL_AFSEL6_Pos); // 配置QSPI外设 QUADSPI-CR 0; // 先关闭QSPI // FSIZE21: 表示Flash容量为2^(211)4MBCSHT4个时钟周期 QUADSPI-DCR (21U 16) | (4U 0); // 设置时钟分频PRESCALER2QCLK AHB时钟 / (2*2) QUADSPI-CR | (2U 24); }这里最关键的是DCR的FSIZE字段。FSIZE21对应Flash容量为4MB这是根据容量字节数 2^(FSIZE1)反推出来的。如果这个值配错QSPI的地址管理会出问题轻则Memory Map区域无法正确映射重则Flash操作直接卡死。还有一个细节是片选高电平时间CSHT。W25Q32在每次命令结束到下一次命令开始之间需要一定的CS高电平时间tSH如果CSHT配置太小Flash可能来不及“换状态”导致下一个命令被忽略。配置成4个QSPI时钟周期在100MHz以下的QSPI时钟频率下都是安全的。4.2 Init函数初始化并验证Flash IDInit函数除了调用QSPI_InitHardware完成引脚和外设初始化我习惯在这里顺手读一下W25Q32的JEDEC ID确认芯片物理连接正确。这样MDK在下载前就能发现Flash接错或虚焊的问题而不是等到擦除/编程时才报一堆莫名其妙错误。int Init(unsigned long adr, unsigned long clk, unsigned long fnc) { uint32_t jedec_id 0; QSPI_InitHardware(); // 间接读模式读取JEDEC ID QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-DLR 2; // 需要读3字节 QUADSPI-CCR (0x9FU 24) | (0x01U 22) | // INSTRUCTION0x9F, IMODE单线 (0x01U 8) | (0x01U 0); // DMODE单线, FMODE间接读 QUADSPI-CR | QUADSPI_CR_EN; // 读取3个字节的ID while((QUADSPI-SR QUADSPI_SR_FTF) 0); jedec_id (QUADSPI-DR 0xFFU); while((QUADSPI-SR QUADSPI_SR_FTF) 0); jedec_id | ((QUADSPI-DR 0xFFU) 8); while((QUADSPI-SR QUADSPI_SR_FTF) 0); jedec_id | ((QUADSPI-DR 0xFFU) 16); if(jedec_id ! W25Q32_JEDEC_ID) { return 1; // Flash ID不匹配返回错误MDK会提示 } QUADSPI-CR ~QUADSPI_CR_EN; return 0; }这里的CCR配置逻辑要说明一下。0x9F是读JEDEC ID的标准命令不需要地址所以ADMODE为0数据从QSPI总线上以单线方式返回所以DMODE为1FMODE为01表示间接读模式数据会进入FIFO通过读DR寄存器取出。如果返回非0MDK会在下载日志里显示“Error: Flash Download failed - Target DLL has been cancelled”同时给出一个错误代码。这点在调试硬件连接时非常有用——如果Flash芯片没焊好MDK会立刻报ID错误而不是等到下载中段才失败。4.3 写使能与忙等待的基础操作在擦除和编程操作之前QSPI Flash都需要先发写使能命令0x06让Flash的WEL位变为1。否则后续的擦除/编程命令会被Flash直接忽略。static void QSPI_WriteEnable(void) { QUADSPI-CR ~QUADSPI_CR_EN; // 0x06写使能仅指令无地址无数据间接写模式 QUADSPI-CCR (0x06U 24) | (0x01U 22) | (0x00U 0); QUADSPI-CR | QUADSPI_CR_EN; while(QUADSPI-SR QUADSPI_SR_BUSY); }等待Flash内部操作完成也很关键。W25Q32执行扇区擦除约几十毫秒、页编程约0.7毫秒时状态寄存器的WIP位会保持为1。我们需要不断读取状态寄存器0x05直到WIP位清零才能进行下一步操作。static void QSPI_WaitBusy(void) { uint8_t sr; do { QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-DLR 0; // 读1字节 // 0x05读状态寄存器数据单线间接读模式 QUADSPI-CCR (0x05U 24) | (0x01U 22) | (0x01U 8) | (0x01U 0); QUADSPI-CR | QUADSPI_CR_EN; while(QUADSPI-SR QUADSPI_SR_BUSY); while((QUADSPI-SR QUADSPI_SR_FTF) 0); sr (uint8_t)(QUADSPI-DR 0xFFU); } while(sr 0x01U); // WIP位为1就继续等 }这里有个容易踩的坑在调用QSPI_WaitBusy之前一定要先等上一个命令的BUSY标志清零再来配置新命令。否则上一次的传输还没结束你改了CCR/AR等寄存器会导致当前命令状态错乱Flash可能进入死锁状态。这也是我在每个辅助函数里都在操作寄存器前先执行QUADSPI-CR ~QUADSPI_CR_EN的原因。4.4 擦除函数的实现W25Q32支持整片擦除0xC7、64KB块擦除0xD8、32KB块擦除0x52和4KB扇区擦除0x20。在FLM算法里我主要实现扇区擦除和整片擦除两种。int EraseSector(unsigned long adr) { unsigned long offset adr - QSPI_FLASH_BASE; QSPI_WriteEnable(); // 4KB扇区擦除命令 0x20 QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-CCR (0x20U 24) | (0x01U 22) | // INSTRUCTION0x20, IMODE单线 (0x01U 20) | (0x03U 18) | // ADMODE单线, ADSIZE3字节 (0x00U 8) | (0x00U 0); // 无数据, 间接写模式 QUADSPI-AR offset; QUADSPI-CR | QUADSPI_CR_EN; while(QUADSPI-SR QUADSPI_SR_BUSY); QSPI_WaitBusy(); // 等待擦除完成 return 0; } int EraseChip(void) { QSPI_WriteEnable(); // 整片擦除命令 0xC7 QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-CCR (0xC7U 24) | (0x01U 22) | // INSTRUCTION0xC7, IMODE单线 (0x00U 8) | (0x00U 0); // 无地址无数据, 间接写模式 QUADSPI-CR | QUADSPI_CR_EN; while(QUADSPI-SR QUADSPI_SR_BUSY); QSPI_WaitBusy(); return 0; }注意EraseSector传入的adr参数是绝对地址比如0x90001000我们传给AR寄存器时需要减去0x90000000得到Flash内的相对偏移。W25Q32的4KB扇区擦除命令是一个地址周期的命令AR寄存器里放着要擦除的扇区起始地址。4.5 ProgramPage分页编程的实现W25Q32的页编程支持一次最多写256字节且不能跨256字节边界。因此ProgramPage函数里必须处理地址对齐问题——哪怕MDK传过来4096字节的数据我也要按“当前地址在页内的偏移”来分块编程。int ProgramPage(unsigned long adr, unsigned long sz, unsigned char *buf) { unsigned long offset adr - QSPI_FLASH_BASE; unsigned long page_off, chunk, i; while(sz 0) { page_off offset 0xFFUL; // 当前地址在页内的偏移 chunk 256UL - page_off; // 当前页剩余空间 if(chunk sz) chunk sz; QSPI_WriteEnable(); // 页编程命令 0x02 QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-CCR (0x02U 24) | (0x01U 22) | (0x01U 20) | (0x03U 18) | (0x01U 8) | (0x00U 0); QUADSPI-AR offset; QUADSPI-DLR chunk - 1; QUADSPI-CR | QUADSPI_CR_EN; // 数据写入FIFO硬件会自动通过IO发送给Flash for(i 0; i chunk; i) { while((QUADSPI-SR QUADSPI_SR_FTF) 0); QUADSPI-DR buf[i]; } // 等待数据发送完成 while(QUADSPI-SR QUADSPI_SR_BUSY); // 等待Flash内部编程完成 QSPI_WaitBusy(); offset chunk; buf chunk; sz - chunk; } return 0; }关键点是DLR寄存器的值为“要发送的字节数减1”AR寄存器存的是当前页的起始地址。数据写入DR寄存器后QSPI外设会按照CCR配置的数据模式把这些字节发给Flash。为什么每次写DR前要等待FTF标志因为H7的QSPI FIFO深度只有16字节连续写入超过16字节的数据会导致FIFO溢出。FTF标志在FIFO还有空间时会置1在写方向所以用它来限流。4.6 Verify校验函数的实现MDK在编程完成后默认会调用Verify函数把Flash里的数据和原始数据比对。这里我使用0x03单线读命令。unsigned long Verify(unsigned long adr, unsigned long sz, unsigned char *buf) { unsigned long offset adr - QSPI_FLASH_BASE; uint8_t rd; QUADSPI-CR ~QUADSPI_CR_EN; QUADSPI-DLR sz - 1; // 读取sz字节 QUADSPI-CCR (0x03U 24) | (0x01U 22) | (0x01U 20) | (0x03U 18) | (0x01U 8) | (0x01U 0); QUADSPI-AR offset; QUADSPI-CR | QUADSPI_CR_EN; for(unsigned long i 0; i sz; i) { while((QUADSPI-SR QUADSPI_SR_FTF) 0); rd (uint8_t)(QUADSPI-DR 0xFFU); if(rd ! buf[i]) { return adr i; // 返回第一个失败地址 } } return 0; }Verify返回0表示全部一致非0则返回第一个不一致的地址。MDK会根据返回值在日志里醒目标红“Verify Failed at xxxxxxxx”。5. 接入MDK下载流程并实测烧写5.1 Flash Download页面配置编译生成FLM文件后接下来把它注册到MDK的下载配置里。以ST-Link为例打开App工程不是FLM工程进入Options for Target - Debug选择ST-Link点击旁边的Settings。切换到Flash Download选项卡点击Add按钮。在弹出的文件选择框中找到我们编译生成的W25Q32_QSPI.FLM选中并确认。在Programming Algorithm列表中会出现“W25Q32_QSPI_4MB”然后设置Start: 0x90000000Size: 0x400000RAM for Algorithm Start: 0x24000000RAM for Algorithm Size: 0x8000编程选项建议勾选Erase Sectors这样MDK只会擦除目标数据所在的扇区比整片擦除更快也更安全。这里的RAM for Algorithm就是MDK用来加载FLM算法镜像的RAM区域必须和FLM工程编译时使用的IROM1起始地址一致。我之前在FLM工程里把代码放在0x24000000所以这里填0x24000000Size给0x800032KB对于这个算法完全够用。5.2 一个验证用的QSPI App工程为了验证下载算法是否正常工作我建了一个极简的App工程新建工程选择STM32H750VBTx。在Options for Target - Target页面把IROM1改成Start: 0x90000000Size: 0x400000IRAM1保持默认Start0x20000000Size0x20000使用DTCM。编写一个点亮板载LED的代码#include stm32h7xx.h void SystemInit(void) { // 实际工程中会调用SystemInit这里简单置空 } int main(void) { RCC-AHB4ENR | RCC_AHB4ENR_GPIOBEN; GPIOB-MODER ~GPIO_MODER_MODE0; GPIOB-MODER | GPIO_MODER_MODE0_0; // PB0输出 while(1) { GPIOB-BSRR GPIO_BSRR_BS0; for(volatile int i 0; i 1000000; i); GPIOB-BSRR GPIO_BSRR_BR0; for(volatile int i 0; i 1000000; i); } }这个工程代码段很小下载到0x90000000后会在QSPI Flash里占一个很小的区域。但要注意这段代码是不能直接从复位后开始运行的因为H750上电时不会自动初始化QSPI外设所以后面我们必须搭配Bootloader来验证XIP执行。5.3 下载实测MDK里的烧写日志在App工程里点击Download后MDK的Build Output窗口会输出类似下面的日志Load C:\\project\\app\\app.axf Erase Done. Programming Done. Verify OK. Application running ...看到“Erase Done”和“Programming Done”只用了不到1秒你就知道这算法成了。我实测烧写一个32KB的测试固件到QSPI Flash整个过程只有几百毫秒对比串口升级每分钟几KB的速度完全是两个时代。下载完成后在MDK的Memory窗口里输入0x90000000应该能看到App的机器码。如果能看到0x20000000或者其他RAM地址相关的变量初始化数据说明数据确实写进去了。5.4 常见失败原因与排查路径我在实际操作中遇到了多次下载失败最有价值的几个排查方向如下现象可能原因排查方法Flash Download failed - Target DLL has been cancelledQSPI引脚配置错误或Flash ID校验失败单独编写一个测试程序在RAM里跑QSPI初始化并读取JEDEC IDVerify failed at address 0x90000xxxQSPI时钟频率过高或Flash处于异常状态调大PRESCALER分频值比如把QCLK降到30MHz以下再试Erase Operation failed写使能命令没有正确发送检查QSPI_WriteEnable中CCR配置0x06命令的FMODE必须是间接写模式Programming failedFIFO溢出导致数据丢失检查ProgramPage里写DR前是否等待FTF标志下载完成后程序无法运行缺少Bootloader初始化QSPI并切换Memory-Mapped模式参考第6.3节编写一个内置Flash里的启动引导程序排查时优先检查JEDEC ID是否能正常读取。只要ID读不出来后面所有操作都白搭。而且JEDEC ID读取这个动作本身就能验证一半的问题如果ID能读出来说明GPIO配置、QSPI时序、Flash供电和焊接基本没问题。6. 做算法期间踩过的坑和几条优化思路6.1 编码、缓存、时钟这些容易绊脚的坑第一个坑是MDK的工程编码问题。MDK默认用GB2312编码保存源文件如果你在代码里写中文注释然后在其他编辑器里打开时会乱码。这个问题在FLM算法工程里尤其恶心因为算法跑在芯片RAM里如果编译器把中文字符串常量生成到代码段里FLM的体积会变大甚至可能导致RAM空间不够。我的建议是FLM工程里所有注释和字符串都用英文。第二个坑是Cortex-M7的缓存ICache/DCache对FLM算法的影响。在MDK加载算法到RAM后芯片的缓存默认是关闭的所以不会有问题。但如果在Bootloader或者App里已经打开了缓存再回MDK里做二次下载残留的缓存数据可能导致QSPI读取错误。遇到这种情况最直接的办法是在下载前先复位芯片让缓存回到关闭状态再执行Flash操作。第三个坑是QSPI时钟频率。H750在480MHz主频下AHB时钟可能是240MHz。如果QSPI的PRESCALER配成0QCLK就是240MHz远超W25Q32的133MHz上限必然通信失败。我在Init里设置PRESCALER2QCLKAHB/(2×2)60MHz这是一个各种环境都安全的保守值。如果你想压榨速度可以在你的硬件上逐步降低分频值做稳定性测试。6.2 提升下载速度的思路FLM算法现在的实现是单线模式SPI Mode下读写速度直观感受是够用了。但如果你想追求更快的下载速度可以从几个方向优化把ProgramPage里的写数据模式改为四线模式DMODE2使用0x32四线页编程命令。这样数据通过4根线并行传输理论速度提升4倍。把Verify里的读命令从0x03改为0x6B四线快速读并设置合适的Dummy Cycles。在FlashDevice结构体里把页大小从4096改成8192甚至更大这样MDK一次调用ProgramPage传入的数据更多减少函数调用次数。不过要注意四线模式下地址、指令、数据的发送模式都要改涉及的CCR配置和Dummy Cycles都要重新微调。建议先把单线版本跑通再做提速优化。6.3 下一步QSPI XIP加载App下载算法解决的是“烧写”问题但要让H750从QSPI Flash里运行代码还得处理“启动”问题。由于H750不像某些芯片那样支持直接从QSPI Flash启动我们需要在内置Flash里放一个很小的Bootloader它的工作流程是初始化QSPI外设和GPIO。把QSPI切换到Memory-Mapped模式FMODE11让0x90000000地址空间可以直接访问Flash内容。把中断向量表寄存器VTOR设置为0x90000000。读取QSPI Flash前4字节初始SP值设置主栈指针然后跳转到0x90000004处的Reset_Handler执行。这个Bootloader只需要几十行C代码配合我们的下载算法整个开发流程就变成内置Flash里放BootloaderQSPI Flash里放App所有代码下载都在MDK里一键完成。调试时还可以直接在QSPI地址上打断点这在以前用串口升级方案时想都不敢想。如果你的App工程里配置了中断向量表偏移比如SCB-VTOR 0x90000000还需要注意调试器加载符号时的地址匹配问题。在Options for Target - Debug - Settings - Flash Download里勾选“Reset and Run”这样下载完会自动复位运行Bootloader会完成QSPI初始化和跳转IDE和硬件两边都不会打架。做这个下载算法的过程中我最深的体会是FLM算法本质上就是把“操作Flash的驱动”打包成一个可加载执行的RAM程序它和你在Bootloader里写的QSPI驱动在逻辑上没有本质区别只是多了一层和MDK之间的接口协议。只要把这个协议摸清楚后面给任何外部Flash做下载算法都只是换几个命令码和参数的事。希望这篇文章能让更多人跳过那些我曾经踩过的坑直接在MDK里享受一键烧写的快感。
返回列表