目錄
1.終端(tty)
/dev/tty*:物理/虛擬終端
/dev/pts/*:偽終端
/dev/tty:當前進程的控制終端
/dev/tty0:當前活動的虛擬控制臺
2.行規程模式(line discipline)
比較行規程和原始模式:
1. 行規程模式
2.原始模式
?3.串口API
termios 結構體
基本的API
4.串口實驗(看看代碼怎么寫)
4.1 串口回環實驗(傳出去馬上傳回來)
自定義 set_opt?設置串口參數函000數:
自定義 open_port 打開串口設備函數?
main函數示例
4.2 GPS 模塊實驗 (不看也行,差不多的,就是加了點應用性)
1.終端(tty)
/dev/tty*:物理/虛擬終端
/dev/ttyS*:物理串口(如 RS-232)
/dev/ttyUSB*:USB 轉串口設備
/dev/tty1 ~?tty63:本地虛擬控制臺(通過 Ctrl+Alt+F1~F12 切換)
/dev/pts/*:偽終端
特點:
????????動態創建:由終端模擬器或 SSH 會話按需生成,退出后自動消失。????????無硬件關聯:完全由軟件模擬,用于多用戶會話管理。
典型用途:????????SSH 遠程連接
????????圖形界面中的終端模擬器(就是Ubuntu圖形化界面的終端,雖然它是tty2,但它是運行在?X Server/Wayland?上的,而不是原生的文本控制臺,是通過 偽終端(/dev/pts/*) 實現的,與 /dev/tty0 無直接關聯)
/dev/tty:當前進程的控制終端
????????指向當前會話實際使用的終端設備。
切換到 tty3,執行su root之后,執行下面命令觀察現象:
while [ 1 ]; do echo msg_from_tty3 > /dev/tty; sleep 3; done
現象:只有執行命令的那個終端會收到并打印信息?
/dev/tty0:當前活動的虛擬控制臺
代表當前前臺虛擬終端,不適用于偽終端(如 SSH 或圖形終端)
切換到 tty3,執行su root之后,執行下面命令觀察現象:
while [ 1 ]; do echo msg_from_tty3 > /dev/tty0; sleep 3; done
現象:把哪個終端切換到前臺,那個終端就會收到并打印信息
2.行規程模式(line discipline)
在終端和串口通信的過程中,設備和程序之間有一個行規程模式處理方式
韋的圖,大致意思就是說 pc 在調試開發板的串口終端上輸入一個字符 “a” ,通過串口傳到開發板后,先不傳入程序(app)中,而是保存在行規程中,然后行規程會把當前字符回傳給 pc,所以串口終端界面上會出現我們輸入的字符,這個過程叫做 “回顯”,需要退格刪掉一個字符也是把退格傳到行規程,行規程刪除字符后再把現存字符回顯到 pc 上。直到行規程收到 “回車鍵” ,才會把保存的字符都發送給程序(app)處理
后面需要把設備的行規程模式設置為原始模式,是因為需要把信息的處理全權交給程序
比較行規程和原始模式:
1. 行規程模式
特點:
????????行緩沖:數據按行處理(遇到\n或EOF才提交給程序)
????????字符回顯:輸入字符會顯示在終端上
????????特殊字符處理:支持Ctrl+C(中斷)、Ctrl+Z(暫停)等控制功能
典型場景:????????用戶交互式終端(如SSH會話、本地Shell)
????????需要逐行輸入的命令行工具
示例:在行規程模式下,輸入 hello 后按回車,程序才會收到完整字符串
2.原始模式
特點:
????????無緩沖:數據立即傳遞給程序,無需等待行結束符
????????無回顯:輸入字符不自動顯示
????????禁用控制字符:Ctrl+C等被視為普通數據
????????完全控制:可精確設置數據位、超時等參數
典型場景:
????????串口通信(如與單片機、傳感器通信)
????????需要實時響應的應用(如游戲、網絡協議棧)
????????二進制數據傳輸
示例:在原始模式下,每次接收到1個字節就會立即觸發讀取操作。
可以看看下面原因:
?3.串口API
在 Linux 系統中,操作設備的統一接口就是:open/ioctl/read/write。
對于 UART,又在 ioctl 之上封裝了很多函數,主要是用來設置行規程等參數
所以UART應用編程的套路就是:
- open;
- 設置行規程,比如波特率、數據位、停止位、檢驗位、RAW 模式(ioctl);
- read/write;
termios 結構體
struct termios 是 Linux 系統中用于終端 I/O 控制的關鍵數據結構,定義在 <termios.h> 頭文件中。它包含了終端設備的全部控制參數,用于配置串口、控制臺等設備的通信行為。
基本的API
tc:terminal contorl
cf:control flag
tcgetattr:獲取終端的屬性
tcsetattr:修改終端參數
tcflush:清空終端未完成的輸入/輸出請求及數據
cfsetispeed:?設置輸入波特率
cfsetospeed:?設置輸出波特率
cfsetspeed: 同時設置輸入、輸出波特率
這些API其實就是修改上面的 termios 結構體,這些函數更底層其實就是調用 ioctl 修改 termios 結構體
4.串口實驗(看看代碼怎么寫)
4.1 串口回環實驗(傳出去馬上傳回來)
自定義 set_opt?設置串口參數函000數:
/*** 設置串口參數* @param fd 串口文件描述符* @param nSpeed 波特率(2400/4800/9600/115200)* @param nBits 數據位(7或8)* @param nEvent 校驗方式(N:無校驗,O:奇校驗,E:偶校驗)* @param nStop 停止位(1或2)* @return 成功返回0,失敗返回-1*/
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{struct termios newtio, oldtio;/* 1. 獲取當前串口配置 */if (tcgetattr(fd, &oldtio) != 0) {perror("tcgetattr failed");return -1;}/* 2. 初始化新配置結構體 */bzero(&newtio, sizeof(newtio));/* 3. 設置控制模式標志 */newtio.c_cflag |= CLOCAL | CREAD; // 保持本地連接和啟用接收newtio.c_cflag &= ~CSIZE; // 清除數據位掩碼/* 4. 設置輸入/輸出模式 */newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始輸入模式(非規范模式)newtio.c_oflag &= ~OPOST; // 原始輸出模式(無處理)/* 5. 設置數據位 */switch (nBits) {case 7:newtio.c_cflag |= CS7;break;case 8:newtio.c_cflag |= CS8;break;default:newtio.c_cflag |= CS8; // 默認8位數據位break;}/* 6. 設置校驗位 */switch (nEvent) {case 'O': // 奇校驗newtio.c_cflag |= PARENB | PARODD;newtio.c_iflag |= (INPCK | ISTRIP);break;case 'E': // 偶校驗newtio.c_cflag |= PARENB;newtio.c_cflag &= ~PARODD;newtio.c_iflag |= (INPCK | ISTRIP);break;case 'N': // 無校驗newtio.c_cflag &= ~PARENB;break;}/* 7. 設置波特率 */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: // 默認9600cfsetispeed(&newtio, B9600);cfsetospeed(&newtio, B9600);break;}/* 8. 設置停止位 */if (nStop == 1) {newtio.c_cflag &= ~CSTOPB; // 1位停止位} else if (nStop == 2) {newtio.c_cflag |= CSTOPB; // 2位停止位}/* 9. 設置非規范模式下的讀取參數 */newtio.c_cc[VMIN] = 1; // 最小讀取字節數:至少讀取1字節才返回newtio.c_cc[VTIME] = 0; // 超時時間(單位:0.1秒),0表示無限等待//等待第1個數據的時間//比如VMIN設為10表示至少讀到10個數據才返回,但是沒有數據總不能一直等吧? 可以設置VTIME,如果超時時間內至少讀到了1個字節,那就繼續等待,完全讀到VMIN個數據再返回/* 10. 清空輸入緩沖區 */tcflush(fd, TCIFLUSH);/* 11. 應用新配置(立即生效) */if (tcsetattr(fd, TCSANOW, &newtio) != 0) {perror("tcsetattr failed");return -1;}return 0;
}
自定義 open_port 打開串口設備函數?
/*** 打開并初始化串口設備* @param com 串口設備路徑(如 "/dev/ttyS0")* @return 成功返回文件描述符,失敗返回-1*/
int open_port(char *com)
{int fd;/* 以讀寫模式打開串口設備,并確保不被用作控制終端 */fd = open(com, O_RDWR | O_NOCTTY);if (fd == -1) {perror("open serial port failed");return -1;}/* 顯式設置文件狀態標志為阻塞模式,也就是當程序無法讀/寫數據,程序會休眠*/if (fcntl(fd, F_SETFL, 0) < 0) {perror("fcntl F_SETFL failed");close(fd); // 失敗時關閉文件描述符return -1;}return fd; // 返回有效的文件描述符
}
main函數示例
/*** 串口通信測試程序* 功能:打開串口,配置參數(115200,8N1),實現簡單的回顯測試*/
int main(int argc, char **argv)
{int fd; // 串口文件描述符int iRet; // 操作返回值char c; // 讀寫數據的緩沖區/* 參數檢查 */if (argc != 2) {printf("Usage: %s </dev/ttySAC1 or other>\n", argv[0]);return -1;}/* 1. 打開串口 */fd = open_port(argv[1]);if (fd < 0) {printf("open %s err!\n", argv[1]);return -1;}/* 2. 配置串口參數(115200波特率,8數據位,無校驗,1停止位)*/iRet = set_opt(fd, 115200, 8, 'N', 1);if (iRet) {printf("set port err!\n");close(fd); // 配置失敗時關閉串口return -1;}/* 3. 串口讀寫測試 */printf("Enter a char: ");while (1) {/* 從標準輸入獲取字符 */scanf("%c", &c);/* 寫入串口 */iRet = write(fd, &c, 1);if (iRet != 1) {printf("write failed\n");continue;}/* 從串口讀取回顯數據(非阻塞模式立即返回)*/iRet = read(fd, &c, 1);if (iRet == 1) {printf("get: %02x %c\n", c, c); // 打印十六進制和ASCII格式} else {printf("can not get data\n");}}close(fd); // 理論上不會執行到這里return 0;
}
因為有可能會出現讀取串口數據時,由于串口回環設備傳輸太慢,導致發送后回環讀取時會出現讀取失敗的情況,是因為數據還沒傳到。
set_opt 中 :
newtio.c_cc[VMIN] = 1; // 最小讀取字節數:至少讀取1字節才返回
newtio.c_cc[VTIME] = 0; // 超時時間(單位:0.1秒),0表示無限等待//等待第1個數據的時間//比如VMIN設為10表示至少讀到10個數據才返回,但是沒有數據總不能一直等吧? 可以設置VTIME,如果超時時間內至少讀到了1個字節,那就繼續等待,完全讀到VMIN個數據再返回
這兩段代碼就設置了等待數據時間(這里設置為0,即無限等待)
這就解決了問題,把數據等到程序才繼續運行,否則就一直阻塞
4.2 GPS 模塊實驗 (不看也行,差不多的,就是加了點應用性)
使用串口接收數據,收到的數據包含:$GPGGA(GPS 定位數據)、$GPGLL (地理定位信息)、$GPGSA(當前衛星信息)、$GPGSV(可見衛星狀態信息)、 $GPRMC(推薦最小定位信息)、$GPVTG(地面速度信息)
只分析$GPGGA (Global Positioning System Fix Data)即可, 它包含了 GPS 定位經緯度、質量因子、HDOP、高程、參考站號等字段。
數據標準格式:
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh
其他關于gps的介紹看手冊吧,這里只放代碼
沿用上面的 set_opt 和 open_port 函數
/*** GPS數據讀取與解析程序* 功能:從串口讀取GPS模塊的NMEA數據并解析位置信息*//*** 從串口讀取一行GPS原始數據* @param fd 串口文件描述符* @param buf 存儲讀取數據的緩沖區* @return 成功返回0,失敗返回-1*/
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); // 每次讀取1個字符if (iRet == 1) {if (c == '$') { // NMEA語句起始符start = 1;i = 0; // 重置緩沖區索引}if (start) {buf[i++] = c; // 存儲有效數據}// 遇到換行符表示一行數據結束if (c == '\n' || c == '\r') {buf[i] = '\0'; // 添加字符串結束符return 0;}} else {return -1; // 讀取失敗}}
}/*** 解析GPS原始數據(GPGGA格式)* @param buf 原始數據緩沖區* @param time 存儲時間信息* @param lat 存儲緯度* @param ns 存儲南北半球* @param lng 存儲經度* @param ew 存儲東西半球* @return 成功返回0,失敗返回-1*/
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) { // 必須是GPGGA語句return -1;} else if (strstr(buf, ",,,,,")) { // 無效定位數據printf("Place the GPS to open area\n");return -1;} else {// 解析關鍵字段sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",tmp, time, lat, ns, lng, ew);return 0;}
}/** 主函數* 用法:./gps_reader </dev/ttySAC1 or other>*/
int main(int argc, char **argv)
{int fd; // 串口文件描述符int iRet; // 函數返回值char buf[1000]; // 原始數據緩沖區char time[100]; // 時間字段char Lat[100]; // 緯度字段char ns[100]; // 南北半球標識char Lng[100]; // 經度字段char ew[100]; // 東西半球標識float fLat, fLng; // 轉換后的經緯度/* 1. 參數檢查 */if (argc != 2) {printf("Usage: %s </dev/ttySAC1 or other>\n", argv[0]);return -1;}/* 2. 打開串口 */fd = open_port(argv[1]);if (fd < 0) {printf("open %s err!\n", argv[1]);return -1;}/* 3. 配置串口(9600波特率,8N1)*/iRet = set_opt(fd, 9600, 8, 'N', 1);if (iRet) {printf("set port err!\n");close(fd);return -1;}/* 4. 主循環:讀取并解析GPS數據 */while (1) {/* 讀取一行NMEA數據 */iRet = read_gps_raw_data(fd, buf);/* 解析GPGGA數據 */if (iRet == 0) {iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);}/* 打印解析結果 */if (iRet == 0) {printf("\n------ GPS Data ------\n");printf("Time : %s\n", time);printf("Lat : %s %s\n", Lat, ns);printf("Lng : %s %s\n", Lng, ew);/* 轉換緯度格式: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("Decimal Coordinates:\n");printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);}}close(fd); // 理論上不會執行到這里return 0;
}