Basic information about below is advisable :
- The compilation process with GCC.
- Linker script and Startup code.
- OpenOCD, GDB and QEMU.
- Makefile.
Startup code is small piece of code (mostly written in assembly language) which is executed before your main procedure/function, in order to prepare the hardware for executing the main code. Startup code helps setup the initial stack pointer, the vector table (which contains the reset vector - the address of function to be executed on each reset of the CPU). On reset of the CPU, the reset handler is executed which initializes the RAM (initialized and uninitialized global variables), and then calls main procedure (application).
Since Ada provides many configuration pragmas for playing with code at low level, it is worth a try to write startup code completely in Ada (and it works ! ).
First Things FirstSTM32F407VGT6 used in this project has :
- Flash (ROM) : Starting at address 0x08000000 and size 1 MB
- SRAM : Starting at address 0x20000000 and size 128KB
For creating a binary executable (here, an.elf file) to be executed on STM32 (or any Cortex-M4), it is required that :
- First word (or first 32-bits) of vector table should contain the Initial value of Stack Pointer (SP)
- Next word of vector table should contain the address of reset handler function.
Note : CPU expects vector table to be located at very beginning of the ROM by default (i.e. 0x08000000 in this case). Hence, the compiled executable must contain vector table at same address.
So, the main objectives of this project would be :
- Within the vector table, first word should contain the end of RAM (= start of stack = initial stack pointer value). And next word of vector table should contain address of the Reset Handler (= startup code)
- Final elf should be compiled with vector table at the very beginning of ROM (at address 0x8000_0000).
- Reset Handler should copy the initialized global variables from ROM to RAM, Zero initialize uninitialized global variables and then call the main procedure (= application)
Note that since we are writing a startup code, which itself initializes the global variables in RAM, means that it can not make use of the global variables. Hence, the initial SP value in vector table should be placed at compile time and not at runtime.
So, use :
- Hard-coded value for SP (can be done)
- OR just Macros like in C (but not possible in Ada)
- OR local variables (which are allocated in Stack) within procedure, but not possible because we want to initialize the stack pointer and also no procedure is being executed when CPU fetches the SP value from first word of vector table.
- OR constants, which will be ultimately stored in ROM, but it's value will be available at compile time. and hence can be used to calculate value of SP. We will use this.
Below lines help calculate the value of SP :
--file : startup.ads
SRAM_Start : constant Unsigned_32 := 16#2000_0000#;
SRAM_Size : constant Unsigned_32 := 16#0002_0000#; -- 128 KB
Stack_Start : constant System.Address := System'To_Address(SRAM_Start + SRAM_Size);
Declaring The Handlers And Implementing The Required OnesIf you refer the vector table for STM32F407xx (in reference manual), it consists of 98 words storing the addresses of different exception and interrupt handler functions/procedures.
Now, for a simple blinky application, we don't require all of the handlers to be implemented, we just require Reset Handler. Hence, we will implement only the Reset Handler, for rest of the handlers we will export these as weak symbols and alias these to a default handler (which can be overridden easily for particular handler, more on this later).
We will discuss the Reset Handler later below, but for now let's take example of NMI Handler (Non Maskable Interrupt Handler), which is the third word in vector table after Initial SP and Reset Handler.
First declare the Default_Handler and a null body (because we don't require it to do anything for now).
--file : startup.ads
procedure Default_Procedure
with
Export,
Convention => Ada,
External_Name => "Default_Handler";
-------------------------------------------------------
--file : startup.adb
procedure default_procedure is null;
Below lines export NMI_Handler symbol as weak symbol and aliases it to Default_Handler
--file : startup.ads
procedure NMI_Handler;
pragma Export(Ada, NMI_Handler, "NMI_Handler");
pragma Weak_External (NMI_Handler);
pragma Linker_Alias (NMI_Handler, "Default_Handler");
----------------------------------------------------------------
--file : startup.adb
procedure NMI_Handler is null;
We will do the same for rest of the handlers too.
Creating A Vector Table And Placing It At Beginning Of The ExecutableA vector table is nothing but an array containing addresses of different handlers (32-bit addresses or words).
Below is the vector table, note the first word is Stack_Start, second word is Address of Reset_Handler, third word is Address of NMI_Handler,... and so on for rest of the handlers
--file : startup.ads
type Address_Array is array (Unsigned_8 range <>) of System.Address;
vector_table : Address_Array := (
Stack_Start,
Reset_Handler'Address,
NMI_Handler'Address,
HardFault_Handler'Address,
MemManage_Handler'Address,
BusFault_Handler'Address,
UsageFault_Handler'Address,
Reserved_Address,
Reserved_Address,
...
...
HASH_RNG_IRQHandler'Address,
FPU_IRQHandler'Address
);
Note that all the values of addresses in vector table above will be calculated at compile time (during linking).
Now that we have created vector table, but if we compile the code now then it is not guaranteed that vector table will be placed at very beginning of the executable, it would be placed at any place within the executable within the.data section.
Hence, we will place the vector table in a separate section ".isr_vector", and we will create our linker script in such a way that section .isr_vector is placed first in executable.
--file : startup.ads
pragma Linker_Section (vector_table, ".isr_vector");
And the linker script, note that the .isr_vector is placed first in .text (code) section of final executable :
--file : stm32_ls.ld
.text :
{
*(.isr_vector)
*(.text)
*(.text.*)
*(.init)
*(.fini)
*(.rodata)
*(.rodata.*)
/* Word alignment */
. = ALIGN(4);
/* end of .text section in Flash (VMA) */
_etext = .;
}> FLASH
Understanding the linker script is a wholly different thing, and we wouldn't be looking into same here, but you can refer to my comments in linker script or refer Thea's detailed blog post (link in references).
Reset Handler And What It DoesBack to core topic for this post, Reset Handler does following preparing an environment for the main application code to execute :
- Copy.data section (Initialized Global Variables) from ROM to RAM
- Initialize.bss section (Uninitialized Global Variables) to 0 in RAM.
- Initialize the std libraries (we won't be doing this as we are not making use of the standard libraries)
- And finally call the main application.
There are multiple variables declared in Reset_Handler to get the values different addresses associated with symbols like Sdata, Edata, Ldata, Sbss and Ebss. Using these symbols we calculate the size of.data and.bss sections (variables Data_Size and Bss_Size). And based on the values of sizes of these sections, we define sections as arrays (variables Data_In_Flash, Data_In_Sram and Bss_In_Sram). Note that these arrays are located at their respective symbol addresses. So, in a way we are not declaring any array, but telling the compiler that given binary data starting at given symbol address should be treated as an array of given size.
--file : startup.adb
-- Start of .data section in RAM
Sdata : Unsigned_8
with Import, Convention => Asm, External_Name => "_sdata";
-- End of .data section in RAM
Edata : Unsigned_8
with Import, Convention => Asm, External_Name => "_edata";
-- Load Address of .data section in FLASH (ROM)
Ldata : Unsigned_8
with Import, Convention => Asm, External_Name => "_la_data";
-- Start of .bss section in RAM
Sbss : Unsigned_8
with Import, Convention => Asm, External_Name => "_sbss";
-- End of .bss section in RAM
Ebss : Unsigned_8
with Import, Convention => Asm, External_Name => "_ebss";
-- Size of .data section
Data_Size : constant Unsigned_32 := Addr_To_U32(Edata'Address) - Addr_To_U32(Sdata'Address);
-- .data section in Flash
-- Index from 1 so as to avoid subtracting 1 from the size
Data_In_Flash : Section_Array (1 .. Data_Size)
with Import, Convention => Asm, External_Name => "_la_data";
-- .data section in RAM
Data_In_Sram : Section_Array (1 .. Data_Size)
with Import, Convention => Asm, External_Name => "_sdata";
-- Size of .bss section
Bss_Size : constant Unsigned_32 := Addr_To_U32(Ebss'Address) - Addr_To_U32(Sbss'Address);
-- .bss section in RAM
Bss_In_Sram : Section_Array (1 .. Bss_Size)
with Import, Convention => Asm, External_Name => "_sbss";
Now, first copy.data section from ROM to RAM, byte by byte :
--file : startup.adb
-- copy .data section from ROM to RAM
for J in Data_In_Sram'First .. Data_In_Sram'Last loop
Data_In_Sram(J) := Data_In_Flash(J);
end loop;
And, then initialize.bss section in RAM with zero, byte by byte :
--file : startup.adb
-- initialize .bss section in RAM
for J in Bss_In_Sram'First .. Bss_In_Sram'Last loop
Bss_In_Sram(J) := 16#00#;
end loop;
And finally call/execute the main procedure :
--file : startup.adb
-- call the main procedure
main_app;
Startup code execution is complete now and main application is now being executed. But continue reading for few more details.
Main Application Procedure NamingIf you look at the files blinky.ads and blinky.adb, you can see that the name of the main application procedure is blink, but the startup code calls the main procedure by name main_app. So firstly, how the blink to main_app procedure name is changed. And Secondly, the startup code (startup.ads or startup.adb) do not with the package blinky within which the procedure blink is located, then how does startup code call this procedure.
This is achieved by first exporting the blink procedure as symbol main_app.
--file : blinky.ads
procedure blink
with
Export,
Convention => Ada,
External_Name => "main_app";
and then importing the same symbol for procedure called main_app.
--file : startup.ads
procedure main_app
with
Import,
Convention => Ada,
External_Name => "main_app";
Advantage of doing this way is that we are not bound to name main procedure as main_app, we can name it anything we wish. And we will not need to modify the startup code if we change the name of main procedure, startup code will always reference main procedure as main_app.
The Main Application CodeAs already mentioned, the application is a very simple blinky of Red LED on STM32F4discovery board. The blink procedure is still as simple as an arduino blink code.
We would first initialize the LED. The Init_Leds procedure leds.adb first enables clock for PORTD (Red LED is connected to PD14), then configures PD14 as output and write 0 to PD14 (LED is off).
Then Led_On and Led_Off procedures write 0 and 1 values to Output Data Register to turn on and off the LED respectively.
CompilationWe won't go into how the compilation is done. But simply, we will be doing Compile > Link > Disassemble
We will be using the makefile for compilation activities, please refer makefile for this project. Also, -g switch is used for compiling, so that debug information is preserved while compiling.
Following make recipes (commands) are used :
- make all : To generate the final.elf, which is the executable to be uploaded to target
- make clean : To remove all the object files.
- make asmble : Just to assemble the source files.
- make disasm : Disassemble the relevant object files (elfs) once compiled using make all.
- make header : to print the header of final.elf, Just to look at the addresses.
- make emu : To debug/run the final.elf using arm-eabi-gnatemu that comes along with arm-elf ada compiler.
- make emu2 : To debug/run the final.elf using qemu-system-gnuarmeclipse provided by xpack.
- make debug : Just to launch the GDB.
- make load : To launch the OpenOCD with configuration for STM32F4Discovery board.
When performing make all and then make disasm, a map file final.map and disassembly files final.asm and final_i.asm will be generated (we are instructing the compiler to generate these files). Let's check these files.
When a source file is compiled into an object file (here.elf format), the object files consists of multiple sections, like :
- .text: it contains the compiled object code
- .rodata : it contains the read-only data, aka constants.
- .data: it contains all the initialized global variables
- .bss: it contains all the uninitialized global variables
- and more...
Now, we can verify a few things, to confirm whether the compilation went as per expected or not.
- if we see final.map file, then the very first thing starting at ROM start (address 0x0800_0000) is vector table from startup.ads file (startup__vector_table is notation of startup.vector_table, a dot is replaced by two underscores). So, we have confirmed from disassembly that vector table is located at beginning of ROM.
- Further, looking into final.asm file, we can see that first word in vector table (at address 0x8000_0000) is 0x20020000, that is equal to end of RAM and the initial Stack Pointer value (SP) from where stack will start.
- The second word within vector table (at address 0x0800_0004) is 0x080001c9. If we are correct, then these must be the address of Reset Handler function. Hence scroll down to this address (actually do a minus 1, = 0x080001c8, because in Thumb mode all the addresses should be odd) and you will see the Reset_Handler function in final.map located at given address. And magically, final_i.asm will also have Reset_Handler at given address but disassembly intermixed with source code, so you know exactly what instructions are executed for given line in source code.
- Also, pack_1.ads has two global variables Magic_word and Initialize_me.Magic_word is already initialized with value 16#1C0DEADA# (isn't it nice !) hence it should go to .data section. Initialize_me is uninitialized, hence should go to .bss section. We can confirm the same from the final.map file.
Debugging using an emulator will help identifying any issues/error without touching the hardware. Won't go into much detail, but simply :
- First launch a command window from directory where final.elf is located. Then launch QEMU using make emu2 and leave it as it is.
- Launch another command window from same directory. And launch GDB using make debug. and connect to qemu on the given TCP port used by qemu.
- Start debugging in second window (gdb) and when stepping through the lines related to turning Red LED on or off, you will see messages in first window (qemu) about Red LED being turned on or off.
You can refer the debug_qemu_gdb.txt for more about such debug session.
Debugging On Target Hardware with OpenOCD+GDBIt's not much different from debugging in emulator, but however the actual result we expect :
- First launch a command window from directory where final.elf is located. Then launch OpenOCD using make load and leave it as it is.
- Launch another command window from same directory. And launch GDB using make debug. and connect to openocd on the given TCP port used by openocd.
- Download elf to hardware and start debugging in second window (gdb) and when stepping through the lines related to turning Red LED on or off,..... you will see actual Red LED turning on and off !
You can refer the debug_harware_gdb.txt for more about such debug session.
Photo And VideoBy default the Ravenscar runtime for STM32F407 discovery (that comes along with the arm-elf compiler) comes with Red LED blinky as a last chance handler if anything goes wrong, so that we know that there is some issue.
BUT, here our main application itself is a Red LED blinky. AND if anything goes wrong with its execution, then basically how we would be able to use the same blinky as LCH, because it would have issues too. Hence, the LCH is a null procedure for current case to keep it simple.
The Minimal RuntimeIf you refer the commands in makefile, you can see an argument being supplied for RTS (Run Time System). This runtime is stored in a separate directory RTS_minimal_stm32f4.
For now, there are no body files (.adb) in this runtime directory, just specifications (.ads) to keep the things simple. So, we didn't need to compile runtime and link it. Note that these specifications are copied form Ravenscar zfp profile for STM32F4.
Software Delay (Nothing Too Fancy)Note that we are not initializing any clock on STM32F4, hence it will run with default 16MHz clock.
Also, for achieving delay between LED toggle, a simple software delay is used. The delay_sw simply counts until a certain number to keep CPU busy until some time, which is our delay. It is not an efficient method to achieve delay, but definitely an easy one.
--file : leds.adb
procedure delay_sw(This : Natural) is
Counter : Natural := 0;
begin
while (Counter < This) loop
Counter := Counter + 1;
end loop;
end delay_sw;
Additional HandlersWhen required to implement a handler for given exception/interrupt, you can implement the same in handler.ads and handler.adb. Refer below dummy implementation for HardFault_Handler.
--file : handlers.ads
procedure HardFault_Handler
with
Export,
Convention => Ada,
External_Name => "HardFault_Handler";
--------------------------------------------------------
--file : handlers.adb
procedure HardFault_Handler is
temp : Integer;
begin
temp := 11;
end HardFault_Handler;
It will overwrite the aliasing to Default_Handler done in startup.ads. You can verify the same from disassembly.
memcpy and heapI haven't implemented memcpy and any heap management related functions/procedures for now.
ReferencesAbove article is just a tip of iceberg, it wouldn't have been possible without what's below :
- Ada and Spark on ARM Cortex-M
- Simon Wright's Cortex-gnat-RTS
- STM32F407VG official documentation
- Cortex -M4 Devices Generic User Guide
- GNAT arm-eabi toolchain
- Kiran Nayak's Github Repo
- Fastbit EBA BareMetal Embedded youtube playlist
- Linker script tutorial on Thea's blog
Comments
Please log in or sign up to comment.