一、UART硬件理论
1.1 作用及功能
UART:通用异步收发传输器,简称串口。
功能:移植u-boot、内核时,主要使用串口查看打印信息。外接各种模块,比如蓝牙GPS模块。
使用UART的时候,要注意1. 波特率 2. 格式:数据位、停止位、校验位、流控。
1.2 发送接收数据流程
ARM发送数据以及PC接收数据流程,假设发送A 0x41 0b01000001。
ARM串口:原本是高电平 ,ARM拉低,保持1bit的时间(也就是波特率)。PC在低电平的时候开始计时,ARM根据数据驱动TxD电平,TxD = Data[0] ,TxD = Data[1] ...TxD = Data[7]。
PC读取数据:在数据位的中间读取引脚状态,Data = RxD[t0]。
校验位:奇/偶 “数据位+校验位”中为1的位的个数是 奇/偶(如果数据位中 1
的个数是奇数,则校验位为 0
,否则为 1
,使总的 1
个数为奇数)(现在一般不使用)。
TTL/CMOS这种方式电平不高,不适合远距离传输
目前开发板基本都会将电平转换芯片做到板子里面。
目前大部分电脑都没有串口,所以现在使用的是下面这个图的方式。
1.3 波特率
当波特率为115200时,且格式为115200,8n1。每秒能发送多少字节数据?
8代表数据位,n代表没有校验位,1代表停止位,传输一个字节需要10bit(含一个起始位),所以发送一个字节需要1/115200 * 10 = 1/11520,1秒能传输11520个字节数据。
二、TTY体系中设备节点的差别
TTY:teletype (teleprinter远程打印机)。(输入输出设备)
/dev/tty0,/dev/tty1,/dev/tty2 都是虚拟终端,/dev/tty0指的是位于前台的那个虚拟终端。
而/dev/ttyS0是真实的串口终端。
/dev/tty是当前程序本身所使用的终端。
console控制台,可以理解为权限更大,能查看更多信息。比如我们可以在Console上看到内核的打印信息,从这个角度上看:·Console是某一个Terminal , Terminal并不都是Console。我们可以从多个Terminal中选择某一个作为Console。很多时候,两个概念混用,并无明确的、官方的定义 。
三、TTY驱动程序框架
在超级终端打出一个字符“l”,涉及发送和接收
如果写错字符删去字符,其实是发送了退格键。
上图是当输入ls之后按下回车的传输情况,当PC机端按下回车,会将enter这个字节通过串口发送给ARM的串口,通过UART驱动程序发送给行规程(主要做字符的缓冲和处理(如回显、编辑、终结符识别等,输入被缓存在行缓冲区,直到遇到“行结束符”(如 \n
)才传给用户空间。)),行规程判断这是一个回车,并将其发送给APP(通常是一个shell),APP查找当前目录下的内容之后将内容发送给行规程,行规程发送给驱动,由UART再将数据发给PC端,得以显示。
四、Linux串口应用编程
4.1 应用基础及串口常用API函数
在Linux系统中,操作设备的统一接口就是:open/iodtl/read/write。对于UART,又在ioctI之上封装了很多函数,主要是用来设置行规程。所以对于UART,编程的套路就是: a. open,b. 设置行规程,比如波特率、数据位、停止位、检验位、RAW模式、一有数据就返回 c. read/write。
如何设置行规程中的参数便成为应用编程的重点。 在Linux中,行规程的参数用结构体termios来表示。
常用函数如下:
4.2 UART回环实验代码
#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: ");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);elseprintf("can not get data\n");}return 0;
}
上述代码做实验的时候会发现,第一次输入a和enter键,会返回两次can not get data,输入b和enter键会返回a和enter键,也就是返回上一次输入的数据,这是因为newtio.c_cc[VMIN] = 0; newtio.c_cc[VTIME] = 0;这样的设置是立即返回,不管是否有数据,由于数据传输慢,所以当数据还没到达UART串口的时候,read函数立即返回。下一次读到的数据就是之前写的数据。
当设置newtio.c_cc[VMIN] = 1; newtio.c_cc[VTIME] = 0;的时候,read函数会阻塞读,读到1个字节就返回,所以这样只要你发送的字符到达了串口硬件缓冲区,内核检测到数据立即得到数据,而不会返回can not get data。
4.3 GPS实验代码
#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;
}/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF> */
int read_gps_raw_data(int fd, char *buf)
{int i = 0;int iRet;char c;int start = 0;while (1){iRet = read(fd, &c, 1);if (iRet == 1){if (c == '$')start = 1;if (start){buf[i++] = c;}if (c == '\n' || c == '\r')return 0;}else{return -1;}}
}/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF> */
int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, char *ew)
{char tmp[10];if (buf[0] != '$')return -1;else if (strncmp(buf+3, "GGA", 3) != 0)return -1;else if (strstr(buf, ",,,,,")){printf("Place the GPS to open area\n");return -1;}else {//printf("raw data: %s\n", buf);sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns, lng, ew);return 0;}
}/** ./serial_send_recv <dev>*/
int main(int argc, char **argv)
{int fd;int iRet;char c;char buf[1000];char time[100];char Lat[100]; char ns[100]; char Lng[100]; char ew[100];float fLat, fLng;/* 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, 9600, 8, 'N', 1);if (iRet){printf("set port err!\n");return -1;}while (1){/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF>*//* read line */iRet = read_gps_raw_data(fd, buf);/* parse line */if (iRet == 0){iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);}/* printf */if (iRet == 0){printf("Time : %s\n", time);printf("ns : %s\n", ns);printf("ew : %s\n", ew);printf("Lat : %s\n", Lat);printf("Lng : %s\n", Lng);/* 纬度格式: ddmm.mmmm */sscanf(Lat+2, "%f", &fLat);fLat = fLat / 60;fLat += (Lat[0] - '0')*10 + (Lat[1] - '0');/* 经度格式: dddmm.mmmm */sscanf(Lng+3, "%f", &fLng);fLng = fLng / 60;fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0');printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);}}return 0;
}
五、字符设备驱动的另外一种注册方法
由于register_chrdev无法指定次设备号,它一般直接将(major, xxx)下的次设备号都占用,这就导致了系统资源的浪费。
注册字符设备区域
有主设备号:register_chrdev_region()
无主设备号:alloc_chrdev_region()
分配设置注册cdev:cdev_alloc,cdev_init,cdev_add,最主要的是设置cdev中的fops结构体。
六、UART驱动
6.1 注册过程
在uart中,设备节点可以说是一个port,一个UART就是一个port。
对于这个驱动模型,主要分为上下三层,最底层的左边是注册一个uart_driver,最底层的右边是注册一个uart_port(对应真实硬件相关信息),下面也是一个platform模型,当platform_driver和DTS中的节点的compatible属性匹配,会调用驱动程序中的probe函数,在probe函数中会获得硬件资源,并且设置uart_port,使用uart_add_one_port函数,该函数最终会去使用cdev_add函数,设置重要的cdev_ops。
6.2 open过程
从open过程可以看出来tty_operations 和 tty_port_operations是核心层(serial_core.c)提供,所以之后写驱动程序,我们要提供硬件相关的struct uart_ops结构体,要提供里面的startup函数。
open设备时确定行规程的代码流程
6.3 read过程(tty-io.c)
流程为:
a. APP读,使用行规程来读,无数据则休眠
b. UART接收到数据,产生中断
c. 中断程序从硬件上读入数据发给行规程
d. 行规程处理后存入buffer
e. 行规程唤醒APP,APP被唤醒后,从行规程buffer中读入数据,返回。
6.4 write过程
流程为:a. APP写。使用行规程来写,数据最终存入uart_state->xmit的buffer里。
b. 硬件发送:怎么发送数据?使用硬件驱动中uart_ops->start_tx开始发送具体的发送方法有2种:通过DMA,或通过中断。
c.中断方式 方法1:直接使能txempty中断,一开始tx buffer为空,在中断里填入数据。方法2:写部分数据到txfifo,使能中断,剩下的数据再中断里继续发送。
这部分的函数特别多还特别乱,存在不同的文件中,发送流程:APP发送数据==>tty-io.c中调用tty_write==>do_tty_write,在该函数中通过copy_from_user和write写到行规程的write_buf中==>进入行规程使用n_tty.c中n_tty_write==>n_tty_write函数调用tty_struct中的tty_operations的write函数进入核心层serial_core.c==>struct tty_operations uart_ops.uart_write==>将数据拷贝到statas中的环形缓冲区==>_uart_start开始发送==>使用8250_port.c(硬件相关的驱动程序中)uart_ops.start_tx函数。
七、UART驱动调试方法、
7.1 /proc/interrupt
查看中断次数
7.2 /proc/tty/drivers
7.3 /proc/tty/driver()
表示这个串口驱动支持三个串口 ,发送统计信息,含接收发送