当前位置: 首页 > article >正文

IMX6ULL_Pro开发板的串口应用程序实例(利用TTY子系统去使用串口)

前提和背景知识

本博文中借助于TTY子系统来使用IMX6ULL_Pro开发板上的串口,关于TTY子系的详细介绍和为什么可以利用TTY子系统来使用开发板上的串口,,详情见博文 https://blog.csdn.net/wenhao_ir/article/details/145431655 【请从这篇博文的开头处开始看】

完整源代码

文件名:serial_send_recv.c

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio,oldtio;
	
	if ( tcgetattr( fd,&oldtio) != 0) { 
		perror("SetupSerial 1");
		return -1;
	}
	
	bzero( &newtio, sizeof( newtio ) );
	newtio.c_cflag |= CLOCAL | CREAD; 
	newtio.c_cflag &= ~CSIZE; 

	newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
	newtio.c_oflag  &= ~OPOST;   /*Output*/

	switch( nBits )
	{
	case 7:
		newtio.c_cflag |= CS7;
	break;
	case 8:
		newtio.c_cflag |= CS8;
	break;
	}

	switch( nEvent )
	{
	case 'O':
		newtio.c_cflag |= PARENB;
		newtio.c_cflag |= PARODD;
		newtio.c_iflag |= (INPCK | ISTRIP);
	break;
	case 'E': 
		newtio.c_iflag |= (INPCK | ISTRIP);
		newtio.c_cflag |= PARENB;
		newtio.c_cflag &= ~PARODD;
	break;
	case 'N': 
		newtio.c_cflag &= ~PARENB;
	break;
	}

	switch( nSpeed )
	{
	case 2400:
		cfsetispeed(&newtio, B2400);
		cfsetospeed(&newtio, B2400);
	break;
	case 4800:
		cfsetispeed(&newtio, B4800);
		cfsetospeed(&newtio, B4800);
	break;
	case 9600:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	case 115200:
		cfsetispeed(&newtio, B115200);
		cfsetospeed(&newtio, B115200);
	break;
	default:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	}
	
	if( nStop == 1 )
		newtio.c_cflag &= ~CSTOPB;
	else if ( nStop == 2 )
		newtio.c_cflag |= CSTOPB;
	
	newtio.c_cc[VMIN]  = 1;  /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
	newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: 
	                         * 比如VMIN设为10表示至少读到10个数据才返回,
	                         * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
	                         * 假设VTIME=1,表示: 
	                         *    10秒内一个数据都没有的话就返回
	                         *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
	                         */

	tcflush(fd,TCIFLUSH);
	
	if((tcsetattr(fd,TCSANOW,&newtio))!=0)
	{
		perror("com set error");
		return -1;
	}
	//printf("set done!\n");
	return 0;
}

int open_port(char *com)
{
	int fd;
	//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
	fd = open(com, O_RDWR|O_NOCTTY);
    if (-1 == fd){
		return(-1);
    }
	
	  if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
	  {
			printf("fcntl failed!\n");
			return -1;
	  }
  
	  return fd;
}


/*
 * ./serial_send_recv <dev>
 */
int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;

	/* 1. open */

	/* 2. setup 
	 * 115200,8N1
	 * RAW mode
	 * return data immediately
	 */

	/* 3. write and read */
	
	if (argc != 2)
	{
		printf("Usage: \n");
		printf("%s </dev/ttySAC1 or other>\n", argv[0]);
		return -1;
	}

	fd = open_port(argv[1]);
	if (fd < 0)
	{
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	iRet = set_opt(fd, 115200, 8, 'N', 1);
	if (iRet)
	{
		printf("set port err!\n");
		return -1;
	}

	printf("Enter a char: "); // 输入的字符利用下面的函数scanf存储在变量c中
	while (1)
	{
		scanf("%c", &c);
		iRet = write(fd, &c, 1);
		if (iRet == 1)
			// printf("write: 0x%02x %c\n", c, c);
			printf("Write→ASCII: %d\n", c);
		else
			printf("can not write data\n");

		iRet = read(fd, &c, 1);
		if (iRet == 1)
			printf("Read→ASCII: %d\n", c);
		else
			printf("can not read data\n");
	}

	return 0;
}


设备文件打开函数open_port的关键代码分析

函数open_port的完整代码

代码如下:

int open_port(char *com)
{
	int fd;
	//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
	fd = open(com, O_RDWR|O_NOCTTY);
    if (-1 == fd){
		return(-1);
    }
	
	  if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
	  {
			printf("fcntl failed!\n");
			return -1;
	  }
  
	  return fd;
}

关键代码fd = open(com, O_RDWR|O_NOCTTY);

fd = open(com, O_RDWR|O_NOCTTY);
  • open(com, flags):使用 open 系统调用打开串口设备。
  • com是输入函数,代表串口终端的TTY设备文件的路径和名字。
  • O_RDWR:以读写模式打开设备。
  • O_NOCTTY:如果我把代码编译成ELF可执行程序,然后在串口终端中运行这个程序,那么实际上这个标志位起不了作用,因为此时进程是肯定有控制终端的。关于这个O_NOCTTY标志位的详解,请参见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/146177988

关键代码if(fcntl(fd, F_SETFL, 0)<0)

if(fcntl(fd, F_SETFL, 0) < 0) /* 设置串口为阻塞状态 */
{
    printf("fcntl failed!\n");
    return -1;
}
  • fcntl(fd, F_SETFL, 0) 用于设置文件状态标志:
    • F_SETFLFile Set Flags的英文缩写。
    • 0 表示清除所有非阻塞模式的标志(即阻塞模式)。
    • 这意味着:
      • 读取操作会阻塞,直到有数据可读。
      • 写入操作会阻塞,直到数据写入完成。
    • 如果 fcntl 失败,则返回 -1

串口通信参数设置函数set_opt的详解

函数set_opt的完整代码

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
	struct termios newtio,oldtio;
	
	if ( tcgetattr( fd,&oldtio) != 0) { 
		perror("SetupSerial 1");
		return -1;
	}
	
	bzero( &newtio, sizeof( newtio ) );
	newtio.c_cflag |= CLOCAL | CREAD; 
	newtio.c_cflag &= ~CSIZE; 

	newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
	newtio.c_oflag  &= ~OPOST;   /*Output*/

	switch( nBits )
	{
	case 7:
		newtio.c_cflag |= CS7;
	break;
	case 8:
		newtio.c_cflag |= CS8;
	break;
	}

	switch( nEvent )
	{
	case 'O':
		newtio.c_cflag |= PARENB;
		newtio.c_cflag |= PARODD;
		newtio.c_iflag |= (INPCK | ISTRIP);
	break;
	case 'E': 
		newtio.c_iflag |= (INPCK | ISTRIP);
		newtio.c_cflag |= PARENB;
		newtio.c_cflag &= ~PARODD;
	break;
	case 'N': 
		newtio.c_cflag &= ~PARENB;
	break;
	}

	switch( nSpeed )
	{
	case 2400:
		cfsetispeed(&newtio, B2400);
		cfsetospeed(&newtio, B2400);
	break;
	case 4800:
		cfsetispeed(&newtio, B4800);
		cfsetospeed(&newtio, B4800);
	break;
	case 9600:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	case 115200:
		cfsetispeed(&newtio, B115200);
		cfsetospeed(&newtio, B115200);
	break;
	default:
		cfsetispeed(&newtio, B9600);
		cfsetospeed(&newtio, B9600);
	break;
	}
	
	if( nStop == 1 )
		newtio.c_cflag &= ~CSTOPB;
	else if ( nStop == 2 )
		newtio.c_cflag |= CSTOPB;
	
	newtio.c_cc[VMIN]  = 1;  /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
	newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间: 
	                         * 比如VMIN设为10表示至少读到10个数据才返回,
	                         * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
	                         * 假设VTIME=1,表示: 
	                         *    10秒内一个数据都没有的话就返回
	                         *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
	                         */

	tcflush(fd,TCIFLUSH);
	
	if((tcsetattr(fd,TCSANOW,&newtio))!=0)
	{
		perror("com set error");
		return -1;
	}
	//printf("set done!\n");
	return 0;
}

这个函数 set_opt 用于配置 Linux 下的串口通信参数,包括波特率、数据位、校验位、停止位等。


函数的各参数意义说明

int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
  • 参数

    • fd:串口设备的文件描述符
    • nSpeed:波特率,如 9600115200
    • nBits:数据位,通常为 78
    • nEvent:校验位,可选 'N'(无校验)、'E'(偶校验)、'O'(奇校验)
    • nStop:停止位,1 表示 1 位停止位,2 表示 2 位停止位
  • 返回值

    • 成功返回 0
    • 失败返回 -1 并打印错误信息

结构体termios的介绍

关于结构体termios的详细介绍,请参看我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/146187742

函数的具体内容解析

0. 初始化结构体termios实例

struct termios newtio,oldtio;

结构体newtio和oldtio用来存储串口的配置实信息,关于结构体termios的详细介绍,请参看我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/146187742

1. 获取原始串口配置

if ( tcgetattr( fd, &oldtio) != 0) { 
    perror("SetupSerial 1");
    return -1;
}
  • tcgetattr(fd, &oldtio) 获取当前串口设置并存入 oldtio
  • tcgetattr 是 Linux TTY(终端)子系统 中的一个函数,它用于获取终端设备(包括串口)的当前属性。
  • 如果获取失败,打印错误并返回 -1
  • 从后面及整个代码来看,并没有再次用到oldtio所以其实这段代码可以删去。

2. 初始化新串口设置

bzero(&newtio, sizeof(newtio));
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
  • bzero(&newtio, sizeof(newtio)):清空 newtio 结构体
  • CLOCAL:让Linux系统忽略DCD (Data Carrier Detect)信号,详细的介绍见博文 https://blog.csdn.net/wenhao_ir/article/details/146187742 【搜索“要搞清楚这个字段值”】
  • CREAD:启用接收器
  • CSIZE:清除数据位设置(后续要重新设置)

3. 关闭与TTY终端有关的对本地输入的处理方式

newtio.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);  /*Input*/
newtio.c_oflag  &= ~OPOST;   /*Output*/

因为我们并不把串口当成终端来使用,所以需要关闭与TTY终端有关的对本地输入的处理方式。

  • ICANON:关闭标准输入(即设置为原始模式,不使用回车换行等字符处理)
  • ECHOECHOE:关闭回显
  • ISIG:关闭信号处理(如 CTRL+C 终止)
  • OPOST:关闭输出处理(即发送的字节不会被修改)

上面几个值的详细解释见博文 https://blog.csdn.net/wenhao_ir/article/details/146187742 【搜索“TTY终端的本地输入处理方式”】


4. 设置数据位

switch(nBits)
{
    case 7:
        newtio.c_cflag |= CS7;
        break;
    case 8:
        newtio.c_cflag |= CS8;
        break;
}
  • CS7:7 数据位
  • CS8:8 数据位(更常见)
  • 具体在这里,我们设置的是8 位数据位。

5. 设置校验位

switch(nEvent)
{
    case 'O': // 奇校验
        newtio.c_cflag |= PARENB; // 使能校验
        newtio.c_cflag |= PARODD; // 设置奇校验
        newtio.c_iflag |= (INPCK | ISTRIP); // 使能奇偶校验和去除第8位
        break;
    case 'E': // 偶校验
        newtio.c_iflag |= (INPCK | ISTRIP);
        newtio.c_cflag |= PARENB; // 使能校验
        newtio.c_cflag &= ~PARODD; // 设置偶校验
        break;
    case 'N': // 无校验
        newtio.c_cflag &= ~PARENB;
        break;
}
  • 'O'(奇校验):PARENB 使能校验,PARODD 设为奇校验,INPCK | ISTRIP 使能奇偶校验并去掉最高位
  • 'E'(偶校验):PARENB 使能校验,~PARODD 设为偶校验
  • 'N'(无校验):~PARENB 关闭校验
  • 具体在这里咱们设置的是无校验。

6. 设置波特率

switch(nSpeed)
{
    case 2400:
        cfsetispeed(&newtio, B2400);
        cfsetospeed(&newtio, B2400);
        break;
    case 4800:
        cfsetispeed(&newtio, B4800);
        cfsetospeed(&newtio, B4800);
        break;
    case 9600:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    case 115200:
        cfsetispeed(&newtio, B115200);
        cfsetospeed(&newtio, B115200);
        break;
    default:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
}
  • cfsetispeed(&newtio, B9600) 设置输入波特率
  • cfsetospeed(&newtio, B9600) 设置输出波特率
  • 只支持 240048009600115200,默认 9600
  • 具体在这里设置的是115200

7. 设置停止位

if (nStop == 1)
    newtio.c_cflag &= ~CSTOPB;
else if (nStop == 2)
    newtio.c_cflag |= CSTOPB;
  • CSTOPB 设为 0(默认):1 位停止位
  • CSTOPB 设为 1:2 位停止位
  • 具体在这里设置的是1位停止位。

8. 设置至少读取多少字节和超时时间

newtio.c_cc[VMIN]  = 1;  
newtio.c_cc[VTIME] = 0;
  • VMIN = 1:读取时至少要获取 1 字节才返回
  • VTIME = 0:不启用超时,即一直等下去

9. 清空缓冲区

tcflush(fd, TCIFLUSH);
  • tcflush(fd, TCIFLUSH):清空输入缓冲区,防止残留数据影响

10. 设置新参数

if ((tcsetattr(fd, TCSANOW, &newtio)) != 0)
{
    perror("com set error");
    return -1;
}
  • tcsetattr(fd, TCSANOW, &newtio) 立即生效新的串口设置
  • 当 tcsetattr 的第二个参数设置为 TCSANOW,表示立即生效,不等待当前正在传输的数据完成。
  • 若失败则返回 -1

11.小结

该函数用于配置串口的各种参数:

  • 波特率240048009600115200
  • 数据位78
  • 校验位
    • 'N' 无校验
    • 'E' 偶校验
    • 'O' 奇校验
  • 停止位12
  • 超时机制:最少读取 1 字节才返回,无超时

函数完成后,fd 对应的串口即可按照新配置进行收发数据。

关于主函数的说明

主函数很简单,没啥好说的,代码如下:

int main(int argc, char **argv)
{
	int fd;
	int iRet;
	char c;

	/* 1. open */

	/* 2. setup 
	 * 115200,8N1
	 * RAW mode
	 * return data immediately
	 */

	/* 3. write and read */
	
	if (argc != 2)
	{
		printf("Usage: \n");
		printf("%s </dev/ttySAC1 or other>\n", argv[0]);
		return -1;
	}

	fd = open_port(argv[1]);
	if (fd < 0)
	{
		printf("open %s err!\n", argv[1]);
		return -1;
	}

	iRet = set_opt(fd, 115200, 8, 'N', 1);
	if (iRet)
	{
		printf("set port err!\n");
		return -1;
	}

	printf("Enter a char: "); // 输入的字符利用下面的函数scanf存储在变量c中
	while (1)
	{
		scanf("%c", &c);
		iRet = write(fd, &c, 1);
		if (iRet == 1)
			// printf("write: 0x%02x %c\n", c, c);
			printf("Write→ASCII: %d\n", c);
		else
			printf("can not write data\n");

		iRet = read(fd, &c, 1);
		if (iRet == 1)
			printf("Read→ASCII: %d\n", c);
		else
			printf("can not read data\n");
	}

	return 0;
}

交叉编译得到elf可执行程序

把源码文件复制到Ubuntu的目录 /home/book/mycode/C0035_UART_app1
在这里插入图片描述
然后运行下面的命令进行编译:

arm-buildroot-linux-gnueabihf-gcc -o serial_send_recv serial_send_recv.c

在这里插入图片描述
在这里插入图片描述

确定使用的串口的设备文件名

我们通过转接板模块将某个串口引出,如下图所示:
在这里插入图片描述
所以我们需要去查看原理图看到底转接板上的UART_A的TX和RX连接的是IMX6ULL的哪个串口。
在这里插入图片描述
在丝印层上找到上图19对应的编号:
在这里插入图片描述
可见是J5…
在这里插入图片描述
啊这…由于引脚复用,所以名字都感觉与串口无关…所以还得一个一个引脚去分析…这太花费时间了。
直接用教程提供的结论吧:“用的是IMX6ULL的第六个串口”,对应的设备文件名为:

/dev/ttymxc5

可以用下面的命令看下有哪些与tty有关的设备文件:

ls /dev/tty*

在这里插入图片描述
由此我们再去查看IMX6ULL与UART6相关的引脚。
先用那个设备树语句生成工具看下:
在这里插入图片描述
打开之后在引脚名里搜索UART,就找到了
在这里插入图片描述
从上面的截图中来看:
UART6的RX对应于E5:CSI_PIXCLK
UART6的TX对应于E5:CSI_MCLK

然后我们再回到原理图中,就可以应证了果然是UART6:
在这里插入图片描述
至于为什么UART6对应的设备文件名是/dev/ttymxc5,这就要去看设备树文件和分析TTY框架的代码了,这里就不再作展开了。具体的情况请百度网盘搜索“1-9_09_UART驱动情景分析_open”,从06分钟左右开始看,那里有详细的名字来历说明。

在开发板上测试生成的程序

先把扩展板上UART_A的TX引脚和RX引脚用杜邦线短接:
在这里插入图片描述
然后把扩展板插到开发板上,如下图所示:
在这里插入图片描述

把生成的ELF可执行程序复制到NFS网络文件目录中:
在这里插入图片描述
打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

然后执行下面的命令运行咱们生成的elf可执行程序serial_send_recv

cd /mnt/UART_app1
./serial_send_recv  /dev/ttymxc5

在这里插入图片描述
我们输入一个字符:
在这里插入图片描述
然后回车,得到下面的运行结果:
在这里插入图片描述

整个输入过程实际上我们输入了两个字符,分别为W和“Enter”,查阅博文:https://blog.csdn.net/wenhao_ir/article/details/125440879,发现两个字符的ASCII码对应如下:
在这里插入图片描述
可见,刚好是对应上的。

至此,测试完毕。


http://www.kler.cn/a/587996.html

相关文章:

  • 蓝桥与力扣刷题(蓝桥 字符统计)
  • linux (centos) 的 nodejs 安装全局包后使用命令无效
  • UE5 RVT 制作场景交互 - 遮罩
  • 安装配置Anaconda
  • es6初步学习
  • k8s serviceaccount在集群内指定apiserver时验证错误的问题
  • 计算机视觉中的MIP算法全解析
  • 使用VSCode开发STM32补充(Debug调试)
  • AI+视觉测试:如何提升前端测试质量?
  • 五大基础算法——模拟算法
  • MySQL -- 基本函数
  • 【Linux进程通信】————匿名管道命名管道
  • Matlab 风力发电机磁悬浮轴承模型pid控制
  • 从需求文档到智能化测试:基于 PaddleOCR 的图片信息处理与自动化提取
  • 每日Attention学习28——Strip Pooling
  • CVPR2024 | TT3D | 物理世界中可迁移目标性 3D 对抗攻击
  • day04_Java高级
  • 练习题:89
  • 考研专业课复习方法:如何高效记忆和理解?
  • 应用于电池模块的 Fluent 共轭传热耦合