一、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()
表示這個串口驅動支持三個串口 ,發送統計信息,含接收發送