当前位置: 首页 > news >正文

ESP32-C3开发实战 SPI篇1:驱动OLED屏与温湿度传感器

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片选0
FSPICS1GPIO3片选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));

这里有几个关键点需要注意:

  1. 时钟频率要根据外设支持的最高频率设置,我的OLED屏支持10MHz,而SHT30只需要1MHz
  2. 模式设置必须与外设一致,这两个设备都使用模式0
  3. queue_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 // DC=0表示命令 }; 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 // DC=1表示数据 }; 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 y=0; y<8; 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: 中等重复性测量,时钟拉伸禁止

读取数据的流程如下:

  1. 发送测量命令
  2. 等待测量完成(约15ms)
  3. 读取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(i=0; i<len; i++) { crc_val ^= data[i]; for(j=0; j<8; 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多设备管理的几个关键点:

  1. 片选信号的控制非常重要,每次只能激活一个设备的CS
  2. 不同设备可能有不同的时钟要求,需要分别配置
  3. 长时间运行时要考虑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控制器很有帮助。

http://www.jsqmd.com/news/661359/

相关文章:

  • ASOF JOIN 在金融数据分析中为何关键?pandas merge_asof() 如何实现精准时序匹配?
  • Ostrakon-VL-8B多图对比实战案例:连锁门店陈列优化与促销效果评估
  • 2026年X光安检机厂家推荐:沈阳明翰科技有限公司,小型/双视角/单视角/政府/法院/医院/学校/车站安检机全供应 - 品牌推荐官
  • 2026年堆焊公司权威推荐/带极堆焊机,Tig热丝堆焊,法兰堆焊设备,热丝氩弧堆焊设备,多功能堆焊焊接机 - 品牌策略师
  • 2026年双面胶带厂家推荐:深圳市鸿源涵科技有限公司,PVC/EVA/PET/棉纸等双面胶带全品类供应 - 品牌推荐官
  • IQuest-Coder-V1-40B-Instruct实际作品展示:AI写的代码到底有多强
  • PDF转图片踩坑实录:解决PyMuPDF处理中文PDF乱码、图片模糊的实战经验
  • 2026中国聚合物泵站标杆企业白皮书:从技术研发到全周期服务的价值博弈 - 泵站报价15613348888
  • 5步掌握AssetStudio:Unity游戏资源提取终极指南
  • 2026年小型对辊破碎机厂家推荐:立式对辊破碎机/全自动对辊破碎机/移动鄂式破碎机厂家 - 品牌推荐官
  • 火影手游饰品属性洗练全解析:暴击还是攻击?409%攻击加成阈值背后的战力计算逻辑
  • Verilog函数进阶:从基础function到automatic递归函数的完整指南(含阶乘案例)
  • 从Sensor到屏幕:YUV、RGB与RAW DATA格式的选型实战与性能权衡
  • RabbitMQ快速入门
  • 剑指offer | 2.3 数据结构相关题目
  • AI头像生成器多风格覆盖:Qwen3-32B支持23种细分美术风格Prompt生成
  • OBS多路RTMP推流插件:5大核心技术优势深度解析与实战指南
  • 2026年新房装修设计哪个好,这些品牌值得关注的干货指南 - mypinpai
  • RL4CO完全指南:用强化学习轻松解决复杂组合优化问题
  • Unity AI Navigation保姆级教程:从NavMesh烘焙到角色点击移动,5分钟搞定寻路系统
  • 盒马鲜生卡回收平台推荐:线上回收是否更靠谱? - 团团收购物卡回收
  • ViTables:突破HDF5数据可视化的边界,让十亿级表格触手可及
  • 从安装包到服务自启:Windows下Tomcat 9.0.x的两种部署姿势全解析(.exe vs .zip)
  • 聚焦理工类考生|湖北理工学院,机械工程强势,赋能未来发展 - myqiye
  • 1 5.8 屏幕键盘的使用:键盘坏了/平板触控时的“救命工具”
  • 百度网盘命令行终极指南:如何用BaiduPCS-Go实现高效文件管理
  • PHP避免进程切换开销的庖丁解牛
  • RISC-V DSP扩展指令集实战:如何用P扩展指令优化音频解码性能
  • 嵌入式现代C++工程实践——第14篇:第二次重构 —— 模板登场,编译时绑定端口和引脚
  • 3大实战场景:深度掌握ComfyUI-VideoHelperSuite的视频合成技巧