If you have experience with a high level language such as C, BASIC, Python or Java, then you will be familiar with concepts such as If-Then-Else statements as well as Loops (While, For, etc) and Functions. You may even have some experience with using an RTOS on a microcontroller. Do you know the details of how these are implemented internally on the processor? There are 3 main conceptual parts to the CPU:
- The processor itself, which executes instruction code.
- The instruction code to be executed.
- Working memory (RAM).
The instruction code can be thought of as a long list of instructions – one instruction per line, where every line has a number (“address”). The instruction code is in the processors native assembly language, which differs from architecture to architecture (the same C code will produce different assembly code for e.g. x86 and ARM processors when compiled).
The Program Counter
Typically, the processor starts at the top of the list of instructions (with some processors it may start elsewhere, but the concept is the same) and works its way down the list, executing one instruction after the other. The processor has something called a Program Counter (PC, not to be confused with Personal Computer) which keeps track of which line of code (address) is currently being executed. After the instruction has been executed, the Program Counter (PC) automatically increments to the next line (address). The instructions can do things like reading from and writing to memory, adding/subtracting/multiplying numbers, manipulating bits in a byte, etc. Instruction code is usually stored in non-volatile memory such as FLASH, because it typically stays the same and does not need to be changed regularly. Non-volatile means that the memory retains/remembers its contents when the power is off – see our article on FLASH memory for more information). Some constant values (e.g. the bytes which make up a bitmap logo) may also be stored in the FLASH. Working values (the values of variables) are stored in the volatile RAM (volatile meaning that the memory loses/forgets its contents when the power is off).
Conditionals and Jumps
Apart from simply executing instructions one after the other, line by line, there are also some instructions which can make the program counter skip a line, or jump completely to a specific line number (or technically by a certain offset). These instructions can be used to implement If-Then-Else type statements (conditionals), as well as Loops and Functions (jumps). For example, a very simply conditional, which switches on an LED when a button is pressed, could be:
1. Test (read) input bit (button), skip a line if zero
2. Set output bit (LED)
3. Further code...
If the button is pressed then no line skip will happen and line 2 will execute, switching the LED on. If the button is not pressed then line 2 will be skipped and not execute and the LED will not switch on. A simple for loop, which toggles an LED between on and off 10 times (yes, potentially very quickly), could be implemented as follows (variable here is shorthand for a value at a specific address in RAM):
1. Set our variable to value 10
2. Toggle output bit (LED)
3. Decrement our variable, skip if zero.
4. Jump to line #2.
5. Further code..
When the code hits line #3 it will decrement the variable; 9, 8, 7, 6, etc. Until the variable decrements to zero, no skip will occur after line #3 and line #4 will execute, jumping execution back to line #2. Once the variable is zero, line #4 will skip and code execution will resume from line #5.
You don’t have to write code for very long before you see that some sections of code can be reused (as opposed to copy-pasting them continually), and this is where functions come in. For example, you may wish to have a function which toggles an LED a certain number of times. We would need two variables – a count variable and a return to variable. In the code below, the green code is the function, and the orange code are the function calls.Pseudo-code illustrating basic need for functions.
Because this sort of situation occurs quite frequently, and because functions may themselves call (jump to) other functions, processors have specific instructions for handling these sort of scenarios. There is an area of memory called a stack, and there are instructions for manipulating the stack. A stack is an area of memory which is used exactly as its name implies – it’s like a stack of papers where you can only work with the top piece of paper: either add a paper to the top or take one off the top. Adding a piece of paper to the top is called pushing to the stack, and taking a piece of paper off the top is called popping the stack.You can see stack depth and push/pop function calls as you single step debug a Proteus simulation.
The technical term for this sort of memory use is Last In First Out (LIFO). When some code wants to call a function, it can execute an instruction which pushes a return address (line #) to the stack, and then jumps to the function line #. The push instruction would add the current line # (program counter address), plus an offset (we want to return to the line # after our function call instruction, not the current line #), to the top of the stack. At the end of the function is a return instruction, which pops the stack and jumps back to the line # remembered at the top of the stack. If the function should call other functions (or even itself, recursively), then return addresses (line #s) keep getting pushed onto the stack until return calls are made and they are popped off the stack again. If too many functions are called within each other, and the stack is not large enough, then the stack will get full and overflow – a “stack overflow”. There are further complexities for passing parameters to functions and returning values from functions, but they all make use of the concept of a stack.
Real-Time-Operating-Systems, or RTOSs, enable running multiple “tasks” in parallel, by maintaining a copy of a stack and program counter for each task, and jumping between them (the exact details of how an RTOS works is a subject for another article).
Lastly, there is the concept of interrupts. An interrupt is a feature of the processor where, upon some external event (e.g. an input pin goes high), the processor will automatically save the current state (program counter, etc) to the stack and jump execution to a special interrupt routine (function). When the interrupt routine is done executing, it pops the stack again and normal program operation resumes.Modern Microcontrollers have a vast array of interrupt sources and priorities (Cortex-M3 shown above).
There are a lot more details to how specific processors implement stacks and interrupts, but they all use these same basic concepts. Compilers abstract the complexities of all of these details, however having a working understanding of what is going on underneath the hood can help debugging and putting together advanced projects.All content Copyright Labcenter Electronics Ltd. 2023. Please acknowledge Labcenter copyright on any translation and provide a link to the source content on www.labcenter.com with any usage.
Get our articles in your inbox
Never miss a blog article with our mailchimp emails
Learn more about our world leading embedded simulation, Proteus VSM. Simulate and debug your microcontroller firmware on the schematic alongside all the analog and digital peripherals connected to it.
Ask An Expert
Have a Question? Ask one of Labcenter's expert technical team directly.
More Like This
Synchronous vs Asynchronous Protocols
Examine the difference between a synchronous and asynchronous protocol.
Passive Analogue Filters
Take a refresher course and learn all about Passive Analogue Filters