1. UART中的硬件流控RTS与CTS
数据流在两个串口间传输,由于两个串口所在处理器处理速度不同或缓冲区大小不同,会出现数据丢失现象,流控能解决这个问题。当接收端数据处理不过来时,就发出"不再接收"的信号,发送端就停止发送,知道收到"可继续发送"的信号再发送。因此流控制可以控制数据传输的进程,防止数据的丢失。PC机中常用的两种流控制是硬件流控制(包括RTS/CTS、DTR/CTS等)和软件流控制XON/XOFF(继续/停止),下面分别说明。
一般来说,只有在半双工通信的情况下需要用到CTS/RTS。
1.1 什么是CTS/RTS?
RTS (Require ToSend,发送请求) 为输出信号,用于指示本设备准备好可接收数据,低电有效,低电说明本设备可以接收数据。
CTS (Clear ToSend,发送允许) 为输入信号,用于判断是否可以向对方发送数据,低电有效,低电说明本设备可以向对方发送数据。
1.2 硬件流控
硬件流控制常用的有RTS/CTS流控制和DTR/DSR(数据终端就绪/数据设置就绪)流控制。硬件流控制必须将相应的电缆线连上,用RTS/CTS(请求发送/清除发送)流控制时,应将通讯两端的RTS、CTS线对应相连,数据终端设备(如计算机)使用RTS来起始调制解调器或其它数据通讯设备的数据流,而数据通讯设备(如调制解调器)则用CTS来起动和暂停来自计算机的数据流。这种硬件握手方式的过程为:
- 在编程时根据接收端缓冲区大小设置一个高位标志(可为缓冲区大小的75%)和一个低位标志(可为缓冲区大小的25%)
- 当缓冲区快满了高于设定高位值,我们在接收端将CTS线置低电平(送逻辑0)
- 当发送端的程序检测到CTS为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将CTS置高电平。
- RTS则用来标明接收设备有没有准备好接收数据。
1.2 一般怎么用
说实话,串口一般用法都很简单,只需接三根线,RX,TX,GND就得了。很少很少,几乎没有遇到过用上RTS与CTS的机会。但一个客户问题让我真遇到了,客户想要用Linux PL011串口驱动,这个驱动看起来已经做了RS485的方向引脚,用户就不用自己做了,于是有了我这篇学习加总结博客。
1.4 Linux UART用于RS485时的一般做法
RS485是一种物理层的差分信号传输标准,如果是普通的不具备硬件自动方向切换的半双工RS485,除了要 RX,TX 引脚外还通常需要需要控制器提供一个方向控制信号接到485转换芯片的 DE/RE 引脚。关于RS485硬件相关介绍参考我的博客【串口 COM口 TTL RS-232 RS-485 区别】。
而RTS/CTS是UART内部的逻辑流控机制。PL011 作为软 IP 核,其核心功能是处理 UART 逻辑。当UART用于RS485时通常有如下做法:
- 方法一:另选一个GPIO作485方向控制信号,或者将RTS引脚重定义为GPIO,这样写应用代码时就需要增加GPIO控制相关代码。
- 方法二:利用Linux最新代码的PL011驱动对RS485的支持(驱动已经禁用了流控),将RTS引脚作为方向控制引脚。应用层代码直接
read() write()不需要写方向控制逻辑,简化开发。
!IMPORTANT
两种方法的优劣
方法一存在问题:数据真正的"旅程"是这样的:
应用层 write() -> 内核 TTY 缓冲区 -> UART 硬件 FIFO -> 物理线路
- 在
write执行后,数据没有被发完而是被存储到了缓冲区中或FIFO中,因此需在write后做延时不精确。- 操作系统的"调度"干扰:你的程序在执行
usleep前后,随时可能被操作系统调度出去,让其他高优先级的进程(比如系统服务、界面刷新)先跑,调度就可能导致该线程被更高优先级的进程占据。方法二的优势 :Linux 的 PL011 驱动通过读外设寄存器完全掌握硬件 FIFO 的状态。它会在数据真正从物理线路上发完之后,再精准地切换 RTS 引脚的电平。这种纳秒级的硬件时序控制,是应用层代码无法比拟的。同时代码可以大大简化。如果未来硬件改回了 RS232,或者换了其他支持原生 RS485 的 UART 芯片,你的应用层代码一行都不用改。这极大地降低了软件维护成本。
2. Linux PL011 驱动支持RS485
只需要CPU与RS485芯片间如下连接(典型半双工):
PL011 UART RS485芯片
----------- -----------
RTS ---------> DE (Driver Enable)(高电平使能发送(也可能是反的))
---------> /RE (Receiver Enable, 内部取反)(低电平使能接收,内部反相)
TX ---------> DI (Data Input)
RX <--------- RO (Receiver Output)
方法1:DTS配置
参考内核Documentation/devicetree/bindings/serial/rs485.yaml目录,增加如下配置可以使能UART工作在485模式:
&uart1 {
status = "okay";
linux,rs485-enabled-at-boot-time;
rs485-rts-delay = <0 0>;
rs485-rts-active-high;
};
完事儿就行啦。
方法2:用户态 ioctl 配置
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <linux/serial.h>
//第一部分代码/
//根据具体的设备修改
const char default_path[] = "/dev/ttyAMA1";
int main(int argc, char *argv[])
{
int fd;
int res;
char *path;
char buf[1024];
//第二部分代码/
//若无输入参数则使用默认终端设备
if (argc > 1)
path = argv[1];
else
path = (char *)default_path;
//获取串口设备描述符
printf("This is tty/usart demo.\n");
fd = open(path, O_RDWR);
if (fd < 0) {
printf("Fail to Open %s device\n", path);
return 0;
}
// 配置RS485需要的结构体
struct serial_rs485 rs485conf;
// 先尝试读取当前RS485配置
if (ioctl(fd, TIOCGRS485, &rs485conf) < 0) {
printf("TIOCGRS485 failed: %s\n", strerror(errno));
perror("ioctl TIOCGRS485");
close(fd);
return 1;
}
printf("Yes, Driver supports RS485!\n");
printf("Current flags: 0x%x\n", rs485conf.flags);
// 清空配置,重新设置
memset(&rs485conf, 0, sizeof(rs485conf));
rs485conf.flags |= SER_RS485_ENABLED; // 使能RS485
rs485conf.flags |= SER_RS485_RTS_ON_SEND; // 发送时RTS=1(也可设为 0)
// rs485conf.flags |= SER_RS485_RTS_AFTER_SEND; // 发送后RTS=1 (与SER_RS485_RTS_ON_SEND不能同时设置)
// 发送前后延时(毫秒)
rs485conf.delay_rts_before_send = 0; // ms
rs485conf.delay_rts_after_send = 0; // ms
// 默认半双工模式(硬件采用 4 线制才能全双工)
//rs485conf.flags &= ~(SER_RS485_RX_DURING_TX);
// 应用配置
if (ioctl(fd, TIOCSRS485, &rs485conf) < 0) {
perror("ioctl TIOCSRS485");
return -1;
}
//第三部分代码/
struct termios opt;
//清空串口接收缓冲区
tcflush(fd, TCIOFLUSH);
// 获取串口参数opt
tcgetattr(fd, &opt);
//设置串口输出波特率
cfsetospeed(&opt, B115200);
//设置串口输入波特率
cfsetispeed(&opt, B115200);
//设置数据位数
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
//校验位
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
//设置停止位
opt.c_cflag &= ~CSTOPB;
//设置为 raw 原始模式(非规范模式)
opt.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ISIG | IEXTEN);
opt.c_oflag &= ~OPOST;
opt.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL | INLCR | IGNCR); //输入标志
//设置读取超时和最小字符数
opt.c_cc[VTIME] = 10; // 读取超时时间,单位为0.1秒(这里是1秒)
opt.c_cc[VMIN] = 0; // 最小读取字符数,0表示非阻塞读取
opt.c_cflag |= (CREAD | CLOCAL); // 使能接收,本地模式
//更新配置
tcsetattr(fd, TCSANOW, &opt);
printf("Device %s is set to 115200bps,8N1\n",path);
if (argc < 3) {
printf("- Usage: %s [device] <message>\n", argv[0]);
printf("-- example: %s /dev/ttyAMA1 UUU\n", argv[0]);
close(fd);
return 1;
}
//第四部分代码/
do {
//发送字符串
int n = write(fd, argv[2], strlen(argv[2]));
if (n < 0) {
perror("write");
}
//tcdrain(fd); // 等待数据发送完成,应该也不需要
//usleep(200); // 额外延时,等待RS485切换到接收模式,其实完全不需要
//接收字符串
memset(buf, 0, sizeof(buf));
res = read(fd, buf, sizeof(buf) - 1);
if (res < 0) {
perror("read");
break;
} else if (res > 0) {
buf[res] = '\0';
printf("Receive res = %d bytes data: %s\n", res, buf);
}
sleep(2);
} while (1);
close(fd);
return 0;
}
测试注意事项
交叉编译上述代码在板子上测试。
使用示波器测试时,输入捕获模式同时测量TX和RTS引脚。
字母 U 对应二进制 (01010101),字母 @ 对应二进制 01000000,测试时发送这两个字符。可以明显看出电平跳变。
参考链接 :UART中的硬件流控RTS与CTS