Skip to content

墙上时钟

Overview

RTC - Real-time clock 实时时钟

好吧, 文件系统需要一个时间, 此时我刚刚想到某一天不小心删掉 .git 目录而丢失了 所有的 stash 的自己... -- 2024.05.19

CMOS

CMOS - Complementary Metal Oxide Semiconductor 互补金属氧化物半导体 它是指制造大规模集成电路芯片用的一种技术或用这种技术制造出来的芯片, 是电脑主板上的一块可读写的 RAM 芯片 因为可读写的特性, 所以在电脑主板上用来保存BIOS设置完电脑硬件参数后的数据,这个芯片仅仅是用来存放数据的 -- BaiduBaike

由于是 RAM , 那么就不能断电, 要不然信息就会丢失, 所有 CMOS 有一块专门的 纽扣电池 来维持信息的存储, 也为 实时时钟 的运转提供支撑.

读取

CMOS 有很多个寄存器, 将这么多的寄存器全部暴露未免也太占用端口资源了, 于是它采取了一种好方法,

只提供两个端口: - Port - 0x70 -> 用于定位需要读写的寄存器 - 0x71 -> 用于读写操作

Register Contents Range
0x00 Seconds 0–59
0x02 Minutes 0–59
0x04 Hours 0–23 in 24-hour mode, 1–12 in 12-hour mode, highest bit set if pm
0x06 Weekday 1–7, Sunday = 1
0x07 Day of Month 1–31
0x08 Month 1–12
0x09 Year 0–99
0x32 Century (maybe) 19–20?
0x0A Status Register A
0x0B Status Register B

在读写之前我们需要先关闭 NMI


NMI - Non-Maskable interrupt 不可屏蔽中断

关闭它很简单, 就是选择寄存器时的附属操作, 端口操作的内容就是 (reg | 0x80)


1
2
3
4
5
6
static inline u8 read_rtc(u16 reg)
{
    /* Close the NMI with the flag 0x80 */
    outb (R_CMOS_ADDR, reg | (1 << 7));
    return inb (R_CMOS_DATA);
}

直接读取出来的东西可能是 BCD 码, 这里是有可能, 具体是怎么的需要 CMOS 自己告诉我们

  • Status Register B
  • Bit 2 == 0 -> 使用 BCD 码

读取出来的可能与当前时间有点差距, 即使处理没有问题, 少了 12 个小时的话, 就说明是 12 小时模式.

  • Status Register B
  • Bit 1 == 1 -> 开启 24 小时

这些都是需要进行额外处理的, 剩下的就是写代码了!


写好了代码了吗? 不知道你跑出来的结果是不是有点奇怪, 可能是 qemu 的锅!

qemu-system-x86_64 ... -rtc base=localtime

更多玩法, 请移步至 qemu-rtc.md

BCD

BCD - Binary-coded decimal

对于一个普普通通的 uint8_t, 它可以存储 0 ~ 255 的无符号整型, 如果使用 BCD 来存储, 那么它只能表示 0 ~ 99 的十进制数.

BCD 使用 4bit 来存储一个十进制位:

对于一个 0 ~ 99 的数字: - (bcd & 0xf) 获取个位 - (bcd >> 4) 获取十位

以此类推...

Unix 时间戳

好了前面的内容已经很简单了, 恭喜你到了这里, 发挥你极致的才智, 来将 RTC 提供的乱七八糟的东西变成一个 Unix 时间戳 吧!

起源

我当然说不清楚, 但是我建议去看看这期视频 1

从工具开始

获取当前时间戳:

date +%s

做一个转换, 时间戳为 0, date 此时默认时会做一个时区转换的, 于是先在环境变量中设置 UTC

TZ=UTC date -d @0
Thu Jan  1 12:00:00 AM UTC 1970

格式

时间戳是 从 1970/01/01 00:00:00 (Unix Epoch2) 开始的, 已经过去的秒数.

时间戳是考虑了闰年的...

def is_leap(year):
    if year % 100 == 0:
        if year % 400 == 0:
            return true
        else:
            return false
    else:
        if year % 4 == 0:
            return true
        else:
            return false

计算已经过去了多少个闰年是有骚操作的, 我先用朴素算法. 运用一点点容斥原理:

  • git 6a3da4a5
\[ {nr} = \lceil \frac{{year} - 1972}{4} \rceil - \lceil \frac{{year} - 2000}{100} \rceil + \lceil \frac{{year} - 2000}{400} \rceil \]

2000 是怎么来的? $$ align_up(1972, 100) = align_up(1972, 400) = 2000 $$ 为什么 year ? 特殊值最直观, year = 1972, 但是 1972 年还没有度过. 类似的, year = 2000 , 2000 年还没有度过


\([a, b]\)\(n\) 个正整数可以被 \(p\) 整除, \(a,b,p \in \mathbf{N^*}\)\(p > 1, p \mid a\)

列举:

\[ a, a + p, a + 2p, \dotsb, a + (n-1)p \]

易得:

\[ \begin{cases} a + (n-1)p \le b \\ a + np > b \end{cases} \Rightarrow \frac{b-a+p}{p} \ge n > \frac{b-a}{p} \]

\(p \mid {b-a}\), 则 \(n = \frac{b-a}{p} + 1 \Leftrightarrow n = \lfloor \frac{b-a}{p} + 1 \rfloor\)

\(p \nmid {b-a}\), 则 \(n = \lceil \frac{b-a}{p} \rceil\)

如果原条件变为 \([a, y)\) 则有 \([a, y) \Leftrightarrow [a, y-1]\), 令 \(b = y-1\)

\(p \mid {y-a-1}\), 则 \(\textbf{1}:n = \frac{b-a}{p} + 1 = \lfloor \frac{y-a+p-1}{p} \rfloor = \lceil \frac{y-a}{p} \rceil\)

\(p \nmid {y-a-1}\), 则 \(\textbf{2}:n = \lceil \frac{y-a-1}{p} \rceil\)

现在证明 \(\textbf{1},\textbf{2}\) 在大条件下是 一致的 即可


当然, 如果不用这个公式可以选择更通用的:

区间 [a, b) 中能被 p 整除的整数个数是:

\[ \lfloor \frac{b-1}{p} \rfloor - \lfloor \frac{a-1}{p} \rfloor \]

那么, 接下来的, 就是将上述综合起来

Docs


  1. https://www.bilibili.com/video/BV14j411h7wC 

  2. https://en.wikipedia.org/wiki/Unix_time