Embedded C Primer & IAR Introduction

Essential C Programming for MPG using the IAR development environment [VIDEO]

Platform Test Tools SAM3U2 Firmware nRF51422 Firmware Software
MPG1
MPG2
Server Simulator Code IAR 7.20.1

Prerequisite Modules

Mastering the development/debug environment (integrated development environment or “IDE”) is essential and you also need to have a good understanding of the C programming language to read and write code for this course and your own projects. This module will demonstrate the basics of working with IAR, go over fundamental C programming syntax, and show many powerful features of the IAR debugger. This particular project is configured to run in simulator mode, so no hardware connection is required.

IAR Introduction

IAR uses Projects inside Workspaces to organize code. Follow these steps to open the project:

  1. Make the Server Simulator code the your active branch in Git (or just download the .zip from the module link).
  2. Launch IAR.
  3. File > Open > Workspace… and navigate to \\Git\Razor_Atmel\firmware_mpgl1\iar_7_20_1 and select mpgl1.efmw-01.eww

A file tree of the project is on the left, and two panes are open that have code files. The basic project structure is the same as the code you will use for the rest of the modules so you can start to get familiar with it.

Iar_open

All source files included in the project are compiled / assembled when the code is built. Header files and other files can be put into the project tree for reference, but the compiler only looks for header files that are #included by the source files. We use a single master header file called configuration.h that is included by all source files. The configuration.h file then includes all the other headers needed in the program. There are arguments for and against this approach.

IAR Debugger

The debugger runs code and gives you access to the memory contents of the processor (or simulated processor in this case). Press Ctrl-D or click the green “Play” button – this rebuilds the current code, flashes it to the processor, and starts the debugger. The code is NOT running yet — the program counter is at the first instruction ready to go. A quick way to determine this is that you can see a green bar in one of the windows — this is showing you the location of the program counter which is the pointer to program memory that is being executed. Note that the “Disassembly” window shows you the actual instructions that the source code was compiled in to.

Iar_debug_start

The project you are working with is already set up with the recommended windows. You can open and close other windows that might have information you want — look under the “View” menu for all of the options and feel free to explore. Right now the main goal is to get familiar with the basics of running the debugger.

  1. Press the triple arrow button next to the red X or press F5 to run the code full speed.
  2. Press the red hand to halt the debugger. This stops the processor at whatever instruction it is processing and automatically downloads all of the processor’s current information into the debug environment.

Notice that the available debugging tools change depending on the state of the debugger (running or halted):

IAR Buttons

There are two things you should avoid doing during debugging:

  1. Do not press the reset button on the development board during debugging. If you need to reset, use the software button.
  2. Be careful to not press the red “X” in the middle of debugging a complex problem. Learning the F5 (RUN), F10 (STEP OVER) and F11 (SINGLE STEP) shortcut keys is highly recommended.

You should notice that the program just sits in a while loop. Notice the “CYCLECOUNTER” value in the “Current CPU Registers” window. This is a special simulator counter that shows how many instruction cycles have passed since you last halted the code. Run and halt the code a few times and also use F10 and F11 to step. Even though the code does not appear to be doing anything, it is in fact executing an instruction that simply branches to itself. The simulator runs at the default clock speed for the processor which is about 16MHz — 16 million instruction cycles per second.

Iar_current_cpu_registers

Although that is cool in itself, we point this out mostly to highlight a debugger feature: in the active debugging windows (i.e. any debugging windows you can see), values of registers / variables that have changed since the last time the code was halted will be red. This is very important to understand and make use of.

C Programmming

The C programming language is a staple to embedded systems. C is well known as a “low level” language even though it is technically a high level language. C is compiled into assembly code just like C++. C and C++ share many similarities, but C does not have the complexity of C++. Today, C++ and object-oriented concepts are often used in C programming even though the compiler does not enforce them. The amount of syntax to learn for writing embedded firmware in C is fairly small. The examples below go over the critical aspects of C. They will work together to create a simple restaurant application with servers carrying drinks.

Basic Types and Naming Conventions

C supports 8-bit, 16-bit, and 32-bit integers both signed an unsigned. It also supports floating-point numbers but in many cases embedded systems do not, so we will focus on integers. There are quite a few different conventions for type names these days, the following list shows just a few for unsigned integer values that you might come across:

  • unsigned char | UCHAR | uint8_t | u8
  • unsigned short | USHORT | uint16_t | u16
  • unsigned long | ULONG | uint32_t | u32

Type definitions are used to equate these to be the same — take a moment to look at them in typedefs.h. We prefer the u8/u16/u32 style. There are also many conventions for naming variables. We will use Hungarian notation which means the variable type is included in the variable name. Variable names should clearly indicate what the variable does.

Add an example variable in main():

u32 u32UselessVariableForExample;

Variables can be initialized when created or later on before they are used. Do not use an uninitialized variable, especially a pointer.

The “scope” of a variable is important to understand. The variables in functions live on the stack and will not be visible outside of the function call — the compiler enforces this. If you need information from functions in other parts of a program, you either have to pass it through function parameters, the function return value, or use global variables. Global variables are often avoided but are sometimes the best solution to certain problems. We use some specific conventions for global variables that are meant to have scope visible to all other applications in the project, and globals whose scope is only global within the task where it is defined. You will see special sections in our source code for globals.

/******************************************************************
Global variable definitions with scope across entire project.
All Global variable names shall start with "G_"
*******************************************************************/
/* New variables */

/*----------------------------------------------------------------*/
/* External global variables defined in other files (must indicate which file they are defined in) */

/***************************************************************
Global variable definitions with scope limited to this local application.
Variable names shall start with "Main_" and be declared as static.
***************************************************************/

Create an 8-bit integer in Main’s global variable section (around line 25) following our naming convention:

static u8 Main_u8Servers = 0;   /* Number of active servers */

Variables can be declared “const” so they cannot be changed.

Preprocessor #define statements in header files are also used often. These are just symbols that get replaced by the value they hold when code is compiled. By convention, symbols are all UPPERCASE and use an underscore between MULTIPLE_WORDS_LIKE_THIS. We also always type cast them (i.e. put the intended type in parenthesis before the number) to imply their size limits and proper usage.

Add a definition for the maximum number of drinks a server can carry on a tray in main_solution.h:

/************************************************************
* Constant Definitions
************************************************************/
#define MAX_DRINKS   (u8)10   /* Maximum number of drinks */

Bit-wise operations

The smallest data type that C handles natively is a bytes. A byte has 8 bits. Working with bits is very common in embedded systems so you have to use bit-wise logic to access them within a byte. Bit-wise operators are:

  • AND: &
  • OR: |
  • XOR: ^
  • Invert bits: ~

A good example is a “flag” register, which is a variable where the programmer assigns meaning to individual bits. For example, define a flag register and assign three bits to track conditions in a system (arbitrarily we choose bit 1, 2 and 8). By convention, bit names are _ALL_CAPS with a leading underscore.

u8 u8FlagRegister = 0;

#define _FLAG_BUTTON_WAS_PRESSED  (u8)0x01
#define _FLAG_ERROR               (u8)0x02
#define _FLAG_RESET_REQUEST       (u8)0x80

At some point in the code, the _FLAG_ERROR bit might need to get set which is done by a bitwise OR:

u8FlagRegister |= _FLAG_ERROR;

Later it might be cleared which is done by a bitwise AND with the inverted bit:

u8FlagRegister &= ~_FLAG_ERROR;

A bit can be toggled with XOR:

u8FlagRegister ^= _FLAG_BUTTON_WAS_PRESSED;

We can also check if a bit is set:

if(u8FlagRegister & _FLAG_BUTTON_WAS_PRESSED)
{
  /* Do something */
}

Enumerated types

Enumerated types (“enum” for short) are important to help programmers use compiler-enforced rules to manage data. They also help to improve clarity and self documentation of code. Enum variables can be created directly, but we tend to declare enum types and then declare variables of that type. This helps to ensure the enum is carefully thought out and reusable across an application.

When defining an enumerated type, a list of values sets the allowed names within the type. By default, the first name is assigned a value 0, the second is 1, and so on. Values can also be assigned explicitly in the list so they are not sequential. By convention we put the word “Type” at the end of the type name and use CAPITAL LETTERS for the values to imply the names themselves are constants. Here are two examples from leds.h that you will work with in the next module:

typedef enum {WHITE = 0, PURPLE, BLUE, CYAN, GREEN, YELLOW, ORANGE, RED, LCD_RED, LCD_GREEN, LCD_BLUE} LedNumberType;
typedef enum {LED_PORTA = 0, LED_PORTB = 0x80} LedPortType;  

Create an enum type for drinks in main.h.

/*************************************************************
* Type Definitions
*************************************************************/
typedef enum {EMPTY, BEER, SHOOTER, WINE, HIBALL} DrinkType;

Note that IAR implements a Boolean type using an enum with uppercase TRUE and FALSE.

Arrays and Strings

Arrays are extremely useful containers for data. An array is a continuous block in memory of the data type specified for the array. Arrays can be single dimension or multi-dimensions. When an array is defined, its size must be known. You can explicitly state it, or use an initialization list and the compiler will size the array to fit all of the init values. A classic one-dimensional array is defined like this:

type atypeName1[];  /* Uninitialized array */
type atypeName2[] = {5, 4, 3, 2, 1};  /* Initialized array that will have a size of 5 */

Add these example arrays at the top of main:

void main(void)
{
  u32 au32BigArray[] = {5, 4, 3, 2, 1};
  DrinkType aeDrinkArray[3] = {BEER, SHOOTER};

You might have heard that “arrays and pointers are the same thing.” This is not true. There is some syntax where arrays and pointers are used interchangeably, but if you need an array, use appropriate array syntax and do the same for pointers.

Run the code and examine both of these arrays in a Watch window.

CArrayInit

Strings in C are simply arrays of u8 (CHAR) that by definition contain a NULL character at the end. NULL is used because it is a non-printable control character (see www.asciitable.com to learn more). If you include string.h in your code, then you have access to standard C string functions.

u8 aString[] = "This is really an array of u8 with a NULL at the end.";

Since a simple string in C is just an array, we can describe it like any other array. Points to know:

  • The first element of an array is index 0.
  • The values inside an array are accessed (indexed) using square parenthesis (e.g. aString[1] would access the element with ‘h’).
  • Use “sizeof” to get the size of any array e.g. sizeof(aMyArray) — it is returned in bytes.
  • If an array type is something other than a single-byte storage type, then the number of elements is NOT equal to the size. The number of elements is sizeof(aMyArray) / sizeof(array_type).
  • The last element of an array is [the number of elements – 1].
  • Memory addresses allocated for an array are sequential.

Pointers

A pointer is a variable that stores an address. Every byte in an ARM microcontroller has an address. If you know the address of a value that you want to look at, you load a pointer with the address of interest and then “dereference” the pointer (i.e. go look at that address) to get the value. Programming embedded systems at a low level is a great way to get a better understand of pointers. The pointer itself is a 32-bit value holding a 32-bit address (at least on our 32-bit ARM processor). When defining a pointer, you must tell the compiler what variable type the pointer will be pointing to which can be any other variable type like a single byte, a struct, or another pointer. Here is what you should know:

  • Declaring a pointer is done with the * operator, e.g. u8* pu8Pointer.
  • Setting a pointer to a variable is done by assigning an address to the pointer, e.g. pu8Pointer = &u8SomeVariable where the ‘&’ is the “address” operator in this case.
  • When you increment a pointer, the value it holds (i.e. the address it is currently holding) will increment by the size of the variable type it is pointing to.

Try the following code at the top of main():

u8 u8Test = 0xA5;
u8* pu8Example;
u32 u32Test = 0x0000ffff;
u32* pu32Example;

/* Load the addresses into our pointer variables */
pu8Example = &u8Test;
pu32Example = &u32Test;

/* Access the variables via the pointers (two different ways) */
*pu8Example += 1;
(*pu32Example)++;

/* Move the pointers (watch out for the second one!) */
pu8Example++;
*pu32Example++;

Start the debugger and use single step (F11) to run the first 4 lines of code (up to but not including *pu8Example++). Make sure you have the “Locals” tab active in the bottom debugging window that shows the variables that are in the current scope. Note the following:

  • The Location (address) of u8Test is 0x20081FFFC; the Value is 0xA5 per the assignment that we did at initialization.
  • The Location (address) of 32Test is 0x20081FFF8; the Value is 65535 per the assignment that we did at initialization. Note that you can right-click on the Value and change the number representation.
  • The Value of pu8Example is 0x20081FFFC which is the address of u8Test and you can expand pu8Example to see the value at that location (trivial in this example, but very useful in other scenarios).
  • The Value of pu32Example is 0x20081FFF8 which is the address of u32Test.

Pointer

Single-step twice to increment the test values using their pointers – watch the variables increase to 0xA6 and 65536.

Single-step two more times to advance the pointers. Notice that pu8Example increments by one byte address, but pu32Example increments by 4 bytes. Also notice that pu32Example has a ‘*’ operator so while you might expect the line of code to increment u32Test, C’s operator precedence assigns ‘*” and “++” the same precedence and thus works right to left so pu32Example is incremented and the ‘*’ operation does not actually do anything.

In a few lines of code, we have captured the essence of pointers.

Structs

Structs are used to group related variables together. They also make for very efficient passing of parameters into functions, since a pointer to a struct is a single 32-bit address to pass even though the information at the end of that pointer could be quite sizeable. Structs improve readability and imply relationships between variables which also improves self-documentation.

Like enums, you can declare structs explicitly or define struct types and then create variables of that type. If a struct typedef is declared and one of the member variables needs to be the struct type, then it has to be declared “void” or else the compiler will give an error. We will be creating a linked list in this module, so define a struct for a drink server which will make up members of our linked list. Since it is a typedef it goes in the main.h header file.

typedef struct
{
  u8 u8ServerNumber;                    /* Unique token for this item */
  DrinkType asServingTray[MAX_DRINKS];  /* Data payload array */
  void* psNextServer;                   /* Pointer to next ServerType*/
} ServerType;

If a struct is declared in the local scope, the dot operator is used to access the struct’s member variables. If a struct pointer is used, the arrow operator accesses the struct’s member variables.
Code the following in main before the while(1) loop for a quick example:

u8 u8CurrentServer;
ServerType sServer1;
ServerType* psServerParser;

psServerParser = &sServer1;
sServer1.u8ServerNumber = 18;
u8CurrentServer = psServerParser->u8ServerNumber;

Restart the debugger to build and reload the code. Try setting a breakpoint at the psServerParser = &sServer1; line by left clicking the margin next to the code – you should see a red dot and highlight appear. Alternatively, left-click the line to set the cursor there, then right-click the line and select “Run to Cursor.” Now do the following:

  1. Activate the “Watch 1” window in the top right debugging space.
  2. Double-click the “sServer1” variable name to select it and either drag it up into the Watch 1 window, or copy and paste it.
  3. Repeat to add psServerParser and u8CurrentServer to the Watch1 window. Even though you can see these values in the Locals window, that window is already starting to get crowded
  4. Expand sServer1 variable to see the struct members; further expand the struct members asServingTray and psNextServer. You now have full few into the variables!
  5. Try to predict what will happen when you step through the 3 lines of code. Test your prediction and understand what is happening.

server_watch

Functions

A function (or sub-routine) is a chunk of code that performs a specific task when it is called. In some cases it will return a result, in other cases it just makes something happen. The basic syntax is:

return_type function_name(arguments)
{
  /* Function code */
  return (return value);

} /* end of function */ 

If a function does not return anything, then return_type is “void”
If a function does not take parameters, then arguments should be “void”

You can spend a lot of time learning how functions are actually implemented in C and how that implementation will use processor resources. To keep things simple, we will recommend that you minimize the number of arguments passed, and try to make functions very purpose-built so they do only one thing. There are also lots of different styles to set up and document functions. We use a “function prototype” in the header file, and code the function itself in the associated source file. We document our functions with a brief description and also have a “Requires / Promises” notes. “Requires” defines the system conditions assumed when the function is called and details the incoming function arguments. “Promises” indicates what system conditions are true when the function exits and what (if anything) is returned.

Add the following function to main.c to initialize a new server:

/*----------------------------------------------------------------------/
Function InitializeServer

Description:
Initializes a new server.  A new server has an empty tray of drinks and is
assigned the next number available.

Requires:
  - psServer_ points to the server list where a new server is to be initialized; 
    since the server list is a pointer to a linked list, this is a pointer-to-pointer
  - Main_u8Servers holds the current number of active servers

Promises:
  - Returns TRUE if the server is initialized
  - Returns FALSE if the server cannot be initialized
*/
bool InitializeServer(ServerType** psServer_)
{
  if(*psServer_ == NULL)
  {
    return(FALSE);
  }

  return(TRUE);
} /* end InitializeServer() */

Copy the function definition line and paste it into main.h with a semi-colon at the end.

/*************************************************************************
* Function declarations
*************************************************************************/
bool InitializeServer(ServerType* psServer_);

Loops and conditional execution

The processor will sequentially execute code from the first line in flash unless it is told to do something else. There are various ways to change the program flow including looping and if/else structures which are fundamentals in any programming language. C has “for” loops, “while” loops and “do/while” loops. It also uses “if/else” and “switch/case” structures.

For loops have the structure:

for(initialize a variable or condition; keep looping if this condition is TRUE; do this when the loop is done before next iteration)
{
  /* do something */
}

While loops have the structure:

while(this condition is TRUE)
{
  /* do some stuff */
}

Do / while loops have this structure:

do
{
} while (this condition is true)

Both for loops and while loops might run 0 times or run continuously until the exit condition occurs, but do/while loops always run at least once. All loops are potentially infinite, so it is a best practice to have at least one logical condition that will terminate the loop (like a timeout) if it depends on an event, external input, or anything else not deterministic.

If / else if / else structures are conditional statements that tell the processor to evaluate a condition and make a decision. The structure is:

if(condition)
{
  /* do something */
}
else if(condition) /* optional, and you can chain as many as you want */
{
  /* do something */
}
else /* optional */
{
  /* do something */
}

Add code to InitializeServer() to initialize all the values:

psServer_->u8ServerNumber = Main_u8Servers;

/* Start with an empty tray */
for(u8 i = 0; i < MAX_DRINKS; i++)
{
  psServer->asServingTray[i] = EMPTY;
}

The Heap

The Heap is an allocation of RAM (the size of which is under control of the programmer) separate from the stack. Two common uses of the heap are for variables that need to persist and be accessible outside of function calls and also for dynamically allocated memory using malloc(). C has some notoriety when it comes to malloc() and memory leaks. A memory leak occurs when memory on the heap is allocated using malloc() but the program (or programmer) does not free (release) it. Since microcontroller-based embedded systems are often very resource-limited, using the heap must be done very carefully.

>Create a new server object using malloc:
To use malloc, you need a pointer to the object type you are trying to create. Call malloc with the size of memory you need. If malloc fails, it returns NULL so this is what must be checked.

  /* Try to create a new server object */
  psNewServer = malloc( sizeof(ServerType) );

  /* Check that we have are not at the maximum server limit */
  if(psNewServer == NULL)
  {
    return(FALSE);
  }

When you are done with the memory, it must be freed back to the heap. This can get difficult if the memory is allocated in a function and then passed to other parts of the code. In the case of our server objects, the new memory contains a pointer to other memory, so if the object is removed, you must also preserve the pointers to the other locations. It helps to draw a picture.

CPointers

If Object 2 is to be removed, set a temporary pointer to it, connect Object 1 to Object 3, then delete Object 2. The edge cases are special cases that must be handled correctly as well. The code for our solution to the exercise for removing a server node looks like this:

        /* If the server's tray is now empty, remove the server */
        if(u8EmptyCount == MAX_DRINKS)
        {
          /* Put a pointer on this node as it will be removed and put the parser back to the start of the list */
          psServerListDoomed = psServerListParser;
          psServerListParser = psServerList;
          
          /* Handle if doomed node is first */
          if(psServerListParser == psServerListDoomed)
          {
            psServerList = psServerListDoomed->psNextServer;
          }
          else
          {
            /* Find the node just before the doomed node */
            while(psServerListParser->psNextServer != psServerListDoomed)
            {
              psServerListParser = psServerListParser->psNextServer;
            }
            
            /* Connect the current node to the node after the doomed list */
            psServerListParser->psNextServer = psServerListDoomed->psNextServer;
          }
          
          /* Free the memory of the doomed node */
          free(psServerListDoomed);
          strcpy(au8MessageCurrent, au8MessageServerRemoved);
          bNewMessage = TRUE;
        } /* end if(u8EmptyCount == MAX_DRINKS) */

Printf and Scanf

There is no complete built-in printf and scanf functionality in an embedded system like the one we use in this program. If you have programmed on a PC, you have likely used printf and scanf and a Windows terminal to read and write characters. What might surprise you is the complexity of implementing printf() and scanf(). IAR includes the “front-end” of these functions that will operate on strings, but it is up to the developer to write a driver to read and write these strings at the hardware level. The easiest and most common place is to a UART peripheral that goes to an RS-232 connection and then to a Windows terminal. The USB-to-Serial adapter that connects to the development boards is the hardware interface between the 0-3V UART signalling on the development board and the +/-5V signalling on the PC. There is a good chance that every embedded system has basic serial port access. In later modules we will demonstrate how this works.

Exercise

Now we will put everything together in this module to build a simulated restaurant where you can order drinks. The servers will be ServerType objects in a linked list. There are a limited number of servers (MAX_SERVERS), and each can carry a limited number of drinks (MAX_DRINKS). A boolean variable will be used to “order drinks” since we do not have any other input to the system. Halt the code and set this to 1. We can also keep an output string location to send messages — a very crude printf() workaround. A breakpoint will be set where the message is updated so the code will halt and you can read the message.

The following details should be implemented:

  • The main loop will run infinitely but after each iteration a pause is inserted to simulate 1ms of processor sleep time. The variable u32LoopCounter will track how many loops have run and function as our system timer.
  • The variable bOrderDrink is set to TRUE through the debugger to request a drink. The type of drink is selected based on ((u32LoopCounter % 4) + 1) to select one of the 4 drinks defined in DrinkType. Drinks will be added to the first server in the list until the server’s tray is full. The message “Drink ordered” is printed.
  • If there is no space for drinks, a new server is added up to MAX_SERVERS. The message “New server added” will be printed when a new server is added. If no more servers are available, the message “No free server” will be printed.
  • A drink is automatically removed from a tray every 3 seconds (every 3000 loop iterations)
  • If a server’s tray becomes empty, the server is removed from the list. The message “Server removed” is printed.

The debug environment should be carefully set up as shown so that all of the server information and variables needed are visible. Run the code and use the debugger to verify that servers and drinks are added and removed properly.

CServerIARDebug

[STATUS: RELEASED: LAST UPDATE: 2016-MAR-05]

*** ENGENUICS WILL NOT BE ABLE TO SHIP PRODUCTS BETWEEN MAY 26 AND JUNE 26, 2017 ***