扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
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_info acpi_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)
通告值: | 描述: |
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 平台只能支持 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)的生产成本,并提高平台可靠性。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。