YAOTU INSIGHTS

STM32 SPI+DMA读写外部Flash实战:从阻塞到高效传输

STM32 SPI+DMA读写外部Flash实战:从阻塞到高效传输
1. 项目缘起与整体方案拆解1.1 为什么我要折腾SPIDMA读写外部Flash手头这个STM32项目主控是STM32F103系列外挂了一颗W25Q64 NOR Flash用来存日志、配置参数和一部分字库数据。一开始我用的是最朴素的阻塞式SPI读写HAL_SPI_Transmit和HAL_SPI_Receive轮询着来单字节读写没问题但一旦要连续读几KB的数据CPU基本就被SPI占死了主循环里其他任务全部卡住串口响应变慢连按键扫描都开始丢帧。这个问题的本质在于SPI的时钟频率虽然可以跑到18MHz甚至更高但CPU要一个字节一个字节地搬运数据每次搬运都要等标志位、清标志位真正有效的数据吞吐被CPU的介入开销严重拖累。外部Flash的页编程和扇区擦除本身就有毫秒级的等待时间如果在这期间CPU还在傻等SPI标志位那整个系统的实时性就没法看了。解决思路很直接把数据搬运这件事从CPU手里拿走交给DMA控制器去做。CPU只负责发起传输、配置好源地址和目的地址然后就可以去处理其他任务等DMA传输完成中断来了再回来收尾。这就是SPIDMA组合的核心价值——让CPU从繁琐的字节搬运中解放出来专注于业务逻辑。这套方案适合谁呢如果你正在用STM32做数据采集、日志存储、OTA升级、字库读取这类需要频繁和大块数据打交道的项目尤其是对实时性有要求的场景那SPIDMA基本是绕不开的配置。哪怕你现在用的是阻塞方式跑得也还行我建议你花点时间把这套机制吃透后面项目复杂度上来了迟早要用到。1.2 整体架构与数据流向设计在动手写代码之前先把整个数据通路理清楚。STM32F103的SPI外设和DMA控制器之间的配合关系是这样的发送方向内存缓冲区 → DMA通道 → SPI数据寄存器 → MOSI引脚 → Flash接收方向Flash → MISO引脚 → SPI数据寄存器 → DMA通道 → 内存缓冲区这里有个关键点SPI是全双工总线发送和接收是同时进行的。也就是说当你发送一个字节的时候同时也会接收到一个字节。对于Flash的读操作我们需要先发送命令和地址比如0x03读命令3字节地址然后再接收数据。对于写操作需要先发送写使能命令再发送页编程命令地址数据。DMA控制器的角色就是一个搬运工它不关心数据内容是什么只负责按照配置好的源地址、目的地址和数据长度一个字节一个字节地搬。STM32F103有DMA1和DMA2两个控制器SPI1的TX和RX分别对应DMA1的Channel3和Channel2SPI2的TX和RX对应DMA1的Channel5和Channel4。这个对应关系是硬件固定的配置的时候不能搞错。整个方案的架构可以分成三层来理解应用层调用Flash读写接口传入缓冲区指针和长度驱动层封装SPIDMA的传输逻辑处理命令、地址、数据的发送和接收硬件层SPI外设配置、DMA通道配置、GPIO引脚配置这样分层的好处是应用层不需要关心底层用的是DMA还是中断还是轮询驱动层可以灵活切换实现方式硬件层的配置通过CubeMX生成后基本不用大改。1.3 方案选型为什么是DMA而不是中断有人可能会问用SPI中断方式不也能解放CPU吗每收发一个字节进一次中断在中断里处理数据。这个方案在小数据量下确实可行但数据量一大中断频率就上去了。假设SPI时钟18MHz一个字节8位加上开销大概每个字节2微秒左右也就是每2微秒进一次中断。这个中断频率对STM32F103来说压力很大中断上下文切换的开销会吃掉大量CPU时间而且中断嵌套处理不好还容易丢数据。DMA的优势在于它是硬件级别的数据搬运不需要CPU介入每个字节的传输。配置好之后DMA控制器自己完成所有搬运工作只在传输完成或半完成时产生一次中断。对于几KB甚至几十KB的数据传输CPU只需要在开始和结束时各介入一次中间的过程完全不用管。还有一个实际考量是代码的简洁性。用DMA的话传输逻辑很清晰配置DMA → 启动传输 → 等完成标志 → 处理数据。用中断的话状态机要复杂得多要处理各种边界情况代码维护成本高。当然DMA也不是没有代价的。DMA通道是有限资源STM32F103的DMA1只有7个通道如果项目里还有ADC、串口、I2C都在用DMA就要仔细规划通道分配。另外DMA传输和CPU访问同一块内存时会有总线仲裁开销不过对于SPI这种中低速外设来说这个影响基本可以忽略。2. 核心细节解析与实操要点2.1 SPI外设配置的关键参数SPI的配置直接决定了通信的稳定性和速度几个关键参数必须搞清楚。时钟极性和相位CPOL/CPHA这两个参数决定了SPI的采样时刻。NOR Flash如W25Q64通常支持Mode 0CPOL0CPHA0和Mode 3CPOL1CPHA1。我一般用Mode 0也就是空闲时时钟为低电平第一个边沿上升沿采样数据。这个模式最通用大部分Flash都支持。如果你不确定手里的Flash用哪个模式查数据手册的时序图看它是在时钟的哪个边沿采样数据的。数据帧格式STM32的SPI可以配置为8位或16位数据帧。对于Flash操作统一用8位因为Flash的命令、地址、数据都是按字节组织的。CubeMX里把Data Size设成8 Bits就行。时钟分频STM32F103的SPI1挂在APB2总线上时钟频率72MHz。分频系数可以选2、4、8、16、32、64、128、256。W25Q64的最高SPI时钟是80MHz部分型号104MHz但实际能跑多快取决于PCB布线和信号完整性。我一般先用分频89MHz调通确认稳定后再逐步提高到分频418MHz或分频236MHz。注意分频2的时候SPI时钟是36MHz已经超过W25Q64在3.3V下的标准最高频率了虽然很多模块实测能跑但批量生产时建议留余量用分频4比较稳妥。片选管理SPI的NSS片选信号有两种管理方式——硬件片选和软件片选。硬件片选是SPI外设自动控制NSS引脚多主机模式下用得多。单主机驱动单个Flash的场景我强烈建议用软件片选也就是把NSS引脚配置成普通GPIO输出在代码里手动拉低拉高。原因很简单硬件片选在DMA传输时的行为不好控制而且Flash的命令序列中间可能需要保持片选有效但插入其他操作软件片选灵活得多。注意用软件片选时NSS引脚在CubeMX里要配置成GPIO_Output初始电平设为高片选无效。SPI外设的NSS模式要选Software否则硬件会干扰你的GPIO控制。2.2 DMA通道配置与数据宽度匹配DMA的配置有几个容易踩坑的地方我一个个说。通道选择前面提过SPI1_TX对应DMA1_Channel3SPI1_RX对应DMA1_Channel2。CubeMX里添加DMA请求时直接选SPI1_TX和SPI1_RX它会自动分配到正确的通道。如果你用的是SPI2对应的是DMA1_Channel5TX和DMA1_Channel4RX。数据宽度这是最容易出错的地方。DMA的源地址和目的地址数据宽度必须和SPI的数据帧格式匹配。SPI配置为8位数据帧时DMA的Peripheral和Memory数据宽度都要设成Byte8位。如果设成Half Word16位DMA会一次搬2个字节但SPI一次只处理1个字节数据就错位了。我见过有人在这里栽跟头读出来的数据全是乱的查了半天才发现是DMA数据宽度设错了。传输方向发送时方向是Memory → Peripheral接收时方向是Peripheral → Memory。CubeMX里配置DMA请求时TX通道的方向会自动设为Memory to PeripheralRX通道设为Peripheral to Memory一般不用手动改。优先级如果项目里多个DMA通道同时工作要合理设置优先级。SPI的DMA优先级建议设为Medium或High太低的话可能被其他通道抢占导致传输延迟。不过对于SPI Flash这种非实时性要求极高的场景Medium就够了。循环模式Flash读写用Normal模式传输完指定长度后DMA自动停止。Circular模式适合ADC连续采样这类场景Flash读写用不上。2.3 Flash命令序列与DMA的配合方式NOR Flash的操作有一套标准的命令序列和DMA配合的时候需要仔细设计。读操作Read Data命令0x03先拉低片选发送命令0x03再发送3字节地址24位地址然后就可以连续读取数据了。用DMA的话流程是这样的拉低片选用阻塞方式发送命令地址4个字节数据量小不值得用DMA配置RX DMA设置接收缓冲区和长度启动DMA接收等待DMA传输完成拉高片选这里有个细节发送命令和地址的时候SPI的RX寄存器也会收到数据因为全双工但这些数据是无效的直接忽略就行。关键是发送完命令和地址后要确保SPI的RX缓冲区是空的否则DMA接收到的第一个字节可能是之前残留的数据。我的做法是在启动DMA接收前先读一次SPI的DR寄存器清空缓冲区。页编程Page Program命令0x02先发送写使能命令0x06再发送页编程命令0x023字节地址然后发送最多256字节的数据。用DMA发送数据的话发送写使能命令0x06阻塞方式拉低片选发送页编程命令0x023字节地址阻塞方式配置TX DMA设置发送缓冲区和长度启动DMA发送等待DMA传输完成拉高片选等待Flash内部编程完成轮询状态寄存器或固定延时注意页编程的地址必须对齐到页边界256字节跨页写入需要拆分成多次页编程操作。这是Flash的硬件限制不是DMA的问题但用DMA的时候更容易忽略因为DMA只管搬数据不管地址对齐。扇区擦除Sector Erase命令0x20擦除操作不需要DMA因为只发送命令和地址没有数据阶段。但擦除后需要等待W25Q64的扇区擦除典型时间是45ms最大400ms。等待方式可以轮询状态寄存器的BUSY位也可以固定延时。我一般用轮询效率高一些。2.4 中断与回调的设计DMA传输完成中断是整个流程的收尾环节。CubeMX里配置DMA时可以勾选中断使能。传输完成后会进入DMA1_Channel2_IRQHandlerRX或DMA1_Channel3_IRQHandlerTX然后调用HAL库的回调函数HAL_SPI_TxRxCpltCallback或HAL_SPI_RxCpltCallback。这里有个设计选择是用HAL库的阻塞式DMA传输函数HAL_SPI_Transmit_DMA/HAL_SPI_Receive_DMA配合回调还是自己写状态机我建议用HAL库的函数配合回调代码简洁不容易出错。但要注意HAL库的DMA传输函数是异步的调用后立即返回你需要用一个标志变量来标记传输是否完成。我的做法是定义一个全局的volatile变量spi_dma_done在回调函数里置1在主循环里轮询这个变量。如果项目对实时性要求高可以在回调里直接处理数据或者发信号量给RTOS任务。用RTOS的话回调里调用xSemaphoreGiveFromISR释放信号量任务里等信号量这样最干净。提示HAL库的SPI DMA传输函数在传输完成后会自动关闭SPI的DMA请求但不会自动拉高片选。片选的控制必须自己在传输完成后手动处理否则Flash会一直处于选中状态功耗增加而且可能干扰其他SPI设备。3. 实操过程与核心环节实现3.1 CubeMX配置全流程打开CubeMX选好芯片型号STM32F103C8T6或其他型号开始配置。RCC配置High Speed Clock选Crystal/Ceramic Resonator外部晶振。Clock Configuration里把HCLK设成72MHzAPB2设成72MHzSPI1挂APB2APB1设成36MHz。SPI1配置ModeFull-Duplex MasterHardware NSS SignalDisable用软件片选Data Size8 BitsFirst BitMSB FirstPrescaler8先设低速调通后面再改Clock PolarityLowClock Phase1 EdgeCRC CalculationDisabledGPIO配置PA5SPI1_SCK复用推挽输出PA6SPI1_MISO浮空输入或上拉输入PA7SPI1_MOSI复用推挽输出PA4GPIO_Output初始High作为Flash的片选DMA配置在SPI1的DMA Settings里点AddSPI1_RXDMA1 Channel2Priority MediumMode NormalData Width ByteSPI1_TXDMA1 Channel3Priority MediumMode NormalData Width Byte两个通道都勾选NVIC中断NVIC配置确保DMA1_Channel2_IRQn和DMA1_Channel3_IRQn使能优先级设成一样的避免嵌套问题。生成代码后HAL库会初始化好SPI和DMA我们只需要在应用层调用就行。3.2 底层驱动封装先定义几个宏和全局变量#define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) extern SPI_HandleTypeDef hspi1; extern DMA_HandleTypeDef hdma_spi1_rx; extern DMA_HandleTypeDef hdma_spi1_tx; volatile uint8_t spi_dma_tx_done 0; volatile uint8_t spi_dma_rx_done 0;DMA传输完成回调void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { spi_dma_tx_done 1; } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { spi_dma_rx_done 1; } }封装一个阻塞式发送单字节的函数用于发送命令和地址static uint8_t spi_send_byte(uint8_t data) { uint8_t rx_data; HAL_SPI_TransmitReceive(hspi1, data, rx_data, 1, 100); return rx_data; }封装DMA发送函数static int spi_dma_send(uint8_t *buf, uint16_t len) { spi_dma_tx_done 0; if (HAL_SPI_Transmit_DMA(hspi1, buf, len) ! HAL_OK) { return -1; } uint32_t tickstart HAL_GetTick(); while (!spi_dma_tx_done) { if (HAL_GetTick() - tickstart 1000) { return -2; // 超时 } } return 0; }封装DMA接收函数static int spi_dma_receive(uint8_t *buf, uint16_t len) { spi_dma_rx_done 0; if (HAL_SPI_Receive_DMA(hspi1, buf, len) ! HAL_OK) { return -1; } uint32_t tickstart HAL_GetTick(); while (!spi_dma_rx_done) { if (HAL_GetTick() - tickstart 1000) { return -2; } } return 0; }3.3 Flash读操作完整实现读操作是最常用的也是DMA优势最明显的场景。以W25Q64为例读命令是0x03地址24位。int flash_read(uint32_t addr, uint8_t *buf, uint32_t len) { if (buf NULL || len 0) return -1; FLASH_CS_LOW(); // 发送读命令和地址 spi_send_byte(0x03); spi_send_byte((addr 16) 0xFF); spi_send_byte((addr 8) 0xFF); spi_send_byte(addr 0xFF); // 清空RX缓冲区防止残留数据干扰DMA接收 __HAL_SPI_CLEAR_OVRFLAG(hspi1); volatile uint8_t dummy hspi1.Instance-DR; (void)dummy; // DMA接收数据 int ret spi_dma_receive(buf, len); FLASH_CS_HIGH(); return ret; }这里有个细节值得展开说发送完命令和地址后SPI的RX寄存器里可能还有之前发送命令时收到的无效数据。如果不清理DMA接收的第一个字节就会是这些残留数据导致整个缓冲区错位。我的做法是读一次DR寄存器把残留数据读走。__HAL_SPI_CLEAR_OVRFLAG是清除溢出标志防止因为RX缓冲区满导致后续接收失败。另外spi_dma_receive里的超时时间设的是1000ms对于几KB的数据传输来说绰绰有余。如果传输的数据量特别大比如几百KB要适当加大超时时间或者用更灵活的超时机制。3.4 Flash写操作完整实现写操作比读操作复杂因为要处理写使能、页对齐和等待编程完成。static int flash_write_enable(void) { FLASH_CS_LOW(); spi_send_byte(0x06); FLASH_CS_HIGH(); return 0; } static int flash_wait_busy(uint32_t timeout_ms) { uint32_t tickstart HAL_GetTick(); uint8_t status; FLASH_CS_LOW(); spi_send_byte(0x05); // 读状态寄存器命令 do { status spi_send_byte(0xFF); if ((status 0x01) 0) { FLASH_CS_HIGH(); return 0; } } while (HAL_GetTick() - tickstart timeout_ms); FLASH_CS_HIGH(); return -1; // 超时 } int flash_write_page(uint32_t addr, uint8_t *buf, uint16_t len) { if (buf NULL || len 0 || len 256) return -1; // 检查页对齐 if ((addr % 256) len 256) { return -2; // 跨页需要拆分 } flash_write_enable(); FLASH_CS_LOW(); spi_send_byte(0x02); // 页编程命令 spi_send_byte((addr 16) 0xFF); spi_send_byte((addr 8) 0xFF); spi_send_byte(addr 0xFF); int ret spi_dma_send(buf, len); FLASH_CS_HIGH(); if (ret ! 0) return ret; // 等待编程完成 return flash_wait_busy(100); }跨页写入需要拆分成多次页编程封装一个自动拆分的函数int flash_write(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t remaining len; uint32_t offset 0; while (remaining 0) { uint32_t page_remain 256 - (addr % 256); uint32_t write_len (remaining page_remain) ? remaining : page_remain; int ret flash_write_page(addr, buf offset, write_len); if (ret ! 0) return ret; addr write_len; offset write_len; remaining - write_len; } return 0; }3.5 扇区擦除与性能实测擦除操作不需要DMA但等待时间比较长需要合理处理。int flash_erase_sector(uint32_t addr) { flash_write_enable(); FLASH_CS_LOW(); spi_send_byte(0x20); // 扇区擦除命令 spi_send_byte((addr 16) 0xFF); spi_send_byte((addr 8) 0xFF); spi_send_byte(addr 0xFF); FLASH_CS_HIGH(); return flash_wait_busy(500); // 扇区擦除最大400ms }实测数据SPI时钟18MHzW25Q64操作数据量阻塞方式耗时DMA方式耗时CPU占用读数据4KB约2.1ms约1.8ms阻塞100% vs DMA约5%写数据256B约0.3ms约0.25ms阻塞100% vs DMA约8%扇区擦除4KB约45ms约45ms轮询等待从数据可以看出DMA方式在传输时间上的优势不算特别大因为SPI时钟频率是瓶颈但CPU占用率的差异是巨大的。阻塞方式下CPU全程被占用DMA方式下CPU只在开始和结束时介入中间可以去处理其他任务。对于多任务系统来说这个差异是决定性的。4. 常见问题与排查技巧实录4.1 DMA传输卡死或数据错位这是最常见的问题表现是程序卡在while(!spi_dma_done)里出不来或者读出来的数据全是0xFF或乱码。排查思路先确认DMA中断有没有进。在回调函数里打个断点或者翻转一个GPIO用示波器看有没有波形。如果中断没进检查NVIC配置里DMA通道的中断有没有使能优先级有没有被其他中断屏蔽。如果中断进了但数据不对重点查DMA的数据宽度配置。SPI是8位DMA的Peripheral和Memory数据宽度都必须是Byte。我遇到过有人把Memory数据宽度设成Half Word结果DMA一次搬2字节但SPI只发1字节数据就错位了。还有一个隐蔽的问题DMA的传输长度和SPI的实际传输长度不匹配。比如你配置DMA接收100字节但SPI只发了50字节就拉高了片选DMA会一直等剩下的50字节直到超时。所以片选拉高的时机必须在DMA传输完成之后。实操心得调试DMA问题时先用小数据量比如4字节测试确认基本流程通了再加大数据量。小数据量下问题更容易定位。4.2 Flash读出来的数据全是0xFF0xFF是Flash擦除后的默认值如果读出来全是0xFF说明要么地址不对要么Flash根本没写入成功。先确认写入操作有没有成功。写完之后立刻读回来对比如果读回来是0xFF说明写入失败。检查写使能命令有没有发页编程命令的地址对不对有没有等待编程完成。还有一个可能Flash的写保护引脚WP被拉低了。W25Q64的WP引脚如果为低状态寄存器的保护位可能被置位导致写入被拒绝。检查硬件电路WP引脚应该上拉到VCC。另外如果你用的是自己画的板子检查SPI的MISO和MOSI有没有接反。这个错误很低级但很常见接反了读出来就是全0xFF或全0x00。4.3 高速SPI下数据不稳定把SPI时钟从9MHz提到18MHz或36MHz后发现偶尔读出来的数据有误码。这通常是信号完整性问题。检查以下几点SPI的走线是否过长尽量缩短Flash和MCU之间的走线距离有没有加匹配电阻高速SPI下可以在SCK线上串一个22Ω到33Ω的电阻减少反射电源去耦是否到位Flash的VCC引脚旁边要放0.1μF的陶瓷电容MISO线的上拉电阻是否合适一般用10kΩ上拉到VCC如果硬件上没法改那就降低SPI时钟频率。稳定比速度重要9MHz对于大多数应用已经够用了。4.4 常见问题速查表现象可能原因解决方法DMA传输卡死中断未使能或优先级被屏蔽检查NVIC配置确认DMA中断使能数据错位DMA数据宽度与SPI不匹配两者都设为Byte读出全0xFF写入失败或地址错误检查写使能和页编程命令确认地址读出全0x00MISO/MOSI接反或Flash未供电检查硬件连接高速下误码信号完整性问题降低时钟频率或加匹配电阻片选一直有效DMA完成后未拉高片选在传输完成后手动拉高CS跨页写入失败地址未对齐到页边界拆分写入每页不超过256字节4.5 几个容易被忽略的细节DMA传输完成但SPI还在忙HAL库的DMA传输完成回调触发时SPI的最后一个字节可能还在移位寄存器里没发完。如果这时候立刻拉高片选最后一个字节可能丢失。我的做法是在回调里等SPI的BSY标志清零再拉高片选或者加一个微秒级的短延时。Flash的忙状态判断擦除和编程操作后Flash内部需要时间完成操作这期间它不会响应任何命令除了读状态寄存器。必须等BUSY位清零才能发下一条命令。我见过有人在擦除后立刻发读命令结果读出来全是0xFF就是因为Flash还在忙。DMA通道冲突如果项目里SPI1和SPI2都在用DMA注意它们的通道分配。SPI1_TX是DMA1_Channel3SPI2_TX是DMA1_Channel5不冲突。但如果SPI1_RX和USART1_RX都要用DMA1_Channel2那就冲突了需要重新规划。CubeMX在分配通道时会提示冲突注意看警告信息。电源管理Flash在擦除和编程时电流比较大W25Q64编程电流约25mA如果MCU的3.3V LDO供电能力不足可能导致电压跌落Flash操作失败。确保LDO能提供足够的电流Flash的VCC引脚旁边加足够的去耦电容。5. 性能优化与进阶玩法5.1 用DMA双缓冲提升连续读写效率单缓冲的DMA传输有个问题传输完成到下一次传输启动之间有个间隙CPU需要处理回调、重新配置DMA这段时间SPI总线是空闲的。对于连续的大数据量读写这个间隙会累积成可观的延迟。STM32F103的DMA不支持真正的双缓冲模式那是F4和F7才有的但可以用两个DMA通道交替工作的方式模拟。具体做法是把数据分成两块第一块用DMA1_Channel2传输第二块用DMA1_Channel3传输如果SPI的TX和RX刚好对应这两个通道的话。不过对于SPI Flash读写来说这个优化的收益有限因为Flash的命令序列本身就有间隙DMA的间隙不是瓶颈。更实际的优化是减少命令开销。比如读操作每次都要发命令地址4字节如果连续读多个扇区的数据可以把多次读操作合并成一次大传输。W25Q64支持连续读地址会自动递增只要不超过芯片容量就行。5.2 结合RTOS的任务化封装如果项目用了FreeRTOS可以把Flash读写封装成独立的任务通过队列接收读写请求处理完后通过信号量通知调用者。这样应用层完全不用关心底层是DMA还是轮询只管发请求等结果。typedef struct { uint8_t op; // 0读, 1写, 2擦除 uint32_t addr; uint8_t *buf; uint32_t len; SemaphoreHandle_t done_sem; int result; } flash_request_t;Flash任务里等队列收到请求后调用对应的驱动函数完成后释放信号量。调用者发完请求后等信号量拿到结果继续执行。这种设计在多任务环境下特别干净Flash操作不会阻塞其他任务。5.3 实测性能数据与优化前后对比我在STM32F103C8T6 W25Q64的平台上做了一组对比测试SPI时钟18MHzDMA方式FreeRTOS环境场景优化前优化后提升读4KB数据2.1ms阻塞1.8msDMA14%写256B数据0.3ms阻塞0.25msDMA17%读4KB时CPU占用100%5%95%写256B时CPU占用100%8%92%连续读1MB520ms460ms12%传输时间上的提升不算惊人因为SPI时钟频率是硬瓶颈。但CPU占用率的改善是质变从100%降到5%意味着CPU可以在Flash传输期间处理其他任务系统的整体吞吐量提升了好几倍。5.4 几个实用的调试技巧用GPIO翻转测量传输时间在Flash读写的开始和结束各翻转一个空闲GPIO用示波器或逻辑分析仪测量高电平持续时间就是实际的传输耗时。这个方法比在代码里打时间戳更准确因为不受中断干扰。用逻辑分析仪抓SPI波形如果数据不对用逻辑分析仪抓SCK、MOSI、MISO、CS四根线的波形对照Flash数据手册的时序图一眼就能看出问题。命令字节、地址字节、数据字节在波形上清清楚楚。我调试Flash问题时逻辑分析仪是最有效的工具没有之一。分步验证先验证单字节读写再验证多字节DMA读写再验证跨页写入最后验证擦除。每一步都确认无误后再进行下一步不要一次性把所有功能都写完再调试那样出了问题很难定位。保留阻塞方式的备用接口DMA方式虽然好但调试阶段或者极端情况下比如DMA通道被占用阻塞方式可以作为备用。我在驱动里同时保留了阻塞和DMA两套接口通过宏切换调试的时候用阻塞量产的时候用DMA。最后分享一个小技巧Flash的ID读取是验证SPI通信是否正常的最快方法。发0x9F命令读回来3个字节W25Q64应该是0xEF、0x40、0x17。如果读回来不对说明SPI配置有问题先别急着调DMA把SPI基础通信调通再说。这个步骤能帮你省下大量排查时间。