)
写在前面大家好我是 EmbeddedCore。上一讲我们学习了 GPIO 与基础外设开发掌握了 LED 控制、按键检测和 PWM 调光技术。从这一讲开始我们将进入更精彩的通信与传感器世界。在物联网项目中数据采集是核心环节。我们需要通过各种通信接口连接传感器获取环境数据然后进行处理和传输。ESP32-S3 提供了丰富的通信外设其中 UART、I2C 和 SPI 是最常用的三种几乎所有的传感器和外设都通过这三种接口与 MCU 通信。本讲将从原理讲起通过完整可运行的代码带你一步步掌握这三种通信接口的使用方法并实现一个实用的温湿度数据采集系统。学完本讲你将能够独立连接和驱动绝大多数常见的传感器和外设。一、三大串行通信接口对比与选型在开始学习之前我们先来对比一下 UART、I2C 和 SPI 三种通信方式的优缺点和适用场景帮助你在实际项目中做出正确的选择通信方式信号线数量传输速度通信距离设备数量复杂度适用场景UART2 条 (TX/RX)慢 (最高几 Mbps)远 (可达几十米)2 个 (点对点)低与电脑、蓝牙模块、GPS 模块通信I2C2 条 (SCL/SDA)中 (最高 4Mbps)近 (几米)最多 127 个中连接传感器、OLED 显示屏、EEPROMSPI4 条 (SCK/MOSI/MISO/CS)快 (最高几十 Mbps)近 (几米)多个 (取决于 CS 数量)中高连接高速外设LCD 屏幕、Flash、SD 卡选型建议简单的点对点通信优先使用 UART连接多个低速外设优先使用 I2C布线简单高速数据传输优先使用 SPI二、UART 串口通信详解2.1 UART 基本原理UARTUniversal Asynchronous Receiver/Transmitter通用异步收发器是一种异步串行通信协议。它不需要时钟信号而是通过预先约定的波特率来同步数据传输。UART 通信使用两条信号线TXTransmit发送数据线RXReceive接收数据线通信时发送方将并行数据转换为串行数据通过 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); } } }实验步骤编译烧录代码到 ESP32-S3 开发板打开串口监控工具如 VSCode 串口监控、串口助手设置波特率为 115200数据位 8停止位 1无校验在发送框中输入任意字符点击发送你将看到 ESP32-S3 原样返回你发送的字符三、I2C 总线通信详解3.1 I2C 基本原理I2CInter-Integrated Circuit集成电路总线是一种同步串行通信协议由飞利浦公司发明。它使用两条信号线SCLSerial Clock时钟线由主机提供SDASerial Data数据线用于传输数据I2C 总线支持多主机和多从机架构每个从机都有一个唯一的 7 位地址。通信总是由主机发起从机根据地址响应。I2C 通信的基本流程主机发送起始信号主机发送从机地址 读写位从机发送应答信号主机或从机发送数据每发送一个字节接收方发送应答信号主机发送停止信号3.2 ESP32-S3 I2C 控制器ESP32-S3 有 2 个 I2C 控制器I2C0 和 I2C1都可以配置为主机或从机模式。作为主机时最高支持 4MHz 的时钟频率。3.3 I2C 数据读写操作ESP-IDF 提供了两种 I2C 数据读写方式简易 API适用于简单的单字节读写命令链接 API适用于复杂的多字节读写常用 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 实战项目 2SSD1306 OLED 显示屏驱动SSD1306 是一款非常流行的 OLED 显示屏控制器通常通过 I2C 接口与 MCU 通信。我们将使用 ESP32-S3 驱动一块 0.96 英寸 128x64 分辨率的 SSD1306 OLED 显示屏。硬件接线OLED 引脚ESP32-S3 引脚VCC3.3VGNDGNDSCLGPIO8SDAGPIO9完整代码 首先创建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)); } }实验步骤按照硬件接线表连接 OLED 显示屏将ssd1306.h、ssd1306.c和font.h文件添加到工程的main目录下修改main/CMakeLists.txt文件将ssd1306.c添加到编译列表中编译烧录代码你将看到 OLED 显示屏上显示出文字四、DHT11 温湿度传感器数据采集4.1 DHT11 传感器介绍DHT11 是一款常用的温湿度传感器它内部集成了一个电阻式感湿元件和一个 NTC 测温元件并与一个 8 位单片机相连。DHT11 通过单总线协议与 MCU 通信只需要一根数据线。DHT11 的技术参数温度测量范围0~50℃精度 ±2℃湿度测量范围20%~90% RH精度 ±5% RH采样周期1 秒4.2 DHT11 通信协议DHT11 使用单总线协议通信流程如下主机发送起始信号拉低总线至少 18ms然后拉高 20~40usDHT11 发送响应信号拉低总线 80us然后拉高 80usDHT11 发送 40 位数据5 字节湿度整数、湿度小数、温度整数、温度小数、校验和数据发送完毕后DHT11 拉低总线 50us然后释放总线4.3 实战项目 3DHT11 温湿度数据采集硬件接线DHT11 引脚ESP32-S3 引脚VCC3.3VGNDGNDDATAGPIO4完整代码#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秒 } }实验步骤按照硬件接线表连接 DHT11 传感器编译烧录代码打开串口监控你将看到每隔 2 秒打印一次温湿度数据五、综合实战温湿度数据采集 OLED 显示 串口上传现在我们将前面三个实战项目结合起来实现一个完整的温湿度数据采集系统每隔 2 秒读取一次 DHT11 温湿度数据将数据显示在 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 总线通信简介SPISerial Peripheral Interface串行外设接口是一种高速同步串行通信协议由摩托罗拉公司发明。它使用四条信号线SCKSerial Clock时钟线由主机提供MOSIMaster Output, Slave Input主机输出 / 从机输入MISOMaster Input, Slave Output主机输入 / 从机输出CSChip Select片选线用于选择从机SPI 总线支持全双工通信传输速度非常快最高可达几十 Mbps。ESP32-S3 有 3 个 SPI 控制器SPI0、SPI1、SPI2、SPI3其中 SPI0 和 SPI1 用于连接内部 Flash 和 PSRAMSPI2 和 SPI3 可以用于连接外部外设。由于篇幅限制本讲不详细讲解 SPI 的使用方法我们将在后续讲解 LCD 显示屏和 SD 卡时再深入介绍。七、常见问题解答与避坑指南7.1 串口乱码怎么办解决方案确保串口监控工具的波特率、数据位、停止位、校验位与代码中配置的一致检查 USB 线是否接触良好降低波特率试试7.2 I2C 通信失败怎么办解决方案检查 SCL 和 SDA 引脚是否接反确保设备地址正确可以使用 I2C 扫描程序查找设备地址检查设备是否上电降低 I2C 时钟频率试试7.3 OLED 不显示怎么办解决方案检查接线是否正确检查 OLED 的 I2C 地址是否正确有些 OLED 的地址是 0x3D确保 OLED 的 VCC 接的是 3.3V不要接 5V检查初始化命令是否正确7.4 DHT11 数据读取失败怎么办解决方案检查 DATA 引脚是否接对确保 DHT11 的 VCC 接的是 3.3V不要接 5V增加延时时间DHT11 的采样周期至少 1 秒检查 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); }八、课后作业基础练习实现串口数据回显功能进阶练习读取 DHT11 温湿度传感器数据综合练习在 OLED 显示屏上显示温湿度数据挑战练习实现通过串口发送命令控制 OLED 显示内容的功能九、下期预告第 5 讲将深入学习FreeRTOS 实时操作系统这是 ESP-IDF 的核心。我们将详细讲解任务创建与管理、消息队列、信号量、互斥锁等核心概念并通过实战项目演示如何使用 FreeRTOS 实现多任务并发处理让你的 ESP32-S3 项目更加高效和稳定。写在最后串口通信和传感器数据采集是物联网开发的基础技能几乎所有的物联网项目都会用到。希望通过本讲的学习你能够熟练掌握 UART、I2C 和 SPI 三种通信接口的使用方法并能够独立连接和驱动常见的传感器和外设。本讲用到的font.h字库文件和完整工程代码我已经上传到了我的 GitHub 仓库大家可以在评论区留言获取。如果你在学习过程中遇到任何问题欢迎在评论区留言讨论。点赞收藏不迷路关注我带你从零基础成为 ESP32-S3 开发高手