4. UART驱动与串口通信:UART硬件原理、Linux串口驱动框架、tty驱动注册、波特率/数据位/校验位配置
大家好,我是你们的讲师。今天我们来聊聊UART驱动与串口通信。说实话,串口在POS机外设里太常见了——打印机、密码键盘、扫码枪,十有八九都走串口。我刚开始做驱动时,觉得串口嘛,不就是收收发发,能有多复杂?结果第一次调一个高速打印机,丢数据丢到怀疑人生。嗯,从那以后,我再也不敢小看UART了。
4.1 UART硬件原理:从物理层说起
UART,全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,它就是个串并转换器。CPU发过来的是并行数据,UART把它转成串行,一根线发出去;接收端再转回并行,交给CPU。
为什么叫异步?因为没有独立的时钟线。收发双方靠约定的波特率来同步。你想想看,两个人约好每秒说多少个字,一个说一个听,节奏对了才能沟通。
UART的物理层就两根数据线:TX(发送)和RX(接收)。再加上地线GND,三根线就能干活。我见过不少新手把TX和RX接反了,结果死活不通。嗯,这里要注意:TX接对方的RX,RX接对方的TX,交叉连接。
数据帧的结构是这样的:
- 起始位:1位,逻辑0,表示开始传输
- 数据位:5~8位,通常是8位,一个字节
- 校验位:可选,奇校验或偶校验
- 停止位:1位或2位,逻辑1,表示传输结束
举个例子,8N1表示8位数据、无校验、1位停止位。这是串口通信最常用的配置。我在项目中遇到过一台老式打印机,非要7E1(7位数据、偶校验、1位停止位),折腾了半天才发现是协议不匹配。
核心要点:UART通信的关键参数就是波特率、数据位、校验位、停止位。收发双方必须完全一致,否则就是乱码或不通。
4.2 Linux串口驱动框架:三层结构
Linux的串口驱动,我习惯把它理解成三层结构:
- tty核心层:负责tty设备的注册、文件操作接口、线路规程
- tty驱动层:提供统一的ops接口,比如open、close、write、read
- UART驱动层:直接操作硬件寄存器,处理中断、DMA等
为什么会这样分层?说白了,就是为了复用。不同的串口硬件(16550、PL011、IMX UART等),只要实现了底层的UART ops,上层tty驱动几乎不用改。我做过一个项目,从IMX6换到RK3288,底层UART驱动重写,上层应用代码一行没动。
Linux内核里,UART驱动层通过struct uart_port和struct uart_ops来抽象硬件。uart_ops里最重要的回调函数有:
startup:启动串口,申请中断、初始化硬件shutdown:关闭串口,释放资源set_termios:配置波特率、数据位、校验位等start_tx:启动发送stop_tx:停止发送
我记得第一次写UART驱动时,set_termios这个函数让我头疼了好几天。因为波特率计算涉及到时钟分频,不同芯片的算法还不一样。
4.3 tty驱动注册:让设备节点出现
tty驱动注册,说白了就是让系统里出现/dev/ttyS0、/dev/ttyS1这样的设备节点。用户程序才能通过open、read、write来操作串口。
注册流程大致如下:
- 分配并初始化
struct uart_driver - 调用
uart_register_driver注册驱动 - 分配并初始化
struct uart_port - 调用
uart_add_one_port添加端口
代码示例:
static struct uart_driver my_uart_driver = {
.owner = THIS_MODULE,
.driver_name = "my_uart",
.dev_name = "ttyMY",
.major = TTY_MAJOR,
.minor = 64,
.nr = 2, // 支持2个端口
};
static int __init my_uart_init(void)
{
int ret;
ret = uart_register_driver(&my_uart_driver);
if (ret)
return ret;
// 添加端口
ret = uart_add_one_port(&my_uart_driver, &my_uart_port);
if (ret)
uart_unregister_driver(&my_uart_driver);
return ret;
}
注册成功后,系统里就会出现/dev/ttyMY0和/dev/ttyMY1。嗯,这里要注意:dev_name字段决定了设备节点的名字前缀。
个人经验:调试时可以用ls -l /dev/tty*查看设备节点是否生成。如果节点没出现,多半是注册流程出了问题,或者设备树里没有使能对应的UART。
4.4 波特率/数据位/校验位配置:set_termios详解
配置串口参数,核心就是set_termios回调函数。用户程序通过tcsetattr系统调用,最终会触发这个函数。
我习惯把配置流程拆成三步:
- 解析termios结构体:从
struct ktermios中提取c_cflag标志位 - 计算硬件参数:根据波特率计算分频系数,根据数据位/校验位设置寄存器
- 更新硬件寄存器:把计算好的值写入UART控制寄存器
波特率计算是重点。以16550 UART为例,公式是:
divisor = UART_CLOCK / (16 * baud_rate)
其中UART_CLOCK是UART模块的输入时钟频率。我遇到过一个问题:板子上UART时钟是24MHz,但驱动里写死了16MHz,结果波特率怎么调都不对。查了两天才发现是时钟配置错了。
数据位和校验位的配置,主要通过c_cflag的这几个位来控制:
| 参数 | c_cflag设置 | 说明 |
|---|---|---|
| 5位数据 | CS5 | 很少用 |
| 6位数据 | CS6 | 偶尔见 |
| 7位数据 | CS7 | 老设备用 |
| 8位数据 | CS8 | 最常用 |
| 无校验 | 无 | PARENB不置位 |
| 奇校验 | PARENB | PARODD | 校验位为奇数个1 |
| 偶校验 | PARENB | 校验位为偶数个1 |
set_termios的实现示例:
static void my_uart_set_termios(struct uart_port *port,
struct ktermios *termios,
struct ktermios *old)
{
unsigned int baud, cflag = termios->c_cflag;
unsigned int divisor;
// 计算波特率
baud = uart_get_baud_rate(port, termios, old, 0, 115200);
divisor = port->uartclk / (16 * baud);
uart_update_timeout(port, cflag, baud);
// 设置数据位
switch (cflag & CSIZE) {
case CS5: /* 5位 */ break;
case CS6: /* 6位 */ break;
case CS7: /* 7位 */ break;
case CS8: /* 8位 */ break;
}
// 设置校验位
if (cflag & PARENB) {
if (cflag & PARODD)
/* 奇校验 */;
else
/* 偶校验 */;
} else {
/* 无校验 */;
}
// 写入硬件寄存器
writel(divisor, port->membase + UART_DLL);
writel(divisor >> 8, port->membase + UART_DLM);
}
避坑指南:我曾经在配置校验位时,只设置了PARENB,忘了清除PARODD位,结果奇校验和偶校验搞反了。调试了两天才发现。建议每次配置前,先把相关位清零,再按需置位。
4.5 实战经验:串口调试三板斧
最后,分享几个我常用的调试方法:
- 示波器看波形:这是最直接的方法。看TX/RX引脚有没有电平变化,波特率对不对。我习惯先发一个0x55(二进制01010101),波形一目了然。
- 逻辑分析仪抓数据:比示波器便宜,能直接解析UART帧。我常用它来验证数据位和校验位配置是否正确。
- loopback测试:把TX和RX短接,发什么收什么。如果收发一致,说明硬件通路没问题。如果不对,八成是驱动配置有误。
嗯,UART驱动这部分内容就到这里。下一章我们会讲SPI驱动,那又是另一番天地了。记住一句话:串口通信,参数一致是王道,硬件通路是基础,驱动配置是关键。