第三章 UART驱动开发:从协议到实战
大家好,我是你们的讲师。今天我们来聊聊UART驱动开发。说实话,UART在嵌入式开发里就像空气一样——平时不太注意,但一旦出问题,整个系统都转不起来。我在MTK平台上调过不下几十个UART外设,从蓝牙模块到4G模组,踩过的坑能写满一本笔记本。嗯,咱们今天就把这些经验掰开揉碎了讲。
3.1 UART协议基础:别小看这三根线
UART,全称是Universal Asynchronous Receiver/Transmitter。说白了,就是异步串行通信。为什么叫异步?因为没有独立的时钟线。收发双方靠约定好的波特率来同步。
我刚开始做驱动时,总觉得UART协议太简单了,不就TX、RX、GND三根线嘛。直到有一次,客户说设备偶尔丢数据,我排查了整整两天,最后发现是波特率偏差超过了2%。从那以后,我再也不敢小看协议细节了。
UART帧格式(8-N-1最常见)
- 起始位:1位,逻辑低电平
- 数据位:5~9位,通常8位
- 校验位:可选(奇校验/偶校验/无校验)
- 停止位:1位或2位,逻辑高电平
你想想看,为什么起始位必须是低电平?因为总线空闲时是高电平。从高到低的跳变,就是接收器开始采样的信号。这个设计很巧妙,对吧?
我的经验:调试UART时,第一步永远是用示波器看波形。别急着改代码。看波形能直接确认:波特率对不对?帧格式对不对?电平极性对不对?我曾经靠这个习惯,半小时就定位了一个别人折腾了两天的硬件问题。
3.2 MTK UART控制器:硬件怎么工作的?
MTK的UART控制器,本质上是一个完整的硬件状态机。它负责:波特率生成、数据收发、FIFO管理、中断控制、流控管理。我个人习惯把控制器分成三个层面来理解:
- 寄存器层:配置参数、控制状态
- FIFO层:收发缓冲,减少CPU干预
- 中断层:事件通知,驱动响应
MTK的UART控制器支持硬件流控(RTS/CTS),这个在高速通信时特别重要。我记得有个项目,蓝牙模块要求921600波特率,不加流控的话,CPU稍微忙一点就丢数据。加上流控后,问题迎刃而解。
注意:MTK的UART FIFO深度通常是64字节(发送和接收各64)。但不同型号可能有差异。我建议你拿到芯片手册后,第一件事就是确认FIFO深度。这个参数直接影响你的中断触发阈值设计。
3.3 DTS配置:设备树里写什么?
在MTK平台上,UART的硬件信息通过设备树(Device Tree Source)来描述。说白了,就是告诉内核:这个UART挂在哪条总线上?中断号是多少?时钟源是什么?
下面是一个典型的MTK UART DTS配置片段:
uart0: serial@11002000 {
compatible = "mediatek,mt6765-uart";
reg = <0 0x11002000 0 0x1000>;
interrupts = <GIC_SPI 91 IRQ_TYPE_LEVEL_LOW>;
clocks = <&infracfg CLK_INFRA_UART0>;
clock-names = "uart0";
dmas = <&apdma 0>, <&apdma 1>;
dma-names = "tx", "rx";
status = "okay";
};
我来解释几个关键字段:
- compatible:驱动匹配的关键字符串,驱动里会用它来绑定
- reg:寄存器基地址和大小,我建议你核对芯片手册上的地址
- interrupts:中断号,MTK用GIC(通用中断控制器)
- clocks:时钟源,UART的波特率发生器依赖这个时钟
- dmas:DMA通道,用于大数据量传输
避坑指南:我曾经在DTS里漏配了clock-names,结果驱动probe时一直拿不到时钟句柄。内核日志只报了一个很模糊的错误。排查了三个小时才发现是DTS少写了一行。所以,DTS配置完成后,一定要用dtc -I dtb -O dts反编译确认。
3.4 串口驱动框架:tty驱动是怎么组织的?
Linux的串口驱动,核心是tty子系统。你想想看,为什么叫tty?历史原因,teletypewriter的缩写。但现代Linux里,tty就是字符设备的一种抽象。
整个框架分三层:
| 层次 | 职责 | 关键结构体 |
|---|---|---|
| tty核心层 | 提供统一的字符设备接口 | struct tty_driver |
| 线路规程层 | 处理数据格式转换 | struct tty_ldisc_ops |
| 硬件驱动层 | 直接操作UART控制器 | struct uart_ops |
我刚开始学的时候,觉得这三层太复杂了。后来发现,其实我们只需要关注硬件驱动层。tty核心层和线路规程层,内核已经帮我们实现好了。我们只需要实现struct uart_ops里的回调函数。
uart_ops里最重要的几个回调:
- startup:初始化硬件,申请中断
- shutdown:关闭硬件,释放中断
- set_termios:设置波特率、数据位、停止位等
- start_tx:启动发送
- stop_tx:停止发送
核心思路:uart_ops就是硬件和内核之间的契约。你实现了这些回调,内核就知道怎么操作你的UART硬件了。剩下的中断处理、FIFO管理,都是在这个框架下填充细节。
3.5 收发数据流程:数据是怎么流动的?
好了,到了最实战的部分。数据到底是怎么从应用程序到硬件,再从硬件回到应用程序的?我画个流程图给你看:
发送流程(应用 -> 硬件):
- 应用调用write()系统调用
- tty核心层将数据写入线路规程的发送缓冲区
- 线路规程调用uart_ops的start_tx回调
- 驱动将数据写入UART控制器的发送FIFO
- 硬件逐位发送到TX引脚
接收流程(硬件 -> 应用):
- 硬件从RX引脚接收到数据,存入接收FIFO
- FIFO达到触发阈值,产生接收中断
- 中断处理函数读取FIFO数据,存入线路规程的接收缓冲区
- tty核心层通知应用有数据可读
- 应用调用read()获取数据
这里有个关键点:中断触发阈值怎么设?我一般设成FIFO深度的一半。比如64字节的FIFO,阈值设32。为什么?因为这样既不会太频繁中断(浪费CPU),也不会等太久才处理(丢数据风险)。
我曾经踩过的坑:有一次我把接收中断阈值设成了1,结果每收到一个字节就触发一次中断。波特率115200时,CPU几乎被中断占满,系统响应变得极慢。后来改成16,问题解决。记住:中断频率和实时性之间要平衡。
还有一个细节:DMA传输。当数据量很大时(比如通过UART下载固件),用PIO模式(CPU逐字节搬运)效率太低。MTK的UART控制器支持DMA,配置好DMA通道后,数据直接在内存和FIFO之间搬运,CPU只需要在传输完成时处理一次中断。
我的建议:如果你的应用场景是少量控制命令(比如AT指令),用PIO模式就够了。如果是大数据量传输(比如固件升级、日志输出),一定要用DMA。我在一个4G模组项目中,用DMA后CPU占用率从30%降到了2%。
好了,UART驱动开发的核心内容就这些。从协议到硬件,从DTS到驱动框架,再到数据流程,每一步都有细节。但只要你理解了这些基础,遇到具体问题时就能快速定位。下一章我们讲SPI驱动,那个比UART稍微复杂一点,但思路是相通的。
记住:调试UART时,先看波形,再看寄存器,最后才改代码。这个顺序能帮你省下大量时间。