Today we're going to learn about using the preprocessor and how it can help with improving our software quality. You will learn about many different preprocessor directives and how these can be used to help create compile time switches. Then we will go through an example of how to invoke the preprocessor and analyze the output. The preprocessor is the first step in our build process. A preprocessor does not do any translation or generation of architecture specific code. It does however, help us with providing specific control of compilation from within files, as well as provide macros that can help with code reuse and readability. The preprocessor provides a special keywords called preprocessor directives. These directives begin with a number sign and have many functions. They can be used to define constants or features, as well as define macro functions. Macro is a term often used interchangeably with the application of a preprocessor directive. These directives are represented with the #def or the #undef keywords. Conditional compilation can also be performed with the compiler using the #if or #else directives. When combining with the # to find directives and conditional compilation, you can provide compile times which functionality into your build. Directive can also be used to include header files with the #include directive. There are directives such as #warning and #error for stopping or printing warning messages in compilation. Lastly, there's a pragma directive which allows us to provide instructions to the compiler through the code rather than at the command line. Let's look at each of these in more depth. The preprocessor is bundled with the gcc application. You may remember that we said the preprocessor takes your original files and transforms them based on the directives you have defined within a file. You can think of this transformation as a search and replace for many directives. Just like how you provided options to stop the compiler before linking, or told the compiler to output C to assembly translation, you can tell it to stop at the preprocessing stage. To do this we need to provide the -E option. By running this command the preprocessed C file has been modified and output to main.i. If you look in this file you can see many declarations and definitions have been added to the top of the main.i file. Let's look at some examples of how the search and replace worked for #def and #include directives. First, let us talk about using the #def or #undef directive to define constants, features, and macro functions. It's bad practice to put random constants around in your code. Other engineers will look at your code and potentially not understand the numbers you chose. Also, if you ever need to make a change, many random constants throughout your code will be hard to maintain. In this case you can define a macro constant as a readable string and give it a MACROS-VALUE, which could be a constant, a string, or even a C statement. Every time you change that defined value, it will be reflected in every use of the macro in your code. For instance, in this example two macro constants get substituted into two different spots in a file with which they are used. The engineer only needs to worry about the original file. And the preprocessor will take care of the rest. If there is a bug in your preprocessor definition, you will not normally see it as the preprocessor output usually goes directly into the compiler. A good example of this is combining C operations with a macro function. Let's go do an example where the macro function works fine and where it also could introduce unwanted behavior. The define directive can also apply to creating a macro function. Let's say you have a specific math formula that is used constantly in your code. You decide it would be better to replace that function everywhere with a macro, so you only need to reedit one formula. To define a macro function, you provide a unique name, potentially some parameters, and then the C operations you wish to perform on those parameters. You can see in this example, the SQUARE function gets substituted out for the equivalent C operation you wrote with the macro function. While the SQUARE is not the most interesting example,, you can apply this concept to match more complex formers and even macro functions with multiple parameters. But you need to be cautious with doing this, as you can introduce some pretty interesting bugs. Because the macro expansion does a direct substitution, you can introduce bugs by operating C operations as an input instead of just values. For instance, if instead we passed in y ++ and the SQUARE instead of y, the y ++ would not just get a post increment once, it would be repeated twice during the substitution. This behavior is actually undefined, and you may run into multiple instances or behaviors undefined. This expansion yields two y ++ operations. And instead of getting a result of y equals three, and a square of four, we get y = four and a square with 6. You don't have to define a macro with any value. Instead you can use just a macro as a Boolean expression to represent a feature. These help us to find a true or false condition that the preprocessor will evaluate. This Boolean directive is most useful in conjunction with the if-else directive for creating compile time switches. The if-else directives should not be new to you as you have likely seen them in code, especially in header files. These will be used to perform conditional operations at compile time. The if-else directives are just like if-else statements in C programs. There's a specific if-else directive that provides a negative test if not defined. A major difference though, is that instead of using brackets, use a #endif to conclude a conditional block. These can be used for debugging and excluding large sections of code from compilation. Here's an example where we have some code inside a preprocessor director that helps us switch between a MSP or a Kinetis platform. If the feature macro, MSP_PLATFORM is defined, the preprocessor will include a specific MSP startup routine. On the other hand, if the Kinetis feature is defined, then the Kinetis start up routine will be included and compiled. If neither of these are defined, then we'll use another preprocessor directive to alert the engineer that an error must have occurred and compilation should be stopped. The compiled output while I have no idea that these functions that weren't included even exist, as they will be removed from the preprocessor. Next is the #include directive, this is used to include and reference other files where you may have software routines in. When you do a #include, the preprocessor actually does a similar copy and paste that we saw at the #def directive. However, it does it with the whole contents of the header file you were including from. Copies the header file contents into the top of file where pound include pre-processor directive was written. There was other information that is added. But we can look at this with a simple example. Here we have two files, a header and an implementation file. The head file contains a macro and a function declaration. The C file contains a function definition, an array declaration, and an include statement for the header file. When you pass this file through the preprocessor, you will see similar preprocessor output on the right. The last directive we will discuss is the pragma director. This is used to provide specific instructions for the compiler from the code itself and not the command line. Pragmas are important if you want to provide a specific option for a specific piece of code that general compile options should not do. A common choice in embedded software is to enforce a no printf use in your code. You can provide a pragma directive to tell the compiler to throw an error if this function was used somewhere in the code. Let's look at an example of combining these directives to create a compile time switch. This is a method of controlling what content in your source files is compiled. You've heard of run time switches before. These are conditional boxes of code, like a if-else statements, that can change which is executed by a run time parameter. This parameter gets stored in a variable. The compile time switch is the same thing but provided the control at the step of compilation or preprocessing. By providing a specific option and compile time, the compiler will integrate architecture specific code into an executable. We do this by adding a -D with a macro name immediately following. This is equivalent to defining the macro in one of the source files that allow for dynamic changes at the command line without modifying software. You might ask yourself, why is this important? Compile time switches allow us make our software more portable without maintaining different software repositories. For instance, a compile time switch can allow us to use the same code for different embedded platforms. Imagine you have a handful of different embedded systems running the same applications. Some could have two different versions of the processor or maybe a different micro-controller all together. You can see here are two hardware platforms with similar software except for the UART library. Regardless of the platform, the code is in the same software repository. We then have some preprocessor directives in a device library file and this chooses the right platform file to include. Based on a compile time switch or defined peram, we can include a specific firmware library for interacting with UART. Everything above this software layer is the same across the two architectures. This design has provided a very portable top layer of software with a very flexible firmware layer that different versions of code can be compiled given an intended platform. While directives are very useful there are some issues with using them. They don't perform type checking. They can potentially increase bugs. And they definitely increase your code size. But they do offer less overhead and greater improvements for writing maintainable and readable programs. There have been some new C features in newer C standards that provide similar functionality. But macros are still widely used in software projects, especially with helping to design portability and platform independence into your software.