debug_teaser.png

 

In the last section, you learned about a few essential tips to help you develop your hardware prototype.  In this section, many hard-learned lessons in software development are shared.  What are the keywords extern, static and volatile all about?  Should you be using recursion or malloc() in your code?  Read on and find out.

 

Code Well, and Everything Works Better


1. Find existing software examples for your hardware devices
The first step in developing any embedded solution is to find examples that can make your task easier. The software examples that you find for your specific parts in your custom solution will help you “see” the device in another light and help you reinterpret the device spec, even if those examples are for another computer architecture or software language.


2. Code for the compiler
There is no perfect computer software language. All languages have their strengths and weaknesses. The software language utilized in Simplicity Studio for the EFM32 family is C. The C language has been around long enough that it is well trusted and performs well on embedded designs, but it can be difficult to master the syntax and its idiosyncrasies. When you are coding in C, you are actually writing instructions that are meant for the compiler and other build tools. Keep that in mind. The C language is “close to the metal” in that your code has only a few steps between the code that you write in a human-readable format, the assembly code, and the binary image that is the result of the build process.


The C code has strict typing that requires certain variables match well enough to perform safe assignment. This is there to protect you from doing stupid things, like comparing the address of a variable (i.e. a pointer) with the contents of a variable. But often in embedded development, you need to be able to convert pure numbers into addresses in order to specify a register address. This requires that you become well acquainted with type casts in order to tell the compiler that you really do know what you are doing.


3. Use descriptive variable and function names
The single best thing that you can do to help make sure that your code is well designed is to use descriptive variable and function names. There is no runtime performance penalty associated with long names in C code. All identifiers are removed when the build tools translate the C code into binary machine code. Consider the following snippet of code found in the FAT Filesystem (FF) library:

	res = dir_sdi(dj, 0);
	if (res == FR_OK) {
		do {	/* Find a blank entry for the SFN */
			res = move_window(dj->fs, dj->sect);
			if (res != FR_OK) break;
			c = *dj->dir;
			if (c == DDE || c == 0) break;	/* Is it a blank entry? */
			res = dir_next(dj, 1);			/* Next entry with table stretch */
		} while (res == FR_OK);
	}


The above code has some comments, which certainly help and are a very good thing, but it’s hard to know the exact reason for this code by just looking at the variables, functions, enumerations and preprocessor symbols. Consider the following code as an alternative:

// Load the first target_directory entry without table stretch
result = set_directory_index(target_directory, NO_TABLE_STRETCH)
if (result == FAT_RESULT_OK) { // Look for a blank entry for the Short File Name over all directories do
{ result = find_next_window_offset(target_directory->file_system_object, target_directory->current_sector); if (result != FAT_RESULT_OK) break; // Window offset was OK, check the entry short_file_name = *target_directory->short_file_name; // Is it a blank or unused entry? if (short_file_name[0] == DELETED_DIRECTORY_ENTRY_BYTE || short_file_name[0] == UNUSED_DIRECTORY) break; // Get the next entry with table stretch result = get_next_directory(target_directory, TABLE_STRETCH); } while (result == FAT_RESULT_OK);
}


Yes, the code is a bit wider and harder to type, but Simplicity Studio offers code completion with the CTRL+Spacebar keyboard shortcut, and you can always cut and paste. What you will gain by this is readability that requires less hunting around for what the variable names are intended to do. We can tell just by looking at the second example that this code is intended to look through a target directory and break when it finds a deleted (previously populated but now available) or zero (never populated) Short File Name entry in the target directory. The descriptive names should allow you to read aloud the code as if it were a good story, telling you the purpose as you read along.


4. Use comments religiously
A good software developer adds lots of comments to the code in several key places. The comments, like long variable names, don’t add to the runtime file size of the resulting executable binary and are simply there to help document the purpose of code. The top of each file in the solution should state its purpose of that file, and there should be lengthy comments at the top of each function stating the purpose of the function as well as describe the inputs and outputs. Beyond these key places, comments should be used on a line-by-line basis wherever the intention of code isn’t clear. The use of descriptive variable names can help explain the purpose of code and make comments less necessary, causing the comments that are there to stand out. Trust me, even you won’t remember the purpose of the code a year after writing it, so comment liberally!


5. Use the emlib library
For the EFM32 programmer, the emlib library is your friend. Use these library calls wherever you can to interface with an EFM32 peripheral. These libraries are well-tested and have additional helper code to look for problems rather than just tweaking registers directly. For example, the following code uses the emlib library:

TIMER_TopSet(TIMER3, 1000);

The same thing can be done by addressing the registers of a memory-mapped peripheral by a preprocessor definition, which defines TIMER3 as 0x40010C00. We don’t use this address, which would be hard to remember, but that is where the TIMER3 is mapped in main memory.

TIMER3->TOP = 1000;


All peripherals are mapped to memory addresses in exactly the same way, so you will sometimes see examples using this pointer notation rather than the emlib library functions. If you will look inside of the TIMER_TopSet function definition in em_timer.h, you will see that the function does exactly the same thing as this example, so in this case the library function has provided no added value. However, with the emlib library, you will sometimes get much more functionality than the simple manipulation of a mapped register. For example, the CMU_ClockEnable function takes care to make a lot of decisions on your behalf before finally using a “bit band” command to ensure that a register bit is atomically set. Use these library functions as frequently as possible to gain the benefit of all of the EFM32 library designer’s hard work.


6. Where and how to define variables to avoid problems with the stack and heap
There are many facets of C that are not obvious to the casual programmer, but become important as you run your code in an embedded design. For starters, all locally-declared variables go on the stack. These are the variables that you define inside functions or any block of code.

 

The stack is a region of memory that starts at the “top of memory” or the highest available address in physical RAM, and then counts downward until you hit the stack limit. If you define too many local variables or if your code dynamically creates those variables by using recursion or other nested functions, you can run out of stack space.

 

Global variables are those that are defined outside of all functions and other blocks of code at the module level.   The compiler automatically allocates memory for your globally-declared variables on the heap, which is part of the main memory pool outside of the stackand will generate a compiler error if you try to allocate too much RAM.  However, the use of the malloc() command in your code allocates RAM on the heap at runtime, dynamically.

 

The use of recursion or the malloc() command on an embedded processor with limited RAM is a risky business! You must understand how many recursive attempts (or malloc() calls) your code will ever require in order to resolve and then design a solution that will never run out of stack space.

 

 

If you define all variables in your code and let the compiler determine how to manage memory automatically, you will run into fewer issues of overrunning the stack or heap.   Even with this precaution, if your code is nearly the size available RAM when you compile and build your code, you will need to learn how to monitor the size of the stack and heap, which is beyond the scope of this section.

 

int foo;  			// Global variable, memory is on the heap

void some_function()
{
	int bar;		// Local variable, memory is on the stack
}



7. The difference between global static variables and local static variables
Variables that are defined using keyword “static” means different things at different scopes. Inside functions, the static keyword is used in front of a variable that is to remember its value in-between calls to the function. It’s sort of “sticky” in that you can initialize it at the first invocation of the function, and then it keeps its value rather than being reinitialized every time the function executes like non-static variables. At the global scope, all variables are “sticky” in that they only get initialized once at the start of runtime and then remember their values after that. However, the static keyword placed in front of global variable indicates to the compiler that the variable is local to that module and not to be used by outside modules. This is a totally different meaning for the same “static” keyword.

int foo1 = 1;  	// Global variable, initialized only once
static int foo2 = 2; 	// Global variable, initialized only once, private to this module

void some_function()
{
	int bar1 = 3;	// Local variable, initialized every time the function is called, 
			// private to this function
	
	static int bar2 = 4;// Local variable, initialized only the first time that this function
			     // is called, private to this function
	
	int foo1;	// This is a bad idea.  Local foo1 overrides global foo1 and makes the 
			// global version unavailable inside this function
}

 

8. The meaning of volatile and extern and how they impact each other
As long as variables and functions are not declared as static within a module, they can then be used external to that module and used in other modules. In order to tell the compiler that you intend to use the same variable across modules, you define a variable the normal way in one module, and add the keyword “extern” in front of the definition in all other modules across the design. Now, all modules in your design can gain access to the same variable. However, if one of the other modules in the design intends to modify the value of a variable outside of the place where it is originally defined, you must add the keyword “volatile” in front of that variable. This volatile keyword tells the compiler that the variable can be changed outside the module’s knowledge and prevents the optimizer from removing statements that seem to have no effect.

// Module A
int foo;  		// Meant to be modified in this module only
volatile int bar;	// Value can change unexpectedly outside of this module 
// Optimizer must always evaluate the value of bar


// Module B
extern int foo;	// We can read this value defined in Module A, but should not modify it
extern int bar;	// Since declared volatile in Module A, we can read and modify this variable


In addition, the use of volatile is hugely important when you use a Release build versus a Debug build. The compiler will actively try to squash unnecessary code as the optimization settings increase. This means that you need to prevent the compiler from doing that by using the volatile keyword on any variable that can change outside of the current scope.

 

In the next section, we will continue down the best practices for software path and learn about inline functions, how to work with flash memory, configuration locks and how you can get into trouble with buffer overruns.

  • Blog Posts
  • Makers