news 2026/8/29 19:29:38

13. 基于TI MSPM0G3507开发板的SPI协议详解与W25Q128 Flash存储驱动实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
13. 基于TI MSPM0G3507开发板的SPI协议详解与W25Q128 Flash存储驱动实战

13. 基于TI MSPM0G3507开发板的SPI协议详解与W25Q128 Flash存储驱动实战

大家好,我是老李,一个在嵌入式行业摸爬滚打了十几年的工程师。最近有不少朋友在玩TI的MSPM0G3507这块板子,经常问我怎么用它的SPI接口去驱动外部的Flash芯片。正好我手头有个项目用到了W25Q128这款SPI Flash,今天我就把整个实战过程整理出来,从SPI协议的基础讲起,到手把手教你写出完整的驱动代码。

这篇文章特别适合刚开始接触MSPM0系列MCU,或者对SPI通信还不太熟悉的朋友。我会尽量用大白话把原理讲清楚,让你不仅能照着做出来,还能真正理解每一步为什么要这么做。

1. SPI协议到底是什么?咱们先搞懂基础

1.1 SPI通信的“四线制”

SPI(串行外设接口)是一种同步串行通信协议,你可以把它想象成MCU(主设备)和外部芯片(从设备)之间的一条“数据高速公路”。这条公路有4条车道:

信号线全称作用通俗理解
SCLKSerial Clock时钟线主设备发出的“节拍器”,告诉从设备什么时候该发送或接收数据
MOSIMaster Out Slave In主出从入线主设备发送数据给从设备的通道
MISOMaster In Slave Out主入从出线主设备接收从设备数据的通道
CS/SSChip Select / Slave Select片选线主设备用来“点名”选择要和哪个从设备通信

注意:有些资料里会把MOSI叫做SDO(串行数据输出),MISO叫做SDI(串行数据输入),其实是一回事,只是叫法不同。

SPI通信是全双工的,这意味着主设备和从设备可以同时发送和接收数据。想象一下两个人打电话,可以同时说话和听对方说话,效率很高。

1.2 SPI的“工作模式”:时钟极性和相位

这是SPI里最容易让人迷糊的地方,但理解了就很简单。SPI有4种工作模式,由两个参数决定:

  1. 时钟极性(CPOL):时钟线在空闲时的电平状态

    • CPOL=0:空闲时SCLK为低电平
    • CPOL=1:空闲时SCLK为高电平
  2. 时钟相位(CPHA):数据在时钟的哪个边沿被采样

    • CPHA=0:在时钟的第一个边沿采样数据
    • CPHA=1:在时钟的第二个边沿采样数据

组合起来就是4种模式:

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

实际项目中怎么选模式呢?看从设备的数据手册!不同的芯片支持的模式可能不同。W25Q128这款Flash芯片支持模式0和模式3,我们这次用模式0。

1.3 硬件SPI vs 软件SPI

你可能听说过“软件模拟SPI”,就是用普通的GPIO口,通过程序控制电平变化来模拟SPI时序。这种方法虽然灵活,但速度慢,占用CPU资源多。

MSPM0G3507内置了硬件SPI外设,咱们这次就用它。硬件SPI的优势很明显:

  • 速度快:最高可达32Mb/s
  • 不占CPU:数据传输由硬件完成,CPU可以干别的事
  • 支持DMA:大量数据传输时效率更高
  • 有FIFO缓冲区:减少数据丢失风险

2. 实战准备:认识我们的“搭档”W25Q128

2.1 W25Q128是什么?

W25Q128是一款SPI接口的Flash存储芯片,容量128Mb(16MB)。你可以把它理解成一个“电子硬盘”,断电后数据不会丢失,可以用来存储程序代码、配置文件、日志数据等。

它的内部结构是这样的:

  • 总容量:16MB(128Mb)
  • 扇区(Sector):4KB(最小的擦除单位)
  • 块(Block):64KB(16个扇区组成一个块)

重要提示:Flash存储有个特点——只能把1变成0,不能把0变成1。要想写入新数据,必须先擦除(把整个扇区变成全1,即0xFF),然后再写入。

2.2 硬件连接:把板子和芯片“接上线”

咱们的TI MSPM0G3507开发板背面已经贴好了W25Q128芯片,省去了焊接的麻烦。你只需要知道它们是怎么连的:

开发板引脚W25Q128引脚SPI信号线说明
PB6CS片选主设备选择从设备
PB9CLK时钟通信时钟
PB7DO(IO1)MISO主设备接收数据
PB8DI(IO0)MOSI主设备发送数据
3.3VVCC电源供电(注意是3.3V!)
GNDGND共地

这里有个细节要注意:PB6~PB9这4个引脚是硬件SPI1的复用功能,在数据手册里:

  • PB8对应SPI1的PICO(外设输入控制器输出),就是MOSI
  • PB7对应SPI1的POCI(外设输出控制器输入),就是MISO

3. 开始配置:让MSPM0G3507的SPI“活”起来

3.1 使用SysConfig图形化配置

TI的CCS开发环境有个很好用的工具叫SysConfig,可以图形化配置外设,不用自己一个个写寄存器。

  1. 新建工程:在CCS里新建一个Empty工程

  2. 打开配置文件:在工程左侧找到empty.syscfg文件并打开

  3. 添加SPI外设

    • 在SysConfig界面左侧找到SPI选项卡
    • 点击"ADD"添加一个SPI实例
    • 选择SPI1(因为我们的引脚对应SPI1)
  4. 配置SPI参数

    • Mode:Controller(主模式)
    • Bit Rate:根据需求设置,W25Q128最高支持104MHz,咱们先设个10MHz试试
    • Data Size:8 bits(W25Q128按字节操作)
    • Clock Phase:0(模式0)
    • Clock Polarity:0(模式0)
  5. 配置引脚

    • 在PinMux里找到PB6、PB7、PB8、PB9
    • 将它们分别配置为SPI1的CS、MISO、MOSI、CLK

这里有个坑我踩过:SPI外设自带的CS引脚(片选)在每次发送完一帧数据后会自动拉高,但W25Q128要求在整个通信过程中CS都要保持低电平。所以咱们不能用硬件SPI的CS功能,得用普通GPIO口手动控制。

  1. 添加GPIO控制CS

    • 在SysConfig里添加一个GPIO
    • 命名为CS,选择PB6引脚
    • 方向设置为Output(输出)
    • 初始电平设为High(高电平,不选中)
  2. 保存配置:按Ctrl+S保存,系统会自动生成配置代码

3.2 工程文件组织

好的代码结构能让项目更清晰,我习惯这样组织:

你的工程/ ├── main.c # 主程序 ├── Hardware/ # 硬件驱动层 │ ├── bsp_w25q128.c # W25Q128驱动源文件 │ └── bsp_w25q128.h # W25Q128驱动头文件 └── ...其他文件

记得在工程设置里添加头文件路径:${PROJECT_ROOT}/Hardware

4. 编写驱动:一步步“驯服”W25Q128

4.1 基础通信函数

首先,咱们需要一个最底层的字节收发函数。这个函数要完成发送一个字节并接收一个字节的任务。

// bsp_w25q128.c #include "bsp_w25q128.h" // SPI单字节读写函数 uint8_t spi_read_write_byte(uint8_t dat) { uint8_t data = 0; // 发送数据 DL_SPI_transmitData8(SPI_INST, dat); // 等待SPI总线空闲(数据发送完成) while(DL_SPI_isBusy(SPI_INST)); // 接收数据 data = DL_SPI_receiveData8(SPI_INST); // 再次等待,确保稳定 while(DL_SPI_isBusy(SPI_INST)); return data; }

这个函数是SPI通信的基础,后面所有高级操作都基于它。

4.2 CS片选控制宏

前面说了,CS引脚要手动控制。咱们在头文件里定义一个宏,方便使用:

// bsp_w25q128.h #ifndef _BSP_W25Q128_H__ #define _BSP_W25Q128_H__ #include "ti_msp_dl_config.h" // CS引脚控制宏 // x=0:拉低CS(选中芯片) // x=1:拉高CS(取消选中) #define SPI_CS(x) ((x) ? DL_GPIO_setPins(CS_PORT, CS_PIN_PIN) : \ DL_GPIO_clearPins(CS_PORT, CS_PIN_PIN)) // 函数声明 uint16_t W25Q128_readID(void); // 读取芯片ID void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte); // 写数据 void W25Q128_read(uint8_t* buffer, uint32_t read_addr, uint16_t read_length); // 读数据 #endif

4.3 读取芯片ID:验证通信是否正常

拿到一块新芯片,第一件事就是读它的ID,确认通信正常。W25Q128的ID读取命令是0x90。

// 读取芯片ID // 返回值说明: // 0xEF13 - W25Q80 (8Mb) // 0xEF14 - W25Q16 (16Mb) // 0xEF15 - W25Q32 (32Mb) // 0xEF16 - W25Q64 (64Mb) // 0xEF17 - W25Q128 (128Mb) - 咱们的芯片应该是这个 uint16_t W25Q128_readID(void) { uint16_t temp = 0; // 1. 拉低CS,选中芯片 SPI_CS(0); // 2. 发送读取ID命令 0x90 spi_read_write_byte(0x90); // 3. 发送24位地址 0x000000 spi_read_write_byte(0x00); spi_read_write_byte(0x00); spi_read_write_byte(0x00); // 4. 读取制造商ID(高8位) temp |= spi_read_write_byte(0xFF) << 8; // 5. 读取设备ID(低8位) temp |= spi_read_write_byte(0xFF); // 6. 拉高CS,取消选中 SPI_CS(1); return temp; }

这里发送0xFF是为了“骗”芯片输出数据。SPI通信时,主设备发送数据的同时也在接收数据,发送0xFF(全1)就是告诉从设备:“我没数据要发,你发你的”。

4.4 Flash操作的特殊机制

4.4.1 写使能(Write Enable)

Flash芯片默认是写保护的,防止误操作。写数据前要先“解锁”。

// 发送写使能命令(0x06) void W25Q128_write_enable(void) { SPI_CS(0); // 选中芯片 spi_read_write_byte(0x06); // 发送写使能命令 SPI_CS(1); // 取消选中 }
4.4.2 等待芯片“忙完”

Flash芯片擦除或写入时需要时间,操作期间是“忙”状态。这时候不能进行其他操作。

// 等待芯片空闲 void W25Q128_wait_busy(void) { unsigned char byte = 0; do { SPI_CS(0); // 选中芯片 spi_read_write_byte(0x05); // 发送读状态寄存器命令 byte = spi_read_write_byte(0xFF); // 读取状态寄存器 SPI_CS(1); // 取消选中 // 检查BUSY位(bit0),为1表示忙 } while((byte & 0x01) == 1); }
4.4.3 扇区擦除

前面说过,Flash写入前必须先擦除。擦除的最小单位是扇区(4KB)。

/** * @brief 擦除一个扇区 * @param addr 扇区号(0~4095,对应16MB空间) * @note 一个扇区4KB,地址要乘以4096得到实际地址 */ void W25Q128_erase_sector(uint32_t addr) { // 将扇区号转换为实际地址 addr *= 4096; W25Q128_write_enable(); // 写使能 W25Q128_wait_busy(); // 等待芯片空闲 SPI_CS(0); // 选中芯片 spi_read_write_byte(0x20); // 发送扇区擦除命令(0x20) // 发送24位地址(分3个字节发送) spi_read_write_byte((uint8_t)(addr >> 16)); // 高8位 spi_read_write_byte((uint8_t)(addr >> 8)); // 中8位 spi_read_write_byte((uint8_t)addr); // 低8位 SPI_CS(1); // 取消选中 W25Q128_wait_busy(); // 等待擦除完成 }

重要提醒:擦除操作比较耗时(典型值45ms),期间芯片会“忙”,所以最后要等待擦除完成。

4.5 核心功能:读写数据

4.5.1 写入数据

写入数据的流程是:擦除→写使能→发送写入命令→发送地址→发送数据。

/** * @brief 向W25Q128写入数据 * @param buffer 要写入的数据缓冲区 * @param addr 写入起始地址(字节地址) * @param numbyte 要写入的字节数 * @note 写入前会自动擦除对应扇区 */ void W25Q128_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte) { unsigned int i = 0; // 1. 先擦除目标扇区 W25Q128_erase_sector(addr / 4096); // 2. 写使能 W25Q128_write_enable(); // 3. 等待芯片空闲 W25Q128_wait_busy(); // 4. 开始写入数据 SPI_CS(0); // 选中芯片 spi_read_write_byte(0x02); // 发送页编程命令(0x02) // 发送24位地址 spi_read_write_byte((uint8_t)(addr >> 16)); spi_read_write_byte((uint8_t)(addr >> 8)); spi_read_write_byte((uint8_t)addr); // 连续写入数据 for(i = 0; i < numbyte; i++) { spi_read_write_byte(buffer[i]); } SPI_CS(1); // 取消选中 W25Q128_wait_busy(); // 等待写入完成 }

这里有个细节:代码中SPI_CS(0)应该是SPI_CS(1),这是个笔误,实际使用时要注意。

4.5.2 读取数据

读取数据就简单多了,不需要擦除,也不需要写使能。

/** * @brief 从W25Q128读取数据 * @param buffer 存储读取数据的缓冲区 * @param read_addr 读取起始地址 * @param read_length 要读取的字节数 */ void W25Q128_read(uint8_t* buffer, uint32_t read_addr, uint16_t read_length) { uint16_t i; SPI_CS(0); // 选中芯片 spi_read_write_byte(0x03); // 发送读数据命令(0x03) // 发送24位地址 spi_read_write_byte((uint8_t)(read_addr >> 16)); spi_read_write_byte((uint8_t)(read_addr >> 8)); spi_read_write_byte((uint8_t)read_addr); // 连续读取数据 for(i = 0; i < read_length; i++) { buffer[i] = spi_read_write_byte(0xFF); } SPI_CS(1); // 取消选中 }

5. 实际测试:让代码“跑”起来

5.1 主程序编写

现在所有驱动函数都准备好了,咱们写个主程序测试一下:

// main.c #include "ti_msp_dl_config.h" #include "stdio.h" #include "bsp_w25q128.h" // 简单的毫秒延时函数 #define delay_ms(X) delay_cycles((CPUCLK_FREQ/1000)*(X)) // 串口发送字符串函数(用于调试输出) void uart0_send_string(char* str) { while(*str != 0 && str != 0) { while(DL_UART_isBusy(UART_0_INST) == true); // 等待串口空闲 DL_UART_Main_transmitData(UART_0_INST, *str++); // 发送字符 } } int main(void) { char uart_output_buff[50] = {0}; unsigned char read_write_buff[10] = {0}; // 系统初始化(SysConfig生成的) SYSCFG_DL_init(); delay_ms(1); // 给硬件一点稳定时间 // 测试1:读取芯片ID sprintf(uart_output_buff, "ID = 0x%X\r\n", W25Q128_readID()); uart0_send_string(uart_output_buff); // 测试2:读取地址0的数据(应该是全FF,因为没写过) W25Q128_read(read_write_buff, 0, 5); sprintf(uart_output_buff, "Before write: %s\r\n", read_write_buff); uart0_send_string(uart_output_buff); // 测试3:向地址0写入字符串"lckfb" W25Q128_write((uint8_t*)"lckfb", 0, 5); delay_ms(10); // 等待写入完成 // 清空缓冲区,重新读取 memset(read_write_buff, 0, sizeof(read_write_buff)); // 测试4:再次读取地址0的数据 W25Q128_read(read_write_buff, 0, 5); sprintf(uart_output_buff, "After write: %s\r\n", read_write_buff); uart0_send_string(uart_output_buff); while(1) { // 主循环 } }

5.2 可能遇到的问题和解决方法

在实际调试中,你可能会遇到这些问题:

  1. 读不到正确的ID

    • 检查硬件连接,特别是电源是不是3.3V
    • 检查SPI模式设置(W25Q128支持模式0和3)
    • 用逻辑分析仪或示波器看波形,确认时序正确
  2. 写入后读出的数据不对

    • 确认写入前执行了擦除操作
    • 检查写入地址是否对齐(最好4字节对齐)
    • 写入后等待时间是否足够(加长delay_ms)
  3. 只能写入一次,第二次写入失败

    • Flash写入前必须擦除,而擦除是以扇区为单位的
    • 如果你要修改某个扇区里的部分数据,需要先把整个扇区读出来,在内存中修改,然后擦除扇区,最后写回整个扇区
  4. 通信速度慢

    • 检查SPI时钟配置,可以适当提高频率
    • 考虑使用DMA传输大量数据

6. 进阶思考:如何优化这个驱动?

这个基础驱动能工作,但在实际项目中还可以优化:

  1. 添加写保护判断:W25Q128有写保护位,操作前可以先检查
  2. 支持跨页写入:W25Q128一页256字节,跨页写入要特殊处理
  3. 添加CRC校验:重要数据可以加CRC校验,提高可靠性
  4. 实现磨损均衡:频繁擦写同一区域会降低Flash寿命,可以轮流使用不同扇区
  5. 封装成文件系统:如果想存复杂数据,可以移植FATFS、LittleFS等文件系统

好了,关于MSPM0G3507的SPI驱动W25Q128的完整教程就到这里。实际做项目时,我建议你先用这个基础版本跑通,理解每个步骤的原理,然后再根据项目需求添加优化功能。

遇到问题别着急,嵌入式开发就是这样,一点点调试,一步步验证。有什么不清楚的,欢迎随时交流。

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

无需GPU!AI读脸术极速版,快速识别照片年龄与性别

无需GPU&#xff01;AI读脸术极速版&#xff0c;快速识别照片年龄与性别 1. 引言&#xff1a;当AI读脸变得像喝水一样简单 你有没有想过&#xff0c;给一张照片&#xff0c;AI就能立刻告诉你照片里的人是男是女、大概多大年纪&#xff1f;听起来像是科幻电影里的场景&#xf…

作者头像 李华
网站建设 2026/7/14 17:13:38

C++游戏毕设避坑指南:从架构设计到性能优化的完整实践

最近在帮学弟学妹们看C游戏毕设的代码&#xff0c;发现大家遇到的问题都惊人的相似&#xff1a;项目初期功能跑得飞快&#xff0c;中期加个新角色或者特效就开始“牵一发而动全身”&#xff0c;后期答辩演示时动不动就卡顿甚至崩溃。这其实不是能力问题&#xff0c;而是缺少一套…

作者头像 李华
网站建设 2026/7/14 17:13:39

C++ 对象和嵌套对象的创建与销毁

在了解了 C 堆、栈运行方式后&#xff0c;我们可以开始一个专题&#xff1a;研究对象&#xff08;特别是嵌套对象&#xff09;的创建与销毁&#xff0c;这是一个表面上看很普通但要搞清楚全部细节却又不简单的话题&#xff0c;本文就带大家细致地剖析一下 C 对象创建与销毁的全…

作者头像 李华
网站建设 2026/7/14 17:13:40

FilterSolutions2019使用手册

双击启动exe左半蓝色按钮对应滤波器的形状&#xff0c;假如你需要elliptic&#xff08;椭圆滤波器&#xff09;点击即可切换滤波器种类1. 巴特沃斯滤波器 (Butterworth Filter)核心特点&#xff1a;最大平坦度响应。通带&#xff1a;在通带内具有最平坦的幅度响应&#xff0c;没…

作者头像 李华
网站建设 2026/7/14 17:13:56

数据结构-队列(链式队列)

数据结构中的链式队列链式队列是一种基于链表实现的队列数据结构&#xff0c;采用先进先出&#xff08;FIFO&#xff09;的原则。与顺序队列不同&#xff0c;链式队列通过动态分配内存存储元素&#xff0c;避免了固定容量限制。链式队列的特点动态扩容&#xff1a;无需预先分配…

作者头像 李华