news 2026/9/25 10:35:58

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

作者头像

张小明

前端开发工程师

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

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

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

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

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


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

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

原因很简单:你的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@addr,compatible加厂商前缀
✅避免忙等待:永远不要在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/9/23 1:46:46

英雄联盟段位修改终极指南:5分钟自定义你的游戏身份

英雄联盟段位修改终极指南&#xff1a;5分钟自定义你的游戏身份 【免费下载链接】LeaguePrank 项目地址: https://gitcode.com/gh_mirrors/le/LeaguePrank 想不想在英雄联盟中体验王者段位的风采&#xff1f;或者和朋友开个有趣的段位玩笑&#xff1f;LeaguePrank这款神…

作者头像 李华
网站建设 2026/9/13 5:33:03

Pyenv virtualenv创建Miniconda-Python3.10兼容环境

Pyenv 与 Virtualenv 构建 Miniconda-Python3.10 兼容环境 在当今 AI 与数据科学项目日益复杂的背景下&#xff0c;开发人员常常面临一个看似简单却极为棘手的问题&#xff1a;为什么代码在一个环境中能跑&#xff0c;在另一个环境就报错&#xff1f; 答案往往藏在“环境差异”…

作者头像 李华
网站建设 2026/9/24 5:56:09

MockGPS深度评测:实测Android位置模拟工具的真实表现

作为一款专为Android系统设计的开源位置模拟应用&#xff0c;MockGPS在开发测试和特定场景使用中展现出了独特价值。本文基于实际体验&#xff0c;从功能完整性、性能表现、适用场景等多个维度&#xff0c;对这款工具进行全面评测分析。 【免费下载链接】MockGPS Android appli…

作者头像 李华
网站建设 2026/9/24 6:45:48

【LangChain框架】全面技术解析

文章目录目录一、前置认知&#xff1a;LangChain核心基础信息1.1 核心基础属性对比表1.2 核心模块关联表二、LangChain核心架构解析2.1 分层架构详细对照表2.2 核心技术亮点&#xff1a;LCEL&#xff08;LangChain Expression Language&#xff09;三、LangChain核心功能模块详…

作者头像 李华
网站建设 2026/9/9 23:52:33

Unity游戏翻译革命:XUnity.AutoTranslator全功能解析与实战指南

Unity游戏翻译革命&#xff1a;XUnity.AutoTranslator全功能解析与实战指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还在为语言障碍而错过精彩的Unity游戏内容吗&#xff1f;XUnity.AutoTranslato…

作者头像 李华
网站建设 2026/9/24 20:32:02

英雄联盟段位自定义神器:LeaguePrank让你的游戏体验焕然一新

英雄联盟段位自定义神器&#xff1a;LeaguePrank让你的游戏体验焕然一新 【免费下载链接】LeaguePrank 项目地址: https://gitcode.com/gh_mirrors/le/LeaguePrank 还在为单调的段位显示感到乏味吗&#xff1f;想不想在朋友面前秀一把"王者"风采&#xff1f;…

作者头像 李华