In the last few years, I have met many embedded developers at trade shows, seminars and the like. In many cases, I hear sheer frustration about how difficult it is to develop embedded systems if you come from a reasonably related background, such as developing Windows or web applications on PC’s. Feedback from our support team confirms many developers share this frustration.
So why is it so difficult to learn embedded development? And why is it the industry has not progressed more to solve the high learning curve? You may want to pass this blog post on to your boss, in case he does not understand the high level of expertise required in your job.
Developing software for GUI applications like Windows software or smart phone/tablet software is easy to get started with, as are web development. Sure, they have their own difficulties, but that is more related to other things, not the very high learning curve and all the work you need to do before the first line of your application code executes. In short, embedded applications are on a much lower abstraction level.
Let’s inspect some of the obstacles an embedded developer has to fight against before the application can do anything meaningful. PC developers can safely ignore everything that happens before the first line of application code in main() or similar. Unfortunately, this is not so for embedded systems.
As the electronic board is powered-up, the processor initializes itself, looks in the interrupt vector table to find the start address of execution, and then jumps to the power-on-reset interrupt handler function, where the first execution really starts.
Already here, a few things kill the struggling PC developer who tries to do embedded development for the first time. But this is easy you say. Just add the power-on-reset interrupt handler function start address to the suitable entry in the interrupt vector table, and make sure to update the linker configuration file to position the INTVECT to the right location in memory.
The debugger and JTAG probe will then ensure the interrupt vector table is flashed on the right address, causing the CPU to jump to the power-on-reset interrupt handler upon start. Oh, and by the way, make sure to use the right #pragma to flag the interrupt handler function as an interrupt handler, causing the compiler to generate different function entry and exit machine code.
Already here, the suffering PC developer is way lost. Power-on-reset? Interrupt vector table? Interrupt handler? Linker configuration file? #pragmas? JTAG probe? Flashing?
The horrors continue. Once execution in fact do start in the power-on-reset interrupt handler, you are responsible for setting up the hardware initializations, for the processor as well as other devices soldered onto the board. Not only do you need to know how to setup the devices, the configuration is board specific to. Oh my!
To initialize the processor and other devices, special memory locations called SFR’s (special function registers) must be read and written to according to a special protocol defined in the chip manual (that rarely are less than 1000 pages by the way). Well actually, I lied. It is usually a bit worse.
You don’t just read and write numbers to memory addresses, you have to read and write certain bits or bit fields in these memory locations, and so you have to do a lot of bit shifting, bit clearing, and bit setting. And just to make things fun; sometimes bits in these SFR registers auto-clear themselves if you read from the memory location once.
High-end processors can have many thousands of SFR register bits. To setup the system before any other software can run, you may have to configure the stack pointer, configure chip select signals, DRAM refresh, memory wait states, and the like. In many cases, the memory where you shall execute the code from may not even be accessible at boot time.
For example, a system may want to copy the code from slow Flash to fast RAM at system initialization, before jumping to the code now moved into RAM. But the RAM memory may not be accessible before you enable chip select signals, configure DRAM refresh, and setup wait states. And yes, make sure to configure the pin function controller such that the enabled chip select signals actually make it out to the physical pins of the chip.
And the story goes on.
Runtime library initialization
Now that we have handled the power-on-reset, and let the interrupt handler initialize the hardware, surely we can relax and start with our application development? Well, no.
You probably want to start write your application code in the main() function or something by now. But why would you think main() gets called? Because it happens automatically when you do PC development, you should not make such “arrogant” assumptions when it comes to embedded systems.
After the hardware initialization is made in the power-on-reset interrupt handler, you must first initialize the C runtime library, and then, you must call main(), where your own application code can finally start. And so it is your responsibility to boot the hardware, initialize the compiler runtime system and call main().
Initializing the C runtime library involves things like clearing variables that should be zero initialized, copy data from Flash to RAM for variables that should be initialized to some other values, etc. For C++ there are a couple of other things that may have to be addressed, etc.
Happy times, my friends!
The linker configuration file
I mentioned the linker configuration file briefly above. This is an invention by the devil himself. It is as if some satanic tool developers in the beginning of history sat down and discussed how to make the damned thing as difficult to understand and use as they possibly could. There is no thing that is so difficult to understand and write for new embedded developers, as the linker file. Even seasoned embedded developers want to hide it away never to be touched again.
What is the purpose of the linker configuration file anyway? Its main purpose is to locate different code and data objects on different memory locations, as different memory regions could be populated with different types of memory (RAM, Flash, EE2PROM, SRAM, SDRAM, etc.), no memory at all (a hole in the memory map), or memory with different speeds that you may want to optimize for.
There may be other uses as well, perhaps your embedded board uses dual-port memory (or a dual-core processor) that uses a shared memory area for communications between 2 processors or cores. Then you need to map those inter-processor data structures onto the shared memory.
Quite fun, this stuff. Isn’t it?
PC developers living in paradise can do simple API calls like printf() to print a string to a console window. Embedded developers must of course spend a day or more to build this functionality themselves. Well, printf() with its formatting options come with the C compiler runtime library, but unfortunately it doesn’t know how to print anything. And not where to, either.
System API functions like printf() must be configured for your board, perhaps by re-directing printf() output to an LCD display, a UART or USB cable, etc. To do that on the GNU GCC compiler you must implement a function called _write(). printf() uses _write() as a generic “send a character somewhere” pipe.
By implementing _write() to send bytes to a UART cable for example, it could be implemented to pass on characters from printf() to a UART using the UART device driver function, for example UART_Channel2_PutChar(). But where does this function come from?
For embedded developers, it doesn’t, unless your chip vendor provides it for you. Again, a lof of SFR bit reading and writing, possibly using inline assembly and often using interrupt handlers and the interrupt #pragmas.
This is not all too difficult for a simple UART, but it can quickly get complex. An example is if you want to use a 64-channel timer, with cross-configured channel dependencies, doing advanced PWM (pulse width modulation) signal output to create advanced signal forms, for example for automobile engine control or advanced stepper motor control. Perhaps we shall throw in automatic DMA transfer as well, triggered by some timer interrupt? Just don’t forget to handle that DMA transfer completed interrupt, when the memory block have been auto-copied by the hardware upon that timer event.
Simple things really can become very, very, complex in embedded land.
Embedded processors can stop execution upon some hardware event, and shift execution to an interrupt handler function temporarily. The CPU knows where they are by looking up their start address in the interrupt vector table, that obviously must be initialized correct by you. But you must also initialize the hardware to trigger the interrupt properly, and additionally you must enable the interrupt too.
Interrupt handlers have different entry and exit machine code compared to normal C functions, and so you must use an interrupt #pragma to make the compiler know this.
Due to aggressive compiler optimization, your application code may not store variables in memory locations but rather in faster CPU registers from time to time. If your interrupt handler breaks application execution when a variable is in a register, it may not work to read or write to it from the interrupt handler. To solve this problem, the ANSI C standard allows the use of the “volatile” keyword for variable declarations. Forgetting to use this keyword is a sure way of creating very hard to find bugs.
But I am sure new embedded developers remember this, don’t you too?
Once all hardware and software initializations are done, main() will finally be called and your application code can start to execute. Hopefully your chip vendor provides a device driver library, or else you will have a number of weeks in front of you building that yourself.
With everything in place, and a few lines of application code finally in main(), let’s try to debug a trivial little program! But wait, the application is written for an embedded microcontroller, perhaps an ARM Cortex-M processor like STM32, LPC or Kinetis. That code will not run the PC used for development, and so the debugger cannot execute the code using the Intel based developer PC.
And so, we need to hook up a JTAG debug probe that connects the target board to the PC debugger, typically using an USB cable. Unfortunately, it may be that also the debugger needs to be configured in special ways to work with the processor you use, and special configurations may be needed to make the debugger work with your custom designed embedded board as well.
Junior engineers are faced with alien things to configure, like choosing JTAG or SWD debug mode, enabling Serial Wire Viewer or Embedded Trace Macrocell usage, setting the right JTAG clock speeds, and the like.
Assuming above debugger initialization configurations are done right, our embedded systems debugger can now download the code to the Flash memory of the processor and perform basic debug operations like single stepping or running to breakpoints. Variables, memory or SFR registers can typically be queried using dedicated debugger views.
Now, we can start to use the heavy artillery of embedded debugging, for example using real-time event and data tracing with SWV/SWO/ITM, or ETM/ETB instruction tracing. These capabilities provides for additional possibilities to screw up in the massive amount of configuration settings these debugger technologies enable.
While embedded system C/C++ compiler and debugger IDE tools, like Atollic TrueSTUDIO, ease the above pain points quite a lot, it cannot be denied that embedded developers still need to have at least a basic understanding of the above matters.
This is in strong contrast to PC developers, who can quite simply start to write high-level application code in main() with little worry of any low-level issues. Sure, PC developers have other problems, like database performance and scalability of server load, but these are on a much higher abstraction level.
I think good embedded developers are extremely skilled, and real heroes who do not get the credit they deserve. And for me having spent my entire career in the embedded tools industry, I have to confess we have more work to do. It really is not easy to become and embedded developer, even if you are a very skilled PC developer.
It would be interesting to hear your comments and thoughts on this in the comment area below! Also feel free to share the article using email or social media, as it could be nice to get a bit wider response on these matters.
If you want to read more on embedded tools that can help remove many of the above pain points, read this whitepaper: