En la parte anterior, ensamblamos un microcontrolador sin RAM basado en el FPGA Altera / Intel. Sin embargo, la placa tiene un conector con SO-DIMM DDR2 1Gb instalado, que, obviamente, quiero usar. Para hacer esto, necesitamos envolver el controlador DDR2 con la interfaz ALTMEMPHY
en un módulo que sea comprensible para el protocolo de memoria TileLink utilizado en RocketChip. Bajo el corte: depuración táctil, programación de fuerza bruta y RAKE.
Como saben, Computer Science tiene dos problemas principales: invalidación de caché y nombres variables. En KDPV, ves un momento raro: los dos problemas principales que CS se encontró y están tramando algo .
DESCARGO DE RESPONSABILIDAD: además de la advertencia del artículo anterior, le recomiendo que lea el artículo hasta el final antes de repetir los experimentos, para evitar daños en el FPGA, el módulo de memoria o los circuitos de alimentación.
Esta vez quería, si no arrancar Linux, al menos conectar la RAM, que en mi placa ya tiene un gigabyte completo (o puede poner hasta cuatro). Se propone un criterio de éxito para considerar la capacidad de leer y escribir a través de un montón de GDB + OpenOCD, incluidas las direcciones que no están alineadas por 16 bytes (el ancho de una solicitud a la memoria). A primera vista, solo necesita arreglar un poco la configuración, el generador de SoC no puede soportar RAM fuera de la caja. Lo admite, pero a través de la interfaz MIG (bueno, y, posiblemente, alguna otra interfaz de Microsemi). A través de la interfaz estándar, AXI4 también lo admite, pero, según tengo entendido, no es tan fácil obtenerlo (al menos, no dominar Platform Designer).
Digresión de letras: hasta donde yo entiendo, hay una serie bastante popular de interfaces AXI "intra-chip" desarrolladas por ARM. Aquí uno pensaría que todo está patentado y cerrado. Pero después de registrarme (sin ningún "programa universitario" y cualquier otra cosa, solo por correo electrónico y completar el cuestionario) y obtener acceso a la especificación, me sorprendió gratamente. Por supuesto, no soy un abogado, pero parece que el estándar es bastante abierto: tienes que usar núcleos con licencia de ARM, o no afirmas ser compatible con ARM, y todo parece estar bien . Pero en general, por supuesto, lea la licencia, lea con abogados, etc.
Monkey and TileLink (fábula)
La tarea parecía bastante simple, y abrí la descripción de la ddr2_64bit
módulo ddr2_64bit
ya disponible en el proyecto del proveedor:
Propiedad de Intel y en general module ddr2_64bit ( local_address, local_write_req, local_read_req, local_burstbegin, local_wdata, local_be, local_size, global_reset_n, pll_ref_clk, soft_reset_n, local_ready, local_rdata, local_rdata_valid, local_refresh_ack, local_init_done, reset_phy_clk_n, mem_odt, mem_cs_n, mem_cke, mem_addr, mem_ba, mem_ras_n, mem_cas_n, mem_we_n, mem_dm, phy_clk, aux_full_rate_clk, aux_half_rate_clk, reset_request_n, mem_clk, mem_clk_n, mem_dq, mem_dqs); input [25:0] local_address; input local_write_req; input local_read_req; input local_burstbegin; input [127:0] local_wdata; input [15:0] local_be; input [2:0] local_size; input global_reset_n; input pll_ref_clk; input soft_reset_n; output local_ready; output [127:0] local_rdata; output local_rdata_valid; output local_refresh_ack; output local_init_done; output reset_phy_clk_n; output [1:0] mem_odt; output [1:0] mem_cs_n; output [1:0] mem_cke; output [13:0] mem_addr; output [1:0] mem_ba; output mem_ras_n; output mem_cas_n; output mem_we_n; output [7:0] mem_dm; output phy_clk; output aux_full_rate_clk; output aux_half_rate_clk; output reset_request_n; inout [1:0] mem_clk; inout [1:0] mem_clk_n; inout [63:0] mem_dq; inout [7:0] mem_dqs; ...
La sabiduría popular dice: "Cualquier documentación en ruso debe comenzar con las palabras:" Entonces, no funciona "". Pero la interfaz aquí no es del todo intuitiva , por lo que aún la leemos . En la descripción, se nos dice de inmediato que trabajar con DDR2 no es una tarea fácil. Necesita configurar PLL, realizar alguna calibración, crack-fex-pex , la señal local_init_done
está local_init_done
, puede trabajar. En general, la lógica de nomenclatura aquí es aproximadamente la siguiente: los nombres con prefijos local_
son la interfaz de "usuario", los puertos mem_
enviarse directamente a las patas conectadas al módulo de memoria, pll_ref_clk
necesita que se le envíe una señal de reloj con la frecuencia especificada al configurar el módulo; el resto se recibirá de él frecuencias, bueno, todo tipo de entradas y salidas de reinicio y salidas de frecuencia, con las que la interfaz de usuario debería trabajar sincrónicamente.
ddr2_64bit
una descripción de las señales externas a la memoria y la interfaz del módulo ddr2_64bit
:
rasgo memif trait MemIf { val local_init_done = Output(Bool()) val global_reset_n = Input(Bool()) val pll_ref_clk = Input(Clock()) val soft_reset_n = Input(Bool()) val reset_phy_clk_n = Output(Clock()) val mem_odt = Output(UInt(2.W)) val mem_cs_n = Output(UInt(2.W)) val mem_cke = Output(UInt(2.W)) val mem_addr = Output(UInt(14.W)) val mem_ba = Output(UInt(2.W)) val mem_ras_n = Output(UInt(1.W)) val mem_cas_n = Output(UInt(1.W)) val mem_we_n = Output(UInt(1.W)) val mem_dm = Output(UInt(8.W)) val phy_clk = Output(Clock()) val aux_full_rate_clk = Output(Clock()) val aux_half_rate_clk = Output(Clock()) val reset_request_n = Output(Bool()) val mem_clk = Analog(2.W) val mem_clk_n = Analog(2.W) val mem_dq = Analog(64.W) val mem_dqs = Analog(8.W) def connectFrom(mem_if: MemIf): Unit = { local_init_done := mem_if.local_init_done mem_if.global_reset_n := global_reset_n mem_if.pll_ref_clk := pll_ref_clk mem_if.soft_reset_n := soft_reset_n reset_phy_clk_n := mem_if.reset_phy_clk_n mem_odt <> mem_if.mem_odt mem_cs_n <> mem_if.mem_cs_n mem_cke <> mem_if.mem_cke mem_addr <> mem_if.mem_addr mem_ba <> mem_if.mem_ba mem_ras_n <> mem_if.mem_ras_n mem_cas_n <> mem_if.mem_cas_n mem_we_n <> mem_if.mem_we_n mem_dm <> mem_if.mem_dm mem_clk <> mem_if.mem_clk mem_clk_n <> mem_if.mem_clk_n mem_dq <> mem_if.mem_dq mem_dqs <> mem_if.mem_dqs phy_clk := mem_if.phy_clk aux_full_rate_clk := mem_if.aux_full_rate_clk aux_half_rate_clk := mem_if.aux_half_rate_clk reset_request_n := mem_if.reset_request_n } } class MemIfBundle extends Bundle with MemIf
clase dd2_64bit class ddr2_64bit extends BlackBox { override val io = IO(new MemIfBundle { val local_address = Input(UInt(26.W)) val local_write_req = Input(Bool()) val local_read_req = Input(Bool()) val local_burstbegin = Input(Bool()) val local_wdata = Input(UInt(128.W)) val local_be = Input(UInt(16.W)) val local_size = Input(UInt(3.W)) val local_ready = Output(Bool()) val local_rdata = Output(UInt(128.W)) val local_rdata_valid = Output(Bool()) val local_refresh_ack = Output(Bool()) }) }
Aquí me esperaba el primer grupo de rastrillos: en primer lugar, ROMGenerator
la clase ROMGenerator
, pensé que el controlador de memoria podría extraerse de las profundidades del diseño a través de una variable global, y Chisel de alguna manera adelantaría los cables. No funcionó. Por lo tanto, tuve que hacer un arnés de cableado MemIfBundle
que MemIfBundle
toda la jerarquía. ¿Por qué no sobresale de BlackBox
y no se conecta de una vez? El hecho es que con BlackBox
todos los puertos externos se introducen en val io = IO(new Bundle { ... })
. Si todo el MemIfBundle
convierte en una variable en el paquete, el nombre de esta variable se convertirá en un prefijo para los nombres de todos los puertos, y los nombres no coincidirán curiosamente con la interfaz del bloque. Probablemente, se puede hacer de alguna manera más adecuada , pero por ahora vamos a dejarlo así.
Además, por analogía con otros dispositivos TileLink (que viven principalmente en rocket-chip/src/main/scala/tilelink
), y especialmente BootROM
, describiremos nuestra interfaz con el controlador de memoria:
class AltmemphyDDR2RAM(implicit p: Parameters) extends LazyModule { val MemoryPortParams(MasterPortParams(base, size, beatBytes, _, _, executable), 1) = p(ExtMem).get val node = TLManagerNode(Seq(TLManagerPortParameters( Seq(TLManagerParameters( address = AddressSet.misaligned(base, size), resources = new SimpleDevice("ram", Seq("sifive,altmemphy0")).reg("mem"), regionType = RegionType.UNCACHED, executable = executable, supportsGet = TransferSizes(1, 16), supportsPutFull = TransferSizes(1, 16), fifoId = Some(0) )), beatBytes = 16 ))) override lazy val module = new AltmemphyDDR2RAMImp(this) } class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) val mem_if = IO(new MemIfBundle)
ExtMem
clave estándar ExtMem
extraemos los parámetros de la memoria externa de la ExtMem
SoC ( esta extraña sintaxis me permite decir "Sé que devolverán una instancia de la clase de caso MemoryPortParameters
(esto está garantizado por el tipo de clave en la etapa de compilación del código Scala, por la condición que no Option[MemoryPortParams]
en tiempo de ejecución al tomar contenidos de la Option[MemoryPortParams]
igual a None
, pero no había nada para crear un controlador de memoria en System.scala
...), por lo tanto, no necesito la clase de caso, y algunos de sus campos son necesarios "). A continuación, creamos el puerto del administrador del dispositivo TileLink (el protocolo TileLink garantiza la interacción de casi todo lo relacionado con la memoria: el controlador DDR y otros dispositivos mapeados en memoria, cachés de procesador, quizás algo más, cada dispositivo puede tener varios puertos, cada uno el dispositivo puede ser administrador y cliente). beatBytes
, según tengo entendido, establece el tamaño de una transacción, y tenemos 16 bytes intercambiados con el controlador. HasAltmemphyDDR2
y HasAltmemphyDDR2Imp
en los lugares correctos en System.scala
, escribimos la configuración
class BigZeowaaConfig extends Config ( new WithNBreakpoints(2) ++ new WithNExtTopInterrupts(0) ++ new WithExtMemSize(1l << 30) ++ new WithNMemoryChannels(1) ++ new WithCacheBlockBytes(16) ++ new WithNBigCores(1) ++ new WithJtagDTM ++ new BaseConfig )
Después de hacer un "boceto de un búho" en AltmemphyDDR2RAMImp
, sinteticé el diseño (algo solo a ~ 30MHz, es bueno que registre desde 25MHz) y, poniendo mis dedos en los módulos de memoria y el chip FPGA, lo cargué en la placa. Entonces vi lo que es una interfaz intuitiva real : esto es cuando das un comando en gdb para escribir en la memoria, y por un procesador congelado y quemado dedos sintiendo un fuerte calor, debe presionar con urgencia el botón de reinicio en el tablero y arreglar el controlador.
Lea la documentación para el controlador DDR2
Aparentemente, es hora de leer la documentación en el controlador más allá de la lista de puertos. Entonces, ¿qué tenemos aquí? ... Vaya, resulta que las E / local_
con el prefijo pll_ref_clk
no deben establecerse sincrónicamente con pll_ref_clk
, que es de 25MHz, sino con phy_clk
que phy_clk
mitad de la frecuencia de memoria para el controlador de media velocidad o, en nuestro caso, aux_half_rate_clk
(tal vez después de todo aux_full_rate_clk
?), que aux_full_rate_clk
la frecuencia de memoria completa y, por un minuto, es 166MHz.
Por lo tanto, es necesario cruzar los límites de los dominios de frecuencia. Según la memoria anterior, decidí usar pestillos, o más bien una cadena de ellos:
+-+ +-+ +-+ +-+ --| |--| |--| |--| |---> +-+ +-+ +-+ +-+ | | | | ---+ | | | inclk | | | | | | --------+----+ | outclk | | ------------------+ output enable
Pero, después de jugar con la hora, llegué a la conclusión de que no podía manejar dos colas en los pestillos "escalares" (en el dominio de alta frecuencia y viceversa), cada uno de los cuales tendrá señales antidireccionales ( ready
y valid
), y aun así, para asegurarme de que algunos beatik no van a la zaga de un ritmo o dos a lo largo del camino. Después de un tiempo, me di cuenta de que describir la sincronización en ready
( valid
sin una señal de reloj común) también es una tarea similar a la creación de estructuras de datos sin bloqueo en el sentido de que necesitas pensar y demostrar formalmente mucho, es fácil cometer un error, es difícil de notar y, lo más importante, todo ya está implementado antes que nosotros: Intel tiene una primitiva dcfifo
, que es una cola de longitud y ancho configurables, que se lee y escribe desde diferentes dominios de frecuencia. Como resultado, aproveché la oportunidad experimental de Cincel fresco, a saber, las cajas negras parametrizadas:
class FIFO (val width: Int, lglength: Int) extends BlackBox(Map( "intended_device_family" -> StringParam("Cyclone IV E"), "lpm_showahead" -> StringParam("OFF"), "lpm_type" -> StringParam("dcfifo"), "lpm_widthu" -> IntParam(lglength), "overflow_checking" -> StringParam("ON"), "rdsync_delaypipe" -> IntParam(5), "underflow_checking" -> StringParam("ON"), "use_eab" -> StringParam("ON"), "wrsync_delaypipe" -> IntParam(5), "lpm_width" -> IntParam(width), "lpm_numwords" -> IntParam(1 << lglength) )) { override val io = IO(new Bundle { val data = Input(UInt(width.W)) val rdclk = Input(Clock()) val rdreq = Input(Bool()) val wrclk = Input(Clock()) val wrreq = Input(Bool()) val q = Output(UInt(width.W)) val rdempty = Output(Bool()) val wrfull = Output(Bool()) }) override def desiredName: String = "dcfifo" }
Y escribió un pequeño binocular simple de tipos de datos arbitrarios:
object FIFO { def apply[T <: Data]( lglength: Int, output: T, outclk: Clock, input: T, inclk: Clock ): FIFO = { val res = Module(new FIFO(width = output.widthOption.get, lglength = lglength)) require(input.getWidth == res.width) output := res.io.q.asTypeOf(output) res.io.rdclk := outclk res.io.data := input.asUInt() res.io.wrclk := inclk res } }
Depuración
Después de eso, el código se convirtió en la transferencia de mensajes entre dominios a través de dos colas ya unidireccionales: tl_req
/ ddr_req
y ddr_resp
/ tl_resp
(el que tiene el prefijo tl_
se registra junto con TileLink, el hecho de que ddr_
está con el controlador de memoria). El problema es que todo estaba bloqueado de todos modos, y a veces hacía bastante calor. Y si la causa del sobrecalentamiento fue la configuración simultánea de local_read_req
y local_write_req
, entonces no fue tan fácil lidiar con puntos muertos. El código al mismo tiempo era algo así como
class AltmemphyDDR2RAMImp(_outer: AltmemphyDDR2RAM)(implicit p: Parameters) extends LazyModuleImp(_outer) { val addrSize = log2Ceil(_outer.size / 16) val (in, edge) = _outer.node.in(0) val ddr2 = Module(new ddr2_64bit) require(ddr2.io.local_address.getWidth == addrSize) val tl_clock = clock val ddr_clock = ddr2.io.aux_full_rate_clk val mem_if = IO(new MemIfBundle) class DdrRequest extends Bundle { val size = UInt(in.a.bits.size.widthOption.get.W) val source = UInt(in.a.bits.source.widthOption.get.W) val address = UInt(addrSize.W) val be = UInt(16.W) val wdata = UInt(128.W) val is_reading = Bool() } val tl_req = Wire(new DdrRequest) val ddr_req = Wire(new DdrRequest) val fifo_req = FIFO(2, ddr_req, ddr_clock, tl_req, clock) class DdrResponce extends Bundle { val is_reading = Bool() val size = UInt(in.d.bits.size.widthOption.get.W) val source = UInt(in.d.bits.source.widthOption.get.W) val rdata = UInt(128.W) } val tl_resp = Wire(new DdrResponce) val ddr_resp = Wire(new DdrResponce) val fifo_resp = FIFO(2, tl_resp, clock, ddr_resp, ddr_clock)
Para localizar el problema, decidí comentar banalmente todo el código dentro withClock(ddr_clock)
(no lo hace, visualmente parece crear una secuencia) y reemplazarlo con un trozo que funciona con seguridad:
withClock (ddr_clock) { ddr_resp.rdata := 0.U ddr_resp.is_reading := ddr_req.is_reading ddr_resp.size := ddr_req.size ddr_resp.source := ddr_req.source val will_read = Wire(!fifo_req.io.rdempty && !fifo_resp.io.wrfull) fifo_req.io.rdreq := will_read fifo_resp.io.wrreq := RegNext(will_read) }
Como más tarde me di cuenta, este trozo tampoco funcionó porque la construcción Wire(...)
, que agregué "por confiabilidad" para mostrar que era un cable con nombre, en realidad utilizó el argumento solo como un prototipo para crear tipo de significado, pero no lo vinculó al argumento de expresión . Además, cuando intenté leer lo que aún se generaba, me di cuenta de que en el modo de simulación hay una amplia selección de afirmaciones sobre el incumplimiento del protocolo TileLink. Probablemente me serán útiles más tarde, pero hasta ahora no ha habido ningún intento de ejecutar la simulación, ¿por qué comenzarla? Verilator probablemente no conoce los núcleos IP de Alter, ModelSim Starter Edition probablemente se negará a simular un proyecto tan grande, pero también juré por la falta de un modelo de controlador para la simulación. Y para generarlo, es probable que primero deba cambiar a la nueva versión del controlador (porque la anterior se configuró en el antiguo Quartus).
De hecho, los bloques de código se tomaron de una versión casi funcional, y no de la que se depuró activamente unas horas antes. Pero es mejor;) Por cierto, puede reensamblar constantemente el diseño más rápido si la WithNBigCores(1)
se reemplaza por WithNSmallCores(1)
, desde el punto de vista de la funcionalidad básica del controlador de memoria, parece que no hay diferencia. Y un pequeño truco: para no conducir los mismos comandos a gdb cada vez (al menos no tengo un historial de comandos entre sesiones allí), simplemente puede escribir algo así en la línea de comandos
../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "x/x 0x80000000" ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set variable *0x80000000=0x1234"
y ejecutar según sea necesario utilizando medios regulares del shell.
Resumen
Como resultado, se obtuvo el siguiente código para trabajar con el controlador:
withClock(ddr_clock) { val rreq = RegInit(false.B)
Aquí todavía cambiaremos ligeramente el criterio de finalización: ya he visto cómo, sin ningún trabajo con la memoria, los datos grabados son como leídos, porque es un caché. Por lo tanto, compilamos un código simple:
#include <stdint.h> static volatile uint8_t *x = (uint8_t *)0x80000000u; void entry() { for (int i = 0; i < 1<<24; ++i) { x[i] = i; } }
../../rocket-tools/bin/riscv64-unknown-elf-gcc test.c -S -O1
Como resultado, obtenemos el siguiente fragmento de listado de ensamblador, inicializando los primeros 16 MB de memoria:
li a5,1 slli a5,a5,31 li a3,129 slli a3,a3,24 .L2: andi a4,a5,0xff sb a4,0(a5) addi a5,a5,1 bne a5,a3,.L2
bootrom/xip/leds.S
al comienzo de bootrom/xip/leds.S
Ahora es poco probable que todo pueda mantenerse en un solo caché. Queda por ejecutar el Makefile, reconstruir el proyecto en Quartus, llenarlo en el tablero, conectar OpenOCD + GDB y ... Presumiblemente, aplausos, victoria:
$ ../../rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "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. 0x0000000000010014 in ?? () (gdb) x/x 0x80000000 0x80000000: 0x03020100 (gdb) x/x 0x80000100 0x80000100: 0x03020100 (gdb) x/x 0x80000111 0x80000111: 0x14131211 (gdb) x/x 0x80010110 0x80010110: 0x13121110 (gdb) x/x 0x80010120 0x80010120: 0x23222120
Es así, lo descubriremos en la próxima serie (tampoco puedo decir sobre rendimiento, estabilidad, etc.).
Código: AltmemphyDDR2RAM.scala .