TIME
- 首发公号:Rand_cs
相信很多朋友接触计算机的时候都有这么一个疑惑,为什么计算机在关机断电,隔一段时间后重启的时间依然正确?
这背后的原因其实不难猜测,关机后重启的时间正确说明关机的情况下时钟仍然在工作,关机的情况下时钟仍然在工作,说明这个时钟应是有备用电源支持它工作的。
这个时钟叫做 R T C RTC RTC, R e a l Real Real T i m e Time Time C l o c k Clock Clock,可以“永久”的存放系统时间,也就是说它在系统关闭,没有电源的情况下也能继续工作。这里说的是没有计算机的电源(那大个儿电池),电子设备要工作那肯定还是需要电源的,这个电源是主板上的一个微型纽扣电池,计算机断电后实时时钟 R T C RTC RTC 就靠它来供电工作,持续对系统计时,这也就是为什么关机重新启动后时间还是正确的原因。
R T C RTC RTC 时间存放在 C M O S CMOS CMOS 中, C M O S CMOS CMOS, C o m p l e m e n t a r y Complementary Complementary M e t a l Metal Metal O x i d e Oxide Oxide S e m i c o n d u c t o r Semiconductor Semiconductor,互补金属氧化物半导体,别管这绕口名字的本身含义,只需要知道 C M O S CMOS CMOS 就是一 R A M RAM RAM 芯片,是 B I O S BIOS BIOS 的 M e m o r y Memory Memory。 B I O S BIOS BIOS 有各种的设置信息,这些信息没有存放在 B I O S BIOS BIOS 本身的芯片里,而是存放在 C M O S CMOS CMOS 里面。
对 C M O S CMOS CMOS 中的数据的读写是通过两个 I / O I/O I/O 端口来实现的,其中,端口 0 x 70 0x70 0x70 是一个字节的只写端口,用它来选择 C M O S CMOS CMOS 的寄存器,然后再通过 0 x 71 0x71 0x71 端口来读写选择的寄存器,也就是前面所说的 i n d e x / d a t a index/data index/data 的方式访问 C M O S CMOS CMOS 数据。下面就来看看 CMOS 中与时间相关的一些数据(寄存器):
CMOS 寄存器(index)
时间
- 00 h 00h 00h,系统时间“秒数”字段
- 02 h 02h 02h,系统时间“分钟”字段
- 04 h 04h 04h,系统时间“小时”字段
- 07 h 07h 07h,系统“日期”字段(0~31)
- 08 h 08h 08h,系统“月份"字段(0~12)
- 09 h 09h 09h,系统公元纪年的后两位(00 表示 2000,01 表示 2001,依次类推)
状态
-
0
A
h
0Ah
0Ah,状态寄存器
A
A
A
- bit 7,0 表示目前可读时间,1 表示日期正在更新,稍后读取。
-
0
B
h
0Bh
0Bh,状态寄存器B
- bit 2,0 表示使用 b c d bcd bcd 格式,1 表示二进制格式
上述就是与时间有关的一些寄存器,其他部分有兴趣的可以看后面的链接。另外在前面启动我们曾提到过 0 x F 0xF 0xF 号寄存器存放的是 s h u t d o w n shutdown shutdown c o d e code code, B S P BSP BSP 在启动 A P AP AP 时将 s h u t d o w n shutdown shutdown c o d e code code 设置为 0 x A 0xA 0xA 使得 A P AP AP 跳去执行 w a r m warm warm r e s e t reset reset v e c t o r vector vector 记录的引导程序,这部分详见启动理论部分@@@@@@@@
相关函数
读取CMOS寄存器
static uint
cmos_read(uint reg) //0x70端口选择寄存器,0x71端口读出来
{
outb(CMOS_PORT, reg); //选择寄存器:向70h端口写寄存器索引
microdelay(200); //等一会儿
return inb(CMOS_RETURN); //从71h端口将数据读出来
}
这个函数就是向 0 x 70 0x70 0x70 端口写要读写的寄存器索引,然后再从 0 x 71 0x71 0x71 端口操作该寄存器,这里就是从 0 x 71 0x71 0x71 端口将数据给读出来
读取时间
struct rtcdate {
uint second;
uint minute;
uint hour;
uint day;
uint month;
uint year;
};
static void fill_rtcdate(struct rtcdate *r) //读取时间
{
r->second = cmos_read(SECS); //秒数
r->minute = cmos_read(MINS); //分钟
r->hour = cmos_read(HOURS); //小时
r->day = cmos_read(DAY); //日期
r->month = cmos_read(MONTH); //月份
r->year = cmos_read(YEAR); //年份
}c
这个函数就是调用 c m o s _ r e a d cmos\_read cmos_read 将存储在 C M O S CMOS CMOS 中的墙上时间给读取出来,这个函数之上还封装了一层 c m o s t i m e cmostime cmostime:
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB); //读取状态寄存器B
bcd = (sb & (1 << 2)) == 0; //0是BCD格式,为默认值,1是二进制值
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1); //读取时间
if(cmos_read(CMOS_STATA) & CMOS_UIP) //如果时间正更新,稍后读取
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0) //如果两者一样,break,如此操作应是为了确保时间准确
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf)) //BCD码转10进制
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000; //读取出来的year位公元纪年的后两位,所以加上2000
}
整个流程应该是很简单的,主要注意一下 B C D BCD BCD 码,所谓 B C D BCD BCD 码,就是使用 4 位二进制来表示十进制数 0 ∼ 9 0 \sim 9 0∼9, B C D BCD BCD 码也分为多种,最常见的就是 8421 8421 8421 码, 8421 8421 8421 表示各位的权重为 8 8 8 4 4 4 2 2 2 1 1 1,这很像二进制表示。举个例子来看看其中差别, B C D BCD BCD 码 0101 0101 0101 表示十进制 5,这似乎与 5 的二进制表示没什么不同,但如果表示十进制数 15, B C D BCD BCD 码为 0001 0001 0001 0101 0101 0101,而 15 的二进制表示为 1111 1111 1111,这就是它们之间的差别,十进制的每一位都用 4 位 二进制来表示。有了这认识之后来看如何 BCD 码如何转化为十进制:
unsigned char bcd2dec(unsigned char bcd)
{
return ((bcd & 0xf) + ((bcd>>4)*10));
}
原理上很简单, B C D BCD BCD 码从低到高每四位表示着十进制数中的一位,其权重从低到高分别为个十百千,这里因为表示时间的数都很小,只用到了 8 位 B C D BCD BCD 码,所以前四位对应十位,需要乘 10,再加上个位(后四位)就是对应的十进制数字了。
上述就是获取读取
R
T
C
RTC
RTC 时间的操作,其实很简单啊,操作
C
M
O
S
CMOS
CMOS 的
0
x
70
0x70
0x70 和
0
x
71
0x71
0x71 端口再作转化就完事了。这些操作有特权级限制的,因为涉及到了
I
O
IO
IO 操作指令 in
和 out
。对于指令的分类除了常见的什么数据转移指令,算数类指令等等,还有一些特殊的指令分类:特权指令和敏感指令,这部分本应该在启动理论那一块儿就该讲述的,结果那一块的零散知识点太多,写到最后搞忘了,实在抱歉这里补上。
先说敏感指令,敏感指的是对
I
O
IO
IO 特权级敏感,这类指令有 in
ins
out
outs
sti
cli
应该都很熟悉我就不解释它们有什么作用了。敏感指令涉及到了
I
O
IO
IO 特权级,这部分我在进程讲述
T
S
S
TSS
TSS,
I
O
IO
IO 位图的时候详细说过,这里再简单回顾一下。这类指令的执行会检查
C
P
L
≤
I
O
P
L
CPL \le IOPL
CPL≤IOPL,级检查当前特权级是否高于
E
L
F
A
G
S
ELFAGS
ELFAGS 中记录的
I
O
IO
IO 特权级,如果当前特权级大,直接执行没什么问题,如果当前特权级小,那么再检查
I
O
IO
IO 位图对应的端口是否被禁止访问,如果没有禁止,则执行指令,如果禁止了,则抛出一般保护性错。而再
x
v
6
xv6
xv6 里面,
I
O
P
L
IOPL
IOPL 设置的是 0,没有使用
I
O
IO
IO 位图即默认禁止所有端口,所以
x
v
6
xv6
xv6 里的敏感指令只能在内核态下运行
而特权指令则是只有
C
P
L
=
0
CPL=0
CPL=0 即在内核态下才能执行的指令,这类指令都关乎中断,需要最高权限才能执行,有很多,咱们随便看看几个比较熟悉的:
l
g
d
t
lgdt
lgdt 加载
G
D
T
GDT
GDT 用的,
l
t
r
ltr
ltr 加载任务寄存器,与控制寄存器相关的
m
o
v
mov
mov 指令,
h
l
t
hlt
hlt 停机指令等等,其他特权指令有兴趣的见
i
n
t
e
l
intel
intel 手册卷三
5.9
5.9
5.9,手册在我公众号后台回复 手册
即可获取链接。
好了本文就到这里吧,有什么问题还请批评指正,也欢迎大家来同我探讨交流一起学习一起进步。