|
Up: introduction
Tutorials Toolchain LPC2103 flashing • “Hello, world” GPIO and leds 7-segment indicator PWM and RGB-led Links LPC2103 user manual |
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. IntroductionBesides 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. StartupIn 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 portOur 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 programSo 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 scriptSo 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. MakefileMakefile 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 runningCopy 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: interworkingThumb: introductionARM7TDMI-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 attemptBefore 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 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 wayTo 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 switchingTo 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 modesFor 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 codeIn 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 startupIt 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
| ||||||||||||||||||||
...
...
...
