[OSDEV]利用ACPI让操作系统关机,重启,响应关机按钮

ACPI(高级配置和电源管理接口,Advanced Configuration and Power Management Interface) 这个到底是什么东西就不多说了,不清楚可以百度

我们用它来关机和重启 (关机的方法貌似不多,就是APM ACPI还有一个貌似是键盘控制器[那天看grub源码,发现grub源码里面就这么关机])

(重启的方法就多了,加载一个坏掉的IDT[比如大小是0的IDT],然后随便触发一个中断就OK了 再就是在开分页机制的情况下弄个随机数给cr3,也会直接重启)

(当然这些方法看起来都不怎么样,我们还是用ACPI比较好)

电脑的状态

这个是在ACPI规范中都规定好了的,大概有以下几种:

1.G0 "working" 正在工作,不解释了吧

2.G1 "sleeping" 睡觉,又分了好多子状态

   a. S1 CPU RAM保持 CPU不执行指令 外部设备电源关闭

   b. S2 CPU关闭 RAM保持

   c. S3 除了RAM和能触发恢复的设备(比如键盘,鼠标)之外的所有设备断电

   d. S4 休眠 所有设备关闭

3.G2 "Soft Off" 软关机,"软件关机"的简称吧(操作系统也是软件,总不能说操作系统是硬件吧....)

4.G3 "Mechanical Off" 机械关机,比如停电了,或者你把电脑的电源拔了... 

(注意这里面没有重启,那个是通过一个名为"Reset Register"的寄存器实现的)

G2软关机也是S5,无论说G2还是说S5都指的一个东西

了解了这个,我们去看一个FADT表,这是我们最重要的一个表

FADT

这个表太长了,从ACPI规范的第114页写到第121页,所以我们不可能全部分析完

(当你阅读的时候,还是建议你打开ACPI规范对着看里面的字段,因为如此之长的表格我们不可能把它复制上来)

(ACPI规范5.0:http://www.acpi.info/DOWNLOADS/ACPIspec50.pdf)

首先,偏移0~36处是Header,基本上就是用于辨认这个表格的,没有实际的数据

偏移36 FIRMWARE_CTRL 我们没有使用,略过....

偏移40 DSDT 这个和SSDT是我们十分重要的一个表格,我们要从中获取到关机命令

偏移44 保留

偏移45 标识OEM的一个东西 略过....

偏移46 当我们按下开关机按钮的时候,对操作系统发送的中断号

偏移47 启用ACPI或者禁用ACPI的端口号

偏移48 ACPI启用命令 把这个字段里的内容发送到偏移47字段指示的端口上就可以启用ACPI

偏移49 ACPI禁用命令 把这个字段里的内容发送到偏移47字段指示的端口上就可以禁用ACPI

偏移54~56 继续略过

偏移56 事件寄存器1a (包括状态寄存器1a和使能寄存器1a)

偏移60 事件寄存器1b (包括状态寄存器1b和使能寄存器1b)

偏移64 控制寄存器1a

偏移68 控制寄存器1b

偏移72~88 继续略过

偏移89 事件寄存器1的大小

偏移89~116 继续略过

偏移116 Reset Register的端口号 Generic Address Structure结构,所以占12个字节

(话说这明明是个寄存器,干嘛要用Generic Address Structure结构,弄的我还以为是个内存地址,试了好长时间才成功)

偏移128 把这个字段里的值写到上一个字段指示的端口中,就可以重启系统

偏移129~268 继续略过

(你也看到ACPI标准已经发展到了5.0版本,所以这里面有些字段可能不支持)

(比如QEMU不支持Reset寄存器..... 这种可以通过header中的length来测试支持到哪里)


我们再来理清一下思路

重启十分清楚了,只要写Reset Register就可以

关机我们需要想控制寄存器写,写哪里我们已经知道了,但是写什么呢? 这正是SSDT/DSDT需要解决的问题

DSDT/SSDT

我们先说一下这三个表格的关系:

|-SSDT

|-FACP

|- |- DSDT

FACP和SSDT是平级的关系,而DSDT则包含在FACP中 (其实包含不怎么准确,只是一个指向DSDT的指针包含在里面)

但是SSDT和DSDT里面数据的格式是完全一样的 (当然只是数据格式,如果数据也完全一样那么我们看一个就够了)

偏移0~36 依然是这个Header,我们只需要其中的length,signature

偏移36~? 数据

我们要做的就是在"数据"这一部分里查找关机的命令 我们来看一下储存关机命令的那一部分的格式

NameOp Name String

0x8     "\_S5_"

PackageOp PkgLength NumElements PackageElementList 

0x10      0xa       4           0xa [data1] 0xa [data2] ....

很显然,第一行是标识符,第二行才是真正的数据,我们要的数据就是表格中用方括号括起来 data1 和 data2

至于怎么用,我们看代码

代码

有些内容已经在前几篇文章中提过的就不多说了,比如调用下面这个函数的地方之类的....

static int parseDT(ACPIHeader *dt)
{
   u32 signature = dt->signature;
   char signatureString[5];
   memcpy((void *)signatureString,(const void *)&signature,4);
   signatureString[4] = '\0';
   printk("Found device %s from ACPI.\n",signatureString);

   if(signature == *(u32 *)"APIC")
      parseApic((ACPIHeaderApic *)dt);
   else if(signature == *(u32 *)"HPET")
      parseHpet((ACPIHeaderHpet *)dt);
   else if(signature == *(u32 *)"FACP")
      acpiEnable(dt,0); /*FADT!!!*/
   else if(signature == *(u32 *)"SSDT")
      acpiEnable(0,dt);  /*SSDT!!!*/
   return 0;
}
这个函数很短,HPET和APIC无视就好,我们迎来了FADT表格和SSDT表格,分别两次把它们传给acpiEnable

int acpiEnable(const void *fadt,const void *ssdt)
{ /*因为fadt和ssdt是两次分别传进来的,所以这边要多处理一下*/
   if(!acpiSsdt && ssdt)
      acpiSsdt = (const ACPIHeader *)ssdt; /*如果传进来了ssdt,但是acpiSsdt还是空的就设置它*/
   if(acpiFadt) /*如果fadt已经设置过了就不用enable了,直接返回*/
      return 0;
   if(!fadt) /*如果acpiFadt没有设置,fadt也是0,那么就是失败了,返回*/
      return -1;
   acpiFadt = (const ACPIFadt *)fadt; /*设置fadt和dsdt*/
   acpiDsdt = (const ACPIHeader *)pa2va(acpiFadt->dsdt);
   if(acpiFadt->smiCommandPort)
      if(acpiFadt->acpiEnable || acpiFadt->acpiDisable) /*判断需不需要我们自己enable,有可能bios已经enable好了,那样的话这里就会跳出*/
         outb(acpiFadt->smiCommandPort,acpiFadt->acpiEnable);
   while(!(inw(acpiFadt->controlRegister1a) & SCI_ENABLED)) /*等待enable完毕*/
      asm volatile("pause");
   if(acpiFadt->controlRegister1b) /*如果这个存在,也等待这个完毕*/
      while(!(inw(acpiFadt->controlRegister1b) & SCI_ENABLED)) 
         asm volatile("pause");
   return 0;
}

至此,ACPI已经启用了,我们来看doPowerOff和doReboot函数

int doPowerOff(void)
{
   closeInterrupt();
   const ACPIHeader *dt = acpiDsdt; /*先找dsdt*/
retry:;
   u8 *data = (u8 *)dt->data; /*获取data和长度*/
   u32 length = dt->length - sizeof(*dt);
   for(;length-- > 0;++data)
   {
      if(data[0] != '_' || data[1] != 'S' || data[2] != '5' || data[3] != '_') 
         continue;   /*检查_S5_*/
      if(data[-1] != 0x8)
         if(data[-1] != '\\' || data[-2] != 0x8)
            continue;   /*检查是否符合格式*/
      if(data[4] != 0x12)
         continue;
      length = 1; /*找到了!*/
      break;
   }
   if(length != 1) /*如果dsdt中没有*/
   {
      if(!acpiSsdt)  /*如果没有ssdt,失败*/
         for(;;);
      if(dt == acpiSsdt) /*如果已经找完了ssdt和dsdt,失败*/
         for(;;);
      dt = acpiSsdt; /*找ssdt*/
      goto retry;
   }

   data += 5; /*跳过一些东西*/
   data += ((data[0] & 0xc0) >> 6) + 2;
   if(data[0] == 0xa)
     ++data;
   u16 s5a = (*data++) << 10; /*获取第一个数据*/
   if(data[0] == 0xa)
      ++data;
   u16 s5b = (*data++) << 10; /*获取第二个数据*/

   for(;;)
   {
      outw(acpiFadt->controlRegister1a,s5a | (1 << 13)); /*发送命令*/
      if(acpiFadt->controlRegister1b)
         outw(acpiFadt->controlRegister1b,s5b | (1 << 13));
   }
   return -1;
}
再来看简单的doReboot:

int doReboot(void)
{
   closeInterrupt();
   if(acpiFadt->header.length < offsetOf(ACPIFadt,unused4[0]))
      goto next;  /*检查是否支持reset寄存器*/
   for(;;) /*发送Reset命令*/
      outb(acpiFadt->resetRegister.address,acpiFadt->resetValue);
next: /*不支持就采用键盘控制器*/
   for(;;)
   {
      while(inb(0x64) & 0x3)
         inb(0x60);  /*把buf读空*/
      outb(0x64,0xfe); /*发送 Reset CPU 指令*/
   }
   return -1;
}

顺便把ACPIFadt等几个结构体贴出来:

typedef struct ACPIHeader{
   u32 signature;
   u32 length;
   u8 version;
   u8 checksum;
   u8 oem[6];
   u8 oemTableID[8];
   u32 oemVersion;
   u32 creatorID;
   u32 creatorVersion;
   u32 data[0];
} __attribute__ ((packed)) ACPIHeader;

typedef struct ACPIAddressFormat{
   u8 addressSpaceID;
   u8 registerBitWidth;
   u8 registerBitOffset;
   u8 reserved;
   u64 address;
} __attribute__ ((packed)) ACPIAddressFormat;
typedef struct ACPIFadt{
   ACPIHeader header;
   u32 firmwareControl;
   u32 dsdt;
   u8 reserved;
   u8 preferredPMProfile;
   u16 sciInterrupt;
   u32 smiCommandPort;
   u8 acpiEnable;
   u8 acpiDisable;
   u8 unused1[56 - 54];
   u32 eventRegister1a;
   u32 eventRegister1b;
   u32 controlRegister1a; /*PM1a_CNT_BLK*/
                         /*System port address of the PM1a Control Register Block.*/
   u32 controlRegister1b; /*PM1b_CNT_BLK*/
                         /*System port address of the PM1b Control Register Block.*/
   u8 unused2[88 - 72];
   u8 eventRegister1Length;
   u8 unused3[116 - 89];
   ACPIAddressFormat resetRegister;
   u8 resetValue;   /*Indicates the value to write to */
                    /*the RESET_REG port to reset the system.*/
   u8 unused4[268 - 129];

} __attribute__ ((packed)) ACPIFadt;

#define SCI_ENABLED    0x1

static const ACPIFadt *acpiFadt = 0;
static const ACPIHeader *acpiSsdt = 0;
static const ACPIHeader *acpiDsdt = 0;
对照上面的分析就不多说了

响应关机按钮

当我们按下机箱上的关机按钮时,很多时候并没有马上关机,依然是和往常一样结束掉所有程序才关机(当然死机之后,长按关机按钮,强制关机除外)

这是怎么做到的呢?

当我们按下关机按钮时,ACPI首先设置状态寄存器里的关机按钮被按下标志,然后检查使能寄存器,查看OS是否需要这个消息

如果需要,ACPI会产生一个SCI中断,告知操作系统状态寄存器发生了变化,操作系统便会检查状态寄存器,做一些响应处理进行关机

看一下前面的分析可以知道,中断号是fadt->sciInterrupt,状态寄存器和使能寄存器都位于事件寄存器

再去查一下ACPI规范便可以知道,状态寄存器是fadt->eventRegister1x + 0,使能寄存器是fadt->eventRegister1x + fadt->eventRegisterLength / 2

也就是说他们均分了事件寄存器,并且状态寄存器在前,使能寄存器在后

这样我们写代码:

static int acpiPowerIRQ(IRQRegisters *reg,void *data)
{
   u16 status = inw(acpiFadt->eventRegister1a);
   if(status & (1 << 8)) /*如果关机按钮被按下了*/
   { /*Section 4.8.3.1.1 Table 4-16*/
     /*1 << 8 :PWRBTN_STS*/
     /*This optional bit is set when the Power Button is pressed.*/
      outw(acpiFadt->eventRegister1a,1 << 8); /*Clear this bit!*/
      doPowerOff();  /*清除这一位并且关机*/
   }
   if(!acpiFadt->eventRegister1b)
      return 0;
   status = inw(acpiFadt->eventRegister1b);
   if(status & (1 << 8)) /*如果存在1b,也对它做同样的检查*/
   {
      outw(acpiFadt->eventRegister1b,1 << 8); /*Clear this bit.*/
      doPowerOff(); /*关机*/
   }
   return 0;
}

static int initAcpiPower(void)
{
   if(!acpiFadt)
      return -1;
   u8 length = acpiFadt->eventRegister1Length; /*获取事件寄存器长度*/
   length /= 2; /*均分*/
   u32 enableRegister1a = acpiFadt->eventRegister1a + length;
   u32 enableRegister1b = acpiFadt->eventRegister1b + length; /*获取两个使能寄存器*/
                        /*See Section 4.8.3.1.2 .*/
   if(enableRegister1b == length)
      enableRegister1b = 0;
   
   outw(enableRegister1a,1 << 8); /*告诉ACPI我们要响应关机按钮*/
   if(enableRegister1b)
      outw(enableRegister1b,1 << 8);
      /*Table 4-17*/
      /*1 << 8:PWRBTN_EN*/

   return requestIRQ(acpiFadt->sciInterrupt,&acpiPowerIRQ); /*申请IRQ*/
}


参考资料

1.ACPI规范5.0 http://www.acpi.info/DOWNLOADS/ACPIspec50.pdf

Advanced Configuration and Power Interface Specification 5.0 

2.OSDEV Forum: "ACPI Power Off" http://forum.osdev.org/viewtopic.php?t=16990

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值