【Linux PL011驱动支持RS485】

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

相关推荐
IT瑞先生1 小时前
Linux系统基础
linux·运维·服务器
modelmd1 小时前
Linux chroot命令
linux
l1t2 小时前
在WSL的ubuntu 26.04容器中用deb安装包安装使用redrock-4.1-1
linux·运维·ubuntu·postgresql
renren-1002 小时前
centos7.9 升级openssl11 导致的系统命令瘫痪
linux·运维·服务器
SWAGGY..2 小时前
Linux系统编程:(六)编译器gcc/g++
linux·运维·服务器
蜡笔婧萱2 小时前
Linux——Web服务器网址建立(http和https的分离)
linux·运维·服务器
.小小陈.3 小时前
Linux 多线程进阶:线程互斥、同步、线程池、死锁与线程安全、读写锁、自旋锁
linux·开发语言·c++
Hello_wshuo3 小时前
v3s镜像从零开始构建
linux·嵌入式
Felven3 小时前
国产ZYNQ multiboot功能介绍与实现
linux·fpga开发·multiboot·国产zynq