STM32 SPI通信详解:从时序到W25Q64 Flash驱动
上一篇I2C讲了SHT30传感器这篇讲SPI。SPI是嵌入式里速率最高的通信协议之一——刷屏、读写Flash、连SD卡全靠它。这篇从SPI协议原理讲到W25Q64 Flash驱动实战附4种工作模式和DMA加速技巧。一、SPI vs I2C什么时候用SPI对比项I2CSPI线数2线SCLSDA4线SCLKMOSIMISOCS速率最高3.4MHz通常几十MHz最高可达100MHz全双工❌ 半双工✅ 全双工多从设备靠地址区分靠CS片选区分应答有ACK无ACK典型器件传感器SHT30/BH1750Flash、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: ___/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\___ 选中 取消选中主设备拉低CS选中从设备主设备产生SCLK时钟每个时钟周期MOSI上发1bit同时MISO上收1bit8个时钟周期 传输1字节收发同时完成传输完成主设备拉高CS3.2 SPI的4种工作模式CPOL CPHA这是SPI最难理解的部分面试常考。CPOLClock Polarity时钟极性CPOL0空闲时SCLK为低电平CPOL1空闲时SCLK为高电平CPHAClock Phase时钟相位CPHA0第1个时钟边沿采样前沿采样CPHA1第2个时钟边沿采样后沿采样组合出4种模式模式CPOLCPHA空闲电平采样边沿000低上升沿采样101低下降沿采样210高下降沿采样311高上升沿采样怎么选查从设备的Datasheet比如W25Q64支持Mode 0和Mode 3触摸屏控制芯片GT911用Mode 0。配错了通信必然乱码。记忆口诀CPOL看空闲时SCLK高还是低CPHA看第几个边沿采样。四、STM32 SPI配置以SPI1PA5-SCK, PA6-MISO, PA7-MOSI连接W25Q64为例。4.1 HAL库版CubeMXCubeMX配置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命令 */idspi_rw(0xFF)8;/* 制造商ID高字节 */id|spi_rw(0xFF);/* 设备ID低字节 */CS_HIGH();returnid;/* W25Q64返回 0xEF16 */}4.2 寄存器版voidspi1_init(void){RCC-APB2ENR|(112);/* SPI1时钟 */RCC-APB2ENR|(12);/* GPIOA时钟 *//* PA5(SCK) PA7(MOSI) 复用推挽, PA6(MISO) 浮空输入 *//* CRL寄存器每4位控制一个引脚PA5在bit20-23, PA6在bit24-27, PA7在bit28-31 *//* 先清零这3个引脚的配置位 */GPIOA-CRL~(0xFFF20);/* 0xB3 二进制10110011 * PA5(SCK) 0xB(1011) 复用推挽输出50MHz * PA6(MISO) 0x4(0100) 浮空输入注意这里0xB3的中间4位是0x4 * PA7(MOSI) 0xB(1011) 复用推挽输出50MHz * 三个引脚的配置值合并写入CRL的对应位置 */GPIOA-CRL|(0xB320);SPI1-CR10;SPI1-CR1|(00);/* CPHA0 */SPI1-CR1|(01);/* CPOL0 → Mode 0 */SPI1-CR1|(12);/* 主模式 */SPI1-CR1|(011);/* 8位数据 */SPI1-CR1|(19);/* 软件管理CS */SPI1-CR1|(33);/* 波特率÷16 4.5MHz */SPI1-CR1|(16);/* 使能SPI */}uint8_tspi_rw(uint8_tdata){while(!(SPI1-SR(11)));/* 等TXE发送缓冲空 */SPI1-DRdata;while(!(SPI1-SR(10)));/* 等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每次写/擦前必须发等待空闲-读状态寄存器bit00空闲5.2 读数据voidw25q64_read(uint32_taddr,uint8_t*buf,uint16_tlen){CS_LOW();spi_rw(0x03);/* 读数据命令 */spi_rw((addr16)0xFF);/* 地址高字节 */spi_rw((addr8)0xFF);/* 地址中字节 */spi_rw(addr0xFF);/* 地址低字节 */for(uint16_ti0;ilen;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);/* bit01表示忙 */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(len256)len256;/* 每页最大256字节 */w25q64_write_enable();CS_LOW();spi_rw(0x02);/* 页写命令 */spi_rw((addr16)0xFF);spi_rw((addr8)0xFF);spi_rw(addr0xFF);for(uint16_ti0;ilen;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((addr16)0xFF);spi_rw((addr8)0xFF);spi_rw(addr0xFF);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%33fpsDMA~8ms5%120fpsLVGL项目从30fps到60fps关键就是SPI换成了DMA方式。七、多从设备怎么接SPI支持一主多从每个从设备用独立CSMCU ──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原因CPOL/CPHA配错——查Datasheet确认模式CS没拉低——开始通信前必须CS_LOW()MISO/MOSI接反——交叉连接主MOSI接从MOSI不是MISO注意SPI的MOSI接MOSIMISO接MISO同名相接不像UART交叉。新手常搞混。坑2Flash写不进去原因没发写使能命令(0x06)——每次写/擦前必须发没擦除就写——Flash只能1写0写前必须擦除跨页写——每页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);坑4DMA发送中途数据错乱原因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入门第一课从裸机到多任务的思维转变》如果这篇文章对你有帮助点赞 收藏 关注这是我持续更新的动力有问题欢迎评论区交流我会逐条回复。作者嵌入式阿蔡