[原创]ACPI.sys,从Windows到Bios的桥梁(2):Windows应用程序响应主板上GPIO(SCI)设备中断 软件篇
前篇<ACPI.sys,从Windows到Bios的桥梁(2):Windows应用程序响应主板上GPIO(SCI)设备中断 硬件篇>只完成了硬件部分,本文完成后半部分:在软件层接收和处理来自GPIO的SCI event。前文提到Bios将GPIO事件交由ACPI命名空间\_GPE处理,以软件工程的思维方式响应事件无非2种方式:a.事件接收方如能处理事件,则独自完成事件;b.事件接收方如
引言
在上一篇<ACPI.sys,从Windows到Bios的桥梁(2):Windows应用程序响应主板上GPIO(SCI)设备中断 硬件篇>只完成了2件事:
1.Bios接收GPIO SCI事件;
2.Bios转发SCI事件到ACPI命名空间\_GPE.Method(_L56),然后打印日志。
本文将处理从硬件层移至软件层,完成下列2步:
1.Method(_L56)通知ACPI.sys;
2.驱动接收及处理ACPI Notification(MSDN将这类来自BIOS的SCI事件称为ACPI Notification)。以下是实现该目标的步骤:
Step 1.创建承上启下的虚拟设备
设备驱动程序必然依存于某类设备(DeviceNode),这类设备或真实存在,或虚拟创建。虽然本段的标题写的是创建虚拟设备,但该虚拟设备不同于Winddk Sample中Toast创建的设备----Toast通过IoCreateDevice创建,而是由Bios在ACPI命名空间中创建的设备,希望读者加以区分。
那么这样的虚拟设备该怎么创建?ACPI Spec 5.4节有现成可用的例子!很多OEM厂商为了实现硬件增值功能就会创建这种虚拟设备,比如联想(联想打钱!)ThinkPad的"Lenovo PM Device"就属于此。我们可以借助RW utility查看它的实现:
嗯,大家可以参考ThinkPad,依葫芦画瓢可以写出这样的ASL code,编译进Bios开机过后,就能在设备管理器里找到带黄标的"Unknown Device":
Scope(\_SB) {
Device(HAND) { #HAND->Han's device
Name(_HID, EISAID("HAN0000"))
Method(_STA) {
Return(0x0B)
}
}
}
查看其Hardware ID,和ASL Code中_HID对象指定的值一致:
Step 2.设计测试驱动,解决黄标
到这一步,已经进入到读者熟悉的软件领域,但是工作量仍然不小。
首先来解决黄标问题。根据设备管理器信息可知黄标是由于Bios虚拟的设备没有安装驱动,所以这一步我们用WinDDK sample----Toast来解决黄标问题!我的测试系统是Win10,安装驱动涉及到2个不起眼的方面:
1).需要为驱动包生成.cat文件;
2).为sys/cat签名。没有这2步,驱动没法安装。
2.1.修改Inf文件,使其匹配Bios虚拟的设备
我假设读者熟悉Wdm驱动,所以直接给出能解决黄标问题的inf文件内容:
[Version]
Signature="$CHICAGO$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%TOS%
CatalogFile=demo.cat
DriverVer=02/23/2021,2.2.0.0
[DestinationDirs]
demoNT.CopyFiles = 12
[Manufacturer]
%HAN%=Demo,NTx86,NTia64,NTamd64
[ControlFlags]
ExcludeFromSelect = *HAN0000
[Demo.NTx86]
%*HAN0000.DeviceDescNTx86%=demo,*HAN0000
[Demo.NTia64]
%*HAN0000.DeviceDescNTia64%=demo,*HAN0000
[Demo.NTamd64]
%*HAN0000.DeviceDescNTamd64%=demo,*HAN0000
[SourceDisksNames]
1=%demo%
[SourceDisksFiles]
demo.SYS = 1
[demo.NT]
CopyFiles=demoNT.CopyFiles
[demo.NT.Services]
AddService=demo,2,demo_Service_Inst
[demo_Service_Inst]
DisplayName = %demo.SvcDesc%
ServiceType = 1
StartType = 0
ErrorControl = 1
ServiceBinary = %12%\demo.SYS
[demoNT.CopyFiles]
demo.SYS
[strings]
HAN = "HAN Co., Ltd."
demo = "demo"
*HAN0000.DeviceDescNTx86 = "Demo x86 ACPI-Compliant Value Added Logical and General Purpose Device"
*HAN0000.DeviceDescNTia64 = "Demo ia64 ACPI-Compliant Value Added Logical and General Purpose Device"
*HAN0000.DeviceDescNTamd64 = "Demo x64 ACPI-Compliant Value Added Logical and General Purpose Device"
demo.SvcDesc = "Demo ACPI-Based Value Added Logical and General Purpose Device Driver"
2.2.根据Inf文件生成.cat
Inf文件指定要使用的.cat文件为:demo.cat,参考我的这篇博文:为sys/cat文件生成测试签名,即可生成demo.cat
Inf2cat.exe /driver:E:\WinDDk\my_test\objchk_win7_amd64\amd64\ /os:7_x64
2.3.为.cat/.sys签名
参考我的这篇博文:为sys/cat文件生成测试签名,可以生成证书文件并为sys/cat签名:
Signtool sign /a /v /s PrivateCertStore /n Contoso.com(Test) demo.cat
Signtool sign /a /v /s PrivateCertStore /n Contoso.com(Test) demo.sys
命令执行后,在.cat/.sys文件的文件属性页就有"Digital Signatures"标签页:
完成以上步骤,并在OS下开启Test Mode,之后在设备管理器里对"Unknow Device" 安装驱动程序,即可解决黄标问题:
ACPI\HAN0000从Other Device类移到了System Device类,另外,设备描述名也从Unknown Device变为Inf文件中DeviceDesc节指定的字符串:
3.编写驱动,接受ACPI Notification
这一节要分2部分:
1.虚拟设备HAN0000中转SCI到ACPI.sys;
2.驱动程序 处理ACPI Notify。
3.1.SCI的中转:进一步完善Bios Method(_L56)的功能,转发SCI到驱动层。
如开头所说,前一篇仅在Method(_L56)中打印日志,仅如此,OS无法获知来自PCH的SCI。参考ACPI Spec 5.6 ACPI Event Programming Model,ASL code还需要执行Notify语句,通知其他设备:
还是以ThinkPad的"Lenovo PM Device"为例,当设备(如EC)发现外部事件触发,同时又需要OS协同配合,此时设备会借道"Lenovo PM Device"这个Bios虚拟设备传话到OS(我怎么觉得它更像是阴阳界的灵媒?)
参考联想的做法,我可以在\_GPE.Method(_L56)中加入下列代码,从而将PCH SCI事件转交给\_SB.HAND。至于\_SB.HAND要做什么?它什么都不用做,躺平就行。因为它属于ACPI驱动,如果Bios层不做处理,驱动程序必须接管处理(官僚主义作风?),这不就是将控制权转移到软件层了?
External(\_SB.HAND, MethodObj)
Method (_L56, 0, Serialized){
DSTR("l-event _L56");
Notify(\_SB.HAND,0x9A)
}
3.2.SCI的处理:完成ACPI驱动
ACPI驱动估计只有OEM厂商的驱动工程师会涉及,所以接下来的内容可能对大家有点陌生,但是我们背靠微软的ACPI driver Spec并结合WinDDK Toast,就能使工作量大大减轻。受限于篇幅,我仅列出用于实现接受ACPI Notification功能的代码。
3.2.1.获得ACPI direct call Interface接口并注册AcpiNotify:
NTSTATUS
DemoAddDevice(__in PDRIVER_OBJECT DriverObject,
__in PDEVICE_OBJECT PhysicalDeviceObject
)
{
//Phase 1
PDEVICE_OBJECT fdo = NULL;
PDEVICE_OBJECT lowerDevice = NULL;
PDemoDATA demoData;
RtlInitUnicodeString(&NameString, L"\\Device\\demo");
status = IoCreateDevice (
DriverObject, // our driver object
sizeof (DEMODATA), // device object extension size
&NameString, // FDOs do not have names
FILE_DEVICE_UNKNOWN,
0, // No special characteristics
FALSE,
&fdo);
lowerDevice = IoAttachDeviceToDeviceStack(fdo,PhysicalDeviceObject);
demoData= fdo->DeviceExtension;
demoData->DeviceObject = fdo;
demoData->LowerDeviceObject = lowerDevice;
demoData->Pdo = PhysicalDeviceObject;
//Phase 2
status = GetAcpiInterfaces(demoData);
RegisterNotifyHandler(demoData);
KeInitializeSpinLock(&demoData->RetrieveLock);
//Phase 3
RtlInitUnicodeString(&DOSNameString, L"\\DosDevices\\demo");
status = IoCreateSymbolicLink(&DOSNameString, &NameString);
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
fdo->Flags |= DO_POWER_PAGABLE;
}
代码片中的Phase 1和Phase 3是常规的Wdm驱动框架;Phase 2则是获得ACPI direct call Interface并注册Notify函数。在微软的ACPI driver Spec<ACPI Driver Interface in Windows Vista>中对ACPI direct call Interface有详尽的介绍:
GetAcpiInterfaces用于获得ACPI direct call Interface:
extern ACPI_INTERFACE_STANDARD AcpiInterfaces;
NTSTATUS
GetAcpiInterfaces(IN DemoDATA demoData)
{
KEVENT event;
IO_STATUS_BLOCK ioStatus;
NTSTATUS status;
PIRP Irp;
PIO_STACK_LOCATION irpSp;
KeInitializeEvent(&event, SynchronizationEvent, FALSE);
Irp = IoBuildSynchronousFsdRequest(
IRP_MJ_PNP,
demoData->LowerDeviceObject,
NULL,
0,
NULL,
&event,
&ioStatus
);
if (!Irp) {
return(STATUS_INSUFFICIENT_RESOURCES);
}
Irp->IoStatus.Status = STATUS_NOT_SUPPORTED;
Irp->IoStatus.Information = 0;
irpSp = IoGetNextIrpStackLocation(Irp);
irpSp->MajorFunction = IRP_MJ_PNP;
irpSp->MinorFunction = IRP_MN_QUERY_INTERFACE;
irpSp->Parameters.QueryInterface.InterfaceType = (LPGUID) &GUID_ACPI_INTERFACE_STANDARD;
irpSp->Parameters.QueryInterface.Version = 1;
irpSp->Parameters.QueryInterface.Size = sizeof (AcpiInterfaces);
irpSp->Parameters.QueryInterface.Interface = (PINTERFACE) &AcpiInterfaces;
irpSp->Parameters.QueryInterface.InterfaceSpecificData = NULL;
//
// send the request down
//
status = IoCallDriver(demoData->LowerDeviceObject, Irp);
return status;
}
AddDevice函数用返回的ACPI direct call Interface(存放在全局变量 ACPI_INTERFACE_STANDARD AcpiInterfaces中),调用RegisterNotifyHandler来注册ACPI Notification回调函数demoNotifyHandler :
NTSTATUS
RegisterNotifyHandler(IN PDemoData demoData)
{
NTSTATUS status;
status = AcpiInterfaces.RegisterForDeviceNotifications(
demoData->Pdo,
demoNotifyHandler,
AcpiInterfaces.Context
);
return status;
}
3.2.2.demoNotifyHandler处理来自Acpi层的Event
一般应用程序才是处理事件的终端,它会以异步等待的方式调用DeviceIoControl等待硬件SCI事件发生:
#define TVALDDRVR_DEVICE_OPEN_NAME TEXT("\\\\.\\demo")
hDriver = ::CreateFile(TVALZ_DEVICE_OPEN_NAME,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OVERLAPPED ,
NULL);
::DeviceIoControl(m_hDriver,
IOCTL_TVALZ_RETRIEVE_EVENT,
&iaeInputBuffer,
sizeof(iaeInputBuffer),
&iaeOutputBuffer,
sizeof(iaeOutputBuffer),
&dwReturned, &ol);
if (::GetLastError() == ERROR_IO_PENDING)
{
dwResult = ::WaitForMultipleObjects(2, hEventArray, FALSE, INFINITE) - WAIT_OBJECT_0;
}
1).当事件没有触发时,IRP进入驱动自定义的IRP Pending队列中。对应的应用程序则阻塞等待;
2).当ACPI Manager接收到来自Bios的SCI事件后,则调用注册的回调函数。此时,驱动程序遍历整个队列,比较队列中Pending IRP!AssociatedIrp.SystemBuffer的值是否和NotifyCode的值相同,如果相同则完成IRP。对应的应用程序被唤醒,进行下一轮Wait/Wake:
#define GPIO_QEVENT_L56 0x9a //\_GPE._L56 发出的Notify Code的值为0x9A
VOID
demoNotifyHandler(IN PVOID Context,
IN ULONG ulNotifyValue
)
{
KIRQL Irql;
PULONG pNotification;
PLIST_RETRIEVE_IRP pEntry, pNextEntry;
PIOBUF_ACPI_EVENT pAcpiEvent;
UINT64 MsrData64 = 0; //AddNewEvent+
WORK_QUEUE_ITEM* turboWorkItem; //AddNewEvent+
PIRP retrieveIrp;
DebugPrint(-1,(" : NotifyHandler...Notification=%x\n", ulNotifyValue));
KeAcquireSpinLock(&RetrieveLock, &Irql);
if (!IsListEmpty((PLIST_ENTRY)&RetrieveList)) {
for (pEntry = (PLIST_RETRIEVE_IRP)RetrieveList.ListEntry.Flink; pEntry != &RetrieveList; pEntry = pNextEntry) {
pNextEntry = (PLIST_RETRIEVE_IRP)pEntry->ListEntry.Flink;
pAcpiEvent = (PIOBUF_ACPI_EVENT)pEntry->pIrp->AssociatedIrp.SystemBuffer;
if (pAcpiEvent->ulNotifyCode == ulNotifyValue) {
RemoveEntryList((PLIST_ENTRY)pEntry);
InsertTailList((PLIST_ENTRY)&CompleteList, (PLIST_ENTRY)pEntry);
if(ulNotifyValue == GPIO_QEVENT_L56)
{
DebugInfo(1,("Demo:receive Q-event _L56\n"));
}
else
{}
//tvalz_AddNewEvent+
}
}
}
KeReleaseSpinLock(&RetrieveLock, Irql);
if (!IsListEmpty((PLIST_ENTRY)&CompleteList)) {
for (pEntry = (PLIST_RETRIEVE_IRP)CompleteList.ListEntry.Flink; pEntry != &CompleteList; pEntry = pNextEntry) {
pNextEntry = (PLIST_RETRIEVE_IRP)pEntry->ListEntry.Flink;
RemoveEntryList((PLIST_ENTRY)pEntry);
IoSetCancelRoutine(pEntry->pIrp, NULL);
pAcpiEvent = (PIOBUF_ACPI_EVENT)pEntry->pIrp->AssociatedIrp.SystemBuffer;
pAcpiEvent->dwStatus = STATUS_SUCCESS;
pEntry->pIrp->IoStatus.Information = sizeof(PIOBUF_ACPI_EVENT);
pEntry->pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pEntry->pIrp, IO_NO_INCREMENT);
}
}
TvalzDebug(Tvalz_INFO,("TVALZ : NotifyHandler...end\n"));
}
由于应用程序等待的NotifyCode就是Bios code中Method (_L56) Notify语句抛出的值,因此只要GPIO引脚没接地,回调函数会一直被调用,因此DbgView界面上会输出大量的日志
附注:为了Enable DbgView输出:
1).在注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager下创建DWORD子项"Debug Print Filter",设置其值为0xF;
2).在DbgView中勾选"Enable Verbose Kernel Output"
结尾
本文完。还有后续文章吗?当然有!驱动程序有部分工作是处理电源\Pnp事件,这些也需要Bios协同配合,所以下一篇是Bios和驱动如何处理电源事件~请期待
更多推荐
所有评论(0)