Introduction
This article will cover basic things about text I / O programming. In the program, which we will create this time ourselves from scratch, we first enter from the keyboard, and then we will display a text line, touching along the path of some unobvious programming features under UEFI.
The second half of the article will be about speeding up the driver load when starting up for debugging, since two minutes that are spent on it now, if you launch it several times during the debugging process, your driver will be very annoying.
Those who are interested - welcome under cat.
Training
Immediately make your catalog for our exercises in the root of
edk2 and call it
EducationPkg . All projects will be created inside it. You can create every single project at the root of
edk2 , there are no obstacles for this, but approximately on the tenth project, the zoo from its projects and packages of the
edk2 framework will divorce at the root, which will lead, at best, to confusion. So, create the directory
C: \ FW \ edk2 \ EducationPkg .
')
Using the UEFI Driver Wizard
Creating a set of files for the
edk2 project is a big topic that deserves a separate article. You can read about it if you want to know the truth right now, in the
EDK II Module Writer's Guide , Chapter 3
Module Development -
here or google. For the time being, we will use the Intel utility
UEFI Driver Wizard , which lies in the downloaded directory in
C: \ FW \ UEFIDriverWizard, to create the fileset.We start the
UEFI Driver Wizard , and the first thing to do is to specify the
Workspace C: \ FW \ edk2 working environment using the
File → OPEN WORKSPACE command . If this is not done first, then the
UEFI Driver Wizard will carefully guide us through all the stages of creation, and then, with apologies, will say that the project driver can not create.
After specifying the Workspace, select
File → New UEFI Driver and perform the following actions:
1. Click on the
Browse button, go to the directory
C: \ FW \ edk2 \ EducationPkg and create inside it your directory
MyFirstDriver , in which we will work further. The driver name will take the same name.
2. Set
Driver Revision to 1.0. I do not advise to put less than one - in the
Wizard there is an error, as a result of which a revision results in 0.0 after creating project files.
3. Everything else is left unchanged, so that it turns out, as shown in the screenshot

Click the Next button and go to the next screen:
We tick the
Component Name Protocol and
Component Name 2 Protocol , do not touch the rest:

and then immediately click Finish, without touching the other settings. We receive a message about the successful creation of the project:

Click
OK and close the
UEFI Driver Wizard , we will not need it anymore.
Adding a module to the list for compilation
Since there is nothing ideal in the world, we will have to manually add a line for our driver to the list of modules for compilation. Here we must stop and give a little help.
All modules in
edk2 are included in the so-called.
Packages are groups of modules that are interconnected according to some common feature. To include a new module in any
package , we need to add the path to the source code of the module to the
PackageName.dsc file (
dsc - short for Description), and this module will be compiled as part of the
Package .
UEFI Wizard Driver our driver for compilation can not be added automatically, alas, for objective reasons (how could he know what you created for your new
package (or which of the existing ones you intend to work with?). Therefore, we prescribe with pens.
Please understand that our
EducationPkg is not yet a
package , but just a collection of projects, and no more.
Open the file
C: \ FW \ edk2 \ Nt32Pkg \ Nt32Pkg.dsc , look for a line in it
# Add new modules here
and after this line we set the path to the file
MyFirstDriver.inf of our driver relative to
C: \ FW \ edk2 . It should turn out like this:
Very important lyrical digression
Now you sadly think that you have to re-configure the project in
Visual Studio , and it is in vain. Remember, in the first article we did not indicate anywhere which project we are talking about. Therefore, you do not need to edit anything, compilation after adding our inf-file to
Nt32Pkg will go like this. This leads us to a very important consequence: we can add to an existing
Visual Studio project, for example,
NT32 , files of any project from the huge
edk2 tree and all of them will be available for editing and, most importantly, setting control points, Watch and all others. features that
Visual Studio offers. Actually, this is one of the most interesting advantages of this approach to working at
UEFI using
Visual Studio . And when you click on
F5, before the compilation starts, the autosave of the modified sources added to the project will be performed. True, we pay for this approach with an increased compilation time, but we will further investigate this problem.
Let's do what we just talked about for our project. Right
click on the
NT32 project in
Visual Studio (
remember to switch it to the default project in
Solution ), select
Add → Existing Item , go to our directory
C:\FW\edk2\EducationPkg\MyFirstDriver
select all files with the mouse and click on
Add , adding them to our
NT32 project.
Compiling and running the driver
Click on
F5 or the
Debugging button in
Visual Studio , wait for the
Shell to load, enter in it:
fs0:
load MyFirstDriver.efi
And then we look at the message about the successful loading of our driver. It does nothing, which is not at all surprising - we haven’t added any functionality to it yet. We just need to check that we correctly added our driver to
edk2 , and nothing more.
Here is what we need to see (the address can be any):

To close the window, click the
Stop Debugging buttons, or
Shift + F5 in
Visual Studio .
Add your code
Open the file
MyFirstDriver.c from the resulting project tree in
Visual Studio and add our code to it. But first, a little theory, before moving on to practice.
In the
UEFI BIOS, all interaction with the hardware, in our case, by emulating it on a virtual machine, occurs through
protocols , you cannot take and write a certain byte to a specific port directly. In a very simplified form, you can consider the protocol as the name of a “class”, an instance of which is created to work with the device when it is registered in the system. Like a regular class instance, the protocol contains data and functions, and all the work with the equipment is ensured by calling the appropriate private functions of the class.
When working in
UEFI ,
tables are used that contain pointers to all instances of the “classes”. There are several of these tables; in the case of our driver, we use the
System Table , using the already declared
gST pointer. The hierarchy of these tables is quite simple: there is a main
System Table , which contains (among other things) references to
Boot Services and
Runtime Services . However, it will probably be easier to show the code:
gST = *SystemTable; gBS = gST->BootServices; gRT = gST->RuntimeServices;
By variable name:
gST stands for:
g - means that the variable is global
ST , you guessed it, means
System TableSo, our task is to send the string
I have written my first UEFI driver to the output device (in our case, the display). It would be nice, of course, to use the same
printf in unix-style and just specify different streams, but alas - there are very serious limitations with the use of
printf in
UEFI , so for now let's keep it aside, until better times.
Insert the output of our line into the function
EntryPoint () of our driver. Add in the variable declaration area our variable of type
CHAR16 (two-byte character encoding
UCS-2 is used ) in the function
MyFirstDriverDriverEntryPoint () in
MyFirstDriver.c :
CHAR16 *MyString = L"I have written my first UEFI driver\r\n";
Lyrical digressionedk2 does not forgive Warning -s, for it is all the same - that Warning , that Error - it sends to, hmm, the fix in both cases, so the compiler options in edk2 are configured by default. He thus implicitly says "Pioneer has no place here." Of course, you can disable this option in configs, but you shouldn’t explain how to do this — if you can disable it, you can remove the sources of Warning messages in the code too, and you don’t need it. Therefore, type the types explicitly and comment on unused variables. Sometimes, in the case of using third-party sources, the complete suppression of errors can be quite a difficult task.
After, at the very end of the
MyFirstDriverDriverEntryPoint () function, insert the output code of our text variable to the output console (default screen, in our case):
gST->ConOut->OutputString(gST->ConOut, MyString);
Click on F5, enter fs0: load MyFirstDriver.efi, and get our line on the screen:

The first program from scratch is written and works. We can congratulate yourself.
Let us now analyze our line:
gST->ConOut->OutputString(gST->ConOut, MyString);
In it:
gST - pointer to the
System TableConOut -
protocol , or, as we conditionally called it, the "class" of text output
OutputString is the output function itself. The first parameter in it, as it is easy to guess, is
this for our protocol
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL .
Let's reformulate the above in a different form, for better understanding. Here is the hierarchy of the main
System Table , obtained in the
Watch window when you stop at a breakpoint in
Visual Studio . The
OutputString function we used is
highlighted . Notice also the
BootServices and
RuntimeServices elements at the bottom of the table, which were discussed earlier:

Entering text from the keyboard and displaying it on the screen
Well, we derived the text contained in the string constant. Now you need to enter something into a string variable and then display this variable on the screen to make sure that you have never made a mistake in the process.
Let's take another screenshot of the same
gST , but already in the
ConIn part. It looks like this:

In our next example, we will memorize the password entered from the keyboard, displaying the characters entered using multi-colored asterisks in the New Year (or drug addict, as one friend noted) style, and after pressing
Enter , display the password in the next line. This is just a training example, replacing the entered password with asterisks is already implemented in the
HII API , as well as re-entering the password, and checking the re-match.
DisclaimerAccording to Feng Shui, it would be necessary to make a “simplifying” pointer * ConOut = gST-> ConOut , but for the purpose of learning, let us leave it as it is, so as not to recall which table we are currently accessing.
First we add local variables in the
MyFirstDriverDriverEntryPoint () function instead of our previously added variable MyString.
UINTN EventIndex; UINTN Index = 0; EFI_INPUT_KEY Keys; CHAR16 PasswordString[256];
Now enter the text of the program, replacing the previously added function
gST->ConOut->OutputString(gST->ConOut, MyString);
to the following:
gST->ConOut->ClearScreen(gST->ConOut);
Hit F5 and settle in the already usual pose for a couple of minutes:

After the
Shell prompt, as always, enter
fs0: and
load MyFirstDriver (or
load my and then
press Tab twice, for
Shell ). We get such a picture (of course, the text will be the one that you entered yourself):

Press Shift + F5 to close debugging, after having admired.
Further in the article we will get acquainted closer with the
UEFI Shell, PCD environment and debug messages - without this, it is impossible to move on. But we will not get acquainted in the abstract, but in the useful process of accelerating and automating the launch of a driver for debugging.
Creating and editing the boot script UEFI Shell
Now, since you probably have already recompiled the program more than once or twice, you are morally ripe to reduce this annoying waiting time for loading
UEFI Shell and driving the same commands each time to load our driver. To begin with, we remove all manual text input to
Shell by writing the appropriate script. We will write the script directly in
Shell . From the point of view of common sense, it is better to open the script file in
Far Manager and edit it there, but what will happen to you in the future that you will be thrown into the
Shell of a real machine, and not a virtual machine with access to its file system from the host. Therefore, once we create a script in the
Shell editor and write it down to get the appropriate skill.
The script file that is executed at startup (similar to
autoexec.bat or
bashrc ) for
UEFI Shell is called
startup.nsh , the one that
Shell invites us to skip every time at boot. Log in to the
UEFI Shell by pressing the
F5 key in
Visual Studio and enter
fs0: to switch to our file system. Now from
Shell enter the command
edit startup.nsh
and in the opened editor, enter, with a line break by
Enter :
FS0: load MyFirstDriver.efi

Next, press
F2 , then
Enter to write and then
F3 to exit the editor back to
Shell .
We will not re-create everything anew this time, since we have not changed anything in the program. We type the
Exit command in the
Shell , and in the text box that opens a-la
BIOS Setup , and in
UEFI terms, the
Main Form , select
Continue . After that, we will be thrown back into
Shell again, but this time the
startup.nsh script we created will be
executed and our driver will start automatically.
Debug messages
For now, we can write debugging messages to the screen, but when we work with
HII forms (
Human Interface Infrastructure ) - such an opportunity will not present itself, the screen will be occupied with hardware configuration forms. What to do in this case?
Somewhat abstract topicAt the very beginning, after receiving a new board from the factory, it is not always, oh, the display console for debugging output is not always available. In terms of the likelihood of possible connection errors, the display goes much ahead of the serial port, in which you can only screw up with the RX-TX direction. Therefore, there is also a serial port output, completely analogous to the one on the screen, with
ConOut replaced by
StdErr . Those. function
gST->ConOut->OutputString(gST->ConOut, L”Test string”);
will display the “Test string” on the display, and the function
gST->StdErr->OutputString(gST->StdErr, L”Test string”);
will output a test message to the serial port. Try it. Later, we will redirect the output in the virtual machine from the serial port to the log file, which may be useful in the future - write logs to a USB flash drive inserted into the USB port on a real hardware.
Display debug information in the OVMF window
To display information in the OVMF window, there is a DEBUG macro, which is usually used as follows:
DEBUG((EFI_D_INFO, "Test message is: %s\r\n", Message));
Where the first argument is a certain analogue of the Linux level of error messages
ERROR_LEVEL , and the second, as you might guess, is a pointer to a string that needs to be output to the OVMF console.
The first argument can take the following values:
#define EFI_D_INIT 0x00000001 // Initialization style messages
#define EFI_D_WARN 0x00000002 // Warnings
#define EFI_D_LOAD 0x00000004 // Load events
#define EFI_D_FS 0x00000008 // EFI File system
#define EFI_D_POOL 0x00000010 // Alloc & Free's
#define EFI_D_PAGE 0x00000020 // Alloc & Free's
#define EFI_D_INFO 0x00000040 // Informational debug messages
#define EFI_D_VARIABLE 0x00000100 // Variable
#define EFI_D_BM 0x00000400 // Boot Manager (BDS)
#define EFI_D_BLKIO 0x00001000 // BlkIo Driver
#define EFI_D_NET 0x00004000 // SNI Driver
#define EFI_D_UNDI 0x00010000 // UNDI Driver
#define EFI_D_LOADFILE 0x00020000 // UNDI Driver
#define EFI_D_EVENT 0x00080000 // Event messages
#define EFI_D_VERBOSE 0x00400000 // Detailed debug messages that may significantly impact boot performance
#define EFI_D_ERROR 0x80000000 // Error
By adjusting the level of appearance of these messages in the log, we can maintain the required balance between the level of logging and the performance of the system as a whole.
More about the DEBUG macro1. Its output has formatting and works like printf formatting. The % r format is very useful for debugging, which displays the Status diagnostic variable not as a 32-bit number in HEX, but as a human-readable line like Supported , Invalid Argument , etc.
2. This macro is automatically disabled when changing Debug to Release , so don’t be in a hurry to comment on it or hang up with ifndefs - everything has already been done for us.
3. Double brackets are really needed there. Try to remove and see what happens.
Small example
To illustrate what was written,
let's add to the
MyFirstDriverDriverEntryPoint () function, immediately after declaring variables, output text from several messages to the log with different levels of debugging and see which ones are displayed and which ones are filtered:
DEBUG((EFI_D_INFO, "Informational debug messages\r\n")); DEBUG((EFI_D_ERROR, "Error messages\r\n")); DEBUG((EFI_D_WARN, "Warning messages\r\n"));
We start to debug and look into the
OVMF window:

It can be seen that the messages with the
EFI_D_INFO and
EFI_D_ERROR levels got into the log, but
they did not hit with the
EFI_D_WARN level.
The mechanism for adjusting debug levels is quite simple: as we see in the list above, each level is characterized by one bit in a 32-bit word. To filter out messages that we do not need at the moment, we set the bit mask to the value of the level that cuts off bits that do not fall into this mask. In our case, the mask was
0x80000040 , because the
EFI_D_WARN level with a value of
0x00000002 was filtered and did not hit the output, and the
EFI_D_INFO level with a value of
0x00000040 , and
EFI_D_ERROR with its value of
0x80000000 were in the mask and the corresponding messages were output.
Now we will not further delve into consideration of the implementation of the debug output level adjustment mechanism, we will consider only ways to change it in practice. There are two of them, the first one is fast, and the second one is correct. We begin, of course, with fast. Open the file
c: \ FW \ Nt32Pkg \ Nt32Pkg.dsc and look for the line in it containing
PcdDebugPrintErrorLevel . Here she is:
gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000040
Change the mask value to
0x80000042 and start building and debugging again.
Since we have changed the configuration file,
edk2 will again rebuild all-all-all binaries for
Nt32Pkg , but this process is finite, and here we see all three lines in the log, which was required to prove:

How to make quickly sorted out. Now do the right thing.
Platform Configuration Database (PCD)
The problem with the previous approach is that the
edk2 tree and the
Nt32Pkg package are the only ones, and changing the system settings for the only project is a direct path to the underworld, because at best in a week you will forget about this change completely and curse the fiend of hell called
edk2 , that a month ago, regularly created from tested source codes under version control exactly what was needed, and now it gives out something completely different. Therefore,
edk2 has a mechanism for changing system settings for a single project in order to localize changes to these settings only for this project. This mechanism is called
PCD -
Platform Configuration Database , and allows a lot. In general, a good style in
edk2 is considered to be to
remove from the source code in
PCD any parameter that may be changed in the future. The length of the article does not allow one to dwell on the
PCD description in more detail; therefore, it is better to look at the details about
PCD here in Section 3.7.3 or
here . For the first time, it is enough to restrict ourselves to reading the file
C: \ FW \ edk2 \ MdeModulePkg \ Universal \ PCD \ Dxe \ Pcd.infFrom the point of view of practice, configuration using
PCD is done like this: in the same file
c: \ FW \ Nt32Pkg \ Nt32Pkg.dsc change the display level of messages in the
OVMF window:
EducationPkg/MyFirstDriver/MyFirstDriver.inf { <PcdsFixedAtBuild> gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042 }
Do not rush to immediately write the file. First, correct back
0x80000042 to the default value
0x80000040 in the line that we edited earlier:
gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000042
And now you can write a file and rebuild the project. We start on debugging on
F5 and we see our treasured three lines in the debug console.
Speeding up debugging
Remove a couple more annoying delays when starting up for debugging. Obviously, the first candidate is waiting for those 5 seconds before launching the
startup.nsh script. Of course, it can also press a space, but any
self-respecting lazy programmer should automate manual operations as much as possible.
Now it is necessary to contradict what was said earlier. The problem is that in these 5 seconds the delays are not written through PCD, but contrary to Feng Shui, directly, in the Shell sources. Therefore, we, whether we want it or not, will have to do the same: open the file
C: \ FW \ edk2 \ ShellPkg \ Application \ Shell \ Shell.c and change the initial value "5" to "1"
ShellInfoObject.ShellInitSettings.Delay = 1;
It would be possible to set it to 0, but you never know ... We will forget to change it on a real hardware system, but it will not be possible to recompile later.
Hit
F5 and enjoy 1 sec. delays instead of 5.
If someone knows how to set this delay value correctly, through
PCD and without editing the
Shell source code - let me know in a personal or in the comments, I will correct it.
Still accelerating
We recall about the progress bar, but in reality - just the timer waiting for the selection of the boot option. This is how it looks on the main screen:

And in
the OVMF launch window, it looks a little different:

As Winnie-the-Pooh used to say, “This is a train — for good reason!” We must find the source and also reduce it to 1 sec.
Run a search on all files with the
* .c extension of the
Zzzzz line, find this line in the source code
C: \ FW \ MdeModulePkg \ Universal \ BdsDxe \ BdsEntry.c and see there such a block of code:
DEBUG ((EFI_D_INFO, "[Bds]BdsWait ...Zzzzzzzzzzzz...\n")); TimeoutRemain = PcdGet16 (PcdPlatformBootTimeOut); while (TimeoutRemain != 0) { DEBUG ((EFI_D_INFO, "[Bds]BdsWait(%d)..Zzzz...\n", (UINTN) TimeoutRemain)); PlatformBootManagerWaitCallback (TimeoutRemain);
Accordingly, it is clear that the TimeoutRemain variable is read from the PCD configuration database , in the PcdPlatformBootTimeOut parameter . Ok, open our configuration file c: \ FW \ Nt32Pkg \ Nt32Pkg.dsc , look for the line with PcdPlatformBootTimeOut there : gEfiMdePkgTokenSpaceGuid.PcdPlatformBootTimeOut|L"Timeout"|gEfiGlobalVariableGuid|0x0|10
, ,
PcdDebugPrintErrorLevel , – ,
MyFirstDriver startup.nsh , , . — , . 10 1 ,
F5 . 23 .
, —
We remove the second display, which we have no reason for yet, but has already begun to annoy. We edit the lines in our favorite configuration file c: \ FW \ Nt32Pkg \ Nt32Pkg.dsc , opening it and removing ! My EDK II 2 and ! UGA Window 2 in two lines to make it: gEfiNt32PkgTokenSpaceGuid.PcdWinNtGop|L"UGA Window 1"|VOID|52
and
gEfiNt32PkgTokenSpaceGuid.PcdWinNtUga|L"UGA Window 1"|VOID|52
You can, of course, change the very inscription in the header of the UGA Window 1 window for something spiritually closer to you personally, but this is up to you.For the future
There is another big reserve for reducing the compile time and launch of the project (at times) - to compile not the entire edk2 , but only our module. But about this, as they say - in the next series. You can still try to do it yourself, an elementary decision, as you will see later.You may now seem overkill - paying so much attention to reducing compilation and launch time, but believe me - when you start writing your programs, the difference between the target 10 seconds before launch, to which we will come, and 2 minutes, which were initially - is huge. Time spent now very quickly will pay off - the third article is not far off, and it will have to debug a lot.