首页 > 其他分享 >【实践问题】UART通信问题解决过程

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

时间:2024-08-13 22:53:24浏览次数:12  
标签:读取 UART 实践 问题 read int fd 串口 options

近期开发了一项通过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函数,传入适当的参数,就可以实现与设备的正常通信了。

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

标签:读取,UART,实践,问题,read,int,fd,串口,options
From: https://blog.csdn.net/Ocean1994/article/details/140961544

相关文章

  • 精读代码,实战进阶&实践Task2
    背景从零入门AI生图原理&实践是Datawhale2024年AI夏令营第四期的学习活动(“AIGC”方向),基于魔搭社区“可图Kolors-LoRA风格故事挑战赛”开展的实践学习——适合想入门并实践AIGC文生图、工作流搭建、LoRA微调的学习者参与学习内容提要:从文生图实现方案逐渐进阶,教程......
  • golang 管道channel相关问题
    一funcmain(){ c1:=make(chanany) <-c1}上面代码运行肯定会报deadlock的死锁错误,但是下面这样,如果有一个协程一直在运行,则不会报错,大致就是因为协程还在运行,所属主协程main不确定是否会往管道c1中写数据,所以就会一直阻塞在这里,上面的代码块或者没有一直执行的协程......
  • 关于渗透测试靶场搭建的问题(小白经验)
    前言可能会有点啰嗦可以跳过我唠叨的环节,在我学习网络安全的过程中我遇到的问题总是多到离谱可以说比一般人多所以我比较的有点自信做这篇文章总结,当然文章末尾有整合资源懒人就直接拿吧,如果有不对的地方请毫不保留的指出谢谢——————————————————————......
  • 敏捷度量实践
    软件研发工程效能度量是一个非常宏大的话题,我们从如下几个维度展开探讨:·为什么需要度量?·如何定义有意义的度量目标?·如何实现有效度量?为什么需要度量?做一件事知道其主要目的,才能有更多的热情和积极的心态把事情做好。管理学大师彼得德鲁克曾经说过“如果你无法度量它,就......
  • inscode的会员计划的python环境问题【版本3.9.16】无法升级python
    购买了inscode的会员计划后,部署python项目遇到python环境无法升级的问题inscode的会员计划的环境是3.9.16,但是项目用的例子需要3.10以上的版本,最终本人也无法完全解决,虽然手动安装了python3.10,一切都可以实现,但是最后环境自动恢复到3.9版本,导致自己手动配置的全废了,本帖子......
  • Java解决递归造成的堆栈溢出问题
    在Java中,递归造成的堆栈溢出问题通常是因为递归调用的深度过大,导致调用栈空间不足。解决这类问题的一种常见方法是使用非递归的方式重写算法,即使用迭代替代递归。1.方法一:非递归的方式重写算法(迭代替代递归)下面通过一个典型的递归例子——计算斐波那契数列的第n项,来演示如何用迭......
  • API 设计 实践
    深度|API设计最佳实践的思考https://mp.weixin.qq.com/s/qWrSyzJ54YEw8sLCxAEKlA 深度|API设计最佳实践的思考谷朴 阿里云开发者 2019年05月09日08:10 阿里妹导读:API是模块或者子系统之间交互的接口定义。好的系统架构离不开好的API设计,而一个设计不够完善的......
  • 通过这五个问题,带你深入了解中国式报表
    一、什么是中国式报表?中国式报表,顾名思义具有中国特色的报表,通常指的是中国企业/机构在财务和业务报告方面的特有风格和规范。 二、中国式报表有什么特点?一句话就可以概括中国式报表:结构复杂、数据量大的一种报表。  ·格式复杂:为了能够展示更为详尽的数据分类和汇总信......
  • unity2022.3.9+Pico更换渲染管线后打包,人物材质不可显示问题
    为了解决字体和场景闪烁问题吗,更换渲染管线旧项目管线是URP 新的项目管线是内置管线buildin()  内置管线需要设置两个地方,可以解决人物材质不显示问题1.PICO-StereoRenderingMode选择 MultiPass模式 2,Player-OtherSetting-AutoGraphicsAPI勾选(注:项目中有......
  • windows系统配置nginx环境运行pbootcms访问首页直接404的问题
    安装pbootcms后访问后台/admin.php可以,但是直接访问首页就404。运行环境运行环境采用的是:windows+nginx+php的环境详细经过客户说伪静态规则一直无法生效,看了一下,代码放到服务器除了后台/admin.php可以访问到,其他页面都是404错误,一直各种尝试导入伪静态,但是所有页面依然是404。......