Experiments in Building UEFI Programs

Thanks to the work of Rust OSDev, building UEFI bootloaders is surprisingly accessible. Just for fun, I built a small toy EFI executable. The executable won't do anything meaningful, such as boot an operating system. In this article, I will go through some of the basics of the EFI environment and how I used them.

Graphics Output Setup

To do anything meaningful in an EFI program, we must use one or more EFI protocols. Protocols can be thought of as analogous to syscalls in a traditional userspace program. There are two protocols we can use to put things on the screen: console I/O, and graphics output. Console I/O is a simple protocol that allows us to just print text to the screen. This protocol is perfect for simple boot messages or interactive command lines - but we want more. This is what the alternative protocol, graphics output, is for. This protocol allows for full control of the output pixels, at the expense of not having builtin niceties like fonts, text positioning, and keyboard input. To give us some drawing primitives, we're going to use the embedded-graphics family of crates. To start off, we'll create a type for holding the graphics output handle and owning the in-progress buffer.

struct Graphics<'a>
{
  gop: ScopedProtocol<'a, GraphicsOutput>,
  width: usize,
  height: usize,
  buffer: Vec<BltPixel>
}

impl<'a> Graphics<'a>
{
  fn new(bt: &'a BootServices) -> Graphics<'a>
  {
    // Grab the handle for the graphics output protocol
    let gop_handle = bt.get_handle_for_protocol::<GraphicsOutput>().unwrap();

    // Signal that we are going to be the exclusive users of it
    let gop = bt.open_protocol_exclusive::<GraphicsOutput>(gop_handle).unwrap();

    // Fetch the resolution - we'll need it later
    let (width, height) = gop.current_mode_info().resolution();

    // Initialize the struct, and initialize the buffer with all black pixels
    Self { gop, width, height, buffer: vec![BltPixel::new(0, 0, 0); width * height] }
  }
}

To make our Graphics struct usable as a target for embedded-graphics functions, we need to implement two traits. The first is trivial - Dimensions, which contains a single function to report the bounding box we must draw within. The second, DrawTarget, is more complex. The primary function we must implement takes an iterator of pixels that correspond to whatever is current being drawn to this target. A smart implementation would batch all these together, then expose a method to bitblit the combined buffer to the screen all at once. In practice, I found that performing a bitblit on every draw call didn't lead to too much flicker, so I opted to leave this as is.

fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
  where I: IntoIterator<Item = Pixel<Self::Color>>
{
  for pixel in pixels
  {
    // ensure the pixel is within the screen coordinates
    if pixel.0.x >= 0 && pixel.0.x < self.width as i32 && pixel.0.y >= 0 && pixel.0.y < self.height as i32
    {
      // open the target pixel for writing
      let target_pixel = self.buffer.get_mut(pixel.0.y as usize * self.width + pixel.0.x as usize).unwrap();

      // copy each channel
      target_pixel.red = pixel.1.r();
      target_pixel.green = pixel.1.g();
      target_pixel.blue = pixel.1.b();
    }
  }

  // bitblit the entire screen
  self.gop.blt(BltOp::BufferToVideo
    {
      buffer: &self.buffer,
      src: BltRegion::Full,
      dest: (0, 0),
      dims: (self.width, self.height),
    })
}

We then create a main function with standard boilerplate for initializing an EFI program. Most of this is handled by the #[entry] macro, but we are responsible for the lifetime of the boot services. We can use the boot services object to initialize our graphics handling struct.

#[entry]
fn main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status
{
  uefi_services::init(&mut system_table).unwrap();
  let bt = system_table.boot_services();
  let mut display = Graphics::new(&bt);

  Status::SUCCESS
}

At this point, all our EFI program does is setup and then immediate exit. There is nothing interesting to see at this point.

Simple Text & Images

To start off, let's draw some text. I wrote a fake bootup message in bios.txt. We'll render it on the screen in the top right corner. To do this, we first need a simple bitmap font. I used FONT_12X16 from the embedded-vintage-fonts crate.

Text::new(
  include_str!("bios.txt"),
  Point::new(8, 24),
  MonoTextStyle::new(&embedded_vintage_fonts::FONT_12X16, Rgb888::WHITE)
).draw(&mut display).unwrap();

Since our drawing target never returns a Err variant, we can safely unwrap without worry of panic. If you are trying this at home, you will want to add a bt.stall(500000) call to the end of your program. This prevents the system from immediately rebooting after drawing to the screen.

Let's put a logo on the screen. We'll use tinybmp as a lightweight bitmap loader. We can bake the logo into our binary using the include_bytes macro. From there, it's a simple matter of calculating a position and sending the buffer over to our drawing target

let bmp_data = include_bytes!("encom.bmp");
let bmp = Bmp::from_slice(bmp_data).unwrap();
let location = Point::new((display.bounding_box().size.width - bmp.bounding_box().size.width - 16) as i32, 16);
Image::new(&bmp, location).draw(&mut display).unwrap();

We've got text, images, and drawing primitives. We're all set to put together a small animation.

Final Product

While we could use a long stream of .draw and .stall calls to animate something frame by frame, this isn't exactly an ergonomic process. Enter asciinema: a tool for recording and playing back terminal interactions. I added a build.rs script that would parse the output of asciicast into rust code for rendering it. I only implemented a small subset of terminal control codes, but now I could record myself interacting with simple CLI programs and play them back within my EFI program. I combined this with the static rendering techniques from the last section to produce a take on one of my favorite scenes from Tron: Legacy.

If you want to see the full code, or download the program for you own ESP, you can check it out here on my GitHub. This was a weekend project, and most of the code does reflect that. I may revisit this someday and maybe implement some simple games as EFI programs.


Tesla Coil: Square Wave MIDI Synthesizer (part 1)

I'm getting a bit ahead of myself with this, but I'm a software guy and I need to write some software. At some point, I want an external interrupter to use the coil as a musical instrument. I need to create a MIDI instrument that produces clean square waves. I'll …

Read More


Tesla Coil: Parameters

First off, secondary coil parameters. I'll be using 3.5" (88.9 mm) diameter PVC gas pipe. Using 509.6 mm of piping and 24 AWG wire, we can fit about 998 turns on the secondary. For the primary coil, I'll be 6 turns of 16 AWG copper wire. The …

Read More


Tesla Coil: Driver Circuit

Driver circuit is based off Zach Armstrong's DRSSTC driver (which itself is based off Loneoceans's SSTC 2). I've redrawn the schematic in KiCAD. I altered the switch to turn off the internal interrupter when the external interrupter is enabled. I reused the GDT footprint off Zach's design. I changed the …

Read More


Tesla Coil: Power Supply Board Design

The power supply for my coil needs to fill two roles. It must generate a 340V DC voltage (max 10A) and a minimum 12V DC voltage (max 1A). I started my design from Plasma Prince's design.

power supply schematic

I initially used a KBU1010 bridge rectifier in my design, however, I was concerned …

Read More