2. UART协议详解与驱动实现
UART,说白了就是串口通信。很多工程师觉得它简单,不就是TX、RX两根线嘛。但我在项目中见过太多因为UART配置不当导致的通信故障——数据错位、丢字节、波特率对不上……今天咱们就把UART从协议到驱动彻底讲透。
2.1 UART帧结构
UART是异步串行通信,没有时钟线。收发双方靠约定的帧格式和波特率来同步。一个完整的UART帧长这样:
| 字段 | 位数 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 固定为低电平,标志帧开始 |
| 数据位 | 5~9 bits | 常用8位,LSB先发 |
| 校验位 | 0/1 bit | 可选奇校验/偶校验/无校验 |
| 停止位 | 1/1.5/2 bits | 固定为高电平,标志帧结束 |
我刚开始做嵌入式时,总觉得校验位可有可无。直到有一次调试工业设备,环境干扰导致数据频繁出错,加了奇校验后问题立刻解决。嗯,从此我再也不敢省校验位了。
起始位与停止位
起始位是低电平,停止位是高电平。为什么这样设计?你想想看,空闲时TX线默认是高电平,起始位的下降沿正好作为同步信号。接收端检测到这个下降沿,就开始采样数据位。
数据位与字节序
数据位通常是8位,对应一个字节。注意UART是LSB first——先发最低位。比如发送0x55(二进制01010101),实际线上顺序是:1→0→1→0→1→0→1→0。
校验位
- 无校验(None):没有校验位,帧最短
- 奇校验(Odd):数据位+校验位中1的个数为奇数
- 偶校验(Even):数据位+校验位中1的个数为偶数
2.2 波特率计算
波特率就是每秒传输的bit数。比如9600波特率,每秒传9600个bit。但注意,这包括起始位、数据位、校验位、停止位所有bit。
实际有效数据传输率 = 波特率 × (数据位 / 帧总位数)
举个例子:9600波特率,8数据位,1停止位,无校验。帧总位数=1+8+0+1=10位。有效传输率=9600×8/10=7680字节/秒。
以常见的16倍过采样为例:
// 假设时钟频率16MHz,目标波特率115200
// 分频系数 = 16MHz / (16 × 115200) ≈ 8.68
// 取整后实际波特率 = 16MHz / (16 × 9) ≈ 111111
// 误差 = |111111 - 115200| / 115200 ≈ 3.5%
波特率误差超过2%就容易丢数据。我建议用整数分频,或者选择误差最小的分频系数。有些芯片支持小数分频,精度更高。
| 标准波特率 | 16MHz时钟分频系数 | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 104 | 9615 | 0.16% |
| 19200 | 52 | 19230 | 0.16% |
| 115200 | 9 | 111111 | 3.55% |
| 115200 | 8(小数分频) | 114943 | 0.22% |
2.3 Linux下UART驱动框架
Linux的UART驱动属于tty子系统。整个框架分三层:
- tty核心层:提供统一的字符设备接口,管理线路规程
- 线路规程(line discipline):处理数据格式转换,比如N_TTY
- UART驱动层:直接操作硬件寄存器,收发数据
UART驱动核心结构体
每个UART驱动都要实现struct uart_ops:
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *port);
void (*set_mctrl)(struct uart_port *port, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *port);
void (*stop_tx)(struct uart_port *port);
void (*start_tx)(struct uart_port *port);
void (*stop_rx)(struct uart_port *port);
void (*enable_ms)(struct uart_port *port);
void (*break_ctl)(struct uart_port *port, int ctl);
int (*startup)(struct uart_port *port);
void (*shutdown)(struct uart_port *port);
void (*set_termios)(struct uart_port *port, struct ktermios *new, struct ktermios *old);
void (*pm)(struct uart_port *port, unsigned int state, unsigned int oldstate);
int (*set_ldisc)(struct uart_port *port, struct ktermios *termios);
// ... 还有更多
};
我个人习惯把startup和shutdown放在最前面实现,它们负责硬件初始化和释放。然后是start_tx和stop_tx,这两个函数直接影响数据收发性能。
tty驱动注册流程
- 分配
uart_driver结构体 - 设置驱动名称、主次设备号、端口数量
- 调用
uart_register_driver()注册 - 为每个端口调用
uart_add_one_port()
/proc/tty/drivers和/sys/class/tty/下能看到你的驱动。如果看不到,说明注册有问题。
2.4 用户空间与内核空间数据交互
用户空间通过标准文件操作访问UART:
// 用户空间代码示例
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open");
return -1;
}
struct termios options;
tcgetattr(fd, &options);
// 设置波特率 115200
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
// 8数据位,无校验,1停止位
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
// 设置原始模式
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
tcsetattr(fd, TCSANOW, &options);
// 收发数据
char buf[256];
int n = read(fd, buf, sizeof(buf));
write(fd, "hello", 5);
close(fd);
内核空间的数据流向是这样的:
- 用户调用
write()→ 系统调用 → tty核心 → 线路规程 → UART驱动的start_tx() - UART驱动把数据写入硬件发送FIFO → 硬件逐bit发送
- 接收时,硬件产生中断 → 中断处理函数读取数据 → 放入tty缓冲区 → 用户
read()拿到数据
read()和write()是阻塞的,默认会等待数据。可以用fcntl()设置非阻塞模式,或者用select()/poll()做多路复用。
中断处理与数据流
UART驱动里最核心的是中断处理函数。我见过很多新手把中断处理写得特别长,导致系统卡死。记住:中断里只做最必要的事——读FIFO、写FIFO、清标志位。数据处理交给工作队列或tasklet。
// 中断处理函数示例(简化)
static irqreturn_t uart_isr(int irq, void *dev_id) {
struct uart_port *port = dev_id;
unsigned int iir = readl(port->membase + UART_IIR);
if (iir & UART_IIR_RX) {
// 接收中断:读FIFO直到空
while (readl(port->membase + UART_LSR) & UART_LSR_DR) {
unsigned char ch = readl(port->membase + UART_RBR);
// 放入tty缓冲区
tty_insert_flip_char(&port->state->port, ch, TTY_NORMAL);
}
tty_flip_buffer_push(&port->state->port);
}
if (iir & UART_IIR_TX) {
// 发送中断:从发送缓冲区取数据写入FIFO
// ...
}
return IRQ_HANDLED;
}
printk(),结果中断频繁触发时系统直接死机。中断里绝对不能做耗时操作,printk()、kmalloc()、信号量这些都要避免。
用户空间配置工具
除了代码里用termios,也可以用stty命令行工具快速配置:
# 查看当前配置
stty -F /dev/ttyS0 -a
# 设置115200 8N1
stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb
# 设置原始模式
stty -F /dev/ttyS0 raw
嗯,UART看似简单,但协议细节和驱动实现里坑不少。从帧结构到波特率计算,从tty框架到中断处理,每一步都影响通信的可靠性。下次调试UART时,记得先检查帧格式和波特率误差——我敢说,80%的串口问题都出在这两个地方。