需求

需要在 linux 中配置好串口,并且通过串口收发数据

解决

打开串口

打开串口直接使用 open 函数即可,需要注意的是, flag 中的参数 O_NOCTTY, O_NDELAY.

  • O_NOCTTY:告诉Unix这个程序不想成为“控制终端”控制的程序,不说明这个标志的话,任何输入都会影响你的程序。
  • O_NDELAY:告诉Unix这个程序不关心DCD信号线状态,即其他端口是否运行,不说明这个标志的话,该程序就会在DCD信号线为低电平时停止。但是在 man 中,是把 O_NDELAYO_NONBLOCK 放在一起,并没有说明两者的区别,所以这这个参数的作用存疑。
  • open 时需要指定串口的串口号,比如 /dev/ttyS0
int OpenDevice(const char * dev)
{
    int fd = 0;
    fd = open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd < 0) {
      fprintf(stderr, "Can not open serial port %s", dev);
      return -1;
    }

    cout << "open " << dev << " Ok!" << endl;

    return fd;
}
if ((serial_fd = OpenDevice(kSerialName.c_str())) < 0) {
    return -1;
}

串口配置

  • 对于任何硬件的配置,都应该先保存原来的旧配置,然后配置新的配置,等到程序退出的时候,把硬件恢复到原来的旧配置,防止影响其他程序的运行。
  • 串口的配置主要是:波特率,数据位,校验位,停止位,流控这些。

波特率

static void SetSpeed(struct termios * opt, int speed)
{
    switch (speed) {
    case 9600:
      cfsetispeed(opt, B9600);
      cfsetospeed(opt, B9600);
      break;
    case 115200:
      cfsetispeed(opt, B115200);
      cfsetospeed(opt, B115200);
      break;
    default:
      cfsetispeed(opt, B9600);
      cfsetospeed(opt, B9600);
      break;
    }
}

数据位

static void SetBits(struct termios * opt, int bits)
{
    opt->c_cflag &= ~CSIZE;
    switch (bits) {
    case 8:
      opt->c_cflag |= CS8;
      break;
    case 7:
      opt->c_cflag |= CS7;
      break;
    case 6:
      opt->c_cflag |= CS6;
      break;
    case 5:
      opt->c_cflag |= CS5;
      break;
    default:
      opt->c_cflag |= CS8;
      break;
    }
}

校验位

static void SetParity(struct termios * opt, char parity)
{
    switch (parity) {
    case 'N':
      opt->c_cflag &= ~PARENB;
      break;
    case 'E':
      opt->c_cflag |= PARENB;
      opt->c_cflag &= ~PARODD;
      break;
    case 'O':
      opt->c_cflag |= PARENB;
      opt->c_cflag |= PARODD;
      break;
    default:
      opt->c_cflag &= ~PARENB;
      break;
    }
}

停止位

static void SetStop(struct termios * opt, int stop)
{
    switch (stop) {
    case 1:
      opt->c_cflag &= ~CSTOPB;
      break;
    case 2:
      opt->c_cflag |= CSTOPB;
      break;
    default:
      opt->c_cflag &= ~CSTOPB;
      break;
    }
}

阻塞

static void SetWait(struct termios * opt, int min, int time)
{
    opt->c_cc[VMIN] = min;
    opt->c_cc[VTIME] = time;
}

其他配置

CLOCAL 表示忽略调制解调器的相关状态行。

static void SetControl(struct termios * opt)
{
    opt->c_cflag |= CLOCAL | CREAD;
}

总的配置

int ConfigDevice(int fd, struct termios * orig, struct termios * opt,
	       int speed, int bits, char parity, int stop,
	       int min, int time)
{
    //struct termios new_term, old_term;
    int err = 0;

    if ((err = tcgetattr(fd, orig))) {
      fprintf(stderr, "get old terminal config error, %d\n", err);
      return err;
    }

    //memset(opt, 0, sizeof(struct termios));
    *opt = {};

    SetControl(opt);
    SetSpeed(opt, speed);
    SetBits(opt, bits);
    SetParity(opt, parity);
    SetStop(opt, stop);
    SetWait(opt, min, time);

    // clear flush: TCIFLUSH, TCOFLUSH, TCIOFLUSH
    tcflush(fd, TCIOFLUSH);

    if ((err = tcsetattr(fd, TCSANOW, opt))) {
      fprintf(stderr, "set new terminal config error, %d\n", err);
      return err;
    }

    cout << "serial config ok!" << endl;

    return 0;
}
if (ConfigDevice(serial_fd, &old_term, &new_term, 115200, 8, 'N', 1, 0, 0) < 0) {
    //return -1;
    goto Error;
}

恢复硬件原来的配置

int ResetDevice(int fd, struct termios * orig)
{
    int err = 0;
    tcflush(fd, TCIOFLUSH);
    if ((err = tcsetattr(fd, TCSANOW, orig))) {
      fprintf(stderr, "reset terminal config error, %d\n", err);
      return err;
    }

    cout << "reset serial ok!" << endl;

    return 0;
}
if (ResetDevice(serial_fd, &old_term) < 0) {
    //return -1;
    goto Error;
}

串口读写

串口读写如果要求不高的话,简单的 read, write 就可以解决问题。如果是希望实现稳定的功能,那么就需要使用 select.

简单读写

while (1) {
    cout << "try to read" << endl;
    int len = read(serial_fd, buf, sizeof(buf));
    if (len <= 0) {
      sleep(1);
      continue;
    }

    if (len + 1 <= sizeof(buf)) {
      buf[len] = '\0';
    } else {
      buf[sizeof(buf) - 1] = '\0';
    }

    printf("len = %d, data: %s\n", len, buf);
    write(serial_fd, buf, len);
    sleep(1);
    break;
}

使用 select 来进行读写

使用 select 有几个地方需要注意:

  • 使用前,需要把监视的 fd 都放到集合中去
  • select 如果是因为文件改动而返回的话,那么集合会被修改成只有改动文件的位置位。所以每次使用之前,都需要重新配置集合。
  • select 返回之后,再使用 ioctl 获取文件可读的字节数。
memcpy(send_buf, s, send_len);

fd_set set_inputs, set_test;
struct timeval timeout;
FD_ZERO(&set_inputs);
FD_SET(serial_fd, &set_inputs);

while (1) {
    printf("write count = %d, len = %d : %s\n",
	 send_count++, send_len, send_buf);
    //cout << "write cout: " << send_count++ << endl;
    write(serial_fd, send_buf, send_len);

    set_test = set_inputs;
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;

    int result = select(serial_fd + 1, &set_test, (fd_set *)nullptr,
		      (fd_set *)nullptr, &timeout);
    switch (result) {
    case 0:
      cout << "timeout" << endl;
      continue;
    case -1:
      fprintf(stderr, "select error\n");
      goto Error;
    default:
      cout << "select default" << endl;
      int nread = 0;
      if (FD_ISSET(serial_fd, &set_test)) {
	  ioctl(serial_fd, FIONREAD, &nread);
	  if (nread == 0) {
	      cout << "ioctl nread = 0" << endl;
	      continue;
	  }

	  nread = read(serial_fd, receive_buf, nread);
	  nread + 1 > kReceiveBufSize ?
	      receive_buf[kReceiveBufSize - 1] = '\0' :
	      receive_buf[nread] = '\0';
	  printf("receive count = %d, len = %d : %s\n",
		 receive_count++, nread, receive_buf);
	  //cout << "receive: " << receive_buf << endl;
      }
      break;
    }
}

错误处理

因为可能有多个地方会出错,所以使用统一的 goto 语句方便错误的集中处理,虽然说 goto 不提倡使用,但是当你明确知道你到底在干什么,会产生什么效果的时候, goto 也不是不可用。

Error:
    close(serial_fd);
    return -1;

强制退出

如果用户使用 C-c 来强行退出的话,还需要把串口配置回原来的配置。建议使用 sigaction 而不是旧的 SIGNAL .

void SignalHandlerInt(int sig)
{
    cout << endl << "user has press Ctrl + c" << endl;

    if (ResetDevice(serial_fd, &old_term) < 0) {
    }

    if (serial_fd > 0) {
      close(serial_fd);
    }

    exit(-1);
}
struct sigaction act;
act.sa_handler = SignalHandlerInt;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, nullptr);

参考

红皮书 Linux 程序设计第4版,第五章终端

linux串口编程 非规范模式 read()问题

read\write 堵塞与非堵塞读取串口数据

Linux 串口读取

Linux下串口通信详解(上)打开串口和串口初始化详解

Linux下串口通信详解(下)读写串口及关闭串口

Linux C/C++串口读写

Linux下实现串口读写操作

linux下的串口通信原理及编程实例

Linux串口调试过程整理(新手向)

记一次linux下串口数据丢包解决过程

Linux串口(/dev/tty*)通信

Linux串口编程(中断方式和select方式)

select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET

关于 ioctl 的 FIONREAD 参数

signal() 和 sigaction()