4esp32s3-uart串口
一、串口原理
esp32s3三个串口外设
UART0 TX-IO43 RX-IO44
UART1 TX-IO17 TX-IO18
UART2 TX-IO7 TX-IO8
ESP32S3串口独特点
1.ESP32S3芯片内部有个交换矩阵,任意引脚都可以配置成uart0、1
2.引导到main函数之前,IDF框架把串口0已经初始化好
3.已经做好printf的重定向 (可以直接用printf函数打印内容在串口显示)
参数配置
波特率 115200
数据位长度 8
停止位 1
奇偶校验位 0
ESP32-UART基本框图

二、代码编写
1.使用串口1普通收发数据
创建文件
bsp组件 bsp_uart.c bsp_uart.h
task线程 task_uart.c task_uart.h
主要记住修改CMakeLists文件
uart初始化配置
uart_config_t 结构体初始化
uart_param_config 填充结构体参数
uart_set_pin 设置外设号+引脚
uart_driver_install 注册驱动、分配缓冲区等
收发数据
uart_write_bytes 直接调用即可
uart_read_bytes:
(1)阻塞式单字节接收
uint8_t data; while (1) { uart_read_bytes(UART_NUM_1, &data, 1, portMAX_DELAY); // 一直等 // 处理收到的 data }(2)带超时的n字节读取(定时轮询)
int len = uart_read_bytes(UART_NUM_1, buf, sizeof(buf), 10 / portTICK_PERIOD_MS); if (len > 0) { buf[len] = '\0'; ESP_LOGI(TAG, "len=%d, UART1 Recv: %s", len, buf); }
代码
#include "bsp_uart.h"
/**
* @brief UART1 初始化函数
* @note 配置 UART1 的波特率、数据位、校验位、停止位等参数,并安装驱动
* @param 无
* @retval 无
*/
void bsp_uart1_init(void)
{
// UART1 配置结构体
uart_config_t uart_config = {
.baud_rate = 115200, // 波特率:115200
.data_bits = UART_DATA_8_BITS, // 数据位:8位
.parity = UART_PARITY_DISABLE, // 校验位:无校验
.stop_bits = UART_STOP_BITS_1, // 停止位:1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 流控:关闭硬件流控
.source_clk = UART_SCLK_APB, // 时钟源:APB总线时钟
};
// 配置 UART 参数
ESP_ERROR_CHECK(uart_param_config(UART1_PORT, &uart_config));
// 设置 UART 引脚(TX、RX,RTS和CTS不更改)
ESP_ERROR_CHECK(uart_set_pin(UART1_PORT, UART1_TX_PIN, UART1_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
// 安装 UART 驱动,接收缓冲区大小1024,发送缓冲区大小0,不使用事件队列
ESP_ERROR_CHECK(uart_driver_install(UART1_PORT, 1024, 0, 0, NULL, 0));
}
#include "task_uart.h"
// 日志标签,用于 ESP_LOGI 等日志输出时标识来源为 UART1
static const char *TAG = "UART1";
/**
* @brief UART1 任务函数
* @note 初始化 UART1,循环检测接收缓冲区数据,读取后将数据以十六进制格式回传
* @param RXParameters 任务参数(未使用)
* @retval 无
*/
void task_uart1(void *RXParameters)
{
uint8_t data[20]; // 数据接收缓冲区,最多存放20字节
char buff[128]; // 字符串格式化缓冲区
size_t ringbuf_len = 0; // 接收缓冲区当前数据长度
// ESP_LOGI(TAG, "UART1 initialized successfully.");
bsp_uart1_init(); // 初始化 UART1 硬件
while (1)
{
// 获取 UART1 接收缓冲区中的数据长度
uart_get_buffered_data_len(UART1_PORT, &ringbuf_len);
if (ringbuf_len > 0)
{
ESP_LOGI(TAG, "ringbuf_len: %d", ringbuf_len);
// 从接收缓冲区读取数据,最多读取 ringbuf_len 字节,超时 10ms
int len = uart_read_bytes(UART1_PORT, &data, ringbuf_len, 10 / portTICK_PERIOD_MS);
if (len > 0)
{
ESP_LOGI(TAG, "LEN: %d", len);
// 逐个处理接收到的字节,并回传十六进制格式的数据
for (size_t i = 0; i < len; i++)
{
sprintf(buff, "Received data: 0x%x\r\n", data[i]);
uart_write_bytes(UART1_PORT, buff, strlen(buff));
}
}
}
// 任务延时 10ms,避免占用过多 CPU
vTaskDelay(pdMS_TO_TICKS(10));
/******** 阻塞接收一个字节(已废弃示例代码)******
* 以下代码为阻塞方式接收单字节的示例,当前未使用:
* 根据接收到的数据控制 LED 状态,并将数据回传
*
* if(data==0x0)
* {
* gpio_set_level(LED_GPIO,0);
* printf("Received data: %x\n", data);
* }
* else if(data==0x1)
* {
* gpio_set_level(LED_GPIO,1);
* printf("Received data: %x\n", data);
* }
*
* sprintf(buff,"Received data: %x\n", data);
* uart_write_bytes(UART1_PORT, buff, strlen(buff));
**************************************************/
}
}
/**
* @brief 创建 UART1 任务
* @note 使用 FreeRTOS 的 xTaskCreate 创建 task_uart1 任务
* @param 无
* @retval 返回任务创建结果,pdPASS 表示成功
*/
BaseType_t bsp_uart1_task_create(void)
{
BaseType_t ret = xTaskCreate(task_uart1, "task_uart1", 2048, NULL, 5, NULL);
return ret;
}
2.协议包
组成
(1)包头
0xAA 占一个字节长度 (接收代码能识别的数据包的开始)
(2)数据长度
LEN 占一个字节长度(知道协议包里有几个字节的真实数据)
(3)数据
N Bytes 占N个字节长度 (真正传输的数据)
(4)校验和Byte 占一个字节长度;取数据和的最后一个字节 (防止数据传输出错)
解析方法
1.一个字节、一个字节的接收数据
2.利用标志位、状态机控制方法
3.组包校验数据是否符合协议包协议:
(1)找头
(2)知道数据长度
(3)保存有效数据
(4)校验有效数据的和
4.提取有用数据
代码
#include "bsp_uart.h"
/**
* @brief 初始化UART1接口
*
* 该函数配置并初始化ESP32的UART1接口,设置波特率、数据位、停止位等参数,
* 并安装UART驱动程序。此函数应在使用UART功能之前调用。
*
* 配置参数:
* - 波特率:115200 bps
* - 数据位:8位
* - 校验位:无
* - 停止位:1位
* - 流控:禁用
* - 时钟源:默认
* - TX引脚:GPIO17
* - RX引脚:GPIO18
*/
void bsp_uart1_init(void)
{
// 配置UART参数结构体
uart_config_t uart_config = {
.baud_rate = 115200, // 波特率设置为115200 bps
.data_bits = UART_DATA_8_BITS, // 数据位:8位
.parity = UART_PARITY_DISABLE, // 校验位:禁用(无校验)
.stop_bits = UART_STOP_BITS_1, // 停止位:1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 流控:禁用硬件流控
.source_clk = UART_SCLK_DEFAULT, // 时钟源:使用默认时钟
};
// 配置UART参数,如果出错会触发错误检查机制
// UART_PORT在头文件中定义为UART_NUM_1
ESP_ERROR_CHECK(uart_param_config(UART_PORT, &uart_config));
// 设置UART引脚分配
// TX引脚:GPIO17 (UART_TX_PIN)
// RX引脚:GPIO18 (UART_RX_PIN)
// RTS和CTS引脚:不使用 (UART_PIN_NO_CHANGE)
ESP_ERROR_CHECK(uart_set_pin(UART_PORT, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
// 安装UART驱动程序
// 参数说明:
// - UART端口:UART_PORT (UART_NUM_1)
// - 接收缓冲区大小:256字节
// - 发送缓冲区大小:0 (使用默认值)
// - 事件队列大小:0 (不使用事件队列)
// - 中断分配标志:NULL (使用默认值)
// - 中断标志:0 (使用默认值)
ESP_ERROR_CHECK(uart_driver_install(UART_PORT, 256, 0, 0, NULL, 0));
}
#include "task_uart.h"
static const char *TAG = "UART1"; // 日志标签
static proto_state_t state = STATE_WAIT_HEAD; // 当前协议解析状态
static uint8_t frame_buf[FRAME_MAX_LEN]; // 帧数据缓冲区
static uint16_t data_len = 0; // 数据长度
static uint16_t data_cnt = 0; // 已接收数据计数
/**
* @brief UART1任务函数
*
* 此任务负责初始化UART并持续监听串口数据。当接收到数据时,
* 将数据传递给协议解析函数进行处理。
*
* @param pvParameters 任务参数(未使用)
*/
void uart1_task(void *pvParameters)
{
uint8_t data;
bsp_uart1_init(); // 初始化UART1硬件
ESP_LOGI(TAG, "uart1_task start\n");
while (1)
{
// 从UART端口读取一个字节数据,阻塞等待
// portMAX_DELAY表示无限期等待直到有数据到达
int ret = uart_read_bytes(UART_PORT, &data, 1, portMAX_DELAY);
if (ret > 0) {
// 如果读取成功,则将数据传入协议解析函数进行处理
proto_parse_byte(data);
}
}
}
/**
* @brief 创建UART1任务
*
* 使用FreeRTOS的xTaskCreate函数创建一个新的任务来处理UART通信。
*
* @return BaseType_t 任务创建结果,pdPASS表示成功,其他值表示失败
*/
BaseType_t uart1_task_create(void)
{
// 创建UART1任务:
// - 任务函数:uart1_task
// - 任务名称:"uart1_task"
// - 栈深度:4096字节
// - 任务参数:NULL(不传递参数)
// - 任务优先级:10(数字越大优先级越高)
// - 任务句柄:NULL(不需要保存任务句柄)
return xTaskCreate(uart1_task, "uart1_task", 4096, NULL, 10, NULL);
}
/**
* @brief 计算数据校验和
*
* 使用简单的累加求和算法计算数据块的校验和。
*
* @param data 指向要计算校验和的数据的指针
* @param len 数据长度(字节数)
* @return uint8_t 校验和结果(只保留低8位)
*/
uint8_t checksum_sum(const uint8_t *data, uint16_t len)
{
uint16_t sum = 0;
// 遍历所有数据字节并累加
for (int i = 0; i < len; i++) {
sum += data[i];
}
// 只保留低8位作为校验和结果
return (uint8_t)(sum & 0xFF);
}
/**
* @brief 处理完整帧数据
*
* 解析并执行来自上位机的完整命令帧。
* 目前支持的命令:
* - 0x01: 控制LED开关
*
* @param data 指向帧数据的指针(不包含帧头、长度和校验字段)
* @param len 数据长度
*/
void process_frame(const uint8_t *data, uint16_t len)
{
// 至少需要包含命令字和参数值两个字节
if (len < 2) {
ESP_LOGW(TAG, "帧长度不足");
return;
}
ESP_LOGI(TAG, "收到完整帧:");
uint8_t cmd = data[0]; // 提取命令字(第一个字节)
uint8_t val = data[1]; // 提取参数值(第二个字节)
char buf[20] = {0}; // 发送响应数据的缓冲区
// 根据命令字处理不同指令
switch (cmd)
{
case 0x01: // 控制 LED 命令
// 根据参数值控制LED状态
if (val == 0x00) {
// 参数0x00:关闭LED(注意硬件连接可能为低电平有效)
bsp_gpio_set_level(LED_GPIO, 0);
ESP_LOGI(TAG, "LED 已打开");
} else if (val == 0x01)
{
// 参数0x01:打开LED(注意硬件连接可能为高电平有效)
bsp_gpio_set_level(LED_GPIO, 1);
ESP_LOGI(TAG, "LED 已关闭");
} else {
ESP_LOGW(TAG, "无效的LED控制参数: 0x%02X", val);
}
// 向上位机发送确认响应
uart_write_bytes(UART_PORT, "UART1 Recv Data:", strlen("UART1 Recv Data:"));
sprintf(buf, "0x%X 0x%X", cmd, val);
uart_write_bytes(UART_PORT, buf, strlen(buf));
uart_write_bytes(UART_PORT, "\n", 1);
// 退出switch语句,结束本次命令处理
break;
default:
// 不支持的命令
ESP_LOGW(TAG, "未知命令: 0x%02X", cmd);
// 退出switch语句
break;
}
}
/**
* @brief 协议解析函数,每接收一个字节调用一次
*
* 使用有限状态机(FSM)解析自定义串口协议数据帧。
* 帧格式:帧头(1字节) + 长度(1字节) + 数据(N字节) + 校验和(1字节)
*
* 状态转换过程:
* 1. STATE_WAIT_HEAD: 等待帧头(0xAA)
* 2. STATE_WAIT_LEN: 等待数据长度字节
* 3. STATE_WAIT_DATA: 接收指定长度的数据
* 4. STATE_WAIT_CHECKSUM: 等待并验证校验和
*
* @param byte 接收到的单个字节数据
*/
void proto_parse_byte(uint8_t byte)
{
// 根据当前状态处理接收到的字节
switch (state)
{
case STATE_WAIT_HEAD:
// 等待帧头状态
// 检查是否为有效的帧头标识
if (byte == FRAME_HEAD) {
// 收到正确帧头,进入等待长度字段状态
state = STATE_WAIT_LEN;
}
// 如果不是帧头,则继续保持在此状态,丢弃该字节
// 退出当前case分支
break;
case STATE_WAIT_LEN:
// 等待长度字段状态
data_len = byte; // 保存后续数据的长度
data_cnt = 0; // 重置数据接收计数器
// 检查长度是否超出最大允许值
if (data_len > FRAME_MAX_LEN) {
// 长度超限,回到等待帧头状态
ESP_LOGW(TAG, "数据长度超限: %d", data_len);
state = STATE_WAIT_HEAD;
} else {
// 长度合法,进入等待数据状态
state = STATE_WAIT_DATA;
}
// 退出当前case分支
break;
case STATE_WAIT_DATA:
// 接收数据字段状态
// 将数据存储到缓冲区中
frame_buf[data_cnt++] = byte;
// 检查是否已接收完所有数据
if (data_cnt >= data_len) {
// 数据接收完成,进入等待校验和状态
state = STATE_WAIT_CHECKSUM;
}
// 否则继续保持在此状态继续接收数据
// 退出当前case分支
break;
case STATE_WAIT_CHECKSUM:
{
// 校验和验证状态(使用代码块避免变量作用域问题)
uint8_t check_recv = byte; // 收到的校验和
uint8_t check_calc = checksum_sum(frame_buf, data_len); // 根据接收数据计算的校验和
// 比较收到的校验和与计算得出的校验和
if (check_calc == check_recv) {
// 校验通过,处理完整帧数据
ESP_LOGI(TAG, "校验成功,处理数据帧");
process_frame(frame_buf, data_len);
} else {
// 校验失败,记录错误日志
ESP_LOGE(TAG, "SUM校验错误: recv=0x%02X calc=0x%02X",
check_recv, check_calc);
}
// 无论校验是否通过,都回到等待帧头状态准备接收下一帧
state = STATE_WAIT_HEAD;
// 退出当前case分支
break;
}
default:
// 异常状态处理
// 如果出现未定义的状态,重置为等待帧头状态
ESP_LOGE(TAG, "未知状态: %d", state);
state = STATE_WAIT_HEAD;
// 退出switch语句
break;
}
}
3.串口1事件队列模式
修改初始化代码
(1)事件队列大小
(2)队列句柄
void bsp_uart1_events_init(int uart_events_queue_size, QueueHandle_t *uart_events_queue_handle)
{
// 配置UART参数结构体
uart_config_t uart_config = {
.baud_rate = 115200, // 波特率设置为115200 bps
.data_bits = UART_DATA_8_BITS, // 数据位:8位
.parity = UART_PARITY_DISABLE, // 校验位:禁用(无校验)
.stop_bits = UART_STOP_BITS_1, // 停止位:1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 流控:禁用硬件流控
.source_clk = UART_SCLK_DEFAULT, // 时钟源:使用默认时钟
};
ESP_ERROR_CHECK(uart_param_config(UART_PORT, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(UART_PORT, UART_TX_PIN, UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(UART_PORT, 1024, 0, uart_events_queue_size, uart_events_queue_handle, 0));
}
原理
1.串口接收到数据之后触发中断
2.IDF在中断里创建事件队列
3.在线程里接收事件,读对应长度的数据
#include "task_uart_events.h"
static const char *TAG = "UART1_EVENTS";
QueueHandle_t uart_event_queue;
void uart1_events_task(void *pvParameters)
{
uart_event_t event;
uint8_t *data = (uint8_t *)malloc(UART_BUFFER_SIZE);
bsp_uart1_events_init(UART_QUEUE_SIZE, &uart_event_queue);
while(1)
{
// 等待串口事件
if (xQueueReceive(uart_event_queue, (void *)&event, (TickType_t)portMAX_DELAY)) {
switch (event.type) {
// 接收数据
case UART_DATA:
ESP_LOGI(TAG, "收到数据长度: %d", event.size);
// 读取数据
int len = uart_read_bytes(UART_PORT, data, event.size, 100 / portTICK_PERIOD_MS);
if (len > 0) {
ESP_LOGI(TAG, "收到数据: ");
for (int i = 0; i < len; i++) {
printf(" 0x%X ", data[i]);
}
printf("\n");
// 可以在这里添加数据处理逻辑
}
break;
// 其他事件
case UART_FIFO_OVF:
ESP_LOGI(TAG, "UART FIFO 溢出\n");
uart_flush_input(UART_PORT);
xQueueReset(uart_event_queue);
break;
case UART_BUFFER_FULL:
ESP_LOGI(TAG, "UART 缓冲区满\n");
uart_flush_input(UART_PORT);
xQueueReset(uart_event_queue);
break;
case UART_BREAK:
ESP_LOGI(TAG, "UART 中断\n");
break;
case UART_PARITY_ERR:
ESP_LOGI(TAG, "UART 校验错误\n");
break;
case UART_FRAME_ERR:
ESP_LOGI(TAG, "UART 帧错误\n");
break;
default:
ESP_LOGI(TAG, "未知 UART 事件: %d\n", event.type);
break;
}
}
}
free(data);
vTaskDelete(NULL);
}
BaseType_t uart1_events_task_create(void)
{
return xTaskCreate(uart1_events_task, "uart1_events_task", 4096, NULL, 10, NULL);
}
仅本人学习他人视频做出的学习笔记。
更多推荐
所有评论(0)