串口編程
串口在Linux中也是一個設備文件(一切皆文件),這一部分從裸機開發轉變過來還需要一定時間適應,不過可以去看看野火的Linux教程,中關於使用shell操作串口的示例有一個宏觀的的體驗和認知。回到通過程序使用串口收發數據,其也就如同讀寫一個普通文件一般,一般步驟是
1、打開串口設備(字符設備)(open系統調用)。
2、配置串口。
3、然后就是如同讀寫文件一樣使用,read和write函數進程數據的收發了。
4、最后就是使用完后的關閉close操作。
第一步打開字符設備通過調用open,這個過程和打開一個普通文件可能的唯一區別可能就是需要加上O_NOCTTY選項實際測試不加上好像也不會因為接收到0x03而導致程序退出(Ctrl+c == ascii(0x03))。
然后就是配置串口的約定參數如struct termios
{ 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 */ };
1.1 c_iflag 輸入設置
負責控制串口輸入數據的處理配置。
標志 | 作用 | 標志 | 作用 |
IGNPAR | 忽略楨錯誤和奇偶校驗 | IGNBRK | 忽略 BREAK 條件 |
INPCK | 打開輸入奇偶校驗 | PARMRK | 標記奇偶錯,只有設置INPCK並且沒有設置 IGNPAR 才有效 |
ISTRIP | 去掉字符第8位 | IGNCR | 忽略輸入中的回車CR |
ICRNL | 將輸入的CR轉換為 NL,除非設了IGNCR | INLCR | 將輸入的NL(換行)轉換為CR |
IXON | 啟用輸出的 XON/XOFF 流控制 | IXOFF | 啟用輸入的 XON/XOFF 流控制 |
IXANY | 嘗試任何字符可做重啟輸出信號,默認只能START字符恢復輸出 | IUCLC | (no posix)將輸入中的大寫字母映射為小寫字母 |
BRKINIT | 如果IGNBRK沒有設置當這是一個控制終端時將發送SIGINT信號 | IUTF8 | (no posix)輸入是utf8字符允許在cooked 模式下正確進行字符擦除 |
IMAXBEL | (no posix)輸入隊列滿時提示(ring bell) |
使用軟件流控制是啟用 IXON、IXOFF 和 IXANY 選項:
opt.c_iflag |= (IXON | IXOFF | IXANY);
相反,要禁用軟件流控制是禁止上面的選項:
opt.c_iflag &= ~(IXON | IXOFF | IXANY);
除此之外還有一個更粗暴的方式就是
opt.c_iflag = 0;
opt.c_iflag |= IXOFF;
1.2 c_oflag 輸出設置
負責控制串口輸出數據的處理配置。
標志 | 作用 | 標志 | 作用 |
OPOST | 執行輸出處理 | OFILL | 對於延遲使用填充符 |
ONLCR | 將NL轉換為CR-NL | OCRNL | 將輸出的CR轉換為NL |
ONLRET | 不輸出CR回車 | ONOCR | 在0列(行首)不輸出CR |
OFDEL | 填充符為DEL,否則為 NULL | OLCUC | 將輸出的小寫字符轉換為大寫字符 |
XTABS | 將制表符擴充為空格 | BSDLY | 退格延遲屏蔽 |
CRDLY | CR 延遲屏蔽 | CMSPAR | 標志或空奇偶性 |
FFDLY | 換頁延遲屏蔽 | NLDLY | 新行延遲屏蔽 |
啟用輸出處理
啟用輸出處理需要在 c_oflag 成員中啟用 OPOST 選項,其操作方法如下:
options.c_oflag |= OPOST;
使用原始輸出
就是禁用輸出處理,使數據能不經過處理、過濾地完整地輸出到串口。
當 OPOST 被禁止,c_oflag 其它選項也被忽略,其操作方法如下:
options.c_oflag &= ~OPOST;
1.3 c_cflag 控制選項
可設置串口的波特率、數據位、奇偶校驗、停止位以及硬件流控制。
標志 | 作用 | 標志 | 作用 |
CSIZE | 數據位屏蔽 | CS7 | 7位數據 |
CS5 | 5位數據 | CS8 | 8位數據 |
CS6 | 6位數據 | CLOCAL | 忽略modem控制線 |
PARENB | 校驗使能 | PARODD | 寄校驗使能,否則是偶校驗 |
CSTOPB | 兩位停止位,否則為1位 | CREAD | 打開接收 |
CRTSCTS | 硬件流控( no posix) |
比如關閉硬件流控,115200波特率,8位數據位,1位停止,偶校驗配置如下:
//設置串口輸出波特率 cfsetospeed(&opt, B115200); //設置串口輸入波特率 cfsetispeed(&opt, B115200); //設置數據位數 opt.c_cflag &= ~CSIZE; opt.c_cflag |= CS8; //偶校驗位 opt.c_cflag |= PARENB; opt.c_cflag &= ~PARODD; //設置停止位 1bit opt.c_cflag &= ~CSTOPB; //激活本地連接和接受使能選項 opt.c_cflag |= CLOCAL | CREAD;
1.3 c_lflag 控制選項
控制串口驅動怎樣控制輸入字符。
標志 | 作用 | 標志 | 作用 |
ISIG | 使能特殊字符的信號發送 | NOFLSH | 在中斷或退出鍵后禁用刷清 |
ICANON | 啟用規范輸入,默認開啟 | IEXTEN | 啟用擴充的輸入字符處理 |
XCASE | ECHOCTL | 如果設置了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制信號被回顯為^X字符形式, | |
ECHOPRT | ECHO | 回送輸入關閉 | |
ECHOE | 如果設置了 ICANON,字符 ERASE 擦除前一個輸入字符,WERASE 擦除前一個詞 |
ECHONL | 如果設置了 ICANON,回送NL |
ECHOK | 如果設置了 ICANON,字符KILL刪除當前行。 | ECHOKE | 如果設置了 ICANON,回顯 KILL 時將刪除一行中的每個字符,如同指定了 ECHOE 和 ECHOPRT 一樣 |
PENDIN | TOSTOP | 對於后台輸出發送SIGTTOU信號 |
經典輸入
經典輸入是以面向行設計的。輸入字符會被放入一個緩沖之中,這樣可以與用戶以交互的方式編輯緩沖的內容,直到收到CR(carriage return)或者LF(line feed)字符。
options.c_lflag |= (ICANON | ECHO | ECHOE);
原始輸入
輸入字符只是被原封不動的接收。一般情況中,如果要使用原始輸入模式,程序中需要去掉ICANON,ECHO,ECHOE和ISIG選項:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
1.4 c_cc[NCCS] 控制字符
主要用於控制串口在收到指定字符設備時的處理方式,如果設置對應的控制字符為1,則特殊字符的特殊功能將啟用,比如
//當ISIG設置時收到Ctrl+c(或0x0177)是認為這是一個中斷,將發送SIGINT信號 options.c_cc[VINTR] = 1;
其中VMIN和VTIME是比較特殊的兩個控制字符
當 MIN > 0,TIME > 0 時
計時器在收到第一個字節后啟動, 在計時器超時之前 (TIME 的時間到) , 若已收到 MIN個字節,則 read 返回 MIN 個字節,否則,在計時器超時后返回實際接收到的字節。
注意:因為只有在接收到第一個字節時才開始計時,所以至少可以返回 1 個字節。這種情形中,在接到第一個字節之前,調用者阻塞。如果在調用 read 時數據已經可用,則如同在 read 后數據立即被接到一樣。
當 MIN > 0,TIME = 0 時
MIN 個字節完整接收后,read 才返回,這可能會造成 read 無限期地阻塞。
當 MIN = 0, TIME > 0 時
TIME 為允許等待的最大時間,計時器在調用 read 時立即啟動,在串口接到 1 字節數據或者計時器超時后即返回,如果是計時器超時,則返回 0。
當 MIN = 0,TIME = 0 時
如果有數據可用,則 read 最多返回所要求的字節數,如果無數據可用,則 read 立即返回 0。
如果只使用四線串口,需要關閉流控,否則可能會出現無法接收數據的問題。其次是幾個標志位的名字很像,千萬不要不把另一個的標志位的宏賦值到另一個標志位上這樣牛頭不對馬嘴的還不容易發現問題。
參考代碼:

1 /** 2 * @brief 初始化串口 3 * @param device 設備 4 * @retval -1 失敗 5 * 0 ok 6 */ 7 int init_serial(char *device) 8 { 9 struct termios opt; 10 int uart_fd; 11 uart_fd = open(device,O_RDWR|O_NOCTTY); 12 if(uart_fd<0){ 13 printf("The %s device open failed.\n",device); 14 return -1; 15 } 16 #if 1 17 //fcntl(uart_fd, F_SETFL, FNDELAY); 18 //清空串口接收緩沖區 19 tcflush(uart_fd, TCIOFLUSH); 20 // 獲取串口參數 opt 21 tcgetattr(uart_fd, &opt); 22 //設置串口輸出波特率 23 cfsetospeed(&opt, B115200); 24 //設置串口輸入波特率 25 cfsetispeed(&opt, B115200); 26 //設置數據位數 27 opt.c_cflag &= ~CSIZE; 28 opt.c_cflag |= CS8; 29 //偶校驗位 30 opt.c_cflag |= PARENB; 31 opt.c_cflag &= ~PARODD; 32 //設置停止位 1bit 33 opt.c_cflag &= ~CSTOPB; 34 //激活本地連接和接受使能選項 35 opt.c_cflag |= CLOCAL | CREAD; 36 //關閉流控 接收回車符 37 //opt.c_cflag &= ~IGNCR; 38 //opt.c_cflag |= IXOFF; 39 //原始數據輸出 40 opt.c_oflag &= (~ONLCR); 41 opt.c_oflag &= (~OCRNL); 42 //關閉echo 43 opt.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); 44 //輸入關閉流控 接收回車符 45 opt.c_iflag |= IXOFF; 46 opt.c_iflag &= (0|IXOFF); 47 #endif 48 //更新配置 49 while(1) 50 { 51 errno = 0; 52 if(tcsetattr(uart_fd, TCSANOW, &opt) != 0) 53 { 54 if(errno == EINTR) 55 { 56 continue; 57 } 58 #ifdef PRINTF_MSG 59 else if(errno == EINVAL) 60 { 61 printf("config para elligal\n"); 62 } 63 else if((errno == ENOTTY )||(errno == EBADF)) 64 { 65 printf("fd error\n"); 66 } 67 else 68 { 69 printf("Device %s is set Fail%d\n"); 70 } 71 #endif 72 return -1; 73 } 74 break; 75 } 76 #ifdef PRINTF_MSG 77 printf("Device %s is set to 115200bps,8bits data , 1bit stop\n",device); 78 #endif 79 return uart_fd; 80 } 81 /** 82 * @brief 讀取串口數據 83 * @param buff 數據幀 84 * @retval 數據長度 85 * 86 */ 87 int recv_data_from_serial(int uart_fd,char *buf) 88 { 89 int ret,len=0,data_len; 90 struct pollfd fds[1]; 91 fds[0].fd = uart_fd; 92 fds[0].events = POLLIN; 93 while(1) 94 { 95 ret = poll(fds,1,0); 96 #ifdef PRINTF_MSG 97 //perror("uart poll"); 98 #endif /*PRINTF_MSG*/ 99 if(ret < 0) 100 { 101 /*調用出錯*/ 102 if(errno == EINTR) 103 { 104 /* 系統調用出錯,可重試 */ 105 continue; 106 } 107 else 108 { 109 /* 系統調用出錯,致命錯誤 */ 110 #ifdef PRINTF_MSG 111 printf("uart read fail exit\n"); 112 #endif /*PRINTF_MSG*/ 113 exit(-1); 114 } 115 116 } 117 else if(ret >= 1) 118 { 119 /*有數據可讀*/ 120 if(fds[0].revents & (POLLIN|POLLPRI)) 121 { 122 if(ioctl(uart_fd,FIONREAD,&data_len)<0) 123 { 124 /* retry */ 125 continue; 126 } 127 ret = read(uart_fd,buf,data_len); 128 if(ret <= 0) 129 { 130 /*讀失敗*/ 131 } 132 else if(ret) 133 { 134 len += ret; 135 buf += ret; 136 } 137 } 138 } 139 else 140 { 141 /* no data to read */ 142 break; 143 } 144 145 } 146 return len; 147 } 148 /** 149 * @brief 通過串口發送數據 150 * @param uart_fd 串口描述符 151 * @param data 數據幀 152 * @param lenth 數據長度 153 * @retval 1 數據長度非法 154 * 0 ok 155 */ 156 int send_data_from_serial(int uart_fd,char *data,int lenth) 157 { 158 int ret,len=0; 159 struct pollfd fds; 160 fds.fd = uart_fd; 161 fds.events = POLLOUT; 162 while(1) 163 { 164 ret = poll(&fds,1,0); 165 if(ret < 0) 166 { 167 /*調用出錯*/ 168 if(errno == EINTR) 169 { 170 /* 系統調用出錯,可重試 */ 171 continue; 172 } 173 else 174 { 175 /* 系統調用出錯,致命錯誤 */ 176 #ifdef PRINTF_MSG 177 printf("uart write fail exit\n"); 178 #endif /*PRINTF_MSG*/ 179 exit(-1); 180 } 181 } 182 else if(ret == 1) 183 { 184 /*可寫*/ 185 if(fds.revents & POLLOUT) 186 { 187 ret = write(uart_fd,data+len,lenth); 188 if(ret < 0) 189 { 190 /*寫失敗*/ 191 } 192 else if(ret > 0) 193 { 194 len+=ret; 195 /*寫完了*/ 196 if(len == lenth) 197 { 198 199 break; 200 } 201 } 202 } 203 } 204 else 205 { 206 /* 退出 */ 207 break; 208 } 209 } 210 return len; 211 } 212 213 int deinit_serial(int uart_fd) 214 { 215 close(uart_fd); 216 return 0; 217 }
參考:https://blog.csdn.net/flfihpv259/article/details/53786604