debug_teaser.png

In the last section, you learned of some essential practices and tips for software development.  We continue those topics in this section, covering inline functions, reading and writing flash memory (including User Page flash), and how to avoid buffer overruns.  The potential issues of typcasting is discussed, and how configuration locks might help when things go really south.

 

Code Well, and Everything Works Better -- Continued

(See the prior section for parts 1 through 8.)

 

9. The implications and limitations of inline functions
If you define a function with the keyword “inline” in front of it, you are telling the compiler that you want that function to be copied into code as if it were typed into the code directly rather than as a function. As the programmer, you hope that this will speed up the code execution time. Whenever a function is called in C, the contents of the input parameters are placed onto the stack together with the return address where the code is supposed to jump back to when the function is done, and then the code jumps to execute the function. By making a function inline, you intend to bypass that time shuffling data to and from the stack. However, the C compiler takes this more or less as a suggestion, as small functions will be automatically inlined by the compiler. The compiler will ignore this inline suggestion for larger functions that the compiler has determined will see little benefit from the speed up.


Inline functions cannot contain locally static variables. Since the function contents are intended to be replaced within the code, a static variable that is private to a function scope has no meaning. Inline functions have no scope.


10. The difference between reading and writing flash memory
To read data from any region of flash memory in EFM32 is very simple. Flash can be accessed with a pointer to a memory address like so:

volatile uint8_t* address = 0x1000;
uint8_t my_value = *address;


By placing the asterisk in front of the address variable, you are dereferencing the pointer, which gives you the byte sized contents of system address 0x1000. The number of bytes retrieved from flash will vary based on the type of variable for my_value. For example, my_value defined as a uint64_t will fetch eight bytes at once from address 0x1000 to 0x1007. Therefore, executing my_value++ as a 64-bit unsigned integer points to 0x1008.


Writing to flash memory is a much trickier business. All flash memory organizes memory into “pages” that group writeable cells together into big chucks that can only be erased together as a group. Therefore, if you want to write to a single byte of non-volatile memory, you have to erase the entire page first. To the programmer, that means that you have to first read the contents of memory for the whole page, store it in RAM, erase the page, then write back the whole page together with the modified byte, hoping that you don’t lose power in the middle of the operation or else you just lost that page of data forever. There are usually timing implications too, because erasing and writing a page takes a lot more time than reading a value. When a page of flash is erased, all of the contents of the page are reset to 1’s, or 0xFF’s in a byte-sized memory viewer. Therefore, when you write a byte to memory, you are simply clearing those bits that are not 1’s.


To write to EFM32 flash, another requirement is that the function that is writing to flash must be resident in RAM, not in flash where normal executing code is located, while the same code is being executed. This requires that any function that intends to write to flash memory that is also being executed is declared with the RAMFUNC declaration as defined in the ramfunc.h library utility file.


11. Store persistent data in flash User Page flash
Main flash memory starts at address zero and is capped at different capacities based on the EFM32 model. See the Reference Manual for your specific chip to find out where main flash memory ends. In addition to the built-in flash, there is a memory region set aside for external flash expansion that resides just beyond the internal main memory, up to 24 MB. Any of the operations to read or write main flash memory covered in the previous section can be used to access main flash memory. However, every time that the chip is programmed, or if the device is erased, the data stored in the region used by your program will be overwritten by you new code. Therefore, reading or writing to main flash is typically used when performing a flash update procedure, for example, in a bootloader. You can of course feel free to use flash memory that is located above the program size as long as you realize that it will be erased if the chip is ever erased with a device erase operation or JTAG programming sequence.


The EFM32 provides a separate page of flash memory that is not erased with a device erase operation or JTAG programming operation. This region of flash memory is called the User Page and starts at address 0x0FE00000. The amount of memory available is a single page, and the size of a flash page varies according to the model of EFM32 chip. Check the Data Sheet or Reference Manual for the size of a flash page. You can use this page for anything that you want to persist on the device from boot-to-boot that is safe from device erase operations, such as device serial numbers, device calibration data, or any data that is specific to each device.

 

12. Avoid buffer overruns and the havoc those can cause
When developing code with a desktop computer, you are lucky to be developing on a system that has an operating system, a built-in file system, and display screen. When you run into trouble with your software, the OS springs into action to rescue you and inform you that you tried to access memory that was outside the bounds of your program, or that you did something that created a fault. You have few luxuries when developing embedded code. There is no OS to watch over things to make sure that your program stays within some nicely defined boundary. If your code decides to write data to address zero or one million, the MCU will try to do what it is told. It cannot tell you that you don’t know what you are doing because there are no limits to what your program can do. You are all powerful, which is a good and a bad thing.


A big problem that embedded developers face is buffer overruns. Working in embedded means lots of direct manipulation on memory addresses. If your program starts to behave erratically or you see a variable that seemingly changes its value without being set, a buffer overrun could be the culprit.


The first thing to watch out for is obvious but still happens to the best of us. If you define an array that is x bytes long, don’t write beyond x-1. If there are x items in an array called foo, you can only address foo[0] to foo[x-1]. The bad thing is that if you address foo[x], foo[x+1], or so on, your code still works sometimes. The MCU will happily comply and write over whatever other variable happens to reside just beyond foo[x-1]. Then your project starts to act funky.  

 

This can also get out of control whenever you are adjusting pointers, while casting the pointers to a different type. For example:

void some_function()
{
// Array of just 4 bytes
uint8_t my_array[4];

// Pointer to group of 4 bytes
uint32_t * my_ptr = (uint32_t *) my_array;
// foo is assigned all 4 bytes of my_array, 
int foo = *my_ptr;

// then my_ptr is incremented by one
// which to my_ptr's type means 4 bytes
// So my_ptr is now sitting out of bounds
my_ptr++;
}

The above demonstrates how important it is to be careful when interpreting structures that are typed a one way at creation, but then later used by pointers that are typed differently.  Just because you can do the type cast, doesn't mean that you should do it.


Another way that you can get into trouble with buffer overruns is by forgetting about the limitations of local scope. In C, most of the work done by functions is performed on pointers that are passed into the function. The return value from a C function is often limited to a “status” byte because simple values are easily returned from C functions on the stack, while other structures are trickier to return. Take for example a simple array that is local to a function:

 

// This function will generate a compiler warning:
// warning: function returns address of local variable
int * some_function()
{
int my_array[4];
return my_array;
}
// This function has no warning!
int * some_other_function()
{
int my_array[4];
int * foo = my_array;
return foo;
}


As soon as the function returns, the memory allocated for my_array will be reclaimed by the system and allocated for the next thing that needs a chunk of memory. If you return a pointer to a local array from a C function, the compiler won’t warn you, and the MCU will happily do what you tell it to do. Once again, the code will work sometimes if there happens to be no new memory allocations immediately after the function returns. Your solution will be intermittent, and you will curse the day you ever decided to work on an embedded project!

 

// This is a better way, pass pre-allocated pointers into the function
// “int my_array[]” and “int * my_array” are identical for function parameters
void some_new_function(int my_array[])
{
my_array[0] = 1;
}


13. Become a type-casting master
When working “close to the metal” as you do in embedded development, you are constantly working with small bits of information, so you end up converting from one type to another often, with limited variable sizes such as 8-bit registers. Some of the examples of this section have shown how you must be aware of the impact on addresses when incrementing a pointer on a uint8_t type versus a uint32_t type. In addition, you must also be aware of how the C compiler interprets and converts types.

 
The casting of types from one type to another doesn’t actually perform any conversion. It’s not a function, but is just a way to tell the compiler how data should be interpreted. It seems that the C compiler should warn you whenever you are assigning variables of different types to one another, but the compiler doesn’t always warn you of this. For example, consider the case of converting signed integers to unsigned integers as in the following:

int8_t a = 0;   // Range is -128 to +127
uint8_t b = 0;  // Range is 0 to 255
long c = 0;     // Range is huge, both positive and negative
a = -64;        // a can be negative
b = 64;         // b can only be positive
c = a + b;      // c is 0
a = b;          // Converting a signed to unsigned is OK, in this case
c = a - b;      // c is again 0
b = 128;        // b is now bigger than a can represent
a = b;          // a is now converted to -128
c = a - b;      // c = -128 - 128 = -256!


This example shows how you can get into trouble as your variables grow in size. Things work OK for a while, but then they can start to fail as the range of your variable is exceeded. Be sure you know what you want when converting types. A uint of size 8 can’t hold anything more than 0 to 255, and an int of 8 bits can’t hold anything beyond -128 to +127. Simply casting a large uint8_t as an int8_t doesn’t magically change your int8_t into a larger int. It is still limited to +127. You will have to use a larger type like int16_t if you want to increase its range.


14. Use configuration locks to mitigate issues with malfunctioning code
Sometimes when embedded engineers do math on pointers, we wander far off course and impact hardware with our lousy software. One way to protect your code from doing something downright awful thanks to buffer overruns and other lousy pointer arithmetic is to use configuration locks. The configuration locks available on many of EFM32’s peripherals require a special value to be written to a configuration lock register in order to allow changes to be made to the peripheral configuration. This prevents misbehaving code from changing the overall configuration of the device.


Keys to Coding Success

 

  • Start with an existing example, then document your changes with well-named variables and lots of comments.
  • Learn more about the C language and understand where and how to declare variables and functions.
  • Don’t try to write to flash memory that hasn’t been erased or to memory that is outside the scope of what your variables can access.

In the next section, we will learn how to better control software builds in the Simplicity Studio IDE.

  • Blog Posts
  • Makers