¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Inicialización de un procesador. Caso Cortex-A8
Antes de comenzar con la descripción técnica de cada etapa de la inicialización del procesador es conveniente aclarar que se entiende por “inicializar el procesador” en el contexto ARM.
Se asume que el lector se encuentra familiarizado con el concepto de Microprocessor-based system on a chip, ya que este artículo tratará el caso del núcleo Cortex-A8 en el sistema Cortex A8 Test Chip y AM3357, los cuales a su vez se integran sobre las placas PB-A8 y Beagle Bone Black
La inicialización de una sistema dispone al menos de tres etapas
- Inicialización del SoC: Consiste en la configuración y establecimiento de un estado conocido de los registros y módulos críticos que puedan impedir la normal ejecución del código necesario para alcanzar la siguiente etapa. Entre los casos de uso mas frecuentes se puede mencionar:
- Registro de referencia ejecución de código (Program Counter, Instruction Pointer)
- Registros de modo de operación (CPSR)
- Registro de referencia de pila (Stack Pointer)
- Registros de propósito general
- Excepciones
- Interrupciones
- Procesadores secundarios
- Reloj interno
- Inicialización de la placa: En esta etapa se configuran los periféricos mínimos que permitan ejecutar el código necesario para alcanzar la siguiente etapa. Entre los más comunes se encuentra:
- Controladores de memorias externas (RAM, ROM). Principalmente para leer y “desempaquetar” el código de la siguiente etapa.
- Controlador de interrupciones
- Controladores de interfaces de comunicación. Usualmente (UART) para actuar como dispositivo de depuración.
- Controladores de vídeo. En algunos casos se requiere lo que se conoce como “Splash screen” para indicarle al usuario que el sistema se esta inicializando
- Controladores de interfaces de entrada/salida. Por lo general se emplean (GPIO) para señalizar el estado del sistema y/o realizar alguna acción alternativa ante un requerimiento del usuario (presion de algún botón)
- Inicialización del sistema operativo o programa principal: Durante la ejecución del código asociado a esta etapa el sistema se inicializa completamente alcanzando todas las funcionalidades para las cuales fue concebido. Esta etapa es la más compleja a nivel de descripción/asociación código hardware, pero se pueden mencionar las inicializaciones más relevantes:
- Paginación
- Reinicialización de excepciones. Se contemplan casos y acciones más complejas que permitan salvaguardar el sistema
- Kernel
- Root File System
- Reinicialización de interrupciones. Se contemplan interacciones más complejas con los periféricos inicializados en esta etapa
- Controladores de bus. (I2C, SPI, PCI)
- Ejecución de programa principal.
El presente artículo solo se enfocará en la primer etapa Inicialización del SoC
¿Dónde está mi código?
Una de las principales barreras que se tiene al momento de inicializar el procesador es donde almacenar el código para que pueda ser ejecutado y todo lo que ello implica (rangos de direcciones, memoria disponible, etc)
No existe una respuesta única ya que esto depende del SoC, es decir cada fabricante puede disponer de su propio método, por lo cual analizaremos los casos propuestos.
En base a la experiencia de quien escribe este articulo la comprensión acabada de la arquitectura interna del SoC es un tema que tiende a ser un agujero negro, ya que cuanto mejor se desee comprender mayor cantidad de interfaces/buses/estándares/protocolos (AXI, AHB, PLxxx, RNDIS, UTMI) se deben dominar, por lo que se brindará tan solo una visión a nivel programador
Cortex A8 test chip
El mapa de memoria brinda entre otra la siguiente información:
Owner | Address range | Bus type | Memory region size |
---|---|---|---|
Northbridge | 0x00000000-0x0FFFFFFF | Dinamic Memory Controller | 256MB (DDR mirror) |
Northbridge | 0x40000000-0x5FFFFFFF | Static Memory Controller | 512MB |
Northbridge | 0x70000000-0x8FFFFFFF | Dinamic Memory Controller | 512MB |
De la tabla se desprende que la memoria RAM se direcciona en el rango 0x70000000-0x8FFFFFFF, y que la parte superior del mismo (256MB) se encuentra espejado en 0x00000000-0x0FFFFFFF. Sin embargo se debe recordar que al estar en la etapa Inicialización del SoC, el DMC no se encuentra configurado por lo que no es posible emplear dicha memoria y por ende cualquier rango asociado a la misma.
Por su parte la ROM se direcciona en el rango 0x40000000- 0x5FFFFFFF y el controlador de este tipo de memorias no requiere inicialización, justamente para poder leer el código que permita satisfacer los requerimientos de la etapa Inicialización del SoC. Al parecer ya se encuentra resuelto el dilema sobre donde leer el código, pero al lector astuto seguramente le surgirá interrogante ¿Cómo puede ser esto posible, si la Reset Vector Address es 0x00000000 o 0xFFFF0000?
Para resolver esta duda perfectamente válida apelamos a la “magia” del Northbridge, este ASIC específico de esta placa se encarga de remapear la NOR de 0x40000000- 0x40FFFFFF a 0x00000000-0x00FFFFFF. Si bien esto ya brinda cierta claridad sobre donde debe situarse el código, es importante observar que el rango remapeado es tan solo de 16MB, frente a los 512MB disponibles en el mapa de memoria, esto significa que si bien la placa puede disponer de una NOR de hasta 512MB el código de arranque en la misma solo puede estar alojado en los primeros 16MB, quedando el restante espacio para disponible para el código complementario. Esto es importante tenerlo presente al momento de generar el mapa de memoria del código por lo general mediante el linker script
AM 335x
A continuación se brinda un extracto del mapa de memoria obtenido del AM335x and AMIC110 Sitara™ Processors Technical Reference Manual
Block Name | Address range | Description | Memory region size |
---|---|---|---|
Feneral Purpose Memory Controller | 0x00000000-0x1FFFFFFF | 8-/16-bit External Memory (Ex/R/W | 512MB |
Boot ROM | 0x40000000-0x4001FFFF | 128kB | |
Boot ROM | 0x40020000-0x4002BFFF | 32-bit Ex/R (2) – Public | 48kB |
SRAM internal | 0x402F0400-0x402FFFFF | 32-bit Ex/R/W | 63kB |
EMIF0 SDRAM | 0x80000000-0xBFFFFFFF | 8-/16-bit External Memory (Ex/R/W) | 1GB |
En ambos casos el código de inicialización se encuentra en una memoria no volátil por lo cual si bien es posible ejecutarlo de la misma, es importante remarcar que no se dispone de pila a menos que la RAM externa sea inicializada o se utilice la SRAM interna (si esta disponible y accesible). Esto último suele ser una opción transitoria sobre todo para fines de depuración hasta disponer de DMC correctamente inicializado
Secuencia de Inicialización
La secuencia básica de la etapa Inicialización del SoC, se puede modelar mediante el diagrama de flujo según el caso de uso, que para nuestro sistema es el rom/flash_sram_dram
En el modelo propuesto se identifican en rojo las configuraciones obligatorias, en amarillo las optativas segun el caso de uso, mientras que en verde las sugeridas
En los apartados subsiguientes se detallan las configuraciones específicas de cada subetapa
mode set & interrupt disable
La primer acción que se debe realizar es garantizar que el procesador se encuentra en el modo y privilegio de operación que permita el acceso a los recursos del mismo.
En ARMv7 este modo se denomina Supervisor normalmente se referencia como svc. Si bien por manual se especifica que este es el modo que el procesador arranca luego de un reinicio, se debe asegurarlo, para lo cual se emplean los 5 bits menos significativos del Current Program Status Register, denominados campo de modo (CPSR.M → CPSR[4:0]).
Dado que el estado del SoC y sobre todo de los periféricos de la placa no es conocido, se debe evitar que algún estimulo externo perturbe la secuencia de inicialización, es decir se deben deshabilitar las interrupciones, que en el caso de ARMv7 son IRQ y FIQ.\
En el caso de ARMv7 la deshabilitación de las interrupciones se realiza con el mismo registro (CPSR.I → CPSR[7], CPSR.F → CPSR[6]) que se emplea para configurar el modo de operación por lo cual en un paso se realizan dos subetapas
mrs r0, cpsr /* Move to general purpose Register a System register*/ bic r0, r0, #0x1f /* BItwise bit Clear*/ orr r0, r0, #0xd3 /* OR bitwise 0xd3 -> 11010011 -> CPSR[76x43210]*/ msr cpsr,r0 /* Move to System register a general purpose Register*/
features get
En implementaciones reales es una buena y muy recomendable práctica identificar las funcionalidades disponibles en el SoC. A tal fin se dispone del conjunto de Registros CPUID, los cuales deben ser accedidos mediante los registros del CoProcessor15 (CP15). Si bien no es necesario para los fines de la Cátedra, se brinda un ejemplo de lectura del mismo por ser una instrucción relativamente “criptica”, para el caso del Main Identifier Register
mrc p15, 0, r0, c0, c0, 0 /*Move to Register a Coprocessor | | | | | |____Código de operación específico del coprocesador | | | | |_______Registro del coprocesador que contiene el segundo operando | | | |___________Registro del coprocesador que contiene el primer operando | | |_______________Registro de propósito general donde se almacena la información | |___________________Código de operación específico del coprocesador |________________________Identificador del coprocesador que se quiere acceder
core configure
Si bien la mayoría de los registros se inicializan con valores coherentes y adecuados luego del reinicio del SoC, de forma tal que el programador se encuentre en un ambiente seguro para realizar la primer etapa de inicialización, existen ciertos casos donde se muy recomendable establecer configuraciones conocidas. El caso más emblemático son los indicadores de condición/estado del Current Program Status Register (CPSR.C → CPSR[31:28]).
mrs r0, cpsr bic r0, r0, #0xf0000000 /* BItwise bit Clear N, Z, C, V bits */ msr cpsr,r0
rom copy
Antes comenzar con descripción de esta subetapa vale la pena responder algunas dudas razonables
¿Donde se debe copiar la ROM?
La respuesta simple es a RAM, pero ¿como puede ser esto posible si aun no se ha inicializado el DCM?.
Por lo general todo SoC dispone de una SRAM interna o la placa presenta una externa que no requiere configuración como es el caso del SMC, esto permite disponer de un espacio de memoria RAM pequeño pero suficiente para los fines de esta subetapa.\
Para los casos de uso planteados disponemos del siguiente rango
Board | Address range | Memory type | Memory type |
---|---|---|---|
PB-A8 | 0x48000000-0x4BFFFFFF | 2MB | Cellular RAM |
Beagle | 0x402F0400-0x402FFFFF | 63k | SRAM internal |
¿Porqué se debe copiar de ROM a RAM?
¿Entre las razones más relevantes se pueden mencionar
- Velocidad de ejecución
- Requerimientos de alineación para el direccionamiento. Se debe recordar que para optimizar el uso de la ROM el código se consolida en forma consecutiva, eliminando en la medida de lo posible todo espacio entre secciones.
- Requerimientos de posicionar el código y datos en páginas y/o segmentos diferenciados
¿Porqué no se puede copiar de ROM a la RAM del sistema?
Como se mencionó anteriormente, principalmente porque el DMC no se encuentra inicializado, pero no solo se trata del DMC sino que todos los subsistemas asociados a la memoria deben estar correctamente configurados, como ser el AXI, el reloj, el PowerModuleController. Esto implica una mayor cantidad de operaciones en las cuales se puede dar una excepción, que recordemos que aun no se encuentran configuradas porque no se dispone de RAM, es decir nos encontraríamos en un bucle, se requiere RAM para disponer de pila para excepciones y llamada a funciones, pero el código para su configuración no se puede ejecutar porque la misma no esta inicializada.
Habiendo esbozado una explicación sobre las razones por las cuales es necesario realizar la copia de ROM (NOR, E2PROM) a RAM (SRAM, CELLRAM), se procede a brindar un ejemplo simple de función de copia que no requiere pila. El código brindado dista mucho de ser óptimo, para lo cual existe un sin numero de referencias, pero cumple su cometido.
ldr r0, _direccion_origen ldr r1, _direccion_destino ldr r2, _cuenta_doublewords bl mem_cpy @ Opcion A con incremento de a double word mem_cpy: add r2, r2, r0 slow_copy: ldr r3, [r0], #4 @ r3=*( i n t * ) ( r0+0 ); r0 = r0+4 str r3, [r1], #4 @ * ( i n t * ) ( r1+0 ) = r3 ; r1 = r1+4 cmp r0, r2 ble slow_copy mov pc, lr
ldr r0, _direccion_origen ldr r1, _direccion_destino ldr r2, _cuenta_doublewords bl mem_cpy @ Opcion B con incremento de a 7 double word veces (r3 a r10 mem_cpy: add r2, r2, r0 fast_copy: ldmia r0!, {r3 - r10} @ r3=*(int *) (r0); r0 = r0+4; r4=*(int *) (r0); r0 = r0+4; r5=*(int *) (r0); r0 = r0+4;...;r10=*(int * ) (r0); r0 = r0+4; stmia r1!, {r3 - r10} @ *(int *) (r1) = r3; r1 = r1+4;*(int *) (r1) = r4; r1 = r1+4;*(int *) (r1) = r5; r1 = r1+4;...;*(int *) (r1) = r10; r1 = r1+4; cmp r0, r2 ble fast_copy mov pc, lr
Aclaracion
La copia de ROM a RAM internas (SRAM, CellRAM) es un paso intermedio, necesario y casi obligatorio para inicializar la RAM del sistema en la mayoría de las placas.
Si bien a los fines de la Cátedra para el caso de uso PB-A8, el tamaño de la CellRAM sería suficiente para implementar la guía de trabajos prácticos debe considerarse como un caso muy particular, observar que incluso en la caso de uso Beagle esto no sería posible.
Lo expresado pone en evidencia la necesidad de inicializar el DCM, pero debido a la complejidad del mismo y por razones pedagógicas (la carga de aprendizaje es suficiente estableciendo el Short-descriptor translation table for small page es más que suficiente), se opta por aprovechar el recurso que provee el QEMU, presentando el espacio de memoria asociado a la DRAM ya disponible para su uso.
stack configure
Esta subetapa es una de las más sencillas, pero no por eso menos importante, ya que permite disponer de un entorno de memoria capaz de soportar la ARM Architecture Procedure Call Standard, que en forma simplificada implica que es posible ejecutar al menos código escrito en C. Esto no es un dato menor, ya que una buena parte (~ 70%) de las rutinas de configuración que implementa cada etapa puede ser reutilizable entre diferentes arquitecturas (ARMv7, ARMv8 e incluso Intel X86).
La configuración de la pila no solo se trata de establecer adecuadamente el valor de R13 (SP), sino también de proveer una zona de memoria limpia para operar con él, entendiéndose por limpia con valores prohibidos de forma que un acceso erróneo pueda ser identificado. Esto último resulta útil en la mayoría de las arquitecturas previas a la ARMv8, ya que en las mismas no se encuentra implementada la protección por hardware de la pila, es decir que ni siquiera se tiene un control del tamaño de esta memoria para identificar si el SP excedió los límites, como si se hace en algunos SoC ARMv8 mediante MSPLIM. Esta técnica denominada El buchón de la pila o en inglés Stack Canaries, presenta un sin número de variantes y su implementación excede al contenido requerido por esta Cátedra.
Es importante mencionar que en un sistema real este procedimiento se debe repetir una vez inicializada la RAM del sistema.
A los fines de la Cátedra, lo mínimo a realizar seria establecer el SP y garantizar que el rango asignado a la pila pueda ser escrito y leído correctamente.
stack_setup: ldr r0, #CONFIG_PILA_LIMITE_DIRECCION sub r0, r0, #(CONFIG_PILA_IRQ_LARGO + CONFIG_PILA_FIQ_LARGO) sub sp, r0, #12 @ 3 words para abort-stack bic sp, sp, #7 @ 8-byte ajuste para satisfacer alineacion requerida por ABI mov pc, lr
ldr r0, #(CONFIG_PILA_LIMITE_DIRECCION - CONFIG_PILA_IRQ_LARGO - CONFIG_PILA_FIQ_LARGO - 12) bl stack_validate stack_validate: ldr r2, 0xA5A5A5A5 ldr r3, 0xA5A5A5A5 ldr r4, 0xA5A5A5A5 ldr r5, 0xA5A5A5A5 ldr r6, 0xA5A5A5A5 ldr r7, 0xA5A5A5A5 ldr r8, 0xA5A5A5A5 ldr r9, 0xA5A5A5A5 ldr r10, 0xA5A5A5A5 stack_write_fast: sub r1, sp, #(CONFIG_PILA_LIMITE_DIRECCION - 36) cmp r1, 0 ble stack_write_slow push {r3 - r10} stack_write_slow: push {r2} cmp sp, r0 bpl stack_write_fast stack_verify_fast: sub r1, sp, #(CONFIG_PILA_LIMITE_DIRECCION - CONFIG_PILA_IRQ_LARGO - CONFIG_PILA_FIQ_LARGO - 36) cmp r1, 0 ble stack_verify_slow pop {r3 - r10} sub r1, r3, r4 cmp r1, #0 bne core_reset sub r1, r5, r6 cmp r1, #0 bne core_reset sub r1, r7, r8 cmp r1, #0 bne core_reset sub r1, r9, r10 cmp r1, #0 bne core_reset stack_verify_slow: pop {r2} cmp r2, #0xA5A5A5A5 bne core_reset cmp sp, r0 ble stack_write_fast mov pc, lr
core reset
En esta subetapa lo que se busca es reiniciar el SoC tras alguna configuración o estado irrecuperable por parte del software.
;Caso de uso Beagle core_reset: ldr r1, #0x48180F00 ldr r3, #0x1 str r3, [r1] mov r0, r0 _reset_loop: b _reset_loop
— ChristiaN 2023/04/23 23:50