023、SPI实战:驱动OLED显示屏、SD卡(SPI模式)、Flash存储器(W25Qxx)

发布时间:2026/6/19 3:27:11
023、SPI实战:驱动OLED显示屏、SD卡(SPI模式)、Flash存储器(W25Qxx) SPI实战驱动OLED显示屏、SD卡SPI模式、Flash存储器W25Qxx从一次诡异的OLED花屏说起去年做一款手持仪表OLED屏在低温环境下偶尔出现半边花屏。示波器抓波形发现SCLK时钟线上有毛刺但逻辑分析仪显示数据帧格式完全正确。折腾三天后发现是SPI时钟极性配置问题——OLED要求CPOL0、CPHA0而SD卡初始化时把SPI模式改成了模式3切换回OLED时没重新配置寄存器。这个坑让我意识到SPI设备共线时模式切换必须显式重置不能依赖驱动库的“自动恢复”。一、SPI总线上的三个“性格迥异”的器件OLEDSSD1306、SD卡、W25Qxx Flash这三个器件在SPI总线上各有脾气OLED纯输出设备只写不读时钟频率通常1-10MHz数据量小但时序敏感SD卡双向通信初始化阶段必须使用低速模式400kHz以下之后可切换到25MHzW25Qxx全双工设备读操作需要先发指令再收数据写操作前必须擦除扇区共线设计时片选信号必须独立控制且每个设备切换后要重新配置SPI模式。我习惯在每次片选拉低前先调用一个spi_config_for_device()函数而不是依赖全局配置。二、OLED驱动那些年我们踩过的初始化坑SSD1306的初始化序列网上抄来抄去但真正能用的版本需要关注三个细节// OLED初始化函数 - 实测可用版本voidOLED_Init(void){// 这里踩过坑复位时序必须大于100usOLED_RST_LOW();delay_us(200);// 别用delay_ms复位时间太长会导致初始化失败OLED_RST_HIGH();delay_us(200);// 关闭显示后再配置否则部分命令不生效OLED_WriteCmd(0xAE);// 关闭显示// 设置对比度 - 不同批次OLED亮度差异很大OLED_WriteCmd(0x81);OLED_WriteCmd(0x7F);// 默认值实际调试时根据环境调整// 这里有个坑段重映射和COM扫描方向必须匹配OLED_WriteCmd(0xA1);// 段重映射列地址127映射到SEG0OLED_WriteCmd(0xC8);// COM扫描方向从COM[N-1]到COM0// 开启显示OLED_WriteCmd(0xAF);}写数据时注意SSD1306支持页地址模式和水平地址模式。我推荐用页地址模式因为每次写一页128字节后自动换行适合显示字符。但如果你要显示图片水平地址模式更高效——连续写入128*64/81024字节即可刷满全屏。一个容易忽略的问题OLED的D/C引脚数据/命令选择必须在SCLK上升沿之前稳定。我见过有人用GPIO翻转后立即拉高片选结果第一个字节被当成命令处理。正确做法先设置D/C电平再拉低片选然后发送数据。三、SD卡SPI模式从初始化到文件读写SD卡在SPI模式下初始化是个“磨人的小妖精”必须严格按照规范来// SD卡初始化 - 别这样写直接上高速uint8_tSD_Init(void){uint8_tcmd[6],resp;// 第一步发送至少74个时钟脉冲让SD卡完成上电// 这里踩过坑有些卡需要80个时钟保险起见发100个for(inti0;i10;i){SPI_WriteByte(0xFF);// MOSI保持高电平}// 第二步发送CMD0进入SPI模式// 注意CMD0的CRC必须正确即使SPI模式不检查CRCcmd[0]0x40;// CMD0cmd[1]0x00;cmd[2]0x00;cmd[3]0x00;cmd[4]0x00;cmd[5]0x95;// 固定CRC别自己算直接抄这个值SD_CS_LOW();SPI_WriteBytes(cmd,6);respSPI_ReadByte();SD_CS_HIGH();// 等待返回0x01空闲状态// 如果返回0xFF表示超时检查硬件连接if(resp!0x01)returnERROR;// 第三步发送CMD8检查SD卡版本// V2.0卡会返回0x01老卡返回0x05// ... 后续循环发送CMD55ACMD41直到返回0x00}关键经验SD卡初始化时片选信号必须在每个命令之间拉高让SD卡释放总线。我见过有人图省事一直拉低片选结果卡在ACMD41循环里出不来。读写数据块时SD卡会返回0xFE作为数据起始令牌。如果读到的不是0xFE需要重新发送CMD17单块读或CMD24单块写。写操作后还要等待SD卡返回0x05数据接受并进入忙状态DO低电平。文件系统层我推荐用FatFs但要注意配置FF_USE_FASTSEEK为1否则大文件读写会慢得让人崩溃。另外SD卡SPI模式下多块写CMD25比单块写效率高很多但需要处理预擦除命令ACMD23。四、W25Qxx Flash擦除和写入的“时间陷阱”W25Q16/32/64系列是嵌入式里最常用的SPI Flash但新手容易在擦除时间上翻车// 写入一页256字节 - 注意必须先擦除再写uint8_tW25Qxx_WritePage(uint32_taddr,uint8_t*data){// 检查地址是否在页边界if(addr0xFF)returnERROR;// 页地址必须256字节对齐// 使能写操作 - 每次写之前必须发这个命令W25Qxx_WriteEnable();// 发送页编程命令SPI_CS_LOW();SPI_WriteByte(0x02);// 页编程指令SPI_WriteByte((addr16)0xFF);SPI_WriteByte((addr8)0xFF);SPI_WriteByte(addr0xFF);for(inti0;i256;i){SPI_WriteByte(data[i]);}SPI_CS_HIGH();// 等待忙状态结束 - 这里踩过坑不能直接用delay// 页编程典型时间0.7ms最大3ms// 扇区擦除典型时间150ms最大400ms// 芯片擦除典型时间10s最大40swhile(W25Qxx_ReadStatus()0x01);// 轮询BUSY位returnOK;}一个血的教训W25Qxx的写操作不能跨页。如果你要写入的数据跨越了256字节边界必须拆分成两次页编程。同样擦除操作最小单位是扇区4KB不能只擦除几个字节。读操作相对简单但要注意连续读模式下地址会自动递增。如果你要读取大量数据用快速读指令0x0B比标准读0x03快得多因为快速读可以设置虚拟字节dummy cycle来适应更高时钟频率。五、三设备共线的实战技巧当OLED、SD卡、Flash挂在同一条SPI总线上时我总结了几条铁律片选信号必须独立不能共用。每个设备的CS引脚接到不同的GPIO且初始化时全部拉高无效状态。时钟极性/相位切换OLED用模式0CPOL0, CPHA0SD卡初始化用模式0但有些SD卡在高速模式下需要模式3CPOL1, CPHA1。W25Qxx通常用模式0或模式3都行但建议统一用模式0。切换设备时先拉高当前设备的CS再配置SPI寄存器最后拉低目标设备的CS。总线空闲状态所有设备CS拉高后MOSI和SCLK应该保持确定电平。我习惯在SPI去初始化后将MOSI拉低、SCLK拉低避免浮空引脚引入噪声。中断保护SPI传输过程中必须关中断否则中断服务函数里如果操作了同一个SPI外设会导致数据错乱。我通常用__disable_irq()和__enable_irq()包裹整个传输过程。六、调试工具和技巧逻辑分析仪是SPI调试的必备工具。我常用的调试方法抓取CS、SCLK、MOSI、MISO四根线观察时序是否符合设备手册检查CS拉低到第一个SCLK上升沿的时间有些设备要求这个时间大于100ns验证数据字节顺序SPI有MSB和LSB两种模式W25Qxx要求MSB first而某些OLED驱动芯片可能要求LSB first如果手头没有逻辑分析仪可以用GPIO模拟SPI来验证时序。虽然慢但能精确控制每个边沿。我曾在调试SD卡时用GPIO模拟SPI发现是硬件SPI的NSS引脚配置错误导致片选信号异常。七、个人经验总结写了十年嵌入式SPI接口的坑踩了无数说几条实在的别迷信硬件SPI的速度。对于OLED这种小数据量设备GPIO模拟SPI完全够用而且调试方便。SD卡和Flash才需要硬件SPI的高速率。SPI设备共线时一定要做“设备切换延迟”。切换设备后发第一个字节前先发一个0xFF的空字节让总线稳定。这个习惯救了我好几次。W25Qxx的写保护寄存器Status Register默认是写保护的需要先发写使能命令0x06才能修改。很多人忘记这一步导致擦除和写入操作无效。SD卡的SPI模式不支持CMD6切换速度所以别想着在SPI模式下切换SD卡的工作电压或总线宽度。老老实实用默认的1位SPI模式。最后一条SPI调试时先确认硬件连接——用万用表量CS、SCLK、MOSI、MISO是否通再查软件。我见过太多人花三天查代码最后发现是杜邦线接触不良。SPI看似简单但每个设备都有自己的“小脾气”。把这三个器件调通基本上嵌入式SPI开发就入门了。下次遇到其他SPI设备无非是换一套指令集和时序参数调试思路是一样的。