Welcome to the Embedded Systems Programming course. My name is Miro Samek and in this
lesson I'll show you how to improve the "blinky" program by using the C preprocessor and the
"volatile" keyword.
As usual, let's start with making a copy of the previous "lesson4" project and renaming
it to "lesson5". If you are just joining the course, you can download the previous projects
from state-machine.com/quickstart.
Get inside the new "lesson5" directory and double-click on the workspace file to open
the IAR toolset. If you don't have the IAR toolset, go back to "lesson0".
So, this is the program you created in "lesson4". It works in that it manages to blink the RED
led on the Stellaris Launchpad board, but it is certainly is not very readable, because
it is full of cryptic numbers and there are no comments to explain what's going on.
To improve the readability of the code, it would be very nice to use names for the registers
instead of the criptic numbers. One way you can achive it, is by means of the C preprocessor,
which lets you define any piece of code as a macro.
For example, let's define a macro for the first register you write-to. Start a new line
with the #-sign followed by the word "define", followed by the name of the macro. The data
sheet calls the register Run-Mode Clock Gating Control Register for GPIO, so let's name the
macro RCGC-GPIO.
After the name you simply paste the piece of code this macro is going to replace.
Once the macro is defined, you can use it instead of the original piece of code.
Press F7 to check if the compiler accepts your code so far.
The C pre-processor is called that way, because it is conceptually a separate first step of
simple text substitution before the real compilation. The pre-processor removes all lines starting
with the #-sign, so the compiler does not see them at all. For example, you can define
a macro to be anything, but as long as it is not used in the code, it doesn't matter
and the code still compiles.
Also, the preprocessor replaces only the macros that are actually used in the code, so the
compiler sees only the replacement sequences of characters and never the macro names themselves.
This means that a macro does not need to be any complete element of the C language. For
example, a macro FOO is just a piece of the pointer-cast expression, but as long as the
text substituted for the macro makes sense in the specific context, the compiler will
happily accept it, because really the compiler cannot tell the difference.
The corollary of all this is that you need to be careful how you define your macros,
so that their meaning will not change unexpectedly depending on the context in which they are
substituted. For example, to avoid surprises, it is always a good idea to enclose the macro
like RCGC-GPIO in parentheses, so that it always means de-referencing of a pointer in
any context it might be used.
It is also possible to define macros using other macros. For example, if you define the
macro GPIOF_BASE as it is specified in the Data Sheet, you can use it in the definitions
of other macros,
such as GPIOF_DIR for the pin direction register with the offset 0x400 from the base address,
GPIOF_DEN for digital enable with the offset 0x51C from the base address ,
and GPIOF_DATA with the offset 0x3FC from the base address.
Finally, it is always highly recommended to add comments to your code. Comments are only
for the benefit of humans, who read your code, and are completely ignored by the compiler.
The C99 Standard supports two types of comments: the traditional C comment delimited by /* */, such
as this one:
and the C++-style comment starting with // and terminated by the end of line, such as these
ones
Comments are allowed everywhere where you could legally place a space, and in fact,
all comments are replaced with a single space before the compilation. Both types of comments
can also be used in the macro definitions.
So, now it will be interesting to see if this code still blinks the LED. To actually see
this, I'm going to use the Stellaris Launchpad board. But If you don't have the board, configure
the Debugger for Simulator and follow along.
Great, the LED blinks as before, so all your changes seem to work.
Let's examine in some detail how the compiler translated the GPIO_DATA macro and weather
or not it introduced any overheads. After all, you might be concerned than now the CPU
needs to add the base address to the offset at runtime.
But when you step through the code, you can immediately see that the LDR.N instruction
loads directly the full address 0x400253FC into R0 without performing any additions.
In other words, the code is as efficient as before, because the compiler folds any constants
computable at compile time and avoids unnecessary computations at runtime.
Finally, it is very interesting to see which single instruction actually turns the LED
on. It turns out that this is the STR instruction. In other words, from the CPU point of view,
talking to the outside world is fundamentally very simple and boils down to writing a specific
value into a specific address.
OK, so your program still works and is as efficient as before. But I don't want to leave
you with the impression that you have to define macros for all the registers yourself. In
fact, typically you don't need to, because the microcontroller vendors, such as Texas
Instruments in the case of the Stellaris board, already provide the macros for you in a separate
file, which I've copied to the lesson5 directory.
You can add this file to the project by right-clicking on the project and choosing the Add->Add Files
menu option. The file name is "lm4f120h5qr.h", which corresponds exactly to the microcontroller
type on your Stellaris Launchpad board. The file extension ".h" means that it is a header
file, specifically designed for inclusion into ".c" files, such as main.c.
When you open the header file, you can see that it contains a whole bunch of macros very
similar to what you have defined yourself. However, the pointer casts used in the header
file are significantly different, which requires some explanation.
Let me grab one macro from the header file and copy it to the main.c file for comparison.
The first discrepancy is the pointer type. The macros in main.c use "unsigned int", while
the header file "unsigned long". I will discuss data types in a separate lesson, but for now
let me only say that on a 32-bit machine, like the ARM processor, the int-type is 32-bit
wide, so is the long-type. So, "unsigned int" and "unsigned long" are equivalent.
So, the real difference is the "volatile" qualifier. It informs the compiler that the
object pointed-to by the pointer might change spontaneously.
When you declare an object to be "volatile", you are telling the compiler that the object
might change even though no statements in the program appear to change it. For example,
two bits in the GPIOF register on the Launchpad board are connected to the user switches.
These bits can be changed when the user presses or releases the switches, which is obviously
not caused by any program instruction. Therefore, the GPIOF register, and in fact most other
I/O registers, are volatile.
This is important, because the compiler can optimize access to non-volatile objects by
reading an object's value into a CPU register, working with that register for a while, and
eventually writing the value in the register back to the object.
The compiler is NOT permitted to do this soft of optimization with "volatile" objects. Every
time the source program says to read-from or to write-to a "volatile" object, the compiler
has to do so.
So clearly, the "volatile" qualifier is useful for I/O registers such as GPIOF. But it can
also be very useful for "normal" variables to prevent optimizations that the compiler
otherwise might do.
For example, the "counter" variable is used only in the two delay loops. But if you look
at these loops from the compiler's perspective, they make no contribution whatsoever to the
computation, because the final value of the "counter" is either overwritten or discarded.
In this case, the compiler is permitted to optimize both delay loops "away".
You can actually see this quite easily by allowing a higher-level of optimization as
follows.
Click on the Project options, choose "C/C++ Compiler" section and click on the "Optimizations"
tab. Select "High" optimization level and click OK.
Recompile and run the program on the Launchpad board.
As you can see, the LED lights up and stays on, all the time...
When you single-step through the code, you can see that the instructions for turning
the LED on and off are still there, but the delay loops between them are gone.
But now, that you know about the "volatile" keyword, you know how to prevent the compiler
from optimizing the delay loops away.
You need to make the "counter" variable "volatile".
By the way, the "volatile" keyword can be placed either before the type, like in the
macro from the header file, or after the type. I recommend placing it after the type.
Let's quickly test weather the "volatile" counter indeed fixed the problem.
Yes, the LED blinks...
You can also see the delay loop when you single-step through the code.
So now, let's make use of the .h header file by actually including it into the main program.
Again, you use the preprocessor for this.
To include a file, you start a new line with the #-sign, followed by the word "include"
followed by the name of the file in double quotes.
Let's put the header file side-by-side with the main program to replace all the macros
you've defined so far with the macros from the header file.
The header file provided by the microcontroller vendor uses the register names from the Datasheet,
so you should have no trouble to recognize the interesting registers, such as:
GPIO_PORTF_DATA
GPIO_PORTF_DIR
and GPIO_PORTF_DEN
If you have any doubts about the correct name of the resister, you can always check it's
address to make sure that this is what you want.
After replacing all the macros, you can remove your own definitions and recompile the code.
Let's test the code one last time to check if the LED will still blink...
This concludes this lesson about the C preprocessor and the "volatile" keyword. From now on, you
are able to write programs that will run correctly at any level of optimization. So congratulations!
In the next lesson you will learn how to use the bit-wise OR and AND operators to blink
other colors of the composite LED and you will also learn about the more advanced features
of the GPIO register.
If you like this channel, please subscribe to stay tuned. You can also visit state-machine.com/quickstart
for the class notes and project file downloads.