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

STM32 SPI通信实战:从基础配置到W25Q64闪存读写

1. SPI通信基础与STM32硬件配置

SPI(Serial Peripheral Interface)是一种高速全双工同步串行通信协议,由摩托罗拉公司设计,广泛应用于嵌入式系统中。它只需要四根信号线就能实现主从设备之间的数据交换,非常适合连接Flash存储器、传感器等外设。

SPI的四根信号线分别是:

  • SCK(Serial Clock):时钟信号,由主设备产生
  • MOSI(Master Output Slave Input):主设备输出,从设备输入
  • MISO(Master Input Slave Output):主设备输入,从设备输出
  • SS(Slave Select):从设备片选信号

在STM32中配置SPI外设时,我们需要重点关注几个关键参数:

  1. 时钟极性(CPOL):决定SCK空闲时的电平状态
  2. 时钟相位(CPHA):决定数据在时钟的哪个边沿采样
  3. 波特率分频:设置SPI通信速率
  4. 数据帧格式:8位或16位数据

以STM32F1系列为例,硬件SPI1的引脚通常对应:

  • PA4:SS(软件控制)
  • PA5:SCK
  • PA6:MISO
  • PA7:MOSI

2. W25Q64闪存芯片特性解析

W25Q64是Winbond公司推出的64M-bit串行Flash存储器,采用SPI接口通信,具有以下特点:

  • 支持标准SPI(模式0和模式3)
  • 最高支持104MHz时钟频率
  • 支持扇区擦除(4KB)、块擦除(32KB/64KB)和整片擦除
  • 页编程操作(每页256字节)
  • 写保护功能和保持/暂停功能

实际项目中我发现,W25Q64对时序要求比较严格。有一次调试时发现写入失败,最后发现是SPI时钟相位配置错误。正确的配置应该是:

  • CPOL=0(空闲时低电平)
  • CPHA=0(第一个边沿采样)

3. STM32 SPI外设初始化实战

下面是一个完整的SPI初始化代码示例,采用STM32标准外设库:

void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; SPI_InitTypeDef SPI_InitStruct; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 2. 配置GPIO // SCK和MOSI配置为复用推挽输出 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); // MISO配置为上拉输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStruct); // SS配置为普通推挽输出 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_SetBits(GPIOA, GPIO_Pin_4); // 默认不选中 // 3. 配置SPI参数 SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode = SPI_Mode_Master; SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 模式0 SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 模式0 SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 18MHz @72MHz PCLK SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStruct.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStruct); // 4. 使能SPI SPI_Cmd(SPI1, ENABLE); }

调试时有个小技巧:如果SPI通信不正常,可以先用逻辑分析仪抓取SCK、MOSI和MISO的波形,确认时序是否符合预期。我曾经遇到过因为GPIO速度配置过低导致波形畸变的问题,将GPIO_Speed提高到50MHz后解决。

4. W25Q64驱动开发与功能实现

4.1 基本读写函数封装

首先需要封装几个基础函数:

// 片选控制 void W25Q64_CS(uint8_t state) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)state); } // 发送一个字节并接收返回值 uint8_t SPI1_ReadWriteByte(uint8_t data) { while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, data); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); } // 读取设备ID(用于检测芯片是否正常) uint32_t W25Q64_ReadID(void) { uint32_t id = 0; W25Q64_CS(0); SPI1_ReadWriteByte(0x9F); // JEDEC ID指令 id |= SPI1_ReadWriteByte(0xFF) << 16; id |= SPI1_ReadWriteByte(0xFF) << 8; id |= SPI1_ReadWriteByte(0xFF); W25Q64_CS(1); return id; }

4.2 闪存操作指令实现

W25Q64的操作都需要通过特定指令实现,常见指令如下:

#define W25X_WriteEnable 0x06 #define W25X_WriteDisable 0x04 #define W25X_ReadStatusReg 0x05 #define W25X_WriteStatusReg 0x01 #define W25X_ReadData 0x03 #define W25X_PageProgram 0x02 #define W25X_SectorErase 0x20

实现写使能和等待写入完成函数:

void W25Q64_WriteEnable(void) { W25Q64_CS(0); SPI1_ReadWriteByte(W25X_WriteEnable); W25Q64_CS(1); } void W25Q64_WaitBusy(void) { W25Q64_CS(0); SPI1_ReadWriteByte(W25X_ReadStatusReg); while((SPI1_ReadWriteByte(0xFF) & 0x01) == 0x01); W25Q64_CS(1); }

4.3 扇区擦除与页编程

Flash存储器必须先擦除才能写入,擦除的最小单位是扇区(4KB):

void W25Q64_SectorErase(uint32_t addr) { W25Q64_WriteEnable(); W25Q64_CS(0); SPI1_ReadWriteByte(W25X_SectorErase); SPI1_ReadWriteByte((addr >> 16) & 0xFF); SPI1_ReadWriteByte((addr >> 8) & 0xFF); SPI1_ReadWriteByte(addr & 0xFF); W25Q64_CS(1); W25Q64_WaitBusy(); }

页编程函数(最多写入256字节):

void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len) { W25Q64_WriteEnable(); W25Q64_CS(0); SPI1_ReadWriteByte(W25X_PageProgram); SPI1_ReadWriteByte((addr >> 16) & 0xFF); SPI1_ReadWriteByte((addr >> 8) & 0xFF); SPI1_ReadWriteByte(addr & 0xFF); for(uint16_t i=0; i<len; i++) { SPI1_ReadWriteByte(data[i]); } W25Q64_CS(1); W25Q64_WaitBusy(); }

5. 实际应用中的注意事项

  1. 时序问题:W25Q64对指令时序有严格要求,每个指令后需要适当延时
  2. 写保护:写入前必须发送Write Enable指令,且WP引脚需接高电平
  3. 跨页写入:单次写入不能跨页(256字节边界)
  4. 擦除时间:扇区擦除需要较长时间(典型值100ms)
  5. 电源稳定性:在写入和擦除操作期间必须保证电源稳定

一个完整的读写流程示例:

  1. 读取设备ID确认通信正常
  2. 擦除目标扇区
  3. 等待擦除完成
  4. 写入数据
  5. 读取回校验
uint8_t buf[256], rbuf[256]; memset(buf, 0xAA, 256); // 擦除第一个扇区 W25Q64_SectorErase(0x000000); // 写入数据 W25Q64_PageProgram(0x000000, buf, 256); // 读取验证 W25Q64_ReadData(0x000000, rbuf, 256); if(memcmp(buf, rbuf, 256) == 0) { printf("Verify OK!\n"); }

在项目开发中,建议将W25Q64的操作封装成独立的驱动模块,便于移植和维护。对于需要频繁读写的场景,可以考虑实现缓存机制来减少擦写次数,延长Flash寿命。

http://www.jsqmd.com/news/323730/

相关文章:

  • Clawdbot部署案例:24G显存下Qwen3-32B性能调优与响应延迟优化详解
  • 静态旁路的双面性:UPS安全机制中的风险与平衡
  • 3步终极指南:让微信聊天记录永不丢失的无忧备份方案
  • SDXL 1.0电影级绘图工坊实战教程:用SDXL生成可商用版权图策略
  • 零基础玩转Nano-Banana:手把手教你做产品拆解图
  • 万物识别镜像输出格式对比:JSON vs 图像标注哪个更实用
  • 造相Z-Image文生图模型v2与Cursor智能IDE集成
  • Java项目智能客服系统实战:从零搭建到生产环境部署
  • WeChatMsg:数据备份与本地化存储的终极解决方案
  • SeqGPT-560M零样本实战教程:用自由Prompt实现定制化文本理解任务(含模板库)
  • 小白必看:QAnything PDF解析模型的安装与OCR识别功能详解
  • RexUniNLU开源大模型应用:构建垂直领域零样本NLU标注辅助平台
  • douyin-downloader终极攻略:无水印视频采集的4个革命性方法
  • AI语音合成与多角色配音:VOICEVOX免费语音工具全攻略
  • lychee-rerank-mm实战案例:跨境电商商品图-多语言描述一致性验证
  • ComfyUI ControlNet Aux模型下载全面攻略:从入门到精通
  • Python GUI开发实战指南:Tkinter从零开始掌握桌面应用开发
  • 新手必看!YOLO11完整环境部署保姆级指南
  • Z-Image在广告设计中的应用:自动化创意内容生成
  • 智能客服系统设计方案:从架构选型到生产环境实战
  • 星图平台GPU算力适配指南:Qwen3-VL:30B在48G A100/H100上的显存占用优化
  • 解锁Windows媒体解码终极优化指南:从入门到精通的LAV Filters配置手册
  • XGantt甘特图组件:构建高效项目管理界面的全栈解决方案
  • BEYOND REALITY Z-Image算力适配方案:Z-Image-Turbo架构显存占用实测分析
  • vlog旁白不用愁!IndexTTS 2.0个性化语音生成教程
  • QWEN-AUDIO对比实测:职场/甜美/磁性/大叔音效展示
  • PowerPaint-V1 Gradio入门指南:两种模式切换逻辑与适用边界说明
  • 5个突破性步骤:3D模型跨软件无缝协作让设计师告别格式障碍
  • 智能分类垃圾桶毕设:从零搭建嵌入式AI垃圾分类系统的完整实践
  • PyQt6实战指南:从界面设计到项目落地的全方位解析