1. ESP32-C3与SPI通信基础第一次拿到ESP32-C3开发板时我就被它丰富的通信接口吸引了。作为一款专为物联网设计的芯片ESP32-C3内置了三个SPI控制器其中GP-SPI2特别适合用来驱动外设。记得当时为了理解SPI的工作原理我特意用面包板做了个简单的实验用杜邦线连接了一个SPI Flash芯片结果因为时钟相位设置错误折腾了半天才读出数据。SPI全称Serial Peripheral Interface是一种同步串行通信协议。它最大的特点就是全双工通信和主从架构。在实际项目中我经常用ESP32-C3作为主机通过SPI同时控制多个设备。比如最近做的一个环境监测节点就是用GP-SPI2同时驱动OLED屏和温湿度传感器。ESP32-C3的GP-SPI2有几个特别实用的特性支持6个独立的片选信号(CS0-CS5)这意味着可以挂载多达6个设备时钟频率最高可达80MHz满足大多数外设需求四种工作模式(模式0-3)适配不同设备时序要求支持DMA传输减轻CPU负担提示新手最容易混淆的是SPI的四种模式主要区别在于时钟极性和相位。大多数SPI设备使用模式0或模式3建议先查阅设备手册确认。2. 硬件连接与引脚配置刚开始玩ESP32-C3时最让我头疼的就是引脚分配。芯片的GPIO功能可以灵活映射但SPI信号有固定对应的引脚。经过几次尝试我总结出了一套可靠的连接方案。对于GP-SPI2关键信号引脚如下信号名称默认GPIO功能说明FSPICLKGPIO6时钟信号FSPIQGPIO2数据输入FSPIDGPIO7数据输出FSPICS0GPIO10片选0FSPICS1GPIO3片选1我的环境监测项目硬件连接如下OLED屏(SSD1306)接CS0温湿度传感器(SHT30)接CS1共用CLK、MOSI、MISO信号线#define OLED_CS_PIN 10 #define SHT30_CS_PIN 3 #define SPI_CLK_PIN 6 #define SPI_MOSI_PIN 7 #define SPI_MISO_PIN 2实际接线时有个小技巧尽量使用短导线特别是时钟信号线。我有次用了20cm的杜邦线结果SPI通信经常出错。后来改用10cm以内的导线问题就解决了。另外如果设备支持3.3V电平可以直接连接如果是5V设备记得要加电平转换电路。3. SPI初始化与配置配置SPI外设时我习惯先创建一个spi_device_interface_config_t结构体把参数都设置好。下面是我在项目中使用的初始化代码#include driver/spi_master.h spi_bus_config_t buscfg { .miso_io_num SPI_MISO_PIN, .mosi_io_num SPI_MOSI_PIN, .sclk_io_num SPI_CLK_PIN, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 4096 }; spi_device_interface_config_t oled_devcfg { .clock_speed_hz 10*1000*1000, // 10MHz .mode 0, // SPI mode 0 .spics_io_num OLED_CS_PIN, .queue_size 7, .pre_cb NULL, .post_cb NULL }; spi_device_interface_config_t sht30_devcfg { .clock_speed_hz 1*1000*1000, // 1MHz .mode 0, // SPI mode 0 .spics_io_num SHT30_CS_PIN, .queue_size 7 }; // 初始化SPI总线 ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, buscfg, SPI_DMA_CH_AUTO)); // 添加OLED设备 spi_device_handle_t oled_handle; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, oled_devcfg, oled_handle)); // 添加温湿度传感器设备 spi_device_handle_t sht30_handle; ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, sht30_devcfg, sht30_handle));这里有几个关键点需要注意时钟频率要根据外设支持的最高频率设置我的OLED屏支持10MHz而SHT30只需要1MHz模式设置必须与外设一致这两个设备都使用模式0queue_size表示传输队列大小如果同时有多个传输任务需要适当增大我曾经遇到过SPI初始化失败的问题后来发现是因为没有正确配置GPIO。ESP32-C3的SPI引脚有些是固定映射有些可以通过GPIO矩阵灵活配置。建议新手先使用默认引脚等熟悉后再尝试其他引脚组合。4. OLED屏幕驱动实现驱动OLED屏时我选择了常用的SSD1306芯片。这种屏幕虽然分辨率不高(通常128x64)但功耗低、接口简单非常适合物联网设备。下面分享我的驱动实现过程。首先需要准备传输数据。SSD1306使用命令和数据两种传输类型通过DC引脚区分。由于我的屏幕没有单独的DC引脚所以使用命令字节的最高位来区分// 发送命令 void oled_send_cmd(spi_device_handle_t handle, uint8_t cmd) { spi_transaction_t t { .length 8, .tx_buffer cmd, .user (void*)0 // DC0表示命令 }; ESP_ERROR_CHECK(spi_device_transmit(handle, t)); } // 发送数据 void oled_send_data(spi_device_handle_t handle, uint8_t *data, uint16_t len) { spi_transaction_t t { .length len * 8, .tx_buffer data, .user (void*)1 // DC1表示数据 }; ESP_ERROR_CHECK(spi_device_transmit(handle, t)); }初始化序列比较长但都是有固定流程的。我整理了一个典型的初始化函数void oled_init(spi_device_handle_t handle) { // 关闭显示 oled_send_cmd(handle, 0xAE); // 设置时钟分频和振荡频率 oled_send_cmd(handle, 0xD5); oled_send_cmd(handle, 0x80); // 设置多路复用比例 oled_send_cmd(handle, 0xA8); oled_send_cmd(handle, 0x3F); // 更多初始化命令... // 最后开启显示 oled_send_cmd(handle, 0xAF); vTaskDelay(100 / portTICK_PERIOD_MS); }实际显示内容时需要先设置显示位置再发送显示数据。我封装了几个常用函数void oled_set_pos(spi_device_handle_t handle, uint8_t x, uint8_t y) { oled_send_cmd(handle, 0xB0 y); oled_send_cmd(handle, ((x 0xF0) 4) | 0x10); oled_send_cmd(handle, x 0x0F); } void oled_clear(spi_device_handle_t handle) { uint8_t buf[128] {0}; for(uint8_t y0; y8; y) { oled_set_pos(handle, 0, y); oled_send_data(handle, buf, 128); } } void oled_show_text(spi_device_handle_t handle, uint8_t x, uint8_t y, char *text) { oled_set_pos(handle, x, y); while(*text) { oled_send_data(handle, font_table[*text - 32], 8); text; } }在实现过程中我发现SSD1306的显示内存是按页组织的每页8行像素。写入数据时要注意这个结构否则显示内容会错位。另外为了提高刷新效率可以尽量减少全屏刷新只更新变化的部分。5. 温湿度传感器数据读取我选择的SHT30是一款高精度数字温湿度传感器支持I2C和SPI接口。在SPI模式下它的通信协议有些特殊之处需要注意。SHT30的命令是16位的高位在前。常用的命令有0x240B: 高重复性测量时钟拉伸禁止0x2C06: 中等重复性测量时钟拉伸禁止读取数据的流程如下发送测量命令等待测量完成(约15ms)读取6字节数据(温度高8位、低8位、CRC8湿度高8位、低8位、CRC8)具体实现代码#define SHT30_MEAS_CMD 0x240B float sht30_read_temp_humi(spi_device_handle_t handle) { uint8_t cmd[2] {SHT30_MEAS_CMD 8, SHT30_MEAS_CMD 0xFF}; uint8_t data[6] {0}; // 发送测量命令 spi_transaction_t t { .length 16, .tx_buffer cmd }; ESP_ERROR_CHECK(spi_device_transmit(handle, t)); // 等待测量完成 vTaskDelay(20 / portTICK_PERIOD_MS); // 读取数据 t.length 6 * 8; t.rx_buffer data; ESP_ERROR_CHECK(spi_device_transmit(handle, t)); // 校验CRC if(!check_crc(data[0], 2, data[2]) || !check_crc(data[3], 2, data[5])) { return -1; // CRC校验失败 } // 计算温度(℃) uint16_t temp_raw (data[0] 8) | data[1]; float temperature -45 175 * (temp_raw / 65535.0); // 计算湿度(%RH) uint16_t humi_raw (data[3] 8) | data[4]; float humidity 100 * (humi_raw / 65535.0); return temperature; }CRC校验函数实现如下uint8_t check_crc(uint8_t *data, uint8_t len, uint8_t crc) { uint8_t i, j; uint8_t crc_val 0xFF; for(i0; ilen; i) { crc_val ^ data[i]; for(j0; j8; j) { if(crc_val 0x80) { crc_val (crc_val 1) ^ 0x31; } else { crc_val 1; } } } return crc_val crc; }在实际使用中我发现SHT30对电源噪声比较敏感。如果电源质量不好测量结果可能会有较大波动。建议在VDD引脚加一个0.1μF的滤波电容。另外连续测量时要注意给传感器足够的休息时间否则内部发热会影响测量精度。6. 多设备协同工作现在我们已经可以分别驱动OLED屏和温湿度传感器了接下来要让它们协同工作。我的设计是每2秒读取一次传感器数据并刷新到屏幕上。首先创建一个任务来处理数据采集和显示void sensor_task(void *arg) { spi_device_handle_t oled_handle ((spi_device_handle_t*)arg)[0]; spi_device_handle_t sht30_handle ((spi_device_handle_t*)arg)[1]; char text_buf[32]; float temp, humi; // 初始化OLED oled_init(oled_handle); oled_clear(oled_handle); while(1) { // 读取温湿度 temp sht30_read_temp_humi(sht30_handle); humi sht30_read_temp_humi(sht30_handle); // 显示温度 snprintf(text_buf, sizeof(text_buf), Temp: %.1f C, temp); oled_show_text(oled_handle, 0, 0, text_buf); // 显示湿度 snprintf(text_buf, sizeof(text_buf), Humi: %.1f %%, humi); oled_show_text(oled_handle, 0, 2, text_buf); // 显示系统运行时间 uint32_t uptime xTaskGetTickCount() * portTICK_PERIOD_MS / 1000; snprintf(text_buf, sizeof(text_buf), Uptime: %lu s, uptime); oled_show_text(oled_handle, 0, 4, text_buf); vTaskDelay(2000 / portTICK_PERIOD_MS); } }然后在主函数中创建任务void app_main() { // 初始化SPI和设备(代码见前面章节) // 创建任务参数 spi_device_handle_t handles[2] {oled_handle, sht30_handle}; xTaskCreate(sensor_task, sensor_task, 4096, handles, 5, NULL); }这个项目让我深刻理解了SPI多设备管理的几个关键点片选信号的控制非常重要每次只能激活一个设备的CS不同设备可能有不同的时钟要求需要分别配置长时间运行时要考虑SPI总线负载避免频繁访问影响系统性能调试时遇到过一个典型问题有时OLED显示会花屏。后来发现是因为在传输过程中被其他任务打断。解决方法是在关键传输段加上互斥锁static SemaphoreHandle_t spi_mutex NULL; // 在app_main中初始化 spi_mutex xSemaphoreCreateMutex(); // 修改传输函数 void oled_send_data(spi_device_handle_t handle, uint8_t *data, uint16_t len) { if(xSemaphoreTake(spi_mutex, portMAX_DELAY) pdTRUE) { spi_transaction_t t { .length len * 8, .tx_buffer data, .user (void*)1 }; ESP_ERROR_CHECK(spi_device_transmit(handle, t)); xSemaphoreGive(spi_mutex); } }经过这些优化后系统运行非常稳定可以长时间工作不出现通信错误。这个项目虽然不大但涵盖了SPI应用的多个关键知识点对理解ESP32-C3的SPI控制器很有帮助。