07_ESP32 SPI 通信

07_ESP32 SPI 通信

SPI(Serial Peripheral Interface,串行外设接口) 是一种高速、全双工的同步串行通信协议。SPI 广泛应用于微控制器与各种外设之间的通信,如 SD 卡、显示屏、传感器、Flash 存储器等。

SPI 具有以下特点:

  • 四线通信:使用 4 根信号线进行通信(在单个从设备时可减少为 3 根)
  • 主从架构:明确的主从关系,主设备控制通信时序
  • 全双工:可以同时进行数据发送和接收
  • 高速传输:速度通常比 I2C 和 UART 更快,可达到数十 MHz
  • 同步通信:由主设备提供时钟信号,确保数据传输的可靠性

SPI 协议的四根核心信号线分别为:

image-20260520130317126

  • SCK(Serial Clock,时钟线):串行时钟线。由主机产生的时钟信号,用于同步数据传输。
  • MOSI(Master Out Slave In,主出从入):主出从入线。主设备通过这条线向从设备发送数据。
  • MISO(Master In Slave Out,主入从出):主入从出线。从设备通过这条线向主设备发送数据。
  • SS/CS(Slave Select/Chip Select,从机选择/片选):片选信号线。主机用此线选择当前要通信的从机,低电平为选中。SPI 主机通过不同的 CS (片选)线选中目标从机。每增加一个 SPI 外设,就要多占用一个独立的 CS 引脚。

ESP32 中的 SPI

ESP32 芯片通常内置多个 SPI 控制器(具体数量请参考目标芯片的规格书):

  • SPI0 和 SPI1:用于访问 Flash 或 PSRAM。
  • SPI2 和 SPI3:可供用户自由使用的通用 SPI 控制器,其引脚可灵活分配。

在 Arduino 环境中,我们通过 SPI.h 库与外设通信。不同型号的 ESP32,其 SPI 总线的访问方式如下:

  • SPI : 默认 SPI 总线对象,对应硬件 SPI2 (FSPI)。
  • SPIClass hspi(HSPI) : 创建第二条总线对象,对应硬件 SPI3。

调用 SPI.begin() 时,如果不指定引脚,库会使用该总线的默认引脚;若提供了自定义引脚号,则会 通过 GPIO 矩阵进行路由

示例 :使用 SPI 控制W25Q64

ESP32 ↔ W25Q64 接线如下:

  • 3.3V → VCC
  • GND → GND
  • GPIO18 → SCK (CLK)
  • GPIO19 → MISO (DO)
  • GPIO20 → MOSI (DI)
  • GPIO5 → CS (片选)

重要:W25Q64 是 3.3V 设备,不能接 5V!

// 1. 引入SPI通信库(W25Q64 闪存芯片使用SPI协议通信)
#include <SPI.h>

// ==================== 硬件引脚定义 ====================
// 定义 ESP32 连接 W25Q64 片选引脚 CS = GPIO5
#define W25Q64_CS 5

// ==================== W25Q64 指令常量定义(芯片手册规定) ====================
#define W25X_READ 0x03     // 读取数据指令
#define W25X_WRITE 0x02    // 写入数据指令
#define W25X_WREN 0x06     // 写使能指令(写入/擦除前必须开启)
#define W25X_SE 0x20       // 扇区擦除指令(4KB 擦除)
#define W25X_JEDEC_ID 0x9F // 读取芯片ID指令(用于识别芯片型号)

// ==================== 函数提前声明 ====================
// 告诉编译器:下面会用到这些函数,防止编译报错
void readChipID();
void eraseSector(uint32_t addr);
void writeData(uint32_t addr, byte *data, int len);
void readData(uint32_t addr, byte *data, int len);

// ==================== setup 函数:只执行一次(初始化) ====================
void setup() {
  // 初始化串口波特率 115200,用于电脑查看打印信息
  Serial.begin(115200);

  // 初始化 SPI 总线
  // 参数顺序:SCK时钟脚, MISO数据输入, MOSI数据输出, CS片选脚
  SPI.begin(18, 19, 20, W25Q64_CS);

  // 设置 CS 引脚为输出模式
  pinMode(W25Q64_CS, OUTPUT);
  // 初始拉高 CS,表示不选中芯片
  digitalWrite(W25Q64_CS, HIGH);

  // 串口打印提示信息
  Serial.println("ESP32 + W25Q64 测试");

  // ==================== 步骤1:读取芯片ID,判断接线是否正确 ====================
  readChipID();

  // ==================== 步骤2:定义要写入闪存的数据 ====================
  byte data_write[] = "你好,世界!";
  // 获取数据长度(包含字符串结束符)
  int dataLen = sizeof(data_write);

  // ==================== 步骤3:擦除扇区 ====================
  // 闪存特性:必须先擦除(变成全1),才能写入新数据
  eraseSector(0x000000); // 擦除 0 地址所在的扇区

  // ==================== 步骤4:把数据写入 W25Q64 ====================
  // 参数:写入地址、数据数组、数据长度
  writeData(0x000000, data_write, dataLen);
  Serial.print("写入数据:");
  Serial.println((char*)data_write);

  // ==================== 步骤5:从 W25Q64 读取数据 ====================
  byte data_read[100]; // 定义数组,用来存放读取到的数据
  readData(0x000000, data_read, dataLen);
  Serial.print("读取数据:");
  Serial.println((char*)data_read);

  // 测试完成提示
  Serial.println("\n测试完成!");
}

// loop 函数:反复循环执行,这里不需要功能,所以空着
void loop() {

}

// ==================== 函数:读取 W25Q64 芯片ID ====================
// 作用:判断芯片是否正常连接,W25Q64 的正确ID是 0xEF4017
void readChipID() {
  // 拉低 CS,开始与芯片通信
  digitalWrite(W25Q64_CS, LOW);

  // 发送读取ID指令
  SPI.transfer(W25X_JEDEC_ID);

  // 连续读取3个字节:厂商ID + 型号ID
  byte manuf = SPI.transfer(0xFF);
  byte mid = SPI.transfer(0xFF);
  byte low = SPI.transfer(0xFF);

  // 拉高 CS,结束通信
  digitalWrite(W25Q64_CS, HIGH);

  // 串口打印芯片ID
  Serial.print("芯片ID:0x");
  Serial.print(manuf, HEX);
  Serial.print(mid, HEX);
  Serial.println(low, HEX);

  // 判断ID是否正确
  if (manuf == 0xEF && mid == 0x40 && low == 0x17) {
    Serial.println("W25Q64 连接成功!");
  } else {
    Serial.println("连接失败,请检查接线!");
  }
  Serial.println();
}

// ==================== 函数:扇区擦除(4KB) ====================
// 作用:把指定地址的一块区域清空为 0xFF,才能写入新数据
void eraseSector(uint32_t addr) {
  // 1. 发送写使能指令(必须发,否则无法擦除)
  digitalWrite(W25Q64_CS, LOW);
  SPI.transfer(W25X_WREN);
  digitalWrite(W25Q64_CS, HIGH);
  delayMicroseconds(10);

  // 2. 发送擦除指令 + 要擦除的地址
  digitalWrite(W25Q64_CS, LOW);
  SPI.transfer(W25X_SE);
  SPI.transfer((addr >> 16) & 0xFF); // 地址高8位
  SPI.transfer((addr >> 8) & 0xFF);  // 地址中8位
  SPI.transfer(addr & 0xFF);         // 地址低8位
  digitalWrite(W25Q64_CS, HIGH);

  delay(100); // 等待擦除完成(需要一点时间)
}

// ==================== 函数:向 W25Q64 写入数据 ====================
// 参数:写入地址、数据数组、数据长度
void writeData(uint32_t addr, byte *data, int len) {
  // 1. 写使能
  digitalWrite(W25Q64_CS, LOW);
  SPI.transfer(W25X_WREN);
  digitalWrite(W25Q64_CS, HIGH);
  delayMicroseconds(10);

  // 2. 发送写指令 + 写入地址
  digitalWrite(W25Q64_CS, LOW);
  SPI.transfer(W25X_WRITE);
  SPI.transfer((addr >> 16) & 0xFF);
  SPI.transfer((addr >> 8) & 0xFF);
  SPI.transfer(addr & 0xFF);

  // 3. 循环发送每一个字节的数据
  for (int i = 0; i < len; i++) {
    SPI.transfer(data[i]);
  }

  // 结束通信
  digitalWrite(W25Q64_CS, HIGH);
  delay(1);
}

// ==================== 函数:从 W25Q64 读取数据 ====================
// 参数:读取地址、存放数据的数组、读取长度
void readData(uint32_t addr, byte *data, int len) {
  // 开始通信
  digitalWrite(W25Q64_CS, LOW);

  // 发送读指令 + 读取地址
  SPI.transfer(W25X_READ);
  SPI.transfer((addr >> 16) & 0xFF);
  SPI.transfer((addr >> 8) & 0xFF);
  SPI.transfer(addr & 0xFF);

  // 循环读取每一个字节
  for (int i = 0; i < len; i++) {
    data[i] = SPI.transfer(0xFF);
  }

  // 结束通信
  digitalWrite(W25Q64_CS, HIGH);
}
posted @ 2026-05-20 13:33  Q&25  阅读(44)  评论(0)    收藏  举报