How Microcontrollers Integrate Software and Hardware

How Microcontrollers Integrate Software and Hardware

Through software development with IO and serial ports, we have already experienced embedded software development. I wonder if anyone is puzzled about why software can control hardware? I certainly had this question when I was learning about the 51 microcontroller. Today, let’s pause software development and analyze how microcontrollers achieve the integration of software and hardware. We will also analyze the compilation and execution of a basic microcontroller program.

Integration of Software and Hardware

Beginners often have a confusion: why can software control hardware? For example, when programming the 51 microcontroller, why can we output high and low levels on the IO port just by writing P1=0X55? To clarify this question, we first need to understand a concept: Address Space.

Addressing Space

What is address space? The so-called address space is the addressing range of the PC pointer, hence it is also called addressing space.

As we all know, our computers have 32-bit and 64-bit systems. Why is that? Because in a 32-bit system, the PC pointer is a 32-bit binary number, which is 0xffffffff, and the range is only 4G of addressing space. Now that memory is getting larger, 4G is simply not enough, so it needs to be expanded. To access memory beyond the 4G range, a 64-bit system was created. What about STM32? It is 32-bit, so the PC pointer is also 32-bit, and the addressing space is 4G.

Let’s take a look at the addressing space of STM32. In the data sheet “STM32F407_Data_Sheet.pdf”, there is a diagram that shows the addressing space allocation of STM32. All chips will have this diagram, usually called the Memory map. When using a new chip, always check this diagram first.

How Microcontrollers Integrate Software and Hardware

  • On the far left, there are 8 blocks, each 512M, totaling 4G, which is the chip’s addressing space.

  • Block 0 contains a section called FLASH, which is the internal FLASH where our program is downloaded, starting at address 0X800 0000. Note that this only has 1M of space. Now STM32 has chips with 2M flash. Where does the FLASH beyond 1M go? Please refer to the corresponding chip manual.

  • In block 1, there are two segments of SRAM totaling 128K, which is the memory we mentioned earlier, used to store variables used by the program. If needed, the program can also run from SRAM. Isn’t the 407 supposed to have 196K?

  • Actually, the 407 has 196K of memory, but 64K is not ordinary SRAM; it is located in block 0’s CCM. These two areas are not contiguous, and CCM can only be used by the core; peripherals cannot use CCM memory, otherwise, it will crash.

  • Block 2 is for Peripherals, which is the peripheral space. On the right, it mainly consists of APB1/APB2, AHB1/AHB2. What are these? We will discuss that later.

  • Blocks 3, 4, and 5 are for FSMC space, which can expand to external SRAM, NAND FLASH, LCD, and other peripherals.

Now that we have analyzed the addressing space, let’s go back and see how software controls hardware. In the example of outputting to the IO port, we configure the IO port by calling library functions. Let’s see how the library function does this.

For example:

GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);

This function actually assigns a value to a variable, specifically to the member BSRRL of the GPIOx structure.

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 /* Check the parameters */
 assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
 assert_param(IS_GPIO_PIN(GPIO_Pin));

 GPIOx->BSRRL = GPIO_Pin;
}

assert_param: This is an assertion used to check whether the input parameters meet the requirements. GPIOx is an input parameter, which is a pointer to a GPIO_TypeDef structure, so we use -> to access its members.

GPIOx is the parameter we passed, GPIOG. What exactly is it? It is defined in stm32f4xx.h.

#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)

GPIOG_BASE is also defined in the file as follows:

#define GPIOG_BASE           (AHB1PERIPH_BASE + 0x1800)
AHB1PERIPH_BASE, AHB1 address, is starting to make sense, right? Let's take a closer look.

/*!< Peripheral memory map */
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)

Now let’s find the definition of PERIPH_BASE.

#define PERIPH_BASE           ((uint32_t)0x40000000)    

At this point, we can see that operating on IO port G is actually manipulating a member of a structure at the address 0X40000000+0X1800. In other words, we are manipulating the registers at this location. Essentially, it is the same as operating on a normal variable, as shown in the two lines of code below, the difference being that variable i is at the SRAM space address, while 0X40000000+0X1800 is at the peripheral space address.

u32 i;
i = 0x55aa55aa;

The register at this peripheral space address is part of the IO port hardware. As shown in the figure, the output data register on the left is the register (memory, variable) we are manipulating, and its address is 0X40000000+0X1800+0x14.

How Microcontrollers Integrate Software and Hardware

Controlling other peripherals is similar; it involves writing data to the peripheral registers, just like manipulating memory, to control the peripherals.

Registers should actually be referred to as memory in general; peripheral registers should be called special registers. Gradually, everyone started calling peripherals registers, while others are referred to as memory or RAM. Why can registers control hardware peripherals? Because, roughly speaking, a BIT in a register is like a switch, ON is 1, OFF is 0. By using this electronic switch to control the circuit, we can control the peripheral hardware.

Pure Software – A Comprehensive Small Program

We have completed the control of serial ports and IO ports, but we only know how to use them, with no knowledge of the underlying workings. How does the program run? Where is the code stored? How is memory preserved? Next, we will learn the basic elements of embedded software through a simple program.

Analyzing Startup Code

  • Where does the function start running?

Every chip has a reset function. After a reset, the chip’s PC pointer (a register that indicates the program’s running position; for multi-stage pipeline chips, the PC may not align with the actual instruction execution position, but for now, we will assume they align) resets to a fixed value, usually 0x00000000. In STM32, it resets to 0X08000004. Therefore, the first code executed after a reset is at 0X08000004. Earlier, we copied a startup code file into the project, right? The file startup_stm32f40_41xxx.s is called startup code because the assembly program inside is the program executed after the reset. In the file, there is a data table called interrupt vector, which contains the execution addresses of various interrupts. Reset is also an interrupt.

When the chip resets, it loads the value of Reset_Handler ( a function pointer) from the interrupt table into the PC pointer, and the chip will execute the Reset_Handler function. ( A function entry is a pointer)

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler

The Reset_Handler function first executes the SystemInit function, which is in the standard library and mainly initializes the chip clock. Then it jumps to __main to execute. What is the __main function?

Is it the main function we defined in main.c? We will discuss this later.

; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

How does the chip know to execute the startup code first? Or how do we place this startup code at the reset position? This involves a file that is generally not focused on, called wujique.sct, located in the wujique\prj\Objects directory. This file is usually called a scatter loading file, and during linking, the compiler uses this file to place various code segments and variables.

In the MDK software, you can find settings related to this in the Options menu under Linker.

How Microcontrollers Integrate Software and Hardware

Uncheck the box for Use Memory Layout from Target Dialog, and previously unmodifiable boxes will now be editable. Click Edit to make changes.

How Microcontrollers Integrate Software and Hardware

The code editing box will display the contents of the scatter loading file, which currently only contains basic content.

This file is very powerful; by modifying it, you can configure many features of the program, such as: 1. Specifying the size and starting position of FLASH and RAM. When we divide the program into BOOT, CORE, APP, or even separate drivers, this can be useful. 2. Specifying the location of functions and variables, such as loading functions to run in RAM.

How Microcontrollers Integrate Software and Hardware

From this basic scatter loading file, we can see:

  • Line 6 defines ER_IROM1, which is our internal FLASH, starting at 0x08000000, with a size of 0x00080000.

  • Line 7 .o (RESET, +First) starts placing an .o file from 0x08000000, and specifies that the RESET block should be placed first using (RESET, +First). What is the RESET block? Please refer to the startup code; the interrupt vector is an AREA named RESET, which is READONLY. This way, after compilation, the RESET block will be placed at 0x08000000, meaning the interrupt vector will be located there. DCD allocates space, 4 bytes, the first being __initial_sp, and the second being the pointer to the Reset_Handler function. In other words, the compiled program will place the pointer (address) of the Reset_Handler function at 0x800000+4. Therefore, when the chip resets, it can find the reset function Reset_Handler.

  • Line 8 *(InRoot$$Sections) What is this? GOOGLE it! We will discuss it later.

  • Line 9 .ANY (+RO) means all other RO sections will be placed afterwards. This means that other code will follow the startup code.

  • Line 11 RW_IRAM1 0x20000000 0x00020000 defines the size of RAM.

  • Line 12 .ANY (+RW +ZI) specifies that all RW and ZI variables will be placed in RAM. RW and ZI refer to variables, and this line specifies where the variables will be stored.

Analyzing User Code

At this point, the basic startup process has been analyzed. Next, we will analyze user code, starting with the main function. 1. After the program jumps to the main function: RCC_GetClocksFreq retrieves the RCC clock frequency; SysTick_Config configures SysTick, enabling the SysTick interrupt every 10 milliseconds.

Delay(5); introduces a delay of 50 milliseconds.

int main(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;

/*!< At this stage the microcontroller clock setting is already configured,
       this is done through SystemInit() function which is called from startup
       files before to branch to application main.
       To reconfigure the default setting of SystemInit() function,
       refer to system_stm32f4xx.c file */

/* SysTick end of count event each 10ms */
  RCC_GetClocksFreq(&amp;RCC_Clocks);
  SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);

/* Add your application code here */
/* Insert 50 ms delay */
  Delay(5);
}

2. The initialization of IO is not discussed here; we enter while(1), which is an infinite loop. Embedded programs are always in an infinite loop; otherwise, they will run away.

/* Initialize LED IO port */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOG, &amp;GPIO_InitStructure);    

/* Infinite loop */
mcu_uart_open(3);
while (1)
{
  GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
  Delay(100);
  GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
  Delay(100);
  mcu_uart_test();

  TestFun(TestTmp2);
}

3. Inside the while(1) loop, the TestFun function is called, which uses two global variables and two local variables.

/* Private functions ---------------------------------------------------------*/
u32 TestTmp1 = 5;// Global variable, initialized to 5
u32 TestTmp2;// Global variable, uninitialized

const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};

u8 TestFun(u32 x)// Function with one parameter, returning a u8 value
{
 u8 test_tmp1 = 4;// Local variable, initialized
 u8 test_tmp2;// Local variable, uninitialized

static u8 test_tmp3 = 0;// Static local variable

 test_tmp3++;

 test_tmp2 = x;

if(test_tmp2&gt; TestTmp1)
  test_tmp1 = 10;
else
  test_tmp1 = 5;

 TestTmp2 +=TestTmp3[test_tmp1];

return test_tmp1;
}

Then the program continues executing in the while loop of the main function. What about interrupts? Yes, interrupts interrupt the normal execution flow of the program. Interrupts are the normal execution flow interruptions. Let’s check the Delay function; if uwTimingDelay is not equal to 0, it will wait indefinitely? Who will set uwTimingDelay to 0?

/**
  * @brief  Inserts a delay time.
  * @param  nTime: specifies the delay time length, in milliseconds.
  * @retval None
  */
void Delay(__IO uint32_t nTime)
{
  uwTimingDelay = nTime;

  while(uwTimingDelay != 0);
}

Search for the uwTimingDelay variable. The function TimingDelay_Decrement will decrement the variable until it reaches 0.

/**
  * @brief  Decrements the TimingDelay variable.
  * @param  None
  * @retval None
  */
void TimingDelay_Decrement(void)
{
  if (uwTimingDelay != 0x00)
  {
    uwTimingDelay--;
  }
}

Where is this function executed? Upon searching, it runs in the SysTick_Handler function. Who uses this function?

/**
  * @brief  This function handles SysTick Handler.
  * @param  None
  * @retval None
  */
void SysTick_Handler(void)
{
  TimingDelay_Decrement();
}

Upon searching, this function is present in the interrupt vector table, meaning that the function pointer is stored in the interrupt vector table. When an interrupt occurs, this function will be executed. Of course, there will be operations to save and restore the context when entering and exiting interrupts. This mainly involves assembly, which we will not analyze for now. If interested, you can research it yourself. Typically, we do not need to worry about context switching when developing programs now.

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

Remaining Questions

1. What is the __main function? Is it the main function we defined in main.c? 2. What is *(InRoot$$Sections) in the scatter loading file? 3. When is the ZI section, which is the zero-initialized data section, initialized? Who initializes it?

Why were these questions left unanswered earlier? Because they are all related! Follow the clues!

Understanding Code Composition through MAP Files

Compilation Results

After compiling the program, information will be output in the Build Output window below:

*** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
Build target 'wujique'
compiling stm32f4xx_it.c...
...
assembling startup_stm32f40_41xxx.s...
compiling misc.c...
...
compiling mcu_uart.c...
linking...
Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
FromELF: creating hex file...
".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed:  00:00:32
  • The build target is wujique.
  • C files are compiling, assembly files are assembling; this process is called compilation.
  • After compilation, linking occurs.
  • Finally, we obtain a compilation result: 9038 bytes of code, 990 bytes of RO data, 40 bytes of RW data, and 6000 bytes of ZI data. CODE is the code, which is easy to understand; what are RO, RW, and ZI?
  • FromELF creates a hex file; FromELF is a useful tool that needs to be added to the options to use.

MAP File Configuration

More specific compilation information is in the map file. In MDK Options, we can see that all information is stored in \Listings\wujique.map

By default, many compilation information options may not be checked; checking all options will increase compilation time.

How Microcontrollers Integrate Software and Hardware

MAP File

Open the map file; it looks chaotic? You will get used to it. We will focus on the key points.

How Microcontrollers Integrate Software and Hardware

  • Overall information from the map

Starting from the end , did you see? The last section of the map content describes the basic overview of the entire program.

How much RO? What exactly is RO?

How much RW? What is RW?

Why does ROM not include ZI Data? Why does it include RW Data?

How Microcontrollers Integrate Software and Hardware

  • Image Component Sizes

Looking up, we see Image component sizes, which is more detailed than the overall statistics.

This part of the content describes the overview of each source file.

First, it is our own source code; this program has little code, only main.o, wujique_log.o, and some other STM32 library files.

How Microcontrollers Integrate Software and Hardware

The second part is the files in the library; did you see? There is a main.o. Is the main function our defined main function? Obviously not; our main function is in the main.o file. In such a small project, so many libraries are used; have you ever paid attention to this? Probably not, unless you have previously compressed a program originally on 1M flash to run on 512K.

How Microcontrollers Integrate Software and Hardware

The third part is also from the library, which we have not analyzed yet.

How Microcontrollers Integrate Software and Hardware

What are library files? Library files are code libraries that others have already written. In the code, we often include some header files, for example:

#include &lt;stdarg.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;  

These are the header files of the library. These header files are stored in the installation directory of the MDK development tool. The library functions we often use include: memcpy, memcmp, strcmp, etc. As long as the code includes these functions, the library files will be linked.

  • File MAP

Further up is the file MAP, which shows the location of each file’s code segments (functions) and variables in ROM and RAM. First, in ROM at 0x08000000, the RESET from startup_stm32f40_41xxx.o is indeed placed there.

What are library files?

Library files are code libraries that others have already written.

In the code, we often include some header files, for example:

#include &lt;stdarg.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;   

These are the header files of the library. These header files are stored in the installation directory of the MDK development tool.

The library functions we often use include:

memcpy, memcmp, strcmp, etc.

As long as the code includes these functions, the library files will be linked.

  • File MAP

Further up is the file MAP, which shows the location of each file’s code segments (functions) and variables in ROM and RAM. First, in ROM at 0x08000000, the RESET from startup_stm32f40_41xxx.o is indeed placed there.

How Microcontrollers Integrate Software and Hardware

Each file has multiple lines, for example, the serial port has four functions.

How Microcontrollers Integrate Software and Hardware

Then for RAM, the variables in main.o are placed at 0x20000000, totaling 0x0000000c, with types Data and RW. The serial port has two types of variables, data and bss. What is bss? These two names are section names, meaning segments. Looking at the previous type and Attr,

RW Data is placed in the .data segment; RW Zero is placed in the .bss segment. RW Zero is actually ZI. Which variables are RW and which are ZI?

How Microcontrollers Integrate Software and Hardware

At this point, we can explain the following concepts:

Code refers to the code and functions.

RO Data refers to read-only variables, such as arrays modified with const.

RW Data refers to read-write variables, such as global variables and static local variables.

ZI Data refers to read-write variables that are automatically initialized to 0, mostly arrays, placed in the bss segment.

RO Size equals code plus read-only variables.

RW Size equals read-write variables (including those automatically initialized to 0), which is the size of RAM.

ROM Size is the size of the target file after compilation, which is the size of FLASH. But why? Why does it include RW Data? Because all global variables need an initialization value (even if not truly initialized, the system will allocate an initialization space). For example, if we define a variable u8 i = 8; this global variable, 8, needs to be stored in the FLASH area.

How Microcontrollers Integrate Software and Hardware

Let’s take a look at the functions; earlier we had a question: Is __main the same function as main? Upon searching for main, we find that main is indeed main, located at 0x08000579.

How Microcontrollers Integrate Software and Hardware

main is main, located at 0x08000189.

How Microcontrollers Integrate Software and Hardware

What happened between __main and main? Remember that line in the scatter loading file?

*(InRoot$$Sections)

__main is within this section. The following image shows the address of __main, at 0x08000189. __Vectors is the interrupt vector, placed at the very beginning.

How Microcontrollers Integrate Software and Hardware

In the scatter loading file, right after RESET is *(InRoot$$Sections).

How Microcontrollers Integrate Software and Hardware

Moreover, the RESET section is exactly 0x00000188 in size.

How Microcontrollers Integrate Software and Hardware

Coincidence? Refer to the PPT document “ARM Embedded Software Development.ppt” or Google it yourself.

How Microcontrollers Integrate Software and Hardware

What functionality does this segment of code accomplish? It mainly initializes the ZI code, which means initializing a portion of RAM to 0. Other environment initializations… Typically, we do not need to worry about this part.

  • Others Further up are other information, such as what optimizations were made and which functions were removed.

Conclusion

At this point, we have a basic impression of how a program is composed and how it runs. However, there will be further detailed explanations regarding interrupts.

How Microcontrollers Integrate Software and Hardware

How Microcontrollers Integrate Software and Hardware

Part of the electronic book screenshots

How Microcontrollers Integrate Software and Hardware

【Complete Set of Hardware Learning Materials】

How Microcontrollers Integrate Software and Hardware

Leave a Comment