Porting UEFI to BeagleBoneBlack: Technical Details I

I’m adding a BeagleBoneBlack port to the Tianocore/EDK2 UEFI implementation. This post details the implementation specifics of the port so far.

About the hardware:
BeagleBoneBlack is a low-cost embedded board that boasts an ARM AM335x SoC. It supports Linux, with Android, Ubuntu and Ångström ports already available. It comes pre-loaded with the MLO and U-Boot images on its eMMC which can be flashed with custom binaries. Bootup can also be done from a partitioned sdcard or by transferring binaries over UART (ymodem) or USB (TFTP). The boot flow is presented here:

The Tianocore Project / Build System

The EDK2 framework provides an implementation of the UEFI specifications. It’s got its own customizable pythonic build system that works based on the config details provided through build meta-files. The build setup is described in this document.

(TL;DR: the build tool parses INF, DSC and DEC files for each package that describe its dependencies, exports and the back-end library implementations it shall use. This makes EDK2 highly modular to support all kinds of hardware platforms. It generates Firmware Volume images for each section in the Flash Description File, which are put into a Flash Description binary with addressing as specified in the FDF. The DSC specifies which library in code should point to which implementation, and the INF keeps a record of a module’s exports and imports. If these don’t match, the build simply fails.)

Implementation

I started out with an attempt to write a bare-metal binary that prints over some letters to UART to get a hang of how low-level development works. Here‘s a great guide to the basics for bare-metal on ARM. All the required hardware has to be initialized in the binary before use, and running C requires an execution environment set up that provides stacks and handles placement of segments in memory. Since U-Boot already handles that in its SPL phase, I wrote a standalone that could be called by U-Boot instead.

The BeagleBoneBlackPkg is derived from the ArmPlatformPkg. I began with echoing the “second stage” steps mentioned here – implement the libraries available and perform platform specific tasks – as I intended to take over boot from U-Boot/MLO.  This also eased me from having to do the IRQ and memory initializations.

I’m using the PrePeiCore (and not Sec) module’s entry point to load the image. It builds the PPIs necessary to pass control over to PEI and calls PeiMain.

Running the FD:  The build generates an .Fd image that will be used to boot the device. The MLO binary I’m using is built to look for and launch a file named ‘u-boot.img’ on the MMC (there’s a CONFIG_ macro to change this somewhere in u-boot), so I just rename the FD to u-boot.img before flashing it.


A U-Boot Independent Standalone Application

U-Boot allows you to load your own applications at the console. The application already has the hardware interfaces available for use (u-boot does it), and everything does not need to be brought up from scratch.

It comes with a sample hello_world program at u-boot/examples/standalone/hello_world.c, which is supposed to print stuff to console. It depends on U-Boot interfaces, but by tracing back the source code, it can be easily re-written to have nothing to do with the U-Boot API.

In the end, hello_world.c:printf()’s job is to write the characters to UART’s address. Implementing this on a BeagleBone Black is pretty easy:

The ARM AM335x TRM mentions the address-offsets of all registers available with the processor. The UART0_BASE is defined at 0x44E09000. Memory mapped registers need to be kept volatile to prevent compiler optimizing them away.

Here’s the code:


/* a simple independent U-Boot loadable application */
/* use U-Boot's example makefile to compile */
#define UART0_BASE 0x44E09000
#define REG(x) (*((volatile unsigned int *) x))
void main(){
int c = 'x';
while(1){
REG(UART0_BASE + 0) = c;
}
}

Compile it similarly to U-Boot’s examples to get a .bin.

To execute it, you can either: 1) copy the SREC over serial, 2) set up TFTP or 3) put it on an sdcard

The load_address below is an env var that specifies where the application will be loaded. It can be changed when building U-Boot.
For the current build, find it from the console using

U-Boot# printenv

U-Boot# fatload mmc 0 <load_address>
[...]

The entry point’s address of the application can be found by looking at the objdump. See this if you have larger applications.
Launching it,

U-Boot# go <entry_point_address>


If you plan to write a purely standalone binary, you are required to initialize the hardware manually and provide a functioning C execution environment. It also requires information regarding placement and relocation of the text and data segments, allocation and zeroing of BSS segment and placement of the stack space. See this and this.


Transferring a .bin from openSUSE to U-Boot, or, Rightly Configuring TFTP on openSUSE

After spending a *lot* of time figuring out why I could not transfer a standalone binary to u-boot running on my BeagleBone Black, I finally discovered it was a firewall issue. This post is to save anyone in the future from suffering the same nightmare as I just went through on openSUSE 13.1.

The Problem:

I needed to put a .bin on my BBB which has U-Boot. The available options are:

Transferring the S-Record

SREC is the hex encoding of binary data generated on compilation. To load this, U-Boot provides the `loads` command at its console. You just need to pass the ASCII-Hex data from the .srec to the serial console (see this). The problem is, the speed of sending this data must be okay with the U-Boot console. Gave me a `data abort` message and my board reset.

Using TFTP

Better option: tftp. Have static IP setup for the host and the board (set env vars ipaddr and serverip on u-boot) and call tftp. It gave me this:

U-Boot# tftp
link up on port 0, speed 100, full duplex
Using cpsw device
TFTP from server 192.168.10.1; our IP address is 192.168.10.2
Filename 'hello_world.bin'.
Load address: 0x80200000
Loading: T T T T T T T T T T T T T T T 
Abort

(**T = Timeout.**)

Fix:

TFTP uses UDP port 69 for transfers. I needed to explicitly check “Open port in firewall” from the TFTP server config from YaST and add port 69 to Firewall->Allowed Services->UDP Ports.