在一个嵌入式系统中需要读取一个持续自增的 64-bit 硬件计时器。由于系统只能通过 32-bit MMIO register 访问该计时器所以硬件把它拆成两个 32-bit 寄存器#define TIMER_LOW_ADDR 0x40001000 #define TIMER_HIGH_ADDR 0x40001010其中TIMER_LOW 表示 timer[31:0] TIMER_HIGH 表示 timer[63:32]计时器会持续递增。当低 32 位从0xFFFFFFFF - 0x00000000发生回绕时高 32 位会加 1。请实现uint64_t read_timer(void);要求安全地返回一个一致的 64-bit timer 值。这个题的考点主要是读取高低位时可能发生的race condition. 考虑以下情况硬件寄存器一开始的值比如HIGH 0x00000001LOW 0xffffffff先读取HIGH: 读到 0x00000001然后在读 LOW 之前timer tick 了一次HIGH 0x00000002LOW 0x00000000再读LOW: 读到 0x00000000如果直接拼0x00000001_00000000这就是错的因为真实时间已经至少是0x00000002_00000000同样的如果我们先读LOW: 读到 0xffffffff然后在读 HIGH 之前timer tick 了一次HIGH 0x00000002LOW 0x00000000再读HIGH: 读到 0x00000002如果直接拼0x00000002_ffffffff这也是错的因为我们的LOW已经wrap 应该读成0x00000002_00000000解决核心是多读一次 HIGH并比较两次 HIGH 的值。如果 hi1 hi2说明在读取 LOW 的前后HIGH 没有变化也就是 LOW 没有发生 wrap-around 进位。因此 LOW 是在同一个 high epoch 下读取到的可以安全拼接(hi1 32) | low如果 hi1 ! hi2说明在两次读取 HIGH 之间LOW 发生了 overflow导致 HIGH 被更新。此时读到的 LOW 可能属于新的 high epoch而第一次 HIGH 属于旧的 epoch所以组合结果不一致需要重新读取直到两次 HIGH 相等。为什么不多读一次 LOW因为 LOW 一直在变。多读 LOW 只能告诉你“时间过了”但不能可靠判断有没有跨过0xffffffff - 0x00000000这个边界。真正有意义的标志是HIGH 有没有变化。 因为 HIGH 只在 LOW wrap 时变化。有了核心思路实现就变得很简单。#define TIMER_LOW_ADDR 0x40001000u #define TIMER_HIGH_ADDR 0x40001010u #define LOW32_REG (*(volatile uint32_t *)TIMER_LOW_ADDR) #define HIGH32_REG (*(volatile uint32_t *)TIMER_HIGH_ADDR) uint64_t read_timer(void) { uint32_t hi1, hi2, lo; do { hi1 HIGH32_REG; lo LOW32_REG; hi2 HIGH32_REG; } while (hi1 ! hi2); return (((uint64_t)hi1 32) | lo); }代码的细节有几点要注意访问MMIO 寄存器有了 MMIO memory address 后需要把地址 cast 成对应宽度的指针(volatile uint32_t *)TIMER_LOW_ADDR这里uint32_t *表示一次读取 32-bitvolatile表示这个地址背后的值可能被硬件随时改变编译器不能缓存、合并或省略读取。然后通过解引用读取寄存器值*(volatile uint32_t *)TIMER_LOW_ADDR所以通常写成宏#define LOW32_REG (*(volatile uint32_t *)TIMER_LOW_ADDR)2. 拼接 64-bit 结果高 32 位要先 cast 成uint64_t再左移 32 bit((uint64_t)hi1 32)然后用 bitwise OR 拼接低 32 位return (((uint64_t)hi1 32) | lo);