The hidden world of MCU start-up code
Watching firmware run on a microcontroller debugger is an almost magical experience. Pushing that little green button in the IDE, progress bars rapidly completing on the display until finally the screen flickers and a single line of code is highlighted with the word main. So many things just went on behind the scenes that it is nearly mind boggling! Let’s examine what goes on prior to reaching program main.
Once the program has been loaded onto the microcontroller, the microprocessor starts execution by first reading the reset vector. The reset vector holds the memory address of where the first instructions for the program are located. These first instructions aren’t the start of the application that exist in main, however, but instead are usually initialization routines hidden away in start-up code.
The start-up code performs a number of critical functions that are necessary to prepare the microcontroller to start executing the developers’ application. The exact steps the start-up code follows will vary slightly from one microcontroller to the next, but generally one of the first steps is to initialize the system clock. Once the system clock has stabilized, the memory environment must be set up by copying initialized variables into RAM. This process is often referred to as "C copy down." The idea is that the microcontroller's memory is being set up to run the C environment that the application code will run under.
The start-up code will also initialize the stack pointer. The stack is an area of memory that will store automatic variables, function parameters, interrupt frames, and function return addresses. A bare-metal system (a system that doesn’t have an RTOS) will have only a single stack area that, by default, is typically sized to a depth of 0x400. The start-up code will often have a few assembly language instructions designed to set the top of the stack pointer.
Once the clock has been initialized, the C copy down performed and the top of the stack has been identified, the last step is to either jump to or call main. Some start-up code will perform a jump to main rather than a function call to main because this can save a few precious bytes of stack space. No matter which method is used, though, a developer will then find themselves at the beginning of main with the debugger halted and waiting for permission to execute the application.
And yet, a developer is usually completely unaware of all the code that has executed prior to reaching the beginning of main. Should a developer uncheck the “run to main” option, though, rather than breaking at the first line of main the application will instead break on the first instruction of the start-up code. A developer can then step through each line of code that is used to setup the processor before main is ever reached. As one might expect, the start-up code is usually different from one development environment to the next. At the beginning of a project it is often useful to step through the start-up code because one never knows what interesting things might be set up before ever reaching main.
The start-up code for a microcontroller can become more complicated if there is a bootloader in the mix. When the application boots, there may be a piece of code that performs a quick check to determine whether the primary application should be executed or whether a firmware update application should be executed instead. If the bootloader is executed to update firmware or to simply start the system in a known state and perform a system sanity check, the start-up code may need to reset the stack pointer rather than simply initializing it.
Embedded software developers used to know every nook and cranny of their software. Since microcontrollers have gone 32-bit, the way in which firmware is developed has rapidly changed. Details of how the processor starts up and the code executes are only the first pieces of code to begin hiding behind in the scenes. One day, embedded developers may know only as much about what’s happening at the microcontroller level as an application developer understands what is happening at the microprocessor level of a server. Despite what seems like a loss of insight, though, the levels of abstraction and hidden code will undoubtedly help developers write code faster and at lower cost while apparent magic works behind the scenes.
Jacob Beningo is principal consultant at Beningo Engineering, an embedded software consulting company. Jacob has experience developing, reviewing and critiquing drivers, frameworks and application code for companies requiring robust and scalable firmware. Jacob is actively involved in improving the general understanding of embedded software development through workshops, webinars and blogging. Feel free to contact him at firstname.lastname@example.org, at his website www.beningo.com, and sign-up for his monthly Embedded Bytes Newsletter here.