关键字:ESP32-S3、ESP-IDF v5.5、UART 串口、I2C 总线、SPI 总线、DHT11 温湿度传感器、SSD1306 OLED 显示屏、物联网数据采集 摘要:本文系统讲解 ESP32-S3 最常用的三种串行通信接口 ——UART、I2C 与 SPI 的工作原理与 ESP-IDF v5.5 实现方法。通过四个循序渐进的实战项目,从基础的串口数据回显入手,逐步深入到 DHT11 温湿度传感器数据采集、SSD1306 OLED 显示屏驱动,最终实现 "传感器采集 + 本地显示 + 串口上传" 的完整物联网数据采集系统。文章最后附上10 + 常见问题解答和硬件接线避坑指南,确保初学者零失败完成所有实验。
写在前面
大家好,我是 EmbeddedCore。上一讲我们学习了 GPIO 与基础外设开发,掌握了 LED 控制、按键检测和 PWM 调光技术。从这一讲开始,我们将进入更精彩的通信与传感器世界。
在物联网项目中,数据采集是核心环节。我们需要通过各种通信接口连接传感器,获取环境数据,然后进行处理和传输。ESP32-S3 提供了丰富的通信外设,其中 UART、I2C 和 SPI 是最常用的三种,几乎所有的传感器和外设都通过这三种接口与 MCU 通信。
本讲将从原理讲起,通过完整可运行的代码,带你一步步掌握这三种通信接口的使用方法,并实现一个实用的温湿度数据采集系统。学完本讲,你将能够独立连接和驱动绝大多数常见的传感器和外设。
一、三大串行通信接口对比与选型
在开始学习之前,我们先来对比一下 UART、I2C 和 SPI 三种通信方式的优缺点和适用场景,帮助你在实际项目中做出正确的选择:
| UART | 2 条 (TX/RX) | 慢 (最高几 Mbps) | 远 (可达几十米) | 2 个 (点对点) | 低 | 与电脑、蓝牙模块、GPS 模块通信 |
| I2C | 2 条 (SCL/SDA) | 中 (最高 4Mbps) | 近 (几米) | 最多 127 个 | 中 | 连接传感器、OLED 显示屏、EEPROM |
| SPI | 4 条 (SCK/MOSI/MISO/CS) | 快 (最高几十 Mbps) | 近 (几米) | 多个 (取决于 CS 数量) | 中高 | 连接高速外设:LCD 屏幕、Flash、SD 卡 |
选型建议:
- 简单的点对点通信优先使用 UART
- 连接多个低速外设优先使用 I2C(布线简单)
- 高速数据传输优先使用 SPI
二、UART 串口通信详解
2.1 UART 基本原理
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是一种异步串行通信协议。它不需要时钟信号,而是通过预先约定的波特率来同步数据传输。
UART 通信使用两条信号线:
- TX(Transmit):发送数据线
- RX(Receive):接收数据线
通信时,发送方将并行数据转换为串行数据,通过 TX 线发送;接收方通过 RX 线接收串行数据,然后转换为并行数据。
2.2 UART 核心参数配置
UART 通信需要双方约定相同的参数才能正常通信,核心参数包括:
- 波特率:每秒传输的比特数,常见值有 9600、115200、921600 等
- 数据位:每个数据帧包含的比特数,通常为 8 位
- 停止位:数据帧结束的标志,通常为 1 位
- 校验位:用于错误检测,通常不使用(无校验)
ESP32-S3 有 3 个 UART 控制器(UART0、UART1、UART2),其中 UART0 默认用于与电脑的串口通信(烧录和调试)。
2.3 UART 数据发送与接收
ESP-IDF 提供了简单易用的 UART 操作函数:
// 安装UART驱动
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int queue_size, QueueHandle_t *uart_queue, int intr_alloc_flags);
// 配置UART参数
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
// 设置UART引脚
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num);
// 发送数据
int uart_write_bytes(uart_port_t uart_num, const void *src, size_t size);
// 接收数据
int uart_read_bytes(uart_port_t uart_num, void *buf, size_t length, TickType_t ticks_to_wait);
2.4 实战项目 1:串口数据回显
串口数据回显是最基础的 UART 实验,功能是将电脑通过串口发送给 ESP32-S3 的数据原样返回。
完整代码:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#define UART_PORT UART_NUM_0
#define TX_PIN GPIO_NUM_43 // ESP32-S3默认UART0 TX引脚
#define RX_PIN GPIO_NUM_44 // ESP32-S3默认UART0 RX引脚
#define BUF_SIZE 1024
void uart_init(void)
{
// 配置UART参数
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB
};
uart_param_config(UART_PORT, &uart_config);
// 设置UART引脚
uart_set_pin(UART_PORT, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 安装UART驱动
uart_driver_install(UART_PORT, BUF_SIZE, BUF_SIZE, 0, NULL, 0);
}
void app_main(void)
{
uart_init();
uint8_t data[BUF_SIZE];
while(1) {
// 读取串口数据
int len = uart_read_bytes(UART_PORT, data, BUF_SIZE – 1, pdMS_TO_TICKS(100));
if(len > 0) {
data[len] = '\\0';
printf("Received: %s", data);
// 将数据原样返回
uart_write_bytes(UART_PORT, data, len);
}
}
}
实验步骤:
三、I2C 总线通信详解
3.1 I2C 基本原理
I2C(Inter-Integrated Circuit,集成电路总线)是一种同步串行通信协议,由飞利浦公司发明。它使用两条信号线:
- SCL(Serial Clock):时钟线,由主机提供
- SDA(Serial Data):数据线,用于传输数据
I2C 总线支持多主机和多从机架构,每个从机都有一个唯一的 7 位地址。通信总是由主机发起,从机根据地址响应。
I2C 通信的基本流程:
3.2 ESP32-S3 I2C 控制器
ESP32-S3 有 2 个 I2C 控制器(I2C0 和 I2C1),都可以配置为主机或从机模式。作为主机时,最高支持 4MHz 的时钟频率。
3.3 I2C 数据读写操作
ESP-IDF 提供了两种 I2C 数据读写方式:
常用 I2C 函数:
// 配置I2C参数
esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
// 安装I2C驱动
esp_err_t i2c_driver_install(i2c_port_t i2c_num, i2c_mode_t mode, size_t slv_rx_buf_len, size_t slv_tx_buf_len, int intr_alloc_flags);
// 写入数据到I2C从机
esp_err_t i2c_master_write_to_device(i2c_port_t i2c_num, uint8_t device_address, const uint8_t *write_buffer, size_t write_size, TickType_t ticks_to_wait);
// 从I2C从机读取数据
esp_err_t i2c_master_read_from_device(i2c_port_t i2c_num, uint8_t device_address, uint8_t *read_buffer, size_t read_size, TickType_t ticks_to_wait);
3.4 实战项目 2:SSD1306 OLED 显示屏驱动
SSD1306 是一款非常流行的 OLED 显示屏控制器,通常通过 I2C 接口与 MCU 通信。我们将使用 ESP32-S3 驱动一块 0.96 英寸 128×64 分辨率的 SSD1306 OLED 显示屏。
硬件接线:
| VCC | 3.3V |
| GND | GND |
| SCL | GPIO8 |
| SDA | GPIO9 |
完整代码: 首先创建ssd1306.h头文件:
#ifndef SSD1306_H
#define SSD1306_H
#include "driver/i2c.h"
#define SSD1306_I2C_ADDR 0x3C // OLED默认I2C地址
#define SSD1306_WIDTH 128
#define SSD1306_HEIGHT 64
void ssd1306_init(void);
void ssd1306_clear_screen(void);
void ssd1306_set_pixel(uint8_t x, uint8_t y, uint8_t color);
void ssd1306_set_string(uint8_t line, uint8_t col, const char *str);
void ssd1306_refresh(void);
#endif
然后创建ssd1306.c驱动文件:
#include "ssd1306.h"
#include "font.h" // 字库文件,文末提供下载
static uint8_t ssd1306_buffer[SSD1306_WIDTH * SSD1306_HEIGHT / 8];
static void ssd1306_write_cmd(uint8_t cmd)
{
uint8_t data[2] = {0x00, cmd};
i2c_master_write_to_device(I2C_NUM_0, SSD1306_I2C_ADDR, data, 2, pdMS_TO_TICKS(100));
}
static void ssd1306_write_data(uint8_t data)
{
uint8_t buf[2] = {0x40, data};
i2c_master_write_to_device(I2C_NUM_0, SSD1306_I2C_ADDR, buf, 2, pdMS_TO_TICKS(100));
}
void ssd1306_init(void)
{
// 初始化命令序列
ssd1306_write_cmd(0xAE); // 关闭显示
ssd1306_write_cmd(0xD5); // 设置时钟分频因子
ssd1306_write_cmd(0x80);
ssd1306_write_cmd(0xA8); // 设置驱动路数
ssd1306_write_cmd(0x3F);
ssd1306_write_cmd(0xD3); // 设置显示偏移
ssd1306_write_cmd(0x00);
ssd1306_write_cmd(0x40); // 设置显示开始行
ssd1306_write_cmd(0x8D); // 电荷泵设置
ssd1306_write_cmd(0x14);
ssd1306_write_cmd(0x20); // 设置内存地址模式
ssd1306_write_cmd(0x00);
ssd1306_write_cmd(0xA1); // 段重映射设置
ssd1306_write_cmd(0xC8); // COM扫描方向
ssd1306_write_cmd(0xDA); // 设置COM硬件引脚配置
ssd1306_write_cmd(0x12);
ssd1306_write_cmd(0x81); // 对比度设置
ssd1306_write_cmd(0xCF);
ssd1306_write_cmd(0xD9); // 设置预充电周期
ssd1306_write_cmd(0xF1);
ssd1306_write_cmd(0xDB); // 设置VCOMH取消选择级别
ssd1306_write_cmd(0x30);
ssd1306_write_cmd(0xA4); // 全局显示开启
ssd1306_write_cmd(0xA6); // 设置显示方式
ssd1306_write_cmd(0xAF); // 开启显示
ssd1306_clear_screen();
ssd1306_refresh();
}
void ssd1306_clear_screen(void)
{
for(int i = 0; i < sizeof(ssd1306_buffer); i++) {
ssd1306_buffer[i] = 0x00;
}
}
void ssd1306_set_pixel(uint8_t x, uint8_t y, uint8_t color)
{
if(x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) return;
if(color) {
ssd1306_buffer[x + (y / 8) * SSD1306_WIDTH] |= (1 << (y % 8));
} else {
ssd1306_buffer[x + (y / 8) * SSD1306_WIDTH] &= ~(1 << (y % 8));
}
}
void ssd1306_set_string(uint8_t line, uint8_t col, const char *str)
{
while(*str) {
for(int i = 0; i < 8; i++) {
uint8_t data = font8x8[*str – ' '][i];
for(int j = 0; j < 8; j++) {
ssd1306_set_pixel(col * 8 + j, line * 8 + i, (data >> j) & 0x01);
}
}
col++;
str++;
}
}
void ssd1306_refresh(void)
{
for(int i = 0; i < 8; i++) {
ssd1306_write_cmd(0xB0 + i); // 设置页地址
ssd1306_write_cmd(0x00); // 设置列地址低4位
ssd1306_write_cmd(0x10); // 设置列地址高4位
for(int j = 0; j < SSD1306_WIDTH; j++) {
ssd1306_write_data(ssd1306_buffer[i * SSD1306_WIDTH + j]);
}
}
}
最后是主程序main.c:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c.h"
#include "ssd1306.h"
#define I2C_MASTER_SCL_IO GPIO_NUM_8
#define I2C_MASTER_SDA_IO GPIO_NUM_9
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 100000
void i2c_master_init(void)
{
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ
};
i2c_param_config(I2C_MASTER_NUM, &conf);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
void app_main(void)
{
i2c_master_init();
ssd1306_init();
ssd1306_clear_screen();
ssd1306_set_string(0, 0, "ESP32-S3 Demo");
ssd1306_set_string(2, 0, "I2C OLED Test");
ssd1306_set_string(4, 0, "Hello World!");
ssd1306_refresh();
while(1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
实验步骤:
四、DHT11 温湿度传感器数据采集
4.1 DHT11 传感器介绍
DHT11 是一款常用的温湿度传感器,它内部集成了一个电阻式感湿元件和一个 NTC 测温元件,并与一个 8 位单片机相连。DHT11 通过单总线协议与 MCU 通信,只需要一根数据线。
DHT11 的技术参数:
- 温度测量范围:0~50℃,精度 ±2℃
- 湿度测量范围:20%~90% RH,精度 ±5% RH
- 采样周期:1 秒
4.2 DHT11 通信协议
DHT11 使用单总线协议,通信流程如下:
4.3 实战项目 3:DHT11 温湿度数据采集
硬件接线:
| VCC | 3.3V |
| GND | GND |
| DATA | GPIO4 |
完整代码:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "rom/ets_sys.h"
#define DHT11_PIN GPIO_NUM_4
static void dht11_reset(void)
{
gpio_set_direction(DHT11_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(DHT11_PIN, 0);
ets_delay_us(20000); // 拉低至少18ms
gpio_set_level(DHT11_PIN, 1);
ets_delay_us(30); // 拉高20~40us
}
static int dht11_check_response(void)
{
gpio_set_direction(DHT11_PIN, GPIO_MODE_INPUT);
int retry = 0;
while(gpio_get_level(DHT11_PIN) == 1 && retry < 100) {
retry++;
ets_delay_us(1);
}
if(retry >= 100) return -1;
retry = 0;
while(gpio_get_level(DHT11_PIN) == 0 && retry < 100) {
retry++;
ets_delay_us(1);
}
if(retry >= 100) return -1;
retry = 0;
while(gpio_get_level(DHT11_PIN) == 1 && retry < 100) {
retry++;
ets_delay_us(1);
}
if(retry >= 100) return -1;
return 0;
}
static uint8_t dht11_read_byte(void)
{
uint8_t data = 0;
for(int i = 0; i < 8; i++) {
while(gpio_get_level(DHT11_PIN) == 0);
ets_delay_us(40);
if(gpio_get_level(DHT11_PIN) == 1) {
data |= (1 << (7 – i));
}
while(gpio_get_level(DHT11_PIN) == 1);
}
return data;
}
int dht11_read_data(float *temperature, float *humidity)
{
uint8_t buf[5];
dht11_reset();
if(dht11_check_response() != 0) {
return -1;
}
for(int i = 0; i < 5; i++) {
buf[i] = dht11_read_byte();
}
if(buf[0] + buf[1] + buf[2] + buf[3] != buf[4]) {
return -1;
}
*humidity = buf[0] + buf[1] * 0.1;
*temperature = buf[2] + buf[3] * 0.1;
return 0;
}
void app_main(void)
{
gpio_reset_pin(DHT11_PIN);
float temperature, humidity;
while(1) {
if(dht11_read_data(&temperature, &humidity) == 0) {
printf("Temperature: %.1f C, Humidity: %.1f %%RH\\n", temperature, humidity);
} else {
printf("Failed to read DHT11 data\\n");
}
vTaskDelay(pdMS_TO_TICKS(2000)); // DHT11采样周期至少1秒
}
}
实验步骤:
五、综合实战:温湿度数据采集 + OLED 显示 + 串口上传
现在我们将前面三个实战项目结合起来,实现一个完整的温湿度数据采集系统:
完整代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c.h"
#include "driver/gpio.h"
#include "rom/ets_sys.h"
#include "ssd1306.h"
#define I2C_MASTER_SCL_IO GPIO_NUM_8
#define I2C_MASTER_SDA_IO GPIO_NUM_9
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 100000
#define DHT11_PIN GPIO_NUM_4
void i2c_master_init(void)
{
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ
};
i2c_param_config(I2C_MASTER_NUM, &conf);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
}
// DHT11驱动代码(同上,此处省略)
static void dht11_reset(void) { … }
static int dht11_check_response(void) { … }
static uint8_t dht11_read_byte(void) { … }
int dht11_read_data(float *temperature, float *humidity) { … }
void app_main(void)
{
i2c_master_init();
ssd1306_init();
gpio_reset_pin(DHT11_PIN);
float temperature, humidity;
char buf[32];
ssd1306_clear_screen();
ssd1306_set_string(0, 0, "ESP32-S3");
ssd1306_set_string(1, 0, "温湿度监测系统");
ssd1306_refresh();
vTaskDelay(pdMS_TO_TICKS(2000));
while(1) {
if(dht11_read_data(&temperature, &humidity) == 0) {
// 串口输出
printf("Temperature: %.1f C, Humidity: %.1f %%RH\\n", temperature, humidity);
// OLED显示
ssd1306_clear_screen();
ssd1306_set_string(0, 0, "温湿度监测");
sprintf(buf, "温度: %.1f C", temperature);
ssd1306_set_string(2, 0, buf);
sprintf(buf, "湿度: %.1f %%RH", humidity);
ssd1306_set_string(4, 0, buf);
ssd1306_refresh();
} else {
printf("Failed to read DHT11 data\\n");
ssd1306_clear_screen();
ssd1306_set_string(0, 0, "读取失败");
ssd1306_refresh();
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
实验效果:
- OLED 显示屏上显示 "温湿度监测" 标题和实时的温度、湿度数据
- 串口监控中每隔 2 秒打印一次温湿度数据
- 用手握住 DHT11 传感器,你会看到温度和湿度数值发生变化
六、SPI 总线通信简介
SPI(Serial Peripheral Interface,串行外设接口)是一种高速同步串行通信协议,由摩托罗拉公司发明。它使用四条信号线:
- SCK(Serial Clock):时钟线,由主机提供
- MOSI(Master Output, Slave Input):主机输出 / 从机输入
- MISO(Master Input, Slave Output):主机输入 / 从机输出
- CS(Chip Select):片选线,用于选择从机
SPI 总线支持全双工通信,传输速度非常快,最高可达几十 Mbps。ESP32-S3 有 3 个 SPI 控制器(SPI0、SPI1、SPI2、SPI3),其中 SPI0 和 SPI1 用于连接内部 Flash 和 PSRAM,SPI2 和 SPI3 可以用于连接外部外设。
由于篇幅限制,本讲不详细讲解 SPI 的使用方法,我们将在后续讲解 LCD 显示屏和 SD 卡时再深入介绍。
七、常见问题解答与避坑指南
7.1 串口乱码怎么办?
解决方案:
7.2 I2C 通信失败怎么办?
解决方案:
7.3 OLED 不显示怎么办?
解决方案:
7.4 DHT11 数据读取失败怎么办?
解决方案:
7.5 如何查找 I2C 设备地址?
解决方案:使用以下 I2C 扫描程序:
void i2c_scan(void)
{
printf("Scanning I2C bus…\\n");
for(uint8_t addr = 1; addr < 127; addr++) {
esp_err_t ret = i2c_master_write_to_device(I2C_NUM_0, addr, NULL, 0, pdMS_TO_TICKS(100));
if(ret == ESP_OK) {
printf("I2C device found at address 0x%02X\\n", addr);
}
}
printf("Scan complete\\n");
}
八、课后作业
九、下期预告
第 5 讲将深入学习FreeRTOS 实时操作系统,这是 ESP-IDF 的核心。我们将详细讲解任务创建与管理、消息队列、信号量、互斥锁等核心概念,并通过实战项目演示如何使用 FreeRTOS 实现多任务并发处理,让你的 ESP32-S3 项目更加高效和稳定。
写在最后
串口通信和传感器数据采集是物联网开发的基础技能,几乎所有的物联网项目都会用到。希望通过本讲的学习,你能够熟练掌握 UART、I2C 和 SPI 三种通信接口的使用方法,并能够独立连接和驱动常见的传感器和外设。
本讲用到的font.h字库文件和完整工程代码,我已经上传到了我的 GitHub 仓库,大家可以在评论区留言获取。如果你在学习过程中遇到任何问题,欢迎在评论区留言讨论。
点赞收藏不迷路,关注我,带你从零基础成为 ESP32-S3 开发高手!
