Compile and link a bootloader using SDCC

From SDCC wiki
Revision as of 11:21, 4 December 2012 by Borutr (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Introduction

With a little effort, SDCC can be used to create a bootloader. The example Makefile and crtstart.asm used in this tutorial can be downloaded from github.

Creating an embedded bootloader is always a bit tricky, and the actual method chosen is often heavily dependent on the target platform and the toolchain. For this example we'll assume a make+SDCC style toolchain and a generic 8051 target. We'll also assume that the bulk of the bootloader is written in C.

This document is the result of a discussion on the SDCC User list. Thanks go to everyone who helped out.

Theory

A typical embedded application binary populates an interrupt vector table (IVT) and one or more blocks of application code. The reset vector is then used to jump to the application code on startup. There are many bootloader architectures that can be used in this case, but for this example we'll simply hijack the reset vector to allow the bootloader to run before the application.

Applications that don't use interrupts can be a problem here because they will often overwrite the IVT with the main code (instead of using the reset vector to jump to the code). Creating a bootloader for that type of application isn't trivial and is well beyond the scope of this document.

Our example bootloader consists of a block of code and a reset vector. The reset vector lives at address 0x0000 and SDCC typically links the main code right after the IVT. The application code that will be installed by the bootloader may supply it's own IVT, complete with reset vector, and probably expects to be located immediately after the IVT. The bootloader has two options for dealing with the application's IVT: relocate the application's entire IVT and forward all interrupts from the real table to the relocated table, or install the application's IVT and only relocate the reset vector. The former is the simplest, but the latter is more common for timing-sensitive applications, so we'll assume the latter method here. Given that most application code doesn't expect to live with a bootloader, most targets are FLASH-based (or block-erase-storage-based), and some targets provide a special protected area for the bootloader, the bootloader code is typically located somewhere other than where applications are typically located.

Implementation

Our goal is to relocate the bootloader code, but not the bootloader IVT (which only populates the reset entry). This presents a bit of a problem because SDCC (as of release 2.9.0) doesn't easily support relocating the code without also relocating the IVT. The first tempting option is to build the bootloader using the '--code-loc' command line option, but that relocates everything.

SDCC does generate a linker script that can be modified to move things around, but it doesn't solve the entire problem. Changing the link address of the HOME segment moves everything. Changing only CSEG relocates the code, but leaves some startup code located immediately after the IVT where it's likely to be clobbered by the application code.

The solution is to relocate everything using the HOME segment and then bypass the stock startup code by renaming main() to bootloader() and using a custom crtstart.asm. SDCC only generates an interrupt vector table for the file that contains main(), so by renaming main() we suppress the generated table in favor of our own. Incidentally, you can rename main() to any legal function name; bootloader() was chosen for clarity.

Now we need to generate our own IVT. The easiest place to do this is in crtstart.asm. So we copy the default file supplied with SDCC and add the following at the top of the file:

 	.area VECTOR    (CODE)
	.globl __interrupt_vect
__interrupt_vect:
 	ljmp	__sdcc_gsinit_startup

The only entry in our table is the jump to the stock startup code. Looking at the startup code in the same file, you'll see that it initializes the stack, calls a few things and then appears to run off to nowhere. Typically that code is linked immediately before the initialization code that's supplied in every module that's being linked. Consequently, it doesn't really run off into the weeds; execution falls through to the initialization code.

At the end of the initialization code for the the module containing main(), there's a jump to to a label that then jumps to main(). Modules that don't have main() don't get that auto-generated jump instruction. Because we renamed main(), we'll have to find some way to wedge that code back where it belongs. Fortunately, the linker locates the GSFINAL segment immediately after all other initialization segments. All we have to do is put our magic jump in GSFINAL, like so:

	.area GSFINAL (CODE)
        ljmp	_bootloader

Now our stack will be set up and all of our C globals will be properly initialized before jumping to bootloader(). But we're not quite finished. If you build this now, you'll get errors about missing symbols. The stock startup library, the one we're not using anymore, includes symbols for the stack and an initialization routine. If you need to use that initialization function (for RAM setup, etc) you'll have to make sure it gets linked appropriately. Otherwise simply delete the offending 3 lines of code. The stack symbol can be easily added to the top of crstart.asm:

	.globl __start__stack
;--------------------------------------------------------
; Stack segment in internal ram
;--------------------------------------------------------
	.area	SSEG	(DATA)
__start__stack:
	.ds	1

Of course, that's assuming you really do want your stack in internal RAM. If not, you'll have to make the appropriate changes. The easiest way to do that is to compile a standard dummy application with the settings you want and then copy the relevant bits from main.lst.

If you've done everything correctly, your crtstart.asm should now look like:

	.globl __start__stack
;--------------------------------------------------------
; Stack segment in internal ram
;--------------------------------------------------------
	.area	SSEG	(DATA)
__start__stack:
	.ds	1

 	.area VECTOR    (CODE)
	.globl __interrupt_vect
__interrupt_vect:
 	ljmp	__sdcc_gsinit_startup

	.globl __start__stack

	.area GSINIT0 (CODE)

__sdcc_gsinit_startup::
        mov     sp,#__start__stack - 1

	.area GSFINAL (CODE)
        ljmp	_bootloader

As mentioned above, we've decided to relocate the code by changing the link address of the HOME segment. There are two ways to do this: modify the linker script, or pass a linker option through SDCC. If this is the only linker modification you're making, the easiest option to stick with the default linker script and use the SDCC command line option...

sdcc ${CFLAGS} -Wl-bHOME=${BOOTLOADER_ADDRESS} bootloader.c

where BOOTLOADER_ADDRESS is your bootloader's new home. If you want to make a custom linker script, build a dummy application and copy the script generated by SDCC. Edit the file and then call the linker directly instead of using SDCC (compile the modules individually using SDCC, then link with aslink). Be careful that you copy the linker script to someplace other than your build output directory, otherwise SDCC might accidentally overwrite it.

And that's it. Now you can get back to writing the actual bootloader code. Good luck.

Personal tools
Namespaces

Variants
Actions
Navigation
Toolbox