ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 SPI通信详解:从协议原理到W25Q128 Flash驱动开发

STM32 SPI通信详解:从协议原理到W25Q128 Flash驱动开发 我早期学习STM32时最先搞定的通信接口是UART毕竟收发打印太直观了。但一遇到SPI整个人就有点懵明明只有四根线怎么比串口还难懂当时拿着W25Q128的Flash模块对着数据手册看时序图看了一晚上都没敢动手写代码。后来真正跑通一次读写把ID读出来那一刻才恍然大悟——SPI其实比UART更简单只是它把“规则”放在了你面前很多人被时序图吓住了而已。这篇文章就围绕“SPI到底是怎么传输数据的”这个问题从协议基础讲到STM32的SPI外设再以W25Q128这个最经典的Flash芯片为例把整个驱动开发的流程走一遍。无论你是刚点完LED灯的新手还是想系统补一补通信基础的同学这篇文章都适合你。看完之后你不仅能看懂时序图还能自己用CubeMX配置SPI、手写一套Flash读写驱动从此告别“抄代码但不明白为什么”的状态。1. 内容整体设计与思路拆解1.1 为什么选SPI作为入门通信协议先聊一个更基础的问题单片机之间、单片机和外设之间通信为什么有那么多协议偏偏SPI值得专门学答案其实很朴素SPI是“规则最透明”的同步串行协议。它不像UART那样需要双方约定波特率也不像I2C那样有地址仲裁和应答机制SPI的核心逻辑就是一个主设备主动产生时钟数据跟着时钟一位一位地走。谁产生时钟谁就掌控节奏从设备完全被动。这种设计让SPI的时序极容易理解也非常适合第一次接触“时序”概念的人。另一个重要原因是SPI在嵌入式系统里的地位。W25Q128这种SPI Flash几乎出现在所有需要存储少量数据的设备上——固件升级、日志记录、字库存储、参数保存——它靠的是“便宜、够用、驱动简单”。掌握SPI协议之后你还能顺手看懂SD卡SDIO模式本质上兼容SPI、触摸屏控制器、各类传感器、以太网控制器等大量外设的驱动逻辑。1.2 我理解的SPI驱动开发思路我做SPI驱动时一般分四步走这套流程也推荐给你第一步吃透外设侧的通信规则。也就是看W25Q128数据手册搞清楚它支持哪些命令、每个命令的帧格式是什么样、每个位什么时候有效。这个阶段花的时间最多但也是最值得的。第二步搞清主控侧的硬件能力。也就是STM32的SPI外设支持哪些模式、时钟最高能到多少、DMA怎么配。很多时候功能实现不了不是代码问题而是主控特性没摸清。第三步用CubeMX把底层引脚、时钟、外设参数配好生成工程骨架。这一步能省掉90%的寄存器配置工作量但你仍然需要知道每个配置项背后的含义否则出问题时无从下手。第四步按“最小可用”原则写驱动。先把读ID这种最简单的命令跑通再逐步实现读写、擦除、状态检测。一次只做一件事验证完再往前走。这套方法不只适用于SPI任何通信协议的驱动开发都可以复用。因为通信的本质是“约定”只要把约定拆解清楚代码只是把约定翻译成机器能执行的步骤而已。2. SPI协议核心时序与工作原理2.1 四根线各自的职责SPI全称Serial Peripheral Interface串行外设接口由Motorola现在的NXP在1979年提出。它用四根线完成主从设备之间的通信信号线全称职责SCLKSerial Clock时钟线由主机产生决定数据传输的节奏MOSIMaster Out Slave In主机输出、从机输入数据从主机流向从机MISOMaster In Slave Out主机输入、从机输出数据从从机流向主机CS/SSChip Select / Slave Select片选线低电平有效主机选中哪个从机就拉低哪个你可以把这四根线理解为一条双向独木桥配一个总指挥SCLK就是总指挥手里的节拍器MOSI和MISO是两条分开的单行道CS则是点名用的喇叭——喊到谁谁就开工。因为是全双工主机发数据的同时从机也能发数据回来所以通信效率非常高。这里有个初学者容易忽略的点SPI没有“地址”概念。主机想跟哪个设备通信就直接拉低那个设备的CS线其他设备看到自己的CS是高点平就乖乖把MISO置为高阻态不参与通信。所以一根总线上挂多个SPI设备是完全可行的只要每根CS分给不同引脚即可。2.2 时钟极性CPOL和时钟相位CPHASPI最难懂也最容易出问题的就是CPOL和CPHA这两个参数。数据手册上关于SPI模式的表格通常长这样SPI模式CPOLCPHA采样沿Mode 000上升沿Mode 101下降沿Mode 210下降沿Mode 311上升沿很多人背下了这张表但不知道为什么。我换个说法解释一下。CPOL决定的是空闲时时钟线的电平。CPOL0表示空闲时SCLK为低电平CPOL1表示空闲时SCLK为高电平。就这么简单它只描述“没事的时候时钟线处于什么状态”。CPHA决定的是数据在哪个边沿被采样。CPHA0表示在第一个边沿采样CPHA1表示在第二个边沿采样。结合CPOL就能确定是上升沿还是下降沿。再打一个比方你和一个朋友约定每周六早上8点见面CPOL就是“周六”还是“周日”CPHA就是“早上”还是“晚上”。两边必须同时满足这两个条件才能在正确的时间碰头。主机和从机如果CPOL或CPHA任何一个不匹配读出来的数据就会是乱的而且这种错乱往往只在高速通信时才容易被发现——有些低速场景下甚至能“歪打正着”跑通。W25Q128支持SPI Mode 0和Mode 3也就是CPOL0/CPHA0或者CPOL1/CPHA1。绝大多数STM32例程都用Mode 0即空闲低电平、上升沿采样这也是最常用的模式。2.3 一次完整的数据传输过程以Mode 0为例把一次8位数据传输从头到尾过一遍主机把CS引脚拉低告诉从机“准备接收”。主机在每个SCLK时钟周期内把要发送的数据从高位到低位依次放到MOSI线上。在SCLK的上升沿主机和从机同时采样对方发来的数据位。连续8个时钟后一个字节传输完成主机把CS拉高一次事务结束。注意一个关键点SPI的数据收发是同步进行的。主机向从机写一个字节的同时从机也在向主机发送一个字节。所以从机的MISO线上到底返回了什么数据取决于从机当前的数据缓冲区和运行状态。这也是为什么很多SPI命令格式是“主机先发送命令字节再从MISO读取响应”——发送和接收本来就是同一个时钟周期里同时完成的。回到W25Q128的例子主机发送0x9F读JEDEC ID命令后理论上还需要继续发送3个空字节0x00同时从MISO上读回3个字节分别对应厂商ID、容量ID等。如果你只发一个命令就不发时钟了从机根本没有机会把数据推出来。这一点新手最容易忽略代码里表现为“读出来的全是0xFF”。3. STM32 SPI外设与CubeMX配置要点3.1 STM32 SPI外设的核心能力STM32的SPI外设是从硬件层面实现了SPI协议的完整逻辑你在代码里只需要做三件事配置参数、把数据塞进发送寄存器、从接收寄存器取数据。中间的时钟产生、移位输出、接收采样全部由硬件完成。以STM32F103系列为例SPI外设支持主模式和从模式最高18MHz的时钟速率取决于系统时钟分频8位或16位数据帧格式硬件NSS管理或软件NSS管理硬件CRC校验发送和接收DMA请求多主机通信的冲突检测对于绝大多数SPI外设应用我们只用其中一小部分功能主机模式、8位数据、软件管理NSS。但了解外设的上限是有必要的比如当你需要把数据以DMA方式搬到内存、不占用CPU时知道“SPI有DMA请求”这件事会让你少走很多弯路。3.2 CubeMX配置的关键选项用CubeMX配置SPI的步骤非常简单但每个选项都不能乱选在Pinout视图中找到SPI1将Mode设为Full-Duplex Master硬件NSS设为Disable用软件管理片选。在Configuration的SPI1参数区按下表设置参数推荐值说明ModeFull-Duplex Master全双工主机模式Data Size8 BitsW25Q128命令和数据都是字节单位Prescaler16或32分频系数决定SCLK频率CPOLLow空闲低电平CPHA1 Edge第一个边沿采样NSSSoftware软件管理片选灵活度最高关于Prescaler的选择有个实用公式SCLK频率 PCLK频率 / Prescaler。F103的APB2总线最高是72MHz如果Prescaler设为16SCLK就是4.5MHz。W25Q128支持最高133MHz的时钟但STM32F103的SPI最高只能到18MHz而且PCB走线、杜邦线质量都会影响高速通信所以我建议把时钟设置在1MHz到10MHz之间兼顾速度和稳定性。这里多说一句如果你用的是杜邦线连接模块SCLK超过10MHz大概率会出现数据错乱。不要一上来就追求最高速率稳定跑通功能后再慢慢提速。3.3 HAL库发送与接收的几个重要细节CubeMX生成的HAL库代码很简洁但有几个细节需要你特别注意HAL_SPI_Transmit(hspi1, pData, Size, Timeout); HAL_SPI_Receive(hspi1, pData, Size, Timeout); HAL_SPI_TransmitReceive(hspi1, pTxData, pRxData, Size, Timeout);三个函数分别对应只发、只收、同时收发。对于W25Q128的很多命令用TransmitReceive最合适因为你需要“一边发命令字节一边读响应字节”。最典型的坑是HAL_SPI_Receive和HAL_SPI_Transmit之间切换时的状态残留。比如你先调用Transmit发送读ID命令再调用Receive读数据中间SCLK可能没有连续产生导致从机状态错乱。我自己的习惯是所有涉及“先发命令再读数据”的场景统一用TransmitReceive把要发送的命令字节填入发送缓冲区接收缓冲区留空等待数据填充。一次性完成整个交换过程从机的时序压力最小代码也最清晰。4. W25Q128 Flash驱动开发全过程4.1 W25Q128芯片的核心特性W25Q128是Winbond华邦生产的一款SPI NOR Flash芯片容量128Mbit也就是16M字节。如果你想往单片机系统里加一个“断电不丢数据”的存储空间它就是最常见的选择。它有几个关键数字值得记住容量128Mbit 16MB页大小256字节扇区大小4KB16个页擦除粒度扇区擦除4KB、块擦除32KB/64KB、整片擦除最大时钟频率标准SPI模式133MHzFlash芯片的“写”和“读”不对称读取可以按任意字节地址进行但写入必须先擦除后编程而且擦除的最小单位是4KB的扇区编程的最小单位是256字节的页。这种“先擦后写、按扇区擦除”的特性是所有Flash共通的理解了这个你就理解了为什么Flash驱动的逻辑比EEPROM复杂——EEPROM可以按字节改写Flash不行。4.2 硬件连接与最小系统搭建我常用的连接方式是SPI1 软件片选引脚分配如下W25Q128引脚STM32F103引脚说明CSPA4GPIO输出低电平有效SCLKPA5SPI1_SCKMOSIPA7SPI1_MOSIMISOPA6SPI1_MISOVCC3.3V注意是3.3V不是5VGNDGND共地如果你买的是现成模块通常还带WP写保护和HOLD引脚模块上一般已经用电阻拉高或拉低处理好了不需要额外配置。但如果你自己画板子这两个引脚一定要处理WP拉高禁用写保护、HOLD拉高允许正常工作悬空会导致莫名其妙的读写失败。CS用普通GPIO控制而不是硬件NSS原因是软件片选在代码上更直观控制自由度也更高。你可以随时创建新的CS引脚去挂第二片Flash或别的SPI设备不用改动SPI外设的配置。4.3 读取Flash ID第一个SPI驱动读JEDEC ID是验证SPI通信是否正常的最简单方法也是我建议所有初学者跑通的第一个函数。命令格式向Flash发送0x9F然后继续发送3个空字节同时在MISO上读回3个字节。第一个字节是厂商IDW25Q128为0xEF第二个字节是类型ID0x40第三个字节是容量ID0x18。代码实现如下void W25QXX_ReadID(void) { uint8_t cmd 0x9F; uint8_t rxData[3] {0}; W25QXX_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, cmd, rxData[0], 1, 100); HAL_SPI_TransmitReceive(hspi1, txDummy, rxData[0], 1, 100); HAL_SPI_TransmitReceive(hspi1, txDummy, rxData[1], 1, 100); HAL_SPI_TransmitReceive(hspi1, txDummy, rxData[2], 1, 100); W25QXX_CS_HIGH(); printf(Manufacturer ID: 0x%02X\n, rxData[0]); printf(Memory Type: 0x%02X\n, rxData[1]); printf(Capacity: 0x%02X\n, rxData[2]); }如果串口打印出EF 40 18说明SPI通信链路完全打通引脚配置正确、时序模式匹配、芯片上电正常。如果读出全是FF先检查片选有没有拉低、电源有没有到位如果读出的是乱码优先怀疑CPOL/CPHA配置和从机不匹配。这里不需要关心Flash内部状态因为JEDEC ID命令在任何状态下都响应它是天然的“通信测试工具”。4.4 写使能、状态寄存器与页编程Flash写入的第一个前置操作是写使能Write Enable。W25Q128规定任何写操作页编程、擦除、写状态寄存器之前必须先发送0x06命令将芯片内部的状态寄存器中的WEL位Write Enable Latch置1。这个锁存位在每次成功的写操作后会自动清零所以每写一次都要重新发0x06。这里有个很常见的坑发送0x06后必须等到WEL位真正变成1再继续写操作。芯片接收0x06命令是“异步”的——命令发完后芯片需要一点时间完成内部状态修改。如果你紧跟着就发页编程命令芯片可能还在处理上一个命令导致本次写操作被忽略。检查WEL位的方法是读取状态寄存器1命令0x05uint8_t W25QXX_ReadStatusReg1(void) { uint8_t cmd 0x05; uint8_t status 0; uint8_t dummy 0xFF; W25QXX_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, cmd, dummy, 1, 100); HAL_SPI_TransmitReceive(hspi1, dummy, status, 1, 100); W25QXX_CS_HIGH(); return status; }状态寄存器1的bit0是BUSY位bit1是WEL位。你要等BUSY位清零、WEL位置1后才说明Flash已经准备好接收编程命令。页编程命令是0x02帧格式为命令字节 3字节目标地址高字节在前 最多256字节数据。关键限制是一页编程写入的数据不能跨256字节的页边界。如果地址是0x00FF你一次写入2个字节第二个字节会回卷到页首地址0x0000而不是写到0x0100。这个限制是Flash驱动里最容易被忽视的“坑王”。解决方法是在写入前先计算当前页还剩多少空间如果本次要写的数据超过剩余空间就拆成多次页编程。页编程代码void W25QXX_WritePage(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; W25QXX_WriteEnable(); cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; W25QXX_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, buf, len, 100); W25QXX_CS_HIGH(); W25QXX_WaitBusy(); }W25QXX_WaitBusy是一个轮询函数不断读状态寄存器1直到BUSY位清零。因为Flash写操作需要时间芯片在写的时候不会响应其他命令所以等待是必须的。4.5 扇区擦除与数据读取擦除操作是Flash驱动里最耗时的一环。W25Q128的4KB扇区擦除大约需要45到400毫秒取决于芯片的具体工况。擦除命令是0x20帧格式为命令字节 3字节扇区地址。void W25QXX_EraseSector(uint32_t sectorAddr) { uint8_t cmd[4]; W25QXX_WriteEnable(); cmd[0] 0x20; cmd[1] (sectorAddr 16) 0xFF; cmd[2] (sectorAddr 8) 0xFF; cmd[3] sectorAddr 0xFF; W25QXX_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); W25QXX_CS_HIGH(); W25QXX_WaitBusy(); }这里要提醒一点擦除地址必须是4KB对齐的也就是说地址的低12位必须是0。如果你传入的地址是0x00000100Flash不会报错但实际擦除的是0x00000000起始的整个扇区。你的数据可能被“静默”擦掉。读取则简单得多。读数据命令0x03帧格式为命令字节 3字节地址后面跟着的数据字节会被Flash按地址递增地返回出来。理论上一次读取的长度没有限制因为读操作不会触发页回卷地址会自动跨页连续递增。void W25QXX_ReadData(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; W25QXX_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 100); W25QXX_CS_HIGH(); }4.6 把整个Flash驱动串起来单独的页编程和扇区擦除有了但实际使用中你要写的可能是任意长度的数据。这时候就要写一个“任意地址任意长度写入”的接口逻辑是检查写入地址是否与扇区对齐不对齐就先读出来备份。擦除目标扇区。把数据按256字节一页、且不跨页边界的原则拆分成多次页编程依次写入。这个函数就是典型 Flash 驱动的“主逻辑”它同时考验你对页边界、扇区擦除和写使能三个关键点的理解。我实践中常用的简化流程是void W25QXX_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t pageRemain; uint32_t writeLen; while (len 0) { pageRemain 256 - (addr % 256); writeLen (len pageRemain) ? len : pageRemain; W25QXX_EraseSector(addr 0xFFFFF000); // 简化处理实际应该检查是否需要擦除 W25QXX_WritePage(addr, buf, writeLen); addr writeLen; buf writeLen; len - writeLen; } }实际项目里不会每次写入都擦除因为擦除很浪费时间。通常做法是先读目标扇区数据与要写入的数据比较逐字节判断哪些位需要从1变为0只有存在“1变0”的需求时才执行擦除如果没有直接按位与逻辑合成新数据后再编程。这样能大幅减少擦除次数延长Flash寿命。W25Q128号称支持10万次擦写听起来很多但如果你每次存日志都整扇区擦除一个扇区用一年可能就顶不住了。所以“擦写均衡”不是服务器SSD专属概念单片机上一样有现实需求。5. SPI调试常见问题与排查实录5.1 最典型的六个故障现象SPI调试遇到的问题归纳起来不外乎以下几类。我按照从“硬件物理层”到“协议逻辑层”的排查顺序整理了一个速查表故障现象可能原因排查方法读出全是0xFFCS没拉低、电源没上、MISO断线万用表量CS电压确认模块供电检查杜邦线读出全是0x00MISO线短路到地、从机没正常工作断开从机MISO测量其对地电阻数据错乱、无规律CPOL/CPHA模式不匹配、时钟太快统一用Mode 0降低Prescaler第一个字节对后续全错CS释放太早或太晚、时序不完整检查CS在整个事务期间是否保持低电平写入后读回仍是0xFF忘记写使能、WP引脚拉低保护发送0x06后检查状态寄存器bit1偶发读写失败电源纹波大、线过长、接触不良加104去耦电容缩短杜邦线改用焊接5.2 关于片选信号的三个经验CS是最容易被忽略的环节但它出问题的概率极高。我踩过的坑集中在这三处一是CS要保持整个事务期间全程拉低。有些人会在每发送一个字节后就拉高CS再拉低以为这样每个字节都是独立事务。但对于W25Q128这种设备很多命令是多字节帧中间CS跳变会被当成“事务终结”后续字节全部无效。二是CS拉低后、SCLK开始前最好加一小段延时。特别是从机端有电平转换或滤波器时CS有效到第一个时钟边沿之间需要建立时间。虽然STM32的GPIO和SPI时钟配合通常没问题但在低速验证时加个微秒级延时能排除一类怪问题。三是同一总线上挂多个设备时CS之间绝对不能重叠。要让前一个设备的CS先拉高再拉低后一个设备的CS。如果两个CS同时低两个从机都会抢占MISO线导致总线冲突和数据损坏。5.3 调试SPI的实用技巧SPI调试最痛苦的地方在于“看不见数据流”。UART可以打印I2C有地址可寻SPI却是一堆高速翻转的方波。我的调试工具箱里按优先级排序是逻辑分析仪 示波器 串口打印。逻辑分析仪是最适合SPI调试的工具100元左右的24通道8通道逻辑分析仪就能完美解码SPI时序直接显示每一根线上的电平和数据内容。用它能直观看到CS是否整个事务都拉低了、SCLK有没有多余毛刺、MISO上的数据位和采样沿是否对齐。如果没有逻辑分析仪串口打印HAL_SPI_TransmitReceive前后接收缓冲区的内容也有排查价值。重点关注接收缓冲区里第一个字节是否等于预期命令、后续字节是否按规律变化。通过分段对比能判断是“字节没发出去”还是“数据被从机忽略了”。5.4 CubMX生成的代码为什么有时会卡死在超时HAL库的HAL_SPI_TransmitReceive带超时参数超时后会返回HAL_TIMEOUT。如果你发现代码卡死在SPI函数里多半是以下三个原因之一最常见的是SCLK没有正常产生。检查SPI外设是否真的处于使能状态——CubMX生成的HAL_SPI_MspInit里如果GPIO时钟没开外设根本无法工作函数会一直等待发送完成标志。第二种情况是从机拉低MISO的时序异常。某些SPI设备在CS有效后会先驱动MISO线如果这个驱动时序和SCLK边沿太接近主机的接收寄存器可能采到不稳定电平持续产生溢出错误导致HAL库一直清理溢出标志。第三种情况是硬件NSS被误启用。如果你在CubeMX里没有把NSS设为Software硬件在收到模式错误时会把SPI状态机卡住。这种问题最隐蔽排查方法是在CubMX中确认NSS设置为Disable或Software。6. 从W25Q128驱动到通用SPI设备驱动的方法论6.1 命令集驱动的通用套路写完W25Q128的驱动后你会发现大多数SPI芯片的驱动套路都是一样的拉低CS、发命令字节、发地址/数据、根据需要接收数据、拉高CS、等待芯片内部操作完成。这个套路可以抽象成一组基础函数// 写寄存器或发送命令 void spi_dev_write_reg(uint8_t reg, uint8_t val) { CS_LOW(); spi_transmit(reg, 1); spi_transmit(val, 1); CS_HIGH(); } // 读寄存器 uint8_t spi_dev_read_reg(uint8_t reg) { uint8_t val 0xFF; CS_LOW(); spi_transmit(reg, 1); spi_receive(val, 1); CS_HIGH(); return val; }下次你换一个SPI接口的传感器或SD卡只需要查数据手册里的寄存器地址表和命令序号套用这个模板就能快速写出一份能用的驱动。这就是“驱动开发方法论”的价值——不是背芯片手册而是建立一套应对未知硬件的流程。6.2 从轮询到中断再到DMA我上边给出的代码全部是阻塞式轮询因为这样最直观适合入门理解。但实际项目中SPI通信经常伴随着大数据量传输轮询会占用大量CPU时间这时候就需要引入中断或DMA。中断方式的核心思路是发起传输后不等待在SPI的TXE/RXNE中断里逐个字节搬运数据DMA方式的核心思路是把RAM里的数据缓冲区的地址配给SPI的DMA通道硬件自己完成“内存到外设寄存器”的搬运。使用DMA之后CPU可以在SPI传输期间去做别的事比如驱动另一个传感器、处理控制逻辑。代价是代码复杂度增加不少需要处理DMA传输完成中断、环形缓冲、半传输中断等。我给你的建议是先跑通轮询版本再用CubMX勾选DMA选项对比一下代码差异很容易理解DMA如何工作。6.3 为什么你需要学会自己看数据手册现在的网络资源极其丰富搜索框里输入“STM32 SPI W25Q128”就能找到成千上万篇教程。但教程再详细它也替代不了数据手册原因很简单别人可能写错了或者理解了但在新芯片上不适用。举一个实际例子我在一个项目中用的Flash芯片不是W25Q128而是另一家厂商的兼容料。代码完全按照W25Q128的驱动写结果读ID一切正常但页编程怎么也写不进去。后来查手册才发现兼容料在页编程时要求CS在发送完所有数据后必须额外保持低电平超过100微秒才允许释放。这个细节在原厂的手册里有标注在教程里完全找不到。所以我把阅读芯片数据手册看作是驱动开发的必修课。一开始不用全看重点看这几块引脚功能描述、命令表、状态寄存器说明、时序特性表。一页一页啃会痛苦但积累几款芯片的阅读经验后你的“硬件直觉”会有质的飞跃。7. 关于这套驱动代码的几个实操建议最后分享几个我在实际项目里用出来的经验帮助你少走弯路。关于硬件部分优先使用开发板自带的SPI Flash芯片而不是外接杜邦线模块。开发板上的走线短、电源干净可以最大程度排除硬件干扰让你专心学协议。如果必须用杜邦线连接外部模块请把通信速率降低到1MHz以下并且加上几个104去耦电容。关于代码结构不要在main函数里堆业务逻辑。把Flash驱动封装成独立模块提供Init、ReadID、Read、Write、Erase几个接口就够了。后续想升级成DMA版本只改模块内部实现不影响上层调用代码。关于验证方式我强烈建议在写完驱动后做一个“倒序验证”先向指定地址写入一串递增数据再读取出来逐一比对。如果数据全对说明驱动逻辑没有问题如果中间有几个字节不对大概率是页边界处理有bug。还有一个调试心态的建议SPI出问题时不要急着改代码。先拿逻辑分析仪抓一遍波形确认每一根线的电平状态是否符合预期。很多时候问题根本不在软件逻辑而是引脚配置冲突、GPIO复用模式错误这样的低级问题。看波形能从根上定位问题比你盲猜改参数要高效得多。
返回列表