¿Qué es un gestor de arranque?
El gestor de arranque es el primer segmento de código que se ejecuta en un sistema integrado después del encendido. Una vez completada la inicialización de la CPU y el hardware relevante, carga la imagen del sistema operativo o el programa de aplicación integrado en la memoria y, a continuación, pasa al espacio donde reside el sistema operativo, iniciando así el funcionamiento del mismo. Este proceso es fundamental para cualquiera que se dedique a la programación de microcontroladores específicos como el STM32.
Al igual que un programa de aplicación, un gestor de arranque es un programa independiente que contiene componentes esenciales como código de inicio, interrupciones, un programa principal (función Boot_main) y, opcionalmente, un sistema operativo. A pesar de su pequeño tamaño, el gestor de arranque engloba funcionalidades críticas.
Los cargadores de arranque suelen depender en gran medida del hardware y son especialmente importantes en el ámbito de los sistemas integrados. Como resultado, crear un cargador de arranque de aplicación universal en el ámbito integrado es casi imposible. No obstante, aún podemos generalizar algunos conceptos sobre los cargadores de arranque para guiar a los usuarios en el diseño y la implementación de cargadores de arranque específicos.
Modos de funcionamiento de un gestor de arranque
La mayoría de los gestores de arranque constan de dos modos de funcionamiento distintos: el modo de arranque y el modo de descarga.
Al encenderse, el gestor de arranque inicializa el entorno de software y hardware del sistema y selecciona uno de los modos de funcionamiento en función de las condiciones actuales del hardware. Esto implica configurar el modo de funcionamiento de la CPU, inicializar la memoria, desactivar las interrupciones y gestionar tareas como desactivar la MMU/caché.
Modo de carga del arranque:
Este modo, también conocido como modo «autónomo», implica que el cargador de arranque carga de forma autónoma el sistema operativo desde un dispositivo de almacenamiento de estado sólido en la máquina de destino a la RAM. Todo este proceso se lleva a cabo sin intervención alguna por parte del usuario. Este modo representa el funcionamiento normal del cargador de arranque.
Modo de descarga:
En este modo, el cargador de arranque de la máquina de destino inicia la comunicación a través de medios tales como conexiones serie o de red con una máquina host para descargar archivos. Los archivos obtenidos de la máquina host suelen almacenarse en la RAM de la máquina de destino por el cargador de arranque antes de escribirse en la memoria Flash o en un dispositivo de almacenamiento de estado sólido similar en la máquina de destino.
¿Cómo funciona un gestor de arranque?
Hay dos tipos de procesos de inicio para un gestor de arranque: de una sola etapa y de varias etapas. Por lo general, los cargadores de arranque de varias etapas poseen funcionalidades más complejas y una mayor portabilidad. Los cargadores de arranque que se inician desde dispositivos de almacenamiento de estado sólido suelen utilizar un proceso de dos etapas, dividido en etapa 1 y etapa 2: la etapa 1 realiza la inicialización del hardware, prepara el espacio de memoria para la etapa 2, copia la etapa 2 a la memoria, configura la pila y, a continuación, pasa a la etapa 2.
Etapa 1 del gestor de arranque
Inicialización del dispositivo de hardware
- Desactivar todas las interrupciones: El manejo de las interrupciones suele ser responsabilidad de los controladores de dispositivos del sistema operativo, por lo que el cargador de arranque puede ignorar las respuestas a las interrupciones durante su ejecución. El enmascaramiento de interrupciones se puede lograr modificando el registro de máscara de interrupciones o el registro de estado de la CPU (como el registro CPSR de ARM).
- Establecer la velocidad de la CPU y la frecuencia del reloj.
- Inicializar la RAM: Esto incluye configurar correctamente los registros de función del controlador de memoria del sistema y varios registros de control del banco de memoria.
- Inicialización de los LED: los LED suelen controlarse a través de GPIO para indicar el estado del sistema (OK o Error). Si no hay LED, la inicialización de UART para imprimir el logotipo del cargador de arranque o la información de caracteres a través de la comunicación serie puede servir para este propósito.
- Desactivar la caché interna de instrucciones/datos de la CPU.
Prepare espacio en la RAM para cargar la etapa 2 del gestor de arranque.
Para una ejecución más rápida, la etapa 2 se carga normalmente en la RAM. Por lo tanto, se debe asignar un rango de memoria disponible para cargar la etapa 2 del cargador de arranque.
Dado que la etapa 2 suele contener código en lenguaje C, el espacio necesario debe tener en cuenta tanto el tamaño ejecutable de la etapa 2 como el espacio de la pila. Además, es preferible que el espacio se alinee con el tamaño de la página de memoria (normalmente 4 KB). Por lo general, 1 MB de espacio RAM es suficiente. El rango de direcciones específico se puede elegir arbitrariamente. Por ejemplo, un enfoque común es asignar la imagen ejecutable de la etapa 2 para que se ejecute dentro de un espacio de 1 MB a partir de la dirección base de la RAM del sistema 0xc0200000. Sin embargo, se recomienda asignar la etapa 2 al 1 MB superior de todo el espacio de RAM (es decir, (RamEnd-1MB) – RamEnd).
Denotemos el tamaño del rango de espacio RAM asignado como «stage2_size» (en bytes), y las direcciones de inicio y fin como «stage2_start» y «stage2_end» (ambas direcciones alineadas con límites de 4 bytes). Por lo tanto:
stage2_end = stage2_start + stage2_size
Además, es imprescindible asegurarse de que el rango de direcciones asignado sea realmente espacio RAM legible y escribible. Para garantizarlo, es necesario probar el rango de direcciones asignado. Un método de prueba adecuado, como el utilizado por «blob», consiste en comprobar la capacidad de lectura y escritura de las dos primeras palabras de cada página de memoria.
Copiar la etapa 2 del cargador de arranque al espacio RAM.
Para ello, asegúrese de dos cosas:
- La ubicación de la imagen ejecutable de la etapa 2 en el dispositivo de almacenamiento de estado sólido.
- La dirección de inicio del espacio RAM.
Establecer el puntero de pila (SP)
El ajuste del puntero de pila (sp) prepara la ejecución del código en lenguaje C. Normalmente, el valor de sp se puede ajustar a (stage2_end-4), lo que representa el extremo superior del espacio RAM de 1 MB asignado en la sección 3.1.2 (la pila crece hacia abajo).
Además, antes de configurar el puntero de pila, es posible desactivar el LED como señal para los usuarios de que la transición a la etapa 2 es inminente.
Tras estos pasos de ejecución, la disposición de la memoria física del sistema debería ser similar a la del siguiente diagrama.

Saltar al punto de entrada C de la etapa 2.
Una vez que todo lo anterior esté listo, puede pasar a la etapa 2 del cargador de arranque para ejecutarlo. Por ejemplo, en los sistemas ARM esto se puede hacer modificando el registro de la PC a la dirección adecuada. La disposición de la memoria del sistema cuando la imagen ejecutable de la etapa 2 del cargador de arranque acaba de copiarse al espacio RAM se muestra en la figura anterior.
Etapa 2 del gestor de arranque
Inicialización del dispositivo de hardware
- Inicializar al menos un puerto serie para la comunicación de salida de E/S con los usuarios del terminal.
- Inicialice los temporizadores y otros componentes de hardware.
Antes de inicializar estos dispositivos, también es posible iluminar el LED para indicar el inicio de la ejecución de la función main(). Tras la inicialización del dispositivo, se puede mostrar cierta información, como cadenas de nombres de programas y números de versión.
Detección del mapeo de memoria del sistema
El mapeo de memoria se refiere a la asignación de rangos de direcciones dentro del espacio de direcciones físicas completo de 4 GB para direccionar las unidades de RAM del sistema. Por ejemplo, en la CPU SA-1100, un espacio de direcciones de 512 MB que comienza en 0xC000,0000 sirve como espacio de direcciones RAM del sistema. En el caso de la CPU Samsung S3C44B0X, se utiliza un espacio de direcciones de 64 MB entre 0x0c00,0000 y 0x1000,0000 como espacio de direcciones RAM del sistema. Aunque las CPU suelen reservar una parte considerable del espacio de direcciones para la RAM del sistema, es posible que no se utilice todo el espacio de direcciones RAM reservado al construir sistemas integrados específicos. Por lo tanto, los sistemas integrados a menudo asignan solo una parte del espacio de direcciones RAM reservado de la CPU a las unidades RAM, dejando sin utilizar parte del espacio de direcciones RAM reservado. Dado este hecho, la etapa 2 del cargador de arranque debe examinar todo el mapeo de memoria del sistema antes de intentar cualquier acción (como leer una imagen del núcleo almacenada en la memoria flash en el espacio RAM). Necesita saber qué partes del espacio de direcciones RAM reservado de la CPU están realmente mapeadas a unidades de direcciones RAM y cuáles se encuentran en estado «sin utilizar».
Descripción del mapeo de memoria
La siguiente estructura de datos se puede utilizar para describir un rango de direcciones continuo en el espacio de direcciones RAM:
typedef struct memory_area_struct {
u32 start; /* the base address of the memory region */
u32 size; /* the byte number of the memory region */
int used;
} memory_area_t;
Estos rangos de direcciones contiguos dentro del espacio de direcciones RAM pueden encontrarse en uno de dos estados:
- used=1 indica que el rango de direcciones continuo se ha implementado y se asigna realmente a unidades RAM.
- used=0 indica que el rango de direcciones contiguas no está implementado en el sistema y permanece sin usar.
Basándose en la estructura de datos memory_area_t descrita anteriormente, todo el espacio de direcciones RAM reservado de la CPU puede representarse mediante una matriz de tipo memory_area_t, como se muestra a continuación:
memory_area_t memory_map[NUM_MEM_AREAS] = {
[0 ... (NUM_MEM_AREAS - 1)] = {
.start = 0,
.size = 0,
.used = 0
},
};
Detección de mapeo de memoria
He aquí un algoritmo sencillo pero eficaz para detectar la situación de mapeo de memoria dentro de todo el espacio de direcciones RAM:
/* Initialize the array */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map[i].used = 0;
/* Write 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
*(u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
/*
* Check whether the address space starting from base address
* MEM_START + i * PAGE_SIZE, with a size of PAGE_SIZE, is a valid RAM address space.
*/
Call the algorithm test_mempage() from section 3.1.2;
if (current memory page is not a valid RAM page) {
/* no RAM here */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM.
* However, we need to determine if it's an alias of some address page within the 4GB address space.
*/
if (*(u32 *)addr != 0) { /* alias? */
/* This memory page is an alias of an address page within the 4GB address space. */
if (memory_map[i].used )
i++;
continue;
}
/*
* The current page is a valid address range mapped to RAM,
* and it's not an alias of an address page within the 4GB address space.
*/
if (memory_map[i].used == 0) {
memory_map[i].start = addr;
memory_map[i].size = PAGE_SIZE;
memory_map[i].used = 1;
} else {
memory_map[i].size += PAGE_SIZE;
}
} /* end of for (…) */
Al ejecutar el algoritmo anterior para detectar el estado de asignación de memoria del sistema, el cargador de arranque también puede imprimir información detallada sobre la asignación de memoria en el puerto serie.
Cargando imágenes del núcleo y del sistema de archivos raíz
Planificación del diseño de la memoria
Esto implica dos aspectos:
- el rango de memoria ocupado por la imagen del núcleo;
- el rango de memoria ocupado por el sistema de archivos raíz. Al planificar la distribución de la memoria, tenga en cuenta la dirección base y el tamaño de las imágenes.
En el caso de la imagen del núcleo, es habitual copiarla a un rango de memoria que comienza en (MEM_START + 0x8000), con un tamaño aproximado de 1 MB (los núcleos Linux integrados suelen tener menos de 1 MB). ¿Por qué dejar un espacio de 32 KB entre MEM_START y MEM_START + 0x8000? Esto se debe a que el kernel de Linux coloca ciertas estructuras de datos globales en este segmento de memoria, como los parámetros de arranque y las tablas de páginas del kernel.
En cuanto a la imagen del sistema de archivos raíz, normalmente se copia en la ubicación que comienza en MEM_START + 0x0010,0000. Si se utiliza un disco RAM como imagen del sistema de archivos raíz, el tamaño sin comprimir suele ser de alrededor de 1 MB.
Copiar desde Flash
Dado que las CPU integradas como ARM suelen direccionar Flash y otros dispositivos de almacenamiento de estado sólido dentro de un espacio de direcciones de memoria unificado, leer datos desde Flash es similar a leer desde unidades RAM. Un simple bucle es suficiente para copiar la imagen desde el dispositivo Flash:
while (count) {
*dest++ = *src++; /* they are all aligned with word boundary */
count -= 4; /* byte number */
};
Configuración de los parámetros de arranque del kernel
Después de copiar la imagen del núcleo y la imagen del sistema de archivos raíz al espacio RAM, se puede preparar el inicio del núcleo Linux. Pero antes de invocar el núcleo, es necesario realizar un paso de preparación: configurar los parámetros de arranque del núcleo Linux.
Los kernels de Linux a partir de la versión 2.4.x esperan que los parámetros de arranque se pasen en forma de lista etiquetada. La lista etiquetada de parámetros de arranque comienza con la etiqueta ATAG_CORE y termina con la etiqueta ATAG_NONE. Cada etiqueta consta de una estructura tag_header que identifica el parámetro, seguida de estructuras de datos que contienen los valores de los parámetros. Las estructuras de datos tag y tag_header se definen en el archivo de encabezado include/asm/setup.h del código fuente del kernel de Linux.
En los sistemas Linux integrados, los parámetros de arranque comunes que debe configurar el cargador de arranque incluyen ATAG_CORE, ATAG_MEM, ATAG_CMDLINE, ATAG_RAMDISK y ATAG_INITRD.
Por ejemplo, así es como se configura ATAG_CORE:
params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
Aquí, BOOT_PARAMS representa la dirección base inicial de los parámetros de arranque del kernel en la memoria, y el puntero params es de tipo struct tag. La macro tag_next() calcula la dirección inicial de la siguiente etiqueta inmediatamente después de la etiqueta actual. Es importante tener en cuenta que aquí se establece el ID del dispositivo para el sistema de archivos raíz del kernel.
A continuación se muestra un código de ejemplo para establecer la información de mapeo de memoria:
for (i = 0; i < NUM_MEM_AREAS; i++) {
if (memory_map[i].used) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);
params->u.mem.start = memory_map[i].start;
params->u.mem.size = memory_map[i].size;
params = tag_next(params);
}
}
En la matriz memory_map[], cada segmento de memoria válido corresponde a una etiqueta de parámetro ATAG_MEM.
El kernel de Linux puede recibir información como parámetros de línea de comandos durante el inicio. Esto nos permite proporcionar información sobre los parámetros de hardware que el kernel no puede detectar por sí mismo o anular la información que el kernel ha detectado. Por ejemplo, utilizamos la cadena de parámetros de línea de comandos «console=ttyS0,115200n8» para indicar al kernel que utilice ttyS0 como consola con la configuración «115200bps, sin paridad, 8 bits de datos». A continuación se muestra un código de ejemplo para establecer la cadena de parámetros de la línea de comandos del kernel:
char *p;
for (p = commandline; *p == ' '; p++)
;
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
strcpy(params->u.cmdline.cmdline, p);
params = tag_next(params);
Tenga en cuenta que, en el código anterior, al establecer el tamaño de tag_header, debe incluir el carácter de terminación «\0» en la cadena y redondearlo al múltiplo de 4 bytes más cercano, ya que el miembro size de la estructura tag_header representa el número de palabras.
A continuación se muestra un código de ejemplo para establecer ATAG_INITRD, que indica la ubicación en la RAM donde se puede encontrar la imagen initrd (en formato comprimido), junto con su tamaño:
params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);
params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;
params = tag_next(params);
Por último, establezca la etiqueta ATAG_NONE para concluir toda la lista de parámetros de inicio:
static void setup_end_tag(void) {
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
Invocación del núcleo
El gestor de arranque llama al núcleo Linux saltando directamente a la primera instrucción del núcleo, es decir, saltando directamente a la dirección MEM_START + 0x8000. Para realizar el salto, deben cumplirse las siguientes condiciones:
- Configuración del registro de la CPU:
R0 = 0
R1 = ID del tipo de máquina (para el número de tipo de máquina, consulte linux/arch/arm/tools/mach-types)
R2 = Dirección base inicial de la lista etiquetada de parámetros de arranque en la RAM - Modo de la CPU:
Desactivar las interrupciones (IRQ y FIQ)
La CPU debe estar en modo SVC - Configuración de la caché y la MMU:
la MMU debe estar desactivada. La caché
de instrucciones puede estar activada o desactivada. La caché
de datos debe estar desactivada.
Si se utiliza C, la invocación del kernel se puede realizar de la siguiente manera:
void (*theKernel)(int zero, int arch, u32 params_addr) =
(void (*)(int, int, u32))KERNEL_RAM_BASE;
theKernel(0, ARCH_NUMBER, (u32)kernel_params_start);
Tenga en cuenta que la llamada a la función Kernel() nunca debe devolver ningún resultado. Si lo hace, significa que se ha producido un error.
Arrancador de sistema integrado frente a arrancador de PC
En las arquitecturas de PC, el gestor de arranque está compuesto por la BIOS (esencialmente firmware) y el gestor de arranque del sistema operativo ubicado en el MBR del disco duro (por ejemplo, LILO, GRUB). Una vez que el BIOS completa la detección del hardware y la asignación de recursos, carga el gestor de arranque desde el MBR del disco duro en la RAM del sistema y cede el control al gestor de arranque del sistema operativo. La tarea principal del gestor de arranque es leer la imagen del núcleo desde el disco duro en la RAM y, a continuación, saltar al punto de entrada del núcleo para iniciar el sistema operativo.
En los sistemas integrados, normalmente no hay ningún programa de firmware como el BIOS (aunque algunas CPU integradas pueden incluir un pequeño programa de arranque integrado). Por consiguiente, toda la tarea de carga y arranque del sistema la lleva a cabo el gestor de arranque. Por ejemplo, en un sistema integrado basado en el núcleo ARM7TDMI, el sistema suele iniciar la ejecución en la dirección 0x00000000 durante el encendido o el reinicio, y esta dirección suele contener el programa del gestor de arranque del sistema.
CPU y placa integrada compatibles con el cargador de arranque
Cada arquitectura de CPU diferente tiene un cargador de arranque diferente. Algunos cargadores de arranque también admiten CPU con múltiples arquitecturas. Por ejemplo, U-Boot admite tanto la arquitectura ARM como la arquitectura MIPS. Además de depender de la arquitectura de la CPU, el cargador de arranque también depende de la configuración de dispositivos específicos integrados a nivel de placa. Es decir, el cargador de arranque no es necesariamente adecuado para las dos placas integradas diferentes, incluso si están construidas con la misma CPU.
Medio de instalación del gestor de arranque
Cuando un sistema se enciende o se reinicia, las CPU suelen obtener instrucciones de una dirección predeterminada establecida por el fabricante de la CPU. Por ejemplo, las CPU basadas en el núcleo ARM7TDMI suelen obtener su primera instrucción de la dirección 0x00000000 después de un reinicio. Los sistemas integrados basados en arquitecturas de CPU suelen asignar algún tipo de dispositivo de almacenamiento de estado sólido (como ROM, EPROM o FLASH) a esta dirección predeterminada. Por lo tanto, después de encender el sistema, la CPU ejecuta primero el programa del gestor de arranque.




