Pada bagian sebelumnya, kami memasang mikrokontroler tanpa RAM berdasarkan Altera / Intel FPGA. Namun, papan memiliki konektor dengan SOGD DIMM DDR2 1Gb, yang, jelas, saya ingin gunakan. Untuk melakukan ini, kita perlu membungkus pengontrol DDR2 dengan antarmuka ALTMEMPHY
dalam modul yang dapat dimengerti untuk protokol memori TileLink yang digunakan di seluruh RocketChip. Di bawah cut - tactile debugging, pemrograman brute force dan RAKE.
Seperti yang Anda ketahui, Ilmu Komputer memiliki dua masalah utama: pembatalan cache dan penamaan variabel. Di KDPV, Anda melihat momen langka - dua masalah utama CS saling bertemu dan sedang merencanakan sesuatu .
PENOLAKAN: Selain peringatan dari artikel sebelumnya, saya sangat menyarankan Anda membaca artikel sampai akhir sebelum mengulangi percobaan, untuk menghindari kerusakan pada FPGA, modul memori atau sirkuit daya.
Kali ini saya ingin, jika tidak mem-boot Linux, maka setidaknya hubungkan RAM, yang pada board saya sudah memiliki seluruh gigabyte (atau Anda dapat menempatkan hingga empat). Kriteria keberhasilan diusulkan untuk mempertimbangkan kemampuan membaca dan menulis melalui sekelompok GDB + OpenOCD, termasuk alamat yang tidak selaras dengan 16 byte (lebar satu permintaan ke memori). Pada pandangan pertama, Anda hanya perlu sedikit memperbaiki konfigurasi, generator SoC tidak dapat mendukung RAM di luar kotak. Ini mendukungnya, tetapi melalui antarmuka MIG (well, dan, mungkin, beberapa antarmuka lain dari Microsemi). Melalui antarmuka standar, AXI4 juga mendukungnya, tetapi, seperti yang saya mengerti, tidak mudah untuk mendapatkannya (setidaknya, tidak menguasai Perancang Platform).
Penyimpangan liris: Sejauh yang saya mengerti, ada serangkaian antarmuka AXI "intra-chip" yang dikembangkan oleh ARM. Di sini orang akan berpikir bahwa itu semua dipatenkan dan ditutup. Tetapi setelah saya mendaftar (tanpa "program universitas" dan apa pun - hanya dengan mengirim email dan mengisi kuesioner) dan mendapatkan akses ke spesifikasi, saya sangat terkejut. Tentu saja, saya bukan pengacara, tetapi tampaknya standarnya cukup terbuka: Anda juga harus menggunakan kernel berlisensi dari ARM, atau sama sekali tidak mengklaim kompatibel dengan ARM, dan kemudian semuanya tampak baik-baik saja . Tetapi secara umum, tentu saja, membaca lisensi, membaca dengan pengacara, dll.
Monkey and TileLink (dongeng)
Tugasnya tampak cukup sederhana, dan saya membuka deskripsi papan modul ddr2_64bit
sudah tersedia dalam proyek dari pemasok:
Properti Intel dan umumnya 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; ...
Kata populer mengatakan: "Dokumentasi apa pun dalam bahasa Rusia harus dimulai dengan kata-kata:" Jadi, itu tidak berhasil. " Tetapi antarmuka di sini tidak sepenuhnya intuitif , jadi kami masih membacanya . Dalam uraian kami segera diberitahu bahwa bekerja dengan DDR2 bukanlah tugas yang mudah. Anda perlu mengkonfigurasi PLL, melakukan kalibrasi, crack-fex-pex , sinyal local_init_done
, Anda dapat bekerja. Secara umum, logika penamaan di sini kira-kira sebagai berikut: nama dengan awalan local_
adalah antarmuka "pengguna", port mem_
langsung di-output ke kaki yang terhubung ke modul memori, pll_ref_clk
perlu dikirim sinyal jam dengan frekuensi yang ditentukan saat mengatur modul - sisanya akan diterima darinya frekuensi, well, segala macam input dan output reset dan output frekuensi, yang antarmuka pengguna harus bekerja dengan sinkron.
Mari kita buat deskripsi sinyal eksternal ke memori dan antarmuka modul ddr2_64bit
:
sifat 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
kelas 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()) }) }
Di sini sekelompok garu pertama menunggu saya: pertama, ROMGenerator
kelas ROMGenerator
, saya berpikir bahwa pengontrol memori dapat ditarik keluar dari kedalaman desain melalui variabel global, dan Pahat entah bagaimana akan meneruskan kabel itu sendiri. Itu tidak berhasil. Karena itu, saya harus membuat harness wiring MemIfBundle
yang MemIfBundle
seluruh hierarki. Mengapa itu tidak keluar dari BlackBox
, dan itu tidak terhubung sekaligus? Faktanya adalah bahwa dengan BlackBox
semua port eksternal dimasukkan ke dalam val io = IO(new Bundle { ... })
. Jika seluruh MemIfBundle
dibuat menjadi satu variabel dalam bundel, maka nama variabel ini akan dibuat awalan untuk nama-nama semua port, dan nama-nama tidak akan klise bertepatan dengan antarmuka blok. Mungkin itu bisa dilakukan entah bagaimana dengan lebih memadai , tetapi untuk sekarang biarkan saja seperti itu.
Lebih jauh, dengan analogi dengan perangkat TileLink lainnya (terutama yang hidup di rocket-chip/src/main/scala/tilelink
), dan terutama BootROM
, kami akan menjelaskan antarmuka kami ke pengontrol memori:
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
kunci ExtMem
standar ExtMem
kami mengekstrak parameter memori eksternal dari ExtMem
SoC ( sintaks aneh ini memungkinkan saya untuk mengatakan “Saya tahu bahwa mereka akan mengembalikan instance kelas kasus MemoryPortParameters
(ini dijamin dengan jenis kunci pada tahap mengkompilasi kode Scala, dengan syarat) bahwa kita tidak akan jatuh runtime dengan mengambil konten dari Option[MemoryPortParams]
sama dengan None
, tetapi kemudian tidak ada yang membuat pengontrol memori di System.scala
...), jadi, saya tidak memerlukan kelas kasus, dan beberapa bidangnya diperlukan "). Selanjutnya, kami membuat port manajer perangkat TileLink (protokol TileLink memastikan interaksi hampir semua yang berkaitan dengan memori: pengontrol DDR dan perangkat yang dipetakan memori lainnya, cache prosesor, mungkin yang lain, setiap perangkat dapat memiliki beberapa port, masing-masing perangkat dapat berupa manajer dan klien). beatBytes
, seperti yang saya mengerti, menetapkan ukuran satu transaksi, dan kami memiliki 16 byte yang dipertukarkan dengan controller. HasAltmemphyDDR2
dan HasAltmemphyDDR2Imp
kita HasAltmemphyDDR2Imp
di tempat yang tepat di System.scala
, tulis konfigurasi
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 )
Setelah membuat "sketsa burung hantu" di AltmemphyDDR2RAMImp
, saya mensintesis desain (sesuatu hanya pada ~ 30MHz, bagus karena saya clock dari 25MHz) dan, dengan meletakkan jari saya pada modul memori dan chip FPGA, saya mengunggahnya ke papan tulis. Lalu saya melihat apa antarmuka intuitif yang sebenarnya : ini adalah ketika Anda memberikan perintah di gdb untuk menulis ke memori, dan oleh prosesor yang beku dan terbakar jari merasakan panas yang kuat, Anda perlu segera menekan tombol reset di papan dan memperbaiki controller.
Baca dokumentasi untuk pengontrol DDR2
Rupanya, sudah waktunya untuk membaca dokumentasi pada pengontrol di luar daftar port. Jadi, apa yang kita miliki di sini? .. Ups, ternyata I / local_
dengan awalan local_
tidak boleh diset secara sinkron bukan dengan pll_ref_clk
, yang 25MHz, tetapi dengan phy_clk
yang phy_clk
setengah frekuensi memori untuk pengontrol setengah-tingkat, atau, dalam kasus kami, aux_half_rate_clk
(mungkin masih aux_full_rate_clk
?), yang aux_full_rate_clk
frekuensi memori penuh, dan sebentar, adalah 166MHz.
Oleh karena itu, perlu untuk melintasi batas-batas domain frekuensi. Menurut memori lama, saya memutuskan untuk menggunakan kait, atau lebih tepatnya rantai mereka:
+-+ +-+ +-+ +-+ --| |--| |--| |--| |---> +-+ +-+ +-+ +-+ | | | | ---+ | | | inclk | | | | | | --------+----+ | outclk | | ------------------+ output enable
Tetapi, setelah bermain-main dengan waktunya, saya sampai pada kesimpulan bahwa saya tidak dapat menangani dua antrian pada kait "skalar" (dalam domain frekuensi tinggi dan sebaliknya), masing-masing akan memiliki sinyal antidirectional ( ready
dan valid
), dan meskipun demikian, untuk memastikan bahwa beberapa beatik tidak akan ketinggalan satu atau dua beat di sepanjang jalan. Setelah beberapa waktu, saya menyadari bahwa mendeskripsikan sinkronisasi ready
- valid
tanpa sinyal jam umum - juga merupakan tugas yang mirip dengan membuat struktur data yang tidak menghalangi dalam arti bahwa Anda perlu berpikir dan secara resmi membuktikan banyak, mudah untuk membuat kesalahan, sulit untuk diperhatikan, dan yang terpenting, semuanya sudah diimplementasikan di hadapan kita: Intel memiliki dcfifo
primitive, yang merupakan antrian panjang dan lebar yang dapat dikonfigurasi, yang dibaca dan ditulis dari domain frekuensi yang berbeda. Sebagai hasilnya, saya mengambil keuntungan dari peluang eksperimental Pahat segar, yaitu, kotak hitam parameter:
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" }
Dan dia menulis teropong kecil sederhana dari tipe data yang berubah-ubah:
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 } }
Debugging
Setelah itu, kode berubah menjadi mentransfer pesan antara domain melalui dua antrian yang sudah searah: tl_req
/ ddr_req
dan ddr_resp
/ tl_resp
(yang dengan awalan tl_
-clock bersama dengan TileLink, fakta bahwa ddr_
ada pada pengontrol memori). Masalahnya adalah segalanya menemui jalan buntu, dan terkadang itu cukup hangat. Dan jika penyebab overheating adalah pengaturan simultan dari local_read_req
dan local_write_req
, maka itu tidak mudah untuk berurusan dengan deadlock. Kode pada saat bersamaan adalah sesuatu seperti
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)
Untuk melokalisasi masalah, saya memutuskan untuk secara tidak sengaja mengomentari semua kode di dalam withClock(ddr_clock)
(bukan, secara visual sepertinya membuat aliran) dan menggantinya dengan rintisan yang berfungsi pasti:
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) }
Seperti yang kemudian saya sadari, rintisan ini juga tidak berfungsi karena konstruk Wire(...)
, yang saya tambahkan "untuk keandalan", untuk menunjukkan bahwa itu adalah kawat bernama, sebenarnya menggunakan argumen hanya sebagai prototipe untuk membuat jenis makna, tetapi tidak mengikatnya dengan argumen-ekspresi . Juga, ketika saya mencoba membaca apa yang masih dihasilkan, saya menyadari bahwa dalam mode simulasi ada berbagai pilihan pernyataan tentang ketidakpatuhan dengan protokol TileLink. Mereka mungkin akan berguna bagi saya nanti, tetapi sejauh ini belum ada upaya untuk menjalankan simulasi - mengapa memulainya? Verilator mungkin tidak tahu tentang Alter's IP Cores, ModelSim Starter Edition kemungkinan besar akan menolak untuk mensimulasikan proyek sebesar itu, tetapi saya juga bersumpah pada kurangnya model pengontrol untuk simulasi. Dan untuk menghasilkannya, Anda mungkin harus terlebih dahulu beralih ke versi baru dari pengontrol (karena yang lama dikonfigurasi di Quartus kuno).
Bahkan, blok kode diambil dari versi yang hampir berfungsi, dan bukan kode yang aktif di-debug beberapa jam sebelumnya. Tapi Anda lebih baik;) Ngomong-ngomong, Anda dapat terus-menerus memasang kembali desain lebih cepat jika pengaturan WithNBigCores(1)
diganti dengan WithNSmallCores(1)
- dari sudut pandang fungsi dasar pengontrol memori, tampaknya tidak ada perbedaan. Dan sedikit trik: agar tidak mengarahkan perintah yang sama ke gdb setiap kali (setidaknya saya tidak memiliki riwayat perintah di antara sesi di sana), Anda cukup mengetik sesuatu seperti ini di baris perintah
../../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"
dan jalankan sesuai kebutuhan dengan menggunakan shell yang biasa.
Ringkasan
Hasilnya, kode berikut diperoleh untuk bekerja dengan controller:
withClock(ddr_clock) { val rreq = RegInit(false.B)
Di sini kita masih akan sedikit mengubah kriteria penyelesaian: Saya telah melihat bagaimana, tanpa kerja dengan memori, data yang direkam seolah-olah dibaca, karena itu adalah cache. Karenanya, kami mengkompilasi kode sederhana:
#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
Hasilnya, kami memperoleh fragmen daftar assembler berikut, menginisialisasi 16 MB memori pertama:
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
di awal bootrom/xip/leds.S
Sekarang tidak mungkin semuanya hanya dapat disimpan dalam satu cache. Masih menjalankan Makefile, membangun kembali proyek di Quartus, mengisinya di papan, sambungkan OpenOCD + GDB dan ... Agaknya, tepuk tangan, kemenangan:
$ ../../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
Begitukah, kita akan mencari tahu di seri selanjutnya (saya juga tidak bisa bilang soal kinerja, stabilitas, dll).
Kode: AltmemphyDDR2RAM.scala .