关键机制深挖 —— 47 位计数器双读与 alarm 一次性中断
rtc-snvs.c 里有两段代码,看着不起眼,却是整颗驱动最见功力的地方:一个是读时间时的「双读循环」,一个是闹钟中断的「一次性」设计。这两个机制解决了 SNVS 硬件的两个固有难题——多寄存器读取的撕裂、闹钟中断的重复触发。这一节我们专门把这两块硬骨头啃下来。
学习目标
理解 47 位硬件计数器为什么会「撕裂」、原厂如何用「双读 + diff 判据」的乐观并发规避它;通过 set_time 的位操作反推计数器真实位布局,坐实「手册撒谎」;搞懂 alarm 为什么设计成 one-shot、handler 如何在中断里自动关中断并唤醒等待者。
硬骨头一:47 位计数器为什么会「撕裂」
SRTC 的秒计数器是一个 47 位的硬件计数器,物理上拆在两个 32 位寄存器里:
SNVS_LPSRTCMR:存高位(实际用到低 15 位)SNVS_LPSRTCLR:存低位(实际用到高 17 位)
要读出完整时间,你必须分两次读这两个寄存器,再拼起来。问题就出在「分两次」上:时钟是不会停下来等你的。设想这个场景——
- 你先读了高位
LPSRTCMR,得到H。 - 就在你去读低位
LPSRTCLR的那一瞬,计数器低位正好溢出、向高位进了一位。 - 你读到的低位
L是进位之后的新值,而高位H是进位之前的旧值。
拼出来的 H : L,高位旧、低位新,整体错位——这个时间值要么跳到未来、要么退回过去,反正不是「现在」。这就是撕裂读(torn read)。
⚠️ 这不是 i.MX6U 独有的坑
任何「一个值拆在多个寄存器、且硬件在持续更新」的设计都有这个问题。你在别的 SoC 的 64 位定时器、高精度 ADC 里都会遇到。本质是「读取不是原子的」。理解了这个解法,你举一能反十。
原厂解法:双读 + diff 判据(乐观并发)
先看读单个 64 位原始值的底层函数(:52):
/* drivers/rtc/rtc-snvs.c:52 —— 读一次完整的 47 位(用 64 位装) */
static u64 rtc_read_lpsrt(struct snvs_rtc_data *data)
{
u32 msb, lsb;
regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &msb);
regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &lsb);
return (u64)msb << 32 | lsb;
}这个函数本身就可能返回撕裂值。真正的对策在它的调用者 rtc_read_lp_counter(:64):
/* drivers/rtc/rtc-snvs.c:64 —— 双读,不一致就重读 */
static u32 rtc_read_lp_counter(struct snvs_rtc_data *data)
{
u64 read1, read2;
s64 diff;
unsigned int timeout = 100;
read1 = rtc_read_lpsrt(data); /* 第一次读 */
do {
read2 = read1;
read1 = rtc_read_lpsrt(data); /* 第二次读 */
diff = read1 - read2;
} while (((diff < 0) || (diff > MAX_RTC_READ_DIFF_CYCLES)) && --timeout);
if (!timeout)
dev_err(&data->rtc->dev, "Timeout trying to get valid LPSRT Counter read\n");
/* Convert 47-bit counter to 32-bit raw second count */
return (u32)(read1 >> CNTR_TO_SECS_SH); /* 右移 15 位 → 32 位秒 */
}思路极其优雅,是一种乐观并发控制(和内核的 seqlock 同源):
- 不加锁(加锁要停掉时钟,代价太大)。先读一次得
read1。 - 紧接着再读一次得新的
read1,把上次的存进read2。 - 算两次的差
diff = read1 - read2。正常情况下,两次读之间只过去了极少几个时钟周期,diff是个很小的正数。但如果发生了进位撕裂,diff要么是负的(回绕),要么是个异常大的值。 - 判据:只要
diff < 0或diff > MAX_RTC_READ_DIFF_CYCLES,就认为这次读「可能撕裂了」,重读,直到两次连续读到一致(差值在合理范围内),或超过 100 次超时。
那个魔法阈值 MAX_RTC_READ_DIFF_CYCLES = 320(:41)不是拍脑袋——注释写得很清楚:RTC 频率 32kHz,320 个周期正好约 10ms,远大于两次 regmap_read 的耗时;正常绝不会超,只有撕裂才会超。这就是判据的物理依据。
为什么是「乐观」?
seqlock/乐观并发的精神是:假设大多数时候没有冲突,冲突了就重试。这比悲观地加锁(每次都付出停时钟的代价)高效得多。你以后在内核里看到 do { read1; read2; } while (inconsistent) 这种模式,基本都是同一个思想。
最后 (u32)(read1 >> CNTR_TO_SECS_SH),CNTR_TO_SECS_SH = 15(:33)。因为 47 位计数器的低 15 位是 32768Hz 的亚秒计数,右移 15 位才得到整秒。所以读出来的虽然是 47 位原始计数,但有效秒数是 32 位。
用 set_time 反推真实位布局
01 节 我们说手册在 47 位计数器的位布局上「撒了谎」。与其争辩,不如直接看内核怎么写的——set_time(:178)的写操作会诚实地告诉你真相:
/* drivers/rtc/rtc-snvs.c:193 —— 写时间时的位拆分 */
/* Write 32-bit time to 47-bit timer, leaving 15 LSBs blank */
regmap_write(data->regmap, data->offset + SNVS_LPSRTCLR, time << CNTR_TO_SECS_SH);
regmap_write(data->regmap, data->offset + SNVS_LPSRTCMR, time >> (32 - CNTR_TO_SECS_SH));把 32 位秒数 time 写进 47 位计数器:
LPSRTCLR = time << 15:秒数左移 15 位,占据低位寄存器的 bit[15..31](17 位)。LPSRTCMR = time >> 17(即>> (32-15)):秒数的高 15 位,进高位寄存器。
拼起来:32 位秒数分布在 47 位计数器的 bit[15..46],低 15 位留给亚秒计数。这就是真相——不是手册说的「高 15 + 低 32」,而是「低 15 位亚秒 + bit15 起的 32 位秒」。手册的位域描述对不上,内核用统一的 64 位读 + 右移 15 一刀解决,根本不纠结手册怎么画。
写时间还要先「停表」
注意 set_time 在写之前调了 snvs_rtc_enable(data, false)(:189),写完再 enable(true)。一边走时一边改计数器会乱套,所以必须先关 SRTC_ENV、改完再开。这和机械表「调时间先拔表冠」是一个道理。
写后同步:rtc_write_sync_lp
还有一个容易漏的细节——写完寄存器,怎么确认它生效了?SNVS 的写入和秒计数是异步的:你 regmap_write 完,值未必立刻反映到计数逻辑,要等下一个 32.768kHz(CKIL)边沿。rtc_write_sync_lp(:109)干的就是这个:
/* drivers/rtc/rtc-snvs.c:120 —— 写后等 3 个 CKIL 周期确认生效 */
/* Wait for 3 CKIL cycles, about 61.0-91.5 µs */
do {
ret = rtc_read_lp_counter_lsb(data, &count2);
...
elapsed = count2 - count1; /* wrap around _is_ handled! */
} while (elapsed < 3 && --timeout);写完后轮询低位计数器,等它跳过 3 个周期(约 61-91µs),说明写入已经和时钟逻辑同步上了。alarm_irq_enable 和 set_alarm 在改完控制位后都会调它,确保设置真正落地。
硬骨头二:alarm 一次性中断
闹钟中断的设计是第二个亮点。先看中断 handler(:278):
/* drivers/rtc/rtc-snvs.c:278 —— alarm 中断处理 */
static irqreturn_t snvs_rtc_irq_handler(int irq, void *dev_id)
{
struct device *dev = dev_id;
struct snvs_rtc_data *data = dev_get_drvdata(dev);
u32 lpsr;
u32 events = 0;
clk_enable(data->clk);
regmap_read(data->regmap, data->offset + SNVS_LPSR, &lpsr);
if (lpsr & SNVS_LPSR_LPTA) { /* LPTA = 闹钟到期标志 */
events |= (RTC_AF | RTC_IRQF);
/* RTC alarm should be one-shot */
snvs_rtc_alarm_irq_enable(dev, 0); /* ← 触发后立刻关掉中断 */
rtc_update_irq(data->rtc, 1, events); /* 通知等待的进程 */
}
/* clear interrupt status —— 写 1 清除 */
regmap_write(data->regmap, data->offset + SNVS_LPSR, lpsr);
clk_disable(data->clk);
return events ? IRQ_HANDLED : IRQ_NONE;
}关键是这一行——
/* RTC alarm should be one-shot */
snvs_rtc_alarm_irq_enable(dev, 0);SNVS 的闹钟硬件,一旦 LPTAR 里的时间和当前时间相等,就会一直触发中断(只要中断使能着)。如果不在 handler 里关掉,它会响个不停、把系统刷爆。所以原厂的设计是 one-shot(一次性):handler 检测到 LPSR_LPTA 置位后,立刻把闹钟中断关掉(alarm_irq_enable(dev, 0),清 LPCR 的 LPTA_EN 位),这样下一次就不会重复触发。
用户想再设一个闹钟?得重新走 set_alarm(:246)——写新的 LPTAR、清 LPSR、再调 alarm_irq_enable 打开。这个「设一次、响一次、自动关」的语义,就是 RTC_WKALM_SET 那类命令期望的行为。
handler 里还有两个标准动作:
rtc_update_irq(data->rtc, 1, events):通知子系统「有事件发生了」。这会让阻塞在read(/dev/rtc0)上等待闹钟的进程被唤醒(06 节 的 alarm demo 就靠这个)。regmap_write(LPSR, lpsr):状态寄存器「写 1 清除」,把刚才置位的LPTA清掉,否则下次进 handler 还会看到它。
为什么 alarm 默认是一次性?
因为「闹钟」的语义就是「到点提醒一次」,不是「每秒提醒」。周期性提醒那是 RTC 的另一种机制(UIE,update interrupt,每秒一次),由 RTC_UIE_ON 命令控制,走的是不同路径。alarm 走一次性,是合理的 API 设计。
小结
这一节啃下了 rtc-snvs.c 最硬的两块:47 位计数器用「双读 + diff>320 判据」的乐观并发规避撕裂读,并用 set_time 的位操作坐实了「手册位布局有误、真相是 bit15 起的 32 位秒」;alarm 中断用 handler 内自动关中断实现 one-shot,配合 rtc_update_irq 唤醒等待者。这两段代码不长,但浓缩了「读硬件并发」「中断语义设计」的实战智慧。
读完这些,rtc-snvs.c 你就算吃透了。下一节我们看看设备树这边 snvs-rtc-lp 节点长什么样,再到板子上把主线 RTC 真正跑起来。