1 Motivation


Figure 1: LPC1114FN28 DIP-28 chip

Bare-metal is often refered to as a microcontroller running specific code without operating system. This convention will be assumed true in this post. Furthermore, I am going to use arm-none-eabi toolchain and LPC1114FN28 based on ARM Cortex-M0. The purpose of this code, is to show how to create a most basic program running on such a hardware with standard open-source tools.

2 Hardware features

An important step is checking datasheet to determin the layout of flash memomry used by the chip of choice LPC1114.

0x40000000 other stuff
...        reserved
0x1FFF4000 16kB boot ROM
...        reserved
0x10001000 4kB SRAM  0x10000000 
...        reserved
0x00006000 32kB on-chip flash
0x000000C0 active interrupt vector

2.1 Interupt vector

The active interrupt vector is the place in memory which has a branching instructions to handle specific interrupts. For the LPC1114, the interupt vector (IV) has 192 (0xC0) interrupts. Any time an interrupt triggering condition occurs, the control will jump to that vector. That means code should be properly aligned if we care not to overlap with that section.

The interrupt vector (IV) holds a series of jump instructions which get triggered when the interrupt triggering event occurs. For this reason IV section should not overlap with the code section. For the LPC1114, we see that 192 addresses are occupied and from the manual that 32 interrupts are used.

Instructions on ARM architecture are generally fixed 32 bit size, so 4 bytes per instruction. The number of interrupts is also 32. We can calculate the number of bytes in memory used to store 32 interrupts, 32 times 4 bytes equals 128 bytes. This corresponds to 0x000000080 (for 128 bytes). The remaining address space of IV is of size 0x000000C0 minus 0x000000080 which is 0x000000040 (64 bytes). The number of instructions that can fit into 64 bytes is 64 bytes by 4 bytes equal 16 instructions.

Therefore, interrupt vector table (IV) can be visualized as follows

...here goes .text...
0x000000C0 16  additional instructions      (64 bytes)
0x00000080 32 instructions for interrupts  (128 bytes)

We will need to load our program code, aligned above this 192 addresses not to overlap with the IV.

3 Linker script

Next step is to craft a simple linker script which will take part in makig an informed decission where to put the stuff in the memory when flashing the binary onto the chip.

Linker script consists of two parts, memory layout and sections. More detailed description over commands available to control linking process can be found in the gnu ld manual.

3.1 Memory layout

The MEMORY command permits defining blocks of memory which may be used by the linker.

flash (rx)  : ORIGIN = 0x00000000, LENGTH = 32k
ram   (rwx) : ORIGIN = 0x10000000, LENGTH = 4k 

In the above linker's script section, we define two regions, for flash and for ram with corresponding starting positions (origin) and their size. In addition permissions for that region are set. The _Reset marks the entry point to the program.

3.2 Sections

The remaining part of the linker script is the SECTIONS block which permits defining the layout of how the binary will be loaded into the memory.


. = ORIGIN(flash);    /* All sections start from the origin of flash */
.text : {             /* Text sectiion holds the program code */
*(.vectors);          /* IV will be placed here */
*(.text);             /* Actuall program code placed here */
} >flash              /* Assign this section to previously defined region flash */

. = ORIGIN(ram);      /* The second section usable by the program starts from the origin of ram */
.data : {             /* Data section holds static variables */
*(.data)              /* Static variables placed here */
} >ram AT >flash      /* Assign this section to ram and load starting from flash */
.bss : {
*(.bss) *(COMMON)     /* Dynamic variables placed here (bss), and uninitialized variables (common) */
} >ram                /* Assign this section to belong to ram, dynamic so nothing to load (no AT) */

. = ALIGN(4);         /* Align to round memory address for the stack */
. = . + 0x1000;       /* Make place for 4kB of stack memory */
stack_top = .;


4 Startup

Next step is to prepare startup script, which needs to acompilish the following tasks:

  • Define 32 interrupt vectors for our device
  • Define Rest name used as the entry point to the program
  • Initialize stack
  • Call entry point and save return address

For that purpose we have to define section .vectors which we have named so in the linker script. Other sections such as .text, .data and .bss are standard, and would be generated by the C compiler. In case if we would chose assembler, and not defined any of remaining sections from ld linker script, they would be excluded from the linking process automatically.

.section INTERRUPT_VECTOR, "x"  /* "x" for executable */
.global _Reset /* Define entry point to the program */
_Reset:        /* The assembler code for the entry from now on */
B Reset_Handler /* Reset */
/* Other interrupts should be defined here, otherwise control may return to random instructions of the code on interrupt event
but for this example we are ignoring this */

LDR sp, =stack_top
BL main
B .
arm-none-eabi-as -mcpu=arm7m -g lpc1114fn28.s -o lpc1114fn28.o # To compile the startup code

5 Program entry and linking

To finalize this example we are going to use simple empty program with a loop

void main(void) {
  while(1) {
    // Nothing here

We can now compile it and link all sections into binary

arm-none-eabi-gcc -c -mcpu=arm7m -g main.c -o main.o
arm-none-eabi-ld -T lpc1114fn28.ld main.o lpc1114fn28.o -o main.elf 
# Linking order is important, first object main.o for listing of the code in debugger

6 Testing in debugger

The following output shows how to upload program into debugger's simulator

arm-none-eabi-gdb main.elf
Reading symbols from main.elf...done.
gdb-peda$ tar sim
Connected to the simulator.
gdb-peda$ load main.elf 
Loading section .text, size 0x20 vma 0x0
Start address 0x0
Transfer rate: 256 bits in <1 sec.
gdb-peda$ list
1 void main(void) {
3 while(1) {
5 }
7 }
gdb-peda$ b main
Breakpoint 1 at 0x8: file main.c, line 5.
gdb-peda$ run
Starting program: /home/mashu/Arm/hello-own-02/main.elf 
Breakpoint 1, main () at main.c:5
5 }