Embedded Systems Programming Course

Programming Languages

Introduction

Just as with operating systems, everybody seems to have a favorite programming language. No matter which programming language you prefer, generally you can find an embedded operating system somewhere that supports your language of choice. Generally, however most embedded system programming is done in one of two languages, assembler or C. I highly recommend that you develop at least a basic understanding of both languages if you truly wish to become an embedded systems programmer.


Assembly Language

When the manufacturers create a new processor, then will document the instructions that the device understands as a series of opcodes. The number of opcodes a processor support varies widely, but generally this falls into two main groups: reduced instruction set computers (RISC) and complex instruction set computers (CISC). No matter which type the processor is, working with opcodes themselves is very difficult. Opcodes are generally one or two bytes, which are instructions to the internal microcode contained inside the processor.

These opcodes perform very specific and very basic operations such as copying a byte of data from RAM to a CPU register, performing basic mathematical operations like adding, subtracting and comparing numbers, and other operations. A CISC-based system may have a 1,000 or more opcodes. Often many of these opcodes perform very similar actions which makes the microcode inside the processor more complex, but simplifies a programmer's job. A RISC-based system will eliminate the redundant opcodes down to a bare minimum. Some RISC-based systems only have a dozen or so opcodes that the processor can handle. More complex operations must be built up by combining the opcodes into groups, or functions. In this way, designers of the processor can really optimize the limited set of instructions, creating very small, very fast microcode inside the processor.

Since it is so difficult to remember all the various opcodes and exact what each means, the Assembly language was invented that can convert English-like commands into opcodes. The language uses a command set that mimics the opcodes supported by the processor as much as possible. Then when the program is compiled, or assembled, the assembly language commands are converted, almost one to one, into the correct opcodes that the processor can execute. This means the programmer can be more productive, since mnemonics are easier to remember than opcodes, while at the same time the resulting code is very small and compact. The assembler almost never inserts extra opcodes above and beyond what the programmer has entered. Please note that some advanced assemblers support the concept of macros, or other preprocessing steps, that may expand a single statement into many opcodes.

Below you will find a short sample assembly language program designed to run on the Rabbit system. Let's examine it to see how it works.

;************************************************************************
;
;   File: first.asm
;
;   A modified version of first.asm from the z2kasm project
;
;   Modified by Randy L. Pearson, 2001
;
;   Original Copyright notice:
;
;   Copyright (C) 2000, Stephen J. Hardy
;
;   tekno@users.sourceforge.net
;   16 Powers Place, Latham, A.C.T. 2615, Australia
;
;   This program is free software; you can redistribute it and/or modify
;   it under the terms of the GNU General Public License as published by
;   the Free Software Foundation; either version 2 of the License, or
;   (at your option) any later version.
;
;   This program is distributed in the hope that it will be useful,
;   but WITHOUT ANY WARRANTY; without even the implied warranty of
;   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;   GNU General Public License for more details.
;
;   You should have received a copy of the GNU General Public License
;   along with this program; if not, write to the Free Software
;   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
;
;
;************************************************************************
;Test program to make sure Core Rabbit Development board is
;connected and talking to the host.

io      sect    r

GCSR    equ     0x00
GCDR    equ     0x0F
MB0CR   equ     0x14
SPCR    equ     0x24
PADR    equ     0x30

// Cold boot instructions...

    setioi  io.MB0CR,0x05        ; MB0CR = 5 to select RAM
    setioi  0x09,0x51            ; Prime watchdog
    setioi  0x09,0x54            ; Disable watchdog
    setioi  io.GCDR,4            ; Clock doubler
    setioi  io.GCSR,0x08         ; Processor and peripheral clocks undivided

;*************************************************
;* Removed this code for Rabbit Core Module - RLP
;*************************************************
;   setioi  io.PADR,255-1        ; Turn on LED 0
;   setioi  io.SPCR,0x04         ; Port A output

    setmem  _boot                ; Load secondary boot
    setioi  io.SPCR,0x80         ; Start from address 0

// Secondary boot.  Placed at zero by loader, and then copies
// most of itself to 0xD000 when it starts executing.

_boot  sect  r,place=0,base=0xD000
    import  io

    ld      bc,#_boot-(start-_boot)	// Section length minus
                                    // initial copying code
    ld      de,start                // Destination virtual address
    ld      hl,start-_boot          // Source address (placed near 0)
    ldir                            // Store in proper base location
    jp      start                   // Start executing at proper addr.

// By this point, code is in proper location.  Flash the LEDs to
// show that we're alive.

;***************************************************
;* Modified to alternately toggle LED 1 and 2 - RLP
;***************************************************

start
    ld      a,0x84
    ioi ld  (SPCR),a                // Make Port A usable for output

    ld      a,1                     // LED 1 on, 2 off
    ioi ld  (PADR),a
    call    delay
    ld      a,2                     // LED 1 off, 2 on
    ioi ld  (PADR),a
    call    delay
    jr      start                   // Repeat

delay
    ld      b,2
loop
    bool    hl
    rr      hl                      // Zero HL
loop2
    dec     hl
    ld      a,l
    or      h
    jr      nz,loop2
    djnz    loop
    ret

As you can probably see, comments are entered in this particular assembly language by using either a semi-colon (;) or two back slashes (//). The semi-colon comments are the traditional assembly comments, while the back slash comment style was borrowed from the C language.

The first important lines to the assembler are these lines:

io      sect    r

GCSR    equ     0x00
GCDR    equ     0x0F
MB0CR   equ     0x14
SPCR    equ     0x24
PADR    equ     0x30

This tells the assembler to begin a new section (block) of memory and to create variables named GCSR, GCDR, MB0CR, SPCR and PADR under a block of memory named 'io'. This is a feature of this particular assembler used to create complex data types similar to other languages arrays or structures. The 'equ' statements store the value to the right in the variable at the left. In this case, all the items are 16-bit hexadecimal values, denoted by the 0x prefixes, which is another feature borrowed from the C language. The numbers could have also been defined as hexadecimal using assembly language syntax like this:

io      sect    r

GCSR    equ     00h
GCDR    equ     0Fh
MB0CR   equ     14h
SPCR    equ     24h
PADR    equ     30h

The next section is the cold boot loading instructions that are needed in order to download the program to the Rabbit board in preparation for running the code.

    setioi  io.MB0CR,0x05        ; MB0CR = 5 to select RAM
    setioi  0x09,0x51            ; Prime watchdog
    setioi  0x09,0x54            ; Disable watchdog
    setioi  io.GCDR,4            ; Clock doubler
    setioi  io.GCSR,0x08         ; Processor and peripheral clocks undivided
    setmem  _boot                ; Load secondary boot
    setioi  io.SPCR,0x80         ; Start from address 0

The net effect of these lines are to ask the Rabbit processor to use RAM to run the program, disables the watchdog timer, doubles the processor clock speed, programs parallel port A for output and finally begins running the code found in the _boot sections of memory. The 'setioi' instructions cause the processor to load a value into a data port at the indicated port addresses. So, the statement 'setioi io.MB0CR,0x05' causes the processor to store the value 0x05 into data I/O port 0x14 or 14h.

Port 14h is a control port inside the Rabbit processor that selects which RAM or Flash ROM chip it should use. The value 0x05 enables the CS2, OE1 and WE1 memory select lines, which cause the RAM chip on the prototype board to be used. In the case of the prototype board, this is the only chip available. A maximum of four (4) RAM or ROM chips can be directly connected to the processor and select in a similar fashion if needed. It is even possible to use some of the parallel port bits as chip select lines if you need even more RAM or ROM than the processor can natively handle.

The 'setioi 0x09,0x51' and 'setioi 0x09,0x54' statements write values to the processor's Watchdog Timer Test Register, which is port 0x09. Sending a value of 0x54 disables the watchdog timer, but you must first write a value of 0x51, 0x52 or 0x53 before you can write the 0x54 value. This is documented in Rabbit's help system and on the easy reference card.

A watchdog timer is normally used in critical application to ensure that the program continues to run. In essence, the CPU repeatedly decrements the watchdog register and if it ever reaches the value 0, the CPU resets itself. The idea is that while the program is running correctly, it should update the watchdog timer value periodically. If that fails to happen, then the processor can assume the program is stuck in an endless loop, or some other error has occurred and needs to be reset to resume normal operations. This is a commonly used technique in embedded systems since the user won't be able to press Ctrl-Alt-Delete when there is no keyboard.

The 'setioi io.GCDR,4' instruction programs the Rabbit processor to operate with a 20ns period. Basically this controls the speed at which the Rabbit is running. The value written changes the way the processor handles the crystal frequencies attached to its main clock pins. The Rabbit has the ability to operate twice as fast internally as the external frequency. The value of 4 programs the Rabbit to assume an external crystal clock frequency of 25 MHz, which makes it then operate internally at a speed of 50 MHz. This value should match whatever clock is actually in use. A value of 0 will disable the processors clock doubling circuitry completely.

The 'setioi io.GCSR,0x08' instruction programs the Rabbit processor to drive the processor and peripheral clock line at the same speed as the main crystal oscillator. This port is used to make the processor slow itself down for power savings mode. A value of 0x14 can be used for to slow the processor down to run from a secondary 32KHz clock, which allows the processor to continue to run, but at a very slow rate. In this mode, the processor can execute about 2000-3000 instructions per second, but only uses 100 ?A of current.

The 'setmem _boot' instruction is used to tell the processor that the code it should be running is the code located in the '_boot' section, which follows. (NOTE: This is not a documented opcode, but instead a special macro used by the Z2K assembler in which this example was written. This command and the next cause the boot loader code built into the processor to begin executing code from Flash ROM, instead of the internal boot loader.) The final instruction in this section exits bootstrap mode and enters normal run mode, which forces the processor to begin running the code found in the '_boot' section.

_boot  sect  r,place=0,base=0xD000
    import  io

    ld      bc,#_boot-(start-_boot)	// Section length minus
                                    // initial copying code
    ld      de,start                // Destination virtual address
    ld      hl,start-_boot          // Source address (placed near 0)
    ldir                            // Store in proper base location
    jp      start                   // Start executing at proper addr.

The first line is an instruction to the assembler telling it that this section of code will be stored at fixed memory address D000h. On the Rabbit system, this points to the Flash ROM chip, where the program was stored when downloaded from the development computer during the bootstrap operations. Next, the code calculates the length and pointers to the program and where in RAM it should be copied. The 'ld' instructions load values in the appropriate CPU registers and the 'ldir' command copies 'BC' bytes of data from the 'DE' address to the 'HL' address. Finally, the 'jp' command causes the processor to jump to the 'start' address in RAM and begin running the code found there.

start
    ld      a,0x84
    ioi ld  (SPCR),a                // Make Port A usable for output

    ld      a,1                     // LED 1 on, 2 off
    ioi ld  (PADR),a
    call    delay
    ld      a,2                     // LED 1 off, 2 on
    ioi ld  (PADR),a
    call    delay
    jr      start                   // Repeat

These commands first force the Parallel Port A to output mode, instead of input mode. Output mode is required if you want to be able to turn on or off bits in the port hardware. In the case of the Rabbit prototype board, port A's two lowest bits are connected to light emitting diodes (LEDs). So in order to turn on or off the LEDs, port A must be changed into output mode. When used in input mode, you could read the port and detected whether the connected lines are high or low. In the prototype board, two switches are available that can be read this way.

After parallel port A is setup for output, next the value 1 is written to parallel port A's register. This causes LED 1, which is connected to the lowest bit to light up. All other lines will be zero, which turns off LED 2. Then after calling the 'delay' function, the value 2 is written to port A, which turns off LED 1 and turns on LED 2. Then after calling the 'delay' function again, the entire code repeats itself by using the 'jr' command to jump back to the top. This repeats until the processor is reset or powered off.

The 'ioi' commands in front of the 'ld' statements are a special feature of the Rabbit processor that instructs it to do a port I/O operation, instead of just writing a 1 or a 2 to RAM memory. It greatly simplifies using the Rabbit processor for data I/O. In many other processors, a special 'in' or 'out' command is used to do data I/O instead. This is a very nice feature that the designers of the Rabbit processor have provided. Other processors would probably require extra steps to do the same thing.

delay
    ld      b,2
loop
    bool    hl
    rr      hl                      // Zero HL
loop2
    dec     hl
    ld      a,l
    or      h
    jr      nz,loop2
    djnz    loop
    ret

The 'delay' function works by loading a value of 2 into the B register, then zeros out the HL register using the 'bool' and 'rr' commands. Now a tight loop is run that subtracts one from the HL register each time and repeats as long as HL is not zero. The 'dec' command subtracts one from the HL register. The 'ld' command copies the lower 8-bits of the HL register to the A register. The 'or' command compares the A register to the H register. If both A and H hold a zero value, then the processor sets the 'zero' bit in the FLAGS register. The 'jr nz,loop2' command tells the processor to jump to the code at label 'loop2', if the 'zero' bit in the FLAGS register is not zero. If it is zero, then no jump occurs. Instead the command 'djnz loop' tells the processor to first subtract 1 from the B register and if it is not zero, jump to the label named 'loop'. Once the B register does become zero, we return from the function using the 'ret' command.

The result of all this is that the processor counts from 65,535 down to zero (HL register) and repeats this twice (B register). This is a common, if simplistic, technique for adding a delay to a program in assembler language.


The C Language

C is probably the most widely used programming language today. It has a number of features that make it a good choice for both small-scale embedded projects, as well as large-scale projects. It has unique features that sometimes make it look almost like an assembly language, yet simplified with many other features that make it into a high level language as well. It's new cousin, C++ adds many new and powerful features, some of which are suitable for embedded designs, and others that are not.

Almost all processors, embedded or not, have a C compiler available for them. Of course, the compiler must support the opcodes of the processor in order to work correctly. You cannot just use any old compiler at all. Generally compilers will strive to be compliant with the ANSI standards, but in the embedded systems market, it is not at all unusual for a compiler to not be 100% ANSI compliant. Generally this is because of 2 main reasons.

First, some features of C may be difficult to implement correctly on the processor. This is sometimes true in older designs, but most modern processors are designed with higher-level languages in mind. For example, the Rabbit processor has been built almost specifically with the C language in mind. It has a wide range of registers and a simplified way of doing I/O, designed for use in C on purpose.

Second, the developers of the compiler may have made a conscience decision to deviate from the standards. Sometimes this may be due to lack of time or expertise, but more often it is so the language can support special features on the chip. Dynamic C is an example of this. While it support the ANSI standards loosely, it has changed some features and added many others to make programming the Rabbit processor easier.

Let's take a look at a C version of the same LED flashing program we discussed earlier. Here is the full listing:

////////////////////////////////////////////////////////////////
//
//first.c
//
// Sample program to flash the Rabbit prototype board's
// LED in sequence.
//
// Revision History
//
// 5/11/01  RLP  Initial Version
//
//////////////////////////////////////////////////////////////

main()
{
 int i;

 WrPortI(SPCR, &SPCRShadow, 0x84);

 while(1)
 {
   WrPortI(PADR, &PADRShadow, 0x01);
   for( i = 0; i < 32000; i++ );
   WrPortI(PADR, &PADRShadow, 0x02);
   for( i = 0; i < 32000; i++ );
 }
}

Most C programs would start with a number of include statements, but notice how the Dynamic C is different. Basically when it starts, it pre-loads all the libraries and headers needed to build programs already. Dynamic C also downloaded a special program to the processor when it first loads. This program is called a monitor program and it is used by the system to aid in debugging. It allows you the programmer to build, install and test programs using a real Rabbit processor, instead of using a simulator. Using the actual processor board is generally a better way to develop software than using a simulator, although they can sometimes have their uses too.

Every C program ever written starts with a function called main() and this one is no different. The first thing that happens is we allocate space to store one integer and name that memory location 'i'. The 'WrPortI' function is a special function provided by the libraries designed to work with the Rabbit processor. The WrPortI(SPCR, &SPCRShadow, 0x84) command writes the value 0x84 into the Slave Port Control Register, which tells the processor to use Parallel Port A as an output port.

Next, the while(1) statement begins an endless loop. Each time through the loop, we alternately write values 0x01 and 0x02 to Parallel Port A, followed by a for loop to cause a delay. Since there are LEDs connected to lines 0 and 1 on Port A, the LEDs turn on and off in sequence.

The second parameter to the WrPortI function calls is a pointer to a variable. Dynamic C requires this and uses it to store the value read back from the register, immediately after writing to the port. This is used in many cases to verify that the write was successful.

Notice how much shorter this program is than the assembly language version. First off, the Dynamic C system takes care of all the bootstrap/download code automatically. In addition, functions like WrPortI greatly simplify I/O operations.

It is also possible to mix C and Assembly language, if desired. Look at this example code:

////////////////////////////////////////////////////////////////
//
// second.c
//
// Sample program to flash the Rabbit prototype board's
// LED in sequence.  (Mixes C and Assembler)
//
// Revision History
//
// 5/11/01  RLP  Initial Version
//
//////////////////////////////////////////////////////////////

void delay()
{
#asm
    ld      b,2
loop:
    bool    hl
    rr      hl
loop2:
    dec     hl
    ld      a,l
    or      h
    jr      nz,loop2
    djnz    loop
#endasm
}

main()
{
#asm
    ld      a, 0x84
    ioi ld  (SPCR),a
loop:
    ld      a,0x01
    ioi ld  (PADR),a
    call    delay
    ld      a,0x02
    ioi ld  (PADR),a
    call    delay
    jr      loop
#endasm
}

As you can see, the special #asm and #endasm compiler pragmas are used to mark the beginning and end of the block of assembly language statements. Refer to the discussion of the assembly language section for a detailed explanation of the code. One thing you should note is that the assembly language code can refer to variables that were defined in C. The variables PADR and SPCR are usable from the assembler code or the C code.

The ability to mix C and assembler together like this can be a real boon to programmers. It allows you to do most of the coding in C, using all of its advanced features. Yet, if you need to maximum speed or want to reduce the size of the program, you can also write it in assembly language. This is generally reserved for programs that must include things like interrupt handlers, which must be as small and fast as possible to prevent loading down the processor. While Dynamic C supports an interrupt handler written in C, it will usually be slower than one written in assembler. It is nice to be able to mix the two languages and take advantage of the best of each.


Other Languages

There are many programming languages in the world beyond assembler and C. Some of them are suitable for embedded systems designs, while others are not. Even with all the languages available on the market today, Assembler and C are the most common ones used in the embedded market. Most other languages just aren't small enough, or fast enough, to be of much use in small-scale embedded systems. During this course, we'll stick to using a smattering of Assembly language and lot of C.

Just remember, there are some other languages, like Ada or Java, that can be used for embedded systems, but they are generally relegated to a niche market. Feel free to investigate those languages if desired. They have powerful features and are more than capable of producing very useful programs, when combined with the right design.