华南农业Taurus战队旭日x3派控制方案搭建分享(一)-x3派串口配置

Linux 串口配置

华南农业大学Taurs战队在2023赛季首次使用x3派作为主控用于轮足机器人运动的运动控制,在探索过程中产出了一些方案搭建的成果在此和社区的小伙伴们进行分享,该系列大概分成五篇三个部分,

  1. 串口(Dbus/Sbus协议支持遥控器发送topic控制机器人)
  2. ros2control框架搭建(两篇)
  3. usb2can模块与周边传感器通信(两篇)

可以实现脱离单片机转而使用x3派实现基本的控制方案搭建,该系列旨在帮助读者从零基础实现让机器人在传感器反馈的基础上“动起来”,后续系列将会有更深入的控制方案分享,由于笔者水平有限,行文难免有所纰漏之处,欢迎在社区或者发送邮件到邮箱和我取得联系,再次感谢地平线对Taurus战队在2023赛季的大力帮助!

Overview

在Linux操作系统中,将所有设备抽象成了一个文件,串口也是使用类似的方式。当系统检测到串口设备时,会将抽象出的文件放置在/dev/目录下,但是不同类型的串口设备又会有不同的命名方式。

当使用串口通信的设备(例如遥控器接收机)插入旭日x3派时系统会将其命名为/dev/ttyS3这意味着插入的设备为标准串口设备,但是如果将USB-485等设备插入时,系统会将其识别为/dev/ttyUSB0,大多数USB转串口设备都会被如此识别,还有许多类似的不同命名方式,在此不做赘述.

既然是文件我们就可以使用文件I/O的方式对文件进行读写操作,此处想说明的是,我们在读写串口文件时使用不带缓冲的文件I/O即,open,read和write等函数,而非标准IO库,因此程序编写者需要考虑缓存区(BUFFER_SIZE)的长度.

本文在串口的基础上实现波特率的定制并搭配串口取反硬件来适配Dbus协议从而实现和DJI DT7&DR16 2.4GHz遥控器的通信。

设备(文件)的打开

先添加必要的头文件:

// 标准头文件
#include <stdio.h>
#include <string.h>

// 系统相关头文件
#include <fcntl.h> // 包含文件打开方式的宏
#include <unistd.h> // write(), read(), close()

插入串口设备,使用命令

ls -m /dev | grep tty

列出相应的串口设备,选择刚刚插入的串口进行打开.

int fd = open("/dev/ttyS3", O_RDWR | O_NOCTTY | O_SYNC);

if (fd < 0)
{
    std::cout<<"[robot_dbus] Unable to open dbus\n"<<std::endl;
}

代码中调用库函数open对相应文件进行打开,该函数接收两个参数(在不创建新文件的情况下),第一个为文件的打开路径,第二个为打开的模式.

这里列举说明当前使用到的打开模式,其他模式感兴趣的读者可以在手册中查看

O_RDWR:读、写打开,我的的串口设备需要进行收发两种动作,因此需要将两种权限全部打开.

O_NOCTTY:如果路径(/dev/ttyS3)引用的是终端设备,则不将该设备分配作为此进程的控制终端,如果不设置该模式,那么终止信号会影响串口的读取.

O_SYNC:使得每次都进行写等待.

函数返回一个文件描述符,在Linux平台下,文件描述符是一个唯一的非负整数,所有打开的文件均可以通过该文件描述来进行引用,例如在读(read),写(write)该文件时,均使用该文件描述符来操作打开的文件.

关于O_SYNC

设置文件打开模式中的O_SYNC使得每次write操作之后都要等待直到数据已经写到磁盘上再返回.在Linux中,wirte只是将数据排入队列,实际的写操作可能在以后的某个时刻进行,这对于实时控制系统来说是不允许的.

当使用O_SYNC时,当从write返回时就知道数据已经写到了设备上,保证了数据的实时性,保证在系统异常时产生的数据丢失.

但是同时这样的设置也会带来系统时间和时钟时间的开销,真正使用时应当进行更多的测试,查看当前方案是否能满足控制需要.

常见错误

上面代码如果出错可能有下面几个可能的原因

  1. 文件路径错误
  2. 权限问题

在碰到相关错误时,可以引用错误提示的相关头文件并且通过相关的错误提示来对错误进行诊断.

#include <errno.h> // 错误提示的相关API

在上面代码中(添加到open之后即可)添加下面代码:

// 检查错误
if (serial_port < 0) {
    printf("Error integer %i from open: %s\n", errno, strerror(errno));
}

当errno = 2,strerror(errno)返回No such file or directory时,仔细检查串口路径的同时查看设备是否插好,接入引脚是否正确.

当errno = 13,strerror(errno)返回Permission denied时,可能是没有赋予用户相应设备的权限,此时可临时使用

sudo chmod 777 /dev/ttyS3

给予用户相应权限,正规项目中应当使用相关启动文件将权限全部赋予用户,之后的文章会有相关阐述.

设备的相关设置

Linux中,可以设置的设备特性都位于termios结构体中,该结构体的使用需要包含termios.h头文件.

本文首先针对该结构体和头文件做相关的简要说明之后再将串口驱动的相关设置完整实现出来,并阐述相关的原因.

struct termios2 {
    tcflag_t c_iflag;		/* input mode flags */
    tcflag_t c_oflag;		/* output mode flags */
    tcflag_t c_cflag;		/* control mode flags */
    tcflag_t c_lflag;		/* local mode flags */
    cc_t c_line;			/* line discipline */
    cc_t c_cc[NCCS];		/* control characters */
    speed_t c_ispeed;		/* input speed */
    speed_t c_ospeed;		/* output speed */
};

通过设置上面结构体的内容并调用函数ioctl应用于相关设备即可完成对设备的设置.

struct termios options;

ioctl(fd, TCGETS2, &options); //一定要调用该函数来初始化串口配置

此处内容涉及到Linux中终端IO的相关知识点,内容较为庞杂,对这部分感兴趣的读者可以自行查找相关资料和书籍.

那么该如何设置该结构体成员的内容呢?

简而言之,输入标志(c_iflag)通过控制终端输入设备控制字符的输入(例如允许输入的奇偶校验),输出标志(c_oflag)控制驱动程序的输出,控制标志位(c_cflag)用来控制串行输入设备,而本地标志(c_lflag)影响驱动程序和用户之间的接口.

接下来我们针对串口设备来对相关参数进行设置.

c_cflag

串口需要通过设置奇偶校验位的方式对数据的正确性进行检验.奇偶校验位在数据帧中告诉接收方一次通信中是否存在错误.此处的设置往往与所要通信设备的特性密切相关,具体可以参考使用外设的用户手册.这里使用的设备是DJI DT7&DR16 2.4GHz遥控器接受系统,因此规定使用偶校验,故将此位使能:

options.c_cflag |= PARENB;

如果你的设备不设置该位,则将此位清除:

options.c_cflag &= ~PARENB;

接下来是设置停止位,如果只使用一个停止位,那么将此位清空即可.

options.c_cflag &= ~CSTOPB;

如果使用了两位停止位,则需要:

options.c_cflag |= CSTOPB;

接下来数据位中的有效位,一般设置为8bit

options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;

8bit是最常用的,如果所用传感器说明有其他设计可以从下面设置中选择

options.c_cflag |= CS5; // 5 bits per byte
options.c_cflag |= CS6; // 6 bits per byte
options.c_cflag |= CS7; // 7 bits per byte

接下来是硬件流控制的设置,一般为disable状态,如果没有特别的需求将其enable的话可能会导致你的串口无法接受数据

options.c_cflag &= ~CRTSCTS;

CREAD和CLOCAL是必须要设置的,确保程序不被其他端口干扰并能正确读入数据.

c_lflag

c_lflag的设置用来决定串口处理输入字符的方式

在处理串行数据的时候,通常还能non-canonical mode,这样的处理方式是告诉驱动程序用c by c的方式来处理数据而不是line by line.

options.c_lflag &= ~ICANON;

使能数据回显(可以不设置)

options.c_lflag &= ~ECHO;
options.c_lflag &= ~ECHOE; 
options.c_lflag &= ~ECHONL;

在处理串行数据的时候,回车等符号不应该对数据发送有什么影响,因此做出下面设置

options.c_lflag &= ~ISIG;

c_iflag

清除软件流控制

options.c_iflag &= ~(IXON | IXOFF | IXANY);

由于我们只是想完完整整的接收到发来的数据,因此不希望数据收入的数据被做处理,因此做出如下设置

options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);

c_oflag

这里的设置中和c_iflag设置类似,失能对发送数据的特殊处理即可(回车等):

options.c_oflag &= ~OPOST;
options.c_oflag &= ~ONLCR;

c_cc

这里将串口配置成非阻塞模式即可,read()API将不会做任何等待.

options.c_cc[VTIME] = 0;
options.c_cc[VMIN] = 0;

波特率定制

这里对于波特率的定制较为关键,如果只是想要几个相对常见的波特率,如下面几种:

B0,  B50,  B75,  B110,  B134,  B150,  B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800

调用下面API即可:

cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);

如果想要定制波特率,那么需要分别设置结构体中的 c_ispeed和 c_ospeed,同时使能定制波特率的标志位

options.c_cflag &= ~CBAUD;

options.c_ispeed = 100000;
options.c_ospeed = 100000;

至此串口的配置基本结束,下面给出完整配置.

void usart_init(const char* serial)
{
  int fd = open(serial, O_RDWR | O_NOCTTY | O_SYNC);

  struct termios2 options
  {
  };
  ioctl(fd, TCGETS2, &options);

  if (fd == -1)
  {
    std::cout<<"[warrior_dbus] Unable to open dbus\n"<<std::endl;
  }

  // Even parity(8E1):
  options.c_cflag &= ~CBAUD;
  options.c_cflag |= BOTHER;

  options.c_cflag |= PARENB;-
  options.c_cflag &= ~PARODD;
  options.c_cflag &= ~CSTOPB;
  options.c_cflag &= ~CSIZE;
  options.c_cflag |= CS8;

  options.c_ispeed = 100000;
  options.c_ospeed = 100000;
  options.c_iflag &= ~(IXON | IXOFF | IXANY);
  options.c_iflag &= ~IGNBRK;  // disable break processing

  /* set input mode (non−canonical, no echo,...) */
  options.c_lflag = 0;
  options.c_cc[VTIME] = 0;
  options.c_cc[VMIN] = 0;

  options.c_oflag = 0;                  // no remapping, no delays
  options.c_cflag |= (CLOCAL | CREAD);  // ignore modem controls, enable reading
  ioctl(fd, TCSETS2, &options);

  port_ = fd;
}

读取和发送则调用read()和write()接口对协议进行解包和按照协议发送即可,不再赘述.

不错