ARM Keil Scatter File Complexity and Startup Code Initialization
The core issue revolves around the implementation of custom startup code in an ARM Keil environment, specifically when transitioning from a GCC-based toolchain to ARMCLANG. The primary challenge lies in understanding and replicating the functionality of the __main
function, which is typically responsible for initializing the memory regions, including zero-initializing the ZI (Zero-Initialized) sections and copying the RO (Read-Only) and RW (Read-Write) sections from their load addresses to their execution addresses. The scatter file, which is used in ARM Keil to define memory regions, is more complex and less intuitive compared to the linker script used in GCC. This complexity is compounded by the need to handle data decompression for RW sections, which is a feature that can be disabled but adds another layer of understanding to the startup code implementation.
The scatter file in ARM Keil defines how different sections of the code and data are placed in memory. It specifies the load and execution addresses for each section, which is crucial for the correct initialization of the memory regions. The scatter file also defines the symbols that are used in the startup code to perform the initialization, such as Image$$region_name$$ZI$$Base
, Load$$region_name$$RO$$Base
, and Image$$region_name$$RO$$Base
. These symbols are generated by the linker and are used to determine the base addresses and lengths of the different memory regions.
The startup code must perform several critical tasks to ensure that the system is properly initialized before the main application code is executed. These tasks include zero-initializing the ZI sections, copying the RO sections from their load addresses to their execution addresses, and decompressing the RW sections if data compression is enabled. The startup code must also ensure that the stack and heap are properly initialized, and that any necessary hardware initialization is performed.
Scatter File Symbol Misinterpretation and Data Compression Overhead
One of the primary causes of the difficulty in implementing custom startup code is the misinterpretation of the symbols generated by the scatter file. The symbols such as Image$$region_name$$ZI$$Base
, Load$$region_name$$RO$$Base
, and Image$$region_name$$RO$$Base
are critical for the correct initialization of the memory regions, but their usage can be confusing, especially for those transitioning from GCC to ARMCLANG. The scatter file syntax and the way these symbols are generated and used can be significantly different from the linker script used in GCC, leading to potential errors in the startup code.
Another significant cause of complexity is the data compression feature in ARM Keil, which can add overhead to the startup code. Data compression is used to reduce the size of the RW sections, but it requires additional steps in the startup code to decompress the data before it can be used. This decompression process can be complex and may require additional memory and processing power, which can be a challenge in resource-constrained systems. Disabling data compression can simplify the startup code, but it may also increase the size of the final binary, which can be a trade-off that needs to be carefully considered.
The scatter file also defines the memory map for the system, which includes the placement of the stack, heap, and other memory regions. The startup code must ensure that these regions are properly initialized and that the memory map is correctly configured. This can be particularly challenging in systems with multiple memory regions, such as those with external memory or multiple RAM banks. The startup code must also handle any memory protection or caching settings that are required for the system to function correctly.
Detailed Steps for Implementing Custom Startup Code in ARM Keil
To implement custom startup code in ARM Keil, the following detailed steps should be followed:
Understanding the Scatter File and Symbol Generation
The first step is to thoroughly understand the scatter file and the symbols generated by the linker. The scatter file defines the memory regions and the placement of the different sections of the code and data. The symbols generated by the linker, such as Image$$region_name$$ZI$$Base
, Load$$region_name$$RO$$Base
, and Image$$region_name$$RO$$Base
, are used to determine the base addresses and lengths of the different memory regions. These symbols must be correctly interpreted and used in the startup code to ensure that the memory regions are properly initialized.
The scatter file typically includes sections for the stack, heap, and other memory regions, as well as the placement of the code and data sections. The startup code must ensure that these regions are properly initialized and that the memory map is correctly configured. This may involve setting up the stack and heap pointers, initializing the memory protection unit (MPU), and configuring any caching settings.
Zero-Initializing the ZI Sections
The next step is to zero-initialize the ZI sections. The ZI sections are those that are not explicitly initialized in the code and are typically used for uninitialized global and static variables. The startup code must zero-initialize these sections to ensure that they start with a known value. This is typically done by iterating over the ZI sections and setting each byte to zero.
The base address and length of the ZI sections are determined by the symbols generated by the linker, such as Image$$region_name$$ZI$$Base
and Image$$region_name$$ZI$$Length
. The startup code must use these symbols to determine the range of memory that needs to be zero-initialized. This is typically done using a loop that iterates over the memory range and sets each byte to zero.
Copying the RO Sections
The startup code must also copy the RO sections from their load addresses to their execution addresses. The RO sections are typically stored in flash memory and must be copied to RAM before they can be executed. The base address and length of the RO sections are determined by the symbols generated by the linker, such as Load$$region_name$$RO$$Base
and Image$$region_name$$RO$$Length
.
The startup code must use these symbols to determine the range of memory that needs to be copied. This is typically done using a loop that iterates over the memory range and copies each byte from the load address to the execution address. The startup code must also ensure that the RO sections are properly aligned and that any necessary padding is added.
Handling RW Data Decompression
If data compression is enabled, the startup code must also handle the decompression of the RW sections. The RW sections are typically stored in flash memory in a compressed format and must be decompressed before they can be used. The base address and length of the RW sections are determined by the symbols generated by the linker, such as Load$$region_name$$RW$$Base
and Image$$region_name$$RW$$Length
.
The startup code must use these symbols to determine the range of memory that needs to be decompressed. This is typically done using a decompression algorithm that is provided by the toolchain or implemented in the startup code. The startup code must also ensure that the decompressed data is properly aligned and that any necessary padding is added.
Disabling Data Compression
If data compression is not required, it can be disabled using the --datacompressor=off
option in the ARMCLANG linker. Disabling data compression can simplify the startup code by eliminating the need to handle decompression. However, this may increase the size of the final binary, which can be a trade-off that needs to be carefully considered.
Disabling data compression can be particularly useful in systems with limited processing power or memory, where the overhead of decompression may be too high. It can also simplify the startup code by reducing the number of steps that need to be performed during initialization.
Initializing the Stack and Heap
The startup code must also initialize the stack and heap. The stack is used for function calls and local variables, while the heap is used for dynamic memory allocation. The base address and size of the stack and heap are typically defined in the scatter file and must be initialized by the startup code.
The startup code must ensure that the stack and heap pointers are properly initialized and that the memory regions are correctly configured. This may involve setting up the stack pointer to point to the top of the stack region and initializing the heap pointer to point to the base of the heap region. The startup code must also ensure that the stack and heap regions are properly aligned and that any necessary padding is added.
Hardware Initialization
Finally, the startup code must perform any necessary hardware initialization. This may include setting up the clock system, configuring the memory controller, and initializing any peripherals that are required for the system to function correctly. The startup code must ensure that the hardware is properly configured before the main application code is executed.
The hardware initialization steps will vary depending on the specific system and the requirements of the application. The startup code must ensure that all necessary hardware components are properly initialized and that any required configurations are applied. This may involve setting up the clock system to provide the correct clock frequencies, configuring the memory controller to access external memory, and initializing any peripherals that are required for the system to function correctly.
Example Startup Code Implementation
Below is an example of how the startup code might be implemented in ARM Keil:
extern unsigned char Image$$region_name$$ZI$$Base;
extern unsigned char Image$$region_name$$ZI$$Length;
extern unsigned char Load$$region_name$$RO$$Base;
extern unsigned char Image$$region_name$$RO$$Base;
extern unsigned char Image$$region_name$$RO$$Length;
extern unsigned char Load$$region_name$$RW$$Base;
extern unsigned char Image$$region_name$$RW$$Base;
extern unsigned char Image$$region_name$$RW$$Length;
void startup_code(void) {
// Zero-initialize the ZI sections
unsigned char *zi_base = &Image$$region_name$$ZI$$Base;
unsigned int zi_length = (unsigned int)&Image$$region_name$$ZI$$Length;
for (unsigned int i = 0; i < zi_length; i++) {
zi_base[i] = 0;
}
// Copy the RO sections
unsigned char *load_ro_base = &Load$$region_name$$RO$$Base;
unsigned char *image_ro_base = &Image$$region_name$$RO$$Base;
unsigned int ro_length = (unsigned int)&Image$$region_name$$RO$$Length;
for (unsigned int i = 0; i < ro_length; i++) {
image_ro_base[i] = load_ro_base[i];
}
// Handle RW data decompression (if enabled)
unsigned char *load_rw_base = &Load$$region_name$$RW$$Base;
unsigned char *image_rw_base = &Image$$region_name$$RW$$Base;
unsigned int rw_length = (unsigned int)&Image$$region_name$$RW$$Length;
// Decompression logic here (if needed)
// Initialize the stack and heap
// Stack and heap initialization logic here
// Perform hardware initialization
// Hardware initialization logic here
// Jump to the main application code
main();
}
This example demonstrates the key steps involved in implementing custom startup code in ARM Keil. The startup code zero-initializes the ZI sections, copies the RO sections, handles RW data decompression (if enabled), initializes the stack and heap, and performs any necessary hardware initialization before jumping to the main application code.
Conclusion
Implementing custom startup code in ARM Keil can be challenging, especially when transitioning from a GCC-based toolchain. The scatter file syntax and symbol generation can be complex, and the need to handle data compression adds another layer of complexity. However, by following the detailed steps outlined above, it is possible to implement custom startup code that correctly initializes the memory regions and prepares the system for the main application code. Disabling data compression can simplify the startup code, but it may also increase the size of the final binary, which is a trade-off that needs to be carefully considered. Properly understanding the scatter file and the symbols generated by the linker is crucial for the successful implementation of custom startup code in ARM Keil.