SPI通信协议
在ESP32-C3上使用SPI连接外设芯片时,需按照以下步骤操作,并调用ESP-IDF的核心函数:
1. 配置SPI总线参数
首先定义并初始化SPI总线配置结构体:
#include "driver/spi_master.h"
// 定义SPI引脚(根据实际连接调整)
#define MOSI_PIN 7
#define MISO_PIN 2
#define CLK_PIN 6
#define CS_PIN 10 // 外设的片选引脚
// 配置SPI总线参数
spi_bus_config_t buscfg = {
.mosi_io_num = MOSI_PIN,
.miso_io_num = MISO_PIN,
.sclk_io_num = CLK_PIN,
.quadwp_io_num = -1, // 未使用
.quadhd_io_num = -1, // 未使用
.max_transfer_sz = 4094, // 最大传输大小
};
2. 初始化SPI总线
调用 spi_bus_initialize 初始化SPI主机:
这个函数用于初始化SPI总线,配置主机模式,设置相关的GPIO引脚,分配DMA资源,设置时钟频率等。可能还会涉及中断的配置,或者检查引脚是否冲突。
// 初始化SPI2_HOST(ESP32-C3可用SPI2_HOST)
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
// 错误处理(如打印日志)
}
3. 配置并添加SPI设备
设置设备参数并添加到总线:
spi_device_interface_config_t devcfg = {
.mode = 0, // SPI模式(0-3,根据外设要求)
.clock_speed_hz = 10 * 1000 * 1000, // 时钟频率(10MHz)
.spics_io_num = CS_PIN, // 片选引脚
.queue_size = 7, // 传输队列大小
.flags = SPI_DEVICE_NO_DUMMY, // 可选标志(如无dummy位)
};
spi_device_handle_t spi;
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
if (ret != ESP_OK) {
// 错误处理
}
4. 执行SPI数据传输
构造事务结构体并发送数据:
// 定义发送和接收缓冲区
uint8_t tx_data[4] = {0x01, 0x02, 0x03, 0x04};
uint8_t rx_data[4] = {0};
spi_transaction_t t = {
.length = 8 * 4, // 数据位数(4字节×8位)
.tx_buffer = tx_data,
.rx_buffer = rx_data,
};
// 阻塞式传输
ret = spi_device_transmit(spi, &t);
if (ret != ESP_OK) {
// 错误处理
}
5. 清理资源
释放SPI设备和总线:
spi_bus_remove_device(spi); // 移除设备
spi_bus_free(SPI2_HOST); // 释放总线资源
核心函数列表
- 总线初始化与释放:
- spi_bus_initialize():初始化SPI总线。
- spi_bus_free():释放总线资源。
- 设备管理:
- spi_bus_add_device():添加设备到总线。
- spi_bus_remove_device():移除设备。
- 数据传输:
- spi_device_transmit():同步传输数据。
- spi_device_queue_trans() 和 spi_device_get_trans_result():异步传输(需队列管理)。
关键注意事项
- SPI模式:需与外设的CPOL和CPHA设置一致。
- GPIO分配:确保引脚无冲突,且支持SPI功能。
- 时钟频率:不超过外设支持的最大频率。
- DMA配置:使用SPI_DMA_CH_AUTO自动选择通道。
- 错误处理:检查所有API调用的返回值。
通过以上步骤和函数,即可在ESP32-C3上实现SPI通信。具体参数需根据外设芯片手册调整。
说明:
关键步骤1:
在ESP-IDF中,使用SPI2_HOST时,通常无需通过 idf.py menuconfig 进行额外配置。
如果SPI传输数据量极大(如大块DMA传输),可能需要调整 DMA缓冲区大小: 进入 menuconfig → Component config → Driver configurations → SPI configuration → SPI master DMA Max Buffer Size。 增大此值可支持更大的单次传输(默认4094字节)。
在第二步中,在ESP32-C3中调用 spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)
初始化SPI总线时,其底层操作可分为以下
1. 硬件SPI控制器配置
- 选择SPI主机:ESP32-C3有两个SPI控制器(SPI1_HOST和SPI2_HOST),此处选择
SPI2_HOST
(通常用于通用SPI通信)。 - 时钟源设置:默认使用APB总线时钟(通常为80MHz),后续设备使用时可以分频。
- DMA通道分配:通过
SPI_DMA_CH_AUTO
自动选择可用DMA通道,用于高效传输数据块。
2. GPIO引脚复用
- 引脚功能重映射:将
mosi_io_num、miso_io_num、sclk_io_num
等配置的物理GPIO引脚连接到SPI控制器的信号线。 - 冲突检查:确保这些GPIO未被其他功能(如UART、I2C)占用。
3. SPI总线全局参数配置
- 信号极性设置:根据后续设备的SPI模式
(CPOL/CPHA)
动态调整,但此时仅初始化默认状态。 - 位序配置:默认MSB(高位先传),除非在buscfg中指定flags修改为LSB。
- 最大传输长度:根据max_transfer_sz设置单次DMA传输的最大字节数(默认4094字节)。
4. DMA引擎初始化
- 缓冲区分配:为DMA传输分配内存缓冲区,用于临时存储SPI数据。
- 中断注册:注册DMA传输完成的中断服务程序(ISR),但用户无需直接处理。
5. 资源互斥锁创建
- 总线锁初始化:防止多个设备或任务同时访问SPI总线造成冲突。
- 线程安全机制:确保后续的
spi_device_transmit()
等函数可安全调用。
6. 错误检查与返回
- 参数合法性校验:检查buscfg中引脚是否有效、DMA通道是否可用等。
- 返回状态码:若成功返回ESP_OK,否则返回错误码(如ESP_ERR_INVALID_ARG)。
典型错误原因
若初始化失败(ret != ESP_OK),可能原因包括:
- GPIO冲突:配置的MOSI/MISO/CLK引脚已被其他外设占用。
- DMA资源不足:系统内其他组件(如I2S、SDMMC)已占用所有DMA通道。
- 无效主机号:ESP32-C3仅支持SPI1_HOST(保留用于Flash)和SPI2_HOST。
- 内存不足:无法为DMA缓冲区分配内存。
初始化后的总线状态
- PI控制器:处于空闲状态,未产生任何时钟信号。
- GPIO电平:MOSI和CLK引脚默认为低电平,MISO由外设控制。
- 后续操作:需通过spi_bus_add_device()添加具体设备并设置通信参数(如SPI模式、时钟分频)。
调试建议
若初始化失败,可通过以下方式定位问题:
if (ret == ESP_ERR_INVALID_ARG) {
ESP_LOGE("SPI", "无效参数(如引脚号错误)");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE("SPI", "无可用DMA通道");
} else if (ret == ESP_ERR_NO_MEM) {
ESP_LOGE("SPI", "内存不足");
}
通过上述步骤,spi_bus_initialize()为SPI总线提供了硬件底层支持,但尚未与外设通信。实际数据传输需在添加设备后通过spi_device_transmit()实现。
关键步骤2:
当调用 spi_bus_add_device(SPI2_HOST, &devcfg, &spi) 时,函数内部会执行以下操作:
(1) 配置设备参数
根据传入的 spi_device_interface_config_t 结构体(devcfg)设置设备属性:
- SPI模式(
.mode
):CPOL(时钟极性)和CPHA(时钟相位)。 - 时钟频率(
.clock_speed_hz
):通过分频器将总线时钟(如80MHz)分频为设备所需频率。 - 片选引脚(
.spics_io_num
):指定该设备的CS引脚。 - 传输队列大小(.queue_size):异步传输时的队列容量。
- 特殊标志(.flags):如
SPI_DEVICE_HALFDUPLEX(半双工模式)、SPI_DEVICE_NO_DUMMY(无虚位传输)
等。
(2) 分配设备句柄
为设备分配一个 spi_device_handle_t 句柄(即代码中的spi变量),后续通过此句柄操作设备(如调用spi_device_transmit())。
(3) 计算时钟分频值
根据 .clock_speed_hz 计算SPI控制器的分频系数,使实际时钟频率尽可能接近用户设定的值(例如10MHz)。
(4) 注册设备到总线
将设备添加到总线的设备链表中,确保多个设备可以共享同一个SPI控制器。 初始化设备的CS引脚为GPIO输出模式,并默认设置为高电平(非激活状态)。 通过spi_bus_add_device()添加的设备是 逻辑实体
,绑定到总线上,通过CS引脚区分。
(5) 配置DMA(如果启用)
如果使用DMA(通过spi_bus_initialize()的dma_chan参数指定),会为设备分配DMA通道缓冲区。
(6) 返回状态
返回ESP_OK表示成功,否则返回错误码(如引脚冲突、内存不足等)。
总线与设备的关系
- 物理总线:如SPI2_HOST是硬件资源,负责底层信号生成和传输。
- 逻辑设备:通过
spi_bus_add_device()添加的设备是逻辑实体
,绑定到总线上,通过CS引脚区分。 - 多设备共存:同一总线可以添加多个设备,例如:
// 添加设备1(CS引脚为GPIO10)
spi_bus_add_device(SPI2_HOST, &devcfg1, &spi_dev1);
// 添加设备2(CS引脚为GPIO11)
spi_bus_add_device(SPI2_HOST, &devcfg2, &spi_dev2);
关键步骤3:
通过 spi_device_transmit() 函数进行数据传输,该函数会根据设备句柄(spi_device_handle_t)和传输参数(spi_transaction_t)执行以下操作:
uint8_t tx_data[4] = {0x01, 0x02, 0x03, 0x04}; // 发送数据
uint8_t rx_data[4] = {0}; // 接收数据缓冲区
spi_transaction_t t = {
.length = 8 * 4, // 传输总位数(4字节 × 8位 = 32位)
.rxlength = 8 * 2, // 实际接收数据位数(2字节)
.tx_buffer = tx_data, // 发送数据指针
.rx_buffer = rx_data, // 接收数据指针(数据将存储在此处)
};
// 执行阻塞式传输
ret = spi_device_transmit(spi, &t);
if (ret != ESP_OK) {
// 错误处理
}
//获取接收到的数据
ESP_LOGI("SPI", "Received data: 0x%02X 0x%02X 0x%02X 0x%02X",
rx_data[0], rx_data[1], rx_data[2], rx_data[3]);
如果接收数据小于等于 32 字节,可以直接使用 rx_data 字段(无需动态分配缓冲区):
spi_transaction_t t = {
.length = 8 * 4,
.tx_buffer = tx_data,
.flags = SPI_TRANS_USE_RXDATA, // 启用内置rx_data
};
// 传输完成后,数据在 t.rx_data 中
ESP_LOGI("SPI", "Received: 0x%02X", t.rx_data[0]);
SPI模式说明
SPI模式由 时钟极性(CPOL, Clock Polarity) 和 时钟相位(CPHA, Clock Phase) 的组合定义,共有4种模式(0-3)。以下是每个模式的具体含义及对应的时序特征:
SPI模式定义
模式 | CPOL | CPHA | 描述 |
---|---|---|---|
0 | 0 | 0 | 时钟空闲时为低电平,数据在上升沿采样。 |
1 | 0 | 1 | 时钟空闲时为低电平,数据在下降沿采样。 |
2 | 1 | 0 | 时钟空闲时为高电平,数据在下降沿采样。 |
3 | 1 | 1 | 时钟空闲时为高电平,数据在上升沿采样。 |
SPI模式时序特征
模式 | 时钟空闲 | 数据采样 |
---|---|---|
0 | 低电平 | 上升沿 |
1 | 低电平 | 下降沿 |
2 | 高电平 | 下降沿 |
3 | 高电平 | 上升沿 |
常见外设模式示例
外设类型 | 典型模式 |
---|---|
SD卡 | 0 |
多数SPI Flash | 0 或 3 |
BME280温湿度传感器 | 0 或 3 |
MAX31855热电偶模块 | 1 |
ST7735 LCD屏 | 3 |