Parte 1: RISC-V / RocketChip en un hábitat antinatural

Configurando RocketChip

Recientemente en Habré se publicó un artículo sobre cómo experimentar con la arquitectura RISC-V sin el costo del hardware. Pero, ¿qué pasa si haces esto en el tablero de depuración? Recuerde los memes sobre el generador de juegos: alrededor de 20 marcas de verificación al estilo de "Los gráficos no son peores que la Crisis", "Puede robar corovanos" y el botón "Generar". El generador de SoC RocketChip está organizado aproximadamente de la misma manera, solo que no hay una ventana con marcas de verificación, sino un código Scala y un poco de ensamblador y archivos Make. En este artículo, mostraré lo fácil que es portar este RocketChip de su Xilinx nativo a Altera / Intel.


DESCARGO DE RESPONSABILIDAD: el autor no es responsable de la placa "quemada": observe cuidadosamente cómo configura los pines, qué conecta físicamente, etc. Bueno, también observe las precauciones de seguridad. No debe pensar que, dado que todo está conectado a través de USB, definitivamente es seguro para una persona: como entendí en mis experimentos anteriores, incluso si trabaja con una placa USB, aún no tiene que tocar la batería de calentamiento con el pie, porque la diferencia potencial ... Oh sí , No soy casi un ingeniero electrónico o plisovode profesional, solo soy un programador de Scala.


Según tengo entendido, la plataforma inicial para depurar RocketChip fue el FPGA Xilinx. A juzgar por el repositorio, que pronto clonaremos, también fue trasladado a Microsemi. En algún lugar escuché sobre el uso de Altera, pero no vi la fuente. Al final resultó que, este no es un gran problema: desde el momento en que se recibió la placa y se estudió el repositorio SiFive Freedom hasta un "sin memoria" funcional (es decir, que solo tiene registros de procesador, BootROM y registros mapeados en memoria) "microcontrolador" de 32 bits (aunque esto ya es lo que resulta que el nanocontrolador resulta ...) 3 días libres y 4 noches de semana han pasado, y me habría llevado incluso menos si se me ocurrió de inmediato definir definir SYNTHESIS nivel mundial.


Materiales


Para empezar: una lista de materiales en el sentido amplio de la palabra. Necesitamos el siguiente software:


  • RocketChip : contiene el procesador en sí y el entorno de ensamblaje (incluido, describe varios sub-repositorios). Capaz de crear RocketChip con el procesador central de Rocket en orden.
  • SiFive Freedom - vinculante para varias placas de depuración - allí agregaremos soporte para nuestro
  • rocket-tools : herramientas para construir código y depurar para arquitecturas RISC-V de 32 y 64 bits
  • openocd-riscv : puerto OpenOCD para trabajar en JTAG con núcleos RISC-V (al menos RocketChip)
  • Comunidad IntelliJ IDEA para editar el código Scala, que es la mayoría en RocketChip
  • La cadena de herramientas premontada de SiFive también puede ser útil, un enlace al que vi en el artículo ya mencionado

Lo que se necesita del hierro:


  • Kit Zeowaa de Aliexpress: placa con Cyclone 4 EP4CE115 y (¿clon?) USB Blaster
  • RaspberryPi como depurador JTAG para soft core

Se supone que la máquina host es una computadora relativamente poderosa con Ubuntu y Quartus Lite 18 instalados.


En realidad, hay una opción para iniciar en la nube de Amazon en sus instancias de FPGA desde el mismo SiFive, llamado FireSim , pero no es tan interesante, Sí, y los LED son poco visibles. . Además, en este caso, necesitará especificar su clave API en la instancia de control para iniciar otras máquinas virtuales, y debe cuidarla, de lo contrario, se rumorea que puede despertarse un día con una deuda de decenas de miles de dólares ...


Estudiamos la situacion


Para comenzar, tomé el proyecto de prueba read_write_1G del proveedor de la placa e intenté agregarle las fuentes necesarias. ¿Por qué no crear uno nuevo? Porque soy nuevo, y en este proyecto los nombres de los pines ya han sido mapeados. Por lo tanto, debe tomar el código fuente de alguna parte. Para hacer esto, necesitaremos el repositorio de freedom ya especificado (que no debe confundirse con freedom-e-sdk ). Para obtener al menos algo, recopilaremos rocket-tools acuerdo con las instrucciones (literalmente lanzando dos scripts y mucha espera), y luego ejecutaremos


 RISCV=$(pwd)/../rocket-tools make -f Makefile.e300artydevkit verilog mcs 

El verilog objetivo generará un gran archivo Verilog con fuentes de procesador para nosotros, y mcs compilará BootROM. No hay que preocuparse de que mcs falle: simplemente no tenemos Xilinx Vivado, por lo que el BootROM compilado no se puede convertir al formato que necesita Vivado.


A través del elemento de menú Proyecto Quartus -> Agregar / Eliminar archivos en el proyecto ... agregue freedom/builds/e300artydevkit/sifive.freedom.everywhere.e300artydevkit.E300ArtyDevKitConfig.v , configure la entidad de nivel superior: E300ArtyDevKitFPGAChip en la pestaña General y comience la compilación (posiblemente , la lista de finalización automática de la entidad de nivel superior aparecerá solo después de la primera compilación). Como resultado, recibimos toneladas de errores que nos AsyncResetReg sobre la ausencia de AsyncResetReg , módulos IOBUF , etc. Si no hay errores, entonces olvidó cambiar la entidad de nivel superior. Si revisa la fuente, puede encontrar directamente el archivo AsyncResetReg.v , pero IOBUF es un enlace al núcleo IP de Xilinx . Primero, agregue freedom/rocket-chip/src/main/resources/vsrc/AsyncResetReg.v . Y plusarg_reader.v también se agregará.


Ejecute la compilación y obtenga otro error:


 Error (10174): Verilog HDL Unsupported Feature error at plusarg_reader.v(18): system function "$value$plusargs" is not supported for synthesis 

En principio, se pueden esperar construcciones no sintetizadas de un archivo con un nombre similar.


plusarg_reader.v
 // See LICENSE.SiFive for license details. //VCS coverage exclude_file // No default parameter values are intended, nor does IEEE 1800-2012 require them (clause A.2.4 param_assignment), // but Incisive demands them. These default values should never be used. module plusarg_reader #(parameter FORMAT="borked=%d", DEFAULT=0) ( output [31:0] out ); `ifdef SYNTHESIS assign out = DEFAULT; `else reg [31:0] myplus; assign out = myplus; initial begin if (!$value$plusargs(FORMAT, myplus)) myplus = DEFAULT; end `endif endmodule 

Como podemos ver, este módulo probablemente lee las opciones de simulación desde la línea de comando, y al sintetizarlo simplemente devuelve el valor predeterminado. El problema es que nuestro proyecto no define define SYNTHESIS nombre. Sería posible ifdef `define SYNTHESIS en la línea anterior justo antes del ifdef , y luego pasar media semana tratando de entender por qué el núcleo no se inicia (y después de todo, la infección se sintetiza ...). No repita mis errores, simplemente abra de nuevo las propiedades del proyecto y, en la pestaña Configuración del compilador-> Verilog HDL Input , defina la macro SYNTHESIS , y al menos 1, no en <NONE> (línea vacía).


Aqui Ahora Quartus jura por los enlaces que faltan: es hora de configurar un proyecto en Idea y comenzar a portar.


Conociendo el proyecto


Le decimos a la idea del proyecto Importar, especificamos la ruta al repositorio de freedom , indicamos el tipo de proyecto sbt, marcamos el uso del shell sbt para las importaciones, para las casillas de compilación. Aquí, parece que el cuento de hadas termina, pero no encuentra la idea de un medio proyecto: todos los códigos fuente están marcados en rojo. Según la información de aquí , obtuve el siguiente procedimiento:


  • abra la ventana de shell sbt
  • entrar clean
  • presione el botón verde de reinicio a la izquierda
  • después de reiniciar sbt, ingrese el primer comando ++2.12.4 , cambiando así todos los subproyectos a Scala versión 2.12.4, luego ejecutaremos compile
  • haga clic en Actualizar todos los proyectos de sbt Idea
  • ¡BENEFICIO !, ahora la luz de fondo funciona correctamente

Hasta ahora, intentaré ensamblar el proyecto de alguna manera, al menos en modo semi-manual. Por cierto, alguien, dime, ¿está quartus_ipcreate en Lite Edition? Crearemos variaciones de IP manualmente por ahora, y los enlaces solo estarán en Scala como BlackBox.


En este caso, estamos interesados ​​en la siguiente jerarquía de directorios:


 fpga-shells () | +-src/main/scala | +- ip/intel <--     IP Variations | | | +- Intel.scala | +- shell/intel <--       | +- ZeowaaShell.scala src/main/scala | +- everywhere.e300artydevkit <--   "" ,    | +- zeowaa/e115 <--      "" SoC | +- Config +- FPGAChip +- Platform +- System 

También necesita agregar un Makefile similar a Makefile.e300artydevkit , algo como esto:


Makefile.zeowaa-e115:


 # See LICENSE for license details. base_dir := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) BUILD_DIR := $(base_dir)/builds/zeowaa-e115 FPGA_DIR := $(base_dir)/fpga-shells/intel MODEL := FPGAChip PROJECT := sifive.freedom.zeowaa.e115 export CONFIG_PROJECT := sifive.freedom.zeowaa.e115 export CONFIG := ZeowaaConfig export BOARD := zeowaa export BOOTROM_DIR := $(base_dir)/bootrom/xip rocketchip_dir := $(base_dir)/rocket-chip sifiveblocks_dir := $(base_dir)/sifive-blocks include common.mk 

Fijaciones


Para empezar, implementamos este IOBUF ; difícilmente será difícil. A juzgar por el código Scala, este es un módulo que controla la "pata" física (¿bola?) Del microcircuito: se puede encender, se puede encender o se puede apagar por completo. En la parte derecha de la ventana Quartus, ingresaremos "IOBUF" en el Catálogo de IP e inmediatamente obtendremos un componente llamado ALTIOBUF . Establezcamos un nombre para el archivo de variación, seleccione "Como un búfer bidireccional". Después de eso, un módulo llamado iobuf aparecerá en nuestro proyecto:


 // ... module obuf ( datain, oe, dataout); input [0:0] datain; input [0:0] oe; output [0:0] dataout; wire [0:0] sub_wire0; wire [0:0] dataout = sub_wire0[0:0]; obuf_iobuf_out_d5t obuf_iobuf_out_d5t_component ( .datain (datain), .oe (oe), .dataout (sub_wire0)); endmodule // ... 

Escribamos un módulo blackbox para ello:


 package ip.intel import chisel3._ import chisel3.core.{Analog, BlackBox} import freechips.rocketchip.jtag.Tristate class IOBUF extends BlackBox { val io = IO(new Bundle { val datain = Input(Bool()) val dataout = Output(Bool()) val oe = Input(Bool()) val dataio = Analog(1.W) }) override def desiredName: String = "iobuf" } object IOBUF { def apply(a: Analog, t: Tristate): IOBUF = { val res = Module(new IOBUF) res.io.datain := t.data res.io.oe := t.driven a <> res.io.dataio res } } 

Describimos el Verilogue inout tipo Analog , y el método de nombre desiredName permite cambiar el nombre de la clase del módulo. Esto es especialmente importante ya que generamos carpetas, no implementaciones.


También necesitamos BootROM: para esto creamos una variación de ROM: 1-PORT (palabras de 2048 x 32 bits, solo registro de dirección, crea el puerto rden ). Creamos un bloque con el nombre rom , porque luego tendremos que escribir un adaptador para la interfaz que la clase ROMGenerator : ROMGenerator lugar de me y falta de nosotros (todavía está conectado a 1):


BootROM.v:


 module BootROM( input wire [10:0] address, input wire clock, input wire me, input wire oe, output wire [31:0] q ); rom r( .address(address), .clock(clock), .rden(me), .q(q) ); endmodule 

Un problema más se descubre de inmediato: los archivos hexadecimales generados por el recopilador, por alguna razón, resultan ser incompatibles con Quartus. Después de buscar un poco en Google sobre el tema de los archivos Intel HEX (Intel fue mucho antes de la compra de este Intel Altera, según tengo entendido), llegamos a un comando que convierte los archivos binarios a HEX:


 srec_cat -Output builds/zeowaa-e115/xip.hex -Intel builds/zeowaa-e115/xip.bin -Binary -Output_Block_Size 128 

Por lo tanto, nuestro Makefile se transformará un poco:


Texto oculto
 base_dir := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) BUILD_DIR := $(base_dir)/builds/zeowaa-e115 FPGA_DIR := $(base_dir)/fpga-shells/intel MODEL := FPGAChip PROJECT := sifive.freedom.zeowaa.e115 export CONFIG_PROJECT := sifive.freedom.zeowaa.e115 export CONFIG := ZeowaaConfig export BOARD := zeowaa export BOOTROM_DIR := $(base_dir)/bootrom/xip rocketchip_dir := $(base_dir)/rocket-chip sifiveblocks_dir := $(base_dir)/sifive-blocks all: verilog $(MAKE) -C $(BOOTROM_DIR) clean romgen || true srec_cat -Output $(BUILD_DIR)/xip.hex -Intel $(BUILD_DIR)/xip.bin -Binary -Output_Block_Size 128 include common.mk 

Se mueve ^ W se sintetiza


Entonces, el proyecto en su conjunto se sintetiza, ahora lo más interesante es la depuración. Al gato primero se le permite entrar a la casa, y quizás el JTAG al microcontrolador. Creemos un sistema casi mínimo para la depuración: BootROM para arrancar, GPIO para parpadear LED y JTAG para comprender por qué nada se carga y no parpadea. Por analogía con E300ArtyDevKit cree un paquete con cuatro archivos. En primer lugar


Config.scala:


 class DefaultZeowaaConfig extends Config ( new WithNBreakpoints(2) ++ new WithNExtTopInterrupts(0) ++ new WithJtagDTM ++ new TinyConfig ) class Peripherals extends Config((site, here, up) => { case PeripheryGPIOKey => List( GPIOParams(address = BigInt(0x64002000L), width = 6) ) case PeripheryMaskROMKey => List( MaskROMParams(address = 0x10000, name = "BootROM")) }) class ZeowaaConfig extends Config( new Peripherals ++ new DefaultZeowaaConfig().alter((site, here, up) => { case JtagDTMKey => new JtagDTMConfig ( idcodeVersion = 2, idcodePartNum = 0xe31, idcodeManufId = 0x489, debugIdleCycles = 5) }) ) 

La descripción es, en su mayor parte, copiada y truncada del E300: preguntamos en qué consiste nuestro núcleo y dónde se ubicará en el espacio de direcciones. Tenga en cuenta que aunque no tenemos RAM (esta es la TinyConfig predeterminada en TinyConfig ), ¡además hay un espacio de direcciones de 32 bits!


También hay un archivo que lleva una cierta cantidad de repetitivo.
System.scala:


 class System(implicit p: Parameters) extends RocketSubsystem with HasPeripheryMaskROMSlave with HasPeripheryDebug with HasPeripheryGPIO { override lazy val module = new SystemModule(this) } class SystemModule[+L <: System](_outer: L) extends RocketSubsystemModuleImp(_outer) with HasPeripheryDebugModuleImp with HasPeripheryGPIOModuleImp { // Reset vector is set to the location of the mask rom val maskROMParams = p(PeripheryMaskROMKey) global_reset_vector := maskROMParams(0).address.U } 

En realidad, el diseño de nuestra "placa base" está en tres (hasta ahora) archivos simples:
Platform.scala:


 class PlatformIO(implicit val p: Parameters) extends Bundle { val jtag = Flipped(new JTAGIO(hasTRSTn = false)) val jtag_reset = Input(Bool()) val gpio = new GPIOPins(() => new BasePin(), p(PeripheryGPIOKey)(0)) } class Platform(implicit p: Parameters) extends Module { val sys = Module(LazyModule(new System).module) override val io = IO(new PlatformIO) val sjtag = sys.debug.systemjtag.get sjtag.reset := io.jtag_reset sjtag.mfr_id := p(JtagDTMKey).idcodeManufId.U(11.W) sjtag.jtag <> io.jtag io.gpio <> sys.gpio.head } 

FPGAChip.scala:


 class FPGAChip(override implicit val p: Parameters) extends ZeowaaShell { withClockAndReset(cpu_clock, cpu_rst) { val dut = Module(new Platform) dut.io.jtag.TCK := jtag_tck dut.io.jtag.TDI := jtag_tdi IOBUF(jtag_tdo, dut.io.jtag.TDO) dut.io.jtag.TMS := jtag_tms dut.io.jtag_reset := jtag_rst Seq(led_0, led_1, led_2, led_3) zip dut.io.gpio.pins foreach { case (led, pin) => led := Mux(pin.o.oe, pin.o.oval, false.B) } dut.io.gpio.pins.foreach(_.i.ival := false.B) dut.io.gpio.pins(4).i.ival := key1 dut.io.gpio.pins(5).i.ival := key2 } } 

Como puede ver, puede escribir generadores en Cincel en un estilo funcional (aquí se muestra un caso muy simple). Pero puedes escribir y claramente cada cable:


ZeowaaShell.scala
 object ZeowaaShell { class MemIf extends Bundle { val mem_addr = IO(Analog(14.W)) val mem_ba = IO(Analog(3.W)) val mem_cas_n = IO(Analog(1.W)) val mem_cke = IO(Analog(2.W)) val mem_clk = IO(Analog(2.W)) val mem_clk_n = IO(Analog(2.W)) val mem_cs_n = IO(Analog(2.W)) val mem_dm = IO(Analog(8.W)) val mem_dq = IO(Analog(64.W)) val mem_dqs = IO(Analog(8.W)) val mem_odt = IO(Analog(2.W)) val mem_ras_n = IO(Analog(1.W)) val mem_we_n = IO(Analog(1.W)) } } class ZeowaaShell(implicit val p: Parameters) extends RawModule { val clk25 = IO(Input(Clock())) val clk27 = IO(Input(Clock())) val clk48 = IO(Input(Clock())) val key1 = IO(Input(Bool())) val key2 = IO(Input(Bool())) val key3 = IO(Input(Bool())) val led_0 = IO(Output(Bool())) val led_1 = IO(Output(Bool())) val led_2 = IO(Output(Bool())) val led_3 = IO(Output(Bool())) val jtag_tdi = IO(Input(Bool())) val jtag_tdo = IO(Analog(1.W)) val jtag_tck = IO(Input(Clock())) val jtag_tms = IO(Input(Bool())) val uart_rx = IO(Input(Bool())) val uart_tx = IO(Analog(1.W)) // Internal wiring val cpu_clock = Wire(Clock()) val cpu_rst = Wire(Bool()) val jtag_rst = Wire(Bool()) withClockAndReset(cpu_clock, false.B) { val counter = Reg(UInt(64.W)) counter := counter + 1.U cpu_rst := (counter > 1000.U) && (counter < 2000.U) jtag_rst := (counter > 3000.U) && (counter < 4000.U) } cpu_clock <> clk25 } 

¿Por qué se divide en tres archivos? Bueno, en primer lugar, fue así en el prototipo :) La lógica de separar Shell de FPGAChip , aparentemente, fue que Shell es una descripción de la interfaz con el mundo exterior: qué conclusiones tenemos en un tablero específico (y cómo se mostrarán en ¡conclusiones de chip!), y FPGAChip está dictado por el hecho de que queremos meter en un SoC específico. Bueno, la plataforma está separada de manera bastante lógica: nota: ZeowaaShell (y, por lo tanto, Platform ) es un RawModule , en particular, no tienen un clock implícito y no se reset ; esto es natural para "conectar la placa", pero es inconveniente para el trabajo (y, probablemente, lleno de errores difíciles con dominios de frecuencia prolíficos). Bueno, Platform ya es un módulo Chisel ordinario, en el que puede describir registros de forma segura, etc.


Jtag


Algunas palabras sobre cómo configurar JTAG. Como ya tenía el RaspberryPi 3 Modelo B +, la solución obvia era intentar usar de alguna manera su GPIO. Afortunadamente, todo ya se ha implementado antes que nosotros : en el nuevo OpenOCD hay una descripción de la interface/sysfsgpio-raspberrypi.cfg , con la que puede decirle al depurador que se conecte a través del bloque (TCK = 11, TMS = 25, TDI = 10, TDO = 9 y GND dejar como ejercicio) - pinout aquí .


Además, tomando Freedom.cfg como base del repositorio riscv-tests , obtuve lo siguiente:


 adapter_khz 10000 source [find interface/sysfsgpio-raspberrypi.cfg] set _CHIPNAME riscv jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x20e31913 set _TARGETNAME $_CHIPNAME.cpu target create $_TARGETNAME riscv -chain-position $_TARGETNAME -rtos riscv init halt echo "Ready for Remote Connections" 

Para trabajar, necesitará el puerto riscv-openocd construido bajo ARM, por lo tanto el obsequio no pasó en lugar de la versión preensamblada de SiFive, debe clonar el repositorio y recopilar:


 ./configure --enable-remote-bitbang --enable-sysfsgpio 

Si alguien sabe cómo ejecutar bitbang remoto, es posible que no necesite crear un puerto personalizado para ARM ...


Como resultado, lanzamos desde la raíz en Malinka


 root@ubuntu:~/riscv-openocd# ./src/openocd -s tcl -f ../riscv.tcl Open On-Chip Debugger 0.10.0+dev-00614-g998fed1fe-dirty (2019-06-03-10:27) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html adapter speed: 10000 kHz SysfsGPIO nums: tck = 11, tms = 25, tdi = 10, tdo = 9 SysfsGPIO nums: swclk = 11, swdio = 25 Info : auto-selecting first available session transport "jtag". To override use 'transport select <transport>'. Info : SysfsGPIO JTAG/SWD bitbang driver Info : JTAG and SWD modes enabled Warn : gpio 11 is already exported Warn : gpio 25 is already exported Info : This adapter doesn't support configurable speed Info : JTAG tap: riscv.cpu tap/device found: 0x20e31913 (mfg: 0x489 (SiFive, Inc.), part: 0x0e31, ver: 0x2) Info : datacount=1 progbufsize=16 Info : Disabling abstract command reads from CSRs. Info : Examined RISC-V core; found 1 harts Info : hart 0: XLEN=32, misa=0x40001105 Info : Listening on port 3333 for gdb connections Ready for Remote Connections Info : Listening on port 6666 for tcl connections Info : Listening on port 4444 for telnet connections 

Después de iniciar sesión allí en SSH con el reenvío de puertos 3333, sustituyendo la IP deseada:


 ssh -L 3333:127.0.0.1:3333 ubuntu@192.168.1.104 

Ahora en el host puede ejecutar GDB bajo la arquitectura riscv32 :


 $ ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q (gdb) target remote :3333 Remote debugging using :3333 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x00000000 in ?? () 

Omitiremos algunas depuraciones a ciegas del JTAG antiadherente debido al hecho de que la macro SYNTHESIS se usa activamente en el archivo generado principal y rebobinará la situación al estado "JTAG está enganchado, pero las luces no parpadean".


Primer código


Como vimos en el Makefile, el código para bootrom se toma del bootrom/xip/xip.S Ahora se ve así:


 // See LICENSE for license details. // Execute in place // Jump directly to XIP_TARGET_ADDR .section .text.init .option norvc .globl _start _start: csrr a0, mhartid la a1, dtb li t0, XIP_TARGET_ADDR jr t0 .section .rodata dtb: .incbin DEVICE_TREE 

Entiendo que debe haber un árbol de dispositivos (el contenido del archivo dtb) para que el sistema operativo lo lea, pero qué tipo de sistema operativo existe sin RAM. Por lo tanto, audazmente reemplácelo temporalmente con bombillas intermitentes:


Texto oculto
  .section .text.init .option norvc .globl _start _start: li a5, 0x64002000 li a1, 0x0F li a2, 0x01 li a3, 0x30 li a6, 0x10 li a7, 0x20 sw zero, 0x38(a5) // iof_en sw a1, 0x08(a5) // output_en sw a2, 0x00(a5) // value sw a1, 0x14(a5) // drive sw a3, 0x04(a5) // input_en // a0 <- timer // a1 <- 0x0F // a2 <- [state] // a3 <- 0x30 // a4 <- [buttons] // a5 <- [gpio addr] // a6 <- 0x10 // a7 <- 0x20 loop: li a4, 0x1000 add a0, a0, a4 bgtu a0, zero, loop lw a4, 0x00(a5) // value beq a4, a6, plus beq a4, a7, minus j store plus: srai a2, a2, 1 beq a2, zero, pzero j store pzero: li a2, 0x08 j store minus: slli a2, a2, 1 beq a2, a6, mzero j store mzero: li a2, 0x01 store: sw a2, 0x0c(a5) // value j loop 

Este código se desarrolló de forma iterativa, así que discúlpeme por la extraña numeración de los registros. Además, honestamente estudié el ensamblador RISC-V usando el método "compilar un fragmento de código en un archivo objeto, desmontar, ver". Cuando leí un libro de electrónica hace unos años, hablaba de programar ATTiny en lenguaje ensamblador. "Aquí están las cosas aburridas y la rutina, probablemente", pensé, pero ahora, aparentemente, ha aparecido el efecto de una tienda sueca : taquilla procesador ensamblado a partir de piezas de repuesto, incluso el ensamblador parece ser nativo e interesante. Como resultado de ejecutar este código, el LED encendido debe "funcionar" hacia la izquierda o hacia la derecha, según el botón que se presione.


Comenzamos ... Y nada: todas las luces están encendidas, no responden a los botones. Vamos a conectarnos a través de JTAG: contador de programa = 0x00000000, de alguna manera todo es triste. Pero en 0x64002000 tenemos registros GPIO disponibles:


GPIOCtrlRegs.scala
 // See LICENSE for license details. package sifive.blocks.devices.gpio object GPIOCtrlRegs { val value = 0x00 val input_en = 0x04 val output_en = 0x08 val port = 0x0c val pullup_en = 0x10 val drive = 0x14 val rise_ie = 0x18 val rise_ip = 0x1c val fall_ie = 0x20 val fall_ip = 0x24 val high_ie = 0x28 val high_ip = 0x2c val low_ie = 0x30 val low_ip = 0x34 val iof_en = 0x38 val iof_sel = 0x3c val out_xor = 0x40 } 

Intentemos moverlos manualmente:


 (gdb) set variable *0x64002038=0 (gdb) set variable *0x64002008=0xF (gdb) set variable *0x64002000=0x1 (gdb) set variable *0x64002014=0xF (gdb) set variable *0x6400200c=0x1 

Entonces ... Uno de los LED se apagó ... Y si no 0x1 , sino 0x5 ... Así es, ahora los LED están encendidos a través de uno. También quedó claro que deben invertirse, pero no es necesario que escriba en el registro 0x00; debe leer desde allí.


 (gdb) x/x 0x64002000 0x64002000: 0x00000030 //    (gdb) x/x 0x64002000 0x64002000: 0x00000020 //   (gdb) x/x 0x64002000 0x64002000: 0x00000010 //   (gdb) x/x 0x64002000 0x64002000: 0x00000000 

Genial, los registros mapeados en memoria se actualizan sin iniciar el procesador, no puede presionar cont + Ctrl-C cada vez, un poco, pero agradable.


Pero, ¿por qué no estamos girando en un bucle, sino que estamos en $pc=0x0000000 ?


 (gdb) x/10i 0x10000 0x10000: addi s1,sp,12 0x10002: fsd ft0,-242(ra) 0x10006: srli a4,a4,0x21 0x10008: addi s0,sp,32 0x1000a: slli t1,t1,0x21 0x1000c: lb zero,-1744(a2) 0x10010: nop 0x10012: addi a0,sp,416 0x10014: c.slli zero,0x0 0x10016: 0x9308 

¿Qué es el Pokémon? No escribí tales instrucciones, ¡me tiraron! Echemos un vistazo más de cerca:


 (gdb) x/10x 0x10000 0x10000: 0xb7270064 0x9305f000 0x13061000 0x93060003 0x10010: 0x13080001 0x93080002 0x23ac0702 0x23a4b700 0x10020: 0x23a0c700 0x23aab700 

Por otro lado, ¿qué debería estar allí?


 $ ../../rocket-tools/bin/riscv32-unknown-elf-objdump -d builds/zeowaa-e115/xip.elf builds/zeowaa-e115/xip.elf:   elf32-littleriscv   .text: 00010054 <_start>: 10054: 640027b7 lui a5,0x64002 10058: 00f00593 li a1,15 1005c: 00100613 li a2,1 10060: 03000693 li a3,48 10064: 01000813 li a6,16 10068: 02000893 li a7,32 1006c: 0207ac23 sw zero,56(a5) # 64002038 <__global_pointer$+0x63ff0770> 10070: 00b7a423 sw a1,8(a5) 10074: 00c7a023 sw a2,0(a5) 10078: 00b7aa23 sw a1,20(a5) 1007c: 00d7a223 sw a3,4(a5) ... 

Como puede ver, Quartus sinceramente puso las mismas palabras que estaban en el archivo de inicialización, pero cambió su endianness. Puedes buscar en Google durante mucho tiempo cómo resolverlo, pero soy un programador , las muletas son nuestras, así que simplemente reescribiré


BootROM.v
 module BootROM( input wire [10:0] address, input wire clock, input wire me, input wire oe, output wire [31:0] q ); wire [31:0] q_r; rom r( .address(address), .clock(clock), .rden(me), .q(q_r) ); assign q[31:24] = q_r[7:0]; assign q[23:16] = q_r[15:8]; assign q[15:8] = q_r[23:16]; assign q[7:0] = q_r[31:24]; endmodule 

Entonces, recolectamos, comenzamos, no brilla. JTAG: $pc - , = 4, , output_en : set variable *0x64002008=0xF , c (continue) — ! , . -, , … , output_en .


:


 Total logic elements 17,370 / 114,480 ( 15 % ) Total registers 8357 Total pins 16 / 281 ( 6 % ) Total virtual pins 0 Total memory bits 264,000 / 3,981,312 ( 7 % ) Embedded Multiplier 9-bit elements 4 / 532 ( < 1 % ) 

Chisel


...

Source: https://habr.com/ru/post/455391/


All Articles