04. 串口通信
一、串口通信简介
串口通信是一种设备间常用的串行通信方式,串口按位(bit)发送和接收字节。串口通信的数据包由发送设备的 TXD 接口传输到接收设备的 RXD 接口。在串口通信的协议层中,规定了数据包的内容,它由起始位、主体数据、校验位以及停止位组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成如下图所示。

串口通信协议数据包组成可以分为 波特率 和 数据帧格式 两部分。、
【1】、波特率
波特率 表示 每秒钟传送的码元符号的个数,所以它决定了数据帧里面每一个位的时间长度。两个要通信的设备的波特率一定要设置相同。
【2】、数据帧
串口通信的数据帧包括 起始位、停止位、有效数据位 以及 校验位。
串口通信的一个数据帧是从起始位开始,直到停止位。数据帧中的 起始位 是由 一个逻辑 0 的数据位 表示,而数据帧的 停止位 可以是 0.5、1、1.5 或 2 个逻辑 1 的数据位 表示,只要双方约定一致即可。
数据帧的起始位之后,就接着是数据位,也称有效数据位,这就是我们真正需要的数据,有效数据位通常会被约定为 5、6、7 或者 8 个位长。有效数据位是低位(LSB)在前,高位(MSB)在后。
校验位 可以认为是 一个特殊的数据位。校验位一般用来判断接收的数据位有无错误,检验方法有:奇检验、偶检验、0 检验、1 检验 以及 无检验。
- 奇校验 是指 有效数据位和校验位中 “1” 的个数为奇数,比如一个 8 位长的有效数据为:10101001,总共有 4 个 “1”,为达到奇校验效果,校验位设置为 “1”,最后传输的数据是 8 位的有效数据加上 1 位的校验位总共 9 位。
- 偶校验 是指 有效数据位和校验位中 “1” 的个数为偶数,比如数据帧:11001010,此时数据帧 “1” 的个数为 4 个,所以偶校验位为 “0”。
- 0 校验 是指不管有效数据中的内容是什么,校验位总为 “0”。
- 1 校验 是指不管有效数据中的内容是什么,校验位总为 “1”。
- 无校验 是指 数据帧中不包含校验位。
尽管比特字节(byte)的串行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。
二、ESP32的UART控制器
ESP32 S3 芯片中有三个 UART 控制器可供使用,并且兼容不同的 UART 设备。此外,UART 还可以用作红外数据交换(IrDA)或 RS485 调制解调器。三个 UART 控制器分别有一组功能相同的寄存器,分别为 UART0、UART1、UART2。
UART 是一种以字符为导向的通用数据链,可以实现设备间的通信。异步通信不需要在发送数据的过程中添加时钟信息,但这也要求发送端和接收端的速率、停止位以及奇偶校验位等参数的配置要相同,唯有如此通信才能成功。
UART 数据帧始于一个起始位,接着是有效数据,然后是奇偶校验位,最后才是停止位。ESP32 S3 芯片上的 UART 控制器支持多种字符长度和停止位。另外,控制器还支持软、硬件控制流和 GDMA,可以实现无缝高速的数据传输。

【1】、RAM
ESP32 S3 芯片中三个 UART 控制器(UART0、UART1、UART2)共用 1024×8 bit 的 RAM 空间。通过配置 UART_TX_SIZE 可以对三个 UART 控制器中的其中一个的 Tx_FIFO 以 1 block 为单位进行扩展。同理,配置 UART_RX_SIZE 也是一样的。
【2】、Clock
UART 作为异步通信的外设,它的寄存器配置模块与 TX/RX/FIFO 都工作在 APB_CLK 时钟域内,而控制 UART 接收与发送的 Core 模块工作在 UARTCore 时钟域。Clock 有三个时钟源,分别为:APB_CLK、RC_FAST_CLK 以及晶振时钟 XTAL_CLK,它们可以通过配置寄存器 UART_SCLK_SEL 来选择使用哪个时钟作为时钟源。选择后的时钟源通过预分频器(Divider)分频后进入 UART Core 模块。该分频器支持小数分频,支持的分频范围为:1~256,分频系数为:
【3】、UART 控制器模块
UART 控制器可以分为两个功能块,分别为:发送块(Transmitter)以及接收块(Receiver)。
发送块包含一个发送 FIFO 用于缓存待发送的数据。软件可以通过 APB 总线向 Tx_FIFO 写数据,也可以通过 GDMA 将数据传入 Tx_FIFO。Tx_FIFO_Ctrl,用于控制 Tx_FIFO 的读写过程,当 Tx_FIFO 非空时,Tx_FSM 通过 Tx_FIFO_Ctrl 读取数据,并将数据按照配置的帧格式转化成比特流。比特流输出信号 txd_out 可以通过配置 UART_TXD_INV 寄存器实现取反功能。
接收块包含一个接收 FIFO 用于缓存待处理的数据。输入比特流 rxd_in 可以输入到 UART 控制器。可以通过 UART_RXD_INV 寄存器实现取反。Baudrate_Detect 通过检测最小比特流输入信号的脉宽来测量输入信号的波特率。Start_Detect 用于检测数据的 START 位,当检测到 START 位之后,Rx_FSM 通过 Rx_FIFO_Ctrl 将帧解析后的数据存入 Rx_FIFO 中。软件可以通过 APB 总线读取 Rx_FIFO 中的数据也可以使用 GDMA 方式进行数据接收。
【4】、UART Core
HW_Flow_Ctrl 通过标准 UART RTS 和 CTS(rtsn_out 和 ctsn_in)流控信号来控制 rxd_in 和txd_out 的数据流。SW_Flow_Ctrl 通过在发送数据流中插入特殊字符以及在接收数据流中检测特殊字符来进行数据流的控制。
三、串口常用函数
ESP-IDF 提供了一套 API 来配置串口。要使用串口功能,需要导入必要的头文件 driver/uart.h。
#include "driver/uart.h"
3.1、配置串口端口
我们可以使用 uart_param_config() 函数 设置指定 UART 端口的通信参数,它的函数原型如下:
/**
* @brief 配置串口端口
*
* @param uart_num UART外设端口号
* @param uart_config UART的参数配置信息
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
形参 uart_num 用来 指定 UART 外设端口号。UART 外设端口号在 uart.h 文件中有定义。
typedef enum
{
UART_NUM_0, // UART port 0
UART_NUM_1, // UART port 1
#if SOC_UART_HP_NUM > 2
UART_NUM_2, // UART port 2
#endif
#if SOC_UART_HP_NUM > 3
UART_NUM_3, // UART port 3
#endif
#if SOC_UART_HP_NUM > 4
UART_NUM_4, // UART port 4
#endif
#if (SOC_UART_LP_NUM >= 1)
LP_UART_NUM_0, // LP UART port 0
#endif
UART_NUM_MAX, // UART port max
} uart_port_t;
形参 uart_config 是 指向 uart_config_t 结构体的指针,它包含了 UART 的参数配置信息。该结构体的定义如下所示:
typedef struct
{
int baud_rate; // 波特率
uart_word_length_t data_bits; // 数据位
uart_parity_t parity; // 校验位
uart_stop_bits_t stop_bits; // 停止位
uart_hw_flowcontrol_t flow_ctrl; // 硬件流控制
uint8_t rx_flow_ctrl_thresh; // 硬件控制流阈值
union
{
uart_sclk_t source_clk; // 时钟源
#if (SOC_UART_LP_NUM >= 1)
lp_uart_sclk_t lp_source_clk;
#endif
};
struct
{
uint32_t backup_before_sleep: 1;
} flags; // 配置标志
} uart_config_t;
成员 baud_rate 代表 串口的波特率,即数据传输速率,具体指的是每秒传输的位数。常见的波特率值包括 9600 和 115200 等。
成员 data_bits 是 数据位的数量,也就是每个字节中的位数。它的可选值如下:
typedef enum
{
UART_DATA_5_BITS = 0x0, // 数据长度:5bits
UART_DATA_6_BITS = 0x1, // 数据长度:6bits
UART_DATA_7_BITS = 0x2, // 数据长度:7bits
UART_DATA_8_BITS = 0x3, // 数据长度:8bits
UART_DATA_BITS_MAX = 0x4,
} uart_word_length_t;t;
成员 parity 是 奇偶校验位,它的可选值如下:
typedef enum
{
UART_PARITY_DISABLE = 0x0, // 不使用校验位
UART_PARITY_EVEN = 0x2, // 偶校验
UART_PARITY_ODD = 0x3 // 奇校验
} uart_parity_t;
成员 stop_bits 是 停止位的数量,它的可选值如下:
typedef enum
{
UART_STOP_BITS_1 = 0x1, // 停止位个数: 1bit
UART_STOP_BITS_1_5 = 0x2, // 停止位个数: 1.5bits
UART_STOP_BITS_2 = 0x3, // 停止位个数: 2bits
UART_STOP_BITS_MAX = 0x4,
} uart_stop_bits_t;
成员 flow_ctrl 是 硬件流控制的设置,它的可选值如下:
typedef enum
{
UART_HW_FLOWCTRL_DISABLE = 0x0, // 不使用硬件流控制
UART_HW_FLOWCTRL_RTS = 0x1, // 只启用 RTS 信号流控制
UART_HW_FLOWCTRL_CTS = 0x2, // 只启用 CTS 信号流控制
UART_HW_FLOWCTRL_CTS_RTS = 0x3, // 同时启用 RTS 和 CTS 信号流控制
UART_HW_FLOWCTRL_MAX = 0x4,
} uart_hw_flowcontrol_t;
成员 rx_flow_ctrl_thresh 是 硬件控制流阈值。
成员 source_clk 用来 配置时钟源,它的可选值如下:
typedef enum
{
UART_SCLK_APB = SOC_MOD_CLK_APB, // 选择 APB 作为时钟源
UART_SCLK_RTC = SOC_MOD_CLK_RC_FAST, // 选择 RTC 作为时钟源
UART_SCLK_XTAL = SOC_MOD_CLK_XTAL, // 选择 XTAL 作为时钟源
UART_SCLK_DEFAULT = SOC_MOD_CLK_APB, // 选择 APB 时钟源为默认选项
} soc_periph_uart_clk_src_legacy_t;
3.2、配置UART引脚
我们可以使用 uart_set_pin() 函数 设置某个管脚的中断服务函数,它的函数原型如下:
/**
* @brief 配置UART引脚
*
* @param uart_num UART外设端口号
* @param tx_io_num 发送引脚号,若不需要此功能,可将此参数设为-1
* @param rx_io_num 接收引脚号,若不需要此功能,可将此参数设为-1
* @param rts_io_num rts引脚号,若不需要此功能,可将此参数设为-1
* @param cts_io_num cts引脚号,若不需要此功能,可将此参数设为-1
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
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);
3.3、安装驱动程序
我们可以使用 uart_driver_install() 函数 安装 UART 驱动程序,并 指定发送和接收缓冲区的大小,其函数原型如下所示:
/**
* @brief 安装驱动程序
*
* @param uart_num UART外设端口号
* @param rx_buffer_size UART接收环形缓冲区大小
* @param tx_buffer_size UART发送环形缓冲区大小
* @param event_queue_size UART驱动程序内部缓冲队列的大小
* @param uart_queue 保存用户定义的用于接收数据的队列句柄
* @param intr_alloc_flags UART中断分配标志
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int event_queue_size, QueueHandle_t *uart_queue, int intr_alloc_flags);
3.4、获取数据长度
我们可以使用 uart_get_buffered_data_len() 函数 获取接收环形缓冲区中缓存的数据长度,其函数原型如下所示:
/**
* @brief 获取数据长度
*
* @param uart_num UART外设端口号
* @param size 保存了接受缓存的数据长度
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t uart_get_buffered_data_len(uart_port_t uart_num, size_t *size);
3.5、接收数据
我们可以使用 uart_read_bytes() 函数 从UART 接收缓冲区中读取数据,其函数原型如下所示:
/**
* @brief 接收数据
*
* @param uart_num UART外设端口号
* @param buf 缓冲区的指针
* @param length 数据长度
* @param ticks_to_wait 超时等待的时间,单位是FreeRTOS节拍计数
* @return int 成功读取的字节数,如果发生错误则返回-1
*/
int uart_read_bytes(uart_port_t uart_num, void *buf, uint32_t length, TickType_t ticks_to_wait);
3.6、发送数据
我们可以使用 uart_write_bytes() 函数 将指定的数据写入到 UART 发送缓冲区,并触发数据的发送,其函数原型如下所示:
/**
* @brief 发送数据
*
* @param uart_num UART外设端口号
* @param src 发送的数据的缓冲区
* @param size 要发送的数据长度
* @return int 成功发送的字节数,如果发生错误则返回-1
*/
int uart_write_bytes(uart_port_t uart_num, const void *src, size_t size);
在使用 uart_write_bytes() 函数发送数据时,数据首先被复制到 UART 发送缓冲区,随后函数会返回,并不会等待数据完全发送完成。因此,若需确保数据完整无误地发送成功,应当调用 uart_wait_tx_done() 函数进行同步等待,直至发送过程完全结束。在确认 UART 已成功初始化,并且已经配置了正确的波特率及其他相关参数之后,即可调用 uart_write_bytes() 函数,将数据准确无误地发送至 UART 设备。
四、实验例程
这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_uarrt.h 文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_uart.c 文件。
#ifndef __BSP_UART_H__
#define __BSP_UART_H__
#include "driver/gpio.h"
#include "driver/uart.h"
#define TX_BUF_SIZE 1024 // 串口发送缓冲区大小
#define RX_BUF_SIZE 1024 // 串口接收缓冲区大小
void bsp_uart_init(uart_port_t uart_port, uint32_t baud_rate, gpio_num_t tx_gpio_num, int rx_gpio_num);
#endif // !__BSP_UART_H__
/**
* @brief 串口初始化
*
* @param uart_port 串口号
* @param baud_rate 波特率
* @param tx_gpio_num 串口发送引脚
* @param rx_gpio_num 串口接收引脚
*/
void bsp_uart_init(uart_port_t uart_port, uint32_t baud_rate, gpio_num_t tx_gpio_num, int rx_gpio_num)
{
uart_config_t uart_config = {0};
uart_config.baud_rate = baud_rate; // 波特率
uart_config.data_bits = UART_DATA_8_BITS; // 数据位
uart_config.parity = UART_PARITY_DISABLE; // 校验位
uart_config.stop_bits = UART_STOP_BITS_1; // 停止位
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; // 硬件流控
uart_config.source_clk = UART_SCLK_APB; // 配置时钟源
uart_config.rx_flow_ctrl_thresh = 122; // 硬件控制流阈值
uart_param_config(uart_port, &uart_config);
// 配置uart引脚
uart_set_pin(uart_port, tx_gpio_num, rx_gpio_num, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 安装串口驱动
uart_driver_install(uart_port, RX_BUF_SIZE, TX_BUF_SIZE, 32, NULL, 0);
}
ESP32-S3 的串口通讯驱动不需要为串口编写中断回调函数,因为在 ESP32-S3 IDF 库中已经封装了数据读写函数。串口通过函数获取 RX 环形缓冲区缓存的数据长度,并判断该数据长度非空后,将其逐一通过读写函数进行操作。
修改【main】文件夹下的 main.c 文件。
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "bsp_uart.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
uart_port_t uart_port = UART_NUM_1;
size_t length = 0;
uint8_t data[RX_BUF_SIZE] = {0};
char *temp = "\n 发送的消息为:\n";
bsp_uart_init(UART_NUM_1, 115200, GPIO_NUM_1, GPIO_NUM_2);
while (1)
{
uart_get_buffered_data_len(uart_port, &length); // 获取环形缓冲区数据长度
if (length > 0)
{
memset(data, 0, RX_BUF_SIZE);
uart_write_bytes(uart_port, temp, strlen((char *)temp)); // 写数据
uart_read_bytes(uart_port, data, length, 100); // 读取串口数据
uart_write_bytes(uart_port, data, strlen((char *)data)); // 写数据
}
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(10));
}
}

浙公网安备 33010602011771号