ESP32系列芯片外设-UART通信
前言
总线舵机是一种利用UART总线进行控制的舵机。笔者购买的总线舵机使用单线UART总线,即TX与RX在同一条线上。有的文章指出这样的设计会产生冲突,但笔者猜测如果总线上只有总线舵机,干扰可以通过软件方法解决。
本文基于ESP32-S3,编译环境是ESP-IDF v5.3.1
本文的例子来源于基于ESP32-S3的高安全性宿舍智能门锁设计
正文
UART配置
ESP-IDF配置UART的步骤分为以下几步:
步骤 1 到 3 为配置阶段,步骤 4 为 UART 运行阶段,步骤 5 和 6 为可选步骤。
设置通信参数
ESP-IDF的配置风格是使用结构体进行配置,UART配置也不例外。完整的配置结构体如下图:
const uart_config_t uart2servo_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_DEFAUL //通信模式
};
随后,使用uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config)
注册该配置,使系统为该UART通信分配相应资源。
该函数第一个传入参数为UART控制器编号,ESP32-S3有3个UART控制器,由于ESP32使用UART_NUM_0向上位机发送日志,故本例使用UART_NUM_2。UART_NUM_SERVO是指向UART_NUM_2的宏定义。
ESP_ERROR_CHECK(uart_param_config(UART_NUM_SERVO, &uart2servo_config));
笔者强烈建议在调用注册函数时同时调用ESP_ERROR_CHECK()
,该函数可以在操作失败时及时报错,便于debug。
设置通信管脚
下一步是为UART控制器分配通信管脚。
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_SERVO, UART_NUM_SERVO_TX, UART_NUM_SERVO_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
通信管脚分配函数uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num)
共有5个参数,分别为UART控制器编号、TX端口、RX端口、RTS控制端口和CTS控制端口。
但笔者没用到RTS和CTS功能,所以使用UART_PIN_NO_CHANGE
指定默认引脚。
安装驱动程序
安装UART驱动程序需要提供下列参数
- UART 控制器编号
- Tx 环形缓冲区的大小
- Rx 环形缓冲区的大小
- 指向事件队列句柄的指针
- 事件队列大小
- 分配中断的标志
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)
是UART驱动安装函数,传入变量与上面提到的参数一一对应。
笔者的实践如下,其中RX_BUF_SIZE宏定义为1024。
ESP_ERROR_CHECK(uart_driver_install(UART_NUM_SERVO, RX_BUF_SIZE * 2, 0, 0, NULL, 0));
这样一来,UART就已经配置完毕,可以开始通信了。
UART收发操作
ESP-IDF编程指南指出:
发送数据的过程分为以下步骤:
- 将数据写入 Tx FIFO 缓冲区
- FSM 序列化数据
- FSM 发送数据
接收数据的过程类似,只是步骤相反:
- FSM 处理且并行化传入的串行流
- FSM 将数据写入 Rx FIFO 缓冲区
- 从 Rx FIFO 缓冲区读取数据
因此,应用程序仅会通过
uart_write_bytes()
和uart_read_bytes()
从特定缓冲区写入或读取数据,其余工作由 FSM 完成。
幸而我们不需要了解技术实现细节,我们只需要调用UART库进行收发操作。
UART发送
UART的发送非常简单,只需要使用uart_write_bytes(uart_port_t uart_num, const void *src, size_t size)
,传入参数分别为UART控制器编号,待发送数据和待发送数据长度,以下为笔者的实践。
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand));
UART接收
UART的接收稍复杂于发送。以下为笔者的UART接收实践,封装为一个函数以便于操作。
void servoUARTread(char *UARTdata)
{
size_t bufferLenth;
ESP_ERROR_CHECK(uart_get_buffered_data_len(UART_NUM_SERVO, &bufferLenth));
uart_read_bytes(UART_NUM_SERVO, UARTdata, bufferLenth, 100);
ESP_ERROR_CHECK(uart_flush(UART_NUM_SERVO));
}
传入一个字符串指针,便于传出数据。
首先,使用uart_get_buffered_data_len()
读取缓冲区数据大小,便于下一步指定读取长度。
随后,使用uart_read_bytes()
读取数据,最后的参数“100”为需要等待读取的free RTOS tick数。
最后,使用uart_flush()
清除缓冲区,以等待下一步读取操作。
单线串口收发干扰的解决
笔者在使用串口助手测试舵机时发现,由于TX、RX短接,每次收到的数据不仅包括应当收到的数据,还包括作为指令发送的数据。简而言之,主机发送的数据会被自己再次收到。
因此,我们可以先预设“应当收到”的数据,再在接收缓冲区中查找是否有数据与我们预设的“应当受到”的数据匹配,如果有,就表明发送成功。
以下为笔者的实践:
strConnect(servoRetrun, "#", servoID, "!"); // 拼凑出舵机“应该”有的返回值
char *isOK = NULL; // 串口读到的信息里有“应该”有的返回值吗?
isOK = strstr(UARTdata, servoRetrun);
if (isOK != NULL)
总线舵机作为对象
示例使用C++编写,所以笔者设计了舵机类,对舵机进行一定封装。
class SERVO
{
private:
char servoID[4] = "000"; //舵机编号,默认为000
char servoCommand[16]; //待发送的舵机指令
char servoRetrun[16]; //应当接收到的舵机回传
char resetPos[5] = "0500";
char openPos[5] = "2000";
public:
bool servo_init(void);
bool opendoor();
void setServoID(char out_servoID);
SERVO(char *out_servoID); //构造函数
~SERVO(); //析构函数
};
SERVO::SERVO(char out_servoID[4] = "000")
{
strcpy(servoID, out_servoID);
servo_init();
}
SERVO::~SERVO()
{
uart_driver_delete(UART_NUM_SERVO);
}
/*******************************************************************************
****函数功能: 初始化舵机
****入口参数: 无
****出口参数: true: 设置成功 false: 设置失败
****函数备注: 无
********************************************************************************/
bool SERVO::servo_init()
{
init_uart2servo();
strConnect(servoCommand, "#", servoID, "PID!"); // 获取舵机ID的指令
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand)); // 发送,然后读取串口
char UARTdata[64];
servoUARTread(UARTdata);
strConnect(servoRetrun, "#", servoID, "!"); // 拼凑出舵机“应该”有的返回值
char *isOK = NULL; // 串口读到的信息里有“应该”有的返回值吗?
isOK = strstr(UARTdata, servoRetrun);
if (isOK != NULL)
{
isOK = NULL;
strcpy(UARTdata, "\0");
strcpy(servoRetrun, "\0");
strConnect(servoCommand, "#", servoID, "PMOD!"); // 舵机读取工作模式指令
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand));
servoUARTread(UARTdata);
strConnect(servoRetrun, "#", servoID, "PMOD1!"); // 舵机的工作模式应该是舵机模式,顺时针最大270度,即1
isOK = strstr(UARTdata, servoRetrun);
if (isOK != NULL)
{
strcpy(UARTdata, "\0"); // 工作模式不是1就设置成1
strConnect(servoCommand, "#", servoID, "PMOD1!");
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand));
}
return true;
}
else
{
return false;
}
}
/*******************************************************************************
****函数功能: 开门
****入口参数: 无
****出口参数: true: 开门成功 false: 开门失败
****函数备注: 初始位为500,开门位为2000
********************************************************************************/
bool SERVO::opendoor()
{
strConnect(servoCommand, "#", servoID, "P", openPos, "T1000!"); // #000P2000T1000!
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand));
vTaskDelay(1000);
strConnect(servoCommand, "#", servoID, "P", resetPos, "T1000!");
uart_write_bytes(UART_NUM_SERVO, servoCommand, strlen(servoCommand));
}