Most of the HAL consists of simple macros or functions that are called via the interfaces described in the previous section. These just perform whatever operation is required by accessing the hardware and then return. The exception to this is the handling of exceptions: either synchronous hardware traps or asynchronous device interrupts. Here control is passed first to the HAL, which then passed it on to eCos or the application. After eCos has finished with it, control is then passed back to the HAL for it to tidy up the CPU state and resume processing from the point at which the exception occurred.
The HAL exceptions handling code is usually found in the file vectors.S in the architecture HAL. Since the reset entry point is usually implemented as one of these it also deals with system startup.
The exact implementation of this code is under the control of the HAL implementer. So long as it interacts correctly with the interfaces defined previously it may take any form. However, all current implementation follow the same pattern, and there should be a very good reason to break with this. The rest of this section describes these operate.
Exception handling normally deals with the following broad areas of functionality:
Startup and initialization.
Hardware exception delivery.
Default handling of synchronous exceptions.
Default handling of asynchronous interrupts.
Execution normally begins at the reset vector with the machine in a minimal startup state. From here the HAL needs to get the machine running, set up the execution environment for the application, and finally invoke its entry point.
The following is a list of the jobs that need to be done in approximately the order in which they should be accomplished. Many of these will not be needed in some configurations.
Initialize the hardware. This may involve initializing several subsystems in both the architecture, variant and platform HALs. These include:
Initialize various CPU status registers. Most importantly, the CPU interrupt mask should be set to disable interrupts.
Initialize the MMU, if it is used. On many platforms it is only possible to control the cacheability of address ranges via the MMU. Also, it may be necessary to remap RAM and device registers to locations other than their defaults. However, for simplicity, the mapping should be kept as close to one-to-one physical-to-virtual as possible.
Set up the memory controller to access RAM, ROM and I/O devices correctly. Until this is done it may not be possible to access RAM. If this is a ROMRAM startup then the program code can now be copied to its RAM address and control transferred to it.
Set up any bus bridges and support chips. Often access to device registers needs to go through various bus bridges and other intermediary devices. In many systems these are combined with the memory controller, so it makes sense to set these up together. This is particularly important if early diagnostic output needs to go through one of these devices.
Set up diagnostic mechanisms. If the platform includes an LED or LCD output device, it often makes sense to output progress indications on this during startup. This helps with diagnosing hardware and software errors.
Initialize floating point and other extensions such as SIMD and multimedia engines. It is usually necessary to enable these and maybe initialize control and exception registers for these extensions.
Initialize interrupt controller. At the very least, it should be configured to mask all interrupts. It may also be necessary to set up the mapping from the interrupt controller's vector number space to the CPU's exception number space. Similar mappings may need to be set up between primary and secondary interrupt controllers.
Disable and initialize the caches. The caches should not normally be enabled at this point, but it may be necessary to clear or initialize them so that they can be enabled later. Some architectures require that the caches be explicitly reinitialized after a power-on reset.
Initialize the timer, clock etc. While the timer used for RTC interrupts will be initialized later, it may be necessary to set up the clocks that drive it here.
The exact order in which these initializations is done is
architecture or variant specific. It is also often not necessary
to do anything at all for some of these options. These fragments
of code should concentrate on getting the target up and running so
that C function calls can be made and code can be run. More
complex initializations that cannot be done in assembly code may
be postponed until calls to
hal_variant_init()
or
hal_platform_init()
are made.
Not all of these initializations need to be done for all startup types. In particular, RAM startups can reasonably assume that the ROM monitor or loader has already done most of this work.
Set up the stack pointer, this allows subsequent initialization code to make proper procedure calls. Usually the interrupt stack is used for this purpose since it is available, large enough, and will be reused for other purposes later.
Initialize any global pointer register needed for access to globally defined variables. This allows subsequent initialization code to access global variables.
If the system is starting from ROM, copy the ROM template of the .data section out to its correct position in RAM. (the Section called Linker Scripts in Chapter 4).
Zero the .bss section.
Create a suitable C call stack frame. This may involve making stack space for call frames, and arguments, and initializing the back pointers to halt a GDB backtrace operation.
Call hal_variant_init()
and
hal_platform_init()
. These will perform any
additional initialization needed by the variant and platform. This
typically includes further initialization of the interrupt
controller, PCI bus bridges, basic IO devices and enabling the
caches.
Call cyg_hal_invoke_constructors()
to run any
static constructors.
Call cyg_start()
. If
cyg_start()
returns, drop into an infinite
loop.