07_ESP32 SPI 通信
07_ESP32 SPI 通信
SPI(Serial Peripheral Interface,串行外设接口) 是一种高速、全双工的同步串行通信协议。SPI 广泛应用于微控制器与各种外设之间的通信,如 SD 卡、显示屏、传感器、Flash 存储器等。
SPI 具有以下特点:
- 四线通信:使用 4 根信号线进行通信(在单个从设备时可减少为 3 根)
- 主从架构:明确的主从关系,主设备控制通信时序
- 全双工:可以同时进行数据发送和接收
- 高速传输:速度通常比 I2C 和 UART 更快,可达到数十 MHz
- 同步通信:由主设备提供时钟信号,确保数据传输的可靠性
SPI 协议的四根核心信号线分别为:

- 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);
}
浙公网安备 33010602011771号