MangoPi
En este caso, se utilizará una placa Mango Pi, con un tamaño muy parecido a la RPi Zero, pero que utiliza un SoC D1, aunque la prueba descrita en este post se podrá realizar en cualquier SBC que utilice un SoC D1.
Secuencia de arranque del D1
Se recomienda echar un vistazo al post anterior donde se aborda el mismo objetivo, pero con el SoC Allwinner H5 (un ARM Cortex-A53). Al igual que el SoC H5 y otros SoCs de Allwinner, lo que hace el procesador cuando arranca es, básicamente:
1.- Copiar el contenido de los primeros 32 Kbytes que empiezan en el sector 16 de la tarjeta de memoria en una zona de la memoria estática interna (más info).
2.- Comprobar el checksum del código, calculado en la cabecera de esos 32 Kbytes (más info).
3.- Si el checksum es correcto, ejecuta la primera instrucción de esos 32 Kbytes (que suele ser una instrucción de salto "jal", por lo que el código de arranque en sí normalmente se coloca justo después de la cabecera de esos 32 Kbytes).
La secuencia es igual a la utilizada en los SoC ARM de Allwinner (como el H5) con la única diferencia de que la instrucción de salto (los 4 primeros bytes de ese bloque) se codifican como la instrucción "jal" de RISC-V (es la misma instrucción tanto para RV32I como para RV64I).
A continuación puede verse el código máquina correspondiente a la instrucción "jal" (Jump And Link) de RISC-V que se coloca en los 4 primeros bytes de la cabecera:
bit bit 31 ......................... 12 +--rd---+ 6 5 4 3 2 1 0 imm[20|10:1|11|19:12] 0 0 0 0 0 1 1 0 1 1 1 1 \-------/ \-----------/ | | | +-------- opcode +-------------------- reg destino (en r0 se guarda la dirección de la siguiente instrucción a "jal")
"imm" es el offset al cual debe saltarse (en complemento a 2, puede ser un valor negativo). Nótese que el valor de "imm" es de 21 bits pero se descarta el bit menos significativo, puesto que el código en RISC-V siempre debe alojarse en direcciones pares de memoria.
Parchear la herramienta mksunxiboot
La herramienta mksunxiboot se encarga de calcular la cabecera con el correspondiente checksum para que el SoC arranque correctamente el código que queramos. Es un código en C muy sencillo que compilamos y ejecutamos en el ordenador, recibe como entrada un fichero ".bin" con el código de arranque y genera un ".bin" con el código de arranque precedido con la cabecera necesaria (con el checksum dentro) para que el SoC reconozca como "correcto" el código y lo ejecute.
La herramienta original está diseñada para SoCs ARM así que para adecuarla al D1 sólo hace falta cambiar la instrucción de salto para que, en lugar de ser una instrucción de salto ARM, sea una instrucción de salto RISC-V.
En "mksunxiboot.c" se sustituye el siguiente código:
img.header.jump_instruction = /* b instruction */ 0xEA000000 | /* jump to the first instruction after the header */ ( (sizeof(boot_file_head_t) / sizeof(int) - 2) & 0x00FFFFFF );
Por este otro:
u32 code_offset = sizeof(boot_file_head_t); img.header.jump_instruction = /* risc-v "jal" instruction */ (((code_offset >> 20) & 1) << 31) | (((code_offset >> 1) & 0x3FF) << 21) | (((code_offset >> 11) & 1) << 20) | (((code_offset >> 12) & 0xFF) << 12) | 0x00000006F;
Y ya está, compilando con:
gcc -o mksunxiboot mksunxiboot.c
Tenemos nuestra herramienta preparada para "firmar" nuestro código RISC-V para el D1.
SPL
En terminología Allwinner, el SPL (o Second Program Loader) es como se llama al código que se ejecuta justo después de la ROM del D1 (el que se le pasa a "mksunxiboot" para que lo firme) y que es el código que se pone en la tarjeta de memoria justo a continuación de la cabecera que calcula "mksunxiboot".
; sector 16 de la tarjeta de memoria jal @inicioCódigoSPL ; primeros 4 bytes de la cabecera ... resto de la cabecera calculada por la utilidad "mksunxiboot" ... @inicioCódigoSPL: ... nuestro código de arranque o SPL ...
Haremos un SPL extremadamente sencillo que se limite a hacer parpadear un pin GPIO al que se le conecta un LED.
#include <stdint.h> void spl() __attribute__ ((naked, section(".spl"))); #define WAIT 40000000ULL #define GPIO_BASE 0x02000000 #define PC_CFG *((uint32_t *) (GPIO_BASE + 0x0060)) #define PC_DAT *((uint32_t *) (GPIO_BASE + 0x0070)) #define PC_PULL0 *((uint32_t *) (GPIO_BASE + 0x0084)) void spl() { PC_CFG = (PC_CFG & 0xFFFFFF0F) | 0x00000010; // PC1 = output PC_PULL0 = (PC_PULL0 & 0xFFFFFFF3) | 0x00000008; // PC1 = pull-down while (true) { PC_DAT |= 0x00000002; // PC1 = 1 for (uint64_t n = 0; n < WAIT; n++) ; PC_DAT &= 0xFFFFFFFD; // PC1 = 0 for (uint64_t n = 0; n < WAIT; n++) ; } }
Como se puede comprobar, el código no hace uso de variables globales ni de llamadas a funciones o librerías, para que sea autocontenido y no genere dependencias externas. Algunos aspectos importantes:
- El nombre de la función es irrelevante, no tiene por qué ser "main". De hecho el código no se enlazará, sólo se compilará.
- Mediante atributos, indicamos al compilador que el código de la función debe alojarse en una sección llamada ".spl" (nombre arbitrario) y que debe ser "naked" (el compilador no generará código de preámbulo o postámbulo, ya que es una función a la que se llega con un salto, no con una llamada).
- El código simplemente inicializa la línea GPIO correspondiente al pin PC1 (el pin número 22 del conector de 40 pines de la Mango Pi), como un GPIO de salida y a continuación se queda en un bucle infinito emitiendo un 1 y un 0 alternativamente, para que pueda parpadear un led conectado a PC1.
Una vez compilado (generado el fichero "spl.o"), se utiliza la utilidad "objcopy" para extraer el código de la sección ".spl" y meterlo en un fichero binario crudo "spl.bin". A continuación, este fichero "spl.bin" es pasado por la utilidad "mksunxiboot" que modificamos anteriormente, para que genere otro "spl_with_signature.bin" que estará firmado y, por tanto, podrá ya ser ejecutado por el D1.
riscv64-none-elf-g++ -mtune=thead-c906 -fno-exceptions -fno-rtti -nostartfiles -c -o spl.o spl.cc riscv64-none-elf-objcopy -O binary -j .spl spl.o spl.bin ./mksunxiboot spl.bin spl_with_signature.bin
El último paso consiste en grabar este fichero "spl_with_signature.bin" directamente en el sector 16 de una tarjeta de memoria (nótese que cada sector en una tarjeta de memoria mide 512 bytes, por tanto el sector 16 es el offset 8192 = 1024 * 8 a nivel de byte):
dd if=spl_with_signature.bin of=/dev/sdX bs=1024 seek=8
Si colocamos un LED entre PC1 y masa (usar siempre una resistencia en serie) y arrancamos el D1 con la tarjeta de memoria que acabamos de tostar, podremos ver nuestro blinker bare metal en acción.
Todo el código en la sección soft.
[ añadir comentario ] ( 874 visualizaciones ) | [ 0 trackbacks ] | enlace permanente | ( 2.8 / 370 )