尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32CubeMX基于QSPI的外部Flash烧写驱动开发全解析

STM32CubeMX基于QSPI的外部Flash烧写驱动开发全解析 做量产烧写方案的时候我给板选的主控是STM32F767板上挂了一片W25Q64外部Flash用来存OTA升级包和运行日志。要用STM32CubeMX把这颗外部Flash的烧写驱动完整做出来从QSPI外设初始化到扇区擦除、页编程、ID校验、内存映射读回之前觉得都是现成环节真正动手才发现坑全藏在细节里。这篇文章记录的就是我这次在STM32CubeMX里从零搭建外部Flash烧写驱动的完整过程包含原理、配置、代码和排障经验适合正在做Bootloader、产线烧写或者想搞清外部Flash驱动底层逻辑的嵌入式开发朋友。1. 项目背景与整体方案设计1.1 为什么要单独做外部Flash烧写驱动在很多人眼里给外部Flash烧写数据似乎不是个复杂事直接拿烧录器连一下Flash引脚就够了。实际做量产你就会发现板子上大多数外部Flash不是直接裸露给烧录器的而是挂在MCU的QSPI或者SPI总线上。烧录器只认识芯片的SWD或JTAG接口并不直接认识你板子上的QSPI Flash。要让烧录工具能通过MCU把数据写进外部Flash就必须在芯片里跑一段“烧写驱动”由这段驱动代替烧录器跟Flash打交道。这类需求的典型场景有三个。第一是OTA升级固件包先临时存到外部FlashBootloader从外部Flash读取新固件再跳转执行或者搬运到内部Flash第二是产线烧写产线上不希望每片板子都焊好固件再贴Flash而是先贴空白Flash后通过烧录器配合烧写算法把镜像写入外部Flash第三是参数与日志存储这类场景虽然不用“烧写”这个词但本质也是对外部Flash进行擦除和写入操作驱动是一套东西。这三个场景都指向同一个核心问题外部Flash驱动不是简单调几个HAL函数就能稳定上量的。你得把擦写时序、状态轮询、跨页写入、异常恢复这些问题都处理干净否则产线上一片板子擦写到一半超时你根本分不清是驱动问题、Flash芯片问题还是硬件连线问题。1.2 SPI Flash与QSPI Flash方案怎么选外部Flash接入方式主要有两种普通SPI接口和QSPI接口。普通SPI接口的数据线只有一根输入一根输出时钟频率一般支持到几十兆赫级别QSPI接口则是在SPI基础上扩展了四根数据线可以并行传输性能高不少。对STM32来说带QSPI控制器的型号主要有F4系列部分型号、F7系列、H7系列如果用的是这些型号优先走QSPI方案。我在这个项目里选的是QSPI W25Q64。为什么选W25Q64而不选其他大容量芯片首先W25Q64容量8MB对存固件升级包、日志、参数都够用其次这颗芯片在市场上的流通量极大价格稳定数据手册里的时序参数也写得非常清楚出了问题网上能找到大量案例参考。W25Q64的页大小是256字节扇区大小4KB块大小64KB擦除最小单位是扇区不是按字节擦除这些基础参数后面写驱动时全都要用上。如果只跑普通SPI也可以直接复用SPI外设CubeMX里把SPI模式配好就行。但有一个重要区别QSPI方案支持内存映射模式也就是Flash可以被映射到MCU的地址空间里CPU直接当作只读存储器访问读数据效率非常高这对OTA场景里做固件搬运是巨大优势。普通SPI方案则只能以命令方式一帧一帧读数据效率低一截。所以我个人建议只要你的MCU自带QSPI控制器外部Flash尽量走QSPI别纠结那几根引脚。1.3 硬件连接与芯片选型要点硬件连接这块经验很重要。W25Q64需要把CS、CLK、DI、DO几个基本引脚接好四线模式下还要接WP和HOLD或者IO2、IO3。在QSPI方案里IO2和IO3除了做数据线往往还兼任WP和HOLD功能这两个引脚上必须正确配置上下拉否则Flash会锁死或者进入保持状态驱动怎么调都不对。我这次使用的接线方式是把Flash挂到STM32F767的QUADSPI总线上片选、时钟、四根数据线分别接到MCU对应引脚这些引脚在CubeMX里配置的时候会自动生成复用映射原理图阶段就尽量按照CubeMX生成的引脚功能去画可以减少后续软件改引脚的麻烦。Flash的供电引脚要加0.1uF和1uF去耦电容电源纹波大是导致Flash擦写不稳定的隐藏杀手。芯片选型上还有个小提醒不要只看容量要看状态寄存器设计和指令集是不是标准厂牌兼容的。市面上很多Flash芯片宣称兼容W25Q系列实际状态寄存器的Bits定义可能不一样编程时如果照搬W25Q的代码可能擦写正常但状态查询不到位最后表现为“写进去读出来不对”。所以驱动开写之前一定要先把数据手册里的指令表、状态寄存器定义、时序参数这三个部分看明白。2. STM32CubeMX配置外部Flash接口2.1 QSPI外设参数配置详细说明打开STM32CubeMX搜索QUADSPI选择Synchronous Transceiver模式这是用QSPI外设跟外部Flash通信的标准配置。接下来需要设置几个关键参数这里逐个说。Clock Polarity和Clock Phase也就是CPOL和CPHAW25Q64支持SPI Mode 0和Mode 3绝大多数情况下选CPOLLow、CPHA1Edge即Mode 0这也是CubeMX的默认值。要是不确定你的Flash芯片支持哪种模式看数据手册的时序图对照SPI模式定义判断别凭感觉猜。Clock Divider这是分频系数决定实际通信频率。QSPI时钟等于外设输入时钟除以(divider1)我用的原理图配置将分频系数设置为2算下来通信时钟大约60MHz。对W25Q64来说这颗芯片在标准SPI模式下最高能跑104MHz60MHz留了足够余量信号稳定又不会太慢。如果板子布线比较随意、走线较长建议降到4或8保证先能通信再优化速度。Flash Size选项用于设置地址映射范围要按实际Flash容量配置。W25Q64是8MB这里就设置为8MB。如果是W25Q12816MB就选16MB。这个参数直接影响后面内存映射模式的地址边界配小了访问高位地址会出错配大了又可能把地址空间浪费掉。Memory mapped enabled这个选项主要看你的场景是否需要把Flash映射到CPU地址空间直接读。需要OTA从外部Flash执行或搬运固件的话这里必须使能如果只是走命令方式读不使能也可以。我这次使能了内存映射因为后续搬运固件方便很多。2.2 时钟树与引脚复用处理CubeMX里配置QSPI不会自动搞定时钟树你得在Clock Configuration页面把QUADSPI的时钟源和分频关系设置好。不同型号的时钟树布局差异很大F7系列QUADSPI时钟通常挂在AHB总线上确保AHB时钟频率在合理范围内再通过QSPI外设内部的分频系数得到通信时钟。引脚复用这点值得单独说。CubeMX根据你在引脚视图里选择的功能自动分配引脚这时候可能出现复用冲突。比如某个引脚既被QSPI用作数据线又被串口或定时器占用了CubeMX会在右下角的Pinout Conflict区域提示你。处理原则是优先保证QSPI需要的引脚不被抢走必要时调整其他外设到备用引脚。引脚配置完成后进System Core里的GPIO页面检查一下QSPI相关引脚的工作模式通常会被自动设置为Alternate Function速度等级建议设为High以支持更高的翻转速率。这里有个容易忽略的细节QSPI的CS引脚是片选信号虽然CubeMX会自动配置但在焊接或飞线调试时一定要确保CS默认是高电平防止Flash在上电瞬间被误选中。2.3 生成工程后的初始化检查清单CubeMX生成代码后初始化流程不是直接就能跑的我建议按以下清单逐项检查。第一检查main.c里MX_QUADSPI_Init函数是否正确执行且被调用在HAL_Init之后。第二检查HAL_QSPI_MspInit函数里有没有把QUADSPI外设时钟、GPIO时钟打开这个函数是MSP层回调时钟没使能全外设地址读出来全是0xFF。第三检查工程里是否包含了HAL库的QuadSPI模块源文件有些IDE模板默认不勾选QSPI生成的工程里缺少stm32f7xx_hal_qspi.c编译时发现函数未定义再回头找就很被动。做完这些检查可以先写一段最简单的JEDEC ID读取代码连上调试器验证驱动底层通路。如果ID能正确读出来说明CubeMX配置、时钟、引脚、初始化都没问题再往上层写擦除和编程就有底气了。这一步花不了几分钟但能让你在调试中避开“到底是硬件问题还是软件问题”的拉锯战。3. 烧写驱动核心代码实现与验证3.1 芯片ID识别与状态寄存器读取外部Flash驱动的第一道关就是读ID。读ID不是做个形式它能验证通信线序是否正确、芯片是否被正确供电、时钟配置是否合理。W25Q系列读ID的指令是0x9F正常能读出三个字节比如W25Q64返回0xEF 0x40 0x17。如果读出来全0xFF说明通信链路没通或者Flash没被选中如果读出来是乱码优先怀疑时钟极性配错了。用HAL库读ID的代码如下uint32_t W25Q64_ReadID(void) { QSPI_CommandTypeDef command {0}; uint8_t id[3] {0}; command.Instruction 0x9F; command.InstructionMode QSPI_INSTRUCTION_1_LINE; command.AddressMode QSPI_ADDRESS_NONE; command.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; command.DataMode QSPI_DATA_1_LINE; command.DummyCycles 0; command.NbData 3; command.DataSize 3; HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, id, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); return (id[0] 16) | (id[1] 8) | id[2]; }注意这里DataMode用的QSPI_DATA_1_LINE也就是通过单线方式读ID。大部分SPI Flash在初始阶段只支持单线指令QSPI四线模式是后来通过特殊指令切过去的所以读ID别急着用四线模式。凡是通信不稳定先统一回到单线模式排查。状态寄存器读取同样重要。W25Q64的状态寄存器1的bit0是BUSY位编程和擦除过程中该位为1操作完成后自动回到0。这是你判断操作是否完成的关键依据。驱动里我会写一个等待非忙函数循环读状态寄存器直到BUSY位清零。static HAL_StatusTypeDef W25Q64_WaitBusy(uint32_t timeout) { QSPI_CommandTypeDef command {0}; uint8_t status 0x01; command.Instruction 0x05; command.InstructionMode QSPI_INSTRUCTION_1_LINE; command.AddressMode QSPI_ADDRESS_NONE; command.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; command.DataMode QSPI_DATA_1_LINE; command.DummyCycles 0; command.NbData 1; command.DataSize 1; uint32_t tickstart HAL_GetTick(); do { HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, status, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); if ((status 0x01) 0) { return HAL_OK; } } while ((HAL_GetTick() - tickstart) timeout); return HAL_TIMEOUT; }这段代码会一直轮询直到芯片空闲或超时。超时值要按最慢的擦除操作估算W25Q64的块擦除典型时间是几百毫秒所以我一般把超时设成2000ms留足余量。如果你在中断里调用这套驱动这种做法会比较烧CPU建议改成状态机或者丢到后台任务里跑但做烧写驱动阶段先用轮询把流程跑通更重要。3.2 扇区擦除、页编程与数据读取擦除是外部Flash写入前置条件。Flash存储器的物理特性决定了它只能把1写成0要把0恢复成1只能通过擦除实现。W25Q64的最小擦除单位是4KB扇区指令是0x20。开发里一个常见错误是只擦除了要写数据那几字节对应的空间然后直接往其他还没擦除的地址写数据结果数据对不上。扇区擦除必须先发送写使能指令0x06再发送擦除指令否则芯片直接忽略操作。整个流程我封装成一个函数HAL_StatusTypeDef W25Q64_EraseSector(uint32_t sector_addr) { QSPI_CommandTypeDef command {0}; command.Instruction 0x06; command.InstructionMode QSPI_INSTRUCTION_1_LINE; command.AddressMode QSPI_ADDRESS_NONE; command.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; command.DataMode QSPI_DATA_1_LINE; command.NbData 0; HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); command.Instruction 0x20; command.AddressMode QSPI_ADDRESS_24_BITS; command.AddressSize QSPI_ADDRESS_24_BITS; command.Address sector_addr; command.DataMode QSPI_DATA_1_LINE; command.NbData 0; HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); return W25Q64_WaitBusy(HAL_QSPI_TIMEOUT_DEFAULT_VALUE); }页编程的指令是0x02一次最多写256字节而且不能跨页。所谓跨页是指写入的数据如果跨越了256字节对齐的页边界必须拆成两次或者多次写否则地址会回卷到页开头把不该覆盖的数据覆盖掉。很多新人在驱动里栽跟头就栽在跨页写没处理好。写一个完整的任意地址长度写入函数时我习惯先拆成页内对齐、整页、尾部三部分处理。下面这个函数支持任意起始地址和任意长度连续写入HAL_StatusTypeDef W25Q64_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t page_remain; uint32_t offset 0; HAL_StatusTypeDef status; while (offset len) { page_remain 256 - (addr % 256); uint32_t write_len (len - offset page_remain) ? page_remain : (len - offset); QSPI_CommandTypeDef command {0}; command.Instruction 0x06; command.InstructionMode QSPI_INSTRUCTION_1_LINE; command.AddressMode QSPI_ADDRESS_NONE; command.DataMode QSPI_DATA_1_LINE; command.NbData 0; HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); command.Instruction 0x02; command.AddressMode QSPI_ADDRESS_24_BITS; command.AddressSize QSPI_ADDRESS_24_BITS; command.Address addr; command.DataMode QSPI_DATA_1_LINE; command.DataSize write_len; command.NbData write_len; HAL_QSPI_Command(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Transmit(hqspi, (uint8_t *)(buf offset), HAL_QSPI_TIMEOUT_DEFAULT_VALUE); status W25Q64_WaitBusy(HAL_QSPI_TIMEOUT_DEFAULT_VALUE); if (status ! HAL_OK) { return status; } addr write_len; offset write_len; } return HAL_OK; }这个函数每次写之前都先发写使能这是最保险的做法。有些人为了省几条指令只在循环外发一次但万一写的过程中出现异常状态位被清掉后续编程就会失灵。每次写都发一次写使能代价很小换来的是稳定性。数据读取用0x03指令单线读取即可。运行时的读操作走内存映射模式更快但驱动底层还是需要一个命令方式的读取函数用于校验例如写完后读回比对。读取函数比较简单不发写使能直接命令加接收数据就行。3.3 内存映射模式切换与应用场景内存映射模式是QSPI最有价值的功能它能把外部Flash直接映射到MCU的地址空间比如0x90000000。开启之后CPU读外部Flash就像读内部存储器一样可以按字节、半字、字直接访问省去了一帧一帧发命令的时间。切换到内存映射模式的方式如下HAL_StatusTypeDef W25Q64_EnableMemoryMappedMode(void) { QSPI_CommandTypeDef command {0}; command.Instruction 0xEB; // Fast Read Quad I/O需按芯片指令表确认 command.InstructionMode QSPI_INSTRUCTION_4_LINES; command.AddressMode QSPI_ADDRESS_24_BITS; command.AddressSize QSPI_ADDRESS_24_BITS; command.AlternateByteMode QSPI_ALTERNATE_BYTES_NONE; command.DataMode QSPI_DATA_4_LINES; command.DummyCycles 2; command.NbData 1; HAL_QSPI_MemoryMapped(hqspi, command, HAL_QSPI_TIMEOUT_DEFAULT_VALUE); return HAL_OK; }这段代码里用的Fast Read Quad I/O指令0xEB是W25Q系列读取数据最快的模式DummyCycles这里需要按数据手册配置不同频率下可能不同。如果你的Flash型号不支持这个指令就不能硬套否则切到内存映射模式后读出来的数据全是异常值。使用内存映射模式时有个大坑它把Flash当作只读存储器你不能再通过命令方式对同一片区域进行写入或擦除。想擦写就得先退出内存映射模式退出方式是调用HAL_QSPI_Abort或者执行一个非映射配置的命令。所以你的驱动架构要设计成两套状态读写模式走命令接口读大量数据时切换到内存映射模式读完再切回来。我踩过的坑是在内存映射模式下看到数据异常于是直接调用擦除函数清理结果擦除命令发出去没反应查了很久才发现QSPI外设还处于内存映射状态。调试这种问题加个模式状态机变量或者用断点观察hqspi.State都能快速定位。4. 把驱动接入烧写与升级流程4.1 对接STM32CubeProgrammer烧写算法如果你需要产线直接把镜像烧进外部Flash可以把你写好的驱动封装成一个烧写算法文件加载到STM32CubeProgrammer里然后烧录器就会先把你这个算法下载到RAM里运行再通过它对外部Flash执行擦除和编程。这类似于Keil里给外部Flash做编程算法但STM32CubeProgrammer的算法文件有自己的格式和接口要求。在STM32CubeProgrammer里烧写算法通常是以.stldr文件形式存在。创建这类文件需要按ST规定的格式编写初始化、擦除、编程、校验四个接口函数然后编译打包。实际操作中不少人是借官方提供的模板改的。你只需要把模板里的初始化函数替换成自己的Flash初始化代码把擦除函数替换成扇区擦除封装把编程函数替换成写数据封装注意地址参数、返回值格式必须符合ST的要求。这个方案最大的优点是产线不用额外改硬件用ST-Link或J-Link连接SWD接口就能把应用程序和外部Flash数据一次性烧进去。我在项目里验证通过后产线效率提升非常明显不用提前烧Flash再贴片省掉了单独烧录环节。4.2 Bootloader在线升级中的驱动调用另一个常见接入点是Bootloader在线升级。整体流程是App端收到升级包后把固件镜像逐块写入外部Flash写完做校验然后Bootloader在启动时检查外部Flash里有没有待升级标志有就把外部Flash里的镜像搬运到内部App区或者直接跳转执行。外部Flash驱动在App端和Bootloader端都要用到所以驱动最好是跨工程复用的独立模块不依赖具体业务逻辑。我写驱动时会专门分出一个w25q64.c文件对外只暴露初始化、读写、擦除、映射几个接口App工程和Bootloader工程都直接引用这份源码。两边的编译环境可能不一样但驱动代码只依赖HAL库没有业务耦合这样做有个好处App端验证过的驱动Bootloader端可以直接复用不用重新验证。联调的时候容易忽略一个点Bootloader运行在低主频配置下QSPI时钟分频可能跟在App里不一样。所以驱动初始化时的分频系数最好根据系统时钟动态计算或者用宏定义区分避免出现“App里读数据正常Bootloader里读出来全是0”的诡异现象。5. 常见问题与调试经验5.1 读ID失败和初始化异常的排查读ID失败的排查应该按从简到繁的顺序来。先确认供电电压和去耦电容没接错再用示波器量CS、CLK、Data引脚在初始化过程中是否有信号翻转。如果你没有示波器最简单的办法是先把通信频率降下来把QSPI分频系数调大比如设置到8或者16排除高频信号完整性问题。软件层面重点检查三件事CubeMX里有没有把QUADSPI外设时钟启用GPIO复用模式是否配置为复用功能HAL_QSPI_MspInit函数是否被HAL_QSPI_Init调用。这三步没问题基本能排除大部分初始化异常。还有一个容易忽视的如果你用了外部上下拉电阻必须确认上拉或下拉的方向和Flash引脚默认电平一致W25Q64的HOLD引脚和WP引脚如果悬空可能导致芯片进入保护或保持状态反应到软件上就是ID读不出来或写不进去。排查时可以加打印信息把每次命令操作后HAL_QSPI_GetError的返回值打印出来HAL库错误码能帮你缩小范围比如超时错误、DMA传输错误、命令参数错误分类很明确。5.2 擦除编程超时与状态位检查擦除编程超时是量产里最常见的故障现象。第一种情况是Flash芯片的BUSY位一直为1这通常意味着芯片工作条件不满足比如供电电压偏低、时钟频率过高导致通讯不稳定、或者芯片本身已经损坏。第二种情况是软件逻辑问题比如你忘了发送写使能指令Flash状态寄存器的写使能锁存位是0导致擦除指令被直接忽略但你还在死等BUSY位。排查时我习惯在擦除或编程前后各读一次状态寄存器把S0到S7的状态都打印出来。W25Q64的状态寄存器bit1是写保护位如果这个位是1说明芯片被软件写保护了必须先执行写状态寄存器指令0x01清除保护。不同厂牌的Flash状态寄存器定义可能不同务必以你手头芯片的规格书为准。等待超时的另一个隐蔽原因是代码在低优先级任务或中断里被频繁打断读状态寄存器的时序被拉长。虽然Flash不会因为读得慢而出错但总超时时间是我们按实际时间估算的如果你用了带操作系统的工程超时判断最好用系统Tick而不是简单靠HAL_Delay。5.3 信号完整性与时钟频率的取舍最后聊一个偏硬件但直接影响软件稳定性的点QSPI通信频率不是越高越好。在样机阶段你可能用60MHz跑得稳稳的到了量产PCB上布线变更、地平面不完整、引脚过长高频信号反射问题就暴露出来了具体表现是时好时坏读ID偶尔失败擦写偶尔超时。我这次遇到的案例是Flash挂在FPC排线上用60MHz分频时读ID正常但连续擦写超过100片板子后会出现零星超时。排线本身没有问题问题在于高速信号在排线连接处阻抗不连续。最后把分频系数从2调到4把频率降到大约30MHz故障就消失了而烧写速度的损失对产线来说完全在接受范围内。所以做外部Flash烧写驱动不一定非得追求最高频率。我现在的习惯是先把频率降低把功能调试稳定等板子量产版本冻结后再试着提高频率每次提升都要跑完整的压力测试至少连续擦写1000次不报错才敢保留这个参数。还有个细节QSPI的Dummy Cycles参数在不同频率下可能要求不同。当你把QSPI频率提高后发现内存映射模式读出来的数据有错位大概率就是Dummy Cycles没按要求调。这个参数在数据手册的AC Characteristics章节里能找到按频率区间查对应数值。外部Flash烧写驱动看起来是底层的小功能实际做下来牵扯到芯片选型、硬件设计、CubeMX配置、时序逻辑、产线适配和信号完整性一整条链路。如果你也在参照这个思路做建议先把驱动跑在最低频率上从读ID开始一步步验证不要上来就追求高性能稳定永远是第一位的。
返回列表