STM32 SPI NOR Flash驱动实战:从CubeMX配置到DMA传输与排错
发布时间:2026/9/28 14:45:53来源:尧图网络
直接进正题。前阵子做一个设备需要在本地保存几百KB的配置参数和运行日志E2PROM容量不够上SD卡又显得小题大做最后敲定用SPI NOR Flash控制器是STM32F407工程用STM32CubeMX生成HAL库代码。整套东西从硬件连到软件调通前前后后折腾了不少时间中间踩了几个很典型的坑其中一个就是SPI通信不生效排查了半天结果发现是片选引脚配置的问题。所以这篇文章我把完整过程梳理一遍从CubeMX配置、SPI协议和STM32硬件SPI的对应关系到NOR Flash的读ID、擦除、页编程这些基础操作再到DMA方式传输和实战排错思路全部串起来讲。这篇文章适合两类人。一类是刚接触STM32和SPI的开发者照着步骤做能少走弯路另一类是用CubeMX配完SPI却发现收发异常、不知道从哪查起的人第六部分的排查链路就是给这类情况的。文中的所有代码和配置我都基于STM32F407 W25Q128这套组合实测过但思路同样适用于W25Q64、SST25VF系列也适用于SPI2/SPI3。1. 准备工作器件选型、硬件接线与CubeMX环境1.1 为什么是SPI NOR Flash这个组合先说选型逻辑。设备存储方案无非几种EEPROM、NOR Flash、NAND Flash、SD卡。EEPROM容量普遍在几KB到几十KB适合存MAC地址、校准参数这种小数据但存不下日志和固件升级镜像。NAND Flash容量大、成本低但坏块管理、ECC校验这些东西写起来非常费劲。SD卡倒是容量管够但掉电保护、文件系统、卡座可靠性都够喝一壶的。NOR Flash正好卡在中间容量几MB到几十MB随机读取快可以像内存一样按地址读出写入按页、擦除按扇区非常适合存配置参数、运行日志、字库、OTA升级包这类中小规模数据。接口上SPI只用4根线比并口NOR Flash省下十几根引脚读写速度虽然不如并口但对绝大多数应用场景绰绰有余。W25Q128这个型号市场占有率最高文档、例程、工具都齐全踩坑时也最容易查到资料所以我选了它。1.2 CubeMX环境准备STM32CubeMX和F4固件包都从ST官网下载没什么特殊的。有一个实操细节CubeMX在首次创建F4工程时会联网拉取固件包网络不好的时候进度条能卡到让人怀疑人生。这种情况不用干等去CubeMX的Help - Manage embedded software packages找到F4的zip包手动下载然后点击From Local导入比在线下载稳定得多。界面语言默认英文网上有汉化的教程但我个人不太建议汉化。不是排斥中文而是CubeMX生成的代码、参数名称、ST官方文档全是英文保持英文界面反而更容易对照参数含义。用熟之后你会发现配置界面的单词翻来覆去就那几个根本不需要字典。1.3 硬件接线F407与W25Q128的引脚对应我用的SPI1接线表如下F407引脚复用功能W25Q128引脚说明PA5SPI1_SCKCLK时钟信号PA6SPI1_MISODOFlash输出数据PA7SPI1_MOSIDIFlash输入数据PB12普通GPIO输出CS片选信号低电平有效3.3V-VCC、VDD供电GND-GND、WP、HOLDWP和HOLD接GND或3.3V这里有个特别容易翻车的点W25Q128的WP引脚写保护和HOLD引脚传输暂停一定不能悬空。WP如果悬空或者被拉低写使能和擦除操作会直接失效现象是Status Register里的位怎么都改不动HOLD如果悬空在某些电气干扰下芯片会进入暂停状态数据读到一半卡住。这两个引脚在普通应用里不需要用直接接3.3V是最省心的做法。我最初打样时忽略了这个结果SPI初始化、指令都对但擦除就是擦不掉查了整整一个下午。2. SPI协议的核心机制与STM32硬件SPI的对应关系2.1 SPI四线工作流程SPI本质上是两个移位寄存器之间的环形交换。主机产生SCK时钟每个时钟沿MOSI输出一个bit到从机同时MISO从从机采回一个bit。也就是说SPI是全双工的发送和接收永远是同时进行的。这个同时进行的特性对初学者来说是个认知门槛。很多人写代码时把发送和接收当成两个独立操作先发指令再收数据。但在SPI总线上你收到的每一个字节都对应你发出的那个字节。NOR Flash的读操作之所以能工作是因为主机发送的每个clk都会从Flash那拿回一个bit所以读数据时必须持续发送时钟发什么内容无所谓一般发0xFF或者0x00重点在于时钟本身。用个不恰当的类比SPI像两个人隔着一扇旋转门你推一下门对面的人就出来一点。你推门是为了让对面出来而不是为了推门本身。NOR Flash读取就是这种模式主机不断给时钟Flash就不断吐出数据。2.2 CPOL/CPHA四种模式与Mode 0的选择SPI通信模式由CPOL时钟极性和CPHA时钟相位共同决定。CPOL决定SCK空闲电平是高还是低CPHA决定数据在SCK的上升沿还是下降沿被采样。组合起来就是四种模式。SPI模式CPOLCPHACubeMX中配置Mode 0Low1 EdgeClock Polarity: Low, Clock Phase: 1 EdgeMode 1Low2 EdgeClock Polarity: Low, Clock Phase: 2 EdgeMode 2High1 EdgeClock Polarity: High, Clock Phase: 1 EdgeMode 3High2 EdgeClock Polarity: High, Clock Phase: 2 EdgeW25Q系列的默认推荐是Mode 0也支持Mode 3。工程里用Mode 0就可以了。CubeMX里的表述容易让人绕晕Clock Phase里写的1 Edge对应CPHA0第一个边沿采样2 Edge对应CPHA1第二个边沿采样。我第一次配的时候以为1 Edge是上升沿、2 Edge是下降沿实际完全是另一回事后来用逻辑分析仪对着波形才彻底搞明白。如果Mode配错现象很典型SCK电平翻转时间不对MISO采到的数据整体错位读ID会读到完全不可预期的值。这个问题在后面排查章节还会细说。2.3 波特率计算与时钟树STM32F407有两组APB总线SPI1挂在APB2上最高84MHzSPI2和SPI3挂在APB1上最高42MHz。时钟树配置时得先确认自己用的是哪个SPI然后看对应APB的频率。W25Q128JV最高支持133MHz的SPI时钟但STM32F407的SPI1在84MHz APB2下Prescaler最低是2分频也就是42MHz。42MHz能不能用理论上可以但实际要看PCB走线质量、Flash供电、干扰情况。我一般会保守一点选4分频也就是21MHz。对于配置数据读写来说这个速度完全够用写操作瓶颈本身在Flash擦除时间上SCK再高也改变不了擦除要几十毫秒这个事实。这里给一个经验值如果你的板子SPI走线超过5厘米或者没有做过阻抗匹配优先选21MHz如果Flash就在MCU旁边、走线很短可以上42MHz读写大块数据能明显感觉到快。不要一上来就追求极限频率通信稳定比什么都重要。2.4 硬件NSS与软件片选工程上的取舍CubeMX里SPI的NSS配置有硬件和软件两种方式。硬件NSS又分NSS输出和NSS脉冲两种由芯片硬件自动控制CS引脚软件NSS则是把CS引脚留给用户程序自己拉低拉高。实际工程里我强烈建议选软件NSS。原因有三点。第一Flash的每个操作都有明确的指令边界CS什么时候拉低、拉高完全由指令流程决定用软件控制最直观可控硬件NSS自动翻转的时机有时候和指令边界对不上很容易出问题。第二以后想换一颗Flash或者自己扩展多片Flash软件片选只需要多配置一个GPIO不用改SPI硬件逻辑。第三排查问题方便CS引脚就是普通GPIO想怎么看、想怎么拉都行用逻辑分析仪抓波形也更清晰。CubeMX里这样操作SPI参数区里把Hardware NSS Signal设为DisableCS引脚单独配置为GPIO_Output。PB12作为CS我初始电平设为High因为Flash在CS为高时处于待机状态这是安全状态。3. CubeMX逐项配置从时钟树到生成工程3.1 新建工程与时钟树设置在CubeMX里选择MCU型号时直接搜索STM32F407VET6或STM32F407ZGT6看手头板子具体型号。选好后第一步是配置时钟我习惯先点RCC把HSE设为Crystal/Ceramic Resonator然后进入Clock Configuration界面HSE频率填开发板实际晶振值常见是8MHz或25MHzPLLM、PLLN、PLLP由CubeMX自动计算目标是HCLK达到168MHzAPB1 Prescaler选4得到42MHzAPB2 Prescaler选2得到84MHzCubeMX界面上如果HCLK超过168MHz会有红色提示调整到绿色168MHz即可。这一步看起来简单但很多人忽略了一个事APB1和APB2的定时器时钟是总线时钟的2倍如果后面要用定时器做精确延时或者PWM这里得顺手看一眼TIMx的时钟源是多少。3.2 SPI1参数区逐项解析在Pinout Configuration里搜索SPI1模式选Full-Duplex MasterCubeMX会自动把PA5、PA6、PA7分配为复用功能。然后进Parameter Settings推荐配置如下参数项推荐值说明ModeFull-Duplex Master全双工主机Hardware NSS SignalDisable使用软件片选Data Size8 BitsFlash指令和数据都是8bitFirst BitMSB FirstSPI协议标准Prescaler4SPI1时钟21MHzClock PolarityLowMode 0Clock Phase1 EdgeMode 0CRC CalculationDisabled标准NOR Flash通信不需要这几个参数看似简单但每一项都有讲究。Data Size选8Bits不用多说NOR Flash的指令集、地址、数据都是字节单位。Prescaler的倍数选择前面已经分析过。First Bit这里必须MSB FirstW25Q系列指令集定义就是高位先出如果改成LSB First所有指令和地址都会被反向发送Flash不可能识别。3.3 CS引脚的GPIO配置在Pinout视图中找到PB12点击后选择GPIO_Output然后进入GPIO SettingsGPIO output level设为High确保上电后Flash处于待机状态GPIO mode用Output Push PullMaximum output speed建议HighCS信号变化要足够快User Label自定义为FLASH_CS这里有一个习惯问题。CubeMX生成的代码里GPIO初始化在HAL_SPI_MspInit函数中这个函数会在HAL_SPI_Init时被自动调用。但很多人喜欢在自己写的初始化函数里再手动配置一遍引脚结果就是两次配置的顺序、参数不一致容易出现莫名其妙的异常。我现在的做法是所有引脚配置都通过CubeMX生成代码里不手动重复配置只在需要改变电平的时候调用HAL_GPIO_WritePin。3.4 生成工程与初步验证Project Manager里设置好工程名、编译工具链我用MDK-ARM然后点GENERATE CODE。生成后打开main.c能看到hspi1的初始化代码再往下有HAL_SPI_MspInit里面是PA5/PA6/PA7的复用配置和PB12的输出配置。这时候先别急着写Flash操作先做一件事用串口打印来确认SPI底层通了。具体方法是在main函数里调一个读ID的函数把读到的三个ID字节通过UART打印出来。如果ID读出来了说明SCK、MOSI、MISO、CS这四根线全部正常后面只要按指令集写驱动就行。如果ID读不出来先别往下写代码停下来排查硬件和配置问题否则后面所有调试都会是叠buff越叠越乱。4. NOR Flash核心操作实现读ID、写使能、擦除、页编程与校验4.1 读JEDEC IDSPI链路的第一块试金石读ID是调通SPI的第一步也是最有效的验证手段。指令是0x9F发送后Flash会返回3字节的JEDEC ID厂商ID、存储类型ID、容量ID。W25Q128返回的是0xEF、0x40、0x18W25Q64返回0xEF、0x40、0x17。如果读出来是这些值说明硬件连接、SPI模式、时序基本都正常。void norflash_read_id(uint8_t *mid, uint8_t *mem_type, uint8_t *capacity) { uint8_t tx_buf[4] {0x9F, 0x00, 0x00, 0x00}; uint8_t rx_buf[4] {0}; FLASH_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 4, HAL_MAX_DELAY); FLASH_CS_HIGH(); *mid rx_buf[1]; *mem_type rx_buf[2]; *capacity rx_buf[3]; }注意rx_buf[0]是发送0x9F这个字节时MISO返回的垃圾数据要丢弃。很多人第一次接触HAL_SPI_TransmitReceive时会觉得奇怪为什么接收缓冲的第一个字节不是ID因为SPI收发同步你发0x9F的同时Flash给你返回的是指令字节对应的输出数据对读ID来说这个数据是无效的从第二个字节开始才是真正的ID输出。4.2 写使能与状态寄存器轮询NOR Flash的写操作擦除、编程前必须发0x06写使能指令。写使能后状态寄存器1的WEL位bit1会被置1。擦除或编程结束后WEL位自动清零。static void norflash_write_enable(void) { uint8_t cmd 0x06; FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); FLASH_CS_HIGH(); } static uint8_t norflash_read_status(void) { uint8_t cmd 0x05; uint8_t status 0; FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, status, 1, HAL_MAX_DELAY); FLASH_CS_HIGH(); return status; } static void norflash_wait_busy(void) { while (norflash_read_status() 0x01) ; }读状态寄存器时有一个细节。调用HAL_SPI_Receive前CS已经拉低HAL库的Receive函数内部会发送0xFF来产生时钟。所以0x05指令发完后紧接着的Receive就是第一个时钟周期此时Flash返回的其实是状态寄存器内容不存在偏移问题可以直接使用返回值。如果发现写完写使能后状态寄存器的WEL位始终是0先检查WP引脚有没有接对。W25Q系列的WP引脚低电平有效接低的话写操作会被硬件禁止这是硬件层保护软件写使能根本不生效。4.3 扇区擦除操作指令与耗时问题擦除操作是NOR Flash写入的前置条件。NOR Flash的位只能从1写成0要从0恢复成1只能靠擦除。所以写入前如果目标区域不是全0xFF必须先擦除。我常用扇区擦除指令是0x20一次擦除4KB。完整代码void norflash_sector_erase(uint32_t addr) { uint8_t cmd[4]; cmd[0] 0x20; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; norflash_write_enable(); FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); FLASH_CS_HIGH(); norflash_wait_busy(); }注意地址是24bit必须按高、中、低三字节依次发送。擦除时间在W25Q128上典型值约45ms最坏情况400ms所以调用这个函数时要做好系统调度的考虑。如果用的是裸机阻塞等待没问题如果上了RTOS建议把等待做成任务延时轮询避免长时间占着CPU。4.4 页编程256字节边界与跨页处理写数据的指令是0x02一次最多编程256字节对应Flash的一页。最关键的限制是页编程不能跨越256字节边界。比如地址0x1FF0处要写64字节这64字节会从0x1FF0写到0x202F跨越了两页如果直接把64字节发过去Flash会从0x1FF0写写到0x1FFF后回卷到当前页首地址0x1F00继续写等于写错了位置数据面目全非。跨页处理的标准做法是写一个通用的写入函数void norflash_write_data(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t page_size 256; uint32_t page_remain; while (len 0) { page_remain page_size - (addr (page_size - 1)); uint32_t write_len (len page_remain) ? len : page_remain; norflash_write_enable(); FLASH_CS_LOW(); uint8_t cmd[4]; cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi1, data, write_len, HAL_MAX_DELAY); FLASH_CS_HIGH(); norflash_wait_busy(); addr write_len; data write_len; len - write_len; } }每次计算当前地址所在页还剩多少空间取可写长度写完再更新地址和数据指针。这个函数能处理任意地址、任意长度的写入是驱动层的核心函数。4.5 数据读取与完整性校验读数据指令是0x0324位地址可以连续读取任意长度直到CS拉高才结束。void norflash_read_data(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; FLASH_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); FLASH_CS_HIGH(); }读取完数据后建议做一次校验。最简单的方式是写固定pattern比如0x55、0xAA、0xA5、0x5A交替读回来对比复杂一点可以算CRC32。调试期间的校验是为了发现硬件或时序问题等驱动稳定后可以去掉pattern校验但关键配置数据的CRC校验还是要保留毕竟Flash本身也有极小概率出现位翻转。5. 用DMA提升传输效率F407的DMA请求映射与配置5.1 为什么需要DMA轮询方式读写FlashCPU每传一个字节都要等SPI数据寄存器空或满大量时间耗在了等待上。读取几十上百KB数据时CPU几乎全程被占满这在低功耗或者多任务场景下不可接受。DMA的好处是让SPI外设直接和内存交换数据CPU只需要在传输完成时收到一个中断回调。F407的DMA功能比F1强很多每个DMA控制器有8个Stream不同外设的请求可以映射到不同的Stream/Channel上配置灵活但容易搞混。5.2 CubeMX里配置DMA在SPI1的DMA Settings选项卡里添加两个DMA请求SPI1_RX选择DMA2 Stream0Channel3方向Peripheral To MemorySPI1_TX选择DMA2 Stream3Channel3方向Memory To Peripheral这是F407参考手册给出的SPI1 DMA请求映射配置错了DMA不会工作。模式选Normal数据宽度Peripheral和Memory都选ByteMemory Increment勾上Peripheral Increment不勾。这里说一下为什么数据宽度必须选Byte。SPI1数据寄存器是8bit模式如果DMA数据宽度配成Half Word或Word会一次搬多个字节导致数据错乱。CubeMX默认继承外设配置但如果手动改动过务必检查。5.3 DMA传输的完整调用流程DMA方式读取Flash时需要注意SPI全双工特性决定了发送缓冲区和接收缓冲区长度必须一致且发送缓冲区里要主动填充0xFF来产生时钟。#define READ_BUF_SIZE 4096 static uint8_t spi_tx_buf[READ_BUF_SIZE 4]; static uint8_t spi_rx_buf[READ_BUF_SIZE 4]; static volatile uint8_t spi_dma_done 0; void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { spi_dma_done 1; } } void norflash_read_data_dma(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t total len 4; spi_tx_buf[0] 0x03; spi_tx_buf[1] (addr 16) 0xFF; spi_tx_buf[2] (addr 8) 0xFF; spi_tx_buf[3] addr 0xFF; memset(spi_tx_buf[4], 0xFF, len); spi_dma_done 0; FLASH_CS_LOW(); HAL_SPI_TransmitReceive_DMA(hspi1, spi_tx_buf, spi_rx_buf, total); while (!spi_dma_done); FLASH_CS_HIGH(); memcpy(buf, spi_rx_buf[4], len); }这里有几个关键点必须强调。第一spi_tx_buf和spi_rx_buf一定要定义成全局变量或者static局部变量不能用栈上的局部数组。DMA传输是异步的函数还没返回时DMA还在搬运数据如果缓冲区在栈上函数退出后栈空间可能被其他代码覆盖DMA搬运的数据就全乱了。这是DMA入门最容易踩的坑没有之一。第二DMA方式的CS拉高时机必须在传输完成回调之后再操作不能像轮询那样HAL_SPI_TransmitReceive返回后直接拉高。回调里置的spi_dma_done标志就是干这个用的。第三DMA的CNDTR寄存器是16bit最多一次传输65535字节超过64KB必须分段。如果后面要做整片Flash读写得自己封装分段的逻辑。5.4 DMA与轮询的实测对比我在这套工程上实测过读取64KB数据的两种方式。轮询方式HAL_SPI_TransmitReceive全程阻塞传输约2分钟不对让我重新算一下。64KB在21MHz下理论传输时间约25msHAL轮询每字节都要检查标志位实际大约30ms左右CPU占用接近100%。DMA方式传输时间约25ms但CPU在这期间几乎完全空闲。差距不在传输速度上因为瓶颈是SPI时钟本身差距在CPU占用率。轮询方式把MCU的全部精力都耗在等待SPI上DMA方式让出CPU去处理其他任务这个优势在大数据量读写时尤其明显。所以我的建议是小数据量的读写比如几十字节的配置参数用轮询就够简单可靠大数据量传输比如OTA升级包写入、日志批量读取用DMA。6. 排查手册SPI通信不生效与数据错乱的实战修复6.1 读回全FF或全00的排查链路这是SPI调试里最经典的现象。读ID读出来全是0xFF说明Flash完全没有回应MISO线始终被上拉到高电平。按下面链路排查先查CS。确认CS引脚确实被拉低了。软件片选模式下CS是普通GPIO如果CubeMX里误配成复用功能代码里调用HAL_GPIO_WritePin是不生效的。拿万用表量PB12引脚电平或者用逻辑分析仪抓CS波形一目了然。再查MOSI和MISO有没有接反。MOSI接Flash的DIMISO接Flash的DO。这两个接反的后果就是主机发了数据Flash收不到Flash吐的数据主机也接不住ID自然全是0xFF。查Flash供电。W25Q128的VCC要3.3V同时WP和HOLD不能悬空。如果WP悬空芯片可能处于硬件写保护状态虽然读ID不受影响但后续擦写会失败。查SCK是否有波形。用示波器看PA5有没有脉冲没有脉冲就检查CubeMX的引脚复用配置PA5是否被其他外设抢占。如果读回全00多半是MISO被外部拉低或者短路常见原因是GPIO上下拉配置冲突、Flash焊接连锡。6.2 CS波形异常导致的操作失败CS是SPI设备最容易被忽视的线。Flash的所有指令都有严格的帧边界CS必须在整个操作期间保持低操作结束才能拉高。用硬件NSS就很容易出问题。硬件NSS在某些模式配置下会在每个字节传输后自动拉高再拉低Flash可能把一次4字节的读ID指令拆成4次独立操作读出来的ID完全不对。所以我前面反复强调用软件片选就是为了完全掌控CS时序。还有一个细节是DMA方式下CS的拉高时机。不少人在调用HAL_SPI_TransmitReceive_DMA之后就立刻拉高CS看起来逻辑没错但DMA传输是异步的函数返回时传输可能才刚开始。CS过早拉高会中断Flash的数据输出导致后面接收到的数据全是0xFF。正确做法是在TxRxCpltCallback回调里拉高CS。6.3 时钟极性与数据错位问题如果读ID能读到但值不对比如0xEF变成了0x77或者ID整体右移一位通常是时钟极性和相位不匹配。SPI双方约定SCK空闲电平和数据采样沿必须一致否则数据采样早一拍或晚一拍读出来的bit串整体偏移。排查方法是看波形。用示波器钳住SCK先看空闲电平是低还是高这决定CPOL0还是1再看数据线在SCK上升沿还是下降沿变化这决定CPHA。通常读ID读出来是一个完全不合理的值直接改CubeMX里的Clock Polarity和Clock Phase从Mode 0切到Mode 3试试多数情况下能解决。还有一种少见情况Flash支持Mode 0和Mode 3但不同批次对Mode 3的兼容性有差异。碰到大批量生产时最好固定用Mode 0兼容性最稳。7. 在FreeRTOS中使用SPI的注意点与后续扩展方向7.1 SPI中断优先级与资源互斥FreeRTOS工程里使用SPI的DMA方式中断优先级的设置有讲究。F407上FreeRTOS默认把PendSV和SysTick设置为最低优先级为了保证调度不被打断外设中断优先级不能高于某个阈值具体数值取决于configMAX_SYSCALL_INTERRUPT_PRIORITY。我的经验是把SPI中断优先级设为5也就是比PendSV优先级高一点同时保证能用FromISR结尾的FreeRTOS API。如果用的是裸机优先级怎么设都行一旦跑RTOS优先级设置不当可能导致两个问题一是中断里调用了非ISR安全的API导致系统卡死二是高优先级中断长时间占用CPU导致任务调度失效。另外多个任务不能同时调用同一个SPI句柄。HAL库的SPI传输函数不是线程安全的两个任务同时往Flash写数据总线数据会交错。工程上要在所有访问Flash的操作外面加一个互斥信号量确保同一时间只有一个任务在使用SPI总线。实践节奏是先拿信号量再执行读或写最后释放信号量。7.2 LittleFS文件系统与KV存储扩展驱动层稳定之后可以往上层走。NOR Flash适合挂LittleFS这是专门为嵌入式设计的小型文件系统掉电保护、磨损均衡、目录结构都有代码量不大和SPI NOR Flash非常搭。如果只是存配置参数不需要文件系统可以写一个简单的KV存储每个条目由Key、Length、Data、CRC组成按固定格式顺序写到Flash里更新时在末尾追加新记录启动时从后往前扫描。配合双备份机制能有效缓解掉电丢数据的问题。从我个人的项目经验来看先把底层驱动封装成独立模块对外暴露读、写、擦除、读写状态这几个函数后面无论接文件系统还是做KV存储都只是在驱动层上面套一层逻辑不会动到底层SPI代码。这套架构让我后续换Flash型号时省了很多事。最后再说一个小技巧调试SPI时把CS引脚的切换、每次SPI传输的字节数、Flash操作类型这些信息通过串口打出来尤其是第一个字节对定位问题帮助极大。等整个驱动稳定后再把这些调试输出删掉只保留关键错误打印。调试信息宁可早加不要事后补这是我做了这么多个嵌入式项目最深的一点体会。
网站建设高端定制企业官网