Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip En parte anterior se ha implementado un controlador de memoria más o menos funcional, o mejor dicho, una envoltura sobre el IP Core de Quartus, que actúa como un adaptador para TileLink. Hoy, en la sección "Portando RocketChip a una placa china poco conocida con Cyclone", verás una consola funcionando. El proceso se ha prolongado un poco: ya pensé que lanzaría Linux rápidamente y seguiríamos adelante, pero no fue así. En esta parte, propongo observar el proceso de arranque de U-Boot, BBL, y los tímidos intentos de inicialización del kernel de Linux. Pero hay una consola: la de U-Boot, que es bastante avanzada y tiene muchas de las características que esperas de una consola completa.

En la parte de hardware se añadirá una tarjeta SD, conectada a través de la interfaz SPI, así como UART. En la parte de software, BootROM será reemplazado por xip en sdboot y, en realidad, se añadirán las siguientes etapas de arranque (en la tarjeta SD).

Mejorando la parte de hardware

Entonces, la tarea: necesitamos pasar a un núcleo "grande" y conectar UART (del Raspberry) y un adaptador SD (se utilizó una pequeña placa de Catalex con seis pines: GND, VCC, MISO, MOSI, SCK, CS).

En principio, todo fue bastante sencillo. Pero antes de darme cuenta, me sentí un poco perdido: después del último intento decidí que una vez más solo había que mezclar en System algo como HasPeripheryUART (y en la implementación, respectivamente), lo mismo para la tarjeta SD, y todo estaría listo. Luego decidí ver cómo se implementaba en un diseño "serio". Entonces, ¿qué tenemos aquí de serio? Arty, al parecer, no sirve, queda el monstruo unleahshed.DevKitConfigs. Y de repente descubrí que hay una serie de overlays que se añaden a través de parámetros mediante claves. Supongo que esto es probablemente muy flexible y configurable, pero al menos me gustaría poder iniciar algo... ¿No tienen algo similar, pero más simple y rudimentario? Fue entonces cuando me encontré con vera.iofpga.FPGAChip para la FPGA Microsemi y rápidamente lo desmenuzaré en citas tratando de hacer mi propia implementación por analogía, ya que aquí más o menos todo el "cableado de la placa base" está en un solo archivo.

Resultó que solo era necesario añadir a System.scala la línea

class System(implicit p: Parameters) extends RocketSubsystem
...
  with HasPeripherySPI
  with HasPeripheryUART
...
{
  val tlclock = new FixedClockResource("tlclk", p(DevKitFPGAFrequencyKey))
  ...
}

class SystemModule[+L <: System](_outer: L)
  extends RocketSubsystemModuleImp(_outer)
...
    with HasPeripheryUARTModuleImp
    with HasPeripheryGPIOModuleImp
...

Línea en el cuerpo de la clase System agrega información sobre la frecuencia a la que funciona esta parte de nuestro SoC en el archivo dts. Según entiendo, DTS/DTB es un análogo estático de la tecnología plug-and-play para dispositivos embebidos: el árbol de descripción dts se compila en un archivo dtb binario y se pasa al cargador para que pueda configurar correctamente el hardware. Lo interesante es que sin la línea con tlclock todo se sintetiza perfectamente, pero compilar BootROM (recuerden, ahora esto será ya sdboot) no será posible: durante el proceso de compilación analiza el archivo dts y crea un encabezado con la macro TL_CLK, gracias a la cual podrá configurar correctamente los divisores de frecuencia para las interfaces externas.

También será necesario ajustar un poco la "distribución":

Platform.scala:

class PlatformIO(implicit val p: Parameters) extends Bundle {

...

  // UART
  io.uart_tx := sys.uart(0).txd
  sys.uart(0).rxd := RegNext(RegNext(io.uart_rx))

  // Tarjeta SD
  io.sd_cs := sys.spi(0).cs(0)
  io.sd_sck := sys.spi(0).sck
  io.sd_mosi := sys.spi(0).dq(0).o
  sys.spi(0).dq(0).i := false.B
  sys.spi(0).dq(1).i := RegNext(RegNext(io.sd_miso))
  sys.spi(0).dq(2).i := false.B
  sys.spi(0).dq(3).i := false.B
}

Las cadenas de registros, para ser honesto, se añadieron simplemente por analogía con otros lugares del código original. Probablemente, deberían proteger contra metastabilidad. Es posible que en algunos bloques ya haya su propia protección, pero al menos quiero arrancar en un nivel "de calidad". Una pregunta más interesante para mí es por qué MISO y MOSI están conectados a diferentes dq? Ответа я пока так и не нашёл, но, похоже, остальной код рассчитывает именно на такое подключение.

Físicamente, simplemente asigné los pines del diseño a contactos libres en el conector y moví el jumper de selección de voltaje a 3.3V.

Adaptador SD

Vista superior:

Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Vista inferior:

Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Depuración de la parte de software: herramientas

Primero hablemos de las herramientas de depuración disponibles y sus limitaciones.

Minicom

En primer lugar, necesitaremos leer lo que el cargador y el núcleo están imprimiendo. Para esto, en Linux (en este caso, en el RaspberryPi) necesitaremos el programa Minicom. De hecho, cualquier programa que funcione con el puerto serie servirá.

Tenga en cuenta que al iniciar, el nombre del dispositivo del puerto debe especificarse como -D /dev/ttyS0 - después de la opción -D. Y la información principal: para salir, use Ctrl-A, X. De hecho, tuve una situación en la que esta combinación no funcionó; entonces, se puede simplemente decir desde otra sesión SSH killall -KILL minicom.

Hay otra característica. En concreto, la Raspberry Pi tiene dos UART, y ambos puertos pueden ya estar configurados para algo: uno para Bluetooth, y el otro, por defecto, muestra la consola del núcleo. Afortunadamente, este comportamiento se puede reconfigurar. siguiendo este manual..

Reescritura de memoria

Durante la depuración, para comprobar la hipótesis, a veces tenía que cargar el cargador de arranque (lo siento) en la memoria RAM directamente desde el host. Quizás esto se pueda hacer directamente desde GDB, pero al final seguí la ruta más sencilla: copié el archivo necesario en la Raspberry, reenvíe también el puerto 4444 a través de SSH (telnet de OpenOCD) y utilicé el comando load_image. Cuando lo ejecutas, parece que todo se ha congelado, pero en realidad «no está dormido, solo está parpadeando lentamente»: está cargando el archivo, simplemente lo hace a una velocidad de un par de kilobytes por segundo.

Características de la instalación de breakpoints

Probablemente, muchos no han tenido que pensar en esto al depurar programas normales, pero los breakpoints no siempre se establecen de forma hardware. A veces, establecer un breakpoint consiste en escribir temporalmente una instrucción especial en el lugar adecuado directamente en código de máquina.Por ejemplo, así funcionaba mi comando estándar b en GDB. Esto es lo que se deduce de ello:

  • no se puede establecer un punto dentro de BootROM, porque ROM
  • se puede establecer un breakpoint en el código cargado en RAM desde la tarjeta SD, pero hay que esperar hasta que se cargue. De lo contrario, no reescribiremos un trozo de código, sino que el cargador sobrescribirá nuestro breakpoint.

Estoy seguro de que se puede solicitar explícitamente el uso de breakpoints de hardware, pero de todos modos su número es limitado.

Sustitución rápida de BootROM

En la etapa inicial de depuración, a menudo surge el deseo de corregir BootROM y volver a intentarlo. Pero hay un problema: BootROM es parte del diseño que se carga en el FPGA, y su síntesis toma unos minutos (¡y eso después de casi compilar instantáneamente la imagen de BootROM desde C y ensamblador!). Afortunadamente, en realidad, todo es mucho más rápido.: la secuencia de acciones es la siguiente:

  • regenerar bootrom.mif (cambié a MIF en lugar de HEX, porque siempre tenía problemas con HEX, y MIF es el formato nativo de Altera)
  • en Quartus decir Processing -> Update Memory Initialization File
  • en el artículo de Assembler (en la columna izquierda de Tasks) dar la orden Start again

Todo esto toma un par de decenas de segundos.

Preparación de la tarjeta SD

Aquí todo es relativamente simple, pero hay que armarse de paciencia y tener alrededor de 14 Gb de espacio en disco:

git clone https://github.com/sifive/freedom-u-sdk
git submodule update --recursive --init
make

Después se debe insertar una tarjeta SD limpia, o mejor dicho, que no contenga nada necesario, y ejecutar

sudo make DISK=/dev/sdX format-boot-loader

… donde sdX — es el dispositivo asignado a la tarjeta. ATENCIÓN: los datos en la tarjeta serán eliminados, sobrescritos y, ¡bueno! Probablemente no valga la pena hacer toda la compilación desde sudo, porque entonces todos los artefactos de la compilación pertenecerán a root, y la compilación tendrá que hacerse desde sudo siempre.

Al final, se obtiene una tarjeta particionada en GPT con cuatro particiones, en una de las cuales hay FAT con uEnv.txt y una imagen de arranque en formato FIT (que contiene varios subimágenes, cada una con su propia dirección de arranque), otra partición — limpia, se supone que se formatea en Ext4 para Linux. Otras dos particiones son misteriosas: en una vive U-Boot (su desplazamiento, según entiendo, está grabado en BootROM), en la otra, parece que viven sus variables de entorno, pero por ahora no las estoy utilizando.

Nivel uno, BootROM

La sabiduría popular dice: "Si en la programación hay danzas con un tambor, en la electrónica también se necesita un extintor". Ni siquiera se trata de que una vez casi quemo la placa, al pensar "Bueno, GND es el mismo nivel bajo" (tal vez, una resistencia no habría estado demás…) Se trata más bien de que si las manos no vienen de donde deben, la electrónica no deja de dar sorpresas: al soldar el conector en la placa, no pude soldar correctamente los contactos — en los videos muestran cómo la soldadura se esparce sola por toda la conexión, solo hay que acercar el soldador, pero a mí le "pega" como le da la gana. Bueno, puede que la soldadura no era adecuada para la temperatura del soldador, puede que algo más… En fin, al ver que ya tenía una docena de contactos, me rendí y empecé a depurar. Y ahí empezó lo misterioso: conecté RX/TX del UART, cargo el firmware — y escribe

INIT
CMD0
ERROR

Bueno, todo tiene sentido — no conecté el módulo de la tarjeta SD. Corregimos la situación, cargamos el firmware… Y silencio… Por más que pensé, el cofre se abría fácilmente: uno de los pines del módulo tenía que conectarse a VCC. En mi caso, el módulo soportaba 5V para la alimentación, así que, sin pensarlo mucho, conecté el cable que provenía del módulo al lado opuesto de la placa. Al final, el conector mal soldado se torció, y simplemente perdí la conexión UART. facepalm.jpg En general, "una cabeza tonta no le da descanso a los pies", y unas manos torcidas a la cabeza...

Al final, vi en Minicom lo que tanto esperaba

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
CARGANDO /

Además, el indicador de carga se mueve y gira. Recuerdo los días de escuela y la carga lenta de MinuetOS desde un disquete. Solo falta que la unidad no chirríe.

El problema es que después del mensaje BOOT no ocurre nada. Así que es el momento de conectarse a través de OpenOCD en Raspberry, a GDB en el host, y ver qué está pasando.

Primero, la conexión con GDB mostró inmediatamente que $pc (contador del programa, dirección de la instrucción actual) se va a 0x0 — probablemente, esto está sucediendo después de un error múltiple. Por lo tanto, justo después de emitir el mensaje BOOT vamos a añadir un bucle infinito. Esto lo retrasará por un tiempo...

diff --git a/bootrom/sdboot/sd.c b/bootrom/sdboot/sd.c
index c6b5ede..bca1b7f 100644
--- a/bootrom/sdboot/sd.c
+++ b/bootrom/sdboot/sd.c
@@ -224,6 +224,8 @@ int main(void)

        kputs("BOOT");

+    while(*(volatile char *)0x10000){}
+
        __asm__ __volatile__ ("fence.i" : : : "memory");
        return 0;
 }

Este ingenioso código se utiliza "por razones de fiabilidad": he oído en alguna parte que, supuestamente, un bucle infinito es un Comportamiento No Definido, y aquí el compilador probablemente no se dará cuenta (Recuerden que 0x10000 se encuentra BootROM).

Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Aparentemente, ¿qué más se puede esperar? - un embedded duro, ¿qué hay de los fuentes? Pero en ese artículo el autor depuraba el código en C... Kriex-fex-peks:

(gdb) file builds/zeowaa-e115/sdboot.elf
Ya se está depurando un programa.
¿Está seguro de que desea cambiar el archivo? (y o n) y
Leyendo símbolos de builds/zeowaa-e115/sdboot.elf...hecho.

Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Solo hay que cargar no un archivo MIF ni un bin, sino la versión original en formato ELF.

Ahora se puede intentar adivinar la dirección donde continuará la ejecución (esta es otra razón por la cual el compilador no debería haber adivinado que el bucle es infinito). El comando

set variable $pc=0xADDR

permite cambiar el valor del registro en tiempo real (en este caso, la dirección de la instrucción actual). Con este mismo comando se pueden cambiar los valores almacenados en memoria (y registros asignados a memoria).

Al final, llegué a la conclusión (no estoy seguro de que sea correcta) de que tenemos "una imagen de la tarjeta SD que no es del sistema correcto", y no se debe regresar al comienzo de los datos cargados, sino a 0x89800 bytes más adelante:

diff --git a/bootrom/sdboot/head.S b/bootrom/sdboot/head.S
index 14fa740..2a6c944 100644
--- a/bootrom/sdboot/head.S
+++ b/bootrom/sdboot/head.S
@@ -13,7 +13,7 @@ _prog_start:
   smp_resume(s1, s2)
   csrr a0, mhartid
   la a1, dtb
-  li s1, PAYLOAD_DEST
+  li s1, (PAYLOAD_DEST + 0x89800)
   jr s1

   .section .rodata

Puede que esto también se deba a que, sin tener a mano una tarjeta innecesaria de 4 Gb, tomé una de 2 Gb y, de forma improvisada, la sustituí en el Makefile DEMO_END=11718750 en DEMO_END=3078900 (no busques el sentido en el significado concreto, no lo hay, simplemente ahora la imagen se ajusta a la tarjeta).

Nivel dos, U-Boot

Ahora seguimos "cayendo", pero ya estamos en la dirección 0x0000000080089a84. Aquí tengo que admitir que, en realidad, la exposición no se hace "con todas las paradas", sino que se escribe parcialmente "después", por lo que aquí ya he podido incluir el archivo dtb correcto de nuestro SoC y corregir la configuración HiFive_U-Boot variable CONFIG_SYS_TEXT_BASE=0x80089800 (en lugar de 0x08000000), para que la dirección de carga coincida con la real. Ahora cargamos la tarjeta del siguiente nivel con otra imagen:

(gdb) file ..\/freedom-u-sdk\/work\/HiFive_U-Boot\/u-boot
(gdb) tui en

Y vemos:

   │304     /*                                               │
   │305      * entrada de trampa                                    │
   │306      */                                              │
   │307     trap_entry:                                      │
   │308         addi sp, sp, -32*REGBYTES                    │
  >│309         SREG x1, 1*REGBYTES(sp)                      │
   │310         SREG x2, 2*REGBYTES(sp)                      │
   │311         SREG x3, 3*REGBYTES(sp)                      │

Y estamos saltando entre las líneas 308 y 309. No es sorprendente, teniendo en cuenta que en $sp está el valor 0xfffffffe31cdc0a0. Lamentablemente, también se "escapa" constantemente debido a la línea 307. Por eso intentaremos establecer un punto de interrupción en trap_entry, y luego volver a entrar en 0x80089800 (El punto de entrada de U-Boot), y esperemos que no requiera un ajuste correcto de los registros antes de la transición... Parece que funciona:

(gdb) b trap_entry
Breakpoint 1 at 0x80089a80: file \/hdd\/trosinenko\/fpga\/freedom-u-sdk\/HiFive_U-Boot\/arch\/riscv\/cpu\/HiFive\/start.S, line 308.
(gdb) set variable $pc=0x80089800
(gdb) c
Continuando.

Breakpoint 1, trap_entry () at \/hdd\/trosinenko\/fpga\/freedom-u-sdk\/HiFive_U-Boot\/arch\/riscv\/cpu\/HiFive\/start.S:308
(gdb) p\/x $sp
$4 = 0x81cf950

Un puntero de pila no muy bueno, hay que decir: apunta a un lugar que no es la RAM (si es que, por supuesto, no tenemos traducción de direcciones, pero esperemos que en el caso más sencillo).

Intentaremos reemplazar el puntero con 0x881cf950. Al final, llegamos a que handle_trap se llama una y otra vez, y nos llevamos a _exit_trap con el argumento epc=2148315240 (en forma decimal):

(gdb) x/10i 2148315240
   0x800cb068 :     lbu     a4,0(a5)
   0x800cb06c :     bnez    a4,0x800cb078 
   0x800cb070 :     sub     a0,a5,a0
   0x800cb074 :     ret
   0x800cb078 :     addi    a5,a5,1
   0x800cb07c :     j       0x800cb064 
   0x800cb080 : addi    sp,sp,-32
   0x800cb084 :       sd      s0,16(sp)
   0x800cb088 :       sd      ra,24(sp)
   0x800cb08c :      li      s0,0

Pongamos un breakpoint en strnlen, continuamos y vemos:

(gdb) bt
#0 strnlen (s=s@entry=0x10060000 "", count=18446744073709551615) en lib/string.c:283
#1 0x00000000800cc14c en string (buf=buf@entry=0x881cbd4c "", end=end@entry=0x881cc15c "", s=0x10060000 "", field_width=<optimizados>, precision=<optimizados>, flags=<optimizados>) en lib/vsprintf.c:265
#2 0x00000000800cc63c en vsnprintf_internal (buf=buf@entry=0x881cbd38 "código de excepción: 5 , ", size=size@entry=1060, fmt=0x800d446e "s , epc x , ra lxn", fmt@entry=0x800d4458 "código de excepción: %d , %s , epc x , ra lxn", args=0x881cc1a0,
 args@entry=0x881cc188) en lib/vsprintf.c:619
#3 0x00000000800cca54 en vsnprintf (buf=buf@entry=0x881cbd38 "código de excepción: 5 , ", size=size@entry=1060, fmt=fmt@entry=0x800d4458 "código de excepción: %d , %s , epc x , ra lxn", args=args@entry=0x881cc188) en lib/vsprintf.c:710
#4 0x00000000800cca68 en vscnprintf (buf=buf@entry=0x881cbd38 "código de excepción: 5 , ", size=size@entry=1060, fmt=fmt@entry=0x800d4458 "código de excepción: %d , %s , epc x , ra lxn", args=args@entry=0x881cc188) en lib/vsprintf.c:717
#5 0x00000000800ccb50 en printf (fmt=fmt@entry=0x800d4458 "código de excepción: %d , %s , epc x , ra lxn") en lib/vsprintf.c:792
#6 0x000000008008a9f0 en _exit_trap (regs=<optimizados>, epc=2148315240, code=<optimizados>) en arch/riscv/lib/interrupts.c:92
#7 handle_trap (mcause=<optimizados>, epc=<optimizados>, regs=<optimizados>) en arch/riscv/lib/interrupts.c:55
#8 0x0000000080089b10 en trap_entry () en /hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot/arch/riscv/cpu/HiFive/start.S:343
Backtrace detenido: el marco no guardó el PC

Parece que _exit_trap quiere mostrar información de depuración sobre la excepción ocurrida, pero no puede. Así que, algo no se está mostrando correctamente en nuestros fuentes. set directories ../freedom-u-sdk/HiFive_U-Boot/ ¡Oh! ¡Ahora se muestran!

Bueno, volvamos a ejecutar y veamos en la traza de pila la causa del problema original que causó el primer error (mcause == 5). Si entendí correctamente lo que está escrito aquí en la línea 37, esto significa Carga acceso fallo. La razón, aparentemente, es que aquí

arch/riscv/cpu/HiFive/start.S:

call_board_init_f:
    li  t0, -16
    li  t1, CONFIG_SYS_INIT_SP_ADDR
    and sp, t1, t0  /* forzar alineación de 16 bytes */

#ifdef CONFIG_DEBUG_UART
    jal debug_uart_init
#endif

call_board_init_f_0:
    mv  a0, sp
    jal board_init_f_alloc_reserve
    mv  sp, a0
    jal board_init_f_init_reserve

    mv  a0, zero    /* a0 <-- boot_flags = 0 */
    la t5, board_init_f
    jr t5       /* saltar a board_init_f() */

$sp tiene ese valor incorrecto, y dentro de board_init_f_init_reserve se produce un error. Parece que aquí está el culpable: una variable con un nombre muy elocuente CONFIG_SYS_INIT_SP_ADDR. Se define en el archivo HiFive_U-Boot/include/configs/HiFive-U540.h. En algún momento, incluso pensé que quizás sería más fácil ajustar el procesador en lugar de terminar el cargador para él. Pero luego vi que esto se parecía más a un artefacto de configuraciones no completamente ajustadas para otra configuración de memoria, y podría intentar hacer lo siguiente:#if 0-

diff --git a/include/configs/HiFive-U540.h b/include/configs/HiFive-U540.h
index ca89383..245542c 100644
--- a/include/configs/HiFive-U540.h
+++ b/include/configs/HiFive-U540.h
@@ -65,12 +65,9 @@
 #define CONFIG_SYS_SDRAM_BASE  PHYS_SDRAM_0
 #endif
 #if 1
-
#define CONFIG_NR_DRAM_BANKS 1
+#define CONFIG_NR_DRAM_BANKS   1
 #define PHYS_SDRAM_0   0x80000000              /* SDRAM Bank #1 */
-#define PHYS_SDRAM_1   
-       (PHYS_SDRAM_0 + PHYS_SDRAM_0_SIZE)      /* SDRAM Bank #2 */
-#define PHYS_SDRAM_0_SIZE      0x80000000      /* 2 GB */
-#define PHYS_SDRAM_1_SIZE      0x10000000      /* 256 MB */
+#define PHYS_SDRAM_0_SIZE      0x40000000      /* 1 GB */
 #define CONFIG_SYS_SDRAM_BASE  PHYS_SDRAM_0
 #endif
 /*
@@ -81,7 +78,7 @@
 #define CONSOLE_ARG                            "console=ttyS0,115200 "

 /* Init Stack Pointer */
-#define CONFIG_SYS_INIT_SP_ADDR                (0x08000000 + 0x001D0000 - 
+#define CONFIG_SYS_INIT_SP_ADDR                (0x80000000 + 0x001D0000 - 
                                        GENERATED_GBL_DATA_SIZE)

 #define CONFIG_SYS_LOAD_ADDR           0xa0000000      /* partway up SDRAM */

En algún momento, la cantidad de soluciones improvisadas de fijaciones tecnológicas alcanzó un punto crítico. Después de un poco de reflexión, llegué a la necesidad de hacer un puerto correcto para mi placa. Para ello, hay que copiar y ajustar una cierta cantidad de archivos a nuestra configuración.

Bueno, aproximadamente, aquí hay un poco

trosinenko@trosinenko-pc:/hdd/trosinenko/fpga/freedom-u-sdk/HiFive_U-Boot$ git show --name-status
commit 39cd67d59c16ac87b46b51ac1fb58f16f1eb1048 (HEAD -> zeowaa-1gb)
Author: Anatoly Trosinenko 
Date:   Tue Jul 2 17:13:16 2019 +0300

    Soporte inicial para la placa Zeowaa A-E115FB

M       arch/riscv/Kconfig
A       arch/riscv/cpu/zeowaa-1gb/Makefile
A       arch/riscv/cpu/zeowaa-1gb/cpu.c
A       arch/riscv/cpu/zeowaa-1gb/start.S
A       arch/riscv/cpu/zeowaa-1gb/timer.c
A       arch/riscv/cpu/zeowaa-1gb/u-boot.lds
M       arch/riscv/dts/Makefile
A       arch/riscv/dts/zeowaa-1gb.dts
A       board/Zeowaa/zeowaa-1gb/Kconfig
A       board/Zeowaa/zeowaa-1gb/MAINTAINERS
A       board/Zeowaa/zeowaa-1gb/Makefile
A       board/Zeowaa/zeowaa-1gb/Zeowaa-A-E115FB.c
A       configs/zeowaa-1gb_defconfig
A       include/configs/zeowaa-1gb.h

Se pueden ver los detalles en el repositorio.

Resultó que en esta placa de SiFive, los registros de algunos dispositivos tienen direcciones diferentes. Y también resultó que U-Boot se configura mediante el mecanismo Kconfig, que ya es familiar de la kernel de Linux; por ejemplo, se puede ordenar make menuconfig, y aparecerá ante usted una interfaz de texto cómoda con descripciones de los parámetros. ? Y así, fusionando las descripciones de dos placas para crear la descripción de una tercera, eliminando configuraciones efectistas de PLL (parece que esto está relacionado de alguna manera con el control desde la computadora host a través de PCIe, aunque no estoy seguro), obtuve un firmware que, en las condiciones adecuadas en Marte, me enviaba a través de UART un mensaje sobre el hash del commit del que estaba compilado y cuánta DRAM tenía (pero esta información la había indicado yo mismo en el encabezado).

Lo único lamentable es que, después de esto, la placa generalmente dejaba de responder a través del JTAG del procesador, y la carga desde la tarjeta SD — desafortunadamente, en mi configuración no es rápida. Por otro lado, a veces el BootROM producía un mensaje que ERROR, no se pudo iniciar, y entonces aparecía U-Boot. En ese momento me di cuenta: aparentemente, tras reiniciar el bitstream en la FPGA, la memoria no se sobreescribe, no tiene tiempo de "desincronizarse", etc. En resumen, se puede simplemente conectarse al depurador cuando aparece el mensaje CARGANDO / conectarse al depurador y dar el comando set variable $pc=0x80089800, evitando así esta carga prolongada (por supuesto, suponiendo que anteriormente se interrumpió lo suficientemente pronto como para no haber cargado nada sobre el código original).

Por cierto, ¿es normal que el procesador se congele completamente y no se pueda conectar el depurador JTAG con los mensajes

Error: unable to halt hart 0
Error:   dmcontrol=0x80000001
Error:   dmstatus =0x00030c82

Espera un momento! ¡Ya he visto esto antes! Algo parecido ocurre durante el deadlock de TileLink, y no confío en el autor del controlador de memoria — él mismo lo escribió... De repente, después de la primera recompilación exitosa del procesador tras editar el controlador, vi:

INIT
CMD0
CMD8
ACMD41
CMD58
CMD16
CMD18
CARGANDO
BOOT

U-Boot 2018.09-g39cd67d-dirty (Jul 03 2019 - 13:50:33 +0300)

DRAM:  1 GiB
MMC:
ANTES DE CARGAR ENVANTES DE FDTCONTROLADDRANTES DE CARGARADDRIn:    serial
Out:   serial
Err:   serial
Presione cualquier tecla para detener el arranque automático:  3

Sobre esta extraña línea anterior In: serial No te preocupes, fui yo tratando de entender en un procesador que se bloquea si funciona correctamente con el entorno. ¿Qué significa "Ya lleva diez minutos así"? ¡Al menos logró reubicarse y pasar al menú de arranque! Un pequeño inciso: aunque U-Boot se carga en los primeros 2^24 bytes desde la tarjeta SD, al iniciarse se copia a otra dirección, que puede estar escrita en el encabezado de configuración o simplemente en las direcciones más altas de la memoria RAM, realiza la reubicación de símbolos ELF y transfiere el control allí. Así que parece que este nivel se ha superado y como bonus, obtuvimos un procesador que no se bloquea completamente después de esto.

Entonces, ¿por qué no funciona el temporizador? Parece que el reloj no avanza por alguna razón...

(gdb) x/x 0x0200bff8
0x200bff8:      0x00000000

¿Y qué tal si giro las agujas manualmente?

(gdb) set variable *0x0200bff8=310000000
(gdb) c

Entonces:

Presiona cualquier tecla para detener el autoboot:  0
MMC_SPI: 0 en 0:1 hz 20000000 modo 0

Conclusión: el reloj no avanza. Probablemente esto también está causando que la entrada del teclado no funcione:

HiFive_U-Boot/cmd/bootmenu.c:

static void bootmenu_loop(struct bootmenu_data *menu,
        enum bootmenu_key *key, int *esc)
{
    int c;

    while (!tstc()) {
        WATCHDOG_RESET();
        mdelay(10);
    }

    c = getc();

    switch (*esc) {
    case 0:
        /* Primer carácter de la secuencia de escape ANSI 'e' */
        if (c == 'e') {
            *esc = 1;
            *key = KEY_NONE;
        }
        break;
    case 1:
        /* Segundo carácter de ANSI '[' */
        if (c == '[') {
...

El problema resultó ser que me pasé un poco: añadí a la configuración del procesador la clave:

  case DTSTimebase => BigInt(0)

... basándome en que en el comentario decía "si no sabes, déjalo en 0". Y efectivamente WithNBigCores estaba configurándolo en 1MHz (como, de hecho, se indicaba en la configuración de U-Boot). Pero yo, maldita sea, soy meticuloso: aquí no sé, ¡aquí 25MHz! Al final, nada funciona. Quité mis "mejoras" y...

Presiona cualquier tecla para detener el autoboot:  0
MMC_SPI: 0 en 0:1 hz 20000000 modo 0
## Tipo de tabla de particiones desconocido 0
libfdt fdt_path_offset() devolvió FDT_ERR_NOTFOUND
** No hay tabla de particiones - mmc 0 **
## Info: tamaño de los datos de entrada = 34 = 0x22
Ejecutando uEnv.txt boot2...
## Error: "boot2" no definido
HiFive-Unleashed #

¡Incluso se pueden ingresar comandos! Por ejemplo, después de hurgar un poco, finalmente se puede adivinar ingresar mmc_spi 1 10000000 0; mmc part, reduciendo la frecuencia del SPI de 20MHz a 10MHz. ¿Por qué? Bueno, en la configuración estaba escrita la frecuencia máxima de 20MHz, que sigue ahí. Pero, según entendí, las interfaces, al menos aquí, funcionan así: el código divide la frecuencia del bloque de hardware (que en mi caso es de 25MHz) por la frecuencia objetivo y establece el valor resultante como divisor en el registro de control correspondiente. El problema es que para un UART de 115200Hz, aproximadamente eso es lo que se necesita, pero si se divide 25000000 entre 20000000 da 1, es decir, funcionará a 25MHz. Puede que esto esté bien, pero si se imponen limitaciones, significa que a alguien le debe importar (aunque no es seguro)... En fin, es más fácil marcarlo y seguir adelante — lejos y, desgraciadamente, por mucho tiempo. 25MHz no es un Core i9.

Salida de la consola

HiFive-Unleashed # env edit mmcsetup
edit: mmc_spi 1 10000000 0; mmc part
HiFive-Unleashed # boot
MMC_SPI: 1 en 0:1 hz 10000000 modo 0

Mapa de particiones para el dispositivo MMC 0  --   Tipo de partición: EFI

Partición    Inicio LBA       Fin LBA         Nombre
            Atributos
            Tipo GUID
            GUID de partición
  1     0x00000800      0x0000ffde      "Vfat Boot"
            attrs:  0x0000000000000000
            tipo:   ebd0a0a2-b9e5-4433-87c0-68b6b72699c7
            tipo:   datos
            guid:   76bd71fd-1694-4ff3-8197-bfa81699c2fb
  2     0x00040800      0x002efaf4      "root"
            attrs:  0x0000000000000000
            tipo:   0fc63daf-8483-4772-8e79-3d69d8477de4
            tipo:   linux
            guid:   9f3adcc5-440c-4772-b7b7-283124f38bf3
  3     0x0000044c      0x000007e4      "uboot"
            attrs:  0x0000000000000000
            tipo:   5b193300-fc78-40cd-8002-e86c45580b47
            guid:   bb349257-0694-4e0f-9932-c801b4d76fa3
  4     0x00000400      0x0000044b      "uboot-env"
            attrs:  0x0000000000000000
            tipo:   a09354ac-cd63-11e8-9aff-70b3d592f0fa
            guid:   4db442d0-2109-435f-b858-be69629e7dbf
libfdt fdt_path_offset() devolvió FDT_ERR_NOTFOUND
2376 bytes leídos en 0 ms
Ejecutando uEnv.txt boot2...
15332118 bytes leídos en 0 ms
## Cargando núcleo de la imagen FIT en 90000000 ...
   Usando la configuración 'config-1'
   Intentando subimagen de núcleo 'bbl'
     Descripción:  BBL/SBI/riscv-pk
     Tipo:         Imagen del núcleo
     Compresión:   sin comprimir
     Inicio de datos:   0x900000d4
     Tamaño de datos:    74266 Bytes = 72.5 KiB
     Arquitectura: RISC-V
     SO:           Linux
     Dirección de carga: 0x80000000
     Punto de entrada:  0x80000000
     Algoritmo de hash:    sha256
     Valor de hash:   28972571467c4ad0cf08a81d9cf92b9dffc5a7cb2e0cd12fdbb3216cf1f19cbd
   Verificando la integridad del hash ... sha256+ OK
## Cargando fdt de la imagen FIT en 90000000 ...
   Usando la configuración 'config-1'
   Intentando subimagen fdt 'fdt'
     Descripción:  no disponible
     Tipo:         Árbol de Dispositivo Plano
     Compresión:   sin comprimir
     Inicio de datos:   0x90e9d31c
     Tamaño de datos:    6911 Bytes = 6.7 KiB
     Arquitectura: RISC-V
     Dirección de carga: 0x81f00000
     Algoritmo de hash:    sha256
     Valor de hash:   10b0244a5a9205357772ea1c4e135a4f882409262176d8c7191238cff65bb3a8
   Verificando la integridad del hash ... sha256+ OK
   Cargando fdt de 0x90e9d31c a 0x81f00000
   Arrancando usando el blob fdt en 0x81f00000
## Cargando componentes desde la imagen FIT en 90000000 ...
   Intentando subimagen de componentes 'kernel'
     Descripción:  núcleo de Linux
     Tipo:         Imagen del núcleo
     Compresión:   sin comprimir
     Inicio de datos:   0x900123e8
     Tamaño de datos:    10781356 Bytes = 10.3 MiB
     Arquitectura: RISC-V
     SO:           Linux
     Dirección de carga: 0x80200000
     Punto de entrada:  no disponible
     Algoritmo de hash:    sha256
     Valor de hash:   72a9847164f4efb2ac9bae736f86efe7e3772ab1f01ae275e427e2a5389c84f0
   Verificando la integridad del hash ... sha256+ OK
   Cargando componentes de 0x900123e8 a 0x80200000
## Cargando componentes desde la imagen FIT en 90000000 ...
   Intentando subimagen de componentes 'ramdisk'
     Descripción:  buildroot initramfs
     Tipo:         Imagen de RAMDisk
     Compresión:   gzip comprimido
     Inicio de datos:   0x90a5a780
     Tamaño de datos:    4467411 Bytes = 4.3 MiB
     Arquitectura: RISC-V
     SO:           Linux
     Dirección de carga: 0x82000000
     Punto de entrada:  no disponible
     Algoritmo de hash:    sha256
     Valor de hash:   883dfd33ca047e3ac10d5667ffdef7b8005cac58b95055c2c2beda44bec49bd0
   Verificando la integridad del hash ... sha256+ OK
   Cargando componentes de 0x90a5a780 a 0x82000000

Está bien, hemos alcanzado un nuevo nivel, pero aún se congela. A veces también lanza excepciones. Puedes ver el mcause, acechando el código en la dirección especificada $pc y después si terminar en trap_entry. El manejador de U-Boot solo puede mostrar para mcause = 0..4, así que prepárense para entrar en un ciclo debido a una carga incorrecta. Aquí me metí en la configuración, empecé a revisar qué había cambiado y recordé: ahí está en conf/rvboot-fit.txt la empresa Finservice ha estado operando desde 2012 y es un corredor financiero independiente líder en el sector de crédito POS en el mercado ruso. Actualmente, Finservice forma parte de un gran holding multifuncional, cuenta con más de 200 empleados y lleva a cabo una expansión activa en todas las regiones de Rusia.

fitfile=image.fit
# a continuación, debe coincidir con lo que hay en FIT (ugha)

Bueno, ajustemos todos los archivos, reemplazaremos la línea de comandos del núcleo de forma aproximada, ya que hay sospechas de que SIF0 — es la salida hacia algún lugar por PCIe:

-bootargs=console=ttySIF0,921600 debug
+bootargs=console=ttyS0,125200 debug

Y de paso cambiaremos el algoritmo de hash de SHA-256 a MD5: no necesito criptografía fuerte (especialmente durante la depuración), se considera que es extremadamente lento, y para la detección de errores de integridad durante el arranque, MD5 es más que suficiente. Y entonces, ¿qué conseguimos? Ahora pasamos el nivel anterior notablemente más rápido (gracias a un hash más simple), y se abrió el siguiente:

...
   Verifying Hash Integrity ... md5+ OK
   Loading loadables from 0x90a5a758 to 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
        linux,initrd-end = ;
        linux,initrd-start = ;
        riscv,kernel-end = ;
        riscv,kernel-start = ;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
chosen {
        linux,initrd-end = ;
        linux,initrd-start = ;
        riscv,kernel-end = ;
        riscv,kernel-start = ;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
};
   Loading Kernel Image ... OK
Booting kernel in
3

Solo que el reloj no avanza...

(gdb) x/x 0x0200bff8
0x200bff8:      0x00000000

Ups, parece que ajustar el reloj fue un placebo, aunque en ese momento pensé que había funcionado. No, por supuesto hay que repararlo, pero primero vamos a mover las manecillas manualmente y veamos qué pasa:

0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=1000000
(gdb) c
Continuando.
^C
Programa recibió señal SIGINT, Interrupción.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=2000000
(gdb) c
Continuando.
^C
Programa recibió señal SIGINT, Interrupción.
0x00000000bff6dbb0 in ?? ()
(gdb) set variable *0x0200bff8=3000000
(gdb) c
Continuando.

Mientras tanto...

   Loading Kernel Image ... OK
Booting kernel in
3
2
1
0
## Starting application at 0x80000000 ...

No, iré a automatizar el ajuste del reloj, ¡puede que intente calibrar el temporizador allá!

Y la dirección de la instrucción actual apunta a algún lugar en

0000000080001c20 :
    80001c20:   1141                    addi    sp,sp,-16
    80001c22:   e022                    sd      s0,0(sp)
    80001c24:   842a                    mv      s0,a0
    80001c26:   00005517                auipc   a0,0x5
    80001c2a:   0ca50513                addi    a0,a0,202 # 80006cf0 
    80001c2e:   e406                    sd      ra,8(sp)
    80001c30:   f7fff0ef                jal     ra,80001bae 
    80001c34:   8522                    mv      a0,s0
    80001c36:   267000ef                jal     ra,8000269c 
    80001c3a:   00010797                auipc   a5,0x10
    80001c3e:   41e78793                addi    a5,a5,1054 # 80012058 
    80001c42:   639c                    ld      a5,0(a5)
    80001c44:   c399                    beqz    a5,80001c4a 
    80001c46:   72c000ef                jal     ra,80002372 
    80001c4a:   45a1                    li      a1,8
    80001c4c:   4501                    li      a0,0
    80001c4e:   dc7ff0ef                jal     ra,80001a14 
    80001c52:   10500073                wfi
    80001c56:   bff5                    j       80001c52

dentro del Berkeley Boot Loader que se ha cargado. Personalmente, me desconcierta la mención htif — interfaz de host, utilizada para el arranque conectado del núcleo (es decir, en cooperación con el ARM host), yo suponía que sería independiente. Sin embargo, si encuentras esta función en el código fuente, se puede ver que no es tan malo:

void poweroff(uint16_t code)
{
  printm("Apagarrn");
  finisher_exit(code);
  if (htif) {
    htif_poweroff();
  } else {
    send_ipi_many(0, IPI_HALT);
    while (1) { asm volatile ("wfin"); }
  }
}

Misión: inicia el reloj

La búsqueda de registros en CLINT nos lleva a

    val io = IO(new Bundle {
      val rtcTick = Bool(INPUT)
    })

    val time = RegInit(UInt(0, width = timeWidth))
    cuando (io.rtcTick) { time := time + UInt(1) }

Que se conecta a RTC, o en el misterioso MockAON, sobre el cual inicialmente pensé: "¿Qué es esto? ¿Poco claro? ¡Desconectamos!" Dado que todavía no me queda claro qué tipo de magia de reloj está sucediendo ahí, simplemente reimplementaré esta lógica en System.scala:

  val rtcDivider = RegInit(0.asUInt(16.W)) // por si acaso soportaré hasta 16GHz, soy optimista :)
  val mhzInt = p(DevKitFPGAFrequencyKey).toInt
  // Supongamos que la frecuencia es un número entero en megahercios
  rtcDivider := Mux(rtcDivider === (mhzInt - 1).U, 0.U, rtcDivider + 1.U)
  outer.clintOpt.foreach { clint =>
    clint.module.io.rtcTick := rtcDivider === 0.U
  }

Avanzando hacia el núcleo de Linux

Aquí la narración ya se ha prolongado y ha comenzado a volverse un poco monótona, así que describiré a grandes rasgos:

BBL asumió la existencia de FDT en la dirección 0xF0000000, pero ya lo había corregido. Bueno, busquemos más... Encontré en HiFive_U-Boot/arch/riscv/lib/boot.c, lo reemplacé por 0x81F00000, indicado en la configuración de arranque de U-Boot.

Luego BBL se quejaba de que no había memoria. Mi camino me llevó a la función mem_prop, que en riscv-pk/machine/fdt.c: de ahí supe que debía marcar el nodo fdt ram como device_type = "memory" — Luego, quizás sea necesario ajustar el generador del procesador, pero por ahora simplemente lo escribiré a mano — de todos modos, transferí este archivo manualmente.

Ahora recibí un mensaje (presentado en formato con saltos de línea):

Este es el dummy_payload de bbl. Para arrancar un kernel real, reconfigura bbl
con la opción --with-payload=PATH, luego recompila bbl. Alternativamente,
bbl se puede usar en modo solo firmware añadiendo nodos de árbol de dispositivo
para un payload externo y usando las opciones -bios y -kernel de QEMU.

Parece que las opciones se especifican correctamente riscv,kernel-start y riscv,kernel-end en DTB, pero se analizan ceros. Depuración query_chosen mostró que BBL intenta analizar una dirección de 32 bits, y encuentra un par <0x0 0xADDR>, y el primer valor parece ser las posiciones bajas. Añadí a la sección chosen

chosen {
      #address-cells = ;
      #size-cells = ;
      ...
}

y ajusté la generación de valores: no añadir 0x0 como primer elemento.

Estos 100500 simples pasos permitirán ver fácil y sencillamente cómo cae el pingüino:

Texto oculto

   Verificando la integridad del hash ... md5+ OK
   Cargando elementos desde 0x90a5a758 hasta 0x82000000
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
chosen {
        linux,initrd-end = ;
        linux,initrd-start = ;
        riscv,kernel-end = ;
        riscv,kernel-start = ;
        #address-cells = ;
        #size-cells = ;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
        stdout-path = "uart0:38400n8";
};
libfdt fdt_path_offset() returned FDT_ERR_NOTFOUND
chosen {
        linux,initrd-end = ;
        linux,initrd-start = ;
        riscv,kernel-end = ;
        riscv,kernel-start = ;
        #address-cells = ;
        #size-cells = ;
        bootargs = "debug console=tty0 console=ttyS0,125200 root=/dev/mmcblk0p2 rootwait";
        stdout-path = "uart0:38400n8";
};
   Cargando la imagen del kernel ... OK
Arrancando el kernel en
3
2
1
0
## Iniciando la aplicación en 0x80000000 ...
bbl loader

                SIFIVE, INC.

         5555555555555555555555555
        5555                   5555
       5555                     5555
      5555                       5555
     5555       5555555555555555555555
    5555       555555555555555555555555
   5555                             5555
  5555                               5555
 5555                                 5555
5555555555555555555555555555          55555
 55555           555555555           55555
   55555           55555           55555
     55555           5           55555
       55555                   55555
         55555               55555
           55555           55555
             55555       55555
               55555   55555
                 555555555
                   55555
                     5

           SiFive RISC-V Core IP
[    0.000000] OF: fdt: Ignorando el rango de memoria 0x80000000 - 0x80200000
[    0.000000] Versión de Linux 4.19.0-sifive-1+ (trosinenko@trosinenko-pc) (gcc version 8.3.0 (Buildroot 2019.02-07449-g4eddd28f99)) #1 SMP Mié Jul 3 21:29:21 MSK 2019
[    0.000000] bootconsole [early0] habilitado
[    0.000000] Ramdisk inicial en: 0x(____ptrval____) (16777216 bytes)
[    0.000000] Rangos de zona:
[    0.000000]   DMA32    [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000]   Normal   [mem 0x00000000c0000000-0x00000bffffffffff]
[    0.000000] Inicio de zona movible para cada nodo
[    0.000000] Rangos de nodo de memoria temprana
[    0.000000]   nodo   0: [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] Configuración de memoria inicial nodo 0 [mem 0x0000000080200000-0x00000000bfffffff]
[    0.000000] En el nodo 0 total de páginas: 261632
[    0.000000]   Zona DMA32: 3577 páginas usadas para memmap
[    0.000000]   Zona DMA32: 0 páginas reservadas
[    0.000000]   Zona DMA32: 261632 páginas, lote LIFO:63
[    0.000000] TLB de E/S de software: mapeado [mem 0xbb1fc000-0xbf1fc000] (64MB)

(el emblema lo muestra BBL, y lo que tiene las marcas de tiempo es el núcleo).

Afortunadamente, no sé cómo en todos lados, pero en RocketChip, al conectar el depurador por JTAG, se pueden atrapar trampas directamente: el depurador se detiene exactamente en ese punto.

El programa recibió la señal SIGTRAP, trampa de seguimiento/punto de interrupción.
0xffffffe0000024ca en ?? ()
(gdb) bt
#0  0xffffffe0000024ca en ?? ()
La traza se detuvo: el marco anterior es idéntico a este marco (¿pila corrupta?)
(gdb) archivo work/linux/vmlinux
Ya hay un programa en depuración.
¿Está seguro de que desea cambiar de archivo? (y o n) y
Leyendo símbolos de work/linux/vmlinux...hecho.
(gdb) bt
#0  0xffffffe0000024ca en setup_smp () en /hdd/trosinenko/fpga/freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:75
#1  0x0000000000000000 en ?? ()
La traza se detuvo: el marco no guardó el PC.

freedom-u-sdk/linux/arch/riscv/kernel/smpboot.c:

void __init setup_smp(void)
{
    struct device_node *dn = NULL;
    int hart;
    bool found_boot_cpu = false;
    int cpuid = 1;

    while ((dn = of_find_node_by_type(dn, "cpu"))) {
        hart = riscv_of_processor_hartid(dn);
        if (hart < 0)
            continue;

        if (hart == cpuid_to_hartid_map(0)) {
            BUG_ON(found_boot_cpu);
            found_boot_cpu = 1;
            continue;
        }

        cpuid_to_hartid_map(cpuid) = hart;
        set_cpu_possible(cpuid, true);
        set_cpu_present(cpuid, true);
        cpuid++;
    }

    BUG_ON(!found_boot_cpu); // < ESTÁS AQUÍ
}

Como se decía en un viejo chiste, CPU no encontrada, ejecutando emulación de software. O no ejecutando. Perdidos en un único núcleo del procesador.

/* The lucky hart to first increment this variable will boot the other cores */
atomic_t hart_lottery;
unsigned long boot_cpu_hartid;

Un buen comentario en linux/arch/riscv/kernel/setup.c — una especie de pintar la cerca al estilo de Tom Sawyer. En resumen, hoy no se encontraron ganadores por alguna razón, el premio se traslada a la siguiente edición…

Con esto propongo terminar este artículo que ya se ha prolongado demasiado.

La continuación sigue. En ella habrá una pelea con un error astuto que logra esconderse si te acercas lentamente con el singlestep.

Screencast de carga de texto (enlace externo):
Parte 3: Casi cargamos Linux desde la tarjeta SD en RocketChip

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster