Linux2.6.6 内核下 ACPI PCI Hot-Plug 的实现机制

简介: ACPI (Advanced Configuration and Power Interface) 是由业界一些软硬件公司共同开发的开放式工业规范。它能使软、硬件、操作系统(OS),主机板和外围设备,依照一定的方式管理用电情况,系统硬件产生的 Hot-Plug 事件,让操作系统从用户的角度上直接支配即插即用设备,不同于以往直接通过基于 BIOS 的方式的管理。

一.ACPI 热拔插的简介

由 INTEL,MICROSOFT 及 TOSHIBA 所共同开发而成的 ACPI(Advanced Configuration & Power Interface,先进架构电源配置标准)能使软、硬件、操作系统(OS),主机板和外围设备,依照一定的方式管理用电情况,系统硬件产生的 Hot-Plug 事件,让操作系统从用户的角度上直接支配即插即用设备,不同于以往直接通过基于 BIOS 的方式的管理。

这种技术对系统平台、外插板卡硬件上都有特定的要求:系统集成热插拔控制集成电路(PHP ASIC)和 PCI 插槽的逆电流控制器,这样在系统启动过程中可以自动监测 PCI 插槽上是否有设备,当探测到 PCI 插槽上无设备时,能够自动将该插槽断电,在系统的 ACPI BIOS 中包含一系列硬件存储空 PCI 插槽的资源信息如地址段、中断号等以便提供给 Hot Plug 的插卡所用的资源列表 ACPI Table,这些资源列表在上电之后用于进行 PCI 设备的枚举和配置,目前南桥芯片上都集成了 ACPI 协议,例如在 Intel 82801DB I/O Controller Hub 4(ICH4)。

ACPI 基本的体系结构如图 1 所指示:
ACPI 系统由主板总线系统的电气特性支持、主板 BIOS 支持、ACPI 层、操作系统热插拔功能的总线驱动构成。



随着 Intel 64 位 PCI 技术的成熟,Microsoft、Novell 和 SCO 开发的操作系统都开始全面支持 PCI设备 ACPI 热插拔技术(PCI Hot Plug)。

Linux 内核从 2.4 开始支持 ACPI 技术,而到了 2.6.0 以上版本的内核全面支持 ACPI 下的 Hot-plug 规范;根据这个基础,在下面,我们将要从底层的角度和应用的角度上详细介绍基于"传统的"x86 体系下 ACPI PCI 热插拔设备 Linux kernel 下的驱动构成和 ACPI 层上工作模式和流程(在上半部分将介绍基本原理和命令形模式),向非 x86 平台移植的注意事项,以及一些 Linux ACPI 热拔插设备驱动程序开发的注意事项。


二.ACPI 驱动体系简介

ACPI 驱动体系是支持 ACPI Hot Plug 的基础,在论述 Hot Plug 之前首先要介绍 ACPI 体系,根据ACPI 规格定义的 ACPI 驱动体系(简称 ACPI CA),目前 ACPI 组织已经提供了完整的 Unix 版本的ACPI 驱动体系,这个体系主要目的在于让操作系统和当前的 ACPI 硬件隔离开,让 Linux 中通过一系列的接口来访问 ACPI 层。下面列出 ACPI CA 的接口,例如电源管理和配置,热拔插等等:

在 ACPI 规范中将 ACPI 体系分割成 ACPI 核心层(Core subsystem),用于提供基本的 ACPI 服务(AML 翻译和名字空间管理);OS 服务层(OS service)提供针对不同的操作系统的和 ACPI 单元接口服务,下面将详细介绍它们。

a. ACPI 核心层:
ACPI 核心层分成几个相互关联的逻辑模块,每个模块之间包含一些相关的 ACPI API,当用户在编写相关的含有 ACPI 服务的驱动程序的时候,会调用这些相关模块的接口。



1. AML Interpreter: 从上可知AML(后面将详细介绍)分析器是基础,负责分析和运行从本地计算机 BIOS 提供的 AML 文件流,一般说来 AML 翻译器为其他的 ACPI 服务模块提供方法节点运行和获得名字空间中某个方法节点的对象服务。

2. ACPI Table Management 是一个负责载入,管理,分析,校验 ACPI 模块中所使用的各种来自系统 BIOS 的一些特殊的支持 ACPI 服务的表格,例如:RSDT,FSDT,FACS,DSDT等等,这些表在操作系统进行初始化的时候被载入内存。

3. Namespace Management 在 AML 翻译器之上提供名字空间服务,它负责创建和管理内部的名字空间。

4. Resource Management:资源管理提供建立在名字空间资源的配置和获取,其中包括了 PCI的设备的地址区间,中断等重要参数。它所提供的服务包括:获取和设定当前的资源,获取设备上可能存在的地址区间以及 PCI 设备的中断路由表(IRQ Routing Tables),获取当前设备的电源支持能力(例如是否支持 S1-S5 状态)。

5. ACPI H/W Management:该模块用于控制对桥芯片上 ACPI 寄存器和时钟以及其他 ACPI 关联硬件的访问,例如 ACPI GPE 状态寄存器和使能寄存器,系统状态获得。Event handling:事件管理模块是用于管理系统控制中断(SCI)的发生和 GPE 事件的响应,SCI 包括 ACPI 时钟中断,以及 GPE 事件管理。这个单元负责"分发"地址空间和操作空间(OperationRegion)的事件到当前的操作系统层,并负责调用相关的句柄来进行处理。

b.OS 服务层:
ACPI OS 服务层(OSL)可以让 ACPI 逻辑模块在本地操作系统上运行。OS 服务层通过可在主机操作系统中使用的接口,设备驱动程序,将从 ACP 核心的服务转换成本地操作系统的访问和调用;而操作系统层通过 OSL 向 ACPI 核心层发出呼叫;OSL 层对 ACPI 核心层实现了一系列完成操作系统独立功能的标准接口(例如存储分配和硬件访问)。OSL 的组成模块介绍:

1.OS 引导服务:
在 OS 载入过程中引导服务是一些初始化的功能,在大多数其它的操作系统初始化之前执行。这些服务包括 ACPI 子系统的初始化。

2.设备驱动载入服务:
对于出现在 ACPI 名字空间中的设备节点,操作系统必须有一个模块用以探测到它们并载入驱动,读入配置空间,设备驱动载入服务提供这项装置。

3.操作系统运行服务:
运行服务包括大部分 ACPI 系统和 OS 交互的外围接口,用于当前内核的进程/线程操作,以及提供和当前操作系统接口的互斥,信号,进程队列,休眠,暂停等,以及事件日志及电源管理功能。

4.异步服务
异步功能包括中断服务(系统控制中断),事件处理和分配(既定事件,GPE 事件,通知事件和操作区访问事件),以及错误处理。

从 OS 至 ACPI 子系统的请求:
ACPI 核心层和 ACPI OS 层,以及操作系统之间的关系如下:



从上面我们看到了在 Linux2.6.6 中提供了一个完整的符合 ACPI 规范的 ACPI 核心层和 OS 接口函数,不过我们下面论述到的 ACPI PCI 设备热拔插只会用到其中的很小一个部分。


三.ACPI 体系中的重要名词

DSDT: DSDT 称做 Differentiated Definition Block,存在于 BIOS 中并与当前的硬件平台兼容的,提供了系统的硬件特性(例如某些设备的内部寄存器和存储器)的应用策略和配置,在系统初始化的时候,DSDT 被当前系统启动时初始化到名字空间中。

FADT:FADT 中包含了 ACPI 的硬件寄存器组(GPE)的应用和配置(包含它们的硬件地址)也包括DSDT表的硬件地址。

ACPI Namespace: 对于ACPI层来说,内存维持了一个目录形式的指向每个设备,以及 GPE 的名字空间,这个名字树是通过初始化的时候由 DSDT 创建的,名字树可以通过 loadtable 方法从 BIOS 中载入 DSDT 改变,而每个设备在 ACPI 层中都被描述成一个对象,包含有对这个设备特性和操作策略的描述列表,系统所有类型设备都是保存在同一个名字树下。在 ACPI OS 层上调用 _ADR 来获得 Namesapce 的设备名,Namespace 的例子见例 1-1:

OSPM(OS-directed Power Management):OSPM 操作系统支持 ACPI 的一个部分,操作系统 (OS)可以从操作系统下驱动程序的角度控制 ACPI 子模块,同时支持 ACPI 包括 SCI 中断,设备事件,系统事件模式,这些事件模式可以充分支持 Hot-plug 方式。

SCI 中断:(System Control Interrupt) 系统控制中断,SCI 中断是一种源自 ACPI 兼容芯片系统中断,系统映射不同的 ACPI 事件中断向量以便共享此中断,当底层硬件产生 SCI 中断的时候(例如设备插入事件引发中断),根据通知 OSPM 层处理相对应的 ACPI 事件,OSPM 层会调用预先安装的中断句柄。

GPE Block Device 和 GPE 事件:GPE Block Device 是平台设计者可按照 FADT(Fixed ACPI Descriptor Table) 描述表中响应 GPE 的寄存器组,GPE 的输入脚。作为 GPE 设备描述块中的地址存在于 FADT 中,每个 GPE Block Device 可以容纳 128 个 GPE 事件,ACPI 层上提供两个通用目标寄存器组--GPE0_BLK 和 GPE1_BLK,(也就是说可以响应 256 个 GPE 事件)每个寄存器组中包含两个等长度的寄存器 GPEx_STS,GPEx_EN,他们的系统地址(硬件地址)都保存在 FADT 中,作为 GPE Blocks 的行为(或者是操作)描述部分存在于 ACPI 名字空间中;用于指示当前的设备的事件,例如设备插入/拔除事件发生的时候,相关的状态位(GPEx_STS中的位,这个是在硬件设计的时候相关设备的事件信号会连接到这些状态位)会被外部的事件所置位,生成 SCI,让 OSPM 层运行相关的控制程方法通知 ACPI 层;GPEx_EN 表示每个事件的使能位,一般说来在南桥(ICH4)中有这几个寄存器,它们的硬件地址保存在 FADT 中。

GPE 事件就是通过 GPE 寄存器组引发 SCI 中断后,通告 OSPM 层有关设备的事件,例如下面介绍 Hot-Plug 的时候会详细或者简略地介绍到总线枚举,设备检查,设备唤醒,设备弹出几个事件。



ACPI Source Language(ASL):ASL 语言是 ACPI 层用于描述特定的 ACPI 对象的 ACPI 专用语言,并且包括了 ACPI 对象的控制方法(Control method),OEM 厂商和 BIOS 设计者在 BIOS 中使用 ASL 定义所有的设备为 ACPI 对象,并且可以生成 ASL 格式的专门的控制方法,1-1 例就是关于 ASL 的例子:
ASL 的语法规参看 ACPI Specification Revision 2.0
AML 和 AML 分析器:AML 是 ACPI 控制方法的虚拟机器语言,AML 执行过程也就是 ACPI 核心驱动层,ACPI 控制方法使用 AML 来进行编写,但是通常而言对编写者来说是写成 ASL 的方式,通过 AML 翻译器进行翻译,AML 翻译器不但具备 ASL 的翻译的功能,而且可以执行 AML 方法,当用 ASL 编写的 DSDT 表被载入到名字空间的时候,将会被 AML 翻译器翻译成执行时候可以辨别的机器码,例如关键字 SCOPE 在进入 AML 编译器之前中是以一个 ACSII 编码保存在 DSDT 中,但 DSDT 被载入名字空间之后将变成 0x10 的单字节数值(AML 操作值为 ScopeOP)。对 AML 的编译过程和转换方式,ASL 中的关键字可以参看 ACPI Specification Revision 2.0 中 section 17 。


四.在 Linux 中的 ACPI 的 Hot-Plug PCI 工作流程

A.Hot-Plug 的执行简述:
首先在系统上电之后,在系统初始化过程中会通过载入 DSDT 并在内存中动态建立名字空间,在名字空间中,针对每个设备都有响应的"方法节点"以描述设备相对应的具体执行操作,(热拔插事件的处理过程在设备名字空间中为一个方法节点,如下面例子中 Method(LXX){... ...}部分)。以下是一个热拔插的 CD ROM 设备在名字空间的运行方法描述例子:

			Device(IDE0) // PCI IDE控制器
   _ADR   0x40001 // PCI 设备4功能模块1
   _SUN {}        //IDE控制器的状态查询方法
   Device(PRIM) 
      _ADR   0 // IDE设备的第一通道
   Device(SECD)
      _ADR   1 // IDE设备的第二通道
      Device(CDRM)
         _ADR   0 // IDE设备的第二通道
//上的CDROM设备
         _EJ0 {
             // 针对IDE设备的第二通道所要拔除的设备的方法节点,一般上对其停止供电
                }   
	_STA{
		}
_GPE		//名字空间中对GPE Block的表示方法
    Method(_Lxx) { // 某个GPEx_STS的状态位发生变化时的响应处理方法节点(以下为插入
//事件的执行方法)
       Sleep(250) // 延迟250微秒
       If (InsertionEvent) {//检查是否为插入事件
          PWRN() // 如果是设备插入,就给设备上电
       }       
       Notify(CDRM, 1)//向上层(操作系统的驱动系统层,以下简称驱动系统层)通告检测到
		//了CDRM设备插入,那么需要唤醒相应的驱动系统层的PCI设备初  
		//始化程序。
}
			1-1名字空间的例子
			

名字空间中对设备对象的访问状态:对于名字空间中的设备对象,可以采用以下的方法来访问和Hot-Plug相关的部分,对当前PCI插槽上设备的操作可由操作系统的驱动层主动向ACPI层发起:
a. _ADR方法来获得名字空间中的PCI设备(Slot)的地址,返回的地址高16位为Slot号,低16位为Function号。
b.EJ0控制方法来运行设备的热拔出,当使用ACPI核心层接口acpi_evaluate_object调用这个方法的时候,设备就从系统中拔除,它可以调用马达,指示灯,螺线管等等设备。
c. _STA控制方法,获知设备的当前状态,调用该方法以验证是否设备在插槽之内。
d. _SUN方法指示在物理插槽中有哪个设备(功能模块)在系统中。
e. _PS0和PS3这些可选的控制方法开启和关闭对应插槽上的功能设备。

如果设备状态模式检测的信号脚已经连接到对应的GPEx_STS的位上,这时候名字空间中GPEx_STS位的事件要使用ASL编写的_LXX(用于表示某个GPE事件中GPEx_STS状态响应位来指明当前的设备插入事件)来描述事件执行策略。并通过初始化节点对象绑定通告函数(Notify Handler)通告到OSPM执行相关设备的PCI枚举操作,以下是设备插入后所发生的事件:

1. 用户插入PCI热拔插设备到Slot内;

2. HPPC(南桥中热插拔控制器)生成GPE事件;

3. ACPI核心控制芯片组产生SCI中断;

4. ACPI硬件会清除这个GPE事件的响应位,并且运行对应GPE位在名字空间中的_LXX控制方法;

5. _LXX控制方法将根据HPPC的相应控制位来决定当前事件是否为一个设备插入事件,以及哪个插槽有设备插入;

6. _LXX控制方法将向驱动层发出通告(Notify)表示当前的PCI总线上的某一个插槽有设备插入;

7. ACPI驱动层(这里开始就进入ACPI OS层部分了)运行_STA方法获得第6步所知的设备的当前状态,如果运行_STA方法返回0x0a表示当前设备不能使能。

8. ACPI驱动层告诉PCI层准备对新插入的设备进行枚举;

9. PCI设备层读入相关设备(PCI功能模块)的配置信息;

a) 对该PCI卡上所有设备根据设备制造厂商获得相关的驱动程序;
b) 使能PCI卡上所有的功能模块;
c) ACPI驱动层运行_PS0控制方法,根据PCI设备的电源管理规范,使能PCI设备上的电源管理状态寄存器,使PCI设备上电;
d) 正常运行/访问设备。

现在大家对热拔插的模式已经清楚了,下面先从底层,也就是AML翻译器的对名字空间的执行模式开始着手,介绍当前设备在热拔插过程中对驱动层发布命令和GPE事件的响应过程之执行方式,包括:

1. 从操作系统驱动层向ACPI底层如何发布一个ACPI命令,及其执行过程,也就是执行型命令;

2. 一个GPE消息引发一个SCI中断的时候ACPI层的响应过程,也就是事件通告/响应型模式;

3. 如何移植一个ACPI到另外的平台(非x86平台),需要对当前的ACPI 和硬件平台相关的部分做什么样的修改;

4. ACPI中AML的翻译器翻译和执行AML编码流过程。

阅读下文的时候请对照Linux ACPI的源代码一同阅读:


五. 例子.执行型 ACPI 命令模式

例子 :ACPI 的核心层调用接口:status = acpi_evaluate_object
(func->handle, "_EJ0", &arg_list, NULL)

简介:
acpi_evaluate_object 为 ACPI 核心层的调用接口,为用户提供核心层的调用,代入的参数是设备的设备名称,可以在名字空间中把它看作设备在名字空间中的路径,按照该名称可以找到对应设备所在的名字节点(需要执行该方法进行的设备),另外的参数就是参数列表和方法名,例如对于设备的"_EJ0"方法而言,带入的参数为一个整型值,为1表示设备弹出,而方法名就是用于定位设备节点中的方法节点的。

1.我们以下面的例子来解释如何执行一个方法节点,例如图 5 中的 EJ0 方法中使用了 "STORE" 操作符表示执行设备弹出的时候需要针对控制寄存器 REGC 写入一个控制值:

                                Method(_EJ0, Arg1) {    
                                STORE (Arg1,REGC) 
                                 }
			例1-2方法节点_EJ0
                

2.调用 acpi_ns_lookup 在名字空间(Namespace)中定位设备节点和需要执行的方法在设备所对应的节点(如图 5中的方法节点),该方法的节点在名字空间节点中以 acpi_namespace_node 的数据结构形式返回(见图 5)。

3. 锁定名字空间的这个节点,避免有其他进程对当前解析节点的修改。

4.从设备节点中获取当前操作的对象描述数据结构,每个操作节点都有相对应于一个 union acpi_operand_object *object 的枚举类集合用于对设备的所执行的具体方法和带入参数进行描述,包含了所有类型节点的对应对象,例如 _EJ0 所对应的是一个方法对象 struct acpi_object_method,对象中有两个重要的字段aml_length和*aml_start,分别表示该对象在名字空间中AML语言描述的位置和长度,以便在分析运行该方法之前对该方法的定位和扫描(见图5)。

5.创建一个工作状态结构struct acpi_walk_state *walk_state。

6.第一次扫描首先获得的第一个操作符是MethodOP(AML编码为0x14),在walk_state中有三个重要字段对当前的操作和带入的参数进行描述:第一个是opcode,表示当前进行操作的操作符,例如上述的例子是MethodOP;另外一个是op_info表示的当前操作符的描述集合,在(\DRIVERS\ACPI\parser\psopcode.c)有一个数组acpi_gbl_aml_op_info是这些操作符的特性参数集合,例如操作符的序列号(也就是AML的编码),操作符号名,操作符的对象类型,例如MethodOP的对象类型是ACPI_TYPE_METHOD,参数类型等;第一次扫描的时候从这个特性参数集合中获取当前节点的操作的对象参数acpi_operand_object,最后一个在在walk_state的重要的字段是操作数数组:union acpi_operand_object *operands [ACPI_OBJ_NUM_OPERANDS+1],前面介绍过了;上例中包含了两个操作对象, "Method"和"STORE",第一次扫描过程获得当前执行方法内所有的操作对象,在执行第一次扫描的时候载入work_state中的operands字段这些操作对象,同时包含执行方法所需要的参数也同时带入到operands数组中;其载入过程是非常复杂,针对每种类型的对象(例如整型和IO空间等类型)的合法性校验以及不同的载入操作。
上述的例子中"STORE"操作节点在载入walk_state以后,opcode为0x14,而operands[0]为Arg1名字空间节点所表示的对象,operands[1]为寄存器REGC名字空间节点所表示的对象(寄存器长度以及物理基址)。
上面的例子是非常简单的,只有两个操作对象;更多执行方法包含有复杂的操作对象,包括IF,WHILE之类的抉择性分析,以及FOR之类的条件循环。
第一次扫描类似于一种合法性检查。

7.有了第一次扫描获得的操作对象集合,就开始第二次扫描,这次扫描的方式只是重新对已经找到的操作节点扫描。

8.现在就是执行这些操作了,我们从上述例子中知道最简单的一个操作至少要包含一个操作对象,在这个例子中是StoreOP,执行这些操作的时候,而真正的执行过程是STORE (Arg1,REGC),这个需要执行的操作是带有参数的,那么首先就需要把参数放在operands中;接下来对操作对象集合中的元素进行整理,并且进行合法性校验,这个步骤比较复杂,关键是涉及到各个操作符和参数对象之间的关联性和合法性的检测。

9. 最后一个步骤就是把已经经过上述步骤整理好的操作对象集合operands发布到AML分析器执行程序列表中去,这个执行程序列表实际为一个数组,数组中是对应各种操作对象执行函数指针,选择这些函数事实上是根据前面所叙述的操作符的描述集合op_info获得当前操作对象的特性,例如我们看到在acpi_gbl_aml_op_info中StoreOP操作符所指向的特性为AML_TYPE_EXEC_1A_1T_1R,那么对应于当前的STORE这个操作符执行函数为acpi_ex_opcode_1A_1T_1R。

在acpi_ex_opcode_1A_1T_1R这个函数中有对STORE的具体执行过程--将当前的参数值存储在指定的名字空间区域的IO区域内,这个IO区域在第一次扫描DSDT时检索到OperationRegion(用于表示硬件空间的地址,包括IO端口,硬件设备的寄存器组等等)构筑名字空间的时候会建立(参见下面的图5),这个过程可以参看acpi_ex_store函数,这个函数最后会落实到调用acpi_ex_field_datum_io函数中,在此可以看到一个buffer类型的端口的操作,这种端口的操作类型是acpi_object_buffer_field,里面有一个字段所指向的具体的名字空间内的IO地址,最后会调用ACPI内部的一个宏把值拷贝到这个地址上。具体内容就不在这里详细叙述了。



六. 例子:事件通告/响应型

a.Linux关于Hot-Plug有关的SCI中断以及通用事件(GPE)的处理:

下面介绍GPE相关的事件,底层硬件的处理过程:
通用事件寄存器(见重要名字解释部分)的逻辑位于南桥芯片之内,通常而言,所有的ACPI相关的寄存器和电源管理的IO以及寄存器都位于南桥。例如ICH4;保存在BIOS的FADT中,在Linux内核中通常以fadt_descriptor_rev2的数据格式表示,以下是和ACPI通用事件寄存器相关的一些字段,acpi_gbl_FADT数据结构的各个字段的值通常在初始化的时候从BIOS中的RSDT表中获得:



通过FADT获得当前的事件寄存器描述之后,就知道当前ACPI系统中事件寄存器的硬件地址,长度,各个状态位的情况,这里就有一个更加高级的数据结构介入来对每个事件(状态位)的响应调动对应的句柄,这个数据结构就是GPE Block结构:struct acpi_gpe_block_info *gpe_block,它是GPE的响应核心,在初始化ACPI驱动的阶段将会由acpi_ev_create_gpe_block这个函数创建这个数据结构,扫描名字空间(Namespace);在名字空间中通常会有专门的全局对象节点针对GPEx_STS寄存器的各个位具体执行操作节点进行描述,如下例:_GPE用来表示GPE寄存器,以及相应的需要通知OSPM处理动作,例如:

_GPE
    Method(_L01) { // Update device
       Sleep(250) // Mechanical Delay 
       Notify(CDRM, 1)
    }

_GPE表示的是当前的通用事件寄存器的ASL描述,而Method(_L01)当前的通用寄存器的某个位的控制方法,_L表示这个位的触发状态为电平触发,01表示的通用寄存器的01位。

在Linux中通过在初始化名字空间的时候使用acpi_ns_walk_namespace函数对当前的_GPE名字空间进行扫描:

... ...
status = acpi_ns_walk_namespace (ACPI_TYPE_METHOD, gpe_device,
  ACPI_UINT32_MAX, ACPI_NS_WALK_NO_UNLOCK, acpi_ev_save_method_info,
  gpe_block, NULL);
... ...

这里回调函数acpi_ev_save_method_info在扫描名字空间的时候被调用,按照名字空间中针对GPE的描述,找到当前的需要实现响应的GPE节点初始化gpe_block中有关GPE寄存器状态位的结构--event_infoacpi_ev_save_method_info回调函数找到_GPE通用事件寄存器的描述节点,并为在名字空间中通用事件寄存器的每个位安排相应的响应,在acpi_gpe_block_info中有一个字段event_info(事件消息描述结构)包含了每个位所对应的名字空间中所执行的方法节点(method_node字段),如下:

struct acpi_gpe_event_info
{
struct acpi_namespace_node   *method_node; 
 /* 当前GPE事件所需要在执行的名字空间的方法节点,如上面例中的Method(_L01)*/
acpi_gpe_handler                handler;     
 /* 执行的上层程序(驱动程序层)句柄,
例如某些时候在PCI设备进行热拔插的时候需要在插入后,
对资源进行重新分配,例如访问ECDT表(ACPI嵌入式控制器专用),
重新定义PCI配置空间并不需要再重新定义Notify
的操作就直接通过这个句柄直接执行,
例如某些ACPI嵌入式控制器的驱动中就提供了acpi_install_gpe_
handler的接口来安装这个句柄*/
void                        *context;           /* 代入的参数 */
struct acpi_gpe_register_info   
 *register_info; /*指向当前GPE寄存器组 */
u8                           flags;          
/*响应的电平(边沿触发或者是电平触发)*/
u8                           bit_mask;          /* GPEx_STS的掩码 */
};
例1-4 GPE寄存器在Linux中的数据描述结构

从上图看到通过acpi_gpe_event_info这个数据结构就把硬件上的状态寄存器和名字空间中的方法节点对应起来了。

GPE的事件响应机制:通常当事件寄存器有某个位发生变化的时候,如果相应的使能寄存器的位表示这个位使能(使能寄存器在event_info的GPE寄存器组字段 enable_address寄存器表示),那么表示当前的事件响应会有效,然后引发SCI中断,任何ACPI中断都会引发SCI中断,它一般是一个普通的CPU电平中断(这个和x86系统的特征相关),低电平有效。

在ACPI模块中函数acpi_ev_handler_initialize对SCI的中断句柄进行初始化,通常在Linux的ACPI驱动中有每个中断一般会对应两个GPE Block(对应于两个不同的寄存器GPE0_STS和GPE1_STS),另外还包括gpe_block_list_head和acpi_gbl_gpe_xrupt_list_head队列负责处理SMP的情况,它们的关系如下图:

在linux的ACPI处理机制中acpi_ev_sci_xrupt_handler函数就是SCI中断处理的句柄,所有GPE共享这个SCI中断句柄,在acpi_ev_gpe_detect函数中扫描acpi_gbl_gpe_xrupt_list_head全局队列上挂的GPE Block检查事件状态,并且分发到相关的事件的处理进程,并根据当前使能状态寄存器来确定是否响应当前事件,然后调用acpi_os_queue_for_execution这个ACPI OS层的执行过程把当前的所需要执行的进程acpi_ev_asynch_execute_gpe_method放入当前的OS中的执行队列中异步执行,这个执行过程首先会检查当前寄存器合法性,然后执行GPE的对应方法节点并清零GPEx_STS,(调用acpi_ns_evaluate_by_handle执行)其过程和前面的执行型ACPI命令的处理过程相同。

b.Notify的方法执行/初始化过程:

在热拔插入的处理过程的名字空间中可以看到当一个GPE事件发生之后,所调用的方法节点通常会调用"NOFITY"操作符,通知OSPM进行处理:

通常一个ASL的通告操作表示如下:

Notify(\_SB.PCI0.P2P2,0)


\_SB.PCI0.P2P2表示当Hot Plug阶段的该设备接收响应事件,0表示ACPI向OSPM通告消息BUS Check通告, 通知PCI总线驱动层进行枚举。


在热拔插中要用到以下的几个系统级别的通告消息如下(另外0x80以上是针对具体设备对象通告,例如电池,温度控制,热拔插等设备):



表1-1设备对象的OSPM通告类型
通告值: 描述:
0 总线检查:设备对象出现,通知OSPM,完成"Plug and Play"的枚举操作。当收到通知时,OSPM执行这个操作,在热拔插的时候,由ACPI AML通过Notify的方式通知该值到OSPM,
1 设备检查:用于通知OSPM,设备或出现或消失。如果设备出现,OSPM将从设备节点的父节点"重新枚举(re-enumerate)"。如果设备拔出,OSPM将使设备的状态定为无效。OSPM可以优化重新枚举的过程。
2 设备唤醒:用于通知OSPM设备发出了它的睡眠事件信号,OSPM需要通知OSPM的本地设备驱动,此情况仅用于支持_PRW的设备。
3 拔出要求:用以通知OSPM设备应被弹出,同时OSPM需要执行"Plug and Play 弹出操作",与此同时OSPM将运行_Ejx方法。
6 总线模式错配:用以通知OSPM设备被插入一个非当前操作模式的槽或背板中。例如:当用户试图将一个PCI设备热插入一个运行在PCI-X模式下的总线的槽中。
7 电源故障:用以通知OSPM,设备由于电源故障不能从D3状态下转出。

前面说了,执行对象节点方法和前面的执行型的ACPI命令的处理过程相同,这里只详细介绍Notify操作,这个操作是底层(ACPI层)向上层(驱动层)通告当前消息,它和普通的命令形操作没有本质上的区别,但由于向操作系统层通告消息,所以要调用一些ACPI OS部分的API;和上面介绍的_EJ0方法一样,最后会根据操作符的描述集合op_info执行句柄acpi_ex_opcode_2A_0T_0R,在这个执行例程中会进一步根据NOTIFY的通告值分发到处理过程,相关的处理过程是异步执行的,因为最终而言,会调用到acpi_os_queue_for_execution这个ACPI OS层的执行接口,它在Linux中执行方式就是把需要执行的任务(而acpi_ev_notify_ dispatch调用NOTIFY节点对象的实际执行者)插入任务队列中,不过在某些采用实时的Linux系统场合,有一些人会把它插入到立即队列中(Immediate Task Queue),两者实现的效果是相同的。

对于一个NOTIFY节点来说,其节点对象类型是struct acpi_object_notify_common common_notify类型,通常它的内部关键的字段如下表示:

...
union acpi_operand_object  *system_notify;     /* 系统的消息通告 */
union acpi_operand_object  *device_notify;     /* 设备消息通告*/
union acpi_operand_object  *handler;           /* 地址空间中的中的执行句柄 */
...

acpi_operand_object这个结构内都包含一个代表消息执行句柄的acpi_object_notify_handler结构,其中的handler,node,context字段分别表示针对当前通告的执行句柄,当前执行句柄所对应设备的名字空间节点,以及执行句柄带入的参数;

common_notify结构会在驱动程序初始化时候安装acpi_object_notify_handler中对应事件操作句柄,操作系统驱动程序会调用ACPI提供的接口acpi_install_notify_handler完成这个的动作,使用这个接口可以屏蔽掉一些作为驱动设计者无须知道的硬件和ACPI层的细节:

在下面是该函数在Linux中的原型:

acpi_status
acpi_install_notify_handler (
	acpi_handle                     device,
	u32                             handler_type,
	acpi_notify_handler             handler,
	void                            *context
)

1. 第一个参数acpi_handle是需要接收通告的设备句柄;

2. 第二个参数handler_type表示当前设备可以处理的消息号类型,分成设备类型和系统类型两种;

3. acpi_notify_handler表示执行消息的回调函数;

4. 回调函数的带入参数。

--详见acpi_install_notify_handler的源代码,它的流程和结构都非常简单,这里就不再详叙了。




七. Linux操作系统驱动程序如何调用ACPI核心层接口以及Hot Plug事件的执行过程

a.系统初始化阶段:

在上电阶段在PCI设备的扫描节点,ACPI模式根集中器(在当前的绝大多数南桥上都集成了ACPI 2.0协议兼容的寄存器和控制电路它在名字空间中处于一种根的地位,被称为根节点,也就是它们不具备上游节点)被PCI总线驱动枚举,从驱动程序的角度上来看可以把根集中器看作一个PCI桥,包含4个地址区间用于描述PCI桥以下次一级的总线的地址分配情况,通过扫描PCI根集中器得到根集中器,把从ACPI BIOS获得的资源(中断,总线号)绑定在PCI的根集中器的描述结构acpi_pci_root上。

1.初始化根集中器:acpi_pci_root_init是初始化根集中器(以下称为ACPI PCI桥)的第一步,在这里首先调用注册函数acpi_bus_register_driver,它的带入参数是acpi设备的驱动程序描述结构struct acpi_driver acpi_pci_root_driver,该函数注册支持ACPI PCI总线的驱动程序,同样一个ACPI PCI桥也和在名字空间创建之后,搜索名字空间中所有设备的类型和名字,并且把它们绑定到相对应的驱动程序上,启动过程中搜索名字空间中已经定义的ACPI PCI,ACPI桥设备都使用一个acpi_device数据结构表示,这个数据结构是从ACPI的层次上描述设备,支持ACPI的PCI桥同样和标准的PCI-PCI桥使用相同的模式遍历,但是在ACPI中可以使用(_ADR,_BBN,_PRT)从名字空间中获得相关的ACPI PCI桥资源信息(例如设备地址资源,总线号,设备的ID号,中断路由),当对acpi_device的所有的字段都完成初始化之后,就会调用acpi_pci_root_add把得到的根集中器的数据结构挂在acpi_pci_roots队列上,顾名思义这个队列就是系统内所有的根集中器的集合。

2.初始化支持Hot Plug的根集中器插槽:在完成根集中器的初始化之后,接着就是要对ACPI PCI Hot Plug的插槽进行初始化,在系统初始化ACPI HotPlug驱动模块的时候,会首先遍历在acpi_pci_roots队列上的所有ACPI根集中器,并且调用Hot Plug驱动模块acpi_pci_register_driver中的add_bridge方法把当前的具备热拔插功能ACPI的根集中器都找到,如何确定是否支持热拔插呢?在Linux HotPlug中有一个函数detect_ejectable_slots检测是否有可以支持热拔插的SLOT(插槽以acpihp_slot结构表示),插槽是在名字空间中定义的,可以理解为物理设备上的插槽,也可以理解为在PCI桥或者根集中器下的一个负责支持或者管理热拔插的设备,插槽上包含了功能模块(function),表示功能是针对具体设备,如果找到支持热拔插的插槽则表示当前的根集中器是支持热拔插的,反之就会对当前的根集中器下的PCI总线做深度遍历,寻找到下一级的支持Hot Plug的ACPI PCI-PCI桥;热拔插的SLOT是连接在宿主桥也可以连接在PCI-PCI桥之上的,在宿主桥之下有PCI-PCI桥,它采用传统的PCI桥递归方式查找下一级总线的PCI桥;宿主桥获取资源(IO和MEM)是从地址空间中获得。从名字空间中调用_CRS对象找到地址资源后挂上当前的桥描述结构的资源树上。而PCI-PCI桥的资源则是从当前桥设备上的配置空间中获得, 例如在名字空间中,下面是某个设备内的资源表示:

Device(EC0)
{
... ...
Name(_CRS,Buffer(){//表示该_CRS所归属的设备的系统空间,IO或者MEM,中断等资源
IO(Decode16,FC00,FC03,4,4)//表示IO区间宽度为16个Bit,开始地址为FC00结束为FC03,4个字节对//齐,长度为4个字节
IRQ(Level,ActiveHigh,Shared{5}//表示当前的中断为电平,高电平有效,共享的,系统中断5
}
)
... ... 
}
		例1-5 嵌入式控制器EC0的CRS对象在名字空间中的描述

通过acpi_evaluate_integer调用指定设备的方法_BBN和_SEG,获得段号和总线号然后调用add_host_bridge初始化PCI宿主桥上的资源,(IO,MEM)获得总线资源:

		status = acpi_walk_resources(handle, METHOD_NAME__CRS,
		decode_acpi_resource, bridge);
	

资源在ACPI中是作为一个对象节点保存在BIOS中的名字空间,acpi_walk_resources函数使用指定的_CRS方法可以读指定设备的资源对象,放在acpi_resource结构中,然后调用decode_acpi_resource对资源对象解析出合适的资源存储空间,IO空间,下级地址,总线地址,预存取存储空间(采用pci_resource数据结构),把解析完毕的资源绑定在桥芯片描述结构的四个资源队列树上(mem_head,io_head,bus_head,p_mem_head),然后调用专门的函数acpiphp_resource_sort_and_combine对资源树进行整理,并且回收/合并一些相连的资源区间;在获取地址空间资源上,传统PCI-PCI桥和PCI宿主桥是一致的。

完成了PCI-PCI桥和PCI宿主桥的资源配置之后,就是调用init_bridge_misc来初始化这两种根集中器,以及一些已经在PCI总线上的设备,并且完成对这些设备的枚举,另外在这里对ACPI PCI系统或者是设备事件的处理句柄的注册,当PCI桥出现了GPE事件发生时刻处理句柄的注册,会用于对这些事件的响应:

首先调用decode_hpp(调用acpi_evaluate_object)获得ACPI hot-plug的一些参数,例如Cache Line的长度,PCI总线延迟周期(以PCI总线的时钟来记数),PERR和SERR使能寄存器。

然后调用acpiphp_detect_pci_resource把在PCI总线上完成枚举的设备上已经占用的资源从桥的资源列表减掉,这样在PCI桥之下留下来的就是空闲的资源树。

遍历ACPI的名字区间,得到指定设备类型的ACPI设备的对象,并且调用相关的用户函数对其进行初始化,这里遍历函数中调用句柄register_slot,参数acpiphp_bridge *bridge为当前的PCI宿主桥或者是PCI-PCI桥结构,如下:

	status = acpi_walk_namespace(ACPI_TYPE_DEVICE, 
	bridge->handle, (u32)1,register_slot, bridge, NULL);
	

register_slot是一个回调函数,遍历所有的已经存在于名字空间中的PCI总线上的PCI设备(也就是PCI的功能function)。遍历的过程中,有的设备是不支持拔插的,那么直接跳过,我们知道在Linux驱动中PCI的每个功能都是结构pci_dev,每个PCI插槽(SLOT)中对应有8个PCI功能设备(function),而在ACPI的名字空间中则列出每个PCI功能设备(下面的例子是一个关于Hot-plug方式的名字空间,例子中就非常明白地显示出每个PCI槽相对应的功能function)。并且为已经在名字空间中体现出来的设备建立数据描述结构acpiphp_func *newfunc,如果设备存在(也许是Legacy设备或者是已经在启动时候插入的热插拔板卡)那么就对其进行对应的资源初始化工作(acpiphp_init_func_resource),直接把在PCI设备配置空间中的地址资源读出,最后把设备挂在对应的插槽结构中(acpiphp_slot)。如果对应的设备没有插槽结构acpiphp_slot,那么就重新建立新的插槽结构。



最后调用acpi_install_notify_handler把控制句柄handle_hotplug_event_func安装在桥上等待ACPI系统事件和设备事件的触发,这两类事件将引发热拔插的后ACPI热拔插系统的动作。

b.发生拔插事件的上层处理:在设备插入后,有两个比较重要的GPE会从设备端或者是系统端发生:ACPI_NOTIFY_BUS_CHECK,ACPI_NOTIFY_DEVICE_CHECK,他们分别表示Bus Check事件和Device Check事件,都通告当前的PCI总线发生了变化,这两个事件的含义见表1-1,其实两者的处理模式基本是一致的,我们介绍一下Device Check的执行流程:

1.首先是遍历当前宿主桥下所有的SLOT,通过调用其_STA方法获得当前每个插槽上的状态,如果没有_STA方法,就直接检查当前的SLOT上的厂商号是否为全F,如果不是,表示有设备插入。

2.当获得当前状态是ACPI_STA_ALL(表示所有的功能模块均有效)的时候,表示当前的设备上需要使能所有的功能模块,这个时候就要调用使能一个SLOT的函数acpiphp_enable_slot。

3. 使能一个SLOT的第一步是给当前设备上电--power_on_slot,调用当前设备的_PS0方法。

4.第二步是使能并且配置一个SLOT--enable_device,在enable_device中的函数acpiphp_configure_slot为每个插槽上的功能模块(function)进行初始化和配置工作,首先检查每个function是否存在,如果不存在,表明该模块是没有对应设备的,这样才进一步调用函数init_config_space对找到的每一个功能块进行初始化,对于一个标准的PCI设备(function),一般有6个存储空间,init_config_space程序将依次扫描6个存储空间,根据每个存储空间的长度(mem/pmem/io)从桥的地址空间"批发"地址资源到pci_resource *res中,并且写入当前找到的PCI功能模块(function)的配置空间中。

5.接下来调用pci_scan_slot扫描每个Slot上设备的功能模块,这里已经进入PCI层的初始化阶段,在上段中已经对ACPI通告的PCI设备的IO/MEM/Pre-MEM的PCI配置空间中的地址寄存器配置完毕,现在就在从PCI配置空间进一步读入设备信息,并根据设备的具体类型进一步初始化设备,获取PCI的IO和存储空间的基地址(pci_dev结构中的资源字段resource),中断输入脚(pci_read_irq函数会读出PCI_INTERRUPT_PIN/PCI_INTERRUPT_LINE寄存器中的内容,它表示了当前PCI设备的中断输出到8259A的中断输入或者是IO APIC的中断输入上),在PCI核心层上完成PCI设备结构(pci_dev结构)的构建工作。从而最终脱离ACPI层到达PCI设备层。

说到中断,ACPI层的PCI中断和标准的PCI设备的处理方式有一些不一样;对于ACPI层来说,这里为ACPI引入了一个新的中断表--PCI Routing Table,它在BIOS中保存了对PCI设备的中断输入脚映射到中断控制器的输入,需要在初始化ACPI子系统的以后,由设备驱动层调用pci_enable_device在PCI层次上初始化设备以及其驱动,特别是使能中断输入和使能配置空间中的各个IO/MEM空间,这里需通过_PRT获取某个设备中断输入;从_PRT方法获得的PCI设备中断引脚映射将覆盖从PCI枚举阶段所建立的PCI设备到中断控制器之间的中断映射;传统的PCI设备是使用中断修正的方式来进行PCI设备的中断映射。

name(_PRT,Package{/*名字空间中PRT采用Package的方式包装中断定义*/
Package{0x0004fff,0,LINKA,8},/*设备的基地址为0x00004fff,中断请求线为中断A,中断输入为8*/
... ...
Package{0x0004fff,3,LINKD,8},/*设备的基地址为0x00004fff,中断请求线为中断D,中断输入为8*/
})
例1-5 显卡中的_PRT表在名字空间中的部分描述

_PRT实质上是和PCI设备从BIOS中获得的中断路径表类似,对ACPI中断驱动初始化而言是实现从名字空间设备的_PRT节点中获取中断资源并且修正中断过程,从上面的表可以看出,设备中断脚到中断控制器的中断路径事实上也是被定义在_PRT中的,ACPI中规定如果是硬件上直接定义了中断输入脚到中断控制器的路径,那么就确定表示使用那个全局的中断(例如系统中断8),而中断INTA-INTD就不会被定义。

所以对于使用ACPI Hot-Plug PCI设备的驱动程序而言,一定需要使能PCI设备的有效资源,对中断重新安排,所以在热拔插的设备在完成插入之后,需要手工使用/sbin/modprobe命令载入设备内核驱动模块,调用.probe设备驱动接口,绝大部分的热拔插都是使用PCI共享中断,所以一般来说会在PRT方法中直接定义成系统的共享中断,直接获取设备_PRT的中断。


八.向嵌入式系统上移植 ACPI Hot-Plug 模块

目前的 ACPI 平台只能支持 x86 的平台,所以 ACPI Hot plug 也是基本建立在 x86 平台之上,在移植到其他的平台上需要注意几个比较重要的事项:

1. 在硬件上需要使用支持 ACPI 2.0 的南桥,例如 Intel 82801DB I/O Controller 或者是 VIA 的 VT8237 等兼容芯片。

2. 在某些没有 BIOS 的嵌入式平台上(ARM9,StrongARM)需要在程序空间中模拟 FADT,DSDT 两个原在 x86 系统中 BIOS 内的表格,另外还需要程序空间以常数的方式装入名字空间的壳,在初始化的时候通过对 PCI 桥根集中器的扫描动态载入(LoadTable())的方式装入新的设备描述,对 acpi_init 程序需要做少量的修改。

3. 上面说过了完成配置由 ACPI 通告的 PCI 设备,例如网卡 3CR990-TX-97(3Com),DGE-550T(D-Link),可以直接在当前 ACPI Hot Plug 层开始对进行设备层次进行初始化,enable_device 使能完设备以后编写一个读取厂商和设备号的函数段,然后根据设备的类型调用相应设备接口中的 .probe 方法进行初始化。

4. 在名字空间中针对所有 SLOT 的全局中断硬件上预先定义,而取消掉在 pci_enbale_device 程序中 _PRT 获得中断的方法。因为很多嵌入式平台上中断很少,而且没有很多的共享情况。那么设备驱动通过 probe 调用 pci_enbale_device 时候,pci 设备描述结构的中断定义就不会发生变化。


九.ACPI PCI 的热拔插模式的缺陷和未来的 PCI Express 在热拔插技术上的改进

在 ACPI 所支持的热拔插方面,存在以下一系列的缺陷:

1.由于设备的Hot-Plug技术依赖于操作系统直接控制以外的硬件平台(热拔插控制器,以及各种平台下的各种名字空间都不尽相同)支持,这样势必存在诸多的变数――也造成了平台的可靠性取决于不同厂商组件。

2.不同的平台、不同的BIOS、驱动程序和控制器。

3.不同的操作系统所使用的ACPI API应用采用的不同使用设计模式(当然在本文针对Linux的讨论这个不是一个严重的问题)。

4.不一致的兼容模式,诸如原始设备制造商(OEM)和独立硬件厂商(IHV)推出的产品造成兼容上的脱节。

5.操作系统的的热拔插设备数据管理机制需要针对不同的运行平台支持模式,移植能力很差。

针对这些缺陷,PCI-SIG 正式公布的PCI Express 1.0 规范,在热拔插方面比传统的PCI体系有很大的改善,PCI Express技术采用下列方法来解决传统热插拔技术中存在的问题 :

1. PCI Express专为改进热插拔能力而设计,为此,PCI Express在设计时包含了热插拔寄存器(不同于ACPI,热插拔寄存器是一种独立的硬件)。

2. 为操作系统提供了一个通用热插拔硬件寄存器接口――使操作系统能够提供固有的热插拔支持(摒弃 了传统基于ACPI BIOS的方法)。

3. 通过在基础体系结构(即在PCI总线体系)一级之上定义硬件所需的热插拔性能来推广一种标准使用模式。

4.构建于标准体系结构基础之上:标准热插拔控制器(SHPC 2002年发行)。

5.由于PCI Express是通过串行差分线路来实现数据/信令传输的,比以往并行传输的传统PCI大大减少的引脚数量,所以可以改进slot的外形来降低原始设备制造商(OEM)的生产成本,并提高平台可靠性。




你可能感兴趣的:(linux,数据结构,object,express,平台,function)