LPC2103 tutorials: “Hello, world”

[Mail:Me:]

In this tutorial you will create the simplest program for LPC2103 board. The only thing it is supposed to do is to output some string through the serial port. You will need working toolchain to compile the code and lpc21isp tool to upload the code to the board.

0. Introduction

Besides the C code of your program you will need three important things. The first one is a startup code — assembler code which does low-level hardware initialization and sets up such things as stack. The second one is some library to output something to serial port. Of course, you can just put the corresponding code in your main program but you will need this functionality in the future so having it as a library is useful. The third thing is a linker script which will tell to the linker where to put which sections of your complied code.

LPC2103 has only 8 KiB of SRAM but our program is going to be very small so it can fit into RAM. We start with ARM code and the modify it to produce thumb code for higher code density.

1. “Hello ARM in RAM”

If you do not know how GNU Make works, go read about it somewhere else. You will need to understand at least the basics to go through this tutorial.

Startup

In our first very basic version of the program we do not care about setting up the hardware, exceptions vectors, stack etc. So the only function of assembler startup file is to run our C program.

You can download startup file here: ramstartup-1.S.

Here is how it looks:

     1	.text
     2	.global	_startup
     3	.func	_startup
     4	_startup:
     5		mov	r0, #0
     6		mov	r1, #0
     7		ldr	lr, =__back
     8		b	main
     9	__back:
    10		b	__back
    11	.endfunc
    12	.end

Line 1 means that the following code goes to .text section which usually contains the code and sometimes read-only data. Line 2 makes _startup symbol visible to the linker; it is an entry point for our program. Lines 3 and 11 denotes function called _startup; unless you compile this file with debugging enabled they do nothing. Line 4 is a label.

Now the real part comes. The registers r0 and r1 contain two arguments of the function we are going to call: int main(int argc, char *argv[]). As we do not want to pass any arguments, let them me zeros (lines 5 and 6). Line 7 loads the address of __back to the link register which is used to return from function call. So when main terminates, the control goes to __back (and stays there in infinite loop, see line 10). Line 8 jumps to main (which will be defined in our C program).

Serial port

Our simple program is going to send a string through UART0. For this purpose you need to know about only two registers: U0SLR and U0THR.

UART0 has 16 byte transmit FIFO. U0THR (transmitter holding register) is the write-only register which represents the top byte of transmit FIFO. You can access it at address 0xE000C000. So when you want to send a character through UART0, you write it to U0THR.

Before you write anything to U0THR, you have to be sure that there is a place in FIFO. Here U0LSR (line status register) comes to help. Bit 5 of this read-only register is set if and only if U0THR is empty.

C program

So here is “Hello ARM in RAM” C program; download it: hello-1.c.

     1	#define U0THR (*((volatile unsigned char *) 0xE000C000)) /* UART0 transmitter holding register */
     2	#define U0LSR (*((volatile unsigned char *) 0xE000C014)) /* UART0 line status register */
     3	#define U0THRE ((U0LSR & (1<<5))) /* UART0 transmitter holding register is empty */

     4	void putch(char c) {
     5		while (!U0THRE);
     6		U0THR = c;
     7	}

     8	void putstr(char *s) {
     9		while (*s) putch(*s++);
    10	}

    11	int main(int  argc, char *argv[]) {

    12		putstr("Hello ARM in RAM\n");

    13		return 0;
    14	}

As you see, we define here two simple functions: void putch(char) and void putstr(char *). The second one is merely using the first one to send null-terminated string through UART0 (line 9). The first one waits until UART0 transmitter holding register becomes empty (lines 3 and 5) and then writes a character to it (line 6).

Linker script

So far we have assembler startup file and C program. To get the final binary we need also linker script which explains to the linker where in memory to put what. Here it comes (download lpc2103_ram.ld):

     1	ENTRY(_startup)

     2	MEMORY
     3	{
     4		RAM (rw) : ORIGIN = 0x40000200, LENGTH = (0x00002000 - 0x00000200 - 0x20 - 0x100)
     5	}

     6	SECTIONS
     7	{
     8		.text :
     9		{
    10			_text = .;
    11			*startup.o (.text)
    12			*(.text)
    13			*(.glue_7)
    14			*(.glue_7t)
    15		} > RAM

    16		. = ALIGN(4);
    17		_etext = .;

    18		.rodata :
    19		{
    20			_rodata = . ;
    21			*(.rodata)
    22		} > RAM

    23		. = ALIGN(4);
    24		_erodata = .;

    25		.data :
    26		{
    27			_data = . ;
    28			*(.data)
    29		} > RAM

    30		. = ALIGN(4);
    31		_edata = .;

    32		.bss :
    33		{
    34			__bss_start = .;
    35			*(.bss)
    36		} > RAM

    37		. = ALIGN(4);
    38		__bss_end = . ;
    39	}
    40	_end = .;

This script defines _startup as an entry point (line 1). Then it defines “RAM” as read-write region of memory (line 4). The beginning of RAM (starting from 0x40000000) is used for exceptions vectors (more on this in the following tutorials); then the region from 0x40000040 to 0x4000011F is used by RealMonitor (our board does not have JTAG connection, so no use in RealMonitor); then the area from 0x40000120 to 0x400001FF is used by ISP command handler in bootloader. So the really useful memory starts at 0x40000200. The top 32 bytes are used by flash programming commands in bootloader, and then at top RAM - 32 starts the stack used by bootloader. The maximum stack usage is 256 bytes. Given this considerations, the available amount of RAM is (total amount, 0x2000) - (non-useful area in the beginning, 0x200) - (top 32 bytes used by flash programming commands, 0x20) - (another 256 bytes for bootloader stack, 0x100).

Line 6 starts the description of placement of different sections into memory. The first sections is .text (line 8); we put in the beginning .text section of startup (I do not know if it is really needed), then .text sections of any other files, and then sections .glue_7 and .glue_7t which are used for thumb interworking code by GNU C. We align the end of .text section (line 16) and put symbols _text and _etext in the beginning and end of the section, just for convenience (lines 10 and 17).

Then we put sections .rodata (for read-only data, lines 18-24) and .data (for read-write data, lines 25-31). Then comes section .bss for zero-initialized statically allocated variables.

Makefile

Makefile is quite self-explanatory:

     1	NAME=hello

     2	STARTUP=ramstartup-1.S
     3	CSRC=hello-1.c

     4	PORT=/dev/ttyUSB0

     5	LDSCRIPT=lpc2103_ram.ld
     6	CC=arm-elf-gcc
     7	OBJCOPY=arm-elf-objcopy
     8	FLASHER=lpc21isp_148x

     9	SPEED=115200
    10	OSC=14746

    11	all: $(NAME)

    12	$(NAME): code.o startup.o
    13		$(CC) -nostdlib -nostartfiles -T $(LDSCRIPT) -o $(NAME).elf code.o startup.o

    14	startup.o: $(STARTUP)
    15		$(CC) -c -o startup.o $(STARTUP)

    16	code.o: $(CSRC)
    17		$(CC) -c -o code.o $(CSRC)

    18	run: $(NAME).hex
    19		$(FLASHER) -hex -term -control $(NAME).hex $(PORT) $(SPEED) $(OSC)

    20	$(NAME).hex: $(NAME).elf
    21		$(OBJCOPY) -O ihex $(NAME).elf $(NAME).hex

    22	clean:
    23		rm -rf code.o startup.o $(NAME).hex $(NAME).elf

If you want target make run to work, make sure that the port in line 4 is correct. Also notice lines 20 and 21, where linked binary is converted to the ihex format which is understood by bootloader.

Compiling and running

Copy all the files in one directory. Execute make:

$ make
arm-elf-gcc -c -o code.o hello-1.c
arm-elf-gcc -c -o startup.o ramstartup-1.S
arm-elf-gcc -nostdlib -nostartfiles -T lpc2103_ram.ld -o hello.elf code.o startup.o

As you see, C program and assemble startup files are compiled and then linked into hello.elf binary. Now make sure your board is connected, PORT definition in Makefile is right and run make run.

 make run
lpc21isp_148x -hex -term -control hello.hex /dev/ttyUSB0 115200 14746
lpc21isp version 1.48
File hello.hex:
	loaded...
Start Address = 0x40000200
	converted to binary format...
	image size : 760
ioctl get ok, status = 6
ioctl set ok, status = 6
ioctl get ok, status = 6
ioctl get ok, status = 6
ioctl set ok, status = 4
ioctl get ok, status = 4
ioctl get ok, status = 4
ioctl set ok, status = 0
ioctl get ok, status = 0
Synchronizing (ESC to abort). OK
Read bootcode version: 2
2
Read part ID: LPC2103, 32 kiB ROM / 8 kiB SRAM (327441)
Will start programming at Sector 1 if possible, and conclude with Sector 0 to ensure that checksum is written last.
Sector 0: .....................
Download Finished and Verified correct... taking 1 seconds
Now launching the brand new code
Terminal started (press Escape to abort)

G 1073742336 A
0
Hello ARM in RAM

This command will convert hello.elf binary to ihex format understood by bootloader, download it to the board, run it and start terminal (see -term option for lpc21isp). The cryptic message you see (G 1073742336 A) is the command to the bootloader to run (Go) the code in arm mode (ARM) at address 1073742336 (or 0x40000200 — exactly where we asked the linker to put .text section). 0 is the return code, i.e. success.

Congratulations! You've run the first program on the board; now the program is infinite loop (see lines 9 and 10 of ramstartup-1.S). Press ESC to leave terminal.

2. Thumb version: interworking

Thumb: introduction

ARM7TDMI-S core of LPC2103 supports so called thumb instruction set. Normal ARM instruction set uses 32-bit instructions; thumb uses 16-bit instructions. So using thumb one can achive higher code density. Of course, thumb instruction set is reduced comparing to the normal ARM instruction set; so the gain is not 50% but it is normal for thumb code to be 25-30% smaller then ARM code. Of course, usage of thumb code also leads to some performance penalty.

ARM and thumb cannot be used together; the processor should swith modes. Below you will see how it can be done.

Thumb: first attempt

Before we proceed, check the size of the binary from the previous example:

 arm-elf-size hello.elf 
   text	   data	    bss	    dec	    hex	filename
    248	      0	      0	    248	     f8	hello.elf

Now modify Makefile adding -mthumb option to the line $(CC) -c -o code.o $(CSRC), so it will look like

...
code.o: $(CSRC)
        $(CC) -c -mthumb -o code.o $(CSRC)
...

This option tells GNU C to compile the code as thumb. Now run make clean and then make:

$ make clean
rm -rf code.o startup.o hello.hex hello.elf
$ make
arm-elf-gcc -c -mthumb -o code.o hello-1.c
arm-elf-gcc -c -o startup.o ramstartup-1.S
arm-elf-gcc -nostdlib -nostartfiles -T lpc2103_ram.ld -o hello.elf code.o startup.o
/opt/LPC2103/lib/gcc/arm-elf/4.3.2/../../../../arm-elf/bin/ld: code.o(main): warning: interworking not enabled.
  first occurrence: startup.o: arm call to thumb

As you see, GNU C complains about calling thumb code (now hello-1.c is compiled as thumb) from ARM code (ramstartup-1.S). Nevertheless, you may check that hello-1.c is compiled as C code:

$ arm-elf-objdump -S code.o

code.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 :
   0:	b580      	push	{r7, lr}
   2:	b081      	sub	sp, #4
   4:	af00      	add	r7, sp, #0

...

The instructions are 16-bit and the size of the final binary significantly decreased:

 arm-elf-size hello.elf
   text	   data	    bss	    dec	    hex	filename
    184	      0	      0	    184	     b8	hello.elf

Unfortunately, because in this binary we are mixing ARM and thumb code, it will not work properly. It will work somehow, because GNU C took some care about our arm call to thumb, but instead of going to infinite loop after in the end, the board will most probably reboot.

The right way

To fix this problem, we need to enable so called thumb interworking, allowing GNU C to build into our program a framework for switching between processor modes when appropriate. To do this, add to both compilation commands in Makefile option -mthumb-interwork, so the corresponding places will look like:

...
startup.o: $(STARTUP)
        $(CC) -c -mthumb-interwork -o startup.o $(STARTUP)

code.o: $(CSRC)
        $(CC) -c -mthumb -mthumb-interwork -o code.o $(CSRC)
...

Run make clean, make and make run. Everything works! The size of resulting binary is much less than the size of the original binary but slightly more than previous broken binary (because thumb interworking framework is build in):

$ arm-elf-size hello.elf 
   text	   data	    bss	    dec	    hex	filename
    196	      0	      0	    196	     c4	hello.elf

3. Thumb: manual mode switching

To have thumb interworking built in is a good idea if your program often calls thumb code from arm code and vice versa. But our case is much simpler, and it is possible to reduce the program size even more by switching off thumb interworking and providing needed mode switch manually.

Switching between ARM and thumb modes

For switching between ARM and thumb modes ARM processors use specific instruction, called “branch and exchange”. The syntax is bx rn, where rn is one of the general purpose registers. After this instruction, the execution goes to an address stored in rn. But because all ARM and thumb instruction should be aligned, no instruction may be situated at an odd address. So the least significant bit of rn is not needed to specify an address.

This bit allows to specify the mode for the processor: if the least significant bit of rn is 0, bx rn simply jumps to an address stored in rn and switches mode to ARM. If the least significant bit is 1, mode is switched to thumb and jump is done to an address, stored in rn, after replacing the least significant bit with 0.

Calling thumb main() from ARM code

In ramstartup-1.S file we jump to C program with b main. Let's change this jump to branch and exchange instruction:

...
	ldr	lr, =__back
	ldr	r2, =main
	bx	r2
__back:
...

You do need to set up the least significant bit of r2 to 0; it will be done by linker (the value of main will be odd).

Run make clean, make (no warnings!), make run... and the board reboots after printing “Hello ARM in RAM”. What's wrong?

Thumb code in startup

It is easy to guess what happened. After main function (in thumb mode) finishes, it returns to startup file, to the label __back. And at that address we have infinite loop coded... in ARM mode.

The easiest solution to our problem is to have infinite loop in thumb mode. Put assembler directive .thumb on the line just before __back label (for the sake of completeness, you may want to put .arm directive on the line before _startup as well). Now run make, make run — everything works. (You can download files for this example: ramstartup-2.S, hello-1.c, Makefile, lpc2103_ram.ld.)

And what about the size of the binary now?

$ size hello.elf
   text	   data	    bss	    dec	    hex	filename
    180	      0	      0	    180	     b4	hello.elf

As expected, it is less than in the case of thumb interworking enabled.

Search this site

...
...
...

Creative Commons License
This work by Alexey Vyskubov is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.