Im vorherigen Teil haben wir einen Mikrocontroller ohne RAM auf Basis des Altera / Intel FPGA zusammengestellt. Die Karte verfügt jedoch über einen Anschluss mit installiertem SO-DIMM DDR2 1 GB, den ich natürlich verwenden möchte. Dazu müssen wir den DDR2-Controller mit der ALTMEMPHY
Schnittstelle in ein Modul ALTMEMPHY
, das für das in RocketChip verwendete TileLink-Speicherprotokoll verständlich ist. Unter dem Cut-Tactile-Debugging, Brute-Force-Programmierung und RAKE.
Wie Sie wissen, hat die Informatik zwei Hauptprobleme: die Ungültigmachung des Caches und die Benennung von Variablen. Bei KDPV sehen Sie einen seltenen Moment - die beiden Hauptprobleme, denen CS begegnet ist und planen etwas .
HAFTUNGSAUSSCHLUSS: Zusätzlich zur Warnung aus dem vorherigen Artikel empfehle ich dringend, den Artikel bis zum Ende zu lesen, bevor Sie die Experimente wiederholen, um Schäden am FPGA, dem Speichermodul oder den Stromkreisen zu vermeiden.
Dieses Mal wollte ich, wenn nicht Linux booten, zumindest den RAM anschließen, der auf meinem Board bereits ein ganzes Gigabyte hat (oder Sie können bis zu vier setzen). Ein Erfolgskriterium wird vorgeschlagen, um die Fähigkeit zum Lesen und Schreiben durch eine Reihe von GDB + OpenOCD zu berücksichtigen, einschließlich Adressen, die nicht durch 16 Bytes ausgerichtet sind (die Breite einer Anforderung an den Speicher). Auf den ersten Blick müssen Sie nur die Konfiguration ein wenig korrigieren, der SoC-Generator kann RAM nicht sofort unterstützen. Es unterstützt es, aber über die MIG-Schnittstelle (naja, und möglicherweise eine andere Schnittstelle von Microsemi). Über die Standardschnittstelle unterstützt AXI4 es ebenfalls, aber meines Wissens ist es nicht so einfach, es zu bekommen (zumindest nicht Platform Designer zu beherrschen).
Lyrischer Exkurs: Soweit ich weiß, gibt es eine recht beliebte Reihe von AXI-Schnittstellen, die von ARM entwickelt wurden. Hier würde man denken, dass alles patentiert und geschlossen ist. Aber nachdem ich mich registriert hatte (ohne „Universitätsprogramme“ und alles andere - nur per E-Mail und Ausfüllen des Fragebogens) und Zugang zur Spezifikation erhalten hatte, war ich angenehm überrascht. Natürlich bin ich kein Anwalt, aber es scheint, dass der Standard ziemlich offen ist: Sie müssen entweder lizenzierte Kernel von ARM verwenden oder überhaupt keine Kompatibilität mit ARM beanspruchen, und dann scheint alles in Ordnung zu sein . Aber im Allgemeinen lesen Sie natürlich die Lizenz, lesen Sie mit Anwälten usw.
Affe und TileLink (Fabel)
Die Aufgabe schien recht einfach zu sein, und ich öffnete die Beschreibung der ddr2_64bit
bereits im Projekt vom Lieferanten erhältlich war:
Intel Eigentum und allgemein 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; ...
Die allgemeine Weisheit sagt: "Jede Dokumentation auf Russisch muss mit den Worten beginnen:" Also, es funktioniert nicht. " Die Benutzeroberfläche hier ist jedoch nicht ganz intuitiv , daher lesen wir sie immer noch . In der Beschreibung wird uns sofort gesagt, dass die Arbeit mit DDR2 keine leichte Aufgabe ist. Sie müssen PLL konfigurieren, eine Kalibrierung durchführen, Crack-Fex-Pex , das Signal local_init_done
ist local_init_done
, Sie können arbeiten. Im Allgemeinen lautet die local_
hier ungefähr wie folgt: Namen mit local_
Präfixen sind die "Benutzer" -Schnittstelle, mem_
Ports mem_
direkt an die mit dem Speichermodul verbundenen Beine ausgegeben werden, pll_ref_clk
muss ein Taktsignal mit der beim Einrichten des Moduls angegebenen Frequenz gesendet werden - der Rest wird von ihm empfangen Frequenzen, na ja, alle Arten von Ein- und Ausgängen werden zurückgesetzt und Frequenzausgänge, mit denen die Benutzeroberfläche synchron arbeiten sollte.
Erstellen wir eine Beschreibung der externen Signale für den Speicher und die Schnittstelle des Moduls ddr2_64bit
:
Merkmal 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
Klasse 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()) }) }
Hier wartete der erste Haufen Rechen auf mich: ROMGenerator
ich mir die ROMGenerator
Klasse angesehen hatte, dachte ich zunächst, dass der Speichercontroller durch eine globale Variable aus den Tiefen des Designs herausgezogen werden könnte, und Chisel würde die Drähte selbst irgendwie weiterleiten. Hat nicht funktioniert. Daher musste ich einen MemIfBundle
Kabelbaum herstellen, MemIfBundle
in der gesamten Hierarchie MemIfBundle
. Warum ragt er nicht aus der BlackBox
und stellt keine BlackBox
Verbindung her? Tatsache ist, dass mit BlackBox
alle externen Ports in val io = IO(new Bundle { ... })
gestopft sind. Wenn das gesamte MemIfBundle
zu einer Variablen im Bundle gemacht wird, wird der Name dieser Variablen als Präfix für die Namen aller Ports festgelegt, und die Namen stimmen nicht mit der Schnittstelle des Blocks überein. Wahrscheinlich kann es irgendwie angemessener gemacht werden , aber lassen wir es jetzt so.
In Analogie zu anderen TileLink-Geräten (hauptsächlich in rocket-chip/src/main/scala/tilelink
) und insbesondere zu BootROM
werden wir außerdem unsere Schnittstelle zum Speichercontroller beschreiben:
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
Standard- ExtMem
Schlüssel extrahieren wir die externen Speicherparameter aus der SoC- ExtMem
( diese seltsame Syntax ermöglicht es mir zu sagen: „Ich weiß, dass sie eine Instanz der MemoryPortParameters
(dies wird durch den Schlüsseltyp in der Phase des Kompilierens des Scala-Codes durch die Bedingung garantiert dass wir nicht in die Laufzeit fallen, indem wir Inhalte aus Option[MemoryPortParams]
gleich None
, aber dann gab es nichts, um einen Speichercontroller in System.scala
zu erstellen ...), daher brauche ich die case-Klasse nicht und einige ihrer Felder werden benötigt "). Als Nächstes erstellen wir den Manager-Port des TileLink-Geräts (das TileLink-Protokoll stellt die Interaktion von fast allem sicher, was mit dem Speicher zu tun hat: dem DDR-Controller und anderen speicherabgebildeten Geräten, Prozessor-Caches, möglicherweise etwas anderem. Jedes Gerät kann mehrere Ports haben Das Gerät kann sowohl Manager als auch Client sein. beatBytes
ich es verstehe, legt beatBytes
die Größe einer Transaktion fest, und wir haben 16 Bytes, die mit dem Controller ausgetauscht werden. HasAltmemphyDDR2
und HasAltmemphyDDR2Imp
wir an den richtigen Stellen in System.scala
, schreiben die Konfiguration
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 )
Nachdem ich in AltmemphyDDR2RAMImp
eine „Skizze einer Eule“ AltmemphyDDR2RAMImp
, synthetisierte ich das Design (etwas nur bei ~ 30 MHz, es ist gut, dass ich von 25 MHz takte) und lud es mit den Fingern auf die Speichermodule und den FPGA-Chip auf die Platine. Dann habe ich gesehen, was eine wirklich intuitive Benutzeroberfläche ist: Dies ist, wenn Sie in gdb einen Befehl zum Schreiben in den Speicher und von einem eingefrorenen Prozessor und geben verbrannt Wenn die Finger eine starke Hitze spüren, müssen Sie dringend die Reset-Taste auf der Platine drücken und den Controller reparieren.
Lesen Sie die Dokumentation zum DDR2-Controller
Anscheinend ist es Zeit, die Dokumentation auf dem Controller über die Liste der Ports hinaus zu lesen. Also, was haben wir hier? .. Ups, es stellt sich heraus, dass die E / A mit dem Präfix local_
nicht synchron gesetzt werden sollten, nicht mit pll_ref_clk
, das 25 MHz ist, sondern entweder mit phy_clk
, phy_clk
Hälfte der Speicherfrequenz für den Controller mit halber Rate phy_clk
, oder in unserem Fall aux_half_rate_clk
(vielleicht doch aux_full_rate_clk
?), das die volle Speicherfrequenz ausgibt und für eine Minute 166 MHz beträgt.
Daher ist es notwendig, die Grenzen von Frequenzbereichen zu überschreiten. Nach alter Erinnerung habe ich mich entschieden, Riegel oder vielmehr eine Kette davon zu verwenden:
+-+ +-+ +-+ +-+ --| |--| |--| |--| |---> +-+ +-+ +-+ +-+ | | | | ---+ | | | inclk | | | | | | --------+----+ | outclk | | ------------------+ output enable
Nachdem ich an der Stunde herumgebastelt hatte, kam ich zu dem Schluss, dass ich nicht zwei Warteschlangen auf den "skalaren" Latches (im Hochfrequenzbereich und umgekehrt) verarbeiten konnte, von denen jedes antidirektionale Signale ( ready
und valid
) hat, und trotzdem, um sicherzugehen, dass Einige Beatik werden nicht hinter ein oder zwei Schlägen auf der Straße zurückbleiben. Nach einiger Zeit wurde mir klar, dass das Beschreiben der Synchronisation im ready
- valid
ohne ein gemeinsames Taktsignal - auch eine Aufgabe ist, die dem Erstellen nicht blockierender Datenstrukturen in dem Sinne ähnelt, dass Sie viel nachdenken und formal beweisen müssen. Es ist leicht, einen Fehler zu machen, es ist schwer zu bemerken und vor allem ist alles bereits vor uns implementiert: Intel hat ein dcfifo
, eine Warteschlange mit konfigurierbarer Länge und Breite, die aus verschiedenen Frequenzbereichen gelesen und geschrieben wird. Infolgedessen nutzte ich die experimentelle Gelegenheit von frischem Meißel, nämlich die parametrisierten Black Boxes:
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" }
Und er schrieb ein einfaches kleines Fernglas mit beliebigen Datentypen:
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 } }
Debuggen
Danach verwandelte sich der Code in die Übertragung von Nachrichten zwischen Domänen über zwei bereits unidirektionale Warteschlangen: tl_req
/ ddr_req
und ddr_resp
/ tl_resp
(die mit dem Präfix tl_
wird zusammen mit TileLink getaktet, die Tatsache, dass ddr_
mit dem Speichercontroller verbunden ist). Das Problem ist, dass sowieso alles festgefahren war und es manchmal ziemlich warm war. Und wenn die Ursache für Überhitzung die gleichzeitige Einstellung von local_read_req
und local_write_req
, war es nicht so einfach, mit Deadlocks umzugehen. Der Code zur gleichen Zeit war so etwas wie
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)
Um das Problem zu lokalisieren, habe ich beschlossen, den gesamten Code in withClock(ddr_clock)
(nicht withClock(ddr_clock)
, visuell sieht es so aus, als würde ein Stream erstellt) und ihn durch einen Stub zu ersetzen, der mit Sicherheit funktioniert:
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) }
Wie ich später bemerkte, funktionierte dieser Stub auch nicht, weil das Wire(...)
-Konstrukt, das ich "aus Gründen der Zuverlässigkeit" hinzugefügt habe, um zu zeigen, dass es sich um einen benannten Draht handelt, das Argument tatsächlich nur als Prototyp zum Erstellen verwendete Art der Bedeutung, aber nicht an das Ausdrucksargument gebunden . Als ich versuchte zu lesen, was noch generiert wurde, stellte ich fest, dass es im Simulationsmodus eine große Auswahl an Aussagen bezüglich der Nichteinhaltung des TileLink-Protokolls gibt. Sie werden mir wahrscheinlich später nützlich sein, aber bisher wurde nicht versucht, die Simulation auszuführen - warum sollte ich sie starten? Verilator weiß wahrscheinlich nichts über die IP-Cores von Alter. ModelSim Starter Edition wird sich höchstwahrscheinlich weigern, ein so großes Projekt zu simulieren, aber ich habe auch geschworen, dass es kein Controller-Modell für die Simulation gibt. Und um es zu generieren, müssen Sie wahrscheinlich zuerst zur neuen Version des Controllers wechseln (da die alte im alten Quartus konfiguriert wurde).
Tatsächlich wurden die Codeblöcke einer fast funktionierenden Version entnommen und nicht derjenigen, die einige Stunden zuvor aktiv getestet wurde. Aber Sie besser;) Übrigens können Sie das Design ständig schneller wieder zusammenbauen, wenn die Einstellung WithNBigCores(1)
durch WithNSmallCores(1)
- aus Sicht der Grundfunktionalität des Speichercontrollers scheint es keinen Unterschied zu geben. Und ein kleiner Trick: Um nicht jedes Mal dieselben Befehle in gdb zu übertragen (zumindest habe ich zwischen den Sitzungen dort keinen Befehlsverlauf), können Sie einfach so etwas in die Befehlszeile eingeben
../../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"
und nach Bedarf mit normalen Mitteln der Shell ausführen.
Zusammenfassung
Als Ergebnis wurde der folgende Code für die Arbeit mit der Steuerung erhalten:
withClock(ddr_clock) { val rreq = RegInit(false.B)
Hier werden wir das Abschlusskriterium noch leicht ändern: Ich habe bereits gesehen, dass die aufgezeichneten Daten ohne Arbeit mit dem Speicher wie gelesen sind, weil es sich um einen Cache handelt. Deshalb kompilieren wir einen einfachen Code:
#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
Als Ergebnis erhalten wir das folgende Fragment der Assembler-Liste, das die ersten 16 MB Speicher initialisiert:
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
am Anfang von bootrom/xip/leds.S
Jetzt ist es unwahrscheinlich, dass alles nur in einem Cache gespeichert werden kann. Es bleibt, das Makefile auszuführen, das Projekt in Quartus neu zu erstellen, es in das Board zu füllen, OpenOCD + GDB zu verbinden und ... Vermutlich Prost, Sieg:
$ ../../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
Ist es so, werden wir in der nächsten Serie herausfinden (ich kann auch nicht über Leistung, Stabilität usw. sagen).
Code: AltmemphyDDR2RAM.scala .