Welcome to the Embedded Systems Programming course.
My name is Miro Samek and in this lesson I'm going to talk about variables and pointers.
Let's start with making a copy of the previous lesson2-project and renaming it to lesson3.
If you don't have the lesson2-project, you can get it online from state-machine.com/quickstart.
Get inside the new lesson3 directory and double-click on the workspace file to open the IAR toolset.
If you don't have the IAR toolset, go back to lesson 0.
So, here is the C program you've created in lesson 2. Let's clean it up a little bit and
have a quick look in the debugger to see where the counter variable lives and how it is accessed.
As you step through the code and watch the Locals window, you see that the counter variable
lives in the R0 register and is accessed by the machine instructions directly.
Now, let's move the variable definition outside the main function, recompile, and go back
to the debugger.
Interestingly, the variable counter is no longer visible in the Locals window. This
is because it is no longer local.
To see the counter variable now we need a different view. Select the View menu and click
on Watch, Watch1 view.
When the watch window opens, click on the first line and type the name of the variable:
counter in out case.
As you can see, the location of the counter variable is now a big number starting with
0x2. This address is the beginning of the Random Accessed Memory in this particular
ARM Cortex-M microcontroller, so the counter variable lives now in RAM.
If this is really the case, than the variable should be also visible directly in the memory
window. To check this, set the memory view to 0x2000000 and change the memory setting
to 4x unit, to conveniently see 4-byte integers.
Now, let's single step through the code and watch the debugger views.
Note that the STR instruction caused change from zero to one in the
e Watch 1 view and the memory view. Now to 2... Now to 3...
Let's try to understand the machine instructions that the compiler has generated to access
the counter variable in memory. The first interesting instruction is the LDR, which
stands for load from memory to a register. The LDR.N instruction loads something from
the label ??main2 to the R0 resiter.
You can actually Scroll down to this label to see what's being loaded. Hey, this looks
familiar. The value loaded to R0 is the address of the counter variable. Let's exectue the
LDR.N instruction to watch R0.
The next LDR instruction loads R0 again, but this time the value comes from the address
that the R0 currently holds, which is the address of the counter variable. Single step
and verify that R0 now has the value 3.
The ADDS instruction does the actual work of incrementing R0 by 1, so R0 becomes 4.
The next LDR.N insruction loads the address of the counter variable to R1.
And finally, the STR instruction stores the value of the R0 register to the memory pointed
to by the R1 register. Please note how the Watch1 and memory views change after this
instruction executes.
At this point, I hope that you start to see the general pattern of accessing memory in
the ARM processor. ARM is an example of the so called
Reduced Instruction Set Computer (RISC) architecture, where memory can be only read by the specical
load instructions, then all data manipulations must happen in the registers, and finally
the modified register values can be stored back into the memory by the special store
instructions. This is in contrast to the Complex Instruction Set Computer (CISC) architecture,
such as the venerable x86 inside the personal computers, where some of the operands of complex
instructions don't need to be in the registers, and can be still in memory.
But regardless of the processor architecture, I hope that you start to appreciate the role
of memory addresses, because every access to memory necessarily requires the knowledge
of the address to load the data from or to store the data to.
All this leads to an interesting question. If those memory addresses are so fundamental
to the CPU, can they be somehow represented at the C-language level?
The answer is "yes". In the C language, the addresses can be stored inside variables called
pointers.
Here is an example of a pointer variable in C. Like most C declarations, the best way
to explain this one is to read it backwards. So, p_int is a pointer--which is what the
star means after the type--to intergers. In other words, p_int is a variable that can
hold addresses of integer variables.
If so, then p_int should be able to hold, among others, the address of the integer counter
variable. Indeed, this can be very easily achieved in C. The operator ampersand gives
the address of the counter variable, and this address can be legally assigned to p_int.
Finally, it is also very useful to get the value stored at a given address from the pointer,
which is called de-referencing the pointer. The C operator for this is the star. Star-p_int
means the value at the address currently stored in the p_int pointer, which is the value of
the counter varaible in this case.
Because of this equvalence, you can replace counter with *p_int, and the program should
work exactly as before.
Let's see if the compiler is happy with this program.
All right, so now let's go to the debugger and prepare the views. This time we need both
the Watch1 view to see the counter variable and the Locals view to watch the p_int pointer.
Before you step through the code, let's take a look at the disassebly view and compare
it side-by-side with the code before introducing the p_int pointer.
As you can see, the LDR instruction, which loads the address of the counter varible to
the R0 register has been moved to the top, and another copy of the same instruction has
been removed altoghether. In other words, introduction of the p_int pointer has simplified
the machine code and improved its efficiency. This code also suggests that the p_int pointer
is now located in the R0 register.
Now you can single-step through the code and watch the counter variable increment, both
in the Watch1 view and in the memory view, exactly as before. This confirms that the
pointer is indeed an alias of the counter variable.
Finally, if you want to execute the loop to the end, but are getting tired of single-stepping
through the code, you can set a breakpoint after the loop and click the Go button to
execute the program at full speed. After you stop at the breakpoint, you can verify that
the final counter value is 21, as expected.
In the last step of this lesson, I'd like to demonstrate the incredible power of pointers.
At this point, it will be just a horrible hack, but it will show you a technique that
is actually used all the time in embedded systems programming.
As I said before, a pointer variable, like p_int, holds the address of an integer. But
this can be almost any address, not just the address of the counter variable. If so, then
let's try to assign a fabricated address to p_int.
In your first attempt you might try to use an address expressed as a hex number, just
as you saw it in the debugger. But when you compile by pressing F7, the compiler rejects
the code.
In your next attempt, you might try to use an unsigned number by prepending the U suffix
to the number. But the compiler doesn't like this either.
At this point your negotiations with the compiler really break down. But the C language has
a mechanism to enforce a type by using type casting. You perform such type casting by
placing the name of the type in parentheses in front of the cast expression. Now, the
compiler has no choice, but to accept it.
All right, so now let's de-reference the pointer and poke some easily recognizable integer
value into it. Embedded programmers seem to like DEAD BEEF for this purpose.
Obviously, this hack needs to be tested. But to preempt your objection that a simulator
can take anything, I'd like to run this code on the Stellaris Launchpad board. So, if you
have the board, plug it into the USB connector of your PC.
Next, set the debugger to the TI Stellaris interface. Also don't forget setting the "Use
flash loader" option under the Download tab.
If you don't have the board, just skip this step and follow along in the simulator.
Either way, click the download and debug button to get to the debugger.
Make sure that you have a breakpoint at the re-assignment of the p_int pointer and also
make the Watch view visible so that you can see the counter variable.
Press Go to run to the breakpoint.
Click in the disassebly window to step one machine instruction at a time.
Exectute the LDR instruction, which loads the fabricated address to the R0 register,
and verifyt that this address appears int the p_int variable in the locals view as well
as in R0 in the register view.
Exectute the next LDR instruction, which loads the value hex-DEADBEEF to R1.
And finally, exectute the STR instruction, which stores DEADBEEF in memory at the p_int
address.
The effect of this is kind of scary. Due to the intentional misalignment of the fabricated
address, the 0xDEADBEEF value gets written partially over the counter variable and partially
over the next word in memory. The Cortex-M4 processor accepted this mis-aligned address,
but Cortex-M0 would have a problem with it.
So, now you see how pointers, as any powerful mechanism, can also be dangerous, if used
carelessly.
This concludes this very quick introduction to pointers. In the next lesson you will put
this knowledge to use to blink the LED of the Stellaris Launchpad board.
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.