Linux 序列埠(Serial Port)程式設計核心在於 struct termios 結構體配置。本文系統整理 UART 基礎、termios 旗標詳解、標準配置流程、完整可執行範例代碼,適用於嵌入式開發、工業控制、Modbus 通訊、GPS/GNSS 接收等場景。
序列通訊基礎知識
| 參數 | 常見值 | 說明 |
| 波特率 | 9600, 19200, 38400, 57600, 115200, 230400, 460800 | 位元傳輸速率 |
| 資料位 | 5, 6, 7, 8 | 通常 8 位元 |
| 停止位 | 1, 1.5, 2 | 通常 1 位元 |
| 奇偶校驗 | None, Even, Odd, Mark, Space | 通常 None |
| 流控制 | None, RTS/CTS (硬體), XON/XOFF (軟體) | 通常 None |
裝置節點:/dev/ttyS0 (COM1)、/dev/ttyS1 (COM2)、/dev/ttyUSB0 (USB 轉序列)、/dev/ttyACM0 (CDC ACM 設備)
termios 結構體完整定義
#include
struct termios {
tcflag_t c_iflag; // 輸入模式旗標
tcflag_t c_oflag; // 輸出模式旗標
tcflag_t c_cflag; // 控制模式旗標 (核心:波特率、資料位、停止位、校驗)
tcflag_t c_lflag; // 本地模式旗標 (規範/非規範模式、回顯、訊號)
cc_t c_cc[NCCS]; // 控制字元陣列 (VMIN, VTIME 等)
speed_t c_ispeed; // 輸入波特率 (Linux 通常同 c_cflag)
speed_t c_ospeed; // 輸出波特率
};

關鍵旗標位定義
c_cflag — 控制模式(硬體參數)
| 旗標 | 功能 | 常用值 |
CBAUD | 波特率掩碼 | B9600, B115200, B460800 等 |
CSIZE | 資料位掩碼 | CS5, CS6, CS7, CS8 |
CSTOPB | 兩停止位 | 設定=2 位,清除=1 位 |
PARENB | 啟用奇偶校驗 | 配合 PARODD 設定奇/偶 |
PARODD | 奇校驗 | 設定=奇,清除=偶(需 PARENB) |
HUPCL | 關閉時掛斷線 | 通常設定 |
CLOCAL | 忽略數據線載波偵測 | 必須設定(本地連接) |
CREAD | 啟用接收器 | 必須設定 |
CRTSCTS | 硬體流控 (RTS/CTS) | 需硬體支援時設定 |
c_iflag — 輸入模式
| 旗標 | 功能 |
IGNBRK | 忽略中斷條件 |
BRKINT | 中斷產生 SIGINT |
IGNPAR | 忽略奇偶錯誤 |
PARMRK | 標記奇偶錯誤 |
INPCK | 啟用輸入奇偶檢查 |
ISTRIP | 剝離第 8 位 |
INLCR/IGNCR/ICRNL | NL/CR 轉換處理 |
IXON/IXOFF/IXANY | 軟體流控 (XON/XOFF) |
c_lflag — 本地模式(規範模式控制)
原始模式關鍵:c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG)
ICANON:規範模式(行緩衝、編輯)、關閉→原始模式(字元級讀取)
ECHO:回顯輸入字元
ISIG:啟用 INTR/QUIT/SUSP 訊號字元
c_cc — 控制字元(非規範模式關鍵)
| 索引 | 含義 | 典型值 |
VMIN | 最小讀取字元數 | 0 (非阻塞), 1 (阻塞至 1 字元), >1 (阻塞至 N 字元) |
VTIME | 讀取逾時 (0.1 秒單位) | 0 (無逾時), 10 (1 秒), 等等 |
VMIN/VTIME 四種組合行為:
| VMIN | VTIME | 行為 |
| >0 | >0 | 讀取最多 VMIN 字元,逾時 VTIME 滴答返回已讀數 |
| >0 | =0 | 阻塞直到讀到 VMIN 字元 |
| =0 | >0 | 非阻塞輪詢,最多等待 VTIME,有資料即返回 |
| =0 | =0 | 完全非阻塞,立即返回(0 字元或可用字元) |
標準配置流程(9 步驟)
int serial_init(int fd, int baud, int databits, char parity, int stopbits) {
struct termios newtio, oldtio;
// 1. 保存原配置
if (tcgetattr(fd, &oldtio) != 0) {
perror("tcgetattr");
return -1;
}
// 2. 清零新配置
bzero(&newtio, sizeof(newtio));
// 3. 啟用本地連接與接收
newtio.c_cflag |= CLOCAL | CREAD;
// 4. 設定資料位
newtio.c_cflag &= ~CSIZE;
switch (databits) {
case 7: newtio.c_cflag |= CS7; break;
case 8: newtio.c_cflag |= CS8; break;
default: return -1;
}
// 5. 設定奇偶校驗
switch (parity) {
case 'O': case 'o': // 奇校驗
newtio.c_cflag |= PARENB | PARODD;
newtio.c_iflag |= INPCK | ISTRIP;
break;
case 'E': case 'e': // 偶校驗
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
newtio.c_iflag |= INPCK | ISTRIP;
break;
case 'N': case 'n': // 無校驗
newtio.c_cflag &= ~PARENB;
break;
default: return -1;
}
// 6. 設定波特率
speed_t speed;
switch (baud) {
case 2400: speed = B2400; break;
case 4800: speed = B4800; break;
case 9600: speed = B9600; break;
case 19200: speed = B19200; break;
case 38400: speed = B38400; break;
case 57600: speed = B57600; break;
case 115200: speed = B115200; break;
case 230400: speed = B230400; break;
case 460800: speed = B460800; break;
default: speed = B9600;
}
cfsetispeed(&newtio, speed);
cfsetospeed(&newtio, speed);
// 7. 設定停止位
if (stopbits == 1)
newtio.c_cflag &= ~CSTOPB;
else if (stopbits == 2)
newtio.c_cflag |= CSTOPB;
else
return -1;
// 8. 非規範模式:最小字元、逾時
newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
newtio.c_cc[VMIN] = 1; // 至少讀 1 字元
newtio.c_cc[VTIME] = 10; // 1 秒逾時
// 9. 清空緩衝區並套用
tcflush(fd, TCIFLUSH);
if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
perror("tcsetattr");
return -1;
}
return 0;
}
完整可執行範例:Modbus RTU 主站輪詢
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE "/dev/ttyUSB0"
#define BAUD 9600
int main() {
int fd = open(DEVICE, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("open");
return 1;
}
fcntl(fd, F_SETFL, 0); // 清除非阻塞旗標
if (serial_init(fd, BAUD, 8, 'N', 1) < 0) {
close(fd);
return 1;
}
// Modbus RTU 讀保持暫存器 (功能碼 0x03)
// 從站 1, 起始位址 0, 數量 2
unsigned char req[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B}; // 含 CRC
unsigned char resp[256];
while (1) {
// 發送請求
write(fd, req, sizeof(req));
tcdrain(fd); // 等待發送完成
// 讀取回應 (預期 7 字節: 從站+功能+位元組數+2暫存器*2+CRC)
int n = read(fd, resp, sizeof(resp));
if (n > 0) {
printf("RX [%d]: ", n);
for (int i = 0; i < n; i++) printf("%02X ", resp[i]);
printf("\n");
} else if (n < 0 && errno != EAGAIN) {
perror("read");
break;
}
usleep(1000000); // 1 秒輪詢間隔
}
close(fd);
return 0;
}
除錯技巧與工具
| 工具/指令 | 用途 |
stty -F /dev/ttyUSB0 -a | 查看當前序列埠所有參數 |
stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb | 快速設定參數 |
minicom -D /dev/ttyUSB0 -b 115200 | 互動式終端機 |
screen /dev/ttyUSB0 115200 | 簡易終端機 |
socat - /dev/ttyUSB0,b115200 | 雙向管道、記錄、TCP 轉發 |
cat /dev/ttyUSB0 | 原始資料流查看 |
ls -l /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* | 列舉可用序列埠 |
dmesg | grep tty | 查看核心驅動載入訊息 |
常見問題排查
| 現象 | 原因 | 解決 |
| 收到亂碼 | 波特率不匹配 | 雙端確認相同波特率、用示波器/邏輯分析儀測量 |
| 無資料接收 | 接收未啟用/接線錯誤 | 檢查 CREAD、TX/RX 交叉、GND 共地 |
| 發送無回應 | 流控制衝突/協議錯誤 | 關閉 CRTSCTS、檢查協議幀格式、CRC |
| 讀取阻塞不返回 | VMIN/VTIME 設定不當 | 非規範模式設定 VMIN=1, VTIME=10 |
| 權限被拒 | 用戶不在 dialout 群組 | usermod -a -G dialout $USER 重新登入 |
小結
Linux 序列埠程式設計核心是 termios 結構體的精確配置。掌握 c_cflag 硬體參數、c_lflag 原始模式、c_cc 讀取行為 三大支柱,配合標準 9 步驟初始化流程,即可構建穩健可靠的序列通訊程式。實戰中善用 stty、minicom、socat 輔助除錯,記住:接線正確是前提,參數匹配是核心,協議正確是保證。