Dalam empat bagian sebelumnya, persiapan dibuat untuk percobaan dengan inti RocketChip RISC-V, yaitu, porting inti ini ke papan "non-standar" untuk itu dengan Altera FPGAs (sekarang Intel). Akhirnya, pada bagian terakhir , ternyata menjalankan Linux di forum ini. Apakah Anda tahu apa yang membuat saya terhibur tentang semua ini? Bahwa pada saat yang sama saya harus bekerja dengan assembler RISC-V, C dan Scala, dan dari semuanya, Scala adalah bahasa tingkat terendah (karena prosesor tertulis di atasnya).
Mari kita buat C tidak menyinggung dalam artikel ini juga. Selain itu, jika bundel Scala + Chisel hanya digunakan sebagai bahasa khusus domain untuk deskripsi perangkat keras yang eksplisit, hari ini kita akan belajar cara "menarik" fungsi C sederhana ke prosesor dalam bentuk instruksi.
Tujuan utamanya adalah implementasi sepele dari instrumentasi mirip AFL sepele dengan analogi dengan QInst , dan penerapan instruksi mandiri hanyalah produk sampingan.
Jelas bahwa ada (dan bukan satu) komersial OpenCL to RTL converter. Saya juga menemukan informasi tentang proyek COPILOT tertentu untuk RISC-V dengan tujuan yang sama (jauh lebih maju), tetapi ada sesuatu yang sangat buruk di Google, dan selain itu, kemungkinan besar juga merupakan produk komersial. Saya terutama tertarik pada solusi OpenSource, tetapi meskipun mereka ada, masih menyenangkan untuk mencoba menerapkannya sendiri - setidaknya sebagai contoh pelatihan yang disederhanakan, dan kemudian bagaimana hasilnya ...
Penafian (di samping peringatan biasa tentang "menari dengan alat pemadam kebakaran"): Saya sangat tidak menyarankan menerapkan secara sembarangan inti perangkat lunak yang dihasilkan, terutama dengan data yang tidak terpercaya - sejauh ini saya tidak begitu percaya diri karena bahkan memahami mengapa data yang sedang diproses tidak dapat "Mengalir" dalam beberapa kasus batas antara proses dan / atau inti. Nah, tentang fakta bahwa data dapat "mengalahkan", saya pikir, dan itu jelas. Secara umum, masih ada validasi dan validasi ...
Sebagai permulaan, apa yang saya sebut "fungsi sederhana"? Untuk keperluan artikel ini, ini berarti fungsi di mana semua transisi (bersyarat dan tanpa syarat) hanya meningkatkan penghitung instruksi dengan nilai konstan. Yaitu, grafik dari semua transisi yang mungkin adalah asiklik (diarahkan), tanpa tepi "dinamis". Tujuan akhir dalam kerangka artikel ini adalah untuk dapat mengambil fungsi sederhana dari program dan, menggantinya dengan plug-in assembler, "menjahitnya" ke dalam prosesor pada tahap sintesis, secara opsional menjadikannya efek samping dari instruksi lain. Secara khusus, percabangan tidak akan ditampilkan dalam artikel ini, tetapi dalam kasus paling sederhana tidak akan sulit untuk membuatnya.
Belajar memahami C (sebenarnya, tidak)
Pertama, Anda perlu memahami bagaimana kami akan menguraikan C? Itu benar, tidak mungkin - tidak sia-sia bahwa saya belajar mengurai file ELF : Anda hanya perlu mengkompilasi kode kami di C / Rust / sesuatu yang lain menjadi bytecode eBPF, dan sudah menguraikannya. Beberapa kesulitan disebabkan oleh fakta bahwa di Scala Anda tidak dapat menghubungkan elf.h
dan membaca bidang struktur. Anda tentu saja dapat mencoba menggunakan JNAerator - jika perlu, mereka dapat melakukan pengikat ke perpustakaan - tidak hanya struktur, tetapi juga menghasilkan kode untuk bekerja melalui JNA (jangan bingung dengan JNI). Sebagai seorang programmer sejati, saya akan menulis sepeda saya dan dengan hati-hati menulis enumerasi dan mengimbangi konstanta dari file header. Struktur hasil dan menengah dijelaskan oleh struktur kelas kasus berikut:
sealed trait SectionKind case object RegularSection extends SectionKind case object SymtabSection extends SectionKind case object StrtabSection extends SectionKind case object RelSection extends SectionKind final case class Elf64Header( sectionHeaders: Seq[ByteBuffer], sectionStringTableIndex: Int ) final case class Elf64Section( data: ByteBuffer, linkIndex: Int, infoIndex: Int, kind: SectionKind ) final case class Symbol( name: String, value: Int, size: Int, shndx: Int, isInstrumenter: Boolean ) final case class Relocation( relocatedSection: Int, offset: Int, symbol: Symbol ) final case class BpfInsn( opcode: Int, dst: Int, src: Int, offset: Int, imm: Either[Long, Symbol] ) final case class BpfProg( name: String, insns: Seq[BpfInsn] )
Saya tidak akan secara khusus menjelaskan proses parsing - ini hanya transfer byte yang membosankan dari java.nio.ByteBuffer
- semua hal menarik telah dijelaskan dalam artikel tentang parsing file ELF . Saya hanya akan mengatakan bahwa Anda perlu menangani opcode == 0x18
dengan hati-hati (memuat nilai langsung 64-bit ke dalam register), karena ini membutuhkan dua kata 8-byte sekaligus (mungkin ada opcode lain seperti itu, tetapi saya belum menemukan mereka) , dan ini tidak selalu memuat alamat memori yang terkait dengan relokasi, seperti yang saya pikir pada awalnya. Misalnya, __builtin_popcountl
jujur menggunakan konstanta 64-bit 0x0101010101010101
. Mengapa saya tidak melakukan relokasi "jujur" dengan menambal file yang diunduh - karena saya ingin melihat karakter dalam bentuk simbolis (maaf untuk permainan kata-kata), sehingga nantinya karakter dari bagian COMMON
dapat diganti dengan register tanpa menggunakan kruk dengan penanganan khusus alamat dari jenis khusus (dan artinya, bahkan dengan tarian dengan UInt
konstan / non-konstan).
Kami membangun perangkat keras sesuai dengan serangkaian instruksi
Jadi, dengan asumsi, semua jalur eksekusi yang mungkin masuk secara eksklusif ke bawah daftar instruksi, yang berarti bahwa data mengalir sepanjang grafik asiklik yang berorientasi, dan semua tepinya didefinisikan secara statis. Pada saat yang sama, kami memiliki logika kombinasional murni (yaitu, tanpa register di jalan), yang diperoleh dari operasi pada register, serta penundaan selama operasi pemuatan / penyimpanan dengan memori. Jadi, dalam kasus umum, operasi mungkin tidak dapat diselesaikan dalam satu siklus clock. Kami akan melakukan yang sederhana: kami akan mentransfer nilai ke dalam bentuk UInt
, tetapi seperti (UInt, Bool)
: elemen pertama dari pasangan adalah nilainya, dan yang kedua adalah tanda kebenarannya. Artinya, tidak masuk akal untuk membaca dari memori, asalkan alamatnya salah, dan menulis pada umumnya tidak mungkin.
Model eksekusi bytecode eBPF mengasumsikan beberapa jenis RAM dengan pengalamatan 64-bit, serta satu set 16 (atau bahkan sepuluh) register 64-bit. Algoritma rekursif primitif diusulkan:
- kita mulai dengan konteks di mana operan instruksi terletak pada
r1
dan r2
, di sisanya - nol, semuanya valid (lebih tepatnya, validitasnya sama dengan "kesiapan" instruksi coprocessor) - jika kita melihat instruksi aritmatika-logis, kita mengekstrak register operan dari konteksnya, menyebut diri kita sebagai ekor daftar dan konteks di mana operan output diganti dengan pasangan
(data1 op data2, valid1 && valid2)
- jika kita menemukan cabang, kita cukup membangun kedua cabang secara rekursif: jika cabang terjadi, dan jika tidak
- jika kita menemukan pemuatan atau penyimpanan ke memori, entah bagaimana kita keluar: kita menjalankan panggilan balik yang ditransfer, dengan asumsi invarian bahwa setelah pernyataan yang
valid
tidak dapat ditarik kembali selama pelaksanaan instruksi ini. Validitas operasi save adalah DAN oleh kami dengan flag globalValid
, yang harus ditetapkan sebelum mengembalikan kontrol. Pada saat yang sama, kita harus membaca dan menulis di bagian depan untuk memproses peningkatan dan modifikasi lainnya dengan benar.
Dengan demikian, operasi akan dilakukan sejajar mungkin, dan tidak dalam langkah-langkah. Pada saat yang sama, saya meminta Anda untuk memperhatikan bahwa semua operasi pada byte memori tertentu secara alami harus sepenuhnya dipesan, jika tidak hasilnya tidak dapat diprediksi, UB. Yaitu *addr += 1
- ini normal, menulis tidak akan dimulai sampai pembacaan selesai (klise karena kita masih tidak tahu harus menulis apa), tetapi *addr += 1; return *addr;
*addr += 1; return *addr;
Saya biasanya memberi nol atau sesuatu seperti itu dengan aman. Mungkin ini layak untuk di-debug (mungkin menyembunyikan beberapa masalah yang lebih rumit), tetapi daya tarik itu sendiri adalah ide yang biasa saja, karena Anda harus melacak alamat memori yang sudah dikerjakan, tetapi saya memiliki keinginan Validasi nilai valid
memungkinkan secara statis. Inilah yang akan dilakukan untuk variabel global berukuran tetap.
Hasilnya adalah kelas abstrak BpfCircuitConstructor
, yang tidak memiliki metode yang diimplementasikan doMemLoad
, doMemStore
, dan resolveSymbol
:
trait BpfCircuitConstructor {
Integrasi inti CPU
Sebagai permulaan, saya memutuskan untuk pergi dengan cara sederhana: terhubung ke inti prosesor menggunakan protokol RoCC (Rocket Custom Coprocessor) standar. Sejauh yang saya mengerti, ini adalah ekstensi reguler bukan untuk semua kernel yang kompatibel dengan RISC-V, tetapi hanya untuk Rocket dan BOOM (Berkeley Out-of-Order Machine), oleh karena itu, ketika menyeret pekerjaan upstream pada kompiler, mnemonics assembler custom0
dikeluarkan dari mereka - custom3
bertanggung jawab atas perintah akselerator.
Secara umum, setiap inti prosesor Rocket / BOOM dapat memiliki hingga empat akselerator RoCC yang ditambahkan melalui konfigurasi, ada juga contoh implementasi:
Configs.scala:
class WithRoccExample extends Config((site, here, up) => { case BuildRoCC => List( (p: Parameters) => { val accumulator = LazyModule(new AccumulatorExample(OpcodeSet.custom0, n = 4)(p)) accumulator }, (p: Parameters) => { val translator = LazyModule(new TranslatorExample(OpcodeSet.custom1)(p)) translator }, (p: Parameters) => { val counter = LazyModule(new CharacterCountExample(OpcodeSet.custom2)(p)) counter }) })
Implementasi yang sesuai ada di file LazyRoCC.scala
.
Implementasi akselerator mewakili dua kelas yang sudah akrab dari pengontrol memori: salah satunya dalam hal ini diwarisi dari LazyRoCC
, yang lain dari LazyRoCCModuleImp
. Kelas kedua memiliki port io
tipe RoCCIO
, yang berisi port permintaan cmd
, port respons resp
, port cache L1D akses mem, output busy
dan interrupt
, dan input exception
. Ada juga port walker tabel halaman dan FPU yang tampaknya belum kita butuhkan (lagi pula, tidak ada aritmatika nyata dalam eBPF). Sejauh ini saya ingin mencoba melakukan sesuatu dengan pendekatan ini, jadi saya tidak akan menyentuh. Juga, seperti yang saya mengerti, ada antarmuka TileLink untuk akses memori yang tidak di-cache, tetapi untuk sekarang saya tidak akan menyentuhnya juga.
Penyelenggara permintaan
Jadi, kami memiliki port untuk mengakses cache, tetapi hanya satu. Pada saat yang sama, suatu fungsi dapat, misalnya, menambah variabel (yang, paling tidak, dapat diubah menjadi operasi atom tunggal) atau bahkan entah bagaimana mengubahnya secara nontrivial dengan memuat, memperbarui, dan menyimpannya. Pada akhirnya, satu instruksi dapat membuat beberapa permintaan yang tidak terkait. Mungkin ini bukan ide terbaik dalam hal kinerja, tetapi, di sisi lain, mengapa tidak, katakanlah, memuat tiga kata (yang, sangat mungkin, sudah ada dalam cache), entah bagaimana memprosesnya secara paralel dengan logika kombinasional (kemudian satu pukulan) dan simpan hasilnya. Oleh karena itu, kita memerlukan semacam skema yang secara efektif “menyelesaikan” upaya akses paralel ke satu port cache.
Logikanya akan menjadi seperti berikut: pada awal generasi penerapan sub-injeksi tertentu (bidang funct
7-bit dalam hal RoCC), sebuah instance dari query serializer dibuat (membuat satu global tampaknya cukup berbahaya bagi saya, karena ia menciptakan banyak dependensi tambahan antara permintaan yang tidak pernah dapat dijalankan secara bersamaan, dan menghamburkan Fmax kemungkinan besar akan). Selanjutnya, setiap "saver" / "loader" yang dibuat terdaftar di serializer. Dalam antrian langsung, jadi untuk berbicara. Pada setiap tindakan, permintaan pertama dalam urutan pendaftaran dipilih - ia diberikan izin pada tindakan berikutnya . Secara alami, logika seperti itu perlu ditutupi dengan tes (saya benar-benar belum memiliki banyak dari mereka, jadi ini bukan hanya verifikasi, tetapi set minimum yang diperlukan untuk mendapatkan setidaknya sesuatu yang dapat dipahami). Saya menggunakan PeekPokeTester
standar dari komponen yang kurang lebih resmi untuk menguji desain Pahat. Saya sudah menggambarkannya sekali .
Hasilnya adalah alat seperti itu:
class Serializer(isComputing: Bool, next: Bool) { def monotonic(x: Bool): Bool = { val res = WireInit(false.B) val prevRes = RegInit(false.B) prevRes := res && isComputing res := (x || prevRes) && isComputing res } private def noone(bs: Seq[Bool]): Bool = !bs.foldLeft(false.B)(_ || _) private val previousReqs = ArrayBuffer[Bool]() def nextReq(x: Bool): (Bool, Int) = { val enable = monotonic(x) val result = RegInit(false.B) val retired = RegInit(false.B) val doRetire = result && next val thisReq = enable && !retired && !doRetire val reqWon = thisReq && noone(previousReqs) when (isComputing) { when(reqWon) { result := true.B } when(doRetire) { result := false.B retired := true.B } } otherwise { result := false.B retired := false.B } previousReqs += thisReq (result, previousReqs.length - 1) } }
Harap dicatat bahwa di sini dalam proses membuat sirkuit digital, kode Scala dijalankan dengan aman. Jika Anda melihat lebih dekat, Anda bahkan dapat melihat ArrayBuffer
mana bagian-bagian sirkuit ditumpuk ( Boolean
adalah jenis dari Scala, Bool
adalah jenis Pahat yang mewakili peralatan langsung, dan bukan boolean yang dikenal saat runtime).
Bekerja dengan L1D Cache
Bekerja dengan cache sebagian besar terjadi melalui io.mem.req
permintaan io.mem.req
dan io.mem.resp
respons io.mem.resp
. Pada saat yang sama, port permintaan dilengkapi dengan sinyal tradisional yang ready
dan valid
: yang pertama memberitahu Anda bahwa ia siap menerima permintaan, yang kedua kami mengatakan bahwa permintaan sudah siap dan sudah memiliki struktur yang benar, di bagian depan, valid && resp
permintaan dianggap diterima. Dalam beberapa antarmuka seperti itu, ada persyaratan "non-respons" dari sinyal dari saat pengaturan ke true
dan ke tepi positif selanjutnya dari valid && resp
(ungkapan ini dapat dibangun menggunakan metode fire()
untuk kenyamanan).
Port respons resp
, pada gilirannya, hanya memiliki tanda yang valid
, dan ini adalah masalah prosesor untuk memperoleh jawaban dalam satu siklus clock: itu "selalu siap" dengan asumsi, dan fire()
mengembalikan hanya valid
.
Juga, seperti yang sudah saya katakan, Anda tidak dapat membuat permintaan ketika itu mengerikan: Anda tidak dapat menulis sesuatu, saya tidak tahu apa, dan membaca lagi apa yang akan ditimpa nanti berdasarkan nilai yang dikurangi juga entah bagaimana aneh. Tetapi kelas Serializer
sudah memahami ini, tetapi kami hanya memberinya tanda bahwa permintaan saat ini telah masuk ke cache: next = io.mem.req.fire()
. Semua yang bisa dilakukan adalah memastikan bahwa di "pembaca" jawabannya diperbarui hanya ketika benar-benar datang - tidak lebih awal dan tidak lebih lambat. Ada metode holdUnless
nyaman untuk holdUnless
. Hasilnya kira-kira implementasi berikut:
class Constructor extends BpfCircuitConstructor { val serializer = new Serializer(isComputing, io.mem.req.fire()) override def doMemLoad(addr: UInt, tpe: LdStType, valid: Bool): (UInt, Bool) = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XRD io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := 0.U io.mem.req.valid := true.B } val doResp = isComputing && serializer.monotonic(doReq && io.mem.req.fire()) && io.mem.resp.valid && io.mem.resp.bits.tag === thisTag.U && io.mem.resp.bits.cmd === M_XRD (io.mem.resp.bits.data holdUnless doResp, serializer.monotonic(doResp)) } override def doMemStore(addr: UInt, tpe: LdStType, data: UInt, valid: Bool): Bool = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XWR io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := data io.mem.req.valid := true.B } serializer.monotonic(doReq && io.mem.req.fire()) } override def resolveSymbol(sym: BpfLoader.Symbol): Resolved = sym match { case BpfLoader.Symbol(symName, _, size, ElfConstants.Elf64_Shdr.SHN_COMMON, false) if size <= 8 => RegisterReference(regs.getOrElseUpdate(symName, RegInit(0.U(64.W)))) } }
Sebuah instance dari kelas ini dibuat untuk setiap subinstruksi yang dihasilkan.
Tidak semua yang ada di heap adalah variabel global
Hmm, apa contoh modelnya? Kinerja apa yang ingin saya pastikan? Tentu saja, instrumentasi AFL! Itu terlihat dalam versi klasik seperti ini:
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_branch(uint64_t tag) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
Seperti yang Anda lihat, itu memuat lebih atau kurang logis memuat dan menyimpan (dan di antara mereka selisih) satu byte dari __afl_area_ptr
, tetapi di sini register meminta peran prev
!
Inilah sebabnya mengapa antarmuka Resolved
diperlukan: itu dapat membungkus alamat memori biasa atau menjadi referensi register. Pada saat yang sama, sejauh ini saya hanya mempertimbangkan register skalar dengan ukuran 1, 2, 4 atau 8 byte, yang selalu dibaca dengan nol offset, jadi untuk register, Anda dapat dengan relatif tenang menerapkan pemesanan panggilan. Dalam hal ini, sangat berguna untuk mengetahui bahwa prev
pertama-tama harus dikurangi dan digunakan untuk menghitung indeks, dan baru kemudian ditulis ulang.
Dan sekarang instrumentasi
Pada titik tertentu, kami mendapat akselerator yang bekerja terpisah dan lebih banyak dengan antarmuka RoCC. Apa sekarang? Mengimplementasikan kembali semua sama, mendorong melalui pipa prosesor? Tampak bagi saya bahwa kruk lebih sedikit akan diperlukan jika, sejalan dengan instruksi yang diinstruksikan, coprocessor dengan funct
nilai utilitas yang dikeluarkan secara otomatis hanya akan diaktifkan. Pada prinsipnya, saya juga harus menyiksa diri sendiri untuk ini: Saya bahkan belajar menggunakan SignalTap, karena debugging hampir buta, dan bahkan dengan kompilasi lima menit setelah perubahan sekecil apa pun (kecuali untuk mengubah bootrom - semuanya cepat di sana) - ini sudah terlalu banyak.
Akibatnya, decoder perintah diperbaiki dan pipeline sedikit "diluruskan" untuk mempertimbangkan fakta bahwa tidak peduli apa yang dikatakan decoder tentang instruksi asli, RoCC yang tiba-tiba diaktifkan itu sendiri tidak berarti bahwa akan ada latensi panjang untuk menulis ke register output, seperti selama operasi pembagian dan ketinggalan cache data.
Secara umum, deskripsi instruksi adalah pasangan ([pola untuk mengenali instruksi], [set nilai yang mengkonfigurasi blok jalur data dari inti prosesor]). Misalnya, default
(instruksi yang tidak dikenal) terlihat seperti ini (diambil dari IDecode.scala
, di desktop Habr tampilannya, terus terang, jelek):
def default: List[BitPat] =
... dan deskripsi tipikal dari salah satu ekstensi di inti Rocket diimplementasikan seperti ini:
class IDecode(implicit val p: Parameters) extends DecodeConstants { val table: Array[(BitPat, List[BitPat])] = Array( BNE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SNE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BEQ-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SEQ, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLT-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLT, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLTU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLTU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGEU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGEU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N),
Faktanya adalah bahwa dalam RISC-V (tidak hanya di RocketChip, tetapi dalam arsitektur perintah pada prinsipnya) ISA membelah menjadi subset wajib I (operasi integer), serta opsional M (perkalian integer dan pembagian), A (atom) didukung secara teratur dll.
Akibatnya, metode asli
def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])]) = { val decoder = DecodeLogic(inst, default, table) val sigs = Seq(legal, fp, rocc, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} this }
telah digantikan oleh
sama, tetapi dengan decoder untuk instrumentasi dan klarifikasi alasan untuk aktivasi rocc def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])], handlers: Seq[OpcodeHandler]) = { val decoder = DecodeLogic(inst, default, table) val sigs=Seq(legal, fp, rocc_explicit, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} if (handlers.isEmpty) { handler_rocc := false.B handler_rocc_funct := 0.U } else { val handlerTable: Seq[(BitPat, List[BitPat])] = handlers.map { case OpcodeHandler(pattern, funct) => pattern -> List(Y, BitPat(funct.U)) } val handlerDecoder = DecodeLogic(inst, List(N, BitPat(0.U)), handlerTable) Seq(handler_rocc, handler_rocc_funct) zip handlerDecoder map { case (s,d) => s:=d } } rocc := rocc_explicit || handler_rocc this }
Dari perubahan dalam pipeline prosesor, mungkin yang paling tidak terlihat adalah:
io.rocc.exception := wb_xcpt && csr.io.status.xs.orR io.rocc.cmd.bits.status := csr.io.status io.rocc.cmd.bits.inst := new RoCCInstruction().fromBits(wb_reg_inst) + when (wb_ctrl.handler_rocc) { + io.rocc.cmd.bits.inst.opcode := 0x0b.U // custom0 + io.rocc.cmd.bits.inst.funct := wb_ctrl.handler_rocc_funct + io.rocc.cmd.bits.inst.xd := false.B + io.rocc.cmd.bits.inst.rd := 0.U + } io.rocc.cmd.bits.rs1 := wb_reg_wdata io.rocc.cmd.bits.rs2 := wb_reg_rs2
Jelas bahwa beberapa parameter permintaan ke akselerator perlu dikoreksi: tidak ada respons yang ditulis ke register, dan funct
sama dengan apa yang dikembalikan oleh decoder. Tetapi ada perubahan yang sedikit kurang jelas: faktanya adalah bahwa perintah ini tidak langsung ke akselerator (empat dari mereka - yang mana?), Tetapi ke router, jadi Anda perlu berpura-pura bahwa perintah memiliki opcode == custom0
(ya, proses, dan justru akselerator nol!).
Periksa
Bahkan, artikel ini mengasumsikan kelanjutan di mana upaya akan dilakukan untuk membawa pendekatan ini ke tingkat produksi yang kurang lebih. Minimal, Anda harus belajar untuk menyimpan dan memulihkan konteks (keadaan register coprocessor) saat berpindah tugas. Sementara itu, saya akan memeriksa apakah itu berfungsi dalam kondisi rumah kaca:
#include <stdint.h> uint64_t counter; uint64_t funct1(uint64_t x, uint64_t y) { return __builtin_popcountl(x); } uint64_t funct2(uint64_t x, uint64_t y) { return (x + y) * (x - y); } uint64_t instMUL() { counter += 1; *((uint64_t *)0x81005000) = counter; return 0; }
Sekarang tambahkan ke bootrom/sdboot/sd.c
di baris main
#include "/path/to/freedom-u-sdk/riscv-pk/machine/encoding.h"
write_csr
, custom0
- custom3
. , illegal instruction, , , , . define
- - , «» binutils customX
RocketChip, , , .
sdboot , , .
:
$ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000000000 in ?? () (gdb) x/d 0x81005000 0x81005000: 123 (gdb) set variable $pc=0x10000 (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. 0x0000000000010488 in crc16_round (data=<optimized out>, crc=<optimized out>) at sd.c:151 151 crc ^= data; (gdb) x/d 0x81005000 0x81005000: 246
funct1 $ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000010194 in main () at sd.c:247 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); (gdb) set variable $a5=0 (gdb) set variable $pc=0x10194 (gdb) set variable $a4=0xaa (gdb) display/10i $pc-10 1: x/10i $pc-10 0x1018a <main+46>: sw a3,124(a3) 0x1018c <main+48>: addiw a0,a0,1110 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 => 0x10194 <main+56>: 0x2f7778b 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> (gdb) display/x $a5 2: /x $a5 = 0x0 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x4 (gdb) set variable $a4=0xaabc (gdb) set variable $pc=0x10194 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x9
Kode sumber