Using QEMU for Embedded Systems Development, Part 2
By Manoj Kumar on July 1, 2011 in Coding, Developers · 3 Comments
QEMU for embedded programming
In the previous articles, we learnt how to use QEMU for a generic Linux OS installation, for networking using OpenVPN and TAP/TUN, for cross-compilation of the Linux kernel for ARM, to boot the kernel from QEMU, and how to build a small filesystem and then mount it on the vanilla kernel. Now we will step out further.
First of all, I would like to explain the need for a bootloader. The bootloader is code that is used to load the kernel into RAM, and then specify which partition will be mounted as the root filesystem. The bootloader resides in the MBR (Master Boot Record). In general-purpose computing machines, an important component is the BIOS (Basic Input Output System). The BIOS contains the low-level drivers for devices like the keyboard, mouse, display, etc. It initiates the bootloader, which then loads the kernel. Linux users are very familiar with boot-loaders like GRUB (Grand Unified Boot-Loader) and LILO (Linux Loader).
Micro-controller programmers are very familiar with the term “Bare-Metal Programming”. It means that there is nothing between your program and the processor — the code you write runs directly on the processor. It becomes the programmer’s responsibility to check each and every possible condition that can corrupt the system.
Now, let us build a small program for the ARM Versatile Platform Baseboard, which will run on the QEMU emulator, and then print a message on the serial console. Downloaded the tool-chain for ARM EABI from here. As described in the previous article, add this tool-chain in your PATH.
By default, QEMU redirects the serial console output to the terminal, when it is initialised with the nographic option:
$ qemu-system-arm --help | grep nographic
-nographic disable graphical output and redirect serial I/Os to console. When using -nographic, press 'ctrl-a h' to get some help.
We can make good use of this feature; let’s write some data to the serial port, and it can be a good working example.
Before going further, we must make sure which processor the GNU EABI tool-chain supports, and which processor QEMU can emulate. There should be a similar processor supported by both the tool-chain and the emulator. Let’s check first in QEMU. In the earlier articles, we compiled the QEMU source code, so use that source code to get the list of the supported ARM processors:
$ cd (your-path)/qemu/qemu-0.14.0/hw
$ grep "arm" versatilepb.c
#include "arm-misc.h"
static struct arm_boot_info versatile_binfo;
cpu_model = "arm926";
It’s very clear that the “arm926″ is supported by QEMU. Let’s check its availability in the GNU ARM tool-chain:
$ cd (your-path)/CodeSourcery/Sourcery_G++_Lite/share/doc/arm-arm-none-eabi/info
$ cat gcc.info | grep arm | head -n 20
.
.
`strongarm1110', `arm8', `arm810', `arm9', `arm9e', `arm920',
`arm920t', `arm922t', `arm946e-s', `arm966e-s', `arm968e-s',
`arm926ej-s', `arm940t', `arm9tdmi', `arm10tdmi', `arm1020t',
`arm1026ej-s', `arm10e', `arm1020e', `arm1022e', `arm1136j-s',
Great!! The ARM926EJ-S processor is supported by the GNU ARM tool-chain. Now, let’s write some data to the serial port of this processor. As we are not using any header file that describes the address of UART0, we must find it manually, from the file (your-path)/qemu/qemu-0.14.0/hw/versatilepb.c:
/* 0x101f0000 Smart card 0. */
/* 0x101f1000 UART0. */
/* 0x101f2000 UART1. */
/* 0x101f3000 UART2. */
Open source code is so powerful, it gives you each and every detail. UART0 is present at address 0x101f1000. For testing purposes, we can write data directly to this address, and check output on the terminal.
Our first test program is a bare-metal program running directly on the processor, without the help of a bootloader. We have to create three important files. First of all, let us develop a small application program (init.c):
volatile unsigned char * const UART0_PTR = (unsigned char *)0x0101f1000;
void display(const char *string){
while(*string != ' '){
*UART0_PTR = *string;
string++;
}
}
int my_init(){
display("Hello Open World ");
}
Let’s run through this code snippet.
First, we declared a volatile variable pointer, and assigned the address of the serial port (UART0). The function my_init(), is the main routine. It merely calls the function display(), which writes a string to the UART0.
Engineers familiar with base-level micro-controller programming will find this very easy. If you are not experienced in embedded systems programming, then you can stick to the basics of digital electronics. The microprocessor is an integrated chip, with input/output lines, different ports, etc. The ARM926EJ-S has four serial ports (information obtained from its data-sheet); and they have their data lines (the address). When the processor is programmed to write data to one of the serial ports, it writes data to these lines. That’s what this program does.
The next step is to develop the startup code for the processor. When a processor is powered on, it jumps to a specified location, reads code from that location, and executes it. Even in the case of a reset (like on a desktop machine), the processor jumps to a predefined location. Here’s the startup code, startup.s:
.global _Start
_Start:
LDR sp, = sp_top
BL my_init
B .
In the first line, _Start is declared as global. The next line is the beginning of _Start‘s code. We set the address of the stack to sp_top. (The instruction LDR will move the data value of sp_top in the stack pointer (sp). The instruction BL will instruct the processor to jump to my_init (previously defined in init.c). Then the processor will step into an infinite loop with the instruction B ., which is like a while(1) or for(;;) loop. If we don’t do this, our system will crash. The basics of embedded systems programming is that our code should run into an infinite loop.
Now, the final task is to write a linker script for these two files (linker.ld):
ENTRY(_Start)
SECTIONS
{
. = 0x10000;
startup : { startup.o(.text)}
.data : {*(.data)}
.bss : {*(.bss)}
. = . + 0x500;
sp_top = .;
}
The first line tells the linker that the entry point is _Start (defined in startup.s). As this is a basic program, we can ignore the Interrupts section. The QEMU emulator, when executed with the -kernel option, starts execution from the address 0x10000, so we must place our code at this address. That’s what we have done in Line 4. The section “SECTIONS”, defines the different sections of a program.
In this, startup.o forms the text (code) part. Then comes the subsequent data and the bss part. The final step is to define the address of the stack pointer. The stack usually grows downward, so it’s better to give it a safe address. We have a very small code snippet, and can place the stack at 0x500 ahead of the current position. The variable sp_top will store the address for the stack.
We are now done with the coding part. Let’s compile and link these files. Assemble the startup.s file with:
$ arm-none-eabi-as -mcpu=arm926ej-s startup.s -o startup.o
Compile init.c:
$ arm-none-eabi-gcc -c -mcpu=arm926ej-s init.c -o init.o
Link the object files into an ELF file:
$ arm-none-eabi-ld -T linker.ld init.o startup.o -o output.elf
Finally, create a binary file from the ELF file:
$ arm-none-eabi-objcopy -O binary output.elf output.bin
The above instructions are easy to understand. All the tools used are part of the ARM tool-chain. Check their help/man pages for details.
After all these steps, finally we will run our program on the QEMU emulator:
$ qemu-system-arm -M versatilepb -nographic -kernel output.bin
The above command has been explained in previous articles (1, 2), so we won’t go into the details. The binary file is executed on QEMU and will write the message “Hello Open World” to UART0 of the ARM926EJ-S, which QEMU redirects as output in the terminal.
Acknowledgement
This article is inspired by the following blog post: “Hello world for bare metal ARM using QEMU“.
相关热门文章
给主人留下些什么吧!~~
评论热议