【嵌入式Linux】嵌入式Linux应用开发基础知识之串口应用编程 您所在的位置:网站首页 Linux串口终端打印串口发送的数据 【嵌入式Linux】嵌入式Linux应用开发基础知识之串口应用编程

【嵌入式Linux】嵌入式Linux应用开发基础知识之串口应用编程

2023-11-25 07:20| 来源: 网络整理| 查看: 265

文章目录 前言1、ARM芯片是如何使用串口发送/接收数据的2、 TTY体系中设备节点的差别做个小实验 3、TTY驱动程序框架4、在STM32MP157上做串口实验的准备工作4.1、使能设备树节点4.2、通过Pinctrl指定引脚4.3、指定设备别名4.4、编译dtb 5、串口回环实验5.1、程序分析5.2、函数分析5.2.1、获取和修改终端属性5.2.2、终端线速(比特率) 5、串口AT指令读写实验5.1、串口的阻塞和非阻塞模式5.2、程序分析 总结附录终端特殊字符 参考资料

前言

韦东山嵌入式Linux应用开发基础知识学习笔记 文章中大多内容来自韦东山老师的文档,还有部分个人根据自己需求补充的内容

视频教程地址: https://www.bilibili.com/video/BV1kk4y117Tu

1、ARM芯片是如何使用串口发送/接收数据的

发送数据:CPU > 内存 > FIFO > 发送移位器 > UART单位 发送完成后产生中断提醒CPU传输完成 接收数据:UART单位 > 接收移位器 > FIFO > 内存 > CPU 接收完成后产生中断提醒CPU传输完成

在这里插入图片描述

▲串口结构图 2、 TTY体系中设备节点的差别

见韦东山老师的文档,这里只做一些粘贴记录

/dev/ttyS0、/dev/ttySAC0、/dev/tty、/dev/tty0、/dev/tty1、/dev/console之间的差别 在这里插入图片描述

▲各种设备节点之间的差别

TTY/Terminal/Console/UART,之间的差别 在这里插入图片描述

▲TTY体系中的相关术语

在这里插入图片描述

▲ 各类设备节点的差别 做个小实验 [root@100ask:~]# cat /proc/cmdline root=/dev/mmcblk2p3 rootwait rw console=ttySTM0,115200

通过上面的命令可以看出开发板的console默认是使用ttySTM0设备的,也就是连接计算机的设别

[root@100ask:~]# echo hello > /dev/console hello

通过上面的命令可以发现串口终端打印出了hello

systemctl stop myir clear > /dev/tty0 echo hello > /dev/tty0

通过上面的命令可以使液晶屏幕终端上就显示出了hello 是的,液晶屏幕设备是前台终端

echo hello > /dev/tty1

上面的命令也能起到同样的效果,这也就意味着液晶屏终端设备实际上是tty1,但是它被设定为前台终端

3、TTY驱动程序框架

在这里插入图片描述

▲TTY驱动程序框架

关于行规程ling discipline:   大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计“哲学”,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范(line discipline)内默认启用。高级应用程序可以通过将行规范设置为原始模式(raw mode)而不是默认的成熟或准则模式(cooked and canonical)来禁用这些功能。大多数交互程序(编辑器,邮件客户端,shell,及所有依赖curses或readline的程序)均以原始模式运行,并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行(译者注:\r\n 和 \n)间自动转换的选项。如果你喜欢,可以把它看作是一个原始的内核级sed(1)。   另外,内核提供了几种不同的行规范。一次只能将其中一个连接到给定的串行设备。行规范的默认规则称为N_TTY(drivers/char/n_tty.c,如果你想继续探索的话)。其他的规则被用于其他目的,例如管理数据包交换(ppp,IrDA,串行鼠标),但这不在本文的讨论范围之内。

在这里插入图片描述

▲TTY架构

在这里插入图片描述

▲TTY架构下的函数调用关系 4、在STM32MP157上做串口实验的准备工作

在这里插入图片描述

▲STM32MP157的扩展板

在这里插入图片描述

▲UART8 IO和单板接口对应关系 4.1、使能设备树节点

  在STM32MP157的内核设备树文件 arch/arm/boot/dts/stm32mp151.dtsi 中,已经设置了uart8节点: 在这里插入图片描述

▲arch/arm/boot/dts/stm32mp151.dtsi

  根据status = "disabled"可以判断该节点未被使能,接下来我们需要使能这个节点,但是并不用在这里使能,可以在其他文件中配置和使能,关于dts和dtsi一些设备树基本概念知识可以戳这里学习

4.2、通过Pinctrl指定引脚

  修改arch/arm/boot/dts/stm32mp15xx-100ask.dtsi,如下: 在这里插入图片描述

▲stm32mp15xx-100ask.dtsi 添加项

其中&uart8_pins_mx和&uart8_sleep_pins_mx可以根据include文件定位到stm32mp157-m4-srm-pinctrl.dtsi 在这里插入图片描述

▲stm32mp15xx-100ask.dtsi include选项

文件路径:arch/arm/boot/dts/stm32mp157-m4-srm-pinctrl.dtsi,如下 在这里插入图片描述

▲stm32mp157-m4-srm-pinctrl.dtsi 4.3、指定设备别名

  UART8对应的设备节点是哪个?它的驱动程序需要从"别名"里确定编号。 文件路径:arch/arm/boot/dts/stm32mp157c-100ask-512d-v1.dts

在这里插入图片描述

▲stm32mp157c-100ask-512d-v1.dts 添加项

对应#include "stm32mp15xx-100ask.dtsi"中的uart8 在这里插入图片描述

▲stm32mp157c-100ask-512d-v1.dts include 4.4、编译dtb

在这里插入图片描述

▲编译dtb文件 5、串口回环实验 5.1、程序分析

main()

#include #include #include #include #include #include #include #include #include /* * ./serial_send_recv */ 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 \n", argv[0]); return -1; } fd = open_port(argv[1]); if (fd printf("set port err!\n"); return -1; } printf("Enter a char: "); while (1) { scanf("%c", &c); iRet = write(fd, &c, 1); iRet = read(fd, &c, 1); if (iRet == 1) printf("get: %02x %c\n", c, c); else printf("can not get data\n"); } return 0; }

int open_port(char *com)

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) struct termios newtio,oldtio; //先获取终端的属性再在其基础上进行设置,这是推荐做法 if ( tcgetattr( fd,&oldtio) != 0) { perror("SetupSerial 1"); return -1; } bzero( &newtio, sizeof( newtio ) ); /* * CLOCAL:忽略调制解调器的状态行(不检查载波信号) //一般必设置的标志 * CREAD :允许输入被接收 * CSIZE :字符大小掩码(第 5 到第 8 位:CS5、CS6、CS7、CS8) //用在设置数据位之前,"&= ~"是清除作用 */ newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; /* * ICANON :规范模式(一行接一行)输入 * ECHO :回显输入字符 * ISIG :启动信号产生字符(INTR、QUIT、SUSP) * * OPOST :执行输出后续处理 */ newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/ newtio.c_oflag &= ~OPOST; /*Output*/ switch( nBits ) { case 7://7位数据位 newtio.c_cflag |= CS7; break; case 8://8位数据位 newtio.c_cflag |= CS8; break; } /* * PARENB:启动奇偶校验 * PARODD:使用奇数奇偶校验;否则使用偶数奇偶校验 * * INPCK :开启输入奇偶校验检查 * ISTRIP:从输入字符中去掉最高位(bit 8) */ 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; } /* * CSTOPB:每字符使用 2 个停止位;否则只使用 1 个 */ 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个数据再返回 */ //TCIFLUSH:刷新输入队列 tcflush(fd,TCIFLUSH); //修改立刻生效 if((tcsetattr(fd,TCSANOW,&newtio))!=0) { perror("com set error"); return -1; } //printf("set done!\n"); return 0; } 5.2、函数分析 5.2.1、获取和修改终端属性 int tcgetattr (int fd, struct termios *termios_p); int tcsetattr (int fd, int optional_actions,const struct termios *termios_p); //Both return 0 on success,or -1 on error

termios_p是一个指向结构体termios的指针,用来记录终端的各项属性

struct termios {//结构体 termios 中的前 4 个字段都是位掩码(数据类型 tcflag_t 是合适大小的整数类型),包含有可控制终端驱动程序各方面操作的标志 //这些终端标志见《Linux/UNIX系统编程手册》62.5 tcflag_t c_iflag; /* input mode flags */ //包含控制终端输入的标志 tcflag_t c_oflag; /* output mode flags */ //包含控制终端输出的标志 tcflag_t c_cflag; /* control mode flags */ //包含与终端线速的硬件控制相关的标志 tcflag_t c_lflag; /* local mode flags */ //包含控制终端输入的用户界面的标志 cc_t c_line; /* line discipline */ cc_t c_cc[NCCS]; /* control characters */ speed_t c_ispeed; /* input speed */ speed_t c_ospeed; /* output speed */ };

  c_line 字段指定了终端的行规程(line discipline)。为了达到对终端模拟器编程的目的,行规程将一直设为 N_TTY,也就是所谓的新规程。这是内核中处理终端的代码中的一个组件,实现了规范模式下的 I/O 处理。行规程的设定同串口编程有关。   数组 c_cc 包含着终端的特殊字符(中断、挂起等),以及用来控制非规范模式下输入操作的相关字段。数据类型 cc_t 是无符号整型,适合于保存这些值。常整数 NCCS 指定了数组中的元素个数。在附录或者《Linux/Unix系统编程手册》62.4节可以查看终端特殊字符,可以通过这个数组来修改一些行规程中生效的字符比如将终端字符由ctrl+c改为ctrl+L   c_ispeed 和 c_ospeed 字段在 Linux 上没有使用到(并且也没有在 SUSv3 中规定)。后面会提到Linux是如何保存终端线速的   当通过 tcsetattr()来修改终端属性时,参数 optional_actions 用来确定何时这些修改将生效。该参数可以被指定为下列值中的一种。 TCSANOW   修改立刻得到生效 TCSADRAIN   当所有当前处于排队中的输出已经传送到终端之后,修改得到生效。通常,该标志应该在修改影响终端的输出时才会指定,这样我们就不会影响到已经处于排队中、但还没有显示出来的输出数据。 TCSAFLUSH   该标志的产生的效果同 TCSADRAIN,但是除此之外,当标志生效时那些仍然等待处理的输入数据都会被丢弃。这个特性很有用,比如,当读取一个密码时,此时我们希望关闭终端回显功能,并防止用户提前输入 在这里插入图片描述

▲关闭终端回显功能

  下面展示了一种stty 命令,stty 命令是以命令行的形式来模拟函数 tcgetattr()和 tcsetattr()的功能,允许我们在 shell 上检视和修改终端属性。当我们监视、调试或者取消程序修改的终端属性时,这个工具非常有用。《Linux/UNIX系统编程手册》62.3 在这里插入图片描述

▲stty

在这里插入图片描述

▲在终端上查询其他tty设备参数 5.2.2、终端线速(比特率)

   不同的终端之间(以及串行线)传输和接收的速率(位数每秒)是不同的。函数 cfgetispeed()和 cfsetispeed()用来获取和修改输入的线速。函数 cfgetospeed()和 cfsetospeed()用来获取和修改输出的线速。

speed_t cfgetospeed (const struct termios *termios_p); speed_t cfgetispeed (const struct termios *termios_p) ; //Both return a line speed from given termios structure int cfsetispeed (struct termios *termios_p, speed_t speed) ; int cfsetospeed (struct termios *termios_p, speed_t speed); //Both return 0 on success,or -1 on error

   这里每一个函数用到的 termios 结构体都必须先通过 tcgetattr()来初始化。 speed:数据类型 speed_t 用来保存线速。这里没有直接以数值形式来设置线速,而是采用了一组符号常量(定义在中)。这些常量定义了一系列离散的值。关于这些常量,有一些例子比如 B300、B2400、B9600 以及 B38400,分别各自对应于线速 300、2400、9600以及 38400 位数每秒。    尽管函数 cfsetispeed()和 cfsetospeed()可以分开指定输入和输出线速,但是在许多终端上这两个速率必须是一样的。此外,Linux 只用一个单独的字段来保存线速(即,假定这两个速率值总是一样的),这表示所有同输入和输出线速率相关的函数访问的都是相同的 termios 结构体字段。

5、串口AT指令读写实验 5.1、串口的阻塞和非阻塞模式

  在使用串口使用AT指令读写时会遇到发送一条指令受到多条的情况,阻塞模式在这个时候可能很难判断什么时候该结束阅读数据,这个时候可以使用串口的非阻塞模式进行多次阅读判断串口缓冲区是否还有数据来进行多次接收。    阻塞的定义:    对于read,block指当串口输入缓冲区没有数据的时候,read函数将会阻塞在这里,移植到串口输入缓冲区中有数据可读取,read读到了需要的字节数之后,返回值为读到的字节数;    对于write,block指当串口输出缓冲区满,或剩下的空间小于将要写入的字节数,则write将阻塞,一直到串口输出缓冲区中剩下的空间大于等于将要写入的字节数,执行写入操作,返回写入的字节数。    非阻塞的定义:   对于read,no block指当串口输入缓冲区没有数据的时候,read函数立即返回,返回值为0。   对于write,no block指当串口输出缓冲区满,或剩下的空间小于将要写入的字节数,则write将进行写操作,写入当前串口输出缓冲区剩下空间允许的字节数,然后返回写入的字节数。

fcntl(fd,F_SETFL,0) //阻塞 fcntl(fd,F_SETFL,FNDELAY) //非阻塞 5.2、程序分析

功能函数: void hexdump(const unsigned char *buf, const int num):将buf[]中保存的num长度的字符串转换为16进制数

void hexdump(const unsigned char *buf, const int num) { int i; printf("hexdump:"); for(i = 0; i str[strlen(str) - 1] = '\r'; //变后缀'\n' -> '\r' str[strlen(str)] = '\n'; //加后缀'\n' }

程序功能:实现AT指令配置功能,其中read_lora函数是接收模块返回信息函数

#include #include #include #include #include #include #include #include #include #define MAXWRITE 100 /* set_opt(fd,115200,8,'N',1) */ int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop); int open_port(char *com); int read_lora(int fd, char *str_read); void hexdump(const unsigned char *buf, const int num); void AT_cmd_exc(char *str);//转换成AT命令格式 /* * ./serial_send_recv */ int main(int argc, char **argv) { int fd; int iRet; char str_write[100]; char str_read[100]; /* 1. open */ /* 2. setup * 115200,8N1 * RAW mode * return data immediately */ /* 3. write and read */ if (argc != 2) { printf("Usage: \n"); printf("%s \n", argv[0]); return -1; } fd = open_port(argv[1]); if (fd printf("set port err!\n"); return -1; } while (1) { bzero(str_write,sizeof(str_write)); bzero(str_read,sizeof(str_read)); printf("command: "); fgets(str_write,sizeof(str_write),stdin); AT_cmd_exc(str_write); //printf("echo:%s", str_write); //hexdump(str_write,strlen(str_write)); iRet = write(fd, str_write, strlen(str_write)); if(iRet != strlen(str_write)) { printf("command send failed!\n"); } /* iRet = 1 :读完一行 iRet = 0 :连续 MAXREADCOUNT 次未读取到数据 */ //iRet = read_lora(fd,str_read); while(read_lora(fd,str_read)) { printf("%s", str_read); bzero(str_read,sizeof(str_read)); } } return 0; } void AT_cmd_exc(char *str)//转换成AT命令格式 { str[strlen(str) - 1] = '\r'; //变后缀'\n' -> '\r' str[strlen(str)] = '\n'; //加后缀'\n' } #define MAXREADCOUNT 10000 int read_lora(int fd, char *str_read) { int iRet; char c; int i = 0; int count = 0; int count_readfailed = 0; bzero(str_read,sizeof(str_read)); while(1) { count++; //printf("read count: %d readfailed count: %d ", count, count_readfailed); iRet = read(fd, &c, 1); //printf(" c:%02X iRet: %d\n", c, iRet); if(iRet == 1) { count_readfailed = 0; str_read[i++] = c; if(c == '\n') return 1; } else if(iRet == -1) { count_readfailed ++; if(count_readfailed >= MAXREADCOUNT) return 0; } } } void hexdump(const unsigned char *buf, const int num) { int i; printf("hexdump:"); for(i = 0; i int fd; //fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY); fd = open(com, O_RDWR|O_NOCTTY); if (-1 == fd){ return(-1); } //fcntl(fd, F_SETFL, 0) struct termios newtio,oldtio; //先获取终端的属性再在其基础上进行设置,这是推荐做法 if ( tcgetattr( fd,&oldtio) != 0) { perror("SetupSerial 1"); return -1; } bzero( &newtio, sizeof( newtio ) ); /* * CLOCAL:忽略调制解调器的状态行(不检查载波信号) //一般必设置的标志 * CREAD :允许输入被接收 * CSIZE :字符大小掩码(第 5 到第 8 位:CS5、CS6、CS7、CS8) //用在设置数据位之前,"&= ~"是清除作用 */ newtio.c_cflag |= CLOCAL | CREAD; newtio.c_cflag &= ~CSIZE; /* * ICANON :规范模式(一行接一行)输入 * ECHO :回显输入字符 * ISIG :启动信号产生字符(INTR、QUIT、SUSP) * * OPOST :执行输出后续处理 */ newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/ newtio.c_oflag &= ~OPOST; /*Output*/ switch( nBits ) { case 7://7位数据位 newtio.c_cflag |= CS7; break; case 8://8位数据位 newtio.c_cflag |= CS8; break; } /* * PARENB:启动奇偶校验 * PARODD:使用奇数奇偶校验;否则使用偶数奇偶校验 * * INPCK :开启输入奇偶校验检查 * ISTRIP:从输入字符中去掉最高位(bit 8) */ 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; } /* * CSTOPB:每字符使用 2 个停止位;否则只使用 1 个 */ 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个数据再返回 */ //TCIFLUSH:刷新输入队列 tcflush(fd,TCIFLUSH); //修改立刻生效 if((tcsetattr(fd,TCSANOW,&newtio))!=0) { perror("com set error"); return -1; } //printf("set done!\n"); return 0; }

在这里插入图片描述

▲测试 总结

在这里插入图片描述

▲串口应用编程 附录 终端特殊字符

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

▲c_cc 参考资料

解密TTY 基于Linux的tty架构及UART驱动详解 Linux dts 设备树详解(一) 基础知识 linux下串口的阻塞和非阻塞操作 linux串口编程参数配置详解



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有