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_SETFL
是File 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
:波特率,如9600
、115200
nBits
:数据位,通常为7
或8
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
:关闭标准输入(即设置为原始模式,不使用回车换行等字符处理)ECHO
、ECHOE
:关闭回显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)
设置输出波特率- 只支持
2400
、4800
、9600
和115200
,默认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.小结
该函数用于配置串口的各种参数:
- 波特率:
2400
、4800
、9600
、115200
- 数据位:
7
或8
- 校验位:
'N'
无校验'E'
偶校验'O'
奇校验
- 停止位:
1
或2
- 超时机制:最少读取 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码对应如下:
可见,刚好是对应上的。
至此,测试完毕。