news 2026/8/7 10:41:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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入门第一课:从裸机到多任务的思维转变》

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

作者:嵌入式阿蔡

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/7 10:40:03

4步终极指南:使用OpenCore Legacy Patcher让老Mac安装最新macOS

4步终极指南&#xff1a;使用OpenCore Legacy Patcher让老Mac安装最新macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 还在为2012-2015年老款Mac无法升级…

作者头像 李华
网站建设 2026/8/7 10:38:57

UE4 UMG ScaleBox六种缩放模式详解与Image对齐实战技巧

1. 项目概述&#xff1a;为什么ScaleBox是UE4 UI设计的基石 在UE4的UI设计里&#xff0c;ScaleBox这个控件&#xff0c;我愿称之为“布局的定海神针”。很多刚接触UMG&#xff08;虚幻运动图形&#xff09;的朋友&#xff0c;可能会觉得它不起眼&#xff0c;不就是个装东西的盒…

作者头像 李华
网站建设 2026/8/7 10:36:57

Unity动画系统进阶:从Animation到Animator状态机完整工作流解析

1. 项目概述&#xff1a;从Animation到Animator的进化之路 如果你是从Unity 4.x甚至更早版本一路用过来的老开发者&#xff0c;看到项目里同时存在 Animation 组件和 Animator 组件&#xff0c;心里大概会涌起一股复杂的怀旧感。十多年前&#xff0c;我们给一个门做开关动画…

作者头像 李华
网站建设 2026/8/7 10:36:00

《AI 工程师“炼丹“日常深夜感悟 踩坑避坑实录》

《AI 工程师"炼丹"日常深夜感悟 踩坑避坑实录》 作者: 南屹川 (Yichuan Nan) (南笙)技术方向: 大模型算法应用、Prompt 与 Agent 实验、AI 辅助内容生成、模型评测与调参方法论 &#x1f4a1; 导语与现场排障背景 在生产环境重构 AI 工程师的"炼丹"日常与…

作者头像 李华
网站建设 2026/8/7 10:35:04

2026世界杯球星全景分析:从巅峰王者到未来新星的战术影响

最近在和朋友聊2026年美加墨世界杯时&#xff0c;发现大家讨论的焦点除了传统豪强&#xff0c;更多集中在了那些正处于职业生涯不同阶段的球星身上。从正值巅峰的“当打之年”到崭露头角的“未来之星”&#xff0c;再到可能上演“最后一舞”的老将&#xff0c;他们的状态和选择…

作者头像 李华
网站建设 2026/8/7 10:34:29

疯狂电路违规的申诉

【疯狂电路申诉】我是华东赛区&#xff0c;一名参赛选手&#xff0c; 想跟你聊一下前面队伍的违规问题。  卓老师您好&#xff0c;打扰您宝贵时间&#xff0c; 我想和您沟通华东赛区疯狂电路的判罚争议。  1、最初赛区公示晋级名额10人&#xff0c; 我们队伍排名第十成功拿…

作者头像 李华