从零开始移植wl_arm平台RTC驱动:一位嵌入式工程师的实战笔记
最近接手了一个国产化工控项目,主控芯片是某款基于ARM架构的wl_arm平台。系统跑的是Linux 5.4内核,整体运行稳定——但有个致命问题:每次断电重启后时间都回到“1970年1月1日”。日志打不出来、定时任务失效、远程运维完全失灵。
排查一圈发现:硬件RTC没工作。
更麻烦的是,标准hwclock命令无法读取设备,/dev/rtc0节点压根不存在。翻遍社区和论坛,几乎没有关于这个私有平台RTC驱动的资料。没办法,只能自己动手移植驱动了。
下面是我踩完所有坑之后总结出的一套完整方案,希望能帮你少走一个月弯路。
为什么必须用硬件RTC?软件计时真的不行吗?
在资源受限的嵌入式系统中,很多人会想:“我用NTP校时+软件计数不就行了?”听起来合理,但在实际场景中漏洞百出:
- 断电即归零:一旦主电源切断,时间信息全丢;
- 启动延迟大:系统要联网获取时间,而网络可能不可达或不稳定;
- 功耗高:为了保持时间准确,CPU不能深度休眠;
- 安全风险:依赖外部授时源,容易被中间人攻击篡改时间戳。
相比之下,一个合格的硬件RTC模块能做到:
- 掉电靠电池继续走时(VBAT供电);
- 工作电流仅几微安,不影响待机功耗;
- 上电瞬间就能提供准确时间;
- 支持报警中断唤醒系统。
尤其对于 wl_arm 这类强调低功耗、高可靠性的国产平台,硬件RTC不是“加分项”,而是系统可用性的底线保障。
wl_arm平台上的RTC长什么样?
wl_arm虽然底层兼容ARMv7/ARMv8指令集,但外设控制器大多是厂商自研IP核。它的RTC模块挂载在APB总线上,通过一组内存映射寄存器控制,典型结构如下:
| 寄存器偏移 | 名称 | 功能说明 |
|---|---|---|
| 0x00 | RTCCON | 控制寄存器:启停、时钟源选择 |
| 0x04 | RTCDATE | 当前日期(BCD编码) |
| 0x08 | RTCTIME | 当前时间(BCD编码) |
| 0x10 | RTCALM | 报警使能位 |
| 0x18 | ALMTIME | 报警时间 |
| 0x1C | TICNT | 周期性中断间隔设置 |
关键参数来自数据手册(WL-SoC_DS_Rev2.1):
- 工作电压:1.8V ~ 3.6V(VBAT域)
- 典型功耗:2.5μA @ 3.0V
- 时钟源:外部32.768kHz晶振 / 内部RC振荡器
- 字节序:小端模式(Little Endian)
⚠️ 注意:该RTC使用BCD编码存储时间值,比如
0x23表示秒为“23”,而不是十进制的35。这是很多初学者最容易出错的地方。
第一步:写一个最简驱动框架
Linux 的 RTC 子系统位于drivers/rtc/目录下,采用典型的三层架构:
1. 用户空间通过/dev/rtc0访问;
2. 核心层(rtc-core.c)统一管理设备;
3. 驱动层实现具体硬件操作。
我们要做的,就是补上第三层——把物理寄存器的操作封装成标准接口。
下面是我在wl_arm_rtc.c中实现的核心代码:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/rtc.h> #include <linux/io.h> #include <linux/interrupt.h> #include <linux/of.h> /* 寄存器偏移定义 */ #define RTCCON 0x00 #define RTCTIME 0x08 #define RTCDATE 0x04 #define RTCALM 0x10 #define ALMTIME 0x18 #define TICNT 0x1C struct wl_arm_rtc_dev { void __iomem *base; struct rtc_device *rtc_dev; int irq_alarm; };读取当前时间:BCD转BIN的细节别搞错!
static int wl_arm_rtc_read_time(struct device *dev, struct rtc_time *tm) { struct wl_arm_rtc_dev *rtc = dev_get_drvdata(dev); unsigned int date, time; time = readl(rtc->base + RTCTIME); date = readl(rtc->base + RTCDATE); tm->tm_sec = (time >> 0) & 0x7F; /* 秒:0~59 */ tm->tm_min = (time >> 8) & 0x7F; /* 分钟 */ tm->tm_hour = (time >> 16) & 0x3F; /* 小时:24小时制 */ tm->tm_mday = (date >> 0) & 0x3F; /* 日 */ tm->tm_mon = (date >> 8) & 0x1F; /* 月:注意是1~12 */ tm->tm_year = (date >> 16) & 0xFF; /* 年份:相对于1900的偏移 */ tm->tm_mon -= 1; /* 内核要求0~11 */ tm->tm_year += 100; /* 假设是20xx年 */ return rtc_valid_tm(tm); /* 验证合法性 */ }这里有两个常见陷阱:
1.tm_mon必须减1,因为内核期望0~11;
2.tm_year是从1900年起算的,我们读出来的是“距2000年的偏移”,所以加100。
设置时间:反过来也要对得上
static int wl_arm_rtc_set_time(struct device *dev, struct rtc_time *tm) { struct wl_arm_rtc_dev *rtc = dev_get_drvdata(dev); unsigned int date, time; time = ((tm->tm_sec & 0x7F) << 0) | ((tm->tm_min & 0x7F) << 8) | ((tm->tm_hour & 0x3F) << 16); date = ((tm->tm_mday & 0x3F) << 0) | (((tm->tm_mon + 1) & 0x1F) << 8) | /* 恢复为1~12 */ (((tm->tm_year - 100) & 0xFF) << 16); writel(time, rtc->base + RTCTIME); writel(date, rtc->base + RTCDATE); return 0; }记住:写进去的格式必须和硬件预期一致,否则你设置了“2025年”结果变成“2125年”也毫不奇怪。
注册驱动:让内核认识你的设备
接下来要把这些函数注册到 RTC 核心层:
static const struct rtc_class_ops wl_arm_rtc_ops = { .read_time = wl_arm_rtc_read_time, .set_time = wl_arm_rtc_set_time, };然后绑定 platform_driver 和设备树匹配规则:
static int wl_arm_rtc_probe(struct platform_device *pdev) { struct wl_arm_rtc_dev *rtc; struct resource *res; int ret; rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL); if (!rtc) return -ENOMEM; /* 映射寄存器地址 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); rtc->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(rtc->base)) return PTR_ERR(rtc->base); /* 请求报警中断 */ rtc->irq_alarm = platform_get_irq(pdev, 0); if (rtc->irq_alarm > 0) { ret = devm_request_irq(&pdev->dev, rtc->irq_alarm, wl_arm_rtc_alarm_irq, 0, "wl_arm-rtc-alarm", rtc); if (ret) return ret; } /* 注册RTC设备 */ rtc->rtc_dev = devm_rtc_device_register(&pdev->dev, "wl_arm-rtc", &wl_arm_rtc_ops, THIS_MODULE); if (IS_ERR(rtc->rtc_dev)) return PTR_ERR(rtc->rtc_dev); platform_set_drvdata(pdev, rtc); return 0; } /* 匹配设备树中的 compatible 字段 */ static const struct of_device_id wl_arm_rtc_of_match[] = { { .compatible = "wl,wl-arm-rtc" }, { } }; MODULE_DEVICE_TABLE(of, wl_arm_rtc_of_match); static struct platform_driver wl_arm_rtc_driver = { .probe = wl_arm_rtc_probe, .driver = { .name = "wl-arm-rtc", .of_match_table = of_match_ptr(wl_arm_rtc_of_match), }, }; module_platform_driver(wl_arm_rtc_driver);几个关键点提醒:
- 使用devm_*系列 API 可自动释放资源,避免内存泄漏;
-platform_get_irq()获取中断号,用于后续唤醒功能;
-MODULE_DEVICE_TABLE(of, ...)必不可少,否则设备树无法匹配。
设备树配置:别让驱动“找不到家”
在wl-soc.dtsi中添加以下节点:
rtc: rtc@100a0000 { compatible = "wl,wl-arm-rtc"; reg = <0x100a0000 0x1000>; interrupts = <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clk_rtc>; status = "okay"; };解释一下每个字段的作用:
-compatible:必须与驱动中的.of_match_table完全一致;
-reg:查看手册确认基地址是否正确,这里是0x100a0000;
-interrupts:SPI 25 表示连接到 GIC 的第25个共享中断线,触发方式要匹配硬件设计;
-clocks:确保 CLK 子系统已提供 32.768kHz 时钟;
-status = "okay":启用设备,否则不会加载驱动。
💡 提示:可以用
grep -r "rtc" arch/arm/boot/dts/查看现有平台参考配置。
调试实录:那些让我熬夜的坑
❌ 问题1:系统重启时间总是1970年
现象:hwclock -r返回Thu Jan 1 00:00:00 1970
排查过程:
1. 检查VBAT是否接了CR2032电池 → ✅ 正常;
2. 用万用表测VBAT引脚电压 → ❌ 只有0.8V!
原因:PCB上漏画了去耦电容,电源不稳定导致RTC复位;
3. 补上一个0.1μF陶瓷电容 → 回归正常。
结论:硬件设计缺陷直接影响驱动表现,不要只盯着代码。
❌ 问题2:报警中断无法唤醒休眠系统
现象:调用echo +5 > /sys/class/rtc/rtc0/wakealarm后进入suspend,但5秒后没唤醒。
排查步骤:
1. 查看/proc/interrupts | grep rtc→ 中断计数无变化;
2. 在wl_arm_rtc_set_alarm()加打印 → 函数确实被调用了;
3. 发现缺少一行关键代码:
enable_irq_wake(rtc->irq_alarm); // 允许该中断作为唤醒源这行要在 probe 阶段执行一次即可。否则即使中断来了,PMU也不会唤醒CPU。
✅ 最终验证流程
一切就绪后,用下面这条命令链快速验证:
# 手动设置当前时间 date -s "2025-04-05 10:00:00" # 写入硬件RTC hwclock --systohc # 断电再上电... # 检查是否恢复 hwclock -r # 输出应为:Sat Apr 5 10:00:00 2025如果输出正确,恭喜你,RTC终于活了!
进阶建议:让你的驱动更健壮
1. 添加电压丢失检测(Voltage Loss Detection)
有些RTC寄存器带 VL 标志位,可在ioctl(RTC_VL_READ)中返回:
static int wl_arm_rtc_ioctl(struct device *dev, unsigned int cmd, unsigned long arg) { if (cmd == RTC_VL_READ) { int vl = readl(rtc->base + RTCSTAT) & RTCSTAT_VL; return put_user(vl ? RTC_VL_DATA_INVALID : 0, (unsigned int __user *)arg); } return -ENOIOCTLCMD; }这样用户空间可以判断上次掉电是否造成时间紊乱。
2. 动态兼容不同型号
如果你的驱动要支持多个 wl_arm 衍生芯片,可以用设备树传递寄存器偏移:
rtc@100a0000 { compatible = "wl,wl-arm-rtc"; reg = <...>; wl,reg-time-offset = <0x08>; wl,reg-date-offset = <0x04>; };驱动中读取:
of_property_read_u32(np, "wl,reg-time-offset", &time_off);提升可维护性。
总结与延伸
现在回过头看,整个RTC驱动移植其实就三件事:
1.告诉内核怎么读写时间(read_time / set_time);
2.告诉系统如何找到硬件(设备树配置);
3.处理好边缘情况(中断、电源、精度补偿)。
虽然文章里贴了不少代码,但真正重要的是背后的思维方式:从硬件手册出发,以用户需求收尾,中间用内核机制搭桥。
当你搞定RTC之后,你会发现其他外设如I2C、SPI、PWM的驱动开发路径几乎一模一样——都是“寄存器操作 + 设备树绑定 + 标准接口封装”的组合拳。
至于未来还能做什么?
- 给RTC加上温度补偿算法,长期走时误差控制在±2ppm以内;
- 结合PTP协议实现微秒级同步,在工业自动化中大显身手;
- 写个udev规则,开机自动校准系统时间,彻底解放运维。
嵌入式世界的门,往往是从这样一个小小的RTC开始推开的。
如果你也在适配某个冷门平台的驱动,欢迎留言交流。毕竟,没人比正在爬坑的人更懂另一双沾满泥的手。