CMOS

来自osdev
Zhang3讨论 | 贡献2022年3月15日 (二) 12:18的版本 (创建页面,内容为““CMOS”是一种极低功耗的静态存储器,与实时时钟(RTC)位于同一芯片上。 它于1984年引入IBM PC AT,使用摩托罗拉MC146818A实时时钟。 CMOS (和实时时钟) 只能通过IO端口0x70和0x71访问。 CMOS存储器的功能是在计算机关闭时为BIOS存储50(或114)字节的“设置”信息 -- 因为有一个单独的电池,可以使时钟和CMOS信息长期保持激活状态。 CMOS值一次访问一个字节…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

“CMOS”是一种极低功耗的静态存储器,与实时时钟(RTC)位于同一芯片上。 它于1984年引入IBM PC AT,使用摩托罗拉MC146818A实时时钟。

CMOS (和实时时钟) 只能通过IO端口0x70和0x71访问。 CMOS存储器的功能是在计算机关闭时为BIOS存储50(或114)字节的“设置”信息 -- 因为有一个单独的电池,可以使时钟和CMOS信息长期保持激活状态。

CMOS值一次访问一个字节,每个字节都可以单独寻址。 每个CMOS地址传统上被称为“寄存器”。 前14个CMOS寄存器访问和控制实时时钟。 实际上,CMOS中唯一真正有用的寄存器是实时时钟寄存器和寄存器0x10。 CMOS中的所有其他寄存器几乎完全过时(或未标准化),因此毫无用处。

不可屏蔽的中断

为了节约,在过去,许多功能被合并在有“空间”的芯片上——即使它们不属于一起。 例如,在PS2键盘控制器上的A20地址。 以同样的方式,将 “NMI禁用” 控制与CMOS控制器和实时时钟放在一起。

NMI旨在以CPU无法忽略的方式将“死机”状态从硬件传送到CPU。 它通常用于发出内存错误信号。 有关NMI的更多信息,请参阅 NMI 文章。

无论何时向IO端口0x70发送字节,高位都会告诉硬件是否要禁止所有NMI到达CPU。 如果该位置1,则禁用NMI(直到下次向端口0x70发送字节)。 发送到端口0x70的任何字节的低位7位用于寻址CMOS寄存器。

CMOS寄存器

最新的CMOS寄存器映射,显示了各种生物之间所有相互冲突的寄存器定义,在名为CMOS.LST的文件中的 RBIL 中。

访问CMOS寄存器

访问CMOS非常简单,但您始终需要考虑如何处理NMI。 您可以通过将寄存器号发送到IO端口0x70来 “选择” CMOS寄存器 (用于读取或写入)。 由于端口0x70的0x80位控制NMI,因此最终也会设置NMI。 因此,您的CMOS控制器始终需要知道您的操作系统是否希望启用NMI。 选择CMOS寄存器如下:

  • outb (0x70, (NMI_disable_bit << 7) | (选定的CMOS寄存器编号));

选择寄存器后,您可以在端口0x71上读取该寄存器的值(使用inb或等效函数),或者向该寄存器写入新值 -- 同样在端口0x71上 ,用outb如下:

  • val_8bit = inb (0x71);

注1:读取或写入端口0x71似乎将“所选寄存器”默认回0xD。 因此,每次要访问CMOS寄存器时,都需要“重新选择”该寄存器。

注2: 在选择端口0x70上的CMOS寄存器之后,在读取/写入端口0x71上的值之前,最好有一个合理的延迟。

校验和

BIOS在启动期间的正常运行取决于CMOS中的值。 因此,使用校验和保护值免受随机变化的影响。 将一个值写入任何CMOS寄存器(RTC除外)都是非常不明智的,因为当您更改一个值时,还必须在另一个寄存器中修复BIOS特定的校验和,否则下一次引导将因“无效校验和”错误而崩溃。 由于校验和位于特定于BIOS的专有寄存器编号,因此祝您好运能找到它。

寄存器0x10

该寄存器包含操作系统可能发现的唯一有用的CMOS值。 它描述了可能连接到系统的两个软驱中每一个的“类型”。 高半字节描述主总线上的“主”软盘驱动器,低半字节描述“从”软盘驱动器。

每个4位半字节和相关软盘驱动器类型的值:

值   驱动类型
 00h	no drive
 01h	360 KB 5.25 Drive
 02h	1.2 MB 5.25 Drive
 03h	720 KB 3.5 Drive
 04h	1.44 MB 3.5 Drive
 05h	2.88 MB 3.5 drive

位0到3 = 从软盘类型,位4到7 = 主软盘类型

内存大小寄存器

有几个CMOS寄存器是标准化的,它们似乎报告了有关系统总内存的有用信息。 但是,它们中的每一个都缺乏您的操作系统所需的重要信息。 使用BIOS函数调用来获取有关内存的信息总是比使用CMOS中的信息要好。 参见检测存储器(X86)

(寄存器0x16 (高字节) | 寄存器0x15 (低字节)) << 10 = 640K = 低内存大小(不计算 EBDA)。

(寄存器0x18(高字节)|寄存器0x17(低字节)<< 10 = 1M到16M(或者可能是65M...绝大多数)之间的总内存, 但是当系统超过64M时,这个数字是特别不可信的,它忽略“memory holes”,忽略内存映射硬件,并且忽略为重要的ACPI系统表保留的内存。

(寄存器0x31 (高字节) | 寄存器0x30 (低字节)) << 16 = 16M和4G之间的总内存... 通常。 但当系统超过4G时,这个数字是不可信的,它忽略了“memory holes”,忽略了内存映射硬件,忽略了为重要的ACPI系统表保留的内存。

硬盘寄存器

在各种位置有许多CMOS寄存器,被各种古老的设备技术所使用,它们用于存储 “硬盘类型” 或其他硬盘信息。 任何此类信息都严格用于过时的基于CHS(磁头柱面扇区Cylinder-Heads-Sector)的磁盘驱动器。 始终可以通过BIOS功能INT13h AH=8获得更好的信息,或者通过在 ATA PIO模式 下向磁盘发送ATA “Identify” 命令。

实时时钟(Real-Time Clock)

RTC会跟踪日期和时间,即使在计算机断电的情况下也是如此。 计算机过去能够做到这一点的唯一其他方法是在每次启动时向人类询问日期/时间。 现在,如果计算机有互联网连接,操作系统有另一种(可以说更好)方式来获取相同的信息。

RTC还可以在IRQ8上生成时钟节拍(类似于PIT在IRQ0上的操作)。 最高可行时钟频率为8KHz。 以这种方式使用RTC时钟实际上可能会产生比PIT更稳定的时钟脉冲。 它还为真正需要近微秒精度的计时事件腾出了空间。 此外,RTC可以在一天中的特定时间生成IRQ8。 有关使用RTC中断的更多详细信息,请参阅RTC文章。

从RTC获取当前日期和时间

要从RTC中获取以下每个日期/时间值,您应该首先确保您不会受到更新的影响 (见下文)。 然后在[#访问CMOS寄存器|常规方式]]中选择相关的“CMOS寄存器”,并从端口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

世纪寄存器(Century Register)

最初,RTC根本没有世纪注册。 在20世纪90年代(随着2000年的临近),硬件制造商开始意识到这可能会成为一个问题;所以他们开始在RTC中添加century寄存器。 不幸的是,由于没有官方标准可遵循,不同的制造商使用不同的寄存器。

这意味着该软件不知道是否有一个世纪寄存器,以及 (如果有) 可能是哪个寄存器。 为了解决这个问题,ACPI规范在其“固定ACPI描述表”的偏移量108处包含了一个“RTC世纪寄存器”字段。 如果该字段包含零,则RTC没有世纪寄存器,如果该字段非零,则它包含要使用的RTC寄存器世纪数字。

如果没有世纪注册,那么软件可以进行猜测。 例如,1990年编写的一个软件可以使用(2位)年份寄存器来确定最可能的世纪 - 如果RTC年份寄存器大于或等于90,则年份可能为“19YY”,如果RTC年份寄存器小于90,则年份肯定为“20YY”。 这样,软件可以正确地确定软件编写后长达99年的世纪。

世纪寄存器同时间和日期健全性检查

如果CMOS/RTC有世纪寄存器,则您的软件是在2014年发布的,而CMOS/RTC说世纪和年份是2008年; 那么显然CMOS/RTC一定是错的。

类似地,人们倾向于偶尔更新操作系统。如果CMOS/RTC有一个世纪寄存器,而您的软件是在2014年发布的,CMOS/RTC说世纪和年份是2154年; 那么操作系统不太可能已经140年没有更新过了,而CMOS/RTC出错的可能性要大得多。

本质上; 当没有世纪寄存器时,猜测世纪的方法(如上所述)比CMOS/RTC世纪寄存器(如果存在)可靠得多。 这意味着世纪寄存器(如果/当存在)可以反向使用,作为检查CMOS/RTC时间和日期是否正常(或CMOS/RTC是否有没电的电池或其他东西)的一种方式。

基本上,您会猜测世纪 (基于软件的发布日期和RTC的年份),然后检查CMOS/RTC世纪是否与您的猜测相同,如果不是,则假设所有CMOS/RTC时间和日期字段无效。

星期寄存器(Weekday Register)

RTC芯片能够跟踪星期几。 它所做的只是在午夜增加其 “工作日” 寄存器,如果增加的值高于7[1],则将其重置为1。 不幸的是,不能保证任何东西都能正确设置该寄存器(包括用户使用BIOS配置屏幕更改时间和日期时)。 它完全不可靠,不应该使用。

确定当前星期几的正确方法是从日期开始计算它 (有关此计算的详细信息,请参阅 Wikipedia上的文章)。

正在进行RTC更新

当芯片更新时间和日期(每秒一次)时,它会先增加“秒”,并检查是否进位。 如果 “秒” 确实要进位,则会增加 “分钟”,并检查是否进位。 这可以在所有的时间和日期寄存器中持续进行(例如,一直到“如果年份进位,则增加世纪”)。 然而,RTC电路通常相对较慢。 这意味着在进行更新时间的同时完全有可能也在读取时间和日期,并获得不可靠/不一致的值 (例如,在9点,你可能读到8:59、8:60、8:00或9:00)。

为了帮助防范此问题,RTC有一个“正在更新”标志(状态寄存器A的位7)。 要正确读取时间和日期,您必须等到 “正在更新” 标志从 “设置1” 变为 “清除0”。 这与检查“正在更新”标志是否清0不同。

例如,如果代码执行"while(update_in_progress_flag != clear)",然后开始读取所有时间和日期寄存器,则在选中“正在更新”标志后,更新可以立即开始,并且代码仍然可能读取到不可靠/不一致的值。 为了避免这种情况,代码应该等到标志被设1,然后在等待标志变成清0。 这样,几乎有1秒的时间来正确读取所有寄存器。

不幸的是,正确执行此操作(等待“正在进行的更新”标志被设1,然后再等待它变成清0)非常慢 - 读取寄存器可能需要整整一秒钟的等待/轮询时间。 这里有两种选择。

第一种选择是依靠“更新中断”。 当RTC完成更新时,它会生成 “更新中断” (如果已启用), IRQ处理程序可以安全地读取时间和日期寄存器,而无需担心更新(也无需检查“正在更新”标志); 只要IRQ处理程序不需要几乎一整秒的时间就可以做到这一点。 在这种情况下,您不会浪费多达1秒的CPU等待/轮询时间,但是在读取时间和日期之前,它可能仍然需要整整一秒钟。 尽管如此,在操作系统引导期间,它仍然是一种有用的技术 - 例如,尽早设置“更新中断”及其IRQ处理程序,然后执行其他操作(例如,从磁盘加载文件),希望IRQ在您需要时间和日期之前发生。

第二种选择是为不可靠/不一致的值做好准备,并在发生时应对它们。 为此,请确保“正在更新”标志已清除(例如"while(update_in_progress_flag != clear)"),然后读取所有时间和日期寄存器; 然后确保“正在更新”标志再次清除(例如"while(update_in_progress_flag != clear)"),并再次读取所有时间和日期寄存器。 如果第一次读取的值与第二次读取的值相同,则这些值肯定是正确的。 如果其中任何值不同,则需要再次执行,并继续执行,直到最新值与以前的值相同。

字节格式

日期/时间RTC字节有4种可能的格式:

  • 二进制或BCD模式
  • 小时格式为12小时或24小时

格式由状态寄存器B控制。 在某些CMOS/RTC芯片上,状态寄存器B中的格式位不能更改。 所以你的代码需要能够处理所有四种可能性,并且它不应该尝试修改状态寄存器B的设置。 因此,您总是需要先读取状态寄存器B,以了解日期/时间字节将以何种格式到达。

  • 状态寄存器B的位1 (值 = 2):如果设1,则启用24小时格式
  • 状态寄存器B的位2 (值 = 4): 如果设1,则启用二进制模式

二进制模式正是您所期望的值。 如果时间为1:59:48 AM,则小时值为1,分钟为59=0x3b,秒为48=0x30。

在BCD模式下,字节的两个十六进制字节中的每一个都被修改为 “表达” 一个 “十进制” 数字。 所以1:59:48表达为 小时 = 1,分钟 = 0x59 = 89,秒 = 0x48 = 72。 要将BCD转换回“良好”二进制值,请使用: binary = ((bcd / 16) * 10) + (bcd & 0xf) [优化: binary = ( (bcd & 0xF0) >> 1) + ( (bcd & 0xF0) >> 3) + (bcd & 0xf)].

24小时时间正是你所期望的。0小时是午夜到上午1点,23小时是下午11点。

把12小时的时间转换成24小时的时间很烦人。 如果小时为PM,则小时字节上的0x80位被设置。 所以你需要对它进行掩码。(对于二进制和BCD模式都是如此。)然后,午夜是12,上午1点是1,等等。 请注意:午夜不是0——它是12——在从12小时格式到24小时格式的计算中,这需要作为特例处理(通过将12设置回0)!

对于星期格式:周日=1,周六=7。

解释RTC值

从表面上看,RTC的这些值似乎非常明显。 主要困难在于确定这些值代表哪个时区。 这两种可能性通常是UTC或系统的时区,包括夏令时。 有关如何处理此问题的更完整讨论,请参阅文章-时间和日期

示例

从CMOS读取

ReadFromCMOS (unsigned char array [])
{
   unsigned char tvalue, index;

   for(index = 0; index < 128; index++)
   {
      _asm
      {
         cli             /*禁用中断*/
         mov al, index   /* 移动索引地址 */
         /* 由于未设置al的0x80位,因此NMI处于活动状态 */
         out 0x70,al     /* 将地址复制到CMOS寄存器*/
         /* 这里进行某种真正的延迟可能是最好的*/
         in al,0x71      /* 提取1个字节到al */
         sti             /* 启用中断*/
         mov tvalue,al
       }

       array[index] = tvalue;
   }
}

写入CMOS

WriteTOCMOS(unsigned char array[])
{
   unsigned char index;

   for(index = 0; index < 128; index++)
   {
      unsigned char tvalue = array[index];
      _asm
      {
         cli             /*清除的中断*/
         mov al,index    /*移动索引地址*/
         out 0x70,al     /* 将地址复制到CMOS寄存器 */
         /* 这里进行某些真正的延迟可能是最好的*/
         mov al,tvalue   /* 将值移至al*/
         out 0x71,al     /* 向CMOS写入1个字节 */
         sti             /* 启用中断 */
      }
   }
}

读取所有RTC时间和日期寄存器

#define CURRENT_YEAR        2022                            // 每年都要改这里!

int century_register = 0x00;                                //如果可能,由ACPI表解析代码设置

unsigned char second;
unsigned char minute;
unsigned char hour;
unsigned char day;
unsigned char month;
unsigned int year;

void out_byte(int port, int value);
int in_byte(int port);

enum {
      cmos_address = 0x70,
      cmos_data    = 0x71
};

int get_update_in_progress_flag() {
      out_byte(cmos_address, 0x0A);
      return (in_byte(cmos_data) & 0x80);
}

unsigned char get_RTC_register(int reg) {
      out_byte(cmos_address, reg);
      return in_byte(cmos_data);
}

void read_rtc() {
      unsigned char century;
      unsigned char last_second;
      unsigned char last_minute;
      unsigned char last_hour;
      unsigned char last_day;
      unsigned char last_month;
      unsigned char last_year;
      unsigned char last_century;
      unsigned char registerB;

      // 注意:这使用“读取寄存器,直到您连续两次获得相同值”的策略
      //       为了避免由于RTC更新而获得不可靠/不一致的值

      while (get_update_in_progress_flag());                //确保没有进行更新
      second = get_RTC_register(0x00);
      minute = get_RTC_register(0x02);
      hour = get_RTC_register(0x04);
      day = get_RTC_register(0x07);
      month = get_RTC_register(0x08);
      year = get_RTC_register(0x09);
      if(century_register != 0) {
            century = get_RTC_register(century_register);
      }

      do {
            last_second = second;
            last_minute = minute;
            last_hour = hour;
            last_day = day;
            last_month = month;
            last_year = year;
            last_century = century;

            while (get_update_in_progress_flag());           //请确保没有进行更新
            second = get_RTC_register(0x00);
            minute = get_RTC_register(0x02);
            hour = get_RTC_register(0x04);
            day = get_RTC_register(0x07);
            month = get_RTC_register(0x08);
            year = get_RTC_register(0x09);
            if(century_register != 0) {
                  century = get_RTC_register(century_register);
            }
      } while( (last_second != second) || (last_minute != minute) || (last_hour != hour) ||
               (last_day != day) || (last_month != month) || (last_year != year) ||
               (last_century != century) );

      registerB = get_RTC_register(0x0B);

      // 如有必要,将BCD转换为二进制值

      if (!(registerB & 0x04)) {
            second = (second & 0x0F) + ((second / 16) * 10);
            minute = (minute & 0x0F) + ((minute / 16) * 10);
            hour = ( (hour & 0x0F) + (((hour & 0x70) / 16) * 10) ) | (hour & 0x80);
            day = (day & 0x0F) + ((day / 16) * 10);
            month = (month & 0x0F) + ((month / 16) * 10);
            year = (year & 0x0F) + ((year / 16) * 10);
            if(century_register != 0) {
                  century = (century & 0x0F) + ((century / 16) * 10);
            }
      }

      // 如有必要,将12小时制转换为24小时制

      if (!(registerB & 0x02) && (hour & 0x80)) {
            hour = ((hour & 0x7F) + 12) % 24;
      }

      // 计算整年(4位)

      if(century_register != 0) {
            year += century * 100;
      } else {
            year += (CURRENT_YEAR / 100) * 100;
            if(year < CURRENT_YEAR) year += 100;
      }
}

另见

外部链接

de::CMOS