【实践问题】UART通信问题解决过程

近期开发了一项通过UART进行读写操作的功能。说起来并不难,但是实际操作起来还是遇到了不少问题,解决问题也费了一番周折。因此记录下来作为积累,也供遇到类似问题的同学参考。

  1. 问题背景
    当前的项目需要开发一项功能:BMC通过UART串口与另一设备通信,进行读写操作。听起来并不难,网上应该都能找到相关的程序,因此很快就着手开始去做。

  2. 初步调试
    设备环境还没准备好,由于BMC串口也是UART设备,发送命令也可以获取返回值,于是就先用BMC串口进行初步的功能测试。
    参考网上的资料,先写了如下测试代码uart.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
 
int main() {
    int fd;
    struct termios options;
    char *serialPort = "/dev/ttyUSB0"; // 串口设备文件路径,BMC串口为ttyUSB0
    speed_t baudrate = B115200; // 设定波特率
    int status;
    char buffer[255];
    int bytes_read;
 
    // 打开串口设备
    fd = open(serialPort, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1) {
        perror("open_port: Unable to open serial port - ");
        return(-1);
    }
 
    // 获取并配置串口选项
    tcgetattr(fd, &options);
    cfsetispeed(&options, baudrate); // 输入波特率
    cfsetospeed(&options, baudrate); // 输出波特率
 
    // 启用接收和发送
    options.c_cflag |= (CLOCAL | CREAD);
 
    // 更新串口配置
    status = tcsetattr(fd, TCSANOW, &options);
    if (status != 0) {
        perror("tcsetattr");
        return -1;
    }
 
    // 清空串口
    tcflush(fd, TCIFLUSH);
 
    // 向串口写入数据
    const char *data = "ifconfig\n";
    write(fd, data, sizeof(data));
 
    // 从串口读取数据
    bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0'; // 确保字符串以null结尾
        printf("Received: '%s'\n", buffer);
    }
 
    // 关闭串口
    close(fd);
 
    return 0;
}

使用此代码生成可执行文件;

gcc -o uart.c uart

BMC串口连接到笔记本USB,编译完成后将可执行文件放到笔记本中进行执行。这部分代码实现的是在BMC串口下发送ifconfig命令,并读取返回的内容。
2.1 输入回车问题
运行uart程序,同时打开minicom查看串口的输入输出情况。下图中上半部分为程序运行情况,下半部分为minicom下查看的串口输入输出情况。

在这里插入图片描述
从图中可以看出,minicom查看的串口中出现了输入的命令“ifconfig“,但没有得到执行,也没有返回内容。虽然以上代码中写入了”ifconfig\n“,但是似乎没有输入回车,导致ifconfig的命令没有得到执行。
在网上查资料,尝试在命令结尾加上‘\n’, '\r’都没有生效。最后发现在写入命令的字符串之后,再单独写入一个‘\n’,可以实现回车的效果:

    // 写入数据到串口
    const char *data = "ifconfig";
    write(fd, data, sizeof(data));
    const char *data2 = "\n";
    write(fd, data2, sizeof(data2));

minicom展示的串口中出现了ifconfig命令的执行结果:
在这里插入图片描述
2.2 uart可执行文件无法读取到返回内容问题
从mincom展示的串口中已经可以看到输出内容,但运行uart程序并没有收集到这些内容。打印上面代码中的bytes_read,发现是-1。
我觉得很奇怪,先是怀疑ifconfig命令返回的速度太快,代码中写入之后立即进行读取仍然没有捕捉到返回内容。
于是编译了一个程序uart2一直读取串口的内容:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <fcntl.h>
 
int main() {
    int fd;
    struct termios options;
    char *serialPort = "/dev/ttyUSB0"; // 串口设备文件路径,BMC串口为ttyUSB0
    speed_t baudrate = B115200; // 设定波特率
    int status;
    char buffer[255];
    int bytes_read;
 
    // 打开串口设备
    fd = open(serialPort, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1) {
        perror("open_port: Unable to open serial port - ");
        return(-1);
    }
 
    // 获取并配置串口选项
    tcgetattr(fd, &options);
    cfsetispeed(&options, baudrate); // 输入波特率
    cfsetospeed(&options, baudrate); // 输出波特率
 
    // 启用接收和发送
    options.c_cflag |= (CLOCAL | CREAD);
 
    // 更新配置到串口
    status = tcsetattr(fd, TCSANOW, &options);
    if (status != 0) {
        perror("tcsetattr");
        return -1;
    }
 
    // 清空串口
    tcflush(fd, TCIFLUSH);
	do
	{
	bytes_read = read(fd,buffer, sizeof(buffer));
	if(bytes_read > 0)
	{
		printf("===bytes_read:%d, buffer:%s\n",bytes_read, buffer);
	}
	}while(1)

修改程序uart.c,修改为只向串口写入数据。
先将程序uart2运行起来,再通过uart程序写入数据,此时发现uart2程序能够读取到串口中的返回内容。
分别打印了uart程序写入数据结束和uart2程序第一次读取到数据时的时间,发现二者之间的间隔有几百毫秒,在uart程序中写入完毕到开始读取的时间间隔远小于此,不会存在来不及读取的问题。
经过多方查找网上的资料以及测试,发现配置串口参数时忽略的两个重要参数才是这个问题的关键:VTIME和VMIN。查找这两个参数的相关解释如下:

VTIME指定了等待的时间,VMIN指定了读取字符的最小数量。
它们不同组合地取值会得到不同的结果,分别如下:
1.当VTIME>0,VMIN>0时。read调用将保持阻塞直到读取到第一个字符,读到了第一个字符之后开始计时,此后若 时间到了VTIME或者时间未到但已读够了VMIN个字符则会返回;若在时间未到之前又读到了一个字符(但此时读到的总数仍不够VMIN)则计时重新开始。
2. 当VTIME>0,VMIN=0时。read调用读到数据则立即返回,否则将为每个字符最多等待VTIME时间。
3. 当VTIME=0,VMIN>0时。read调用一直阻塞,直到读到VMIN个字符后立即返回。
4. 若在open或fcntl设置了O_NDELALY或O_NONBLOCK标志,read调用不会阻塞而是立即返回,那么VTIME和VMIN就没有意义,效果等同于与把VTIME和VMIN都设为了0。

而我的代码在打开文件的时候确实设置了O_NDELALY标志,导致的结果就是read只读一次,没有等到能够读取到数据就立即返回了。

于是据此将以上串口初始化部分封装为函数:

int UartInit(char* SerialPort, speed_t baudrate) {
  // UART串口配置
  struct termios options;
  int status;

  // 打开串口设备
  int fd = open(SerialPort, O_RDWR | O_NOCTTY);
  if (fd == -1) {
    perror("open_port: Unable to open serial port - ");
    return (-1);
  }

  // 获取并配置串口选项
  tcgetattr(fd, &options);
  cfsetispeed(&options, baudrate); // 输入波特率
  cfsetospeed(&options, baudrate); // 输出波特率

  // 设置串口选项:无奇偶校验位,8位数据位,1位停止位,无软件流控
  options.c_cflag &= ~PARENB;
  options.c_cflag &= ~CSTOPB;
  options.c_cflag &= ~CSIZE;
  options.c_cflag |= CS8;
  options.c_cflag &= ~CRTSCTS; // 无硬件流控

  // 最少读取1字节
  options.c_cc[VMIN] = 1;
  options.c_cc[VTIME] = 0;
  tcflush(fd, TCIFLUSH); // 清空输入缓冲区

  // 使用配置后的选项
  status = tcsetattr(fd, TCSANOW, &options);
  if (status != 0) {
    perror("tcsetattr");
    return -1;
  }

  return fd;
}

这样修改之后,只需要运行一个程序,写入命令之后再读取即可。

2.3 读取返回内容何时终止问题

读写的问题解决了,但是又面临这样一个问题:发送命令后,设备的返回内容长度是不确定的,如何判断读取了全部的返回内容呢?也就是说何时停止读取并处理读取到的数据呢?
其实通过串口与设备的交互与在Linux系统类似,在终端下发送一条命令,按下回车,返回所需内容之后会显示一个root@localhost样式的提示符,可以根据设备实际返回的提示符确认何时停止读取。
于是将初始化,读,写分别封装成了函数,修改后的代码如下:

#include "./include/uart.h"

/*
  Func:UartInit
  serialport:串口设备名称
  baudrate:串口波特率

  ret:fd,打开串口设备的文件描述符
  
*/
int UartInit(char* SerialPort, speed_t baudrate) {
  // UART串口配置
  struct termios options;
  int status;

  // 打开串口设备
  int fd = open(SerialPort, O_RDWR | O_NOCTTY);
  if (fd == -1) {
    perror("open_port: Unable to open serial port - ");
    return -1;
  }

  // 获取并配置串口选项
  tcgetattr(fd, &options);
  cfsetispeed(&options, baudrate); // 输入波特率
  cfsetospeed(&options, baudrate); // 输出波特率

  // 设置串口选项:无奇偶校验位,8位数据位,1位停止位,无软件流控
  options.c_cflag &= ~PARENB;
  options.c_cflag &= ~CSTOPB;
  options.c_cflag &= ~CSIZE;
  options.c_cflag |= CS8;
  options.c_cflag &= ~CRTSCTS; // 无硬件流控

  // 最少读取1字节
  options.c_cc[VMIN] = 1;
  options.c_cc[VTIME] = 0;
  tcflush(fd, TCIFLUSH); // 清空输入缓冲区

  // 使用配置后的选项
  status = tcsetattr(fd, TCSANOW, &options);
  if (status != 0) {
    perror("tcsetattr");
    return -1;
  }

  return fd;
}

/*
  Func:UartWrite
  fd: 打开的串口文件描述符;
  data:向串口发送的命令;
  datalength:发送命令长度;
*/

int UartWrite(int fd, const char* data, int datalength)
{
  if(data == NULL)
  {
    return -1;
  }

  // 清空串口
  tcflush(fd, TCIFLUSH);

  //向串口写入命令
  write(fd, data, datalength);

  //写入命令后写入回车,发送命令
  char cr = '\r';
  write(fd, &cr, 1);

  return 0;

}

/*
  Func:UartRead, 
  fd: 打开的串口文件描述符;
  data: 读取的内容
  endstr:标志读取内容结束的字符串
  ret: datalength:读取内容的长度
*/

int UartRead(int fd, char* data, const char* endstr)
{
  char buffer[255];
  int bytes_read = 0;
  int datalength = 0;


  do
  {
    bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read > 0) 
    {
        strncat(data, buffer, bytes_read);
        datalength += bytes_read;
        if(datalength > sizeof(endstr) )
        {
            if(strncmp(&(data[datalength-strlen(endstr)]),endstr,strlen(endstr) ) == 0)
            {
                //End reading
                datalength = datalength - strlen(endstr);
                break;
            }
        }
    }
  } while(1);
  data[datalength] = '\0';

  return datalength;
}

这样在程序中根据需要调用UartInit, UartWrite, UartRead函数,传入适当的参数,就可以实现与设备的正常通信了。

问题解决之后回头来看,其实原理和代码都不难,主要还是因为自己没有接触过,实践经验比较少,以后继续在实践中积累,学到的东西无论多少,都是一种进步。

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值