news 2026/4/23 13:43:37

基于ARM平台的SerialPort驱动移植操作指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于ARM平台的SerialPort驱动移植操作指南

从零开始搞定ARM平台串口驱动移植:不只是“改个设备树”那么简单

你有没有遇到过这种情况?
系统跑起来了,板子也通电了,但串口就是没输出——黑屏、乱码、丢数据……明明代码一模一样,换个芯片就出问题。这时候你就知道,串口不是插上线就能用的“即插即用”外设,而是一个需要精心调教的硬件接口

尤其是在ARM平台上,无论是Cortex-M的小型工控模块,还是Cortex-A上的Linux嵌入式网关,只要涉及底层通信,SerialPort(UART)驱动移植几乎是每个嵌入式工程师绕不开的一课。它看似简单,实则暗藏玄机:寄存器偏移错一位,整个通信链路瘫痪;波特率算差一点,高波特率下全盘崩溃。

本文不讲空话套话,也不堆砌术语,而是带你一步步拆解ARM平台上串口驱动移植的真实流程——从设备树怎么写、中断为何不触发,到DMA如何启用、休眠唤醒为何失效。我们会结合实际开发经验,把那些手册里不会明说、但你一定会踩的坑,全都摊开来讲。


为什么不能直接用标准驱动?

很多人以为:“Linux内核不是已经有8250pl011这些通用串口驱动了吗?照着抄一遍设备树不就行了?”
听起来很美,现实却常常打脸。

原因很简单:你的SoC里的UART控制器,很可能压根不是标准IP核

比如全志H3、瑞芯微RK3399、NXP i.MX系列,虽然都叫“UART”,但它们的寄存器布局、时钟门控方式、甚至中断触发机制,可能和经典的ARM PL011完全不同。有的是32位寄存器对齐,有的却是16位间隔还带偏移;有的支持DMA搬移,有的只能靠轮询……

所以一旦你发现串口初始化失败、读不到数据、或者中断进不去,别急着怀疑接线或波特率——先确认你的硬件是不是“非标选手”

判断依据:查芯片手册!看UART章节的寄存器映射表。如果跟PL011不一致,那就得自己动手写驱动了。


驱动移植的核心任务:连接软件与硬件的“翻译官”

在Linux中,TTY/UART子系统是一套分层设计:

应用层 → line discipline → TTY core → UART driver → 硬件寄存器

我们作为驱动开发者,要做的就是实现中间这一层“UART driver”,让它能正确地读写特定SoC的UART控制器。

这个过程本质上是在做三件事:
1.告诉内核:“这儿有个串口设备”——通过设备树描述资源。
2.告诉内核:“该怎么操作它”——实现uart_ops函数集。
3.告诉内核:“出了事找我”——注册中断处理程序。

下面我们逐个击破。


第一步:让内核“看见”你的串口 —— 设备树配置的艺术

设备树是现代ARM Linux的灵魂。没有正确的.dtsi节点,再好的驱动也白搭。

一个典型的UART节点长这样:

uart0: serial@9000000 { compatible = "myvendor,uart-v1"; reg = <0x9000000 0x1000>; interrupts = <GIC_SPI 24 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clkc CLK_UART0>; clock-names = "uart_clk"; power-domains = <&pd_uart>; reg-shift = 2; /* 寄存器按4字节对齐 */ reg-io-width = 4; /* 使用32位访问 */ status = "okay"; };

几个关键点必须注意:

  • compatible字段决定了匹配哪个驱动。如果你写了"myvendor,uart-v1",那你的驱动.of_match_table就得对应上,否则 probe() 根本不会被调用。
  • reg-shift很容易被忽略。假设硬件寄存器每项占4字节,但索引是以单字节为单位递增的,就需要设置reg-shift=2(即左移2位,×4)。否则读出来的全是错位数据。
  • clocks必须准确指向主控时钟源。很多初学者只填了个频率,却不申请时钟,在低功耗场景下会被PM框架关闭时钟导致通信中断。
  • status = "okay"别忘了打开,不然设备直接被禁用。

💡小技巧:可以用of_property_read_u32(np, "clock-frequency", &freq)在驱动中动态获取时钟频率,避免硬编码。


第二步:搭建驱动骨架 —— uart_driver 与 uart_ops

1. 定义驱动实例

static struct uart_driver my_uart_driver = { .owner = THIS_MODULE, .driver_name = "my_serial", .dev_name = "ttyMY", .major = 0, .minor = 0, .nr = 2, // 支持两个端口 };

.dev_name决定了设备文件名,比如/dev/ttyMY0/dev/ttyMY1.nr是最大端口数,别设太大浪费内存。

记得在模块加载时注册:

ret = uart_register_driver(&my_uart_driver); if (ret) return ret;

2. 实现操作函数集(核心!)

这是真正控制硬件的地方。最关键的几个函数如下:

startup()—— 上电准备
static int my_uart_startup(struct uart_port *port) { int ret; // 使能时钟 clk_prepare_enable(port->clk); // 请求中断 ret = request_irq(port->irq, my_uart_interrupt, IRQF_SHARED, "my_uart", port); if (ret) { dev_err(port->dev, "failed to request irq\n"); goto err_clk; } // 初始化FIFO、清除状态 writel(UART_CR_RXE | UART_CR_TXE, port->membase + UART_CTRL); return 0; err_clk: clk_disable_unprepare(port->clk); return ret; }

⚠️ 注意事项:
- 中断一定要用request_irq注册,不要漏掉IRQF_SHARED(多中断共享GIC时需支持)。
- 时钟要在使用前 enable,退出时 disable,否则耗电严重。

set_termios()—— 设置波特率、数据格式

这才是最容易出问题的部分!

static void my_uart_set_termios(struct uart_port *port, struct ktermios *termios, struct ktermios *old) { unsigned int baud, quot; unsigned long flags; baud = uart_get_baud_rate(port, termios, old, 300, 4000000); // 计算分频系数:quot = clk / (16 * baud) quot = DIV_ROUND_CLOSEST(port->uartclk, 16 * baud); spin_lock_irqsave(&port->lock, flags); // 写入DLL/DLM(假设支持DLAB模式) writel(quot & 0xff, port->membase + UART_DLL); writel((quot >> 8) & 0xff, port->membase + UART_DLM); // 设置数据位、停止位、校验 unsigned int lcr = 0; switch (termios->c_cflag & CSIZE) { case CS5: lcr |= UART_LCR_WLEN5; break; case CS6: lcr |= UART_LCR_WLEN6; break; case CS7: lcr |= UART_LCR_WLEN7; break; case CS8: lcr |= UART_LCR_WLEN8; break; } if (termios->c_cflag & CSTOPB) lcr |= UART_LCR_STOP; if (termios->c_cflag & PARENB) { lcr |= UART_LCR_PARITY; if (!(termios->c_cflag & PARODD)) lcr |= UART_LCR_EVENPAR; } writel(lcr, port->membase + UART_LCR); // 更新TTY层缓存 uart_update_timeout(port, termios->c_cflag & CBAUD, baud); spin_unlock_irqrestore(&port->lock, flags); // 清除接收FIFO writel(UART_FCR_CLEAR_XMIT | UART_FCR_CLEAR_RCVR, port->membase + UART_FCR); // 允许接收中断 writel(UART_IER_RDI, port->membase + UART_IER); }

📌 波特率误差必须控制在 ±3% 以内。例如输入时钟48MHz,目标115200bps:

Divisor = 48000000 / (16 × 115200) ≈ 26.04 → 取整26 实际波特率 = 48000000 / (16 × 26) ≈ 115384.6 误差 = (115384.6 - 115200)/115200 ≈ +0.14%

完全OK。但如果用了错误的时钟源(比如误当成24MHz),误差就会飙升至50%,通信必然失败。


第三步:中断服务程序 —— 数据来了怎么办?

中断是串口稳定运行的关键。如果不用中断,CPU就得不断轮询,效率极低。

static irqreturn_t my_uart_interrupt(int irq, void *dev_id) { struct uart_port *port = dev_id; unsigned int status, ier; status = readl(port->membase + UART_ISR); // 中断状态寄存器 ier = readl(port->membase + UART_IER); if (!(status & UART_ISR_INT_MASK) || !(ier & UART_IER_EN)) return IRQ_NONE; spin_lock(&port->lock); /* 接收数据就绪 */ while (status & UART_ISR_RX_READY) { unsigned char ch; unsigned int ls = readl(port->membase + UART_LSR); ch = readl(port->membase + UART_RBR); // 自动清除中断标志 uart_insert_char(port, ls, UART_LSR_OE, ch, TTY_NORMAL); } /* 发送缓冲空 */ if (status & UART_ISR_TX_EMPTY) { struct circ_buf *xmit = &port->info->xmit; if (!uart_circ_empty(xmit)) { writel(xmit->buf[xmit->tail], port->membase + UART_THR); xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1); uart_write_wakeup(port); // 唤醒等待发送的进程 } } spin_unlock(&port->lock); return IRQ_HANDLED; }

🔍 关键细节:
-uart_insert_char()是TTY子系统的标准入口,不可跳过。
-uart_write_wakeup()会通知上层可以继续写入,否则write()可能会一直阻塞。
- 所有修改环形缓冲的操作都要加锁,防止并发访问。


常见坑点与调试秘籍

❌ 问题1:串口乱码 or 丢帧

现象:接收数据偶尔错一位,或者整包丢失
排查方向
- 检查波特率是否精确(重点看时钟源)
- 是否启用了FIFO?若未清空可能导致旧数据残留
- 中断优先级太低,被其他ISR抢占导致响应延迟

🔧 解决方案:
- 启用DMA接收(减少中断频率)
- 提高中断优先级:irq_set_irq_type(irq, IRQ_TYPE_EDGE_RISING);
- 增大TTY缓冲区:修改CONFIG_UNIX98_PTYS或自定义 buffer size

❌ 问题2:休眠后无法恢复

现象:系统suspend/resume后串口无响应
根本原因:没有实现PM回调函数,硬件状态未重置

✅ 正确做法:

static int my_uart_suspend(struct device *dev) { struct uart_port *port = dev_get_drvdata(dev); uart_suspend_port(&my_uart_driver, port); return 0; } static int my_uart_resume(struct device *dev) { struct uart_port *port = dev_get_drvdata(dev); my_uart_hw_init(port); // 重新初始化寄存器 uart_resume_port(&my_uart_driver, port); return 0; } static const struct dev_pm_ops my_uart_pm_ops = { .suspend = my_uart_suspend, .resume = my_uart_resume, };

记得在platform driver中绑定.pm = &my_uart_pm_ops


性能优化建议:什么时候该上DMA?

场景推荐模式
波特率 ≤ 115200,数据量小中断模式即可
波特率 ≥ 921600,持续传输必须启用DMA
实时性要求高(如工业控制)DMA + 双缓冲机制

DMA的好处显而易见:CPU不再频繁被打断,可以把时间留给更重要的任务。你可以配合scatter-gather模式批量搬运数据,进一步提升吞吐量。

当然,代价是复杂度上升——你需要管理DMA通道、处理完成回调、考虑内存一致性等问题。但对于高性能嵌入式系统来说,这笔投资值得。


最佳实践清单(收藏备用)

设备树命名规范:用serial@addrcompatible加厂商前缀
避免忙等待:永远不要在while循环里读LSR_DR!必须用中断或DMA
安全访问检查:每次访问port->membase前判断是否为NULL
并发保护:所有涉及缓冲区的操作都加spin_lock_irqsave()
日志分级输出:调试阶段用dev_dbg(),发布时关闭
兼容老内核:注意uart_add_one_port调用时机(应在probe()成功后)


写在最后:串口虽老,其命维新

尽管USB、以太网、Wi-Fi层出不穷,但在嵌入式世界里,串口依然是最可靠的“生命线”
- 调试阶段,它是唯一能看到kernel log的方式;
- 工业现场,它是Modbus、CAN网关最常见的前端接口;
- 即便是AI边缘盒子,启动阶段照样依赖串口console排障。

掌握ARM平台下的SerialPort驱动移植,并非为了炫技,而是为了真正掌控系统的每一寸硬件资源。当你能在陌生的SoC上亲手点亮第一个printk("Hello UART!\n"),那种成就感,远胜于调通千行应用逻辑。

这条路没有捷径,只有反复阅读手册、调试寄存器、分析波形。但只要你走过一次全程,下次面对任何新平台,心里都会多一分底气。

如果你在移植过程中遇到了其他难题——比如多串口冲突、RS-485方向控制、IrDA模式切换——欢迎留言讨论,我们可以一起深入剖析。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 12:12:06

PyTorch模型部署Flask API|Miniconda-Python3.10生产化封装

PyTorch模型部署Flask API&#xff5c;Miniconda-Python3.10生产化封装 在AI项目从实验室走向真实业务场景的过程中&#xff0c;一个常见的困境是&#xff1a;模型在本地训练效果很好&#xff0c;但一旦要上线服务&#xff0c;就频频出现依赖冲突、环境不一致、推理延迟高等问题…

作者头像 李华
网站建设 2026/4/23 12:18:56

基于Java+SpringBoot+SpringBoot智能雨伞借取系统(源码+LW+调试文档+讲解等)/智能雨伞租赁系统/雨伞智能借还系统/共享智能雨伞系统/智能雨伞使用系统/雨伞智能管理借取系统

博主介绍 &#x1f497;博主介绍&#xff1a;✌全栈领域优质创作者&#xff0c;专注于Java、小程序、Python技术领域和计算机毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2025-2026年最新1000个热门Java毕业设计选题…

作者头像 李华
网站建设 2026/4/23 12:16:09

Unity游戏翻译革命:XUnity.AutoTranslator深度使用指南

Unity游戏翻译革命&#xff1a;XUnity.AutoTranslator深度使用指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为外语游戏中的对话和菜单而困扰吗&#xff1f;XUnity.AutoTranslator作为一款功能…

作者头像 李华
网站建设 2026/4/23 11:29:01

Markdown Table of Contents插件提升Miniconda-Python3.10文档结构

Markdown TOC 插件如何重塑 Miniconda-Python3.10 文档体验 在数据科学与人工智能项目中&#xff0c;一个常见的尴尬场景是&#xff1a;团队成员花费半小时才在一份冗长的 README.md 文件里找到“如何启动 Jupyter”的那一行命令。更糟的是&#xff0c;文档里的目录早已过时——…

作者头像 李华
网站建设 2026/4/19 6:40:21

XUnity Auto Translator:突破语言壁垒的Unity游戏翻译神器

XUnity Auto Translator&#xff1a;突破语言壁垒的Unity游戏翻译神器 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为外文游戏中的生涩文字而苦恼吗&#xff1f;XUnity Auto Translator作为一款专…

作者头像 李华
网站建设 2026/4/20 4:34:49

Unity游戏翻译神器:XUnity Auto Translator完整使用指南

Unity游戏翻译神器&#xff1a;XUnity Auto Translator完整使用指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为看不懂的日系RPG、欧美独立游戏而烦恼吗&#xff1f;XUnity Auto Translator作为…

作者头像 李华