1. 简介
UART可以说是开发者使用得最多的外设之一了,打印log几乎都是使用串口来实现的。UART是一种异步全双工的通信方式,异步传输的特性使得它仅需2根线就可以完成全双工的传输,但这也要求发送端和接收端的速率、停止位、奇偶校验位等都要相同,通信才能成功。
一个典型的UART帧开始于一个起始位,紧接着是有效数据,然后是奇偶校验位(可有可无),最后是停止位。 ESP32上的UART控制器支持多种字符长度和停止位。另外,控制器还支持软硬件流控和 DMA,可以实现无缝高速的数据传输。
2. 硬件架构
ESP32中的UART有两个时钟源:80MHz APB_CLK和参考时钟REF_TICK,时钟源进来后会通过分频器调整到对应的频率,分频后的时钟会分别进到发送块和接收块。
发送器和接收器都各自有一个FIFO来缓存数据,每个FIFO有128字节的空间,ESP32中3个UART设备会共享一块1KB大小的内存作为FIFO空间。获取FIFO数据除了通过CPU来实现,还可以通过内部DMA实现,以提高效率。
UART设备也支持硬件流控和软件流控,以支持像RS-485、红外遥控等协议的开发。
3. 流控
3.1 硬件流控
硬件流控主要通过输出信号rtsn_out和输入信号dsrn_in进行数据流控制。
输出信号rtsn_out为高电平表示请求对方发送数据,rtsn_out为低电平表示通知对方中止数据发送直到rtsn_out恢复高电平。
当UART检测到输入信号ctsn_in的沿变化时会产生UART_CTS_CHG_INT中断并且在发送完当前数据后停止接下来的数据发送。
输出信号dtrn_out为高电平表示发送方数据已经准备完毕,UART在检测到输入信号dsrn_in 的沿变化时会产生UART_DSR_CHG_INT中断。软件在检测到中断后,通过读取UART_DSRN可以获取dsrn_in的输入信号电平,从而判断当前是否可以接收数据。
3.2 软件流控
软件流控主要通过在发送数据流中插入特殊字符以及在接收数据流中检测特殊字符来实现数据流控功能。
软件可以通过置位UART_FORCE_XOFF来强制停止发送器发送数据,也可以通过置位UART_FORCE_XON来强制发送器发送数据。
UART还可以通过传输特殊字符进行软件流控,当UART 接收的数据字节数超过UART_XOFF的阈值时,可以通过发送UART_XOFF_CHAR来告知对方停止发送数据。
软件也可以在任意时候发送流控字符。置位UART_SEND_XOFF,发送器会在发送完当前数据之后插入发送一个UART_XOFF_CHAR;置位 UART_SEND_XON,发送器会在发送完当前数据之后插入发送一个UART_XON_CHAR。
4. 例程
例程中使用ESP32的串口1和串口2,相互连接并实现数据收发。
cpp
#include "driver/gpio.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "esp_log.h"
#include <string.h>
#define TAG "app"
void app_main()
{
uart_config_t uart_cfg = {0};
uart_cfg.baud_rate = 115200,
uart_cfg.data_bits = UART_DATA_8_BITS; // 8位数据
uart_cfg.parity = UART_PARITY_DISABLE; // 无校检
uart_cfg.stop_bits = UART_STOP_BITS_1; // 1位停止位
uart_cfg.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; // 无硬件流控
uart_cfg.source_clk = UART_SCLK_DEFAULT; // 默认时钟源,APB时钟
/* 初始化串口1 */
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, 1024 * 2, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_cfg));
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, 17, 18, -1, -1));
/* 初始化串口2 */
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_2, 1024 * 2, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(UART_NUM_2, &uart_cfg));
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_2, 21, 22, -1, -1));
const char *str = "Hello, World!";
while (1) {
/* 串口1发送 */
int send_len = uart_write_bytes(UART_NUM_1, str, strlen(str));
ESP_LOGI(TAG, "Send %d bytes, data: %s", send_len, str);
uart_wait_tx_done(UART_NUM_1, portMAX_DELAY);
/* 串口2接收 */
char buf[128] = {0};
int read_len = uart_read_bytes(UART_NUM_2, buf, 128, 100 / portTICK_PERIOD_MS);
if (read_len >= 0) {
ESP_LOGI(TAG, "[UART2] Receive %d bytes, data: %s", read_len, buf);
} else {
ESP_LOGE(TAG, "[UART2] Receive data timeout");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
先初始化uart_config_t结构体,我设置的是常见的115200波特率,无校检,1位停止位,时钟为APB时钟。
uart_driver_install注册串口设备,参数1为串口号;参数2和3是接收和发送缓冲区大小,缓冲区的大小必须大于FIFO的大小,发送缓冲区可以为0,我设置接收缓冲区2KB,无发送缓冲;参数4和5是消息队列的大小和句柄,可以通过它来接收串口的事件数据;参数6是注册标志,一般默认为0即可。
uart_param_config配置串口通信参数;uart_set_pin配置串口引脚,参数依次为发送管脚、接收管脚、RTS管脚和CTS管脚,管脚不用的话设置为-1。
uart_write_bytes发送数据,如果发送缓冲区为0,那么所有数据压入发送FIFO就会返回 ;如果发送缓冲区不为0,那么所有数据复制进缓冲区时就会返回。其实无论哪一种情况,函数返回都不能保证数据一定完全发送了,所以最好后面调uart_wait_tx_done等串口完全发送再执行下一步操作。
使用uart_read_bytes接收串口数据,最后一个参数可以设置等待时长,我这里是等待100ms。