5

Programming and Implementation Guidelines

Mark Kraeling    CTO Office, GE Transportation, Melbourne, FL, United States

Abstract

Many approaches come to mind when considering software programming and implementation. One approach might be syntax-oriented—how the code looks and is written. Another approach might be to consider the structural rules that programmers must follow—to keep the code “cleaner.” The ways that software is written and how it is formatted can bring about heated arguments between developers. This chapter is not written to provide a specific way of implementing software, but instead focuses on recommendations—so that a determination can be made on whether they are incorporated or not. There isn’t a single answer to how software should be implemented because there are many factors involved.

Keywords

Code readability; Code maintainability; Code testability; Programming; Hardware platform; Libraries; Coding standard; Syntax standard; Variable declarations; Conditional compilation

1 Introduction

Many approaches come to mind when considering software programming and implementation. One approach might be syntax-oriented—how the code looks and is written. Another approach might be to consider the structural rules that programmers must follow—to keep the code “cleaner.” The ways that software is written and how it is formatted can bring about heated arguments between developers. This chapter was not written to provide a specific way of implementing software, rather focuses on recommendations and good practices. There isn’t a single answer to how software should be implemented because there are many factors involved.

The first factor is project size. Time and time again there are arguments around project structure, use of global variables, and other factors. There are a lot of implementation guidelines that are largely dependent on how large (in terms of source lines of code, for instance) a project is. Having 30 software engineers embark on an activity to use nothing but assembly language, using cryptic variable names, all in the same 8-bit processor space is unlikely to be fruitful. Take that same project, and instead have two software engineers working on it. This seems a little more reasonable! Keeping project size in mind is important when reading over these guidelines.

The second factor is programmer experience and background. Hopefully there is a degree of freedom to tailor some of the implementation guidelines based on what the members of a team can do well, and not so well. It’s quite possible that your team may be made up of people that moved over from another project on the team, another division of the same company, or even another company altogether. There may be implementation guidelines and standards that one group is comfortable doing—providing a benefit to the rest of the team. Don’t fall into the trap of believing “that is the way it has always been done, keep doing it.” An assessment of the way programming and implementation is being done is healthy—if it is done at the right time. Trying to change course in the middle of project delivery isn’t that time—at the beginning or between major releases may be a more appropriate time.

The third factor is future maintainability and project length. The shorter the duration of the project, or if maintainability is not a key factor, the more likely there will be a lack of effort in terms of project structure and commenting. Don’t misunderstand—having useful comments in code is always good for reviewers or to jog your own memory after a weekend! Upon reading the guidelines suggested here—temper some of them if your project is comprised of one programmer putting together code for a project that lasts a month.

There are other factors as well, including safety-critical code development, software that is being sold as software for others to use in their products, and industry regulations for your product or market segment. All these influence (or even dictate) the implementation of software for your product.

1.1 Principles of High-Quality Programming

The implementation guidelines in this chapter are derived to drive higher quality programming on embedded systems. Embedded systems by their very nature are products or systems where the computer processing portion isn’t necessarily evident to the user. Because of this, end customer quality assessment is not directly an assessment of the software, rather the performance characteristics of the system itself. In this way, quality can be measured in a variety of different ways.

1.1.1 Readability

Readability in software programming can be defined as the ease with which the software is read and understood. Readability of software can be somewhat objective. Programmers that are “journeyman” and move from one project to another throughout their career tend to have an easier time reading a variety of software code. However, making software more readable helps in reviewing and maintaining it over the course of its life. Simplicity in logic, conditional statements, and the structure of the code all help with readability.

The following is an example of a proper “C” code segment, that isn’t entirely readable:

// Check for stuff to proceed
if((!((Engine_Speed!=0)||(Vehicle_Speed!=0))) || SecureTest!=FALSE ){
 // ABC…
}

With a little better readability, the same conditional can be written as:

// Check for secure testing to be running, or if vehicle is stopped
// along with the engine not running. Then we can execute < ABC >
if (( Secure_Test == TRUE ) || 
 (( Vehicle_Speed == 0 ) && ( Engine_Speed == 0 )))
{
 // ABC…
}

1.1.2 Maintainability

Maintaining the code after it is written is a task that can become extremely difficult. Often the code may not make sense to others that look at it. This can lead to incorrect interpretation, so even though a new feature goes into the code, the existing code around it breaks. If someone besides the author comes into the code to make a change, and if they don’t understand the existing structure, then another “if” condition can be placed at the bottom of the code to avoid making any changes to its top part.

Consider using descriptive comments in the code to capture the “intent” of what is being done. Comments can help clarify its overall purpose later when the code is being updated and the person doing the updates needs a solid reference in terms of the structure of the code. For example, a comment of “Reset timer because if we are here we have received a properly formatted, CRC-checked, ping request message” is much better than “Set timer to 10 seconds.”

1.1.3 Testability

One of the key components for writing good software is writing software with testability in mind. To be “testable” (either for unit testing or debugging) each executable line of code and/or each execution path of the software must have the ability to be tested. Combining executable lines within conditional statements is not a good idea. If an equate or math operation occurs within an “if” evaluation, portions of it will not be testable. It is better to do that operation before the evaluation. This allows a programmer to set up a unit test case or modify memory while stepping through to allow a variety of options in choosing which path to take.

Consider the following code segment:

if ( GetEngineSpeed() > 700 )
{
 // Execute All Speed Governor code
}

For high-level source code debugging, it would not be immediately clear what the engine speed was while debugging. The tester could analyze the register being used for the return value, but it certainly is not readily apparent. Rewriting the code to use a local variable allows the variable to be placed into a watch window or other source analysis window. The code could be rewritten as follows:

current_engine_speed = GetEngineSpeed();
if ( current_engine_speed > 700 )
{
 // Execute All Speed Governor code
}

One argument for this could be program efficiency. This was certainly true years ago when embedded compilers were not very efficient in taking high-level source code and translating it to machine instructions. Today, with compiler optimizers written to look for optimizations via multiple passes through the code, most of these opportunities have been taken care of.

1.2 What Sets Embedded Apart From General Programming

The easiest way to evaluate what sets embedded apart from general programming is to look at the characteristics of an embedded programmer. The better embedded programmers tend to have a good working knowledge of hardware. They also are very aware of the resources they have, where bottlenecks could be in their system, and the speed associated with the various functions they need to perform.

There are varying definitions of what an embedded system is, but my favorite definition is “a system where the presence of a processor is not readily apparent to the user.” Because the processor itself is “hidden,” an embedded programmer concentrates on a set of performance and system requirements to complete specific tasks. As such, the software itself is just a part of the system, and the rest of the embedded platform around it is important as well.

An embedded software programmer keeps the following items in mind:

  1. 1) Resources. Every line of code and module that is written is scrutinized for the processing time it takes to execute as well as the amount of other resources (such as memory) being used. It becomes more difficult writing a tight embedded system using dynamic allocation languages such as C ++ and Java compared with programming languages like C and assembly.
  2. 2) Hardware features. Software is split between the hardware pieces of the embedded system that can execute them more efficiently as opposed to separating software by a software-only architecture. Interrupts, DMAs, and hardware coprocessors are key components in software design.
  3. 3) Performance. An embedded programmer has a keen sense of what the hardware can and cannot do. For processors that do not have floating-point units, mathematical equations and calculations are done using fixed-point math. The programmer also focuses on performing calculations that are native to the atomic size of the processor, so they shy away from doing 32-bit calculations on a 16-bit processor, for instance.

2 Starting the Embedded Software Project

One of the easier things to do is to start a fresh embedded project, as opposed to inheriting a project written a long time ago. Starting a new project is typically an exciting time and programmers look forward to starting something new. Promises to not repeat previous evils are recited by programmers. The software will be correctly completed first time around! Depending on how many projects exist or are being kicked off at a company, this event may not happen very often.

It is also the easiest and best time to get organized and determine how the software team should develop embedded software. No new source code has been written yet—though there may be libraries or core modules that are going to be pulled into the software baseline. This is the best time to determine how the project is going to be handled and to get buy-in from all the programmers that will be involved in the development cycle that will be followed.

It is a lot more difficult to institute new standards or development practices in the middle of a project. If faced with that situation, the best time to make any change is after some incremental delivery has been made. Changes to standards that try to take place “weeks before software delivery” typically add more confusion and make things worse. Unless there is total anarchy, or if the project can afford to have everyone stop, come together, and agree upon a new direction, then wait until after a major release delivery of some kind before making changes.

The following subsections consider software items that are discussed and agreed upon as a team (and written down!)

2.1 Hardware Platform Input

Although this chapter is dedicated to software programming and implementation guidelines, it is worth mentioning that there should have already been an opportunity to provide input to the hardware developers on software aspects. Items like hardware interrupt request lines and what they are tied to play a key role in the organization and the performance of the embedded software. Also, other resource inputs, such as memory size, on-chip vs. off-chip resources, type of processor being used, and other hardware I/O interfaces, are critical to embedded development.

Another key aspect is the debugging interface of the processor. An interface like JTAG may be perfect for hardware checking but may not have all the functionality that is available to a software programmer. Many processors (like those based on ARM™ cores) have a JTAG interface but also have a software-centric type of debugging interface using additional lines on the same chip. Bringing those out to a header for software development boards makes debugging and insight into the operation of the software much easier.

Because this chapter focuses on software programming guidelines, there won’t be any further discussion of this topic. However, make sure that the connection with the hardware developers is made early, or it could be very difficult to follow software implementation guidelines!

2.2 Project Files/Organization

There are three key components that go into project file organization. The first is identifying any dependencies that the project has on the configuration management (CM) system being used. Some CM tools prefer directory structures to look a certain way to increase interoperability with other existing systems. The second component is the compiler/debugger/linker suite that is being used for the project. The directory structure for some of the files for these components (like libraries) may need to be organized a specific way. The third is project file organization. Project file organization may be determined by team preference or a file organization that is prescribed by other embedded projects done by the same group or at the same company.

To make things easier for development, there should be a separation between the following items listed here. The most common way to separate these is by using subdirectories, or separate folders depending on the development environment.

2.2.1 Source Files Written Locally

This directory should contain all the source files that have been written by your development team. Depending on the number of modules being written or the size of the overall code base, consider further subdividing this into more subdirectories and folders. For multiple processor systems, it may make sense to separate by processor (such as “1” and “2”) and have another directory at the same level that contains files common to both.

An additional way to further subdivide a large source files directory is to subdivide it by functionality. Maybe dividing it into major feature groupings, such as “display,” “serial comm,” and “user IO,” would make sense. Indicators of a good project and good directory organization is if software falls into a category easily without a lot of searching around for it or if there are no arguments whether it belongs in one place or another.

2.2.2 Source Files From Company Libraries

This directory should either contain the software or links to the general repository where your company keeps libraries of source files useable in all projects. When doing links, it is important that some sort of control be in place so that new files just don’t show up every time the software is built. Version control needs to be kept tight to ensure no unexpected changes occur between the tested and released baseline. Links to specific versions of files work best. If the files must be physically copied into this directory with no links, it is very important to remember (and have written down) exactly which version was copied. To this end, periodic checking back to the library should be done in addition to checking for newer updates or bug fix releases.

The same approach applies to this directory or folder as mentioned earlier, that is, depending on the number of files being used it may make sense to break it down further into subdirectories or subfolders as well.

2.2.3 Libraries From Third Parties

There may be libraries that are used by third parties as well. There might also be source code—maybe an operating system or network stack that has been provided for you. It is critically important to have these files in a separate directory from the other source files! Programmers need to know that these files probably shouldn’t be changed, but there could be a tie-off that needs to happen with the software provider. If these are mixed in with the general population of source files that are written by the software team, there is a larger risk that they could be changed inadvertently.

Typically, there are files provided by third parties that are supposed to be changed. These may include definitions or links to pieces in the embedded system. For instance, one common entry is defining the number of tasks for an RTOS. Files that are supposed to be changed should either go in their own subdirectory in this group or be pulled over into a folder in the source files that your group is writing. Then privileges like “no modify/no write” could possibly be applied to the folder, to make sure they are not changed.

2.2.4 Libraries From Compiler/Linker Toolsets

There may be restrictions on where the libraries, that the compiler and linker toolsets provide, can be located. Typically, these can just be left alone. All developers need to agree up front which libraries are going to be used. The toolset company may include a full “C stdlib” available for use, or other alternatives like a smaller “micro” library that can be used instead. Trade-offs between the various libraries should be done, like whether the library allows reentrant library use, the functionality that is available, and the size of the library when linked in your embedded system.

There also may be options to remove libraries entirely from use. A common library that we often remove is the floating-point link library. So, library functions like a floating-point multiply (fmul) cannot be linked into the system. If a programmer has a link to this library, it won’t link, and the mistake can be corrected.

2.3 Team Programming Guidelines

How a team agrees to program the system and the criteria they will use for evaluating other programmers’ source code is something important to decide upon up front. If a programmer holds to a higher standard of software development, only becoming clear in the first code review after that programmer has already designed and written the code, then it is too late. The criteria for how a programmer can successfully pass a code review should be understood up front, so time isn’t wasted rewriting and unit-testing code multiple times.

Guidelines could include a variety of rules or recommendations. The more the guidelines are verifiable, the more successful they will be. For example, if a guideline for the programmer is that “the code is not complex,” it could be hard to verify, as the definition of complexity is largely subjective within a group of programmers. One may feel it is too complex, another may not. This guideline could be made verifiable if the word complex correlated to a set of measurable criteria.

To take this example a bit further, the group could decide to use a cyclomatic complexity measurement to evaluate a software module—the software is run through a tool that produces a complexity number for the module. Higher numbers represent more complex code according to the formula, lower numbers represent simpler. With a complexity formula that measures the number of “edges” and “nodes” in your software program, the simplest complexity represented by a value of “1” is a program that contains no “if” or “for” conditions and has a single entry and exit point. As the number of conditions and flows increase, the complexity increases. So, the evaluation criteria could change to “the code is not complex, cyclomatic complexity <= 18.” Thus it is no longer subjective.

What this is hinting at is a “checklist” of sorts that a programmer could use when writing and preparing his software code for review. Having the list of accepted programming guidelines up front that everyone follows makes expectations clear. The following are examples of items that could be on a “Software Guidelines Checklist” that would be evaluated for each module reviewed:

  •  Conformance to syntax standard.
  •  Cyclomatic complexity calculation.
  •  Number of source lines per function/file.
  •  Number of comments.
  •  Ratio of number of source lines to number of comments.
  •  Run through code formatter.
  •  Comment and design document understandability/matches code.
  •  Code under CM control is linked to a “change request.”
  •  No compiler warnings.
  •  Rule exceptions properly documented (if warnings ignored or don’t match standard).
  •  #pragma directives documented clearly in source code.
  •  Nonconstant pointers to functions are not present.
  •  All members of union or struct are fully specified.
  •  Data representation (scale, bits, bit assignments) clearly documented.
  •  Data defined and initialized before being used.
  •  Loop bounds and terminations are correct.
  •  Mathematical operations correct (no divide-by-zero, overflows).
  •  No deadlocks, priority inversions, reentrant faults.

2.4 Syntax Standard

There are a variety of ways a coding syntax standard can appear. A syntax standard defines the way code is spaced, capitalized, and formatted when written into source code. Personal preference needs to be considered when using a syntax standard for a group. There may also be a mix of syntax rules a group could incorporate on a project—there may be some rules that are not mandatory but are recommended. This section contains some ideas about how this might look. The most important thing is getting the developers to agree on a given standard and ensuring they stick to it throughout. If the project is reusing quite a bit of code, preference should be given to the standard that the existing code uses.

This section has some ideas about how the syntax standard could be developed. There isn’t a right or wrong here—apart from if developers on a team are all doing something different. In such cases, this impacts the ability to review the code or go in and easily make changes. If the code is developed by all team members using the same syntax, then it is much easier to change, as well as understand, when reviewing the work.

Adopting a set of coding standards is also desirable. One such standard set is “MISRA-C” (Motor Industry Software Reliability Association), which defines rules that C and C ++ source code should follow to be reliable, secure, portable, and safe. Though it was developed for the transportation industry, it has been accepted widely outside this sector, especially when safety elements come into play. Versions of the full standard exist from “MISRA C:1998” through to the latest “MISRA C:2012,” incorporating various amendments that are a bit newer. The other benefit to using a standard such as this is that there are a variety of automated tools that can be run to check compliance of the code to this standard.

Subsequent sections in this chapter outline some of the syntax-oriented coding standard items that can be found.

2.4.1 Code WhiteSpace

The following are examples of how various software lines can add white space to increase the readability of the code itself. All these examples are operationally equivalent—they produce the same machine code. They are listed in order of the amount of white space they use:

int i;
for(i = 0;i < 20;i ++)
{
 printf(“%02u”,i*2);
}

int i;
for ( i = 0; i < 20; i ++ )
{
 printf( “%02u”, i*2 );
}
int i;
for ( i = 0; i < 20; i ++ )
{
 printf( “%02u”, i * 2 );
}

The examples above concern themselves with the white space that is between the various operators and numbers on a given line of source code. Numerous studies indicate that more white space increases readability in software code. This would support using the third example outlined above. However, if the amount of white space causes the software to wrap to the next line, then too much white space has been used, because wrapping is very unreadable.

2.4.2 Tabs in Source Files

Most syntax standards indicate that tab characters should not be used in source files when writing code. This is because the tab character could be interpreted differently by source editing tools, file viewers, or when it is printed. They are also not readily visible when editing. Source code editors typically provide a way to substitute spaces with the tab character. So, while programming, when the tab key is hit, it automatically replaces the tab with X number of spaces.

This brings about an important point. How many spaces should represent a tab key press or a normal indent in source code? Most editors have a substitution for either “3” or “4” spaces per tab. Either is fine—again, this will be based on some personal preference in addition to how the rest of the code is formatted. In terms of improved alignment, the amount of indent space depends on the spacing that is used for other things, like the “for” loop spacing identified earlier.

2.4.3 Alignment Within Source

How things are aligned in source code makes an impact on readability as well. Take into consideration the following two operationally equivalent sections of code:

int incubator = RED_MAX; /* Setup for Red Zone */
char marker = ‘’; /* Marker code for zone */

int incubator = RED_MAX /* Setup for Red Zone */
char marker = ‘’; /* Marker code for zone */

White space is used on the second example, lining up the variable names, initialization values, and comments on the same column for the code block.

The examples above were quick examples to demonstrate how different code syntax with white space can be used. Consistency and readability are key components to writing good embedded software source code.

2.5 Safety Requirements in Source Code

When writing safety-critical software, the implementation guidelines for software source code change. Many considerations need to be made when developing this code.

Is all the code in your system safety-critical? If a system is safety-critical, it may not actually rely on all the code to be safety critical. The system itself needs to have fail-safe operations in place so that things fail to the least permissive case, as defined by FMEA analysis. There may be operations like logging that are not required to be safety-critical since they cannot cause the safety-critical code in the system to act in an unsafe manner.

Documentation of safety-critical sections of code is important. Special care and consideration should be given to mark these sections differently, or even have comments that refer directly to the safety case or documentation that the code adheres to. Using all capitals such as “SAFETY-CRITICAL CODE SECTION START” in a comment section certainly alerts programmers, who might be changing code or adding new requirements, to the fact that they should tread lightly in these sections.

As discussed above, development standards, such as “MISRA C” (Motor Industry Software Reliability Association) and “MISRA C ++,” can also help facilitate writing code that operates in a safe manner. There are many users of the standard outside the automotive and transportation industries, including medical and defense users. There are also many tools that can check source code for MISRA compliance that can be included as part of the overall software build process. Picking up and using this standard can be helpful for implementation.

There may be special programming requirements for safety-critical sections of code. There may be a separate development guideline list, that includes things like performing a software FMEA on the safety-critical code section being implemented. There also may be additional reviewers in the code review itself, such as representatives from a safety team or a software engineer that specializes in safety-critical code development.

The following are additional factors or checklist items that could be considered a part of safety-critical code development:

  •  Adherence and checking to a standard, such as MISRA C or C ++.
  •  Safety sections clearly marked to standard.
  •  Data that is safety-critical incorporates “safe” or similar wording in the variable name.
  •  All safety-critical variables are initialized to the least permissive state.
  •  Any safety-critical data is clearly marked as stale and/or deleted after use.
  •  Comparisons between safety-critical data are handled correctly.
  •  All paths are covered when variables are used for path decision making.
  •  Checks are in place to make sure safety-critical code is executed on time.
  •  Periodic flash and RAM checks are done to check hardware correctness.
  •  Safety-critical data is protected by regular CRC or data integrity checks.
  •  “Voting” mechanisms between software and processors is done correctly.
  •  Safety dependencies on functions (like a watchdog timer) are checked periodically for correct operation.

More details on safety-critical software development are outlined in Chapter 11 (Safety-critical Development).

3 Variable Structure

3.1 Variable Declarations

One of the key components for developing an embedded software system is determining how the data in the system will be declared and used. To discuss each type of variable declaration, it is probably best to break them down by type. The three primary types of variables in a system are global variables, file-scope variables, and local variables.

3.1.1 Global Variables

Global variables are variables that are visible to any linked component of the system in a single build. They could be declared at the top of a source file but could also be present in header files where the variable is declared in one spot, and then made available as an extern to any other file that includes that header file. There certainly is an entire philosophy associated with global variables—some programmers hate them, and software leads have been known to ban them.

There are differing opinions on the usage of global variables. Programmers can define a correct and “right” way to use them, if they don’t help foster the creation of unorganized (spaghetti) code. There are a couple of guidelines that could be used to allow global variables into your system, as this will typically help increase the performance of the system without using access functions to modify encapsulated local data.

The first is to declare the variable in a header file. Anyone that includes the header file would then have access to the variable, but it also helps make sure that if the global was declared as an unsigned integer all the extern references would match. The header file (ip.h) would look something like this:

#ifdef IP_C
#define EXT
#else
#define EXT extern
#endif

EXT uint16_t IP_Movement_En
EXT uint16_t IP_Direction_Ctrl

#undef EXT

The example above would need each of the source files to declare a definition of their “filename_C” for the variable to be declared. The source file (ip.c) would look like this:

#define IP_C
#include “ip.h”
#undef IP_C
#include … /* Rest of the include files needed by the source file */

By declaring the variable in a header file, the type will be correct, and identifying who is including the header file will also provide a good indication of who might be looking at this variable. Using this type of method could also allow the team to dictate that no global variables are declared in source files—they would be declared in this manner only.

The second recommendation for using global variables is to always prefix the name with the “owner” of the variable itself. In the example above, IP stands for “Input Processing.” So, any variable used with global scope of IP_xxx is a variable declared in the input processing header. This helps by not having a bunch of random names floating around for variables.

The third recommendation that would help make global variable usage easier relates to the second recommendation mentioned above. After a global variable is declared in a header file, the only program that could modify that variable would be an input processing source file, like “ip.c”. Other source files would have “read” access to that variable, but not be allowed to change its value. Of course, the compiler would allow the programmer to change it—but if this was a rule the project team wanted to use it would be easy to find in a code review. Any instance of a variable prefixed with the “ownership” shouldn’t be modified by another program. Consider the source lines below in output processing (op.c):

if ( IP_Movement_En == TRUE )
{
 if (( IP_Direction_Ctrl == IP_FORWARD ) ||
 ( IP_Direction_Ctrl == IP_REVERSE ))
 {
 OP_Display_Movement = TRUE;
 IP_Display_Shown = TRUE; /* Unacceptable… */
 }
 else
 {
 OP_Display_Movement = FALSE;
 }
}

In the example above, we do not want to modify an input processing variable following the third recommendation. This hopefully would be easy to see during a code inspection or review. Instead, consider having input processing figure this out by looking at the variable OP_Display_Movement. If this cannot be done, then a function call from here to an input processing function, having that function change IP_Display_Shown, may work. For debugging purposes, and to try to keep the code organized, having a rule like this in place can make global variables a lot cleaner.

The final recommendation for global variables, in addition to showing “ownership” of the variable by prefixing the source file indicator, is to capitalize each letter in the variable name. This would be a further indication that the variable is a global variable that could be read at multiple places, so changes to meaning, scaling, or size could cause a ripple effect to propagate throughout the system.

3.1.2 File-Scope Variables

File-scope variables are used to share data between multiple functions in a single source file. They are easier to use than global variables, because typically there is a single owner in a particular source file, at least when it is initially written. File-scope variables make it easy to share data between functions, without having to pass them as arguments on the stack.

A key recommendation is to keep the keyword “static” in front of each of the file-scope variables being declared. This keeps them from being used by other files and keeps things local. One issue with this is visibility on the map (or possibly even the debugger) file. For compilers, if a variable is declared in a file without visibility to other files, there isn’t a need to put a reference to it for linking. Sometimes it is nice to be able to see such a variable during source line debugging or peeking at memory while the system is running.

To give such variables visibility during debugging, consider declaring the file-scope variables in the following way in a source file:

STATIC uint32_t IP_time_count;
STATIC uint16_t IP_direction_override;

This “STATIC” definition would then reside in one of the “master” header files in your system. Further discussion of this type of header file (portable.h) is provided in Section 3.2. When compiling the debugger version of your code, the programmer can define a keyword “DEBUG” for those source files—when it is time to release the code the keyword “DEBUG” is not defined. This is particularly useful if there are special setup steps that need to be completed (like turning on a debug function in the microcontroller at initialization). With this type of setup, the following lines would appear in the common header file:

#ifdef DEBUG
#define STATIC
#else
#define STATIC static
#endif

Another recommendation for file-scope variables is the use of capitalized and lowercase characters. Consider prefixing all the file scope variables with the same “filename” or feature set indicator at the front, then making all other letters lowercase. In this way, it will be easy to discern between a global variable and a file-scope variable.

3.1.3 Local Variables

Local variables have the easiest recommendations of all types of variable. The first recommendation is to drop the prefix mentioned in the previous two sections, because it is just a variable within the function. Second, with the use of decent comments, the variable names for local variables really do not need to be overly descriptive. In my opinion, it is alright to have variable names such as n, i, j, etc., when using them to index arrays and loop variables. Even a simple variable like “count” is OK—again if there are comments to let an observer know what the function is trying to do.

The other type of local variable in a function is one with the keyword “static” in front of it. This is used when a variable needs to retain the data through multiple calls of the function, but it is not shared by any of the other functions in a file.

For local variables, consider keeping them as all lowercase. In the case of a “static” variable declared in a function, consider capitalizing the first character. In that way, when looking through the function or maintaining it later, the variable retains its value. The following is an example of how function local variables can look:

static void ip_count_iterations( void )
{
uint16_t i, j, n;
static uint16_t error_count_exec = 0;
uint32_t *reference_ptr;
. . .

3.2 Data Types

One of the key attributes for embedded systems is resource management. In the preceding sections, the declarations that were made were using type definitions. To keep an embedded system portable to other processors, and to keep resources in check, type definitions can be used for various data types. The following is a list of type definitions that can be declared in a master header file, that will be included by all source files.

Consider a file called “portable.h” which is included by the source files:

  typedef unsigned char uint8_t;
  typedef unsigned short int uint16_t;
  typedef unsigned long int uint32_t;
  typedef signed char int8_t;
  typedef signed short int int16_t;
  typedef signed long int int32_t;

Because an “integer” size is dependent on the microcontroller architecture size, the programmer can use the type definitions above and then only change the file if porting to a different platform. Library templates can also be written using the type definitions provided above, so that when they are pulled in and used on any platform they work correctly. Another variation of the same concept above is to shorten the type definitions, to save some white space when writing the source files. A variation of the definitions above is shown here:

  typedef unsigned char UINT8;
  typedef unsigned short int UINT16;
  typedef unsigned long int UINT32;
  typedef signed char INT8;
  typedef signed short int INT16;
  typedef signed long int INT32;

Building on the same naming convention, when structures are declared, consider adding a suffix “_t” to identify it as a type definition. An example of a structure declaration is shown here:

#define DIO_MEM_DATA_BLOCKS 64
typedef struct
{
UINT16 block_write_id;
UINT16 block_write_words;
UINT16 data[DIO_MEM_DATA_BLOCKS];
UINT16 block_read_id;
UINT16 startup_sync1;
UINT16 startup_sync2;
} DIO_Mem_Block_t;

Following the same convention, a union type definition is shown here:

typedef union
{
UINT16 value;
struct
{
UINT16 data :15;
UINT16 header_flag :1;
} bits;
} DIO_FIFO_Data;

For the struct and union examples provided earlier, type definitions are used for data sizes as discussed previously. Spacing and white space is decided upon by the programmers—having it maintained uniformly across all source files adds to maintainability.

Another thing to notice is that type definitions mentioned earlier are prefixed with “DIO_” and contain a mix of capital and lowercase letters. This is a stylistic choice. One thought process is to have type definitions in header files declared in this manner and file-scope type definitions all lowercase, without the need for a prefix. As discussed in the “Variable Declarations” section, this can help a reviewer understand if the structure is something that may be global in scope or just local.

3.3 Definitions

3.3.1 Conditional Compilation

Another topic in developing embedded software is the use of conditional compiles in the source code. Conditional compiles allow a compiler to dictate which code is compiled and which code is skipped. There are many books written for software engineering that suggest conditional compiles should not be used in the code.

For hardware-oriented code that is written to work on multiple processors in a system, there may be conditional compiles to specify “Processor A” vs. “Processor B.” For software source code, if > 15% of the source code has conditional compiles in it, consideration should be given to splitting the code, keeping the common code in one file and separating the reason for the conditional compiles between two (or more) files. As the number of conditional compiles increases the readability decreases. Files with minimal conditional compiles are likely easier to maintain than a file that has been branched or separated, but again, as the number of conditional compiles increases past 15% the maintainability drops.

Consider the following source code section for a module written to run on two processors specified as PROCA and PROCB. Depending on which makefile is selected, the compiler defines one of these two values depending on the processor target.

    frame_idle_usec = API_Get_Time();
#ifdef PROCA
/* Only send data when running on processor B */
ICH_Send_Data( ICH_DATA_CHK_SIZE, (uint32_t *)&frame_idle_usec );
#else
#ifdef PROCB
/* Nothing to send with processor B in this situation */
#else
/* Let’s make sure if we ever add a PROCC, that we get error */
DoNotLink();
#endif /* PROCA */
#endif /* PROCB */

There is one additional thing to note with this code. In this example, we do not simply just look for processor A and then do nothing if we are not processor A. There is an else condition, so that if we ever run on a processor besides A or B, a made-up function “DoNotLink()” will be called, which will result in a compiler warning and a linker error (the function doesn’t exist). In this way, if another processor is added in the future it will force the software engineer to look at this code to see if a special case should be added for this new processor. It is simply a defensive technique to catch various conditional compiles that may exist in the source code baseline.

3.3.2 #Define

A commonly used symbolic constant or preprocessor macro in C or C ++ coding is implemented using the    #define.

Symbolic constants allow the programmer to use a naming convention for values. When used as a constant, it can allow better definition as opposed to “magic numbers” that are placed throughout the code. It allows the programmer to create a common set of either frequently used definitions in a single location, or to create more singular instances to help code readability.

Consider the following code segment:

// Check for engine speed above 700 RPM
if ( engine_speed > 5600 )
{

The code segment checks for a value of 5600. But where does this come from? The following is a slightly more readable version of the same code segment:

// Check for engine speed above 700 RPM
if ( engine_speed > (700 * ENG_SPD_SCALE ))
{

This is a little better as it uses a symbolic constant for the fixed-point scaling of engine speed, which is used throughout the software code baseline. There certainly should not be multiple definitions of this value, such as having ENGINE_SPEED_SCALE and ENG_SPD_SCALE both used in the same code baseline. This can lead to confusion or incompatibility if only one of these scalar values is changed. The code segment above also has “700” being used. What if there are other places in the code where this value is used? What is this value? The following code segment is more maintainable and readable:

// Check for speed where we need to transition from low-speed to all-
// speed governor
if ( engine_speed > LSG_TO_ASG_TRANS_RPM )
{

A #define would be placed in a header file for engine speed for visibility to multiple files, or in the header of this source file if it is only used in a file-scope scenario. The #define would appear as:

// Transition from low-speed to all-speed governor in RPM
#define LSG_TO_ASG_TRANS_RPM (UINT16)( 700 * ENG_SPD_SCALE )

This has the additional cast of UINT16 to make sure that the symbolic constant is a fixed-point value so floating-point evaluations are not made in the code. This would be important if the transition speed was defined as 700.5 RPM, or even if a value of 700.0 was used as the transitional speed. Once floating-point values appear in the code, the compiler tends to keep any comparisons or evaluation using floating-point operations.

Preprocessor macros allow the programmer to develop commonly used formulas used throughout the code and define them in a single location. Consider the following code segment:

Area1 = 3.14159 * radius1 * radius1;
Area2 = 3.14159 * (diameter2 / 2) * (diameter2 / 2);

The code listed above can be improved by creating a preprocessor macro that calculates the circular area as opposed to listing it in the code. Another improvement would be to use a symbolic constant for PI so that the code can use the same value throughout. That way if additional decimal places are used, it can be changed in one location. The following could be defined at the top of the source file, or in a common header file:

#define PI 3.14159
#define AREA_OF_CIRCLE(x) PI*(x)*(x)

The code could then use this preprocessor macro as follows:

Area1 = AREA_OF_CIRCLE(radius1);
Area2 = AREA_OF_CIRCLE(diameter2 / 2);

The code segments shown above could be used for a higher end microcontroller that has floating-point hardware or for a processor where floating-point libraries are acceptable. Another implementation could be in fixed-point, where tables would approximate PI values so that native fixed-point code could speed up processing times.

Content Learning Exercises

  1. 1. Q: Why is it important to add comment to code?
    A: The comments can help clarify its overall purpose later when the code is being updated and the person doing the updates needs a solid reference in terms of the structure of the code.
  2. 2. Q: What items should have the ability to be tested to consider the software testable?
    A: Each executable line of code and/or each execution path of the software must be able to be tested.
  3. 3. Q: What are conditional compiles used for and why is their use recommended to be kept to a minimum?
    A: Conditional compiles allow a compiler to dictate which code is compiled and which code is skipped. As the number of conditional compiles increases the readability of the code decreases. Files with minimal conditional compiles are likely easier to maintain than files that have been branched or separated.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset