之前在写UEFI程序的时候如果想在终端打印信息,或者接收用户输入的时候调用SystemTable下的ConIn/ConOut protocol就ok了。
//向终端设备输出字符
Status = gST->ConOut->OutputString (
gST->ConOut,
L"hehe"
);
//接收输入设备的信息 EFI_INPUT_KEY Key;
Status = gST->ConIn->ReadKeyStorke (
gST->ConIn,
&Key
);
这样就可以把信息发送到自己主机所有接着的输出终端上/或者读取所有的输入设备。但是当时有个疑问就是用户如何通过一个读取接口把所有的输入设备的输入内容读出来? 于是开始看spec 找到相关的介绍,那么下面就一起开始学习一下这个UEFI的feature —–Console Splitter
Console Splitter
可以参考Beyond BIOS 中关于Console Splitter 的具体介绍,这里就不贴原文了,只是简单的解释一下Console Splitter 的基本原理。就是UEFI 为了系统有多个输入/输出设备那么它提供了一种splitter/merging 的一种机制。简单地讲就是Console Splitter 把自己安装在gST 这个system table 上并且作为一个主要的Console 设备。那么这个Splitter driver 它会虚拟三个设备virtual ConIn , virtual ConOut, virtual ErrOut, 并且给这个安装相应的protocol。它会不停地检测这些输入输出设备,这些输入输出设备是由相应ConOut ,ConIn, ErrOut 这几个变量指定(热插拔设备除外),其实ConOut, ConIn, ErrOut,就是存放了指定设备的PATH,关于设备PATH之后博客会介绍。
如图片所示,Console Splitter 监视这些输入输出设备。
下面开始讲解一下这个Splitter driver的原理和实践过程。
(edk2/MdeModulePkg/Univeral/Console/ConSplitterDxe)
这里只贴有ConIn 的code, 而ConOut,ErrOut大同小异就不贴code
EFI_STATUS
EFIAPI
ConSplitterDriverEntry(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
//
// Install driver model protocol(s).
//
Status = EfiLibInstallDriverBindingComponentName2 (
ImageHandle,
SystemTable,
&gConSplitterConInDriverBinding,
ImageHandle,
&gConSplitterConInComponentName,
&gConSplitterConInComponentName2
);
ASSERT_EFI_ERROR (Status);
Status = ConSplitterTextInConstructor (&mConIn);
if (!EFI_ERROR (Status)) {
Status = gBS->InstallMultipleProtocolInterfaces (
&mConIn.VirtualHandle,
&gEfiSimpleTextInProtocolGuid,
&mConIn.TextIn,
&gEfiSimpleTextInputExProtocolGuid,
&mConIn.TextInEx,
&gEfiSimplePointerProtocolGuid,
&mConIn.SimplePointer,
&gEfiAbsolutePointerProtocolGuid,
&mConIn.AbsolutePointer,
NULL
);
if (!EFI_ERROR (Status)) {
//
// Update the EFI System Table with new virtual console
// and update the pointer to Simple Text Input protocol.
//
gST->ConsoleInHandle = mConIn.VirtualHandle;
gST->ConIn = &mConIn.TextIn;
}
}
这就是Splitter driver的入口函数,首先在这个UEFI driver的Image上安装UefiDriverBindingProtocol。构造一个vistual ConIn device,
然后在这个device 的handl上安装上TextIn ,TextInEx, SimplePointer, AbsolutePointer 相应的protocol.
看一下ConSpliterConInDriverBinding 的 Support 函数
EFI_STATUS
EFIAPI
ConSplitterConInDriverBindingSupported (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
return ConSplitterSupported (
This,
ControllerHandle,
&gEfiConsoleInDeviceGuid
);
}
EFI_STATUS
ConSplitterSupported (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_GUID *Guid
)
{
EFI_STATUS Status;
VOID *Instance;
//
// Make sure the Console Splitter does not attempt to attach to itself
//
if (ControllerHandle == mConIn.VirtualHandle ||
ControllerHandle == mConOut.VirtualHandle ||
ControllerHandle == mStdErr.VirtualHandle
) {
return EFI_UNSUPPORTED;
}
//
// Check to see whether the specific protocol could be opened BY_DRIVER
//
Status = gBS->OpenProtocol (
ControllerHandle,
Guid,
&Instance,
This->DriverBindingHandle,
ControllerHandle,
EFI_OPEN_PROTOCOL_BY_DRIVER
);
if (EFI_ERROR (Status)) {
return Status;
}
gBS->CloseProtocol (
ControllerHandle,
Guid,
This->DriverBindingHandle,
ControllerHandle
);
return EFI_SUCCESS;
}
简单地分析这个函数,这个函数是测试所有的controller handles是否安装了gEfiConsoleInDeviceGuid, 如果support 函数返回success,然后就会调用start 函数。它会检测所有的controller handles 如果这些handles 上有安装device path 并且这个device 的path 和 ConIn 上文提到的变量它是一个mutilate instance path, 如果有匹配其中一个instance 的path 那么就会给这个controller安装这个guid, 热插拔的输入设备不会判断它的path 是否匹配ConIn 中的paths)。那么假设有一个设备它满足这support 函数的要求,所以Start 函数会被调用,下面看看start 函数。
EFI_STATUS
EFIAPI
ConSplitterStdErrDriverBindingStart (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
EFI_STATUS Status;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *TextOut;
//
// Start ConSplitter on ControllerHandle, and create the virtual
// agrogated console device on first call Start for a StandardError handle.
//
Status = ConSplitterStart (
This,
ControllerHandle,
mStdErr.VirtualHandle,
&gEfiStandardErrorDeviceGuid,
&gEfiSimpleTextOutProtocolGuid,
(VOID **) &TextOut
);
if (EFI_ERROR (Status)) {
return Status;
}
}
EFI_STATUS
ConSplitterStart (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_HANDLE ConSplitterVirtualHandle,
IN EFI_GUID *DeviceGuid,
IN EFI_GUID *InterfaceGuid,
OUT VOID **Interface
)
{
EFI_STATUS Status;
VOID *Instance;
//
// Check to see whether the ControllerHandle has the DeviceGuid on it.
//
Status = gBS->OpenProtocol (
ControllerHandle,
DeviceGuid,
&Instance,
This->DriverBindingHandle,
ControllerHandle,
EFI_OPEN_PROTOCOL_BY_DRIVER
);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Open the Parent Handle for the child.
//
Status = gBS->OpenProtocol (
ControllerHandle,
DeviceGuid,
&Instance,
This->DriverBindingHandle,
ConSplitterVirtualHandle,
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER
);
if (EFI_ERROR (Status)) {
goto Err;
}
//
// Open InterfaceGuid on the virtul handle.
//
Status = gBS->OpenProtocol (
ControllerHandle,
InterfaceGuid,
Interface,
This->DriverBindingHandle,
ConSplitterVirtualHandle,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
if (!EFI_ERROR (Status)) {
return EFI_SUCCESS;
}
//
// close the DeviceGuid on ConSplitter VirtualHandle.
//
gBS->CloseProtocol (
ControllerHandle,
DeviceGuid,
This->DriverBindingHandle,
ConSplitterVirtualHandle
);
Err:
//
// close the DeviceGuid on ControllerHandle.
//
gBS->CloseProtocol (
ControllerHandle,
DeviceGuid,
This->DriverBindingHandle,
ControllerHandle
);
return Status;
}
在ConSplitterStart函数里,它主要是返回一个接口,这个接口就每个device上各自的SimpleInProtocol 的接口。所以这个函数我们主要看
Status = gBS->OpenProtocol (
ControllerHandle,
InterfaceGuid,
Interface,
This->DriverBindingHandle,
ConSplitterVirtualHandle,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
其实OpenProtocol 可以这么理解DriverBindingHandle 就是这个driver 和 ConSplitterVirtualHandle 它俩打开了位ControllerHandle也就是device的handle上的SimpleInProtocol 这个protocol的接口。所以返回的Interface就是device上独自protocol接口。EFI_OPEN_PROTOCOL_GET_PROTOCOL,只是记录了ConSplitterVirtualHandle和ControllerHandle是子与父的关系。具体关于OpenProtocol请参考UEFI spec
接下来driver就会调用ConSplitterTextInAddDevic(&mConIn, TextIn); 把这个接口存放在一个数组里。如果有多个device那么这个数组就是存放的每个device的SimpleTextInputProtocol 的实例。
在ConSplitterDriverEntry function 里我们发现了
gST->ConsoleInHandle = mConIn.VirtualHandle ;
gST->ConIn = &mConIn.TextIn ;
所以前文开头提到的我们在App里读取输入设备的信息调用的统一接口就mConIn.TextIn 这个protocol提供的接口。那么接下来就来看看这些接口的实现:
(ReadKeyStroke 对应的就是ConSplitterTextInReadKeyStroke)
ConSplitterTextInReadKeyStroke里我们主要看ConSplitterTextInPrivateReadKeyStroke(Private, Key)在这个函数里我们看到一个for 循环它就是会调用各个device提供的接口。
for (Index = 0; Index < Private->CurrentNumberOfConsoles; Index++){
Status = Private->TextInList[Index]->ReadKeyStroke (
Private->TextInList[Index],
&CurrentKey
);
}
所以以上可以理解就是这个splitter In driver 查询所有的input device 如果找到就把device 上SimpleTextInputProtocol实现的接口统一放在一个数组里,当在应用程序去调用gST->ConIn->ReadKeyStroke的时候这个driver就会把所有device上的ReadKeyStroke函数都循环调用一遍。这个就是简单的流程,但是我们分析还没有结束,因为在读取的用户输入的时候我们必须把当前程序给阻塞住,来等待用户输入。所以一般UEFI 程序写读取输入信息的时候是这样:
EFI_STATUS
WaitForKeyStroke(
IN OUT EFI_INPUT_KEY *Key
)
{
EFI_STATUS Status;
UINTN Index;
while (TRUE) {
Status = gST->ConIn->ReadKeyStroke(gST->ConIn, Key);
if (!EFI_ERROR(Status)) {
break;
}
if (Status != EFI_NOT_READY) {
continue;
}
gBS->WaitForEvent(1, &gST->ConIn->WaitForKey, &Index);
}
return Status;
}
我们看到这个函数会调用gBS->WaitForEvent 它会把当前程序阻塞住不往下执行直到gST->ConIn->WaitForKey 这个event 被signal 起来才会往下执行。所以接下来,我们可以看一下这个event 在什么时候被signal起来。Trace code 我们发现这个event 是在ConSplitterTextInConstructor 这function 里被注册的
Status = gBS->CreateEvent (
EVT_NOTIFY_WAIT,
TPL_NOTIFY,
ConSplitterTextInWaitForKey, ---callback
ConInPrivate,
&ConInPrivate->TextIn.WaitForKey ---Event
);
这里创建的是一个 EVT_NOTIFY_WAIT 类型的event,这个类型的event 意思就是当这个event 被WaitForEvent 或者 CheckEvent 的时候如果当时event没有被signal 起来的时候它的callback 就会调用,打算你二者有点区别,用WaitForEvent的时候如果这个event没有signal 的时候callback会一直被调用直到event被signal,而用CheckEvent 的时候callback 就会被调用一次。有兴趣可以看一下,WaitForEvent 内部就一个while循环然后不停的调用CheckEvent。
因为我们在应用程序里调用了 gBS->WaitForEvent(1, &gST->ConIn->WaitForKey, &Index); 所以ConSplitterTextInWaitForKey 这个function 就会不停地跑
VOID
EFIAPI
ConSplitterTextInWaitForKey (
IN EFI_EVENT Event,
IN VOID *Context
)
{
EFI_STATUS Status;
TEXT_IN_SPLITTER_PRIVATE_DATA *Private;
UINTN Index;
Private = (TEXT_IN_SPLITTER_PRIVATE_DATA *) Context;
if (Private->KeyEventSignalState) {
//
// If KeyEventSignalState is flagged before, and not cleared by Reset() or ReadKeyStroke()
//
gBS->SignalEvent (Event);
return ;
}
//
// If any physical console input device has key input, signal the event.
//
for(Index = 0; Index < Private->CurrentNumberOfConsoles; Index++){
Status=gBS->CheckEvent (Private->TextInList[Index]->WaitForKey);
if (!EFI_ERROR (Status)) {
gBS->SignalEvent (Event);
Private->KeyEventSignalState = TRUE;
}
}
}
我们可以看到它里面有一个for 循环,还是去检查这个数组里记录的input device 他们的waitforkey 的event, 如果它们之中有Event被signal起来的时候它们这个函数就会调用gBS->SignalEvent (Event) 来把自身的Event给signal 起来,所以这个callback 就不会被继续调用了。那么各自device的event其实是它们相应driver 写好的,当有信息输入的时候它们各自的event就会被signal起来,我们我们这里调用CheckEvent 的时候就会返回True,我们的callback就会结束继续调用。
所以到这里我们把Console Splitter 中关于ConIn的function就分析完了,其他的几个driver 基本原理是一样的。
下一篇我们来实践一下,要不然看一遍就是走马观花。下篇,我们会自己写一个虚拟的Input device 然后通过binding 这个 Console Splitter driver ,我们就可以在应用程序里读取这些输入信息。
引用了 Beyond BIOS Second Editon 中关于Splitter 中的内容