Linux 串口通信是嵌入式開發、工業控制、儀表設備對接的基礎技能。本文提供完整的用戶態串口應用程序框架,涵蓋打開設備、配置屬性(波特率、校驗、數據位、停止位)、讀寫操作、非阻塞/阻塞模式切換,並深度解析 ttyS* 與 ttySAC* 設備節點的內核版本差異。
一、 核心數據結構 termios
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 等)
};
二、 完整應用程序框架
#include
#include
#include
#include
#include
#include
#include
// 打開串口:返回文件描述符,失敗返回 -1
int open_port(const char *device) {
int fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) {
perror("open_port");
return -1;
}
// 恢復阻塞模式(去除 O_NDELAY)
if (fcntl(fd, F_SETFL, 0) < 0)
perror("fcntl F_SETFL");
return fd;
}
// 配置串口參數
int set_opt(int fd, int speed, int databits, char parity, int stopbits) {
struct termios newtio, oldtio;
if (tcgetattr(fd, &oldtio) != 0) { perror("tcgetattr"); return -1; }
bzero(&newtio, sizeof(newtio));
// 1. 啟用接收、忽略調製解調器控制線
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
// 2. 數據位
switch (databits) {
case 7: newtio.c_cflag |= CS7; break;
case 8: newtio.c_cflag |= CS8; break;
default: return -1;
}
// 3. 校驗位
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;
}
// 4. 波特率
speed_t sp;
switch (speed) {
case 2400: sp = B2400; break;
case 4800: sp = B4800; break;
case 9600: sp = B9600; break;
case 19200: sp = B19200; break;
case 38400: sp = B38400; break;
case 57600: sp = B57600; break;
case 115200: sp = B115200; break;
case 460800: sp = B460800; break;
default: sp = B9600;
}
cfsetispeed(&newtio, sp);
cfsetospeed(&newtio, sp);
// 5. 停止位
if (stopbits == 1)
newtio.c_cflag &= ~CSTOPB;
else if (stopbits == 2)
newtio.c_cflag |= CSTOPB;
else
return -1;
// 6. 非規範模式,無最小字符/超時限制(即時讀取)
newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;
// 7. 刷新輸入隊列,立即生效
tcflush(fd, TCIFLUSH);
if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
perror("tcsetattr");
return -1;
}
printf("Serial port configured: %d %d%c%d\n", speed, databits, parity, stopbits);
return 0;
}
// 主程序示例
int main() {
const char *dev = "/dev/ttySAC0"; // 根據平台調整
int fd = open_port(dev);
if (fd < 0) return 1;
if (set_opt(fd, 115200, 8, 'N', 1) < 0) {
close(fd);
return 1;
}
char *msg = "Hello\n";
for (int i = 0; i < 20; i++) {
int n = write(fd, msg, strlen(msg));
printf("Write %d bytes: %d\n", (int)strlen(msg), n);
sleep(1);
}
close(fd);
return 0;
}
三、 關鍵配置參數速查表
| 宏 | 屬於 | 含義 |
| CLOCAL | c_cflag | 忽略調製解調器狀態,直接連接 |
| CREAD | c_cflag | 啟用接收器 |
| CS7/CS8 | c_cflag | 7/8 數據位 |
| PARENB | c_cflag | 啟用奇偶校驗 |
| PARODD | c_cflag | 奇校驗(未設置則偶校驗) |
| INPCK | c_iflag | 啟用輸入奇偶檢查 |
| ISTRIP | c_iflag | 剝離第 8 位 |
| CSTOPB | c_cflag | 2 停止位(未設置則 1 位) |
| VMIN | c_cc | 最小讀取字符數 |
| VTIME | c_cc | 讀取超時(十分之一秒) |
常見 VMIN/VTIME 組合
VMIN=0, VTIME=0:非阻塞輪詢,立即返回。
VMIN>0, VTIME=0:阻塞直到讀到 VMIN 字符。
VMIN=0, VTIME>0:超時讀取,最多等待 VTIME*0.1s。
VMIN>0, VTIME>0:讀到第一個字符後,最多等待 VTIME 獲取剩餘字符。
四、 設備節點之謎:ttyS* vs ttySAC*
實測現象:S3C2410 開發板上 /dev/ttyS0 打開失敗,/dev/ttySAC0 成功。
根因:內核版本與驅動模型變更
| 內核版本 | 驅動文件 | 設備節點命名 | 註冊接口 |
| 2.4 | drivers/char/serial.c | ttyS0, ttyS1... | register_serial() |
| 2.6+ | drivers/serial/samsung.c | ttySAC0, ttySAC1... | uart_register_driver() + platform_device |
Samsung SoC 串口控制器在 2.6 內核中遷移至新 uart 子系統,驅動 samsung.c 註冊 struct uart_driver 時指定 dev_name = "ttySAC",因此節點名為 ttySAC*。舊版 ttyS* 由 8250 驅動佔用,S3C 系列不再使用。
驗證方法
# 查看內核註冊的串口驅動
cat /proc/tty/drivers
# 輸出示例:
/dev/ttySAC /dev/ttySAC 204 64-79 serial # Samsung UART
/dev/ttyS /dev/ttyS 4 64-67 serial # 8250/16550
五、 進階主題
- 硬件流控:啟用
CRTSCTS(c_cflag),需硬件支持 RTS/CTS 線。
- RS485 方向控制:使用
ioctl(fd, TIOCSRS485, &rs485conf) 配置發送使能極性、延遲。
- 異步通知:
fcntl(fd, F_SETFL, FASYNC) + fcntl(fd, F_SETOWN, getpid()) + 信號處理函數 SIGIO。
- 設備樹配置:
status="okay"、current-speed、clock-frequency、pinctrl 綁定。