Writing the Startup Code and Linker Script for the TM4C ARM Microcontroller from scratch
Sunday, October 28, 2018 Posted in Embedded SystemsTags : arm cortex-m C tm4c microcontrollers linux
Nearly every ARM Cortex-M needs needs a startup code file to define it’s Main Stack Pointer, Reset Handler and Vector Table and a linker script telling it to place which sections into which parts of memory, these two files are crucial,without which even a simple blinky program cannot be run. Usually,the vendor provided versions of these files are sufficient,but the TI provided ones require a ~500MB download,and registration on the TI website,labyrnthine folder structure with very little documentation of what goes where or how is the project structured
To top it all off,nearly every file in the TI package has this license in it :
Texas Instruments (TI) is supplying this software for use solely and exclusively on TI’s microcontroller products. The software is owned by TI and/or its suppliers, and is protected under applicable copyright laws. You may not combine this software with “viral” open-source software in order to form a larger program.
Which is why i decided to write my own Open Source Startup code and Linker Script,and document how both of them work on this blog.
Startup Code
First we start off with the Startup code,which has two main functions
- Initializing the Vector Table with the
Main Stack Pointer
- Implementing the
Reset_Handler
function,which consists of,- Copying
.data
sections of the memory from Internal Flash to Internal SRAM - Setting
.bss
values to 0 - And finally pointing to the
main()
function of your program,after these initialization tasks are done
- Copying
Writing Startup.h
The Vector Table is an array of void
functions which is placed into a section in the memory,namely
.vector_table
in this case,this name has to also be reflected in the linker script.
The functions each map to an Interrupt index-wise,which is hardcoded into the microcontroller itself,
the interrupt vector table is defined in the TM4C123GH6PM data sheet, pages 147-149, and we will be using it to write our own vector table.
We will be having around ~120 or so function prototypes in our vector table, so we need to aliase undefined function prototypes to a default handler function, we do this by defining a macro in our
startup.h
file like so
#define DEFAULT __attribute__((weak, alias("Default_Handler")))
This macro, DEFAULT
will expand to __attribute__((weak, alias("Default_Handler")))
which is a command to the GNU GCC compiler to aliase a given function to the Default_Handler
if it is not defined
For example,we are going to explicitly define the Reset_Handler
function prototype in the startup.c
file, but not the NMI_Handler
or other functions for now, so we mark them as DEFAULT
void Reset_Handler(void);
DEFAULT void NMI_Handler(void);
DEFAULT void SVC_Handler(void);
DEFAULT void DebugMonitor_Handler(void);
DEFAULT void PendSV_Handler(void);
DEFAULT void SysTick_Handler(void);
We then start writing the ISR function prototypes
DEFAULT void GPIOPortA_ISR(void);
DEFAULT void GPIOPortB_ISR(void);
DEFAULT void GPIOPortC_ISR(void);
DEFAULT void GPIOPortD_ISR(void);
DEFAULT void GPIOPortE_ISR(void);
DEFAULT void UART0_ISR(void);
DEFAULT void UART1_ISR(void);
DEFAULT void SPI0_ISR(void);
DEFAULT void I2C0_ISR(void);
DEFAULT void PWM0Fault_ISR(void);
DEFAULT void PWM0Generator0_ISR(void);
.........
The rest of the ISR prototypes can be viewed on my Github Repo(tm4c-linux-template), these prototypes conform to the Interrupt Vector Table interrupts from the datasheet
These function prototypes are of return type void
, and standard arrays cannot be declared as void, so we need to define new types for these
typedef void (*element_t)(void);
Here, *element_t
is a pointer passed to void function and cast as void
next we define a union
for our main stack pointer and our ISRs
typedef union {
element_t isr;
void *stack_top;
} vector_table_t;
void *stack_top
is a pointer to the top of the stack,and the 0th element of the vector table.
element_t isr
stands for the void functions that will be added to the vector table
Lastly,we have to declare external variables,mainly the sections,and the main()
program entry point
extern int main(void);
extern uint32_t _stack_ptr;
extern uint32_t _etext;
extern uint32_t _data;
extern uint32_t _edata;
extern uint32_t _bss;
extern uint32_t _ebss;
Here the extern
keyword simply tells to compiler to look for these keywords in another file external to this,the uint32_t
means an unsigned,32 bit integer,to dispel ambiquity of int sizes on different architectures.
int main(void)
is the entry point to your main()
program.
_stack_ptr
is the pointer to the top of the stack,i.e the last address of RAM,this is defined in the linker script.
_data
is the start of the .data
section,and _edata
is the end of the .data
section,same convention applies to the other section variables too.
Writing Startup.c
We start by including our startup.h
header file with out definitions and the <stdint.h>
#include <stdint.h>
#include "startup.h"
Next we direct the compiler to place the following vector table into the section .vector_table
in the .data
segment
__attribute__((section(".vector_table")))
Now we define our vector table like so
const vector_table_t vectors[] = {
{.stack_top = &_stack_ptr},
Reset_Handler,
NMI_Handler,
HardFault_Handler,
MemManageFault_Handler,
BusFault_Handler,
UsageFault_Handler,
0,
0,
0,
0,
SVC_Handler,
DebugMonitor_Handler,
0,
PendSV_Handler,
SysTick_Handler,
GPIOPortA_ISR,
GPIOPortB_ISR,
GPIOPortC_ISR,
GPIOPortD_ISR,
GPIOPortE_ISR,
UART0_ISR,
UART1_ISR,
SPI0_ISR,
/*MORE ISRS FOLLOW FROM HERE */
};
Here,
- The 0th element,
{.stack_top = &_stack_ptr}
assigns the Main Stack Pointer defined in the Linker Script,_stack_ptr
to the union element.stack_top
defined invector_table_t
- The 1st element is the
Reset_Handler
, that is called when theButton on the microcontroller is pressed,or the reset flag is set
Finally we define the Reset_Handler
and Default_Hanlder
void Reset_Handler(void)
{
uint32_t *src, *dest;
src = &_etext;
for (dest = &_data; dest < &_edata;)
{
*dest++ = *src++;
}
for (dest = &_bss; dest < &_ebss;)
{
*dest++ = 0;
}
main();
}
In this function,
- Two pointers are declared,
*src
and*dest
*src
is set to the address of_etext
,and in the first loop*dest
is set to the address of_data
- Till
dest
reaches the end ofedata
it will loop and copy the contents of*src
into it - This is copying the data from
.data
residing on the Flash,to the RAM dest
is now set to the address of_bss
and every element ofdest
,i.e_bss
is now being set to0
- Lastly,your
main()
is called,and control handed over to it
Default_Handler
is also defined here,and it just infinitely loops when called
void Default_Handler(void)
{
while (1)
{}
}
With this,we are finally done writing the startup code
The rest of the code can be viewed on my Github Repo(tm4c-linux-template)