串口通信

发布时间 2023-04-04 08:36:36作者: 王曲

1.终端设备文件

在 Linux 下终端的设备文件都位于/dev/目录下,以 tty*开头的字符命名,可使用如下命令查看:

ls /dev/tty*

img

  • 可以看到有个名为“ttymxc0”的设备,“ttymxc0”就是开发板的串口 1,它已被默认被用在命令行的终端上。

  • stty 命令,用于显示或设置终端的各种参数,如波特率、字符大小、奇偶校验、停止位等。

#输出当前终端参数
stty
#查看ttymxc0的参数
stty -F /dev/ttymxc0
#设置通讯速率,其中ispeed为输入速率,ospeed为输出速率
stty -F /dev/ttymxc0 ispeed 115200 ospeed 115200

2.命令行串口通信

  • 使用 Type-C 接口的数据线连接开发板和 PC
  • 打开 MobaXterm 终端
  • 设置波特率,根据第一步的 stty 命令中得到的 ttymxc0 的波特率
  • 如果开发板已经连接 WiFi,在开发板上使用 ifconfig 命令查看 ip 地址
  • 在 Windows 或 Ubuntu 中使用 ssh 命令连接开发板ssh debian@192.168.31.xxx
    以下命令在 ssh 终端中执行
echo "Hello" > /dev/ttymxc0

然后将会在 MobaXterm 终端中看到 Hello,如下图。
左为 ssh 终端,右为 MobaXterm 终端
img


3.Linux 下的串口调试工具

  • minicom
#安装minicom
sudo apt-get install minicom
#打开minicom设置
sudo minicom -s

设置界面如下图
img
串口设置界面如下图
img
进入 minicom

sudo minicom

4.串口通信实验代码

  • 串口通信实验代码
#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>

/第一部分代码/
//根据具体的设备修改
const char default_path[] = "/dev/ttymxc2";
// const char default_path[] = "/dev/ttymxc2";


int main(int argc, char *argv[])
{
   int fd;
   int res;
   char *path;
   char buf[1024] = "Embedfire tty send test.\n";

   /第二部分代码/

   //若无输入参数则使用默认终端设备
   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;
   }

   /第三部分代码/
   struct termios opt;

   //清空串口接收缓冲区
   tcflush(fd, TCIOFLUSH);
   // 获取串口参数opt
   tcgetattr(fd, &opt);

   //设置串口输出波特率
   cfsetospeed(&opt, B9600);
   //设置串口输入波特率
   cfsetispeed(&opt, B9600);
   //设置数据位数
   opt.c_cflag &= ~CSIZE;
   opt.c_cflag |= CS8;
   //校验位
   opt.c_cflag &= ~PARENB;
   opt.c_iflag &= ~INPCK;
   //设置停止位
   opt.c_cflag &= ~CSTOPB;

   //更新配置
   tcsetattr(fd, TCSANOW, &opt);

   printf("Device %s is set to 9600bps,8N1\n",path);

   /第四部分代码/

   do {
      //发送字符串
      write(fd, buf, strlen(buf));
      //接收字符串
      res = read(fd, buf, 1024);
      if (res >0 ) {
      //给接收到的字符串加结束符
      buf[res] = '\0';
      printf("Receive res = %d bytes data: %s\n",res, buf);
   } while (res >= 0);

   printf("read error,res = %d",res);

   close(fd);
   return 0;
}
  • 第一部分:定义了默认使用的串口终端设备路径及其它一些变量。

  • 第二部分:根据 main 是否有输入参数确认使用哪个设备路径,并通过 open 的 O_RDWR 读写模式打开该设备。

  • 第三部分:定义了一个结构体 termios 用于获取、设置终端设备的参数,包括波特率、数据位数、校验位等, 这是本章的重点,在下一小节详细说明。

  • 第四部分:在 while 循环中对终端设备使用 read 和 write 进行读写,从而控制串口收发数据。 代码中在接收到的内容末尾加了’0’结束符,主要是为了方便使用字符串的方式处理内容。

termios 结构体

示例代码中的第三部分,使用了 termios 结构体,它是在 POSIX 规范中定义的标准接口。 Linux 系统利用 termios 来设置串口的参数,它是在头文件<termios.h>包含的<bits/termios.h>中定义的, 该文件中还包含了各个结构体成员可使用的宏值, 请自己使用 locate 命令查找该文件打开来阅读,关于 termios 结构体的定义摘录如下所示。

struct termios {
   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 */
   #define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
   #define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};
  • c_iflag:输入(input)模式标志,用于控制如何对串口输入的字符进行处理,常用的选项值见下表。
选项值作用
INPCK 启用输入奇偶校验
IGNPAR 忽略奇偶校验错误
IGNRCR 忽略回车符
IXON 启用软件流控制
IXOFF 关闭软件流控制
  • c_oflag:输出(output)模式标志,用于控制如何对串口输出的字符进行处理,常用的选项值见下表。
选项值作用
ONLCR 将换行符 NL 转换为回车换行符 CRNL
OCRNL 将回车符 CR 转换为换行符 NL
ONLRET 不输出回车
OFILL 使用填充字符填充输出行
  • c_cflag:控制(control)模式标志,用于控制串口的通信参数,常用的选项值见下表。
选项值作用
CSTOPB 设置两个停止位
CSIZE 字符大小掩码
PARENB 启用奇偶校验
PARODD 奇校验
  • c_lflag:本地(local)模式标志,用于控制串口的本地模式,常用的选项值见下表。
选项值作用
ECHO 打开回显功能
ICANON 使用标准输入模式
ISIG 允许信号产生
ECHONL 若该标志位和 ICANON 标志位同时被设置,则回显换行符 NL
  • c_cc:控制字符,用于控制串口的控制字符,常用的选项值见下表。
选项值作用
VMIN 读取字符的最小数量
VTIME 读取第一个字符的等待时间(单位为 0.1 秒)
VINTR 中断字符
VERASE 擦除字符
  • c_ispeed 和 c_ospeed:记录串口的输入和输出波特率(input speed 和 output speed), 部分可取值如下代码所示,宏定义中的数字以“0”开头,在 C 语言中这是表示 8 进制数字的方式。
//注意以0开头的数字在是C语言的8进制数字形式
#define B1200 0000011
#define B1800 0000012
#define B2400 0000013
#define B4800 0000014
#define B9600 0000015
#define B19200 0000016
#define B38400 0000017
  • 宏定义:termios 结构体内部有_HAVE_STRUCT_TERMIOS_C_ISPEED 和_HAVE_STRUCT_TERMIOS_C_OSPEED 两个宏定义,它们的宏值都为 1,表示它支持 c_ispeed 和 c_ospeed 表示方式, 部分标准中不支持使用这两个结构体成员表示波特率,而只使用 c_cflag 来表示。

配置串口波特率

  • 修改串口波特率
//定义termios型变量opt
struct termios opt;
//fd是使用open打开设备文件得到的文件句柄
// 获取串口参数opt
tcgetattr(fd, &opt);
//设置串口输出波特率
cfsetospeed(&opt, B9600);
//设置串口输入波特率
 cfsetispeed(&opt, B9600);
 //更新配置
 tcsetattr(fd, TCSANOW, &opt);

代码中使用到了头文件 termios.h 的库函数 tcgetattr、cfsetispeed、cfsetospeed 和 tcsetattr。

其中 tcgetattr 和 tcsetattr 函数分别用于读取和设置串口的参数,原型如下:

#include <termios.h>

#include <unistd.h>

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
  • 形参fd:指定串口设备文件的文件描述符。

  • 形参termios_p:指向串口参数的结构体termios,tcgetattr读取到的参数会保存在该结构体中, 而tcsetattr则根据该结构体配置设备参数。

  • 形参optional_actions:仅tcsetattr函数有这个参数,它用于指示配置什么时候生效, 它支持的配置参数如下:

  • TCSANOW表示立即生效。

  • TCSADRAIN表示待所有数据传输结束后配置生效。

  • TCSAFLUSH表示输入输出缓冲区为空时配置有效。

跟示例代码中的一样,通常都使用选项TCSANOW,让写入的参数配置立马生效。

代码中的cfsetispeed和cfsetospeed函数分别用于设置termios结构体的输入和输出波特率, 另外还有cfsetspeed函数可以同时设置输入和输出波特率参数为相同的值,原型如下:

int cfsetispeed(struct termios *termios_p, speed_t speed);

int cfsetospeed(struct termios *termios_p, speed_t speed);

int cfsetspeed(struct termios *termios_p, speed_t speed);

使用这些函数要注意两点:

  • speed参数需要使用类似前面代码定义的宏值。

  • 这三个函数只是修改了termios的opt变量的内容,并没有写入到设备文件, 因此在修改完它的内容后,还需要调用tcsetattr函数,把opt变量中的配置写入到设备,使它生效。

配置串口停止位

c_cflag中的标志位CSTOPB,用于设置串口通信停止位的长度。若该值为0, 则停止位的长度为1位;若设置该位为1,则停止位的长度为两位,具体实现如下所示。

//在bits/termios.h文件中关于CSTOPB的定义
//注意以0开头的数字在是C语言的8进制数字形式
#define CSTOPB 0000100
//
//设置停止位示例
//定义termios型变量opt
struct termios opt;
// 获取串口参数opt
tcgetattr(fd, &opt);

/* 设置停止位*/
switch (stopbits)
{
   //设置停止位为1位
   case 1:
   opt.c_cflag &= ~CSTOPB;
   break;
   //设置停止位为2位
   case 2:
   opt.c_cflag |= CSTOPB;
   break;
}

//更新配置
tcsetattr(fd, TCSANOW, &opt);

示例代码依然是采取了获取当前参数、修改配置、更新配置的套路。

修改配置的代码中使用了“&=~”、“|=”这种位操作方法,主要是为了避免影响到变量中的其它位, 因为在c_cflag的其它位还包含了校验位、数据位和波特率相关的配置,如果直接使用“=”赋值, 那其它配置都会受到影响,而且操作不方便。在后面学习裸机开发,对寄存器操作时会经常用到这种方式。 若没接触过这些位操作方式,可参考本书附录中《第65章 位操作方法》的说明。

简单来说,示例中的“&=~”把c_cflag变量中CSTOPB对应的数据位清0, 而“|=”则把c_cflag变量中CSTOPB对应的数据位置1, 达到在不影响其它配置的情况下把停止位配置为1位或两位。

配置串口校验位

配置串口的校验位涉及到termios成员c_cflag的标志位PARENB、PARODD 以及c_iflag的标志位INPCK, 其中PARENB和INPCK共同决定是否使能奇偶校验,而PARODD 决定使用奇校验还是偶校验, 配置的示例代码如下所示。

//bits/termios.h的位定义
//注意以0开头的数字在是C语言的8进制数字形式
/* c_cflag bit meaning */
#define PARENB 0000400
#define PARODD 0001000
/* c_iflag bits */
#define INPCK 0000020
//
//定义termios型变量opt
struct termios opt;

// 获取串口参数opt
tcgetattr(fd, &opt);

switch (parity)
{
   case 'n':
   case 'N':
      options.c_cflag &= ~PARENB; /* 不使用奇偶校验 */
      options.c_iflag &= ~INPCK; /* 禁止输入奇偶检测 */
      break;

   case 'o':
   case 'O':
      options.c_cflag |= PARENB; /* 启用奇偶效验 */
      options.c_iflag |= INPCK; /* 启用输入奇偶检测 */
      options.c_cflag |= PARODD ; /* 设置为奇效验 */
      break;

   case 'e':
   case 'E':
      options.c_cflag |= PARENB; /* 启用奇偶效验 */
      options.c_iflag |= INPCK; /* 启用输入奇偶检测 */
      options.c_cflag &= ~PARODD; /* 设置为偶效验*/
      break;
}

//更新配置
tcsetattr(fd, TCSANOW, &opt);

配置非常简单,不校验时同时把PARENB和INPCK位清零,启用校验时把PARENB和INPCK同时置1, 而PARODD为1时指定为奇校验,为0时是偶校验。

配置串口数据位

串口的数据位是由c_cflag中的CSIZE配置的,由于串口支持5、6、7、8位的配置,一共有四种, 所以在c_cflag中使用了两个数据位进行配置,在配置前我们需要先对CSIZE数据位清零, 然后再赋予5、6、7、8的宏配置值,具体代码如下所示。

//bits/termios.h的位定义
//注意以0开头的数字在是C语言的8进制数字形式
#define CSIZE 0000060
#define CS5 0000000
#define CS6 0000020
#define CS7 0000040
#define CS8 0000060
//
//定义termios型变量opt
struct termios opt;
// 获取串口参数opt
tcgetattr(fd, &opt);

//先清除CSIZE数据位的内容
opt.c_cflag &= ~CSIZE;

switch (databits) /*设置数据位数*/
{
   case 5:
      opt.c_cflag |= CS5;
      break;
   case 6:
      opt.c_cflag |= CS6;
      break;
   case 7:
      opt.c_cflag |= CS7;
      break;
   case 8:
      opt.c_cflag |= CS8;
      break;
}
//更新配置
tcsetattr(fd, TCSANOW, &opt);

ioctl系统调用

  • 跟设备文件相关的函数操作
//前面实验中对设备文件操作的函数
fd = open(path, O_RDWR);
write(fd, buf, strlen(buf));
read(fd, buf, 1024);
close(fd);
tcgetattr(fd, &opt);
tcsetattr(fd, TCSANOW, &opt);

仔细分析这些操作,发现万里晴空出现了两朵乌云。open、write、read、close都是Linux的系统调用, 而tcgetattr、tcsetattr则是库函数。而且按照传统的认知,文件操作大都是跟内容挂勾的, 上一章节的input事件设备文件记录了上报的事件信息,而tty设备的文件却不是记录串口终端的配置参数, 因为对文件的write操作是对外发送数据,而read则是读取接收到的数据, 也就是说,“tty*”文件并没有记录串口终端的配置信息, 那么tcgetattr、tcsetattr这两个函数究竟做了什么神仙操作?

  • ioctl原型
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
  • 参数fd:与write、read类似,fd文件句柄指定要操作哪个文件。

  • 参数reques:操作请求的编码,它是跟硬件设备驱动相关的,不同驱动设备支持不同的编码, 驱动程序通常会使用头文件提供可用的编码给上层用户。

  • 参数“…”:这是一个没有定义类型的指针,它与printf函数定义中的“…”类似, 不过ioctl此处只能传一个参数。部分驱动程序执行操作请求时可能需要配置参数, 或者操作完成时需要返回数据,都是通过此处传的指针进行访问的。

使用ioctl代替tcgetattr和tcsetattr

ioctl系统调用是Linux系统中最为强大的系统调用之一,它可以用来控制设备驱动程序, 也就是说,它可以用来控制设备文件,而设备文件就是我们的串口终端, 所以我们可以使用ioctl来代替tcgetattr和tcsetattr函数, 从而实现对串口终端的配置,具体代码如下所示。

#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>

//根据具体的设备修改
const char default_path[] = "/dev/ttymxc2";
// const char default_path[] = "/dev/ttymxc2";


int main(int argc, char *argv[])
{
   int fd;
   int res;
   struct termios opt;
   char *path;
   char buf[1024] = "Embedfire tty send test.\n";

   //若无输入参数则使用默认终端设备
   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;
   }
   //清空串口接收缓冲区
   tcflush(fd, TCIOFLUSH);
   // 获取串口参数opt
   // tcgetattr(fd, &opt);

   res = ioctl(fd,TCGETS, &opt);

   opt.c_ispeed = opt.c_cflag & (CBAUD | CBAUDEX);
   opt.c_ospeed = opt.c_cflag & (CBAUD | CBAUDEX);

   //输出宏定义的值,方便对比
   printf("Macro B9600 = %#o\n",B9600);
   printf("Macro B115200 = %#o\n",B115200);
   //输出读取到的值
   printf("ioctl TCGETS,opt.c_ospeed = %#o\n", opt.c_ospeed);
   printf("ioctl TCGETS,opt.c_ispeed = %#o\n", opt.c_ispeed);
   printf("ioctl TCGETS,opt.c_cflag = %#x\n", opt.c_cflag);

   speed_t change_speed = B9600;
   if(opt.c_ospeed == B9600)
      change_speed = B115200;

   //设置串口输出波特率
   cfsetospeed(&opt, change_speed);
   //设置串口输入波特率
   cfsetispeed(&opt, change_speed);
   //设置数据位数
   opt.c_cflag &= ~CSIZE;
   opt.c_cflag |= CS8;
   //校验位
   opt.c_cflag &= ~PARENB;
   opt.c_iflag &= ~INPCK;
   //设置停止位
   opt.c_cflag &= ~CSTOPB;

   //更新配置
   // tcsetattr(fd, TCSANOW, &opt);
   res = ioctl(fd,TCSETS, &opt);

   //再次读取
   res = ioctl(fd,TCGETS, &opt);

   opt.c_ispeed = opt.c_cflag & (CBAUD | CBAUDEX);
   opt.c_ospeed = opt.c_cflag & (CBAUD | CBAUDEX);

   printf("ioctl TCGETS after TCSETS\n");

   //输出读取到的值
   printf("ioctl TCGETS,opt.c_ospeed = %#o\n", opt.c_ospeed);
   printf("ioctl TCGETS,opt.c_ispeed = %#o\n", opt.c_ispeed);
   printf("ioctl TCGETS,opt.c_cflag = %#x\n", opt.c_cflag);

   do{
      //发送字符串
      write(fd, buf, strlen(buf));
      //接收字符串
      res = read(fd, buf, 1024);
      if(res >0 ){
         //给接收到的字符串加结束符
         buf[res] = '\0';
         printf("Receive res = %d bytes data: %s\n",res, buf);
      }
   }while(res >= 0);

   printf("read error,res = %d",res);

   close(fd);
   return 0;
}