UART子系统控制器驱动:基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—UART控制器驱动开发)
⭐本人在本站记录的 Linux bsp及驱动项目为个人在今年秋招/春招求职独立作品已做好完整的GitHub和CSDN的 Commit 时间戳存证最早为2026年3月。 为维护校招公平源码及完整框架将于本人2027 年 6 月入职后允许使用。 阶段性开发期间请同届求职同学自觉勿搬运或抄袭感谢理解⭐本文是个人项目记录四UART子系统控制器驱动基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—UART控制器驱动部分的完整开发记录。涵盖框架分析、硬件知识、代码实现、设备树配置、测试验证、调试分析、问题排查全过程。项目及文档已开源在我的Github会随着逐步做完这个项目的BSP部分以及后面的驱动部分一直更新有完整的文档和源码欢迎Star⭐仓库地址 [https://github.com/illusOwo-ff/i.MX6ULL-bsp-driver-project]目录一、UART子系统框架tty/serial子系统三层结构通信原理数据帧格式FIFO中断波特率计算二、用到的IMX6ULL UART寄存器硬件结构取自NXP官方芯片手册3562页编辑用到的寄存器介绍寄存器与函数的对应关系驱动注册流程三、回调函数实现逻辑startup硬件初始化shutdown硬件关闭set_termios配置数据格式和波特率start_tx / stop_tx / stop_rx中断处理函数console write轮询发送四、并发保护五、设备树配置官方uart6节点imx6ull.dtsi中默认disabled板级设备树修改在我的设备树imx6ull-my14x14-emmc.dts中添加六、测试验证加载检查回环测试七、strace调试分析TX路径追踪RX路径追踪编辑完整数据通路八、问题记录问题1pinctrl组找不到问题2insmod时NULL pointer dereference一、UART子系统框架UART子系统没有设备驱动这一层。不像SPI/I2C有三层结构控制器驱动→设备驱动→应用程序UART的用户态程序直接通过tty设备文件/dev/ttyXXX操作控制器驱动。应用程序执行open/read/write/close经过tty层直接到达UART控制器驱动。tty/serial子系统三层结构通信原理UARTUniversal Asynchronous Receiver/Transmitter通用异步收发器是最简单的串行通信方式。异步的意思是收发双方没有共用时钟线——不像SPI有个SCLK让双方同步。UART只靠两根数据线通信TX发送和RX接收双方必须事先约定好以多快的速度发数据也就是波特率。数据帧格式UART通信是以帧为单位的一帧就是传一个字符。一帧的结构空闲(高电平) → [起始位] → [数据位] → [校验位(可选)] → [停止位] → 空闲(高电平) 1bit 5-8bit 0或1bit 1或2bit起始位固定是一个低电平。线路空闲时保持高电平接收方看到从高变低就知道一帧数据开始了——这就是异步通信的同步机制靠起始位的下降沿来对齐。数据位实际传输的数据通常是8bit一个字节低位先发。校验位可选奇校验或偶校验用来简单检错。大多数场景不用校验设成None。停止位1bit或2bit高电平标志一帧结束。FIFOIMX6ULL的UART控制器内部有TX FIFO和RX FIFO各32字节深。FIFO就是一个硬件缓冲队列。没有FIFO的话CPU每发一个字节都得等硬件发完才能送下一个效率很低。有了FIFO发送方向CPU可以连续往TX FIFO里塞最多32个字节然后去干别的事硬件会自动一个一个发出去。接收方向硬件收到的数据自动存进RX FIFOCPU不需要每收一个字节就来取可以等FIFO里积累了几个再一次性读走。FIFO触发水位Trigger Level你可以配置RX FIFO里积累到几个字节时触发中断。比如设成1就是每收到1个字节就中断设成16就是攒到16个才中断。低水位中断响应快但CPU开销大高水位反过来。对于我们的项目RX触发水位设1就行简单可靠TX触发水位也用默认值。中断RX就绪中断RRDYRX FIFO里的数据量达到触发水位时产生→中断处理函数从URXD寄存器读数据送给tty层。TX就绪中断TRDYTX FIFO的空位数达到触发水位时产生意思是FIFO有空间了可以继续塞数据→中断处理函数从circ_buf取数据写入UTXD寄存器波特率计算从手册中得到波特率计算公式BaudRate Ref_Freq / (16 × (UBMR 1) / (UBIR 1))其中Ref_Freq是UART模块的参考时钟频率经过UFCR寄存器里RFDIV分频后得到。在实际代码里通常先确定目标波特率反过来算UBIR和UBMR的值。官方驱动里的做法是UBIR baud_rate × 16 - 1 UBMR ref_freq - 1然后内核会自动做约分注意先写UBIR再写UBMR硬件在写UBMR时才更新波特率二、用到的IMX6ULL UART寄存器硬件结构取自NXP官方芯片手册3562页用到的寄存器介绍数据寄存器收发数据用的寄存器偏移说明URXD0x00接收数据寄存器。读它就能拿到RX FIFO里的一个字节。低8位是数据高位有一些错误标志校验错、帧错等UTXD0x40发送数据寄存器。往它写一个字节就会进入TX FIFO等待发送。只用低8位控制寄存器配置UART行为的寄存器偏移说明UCR10x80控制寄存器1。最重要的bitUARTENUART总开关、RRDYENRX就绪中断使能、TRDYENTX就绪中断使能UCR20x84控制寄存器2。最重要的bitSRST软复位、RXEN/TXEN收发使能、WS字长8/7位、STPB停止位、PREN/PRNE校验UCR30x88控制寄存器3。主要需要设置RXDMUXSEL位必须设为1这是IMX6ULL的一个硬件要求不设的话收不到数据UCR40x8C控制寄存器4。基本不需要动它FIFO控制寄存器寄存器偏移说明UFCR0x90FIFO控制。最重要的bitRFDIV参考时钟分频因子影响波特率计算、TXTLTX FIFO触发水位、RXTLRX FIFO触发水位状态寄存器查询当前状态的寄存器偏移说明USR10x94状态寄存器1。RRDY位RX FIFO有数据、TRDY位TX FIFO有空位。中断处理函数里读这个判断中断类型USR20x98状态寄存器2。TXDC位发送完全完成TX FIFO空且移位寄存器也空、RDR位RX FIFO非空。tx_empty函数读这个波特率寄存器寄存器偏移说明UBIR0xA4波特率增量寄存器。和UBMR配合设置波特率UBMR0xA8波特率调制寄存器。和UBIR配合设置波特率测试寄存器寄存器偏移说明UTS0xB4测试寄存器。TXFULL位TX FIFO满了和RXEMPTY位RX FIFO空了可以在这里查。console的write函数轮询发送时用这个判断FIFO是否满寄存器与函数的对应关系函数要操作的寄存器startupUCR1, UCR2, UCR3, UFCR初始化硬件使能UART和中断shutdownUCR1, UCR2禁止中断关闭UARTset_termiosUCR2, UFCR, UBIR, UBMR配置数据格式和波特率start_txUCR1使能TX就绪中断stop_txUCR1禁止TX就绪中断stop_rxUCR1禁止RX就绪中断tx_emptyUSR2读TXDC位中断处理函数USR1判断中断类型、URXD读数据、UTXD写数据、USR1清中断标志console writeUTS轮询TXFULL位、UTXD写数据三、回调函数实现逻辑startup硬件初始化① UCR1清UARTEN → 先关UART再配置 ② UCR2清SRST → 软复位等SRST自动变回1 ③ UCR3设RXDMUXSEL → IMX6ULL硬性要求 ④ UCR2设IRTS|WS|RXEN|TXEN → 忽略流控、8bit、使能收发 ⑤ UFCR设RXTL1, TXTL2, RFDIV1分频 ⑥ UCR1设UARTEN|RRDYEN → 开UART、开RX中断shutdown硬件关闭① UCR1清RRDYEN|TRDYEN → 禁止所有中断 ② UCR2清RXEN|TXEN → 禁用收发 ③ UCR1清UARTEN → 关闭UARTset_termios配置数据格式和波特率读UCR2根据termios-c_cflag配置 - CSIZE→WS位CS7/CS8 - CSTOPB→STPB位1/2停止位 - PARENB/PARODD→PREN/PROE位校验 写回UCR2 计算波特率 baud uart_get_baud_rate(port, termios, old, 50, 4000000) 写UBIR baud*16-1 写UBMR ref_freq-1 uart_update_timeout(port, cflag, baud)console write轮询发送循环每个字符 等待UTS的TXFULL位变0TX FIFO有空位 写字符到UTXD 如果是\n额外发一个\r四、并发保护在写UART驱动的时候使用uart_port内置的spinlockport-lock保护共享资源。函数锁的情况start_tx / stop_tx / stop_rx内核在调用前已持port-lock不需要自己加set_termios内核在调用前已持port-lock中断处理函数需要自己spin_lock_irqsave(port-lock)使用spin_lock_irqsave而不是spin_lock因为中断处理函数和进程上下文start_tx等共享port-lock必须在获取锁时同时关中断防止死锁。tty_flip_buffer_push放在锁外面因为它内部可能睡眠不能在持spinlock时调用。五、设备树配置官方uart6节点imx6ull.dtsi中默认disableduart6: serial021fc000 { compatible fsl,imx6ul-uart, fsl,imx6q-uart, fsl,imx21-uart; reg 0x021fc000 0x4000; /* 寄存器基地址和范围 */ interrupts GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH; /* 中断信息 */ clocks clks IMX6UL_CLK_UART6_IPG, clks IMX6UL_CLK_UART6_SERIAL; clock-names ipg, per; status disabled; };板级设备树修改在我的设备树imx6ull-my14x14-emmc.dts中添加通过grep在imx6ul-pinfunc.h中搜索UART6可用引脚→确认开发板转接板上UART6_TX/RX对应CSI_MCLK/CSI_PIXCLK→确认这两个引脚的原用途CSI摄像头并关闭→配置pinctrl复用为UART6功能。与官方驱动并列存在compatible设为自定义字符串官方imx-uart驱动不匹配只有自己的驱动匹配。reg/interrupts/clocks从dtsi继承不需要重复写。六、测试验证加载检查insmod uart_driver.ko # → ttyZXR0 at MMIO 0x21fc000 (irq 73) is a ZXR_my_UART ls /dev/ttyZXR0 # 设备文件存在 cat /proc/interrupts | grep my_uart # 中断已注册初始计数0 cat /proc/tty/driver/ZXR_MY_UART # tx:0 rx:0 stty -a -F /dev/ttyZXR0 # 查看端口配置回环测试硬件连接转接板J6的UART6_TXpin4和UART6_RXpin5用杜邦线短接。stty -F /dev/ttyZXR0 115200 raw -echo # 配置波特率和原始模式 cat /dev/ttyZXR0 # 后台监听接收echo hello /dev/ttyZXR0 # 发送数据 # → 终端显示 hello数据TX发出→短接线→RX收回 cat /proc/tty/driver/ZXR_MY_UART # tx:6 rx:6收发字节对称 cat /proc/interrupts | grep my_uart # 中断计数增加七、strace调试分析TX路径追踪strace -T -e traceopen,close,write,ioctl -o /tmp/uart_write.log \ sh -c echo hello /dev/ttyZXR0关键输出open(/dev/ttyZXR0, O_WRONLY|...) 3 0.000423 ← 触发startup423μs write(1, hello\n, 6) 6 0.000250 ← 触发start_tx→中断发送250μsRX路径追踪strace -T -e traceopen,close,read,ioctl -o /tmp/uart_read.log cat /dev/ttyZXR0 echo test_read /dev/ttyZXR0 kill %1关键输出open(/dev/ttyZXR0, O_RDONLY|...) 3 0.000328 ← 触发startup read(3, test_read\n, 4096) 10 9.616793 ← 阻塞等待数据9.6秒是操作间隔完整数据通路echo hello → write() 系统调用进入内核 → tty层放入circ_buf → start_tx() 使能TX中断 → TX中断 → 中断处理函数写UTXD → 硬件TX发出 → 杜邦线回环到RX → 硬件RX收到 → RX中断 → 中断处理函数读URXD → tty_insert_flip_char → tty_flip_buffer_push → read() 系统调用返回数据到用户态 cat 打印 hello八、问题记录问题1pinctrl组找不到打印信息imx6ul-pinctrl: unable to find group for node myuart6grp原因IMX6ULL kernel 4.9的pinctrl驱动要求pinctrl组作为iomuxc的直接子节点。最初把pinctrl组嵌套在imx6ull-myboard容器里驱动在该层级找不到。排查方法反编译DTB确认节点存在dtc -I dtb -O dts xxx.dtb | grep myuart6grp查看运行时设备树结构ls /proc/device-tree/soc/aips-bus02000000/iomuxc020e0000/发现官方pinctrl组uart1grp等都是iomuxc的直接子节点不在容器里我的解决方法去掉中间层容器pinctrl组直接放在iomuxc下。问题2insmod时NULL pointer dereference打印信息PC is at 0x0, LR is at uart_add_one_port原因设置了UPF_BOOT_AUTOCONF标志uart_add_one_port内部调用port-ops-config_port()但该函数指针为NULL。排查方法看崩溃类型NULL pointer dereference→某个指针是NULL看LR在uart_add_one_port里→内核框架调用了一个NULL函数指针推理UPF_BOOT_AUTOCONF触发自动配置→调用config_port→未实现→NULL我的解决方法实现config_port函数设置port-type