This module takes you through an overview of the firmware system coded for the EiE development board. The very important goal is for you to understand the architecture of the EiE firmware system, especially the timing mechanisms. The module concludes with the classic “Hello world” of embedded systems: a blinking LED.
To begin, ensure you have the “Master” branch active in Github Desktop. Create a new branch from this called “yourinitials_firmware_system”. Note, there may already be a “firmware_system” branch in the repository from when it was cloned – ignore this one (delete if you want) and make your own. Make sure yours is the active branch.
Open the project in IAR and verify your system is up and running by doing the following:
- Connect the development board J-Link USB cable.
- Make sure the J-Link CDC (COM port) appears in Device Manager
- Open the .eww workspace file in IAR
- Start the debugger (flashes the code) but do not run it yet
- Open Tera Term and select the COM port that the USB to Serial enumerates to and ensure the port settings are 115200, 8 bit, no parity, 1 stop bit and no flow control.
Now run the code in the debugger and observe the debug output in Tera Term. After you see “Initialization complete” type “en+c00” in Tera Term and press enter to see the board’s debug menu.
A pseudo operating system
The EiE development board firmware is called a “bare metal” system because it does not use an operating system. However, it is much more than a linear sequence of function calls that a very simple firmware system might use. The base system is written as a “State Machine Super loop.” The function calls in the EiE system loop are done with function pointers which point to the current state in each task. This acts more like a round-robin scheduler that gives processor time to all the tasks in the system for the task to work on whatever it needs to do. The tasks themselves behave like objects in an object-orientated design. The key difference from a real operating system is that the processor time which a task gets is managed by the programmer instead of the firmware. You might call it a “pseudo operating system.”
Drivers and applications are added to the system as tasks that are as independent and mutually exclusive as possible. Each task must follow the rules of the system to ensure all the other tasks continue to operate properly. The task may provide services to other tasks through public functions of its Application Programming Interface (API).
Like most (quite possibly “all”) embedded systems, the main part of the firmware for the development board runs inside an infinite loop. This follows an Initialization section that runs only when the processor first starts which is either when it is powered on, or if something causes it to reset. Stripped of all the actual code, the EIE firmware structure looks like this:
ake the EiE system work, each of the two sections has very important rules that you as a developer in the system must follow. Dictating rules in documentation saves a massive amount of coding to otherwise implement those rules in the software. If we wrote a system that did not rely on written rules but provided the multitasking capabilities of this system, we would essentially be writing a full, real time operating system.
Initialization code only runs once when the system is powered on or reset. Every task must have an initialize function that is called in this section of code. Regardless of what the task does, its initialization function must follow three simple rules:
- It cannot make use of non-initialized system functionality.
- Initialize functions may take as long as they want but eventually they must return.
- When the task’s Initialize function is finished, the task is either ready to run or assigned a safe error state that will not blow up the system in the Super loop.
Once all tasks are initialized, the system enters the main super loop which is simply a while(1) loop that should never terminate. The loop continuously executes a sequence of function calls that give every task some processor time. This is really the simplest form of an operating system scheduler: a non-prioritized, non-preemptive, round-robin scheduler.
The fundamental difference between this scheduler and that of an operating system is that the processor time allocated to each task is completely up to the programmer. Therefore, we define only one very important rule that an application must adhere to:
- The cumulative execution time of all tasks that run in a single iteration of the super loop shall not exceed one system tick period.
The default system tick period is 1ms. That means that if there are 10 applications in the finished system, each task has 100us to work during every loop iteration. Though one hundred microseconds probably does not sound like a lot of time, the EiE system which runs at 48MHz gives each task 4,800 processor cycles to use. With knowledge about the other tasks that are running in the system, you can likely safely extend the number of cycles your task uses. If a task takes too long, it will potentially affect timing with other tasks that may or may not be detrimental to the system. However, the system will continue to run normally once your rogue task eventually returns to the main loop to allow the other tasks to run.
The system also makes extensive use of interrupts for high-priority, non-deterministic operations like sending and receiving data from the various communication buses. In this way it achieves real-team performance on critical tasks subject only to the user-defined interrupt priority assignments.
Timing is Everything
Other than the simplest devices, an embedded system must know how much time is going by. This does NOT mean the firmware has to know what time of day or what month it is (a.k.a “real” time), but it must be able to determine intervals, like toggling the state of an LED every 500ms to make it blink. Timing is also critical for things like communications protocols that must send data at precise times. Time-awareness comes from the base system clock which drives the processor execution.
For accurate timing, a “crystal oscillator” is the hardware of choice as it gives a time base accurate on the order of 20 parts per million (ppm) even over varying voltage and temperature. The EiE dev board crystal is 12MHz, and there is some hardware onboard the microcontroller called a “phased lock loop” (PLL) that upconverts this to provide the main clock of 48MHz. It is this signal that all the timing in the firmware is based. There’s a whole section on this in the EiE textbook if you’re interested in learning more.
One of the “things” the clock drives is called the system tick. This is just a digital counter that counts up to some value. When it reaches that value, it sends a signal called an “Interrupt” that can immediately cause the processor to pause what it’s doing and service the interrupt. With some very simple math, the system tick can be setup to provide the system time base. For EiE, it is set for 1 millisecond.
The ARM core system tick is the highest priority interrupt in the system, so the 1ms tick counter is a very reliable representation of actual time elapsed. This 1ms counter value is maintained globally in the system as G_u32SystemTime1ms so any task can reference it directly and it is always visible in a Watch window since it is always in scope. Thanks to Hungarian notation, you should be able to calculate how much “real time” it takes for the G_u32SystemTime1ms to overflow. There’s also a one-second timer, G_u32SystemTime1s, that can track longer intervals. How long? Do you think it’s large enough?
Thanks to a few rules and a reliable time base, we end up with a simple yet capable multitasking system that has essentially a zero-footprint kernel. Granted, it is potentially quite fragile but when used properly it is very functional and robust. This system is used for the entirety of the EiE firmware program, so by the end of this module you should have a very solid understanding of how it works, how to use it successfully, and what sort of advantages and disadvantages it has overall.
The starting firmware build for this module has the full code to run the peripherals on the dev board. The drivers that provide this functionality are each their own tasks in the system that receive processor time with each iteration through the main loop as described above. In most cases, the tasks are idle and would execute in just a few microseconds. There are also three User Application tasks in the loop that are setup for you to add your own firmware into the system. In most cases, the only files you need to edit are the user_app.c and user_app.h files. If you needed to write more tasks, simply follow the instructions at the top of user_app.c and user_app.h to make additional user tasks.
Even veteran developers likely start up a new platform with a simple “Hello world” type application. In the embedded world, this is classically a blinking LED. Getting that blinky light on a new development board with a new processor and new firmware is one of the most satisfying moments of the embedded programming experience!
The really low-level coding is already done, but from your starting point it will still be satisfying to get a light blinking. Yes, the API provides functionality that could make this happen in a single line of code, but you do not yet know how to use it. Plus, that would bypass the point of this exercise which is to really show how the system timing works in as low-level way as possible right now.
So you will add just a bit of code to create a user_app driven hello world program. The exercise is an easy introduction to coding your own user application and to see if you understand how the system works. Remember that you are are responsible for meeting the rules of this system. Most importantly you must understand that the main system loop executes once every millisecond and thus every task in the system gets processor time every millisecond but the sum of all task execution in a single cycle cannot exceed 1ms.
You will hijack the Heartbeat LED which by default is used to mark the start and end of a sleep cycle which is also the end of each 1ms period. Right now if you probe the heartbeat LED with an oscilloscope, you will see a low duty cycle 1 kHz square wave. The system is sleeping when the LED is off, and the system is executing code in the main loop when the LED is on. Since the intensity of an LED increases with duty cycle, the heartbeat LED will be brighter when the system is spending less time sleeping.
Download and run the code on the dev board right now and make sure you see the small red LED next to the processor.
The heartbeat LED is the only LED that can be accessed without interfering with the LED driver. This is a bare metal system after all — we could access ANY bit of memory and peripheral in the processor so we must be careful. The plan is to use the inherent 1ms system timing to blink the Heartbeat LED at a rate you can actually see.
To do this you will need the following:
- The existing function (macro) HEARTBEAT_OFF() to turn off the Heartbeat LED
- The existing function (macro) HEARTBEAT_ON() to turn on the Heartbeat LED
- The C-programming skills you picked up from the previous module.
- Your newly discovered knowledge of the system timing.
All of your code should be written in static void UserApp1SM_Idle(void) inside user_app1.c and you probably will add a few things to the user_app1.h header file so that you can easily change the blink rate. Good programming practices will be emphasized from the very beginning! The steps below will guide you through!
Turn off the Heartbeat in main
Start by commenting out the code that manages the heartbeat around SystemSleep() at the end of the main loop in main.c. Build the code and run it just to make sure you no longer see the heartbeat light.
Open user_app1.c and find the UserApp1Initialize() function. You can find it quickly by clicking the small “f()” symbol in the top right of the user_app1.c window. Initialize the heartbeat LED to the OFF state by calling the HEARTBEAT_OFF() function. For now, ignore the rest of the code in this function.
Scroll down to UserApp1SM_Idle. The system will execute the code in this function every millisecond – this is promised by our rules, although never guaranteed. For non-mission critical timing, it is safe to rely on the 1ms period. Therefore, simply set up a counter that increments each time UserApp1SM_Idle is called and check when it hits the trigger value you want. Since you are being a good programmer, the trigger value should be a preprocessor definition in user_app1.h. Don’t hard-code values!
What is a reasonable size for this number? The variable that will be counting is incrementing every millisecond. 1000ms is one second. If you want the light to run a full on/off cycle in one second (i.e. the period is one second), then what value will you need to count to in order to monitor the on time and off time? Do you need much higher than this?
Make a preprocessor definition in user_app.h:
U16_COUNTER_PERIOD_MS is just a symbol for the number 500. When used in the code, the compiler will put “500” wherever it finds the symbol when building the code. The name starts with “U16” to remind everyone how big the number could be. The “_MS” at the end reminds people the value is specified in milliseconds.
Return your editor to UserApp1SM_Idle(). Now it’s time to write the code to manage the timer. Think about the steps involved and draw a flow chart to capture the algorithm. Remember that UserApp1SM_Idle() is going to run every 1 millisecond. So you need to do the following:
- Initialize the counter to 0 if you plan to count up, or to U16_COUNTER_PERIOD_MS if you plan to count down (counting down is usually more efficient)
- Increment (or decrement) the counter.
- Check if the counter has reached U16_COUNTER_PERIOD_MS.
- If it has, do something (and don’t forget to reset the counter); if not, don’t do anything until next entry to UserApp1SM_Idle().
How do you keep the counter value in memory even when the function exits? Check back to the C-primer module if you forgot. The solution is shown below. Hopefully you have written your version and can just compare…
Build the code and run it. Put a break point where u16Counter is reset back to 0 to make sure everything is working as you expect. Examine u16Counter in a Watch window along with G_u32SystemTime1ms. Run a few times to the breakpoint and observe G_u32SystemTime1ms. Practice disabling the breakpoint in the Breakpoints window to let the code run for a longer period without having to delete and add the breakpoint back.
Blink the LED
The last step is to blink the LED. Since you only have the ON and OFF functions to work with, you have to keep track of the last state. Use could use a static boolean variable for this, and call ON or OFF based on the boolean value. A simple if/else statement will work.
If this isn’t easy, think about it for a while. Discuss with a partner or your Leader. Then write the code and give it a try. If it doesn’t work, try to use the debugger to figure out why. This is excellent debugging practice because you need to step slowly through part of the code, but then use breakpoint and run full speed while you are killing time. Please do not step through 500 iterations of UserApp1SM_Idle()!!!
A solution is shown below. If your code isn’t working, make sure to step through and figure it out. Keep the break point on the counter reset so the code runs full speed to get through all the wait cycles. The code is simple but the concept is essential to understand.
Change the code to automatically double the blink speed every few seconds until you cannot see it blinking anymore, then slow it back down (or reset back to the 1Hz rate and repeat). You might also try changing the blinking duty cycle to something other than 50%.
[LAST UPDATE: 2023-OCT-04]