当前位置: 首页 > news >正文

STM32 SPI通信详解:从时序到W25Q64 Flash驱动

上一篇I2C讲了SHT30传感器,这篇讲SPI。
SPI是嵌入式里速率最高的通信协议之一——刷屏、读写Flash、连SD卡全靠它。
这篇从SPI协议原理讲到W25Q64 Flash驱动实战,附4种工作模式和DMA加速技巧。


一、SPI vs I2C:什么时候用SPI?

对比项I2CSPI
线数2线(SCL+SDA)4线(SCLK+MOSI+MISO+CS)
速率最高3.4MHz通常几十MHz,最高可达100MHz+
全双工❌ 半双工✅ 全双工
多从设备靠地址区分靠CS片选区分
应答有ACK无ACK
典型器件传感器(SHT30/BH1750)Flash、LCD屏、SD卡

选型原则

  • 低速传感器(<1MHz)→ I2C(省引脚)
  • 高速设备(Flash/屏幕)→SPI(速度快)

我做LVGL项目时,480×480屏幕刷一帧要460KB数据,I2C完全扛不住,必须用SPI。W25Q64存字库和图片资源,也是SPI。


二、SPI的4根线

主设备(MCU) 从设备(Flash/屏幕) ┌──┐ ┌──┐ │ │ SCLK ──────────────►│ │ 时钟线(主→从) │ │ MOSI ──────────────►│ │ 主出从入(主→从) │ │ MISO ◄──────────────│ │ 主入从出(从→主) │ │ CS ──────────────►│ │ 片选(低有效) └──┘ └──┘
信号全称方向作用
SCLKSerial Clock主→从时钟线,主设备产生
MOSIMaster Out Slave In主→从主设备发数据
MISOMaster In Slave Out从→主主设备收数据
CSChip Select主→从片选,低电平选中从设备

关键:SPI是全双工的——MOSI和MISO同时工作,发一个字节的同时也能收一个字节。这是SPI比I2C快的核心原因。


三、SPI通信时序

3.1 基本时序

SCLK: ‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_ 1 2 3 4 5 6 7 8 (8个时钟=1字节) MOSI: ─D7─D6─D5─D4─D3─D2─D1─D0─► MISO: ◄─D7─D6─D5─D4─D3─D2─D1─D0─ CS: ___/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\___ 选中 取消选中
  1. 主设备拉低CS,选中从设备
  2. 主设备产生SCLK时钟
  3. 每个时钟周期,MOSI上发1bit,同时MISO上收1bit
  4. 8个时钟周期 = 传输1字节(收发同时完成)
  5. 传输完成,主设备拉高CS

3.2 SPI的4种工作模式(CPOL + CPHA)

这是SPI最难理解的部分,面试常考。

CPOL(Clock Polarity,时钟极性):

  • CPOL=0:空闲时SCLK为低电平
  • CPOL=1:空闲时SCLK为高电平

CPHA(Clock Phase,时钟相位):

  • CPHA=0:第1个时钟边沿采样(前沿采样)
  • CPHA=1:第2个时钟边沿采样(后沿采样)

组合出4种模式:

模式CPOLCPHA空闲电平采样边沿
000上升沿采样
101下降沿采样
210下降沿采样
311上升沿采样

怎么选:查从设备的Datasheet!比如W25Q64支持Mode 0和Mode 3,触摸屏控制芯片GT911用Mode 0。配错了通信必然乱码。

记忆口诀:CPOL看"空闲时SCLK高还是低",CPHA看"第几个边沿采样"。


四、STM32 SPI配置

SPI1(PA5-SCK, PA6-MISO, PA7-MOSI)连接W25Q64为例。

4.1 HAL库版(CubeMX)

CubeMX配置

SPI1 → Mode: Full-Duplex Master(全双工主模式) Parameter Settings: Frame Format: Motorola ← Motorola格式是SPI的标准帧格式(另一种TI格式极少使用),默认选Motorola即可 Data Size: 8 Bit First Bit: MSB First Prescaler: 4 → 72MHz/4 = 18MHz (W25Q64最大支持104MHz) Clock Polarity (CPOL): Low Clock Phase (CPHA): 1 Edge → 即 SPI Mode 0 CS引脚:用GPIO输出(PA4),手动控制

代码

/* 收发一个字节(全双工) */uint8_tspi_rw(uint8_ttx_data){uint8_trx_data;HAL_SPI_TransmitReceive(&hspi1,&tx_data,&rx_data,1,100);returnrx_data;}/* CS控制 */#defineCS_LOW()HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET)#defineCS_HIGH()HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET)/* 读取W25Q64 ID */uint16_tw25q64_read_id(void){uint16_tid;CS_LOW();spi_rw(0x9F);/* 读ID命令 */id=spi_rw(0xFF)<<8;/* 制造商ID(高字节) */id|=spi_rw(0xFF);/* 设备ID(低字节) */CS_HIGH();returnid;/* W25Q64返回 0xEF16 */}

4.2 寄存器版

voidspi1_init(void){RCC->APB2ENR|=(1<<12);/* SPI1时钟 */RCC->APB2ENR|=(1<<2);/* GPIOA时钟 *//* PA5(SCK) PA7(MOSI) 复用推挽, PA6(MISO) 浮空输入 *//* CRL寄存器每4位控制一个引脚,PA5在bit20-23, PA6在bit24-27, PA7在bit28-31 *//* 先清零这3个引脚的配置位 */GPIOA->CRL&=~(0xFFF<<20);/* 0xB3 = 二进制10110011 * PA5(SCK) = 0xB(1011) = 复用推挽输出50MHz * PA6(MISO) = 0x4(0100) = 浮空输入(注意这里0xB3的中间4位是0x4) * PA7(MOSI) = 0xB(1011) = 复用推挽输出50MHz * 三个引脚的配置值合并写入CRL的对应位置 */GPIOA->CRL|=(0xB3<<20);SPI1->CR1=0;SPI1->CR1|=(0<<0);/* CPHA=0 */SPI1->CR1|=(0<<1);/* CPOL=0 → Mode 0 */SPI1->CR1|=(1<<2);/* 主模式 */SPI1->CR1|=(0<<11);/* 8位数据 */SPI1->CR1|=(1<<9);/* 软件管理CS */SPI1->CR1|=(3<<3);/* 波特率÷16 = 4.5MHz */SPI1->CR1|=(1<<6);/* 使能SPI */}uint8_tspi_rw(uint8_tdata){while(!(SPI1->SR&(1<<1)));/* 等TXE发送缓冲空 */SPI1->DR=data;while(!(SPI1->SR&(1<<0)));/* 等RXNE接收缓冲非空 */returnSPI1->DR;}

五、W25Q64 Flash驱动实战

W25Q64是64Mbit(即8MB)的SPI Flash,常用于存字库、图片、配置数据。

MB和Mbit的区别:1 Byte = 8 bit,所以64Mbit = 64÷8 = 8MB。Flash芯片通常用Mbit标注容量(因为芯片内部按bit存储),但我们编程时用Byte(字节)计算。W25Q64 = 64Mbit = 8MB = 8388608字节。

5.1 常用命令

命令代码功能
读ID0x9F读取制造商和设备ID
读数据0x03从指定地址读数据
页写0x02向指定地址写数据(每页256字节)
擦除扇区0x20擦除一个4KB扇区
读状态0x05读取状态寄存器(判断忙/闲)
写使能0x06每次写/擦前必须发
等待空闲-读状态寄存器bit0,0=空闲

5.2 读数据

voidw25q64_read(uint32_taddr,uint8_t*buf,uint16_tlen){CS_LOW();spi_rw(0x03);/* 读数据命令 */spi_rw((addr>>16)&0xFF);/* 地址高字节 */spi_rw((addr>>8)&0xFF);/* 地址中字节 */spi_rw(addr&0xFF);/* 地址低字节 */for(uint16_ti=0;i<len;i++){buf[i]=spi_rw(0xFF);/* 发0xFF读数据 */}CS_HIGH();}

5.3 写数据(需先擦除)

/* 等待Flash空闲 */voidw25q64_wait_busy(void){CS_LOW();spi_rw(0x05);/* 读状态 */while(spi_rw(0xFF)&0x01);/* bit0=1表示忙 */CS_HIGH();}/* 写使能 */voidw25q64_write_enable(void){CS_LOW();spi_rw(0x06);CS_HIGH();}/* 页写(最多256字节) */voidw25q64_page_write(uint32_taddr,uint8_t*buf,uint16_tlen){if(len>256)len=256;/* 每页最大256字节 */w25q64_write_enable();CS_LOW();spi_rw(0x02);/* 页写命令 */spi_rw((addr>>16)&0xFF);spi_rw((addr>>8)&0xFF);spi_rw(addr&0xFF);for(uint16_ti=0;i<len;i++){spi_rw(buf[i]);}CS_HIGH();w25q64_wait_busy();/* 等写完成 */}

5.4 擦除扇区

voidw25q64_erase_sector(uint32_taddr){w25q64_write_enable();CS_LOW();spi_rw(0x20);/* 扇区擦除命令 */spi_rw((addr>>16)&0xFF);spi_rw((addr>>8)&0xFF);spi_rw(addr&0xFF);CS_HIGH();w25q64_wait_busy();/* 擦除较慢,约几百ms */}

Flash特性:Flash的存储单元默认状态是1,写入操作只能把1变成0,不能把0变成1。要把0变回1,必须通过擦除操作(整片变回1)。所以写之前必须先擦除。擦除最小单位是4KB扇区。


六、SPI + DMA加速

刷屏时用CPU轮询SPI太慢,用DMA能提速3-4倍。

6.1 DMA发送

/* DMA方式发送大量数据 */HAL_SPI_Transmit_DMA(&hspi1,frame_buf,460800);/* 发送完成回调 */voidHAL_SPI_TxCpltCallback(SPI_HandleTypeDef*hspi){CS_HIGH();/* 拉高CS */lv_disp_flush_ready(&disp_drv);/* 通知LVGL */}

6.2 性能对比

方式460KB耗时CPU占用帧率
CPU轮询~30ms100%33fps
DMA~8ms<5%120fps

LVGL项目从30fps到60fps,关键就是SPI换成了DMA方式。


七、多从设备怎么接?

SPI支持一主多从,每个从设备用独立CS:

MCU ──SCLK──────────┬── Flash SCLK └── LCD SCLK ──MOSI──────────┬── Flash MOSI └── LCD MOSI ──MISO──────────┬── Flash MISO └── LCD MISO ──CS1 ────────── Flash CS ──CS2 ────────── LCD CS

通信时只拉低目标设备的CS,其他设备CS保持高。这样SCLK/MOSI/MISO可以共享,节省引脚。

/* 选中Flash通信 */FLASH_CS_LOW();spi_rw(0x9F);FLASH_CS_HIGH();/* 选中LCD通信 */LCD_CS_LOW();spi_rw(lcd_cmd);LCD_CS_HIGH();

注意:同一时刻只能有一个从设备CS为低,否则数据会冲突。


八、常见踩坑

坑1:通信全是0xFF或0x00

原因

  1. CPOL/CPHA配错——查Datasheet确认模式
  2. CS没拉低——开始通信前必须CS_LOW()
  3. MISO/MOSI接反——交叉连接,主MOSI接从MOSI(不是MISO!)

注意:SPI的MOSI接MOSI,MISO接MISO,同名相接(不像UART交叉)。新手常搞混。


坑2:Flash写不进去

原因

  1. 没发写使能命令(0x06)——每次写/擦前必须发
  2. 没擦除就写——Flash只能1写0,写前必须擦除
  3. 跨页写——每页256字节,跨页写会回卷覆盖前面数据

解决:严格按"写使能→写数据→等忙"流程,跨页时分段写。


坑3:读到的数据错位

原因:SPI的TransmitReceive是全双工的,发的同时也在收。如果只发不收,接收缓冲区会溢出,导致下次读数据错位。

解决:用HAL_SPI_TransmitReceive而非HAL_SPI_Transmit,即使不想收数据也要读DR寄存器清空。

/* 错误:只发不收 */HAL_SPI_Transmit(&hspi1,&data,1,100);/* 正确:收发同时(即使不要收的数据) */HAL_SPI_TransmitReceive(&hspi1,&data,&dummy,1,100);

坑4:DMA发送中途数据错乱

原因:DMA发送期间,源数据被其他代码修改了。

解决:DMA发送期间不要修改源缓冲区;或用双缓冲,DMA发A时CPU填B。


九、总结

要点内容
SPI四线SCLK/MOSI/MISO/CS,全双工
4种模式CPOL×CPHA组合,查Datasheet确定
W25Q64读(0x03)/写(0x02)/擦(0x20)/写使能(0x06)
Flash特性写前必须擦除,每页256字节
DMA加速刷屏460KB从30ms→8ms
多从设备共享三线,独立CS片选

一句话总结:SPI的核心是4线全双工 + CPOL/CPHA选模式,Flash驱动记住"写前必擦除、写前必使能、写后等空闲"三原则。


下一篇预告:《FreeRTOS入门第一课:从裸机到多任务的思维转变》

如果这篇文章对你有帮助,点赞 + 收藏 + 关注,这是我持续更新的动力!
有问题欢迎评论区交流,我会逐条回复。

作者:嵌入式阿蔡

http://www.cnnetsun.cn/news/3886934.html

相关文章:

  • 4步终极指南:使用OpenCore Legacy Patcher让老Mac安装最新macOS
  • UE4 UMG ScaleBox六种缩放模式详解与Image对齐实战技巧
  • Unity动画系统进阶:从Animation到Animator状态机完整工作流解析
  • 《AI 工程师“炼丹“日常深夜感悟 踩坑避坑实录》
  • 2026世界杯球星全景分析:从巅峰王者到未来新星的战术影响
  • 疯狂电路违规的申诉
  • Python电商用户行为分析系统开发实践
  • Blender到Unity模型转换全流程:解决材质丢失、动画错乱与性能优化
  • MQTT协议深度解析:从物联网通信原理到SpringBoot与ESP32实战应用
  • 从开源项目Beehave学习AI智能体行为树设计模式与工程实践
  • 公司网站建设后期维护:决定网站生死的关键环节
  • 终极指南:使用OpenCore Legacy Patcher让老旧Mac焕发新生,支持macOS Big Sur到Sequoia
  • UPF中set_level_shifter命令详解:多电压域芯片设计的电平转换器配置实战
  • WLAN设备深度解析:从核心原理到实战排障,打造稳定高效无线网络
  • 阿里P7+资深Android面经:DexClassLoader原理、Tinker热修复、Sophix底层、Atlas插件化
  • 终极指南:如何使用WaveTools鸣潮工具箱提升游戏体验
  • Kubernetes网络通信实战:从Pod到Service全链路验证
  • 揭秘北京市城乡建设学校网站如何助力学子规划职业生涯与学术提升路径
  • Windows平台基于SOEM开源库实现EtherCAT主站控制禾川伺服驱动器
  • Elasticsearch _mget API 实战指南:批量查询、字段过滤与错误处理
  • Cookie、Session、Token与OAuth:Web登录验证核心技术深度解析与实战选型
  • 品牌出海TikTok预算怎么分?月预算10万美金的内容矩阵与本地化完整配置方案解析
  • Fiddler走进爱尔兰酒吧:从技术笑话看调试工具的文化隐喻与创意启发
  • Cortex-M3微控制器实现PCIe端点设备:低成本嵌入式系统的高速数据通道设计
  • 12 RAG 搜不准?面试官想听的是“混合检索+重排“,不是换个向量库
  • 揭秘江苏海宏建设工程有限公司网站背后的匠心精神与透明化服务实录
  • 国产PLC运行时系统设计:高精度调度、增量更新与热备冗余的实现
  • 电赛国一报告模板:从结构到实战的完整指南
  • OpenCore Legacy Patcher解决方案:让老旧Mac重获新生的技术指南
  • 佳木斯建设局网站深度解析:从指尖指尖到城市肌理的民生连接点,揭秘数字时代的透明政务与高效服务