本次综合实验中我采用了 UC Berkeley 的 Chisel3 作为硬件描述的工具。
Chisel 相比 Verilog,有如下优势:
Chisel 是基于 Scala 语言设计构建的。 Scala 是一个以 Java 为基础的「Scalable Language」,在执行前会被翻译为 Class 文件,最终在 JVM 中运行。
Chisel 经过 Firrtl 中间层后,会被翻译为 Verilog 代码,然后再用 Vivado 导入综合/实现/烧写。
主要参考 https://github.com/ucb-bar/chisel-tutorial.git。
在 ArchLinux 下只需要 pacman -Sy sbt scala
安装 sbt
构建工具和 scala
; Chisel 的相关代码会在第一次使用引用 Chisel 的 sbt
项目时由 sbt
经由 Maven
构建系统下载。下载可能需要网络加速服务,或切换镜像(比较困难,因为硬写在了 sbt
的 jar
包中了),因为 Maven
主镜像非常缓慢。
为了方便大家熟悉 Chisel,Chisel 项目准备了 chisel-template 项目。用 git clone https://github.com/freechipsproject/chisel-template
即可 clone 到本地。
http://www.biyezuopin.vip
目录结构大致如下所示:
.
├── build.sbt # sbt 构建文件
├── project # sbt 生成的文件,无需关心
│ ├── build.properties
│ └── plugins.sbt
├── README.md # template 的介绍
├── scalastyle-config.xml
├── scalastyle-test-config.xml
└── src # 源码目录├── main # 一般用来放 Design Sources│ └── scala│ └── gcd│ └── GCD.scala└── test # 一般用来放 Testbench└── scala└── gcd├── GCDMain.scala└── GCDUnitTest.scala8 directories, 9 files
这个目录结构和 sbt
的构建有关系,更详细的介绍请参见 sbt
的手册,大概是 src
下面每一个目录都是一个子的 sbt
构建单位(项目),可以写独立的 build.sbt
的那种。
我主要用到了四个方面的帮助
freechipsproject
@ Github)一般采用 Chisel 的 PeekPokeTester 的 poke
(置数)和 step
(前进 n 个时钟周期),在运行的时候加上 --generate-vcd-output on
选项,然后用 GTKWave 打开生成的 vcd 文件。此法调试和 Vivado 体验近似。
.
├── ALU.anno.json
├── ALU.fir
├── ALU.v
├── BlackBoxCGROM.v
├── black_box_verilog_files.f
├── build.sbt
├── CPU.anno.json
├── CPU.fir
├── CPU.v
├── DDU.anno.json
├── DDU.fir
├── DDU.v
├── project
├── scalastyle-config.xml
├── scalastyle-test-config.xml
├── src
│ ├── main
│ │ └── scala # 除新加入的 .scala,剩余基本为原 CPU 的代码
│ │ ├── ALU.scala
│ │ ├── Control.scala
│ │ ├── CPU.scala # 加入了几条新指令
│ │ ├── Datapath.scala # 加入了几条新指令
│ │ ├── DDU.scala
│ │ ├── Instr.scala
│ │ ├── Mem.scala
│ │ ├── MMap.scala # 进行程序存储器,UART Cell 和 Textmode 显存的内存地址转换
│ │ ├── RegFile.scala
│ │ ├── TestField.scala
│ │ └── VGA # VGA 相关代码
│ │ ├── Counter.scala # VGACounter 类,用来便捷构建 reset + carry 的计数器
│ │ ├── VGACore.scala # VGA 核心模块
│ │ └── VGA.scala # 字模 ROM <&#61;&#61;> VGACore <&#61;&#61;> 显存
│ └── test
│ ├── resources # 资源文件
│ │ ├── BlackBoxCGROM.1.v # 使用 Block RAM IP 核的 CGROM Blackbox 模型
│ │ ├── BlackBoxCGROM.v # 使用 Dist RAM IP 核的 CGROM Blackbox 模型
│ │ ├── build.sh # 根据 util.c 构建内存 COE&#xff08;仅有效载荷部分&#xff09;的构建脚本
│ │ ├── inst_rom.coe # Deprecated
│ │ ├── inst_rom.S # Deprecated
│ │ ├── linker.ld # *链接器脚本
│ │ ├── main.asm # Deprecated&#xff0c;下同
│ │ ├── main_prog.asm
│ │ ├── main_prog.txt
│ │ ├── main.S
│ │ ├── mips1.asm
│ │ ├── NOTE.md
│ │ ├── start.o
│ │ ├── start.S # 用来设置栈地址的汇编代码&#xff0c;必须保证紧跟 main_loop# &#xff08;如果把 main_loop 写在 util.c 的最前面&#xff0c;一般是可以的&#xff09;# 尽管这样&#xff0c;还是应该想办法把这里的行为改进。# &#xff08;主要的困难&#xff1a;在这里添加 j main_loop 会生成额外的 .pic 段&#xff0c;为什么&#xff1f;&#xff09;
│ │ ├── test_jal_jr.asm # 用来测试 jal 和 jr 指令是否工作正常的汇编代码
│ │ ├── test_jal_jr.txt
│ │ ├── util.1.c
│ │ ├── util.c # util.c&#xff0c;CPU 运行的主程序
│ │ ├── util.c.S
│ │ ├── util.elf
│ │ ├── util_final.bin # 进行 objcopy&#xff0c;剔除所有符号后的 util_final.elf
│ │ ├── util_final.elf # 和 start.S 一同编译链接后的 util.c&#xff0c;详情参见 build.sh
│ │ ├── util_final.elf.objdump.d # 反汇编后的 util_final.elf
│ │ ├── util.o
│ │ └── xxd_c4_util_final_bin.txt # 利用 xxd 输出二进制后的 util_final.bin
│ └── scala
│ ├── ALU.scala
│ ├── CPU.scala
│ ├── DDU.scala
│ ├── RegFile.scala
│ ├── TestField.scala
│ └── VGA.scala # VGA Testbench
├── target
└── test_run_dir
http://www.biyezuopin.vip
例&#xff1a;在与 build.sbt
同层的目录下运行 sbt
后&#xff1a;
test:runMain ddu.DDUGen
用于生成 DDU 的 Verilog 代码test:runMain ddu.DDUTest --generate-vcd-output on
用于运行 DDU &#43; CPU Testbench 并生成 VCD 波形文件&#xff0c;可以用 GTKWave 打开test:runMain multicpu.CPUTest --generate-vcd-output on
运行 CPU Testbenchtest:run
可以查看所有可以运行的 main classes下面为 test:run
的示例&#xff1a;
sbt:MultiCycleCpu> test:run
[warn] Multiple main classes detected. Run &#39;show discoveredMainClasses&#39; to see the listMultiple main classes detected, select one to run:[1] ddu.DDUGen[2] ddu.DDUTest[3] gcd.GCDMain[4] gcd.GCDRepl[5] multicpu.ALUGen[6] multicpu.ALUTest[7] multicpu.CPUGen[8] multicpu.CPUTest[9] multicpu.RegFileGen[10] multicpu.RegFileTest[11] testfield.TestFieldGen[12] testfield.TestFieldTestEnter number:
构建生成的内容一般在 test_run_dir
&#xff0c;ALUGen/DDUGen
等生成的一般在项目主目录下。
poke
&#xff08;置数&#xff09;操作总是慢半个时钟周期&#xff0c;以及 iotester
执行效率和 Vivado Simulator 一样感人 top.v
DDU 缺乏时钟分频&#xff0c;所以在 Vivado 中写一个 top.v 用来实例化 Clocking Wizard IP 核&#xff0c;同时把 xdc 处理好。
利用 C 语言可以极大的简化计算地址和想指令的苦恼。
本次设计采用 mipsel-linux-gnu-gcc
进行编译。在 Baremetal 环境编译需要注意以下事项&#xff1a;
-nostdlib
-ffreestanding
-static
选项-fno-delayed-branch
-Wa,-O0
来禁用之&#xff08;参见 这篇 StackOverflow&#xff09;objcopy
把符号裁掉 build.sh
。扩展的指令&#xff1a;
JAL
和 JR
- 用来实现 C 的过程调用和返回LUI
- C 编译器经常使用 LUI 和 LW/SW 指令用来实现装入/写出ADDIU
SLTIU
等带 U 的指令 - C 编译器经常使用此版本&#xff0c;避免异常&#xff08;虽然我也没实现&#xff09;SLL
- C 编译器利用移位和加法来进行乘以常数的运算拓扑关系&#xff1a;
UART RX 通过内存映射 IO 的形式&#xff0c;连接到 CPU&#xff1b;CPU 轮询 UART Cell 地址&#xff08;0xFF00
&#xff09;&#xff0c;检测到数据有效后&#xff0c;取出数据并且写入显存&#xff08;0x3000
~0x4F40
&#xff09;&#xff1b;显存和 CGROM&#xff08;Character Generation ROM&#xff09;一起来显示对应字母。
UART Rx 通过检测下降沿开始移位&#xff0c;在 CLK_PER_BITS/2
时间后检测是否仍为低电平&#xff08;此时应该是 Start Bit 的一半&#xff09;
如果仍为低电平&#xff0c;则开始接收&#xff0c;否则认为是错误数据&#xff08;毛刺等&#xff09;&#xff0c;放弃&#xff0c;否则进入 &#xff08;3&#xff09;
每隔 CLK_PER_BITS
采样&#xff0c;共八次&#xff0c;最后再等待 CLK_PER_BITS/2
后进入等待 CPU 取走的状态
Valid
为高&#xff1b;不停检测&#xff0c;如果 Ready
为高&#xff0c;则进入可以接收下一个字节的模式&#xff08;1&#xff09;Memory Cell 如下&#xff1a;
// &#43;----------------------------------------------&#43;
// | 31 | 30 | 29 ... 8 | 7 ... 0 |
// | READY | VALID | don&#39;t care | Serial data |
// &#43;----------------------------------------------&#43;
util.c
int cursor_h &#61; 0;
int cursor_v &#61; 0;void putchar(int ch);
int getchar();
void clear_scr();
void backspace();void main_loop() {cursor_h &#61; 0;cursor_v &#61; 0;int ch;//clear_scr();putchar(&#39;>&#39; | 0xF00);for (;;) {// putchar(&#39;_&#39; | 0x700);ch &#61; getchar();if (ch &#61;&#61; &#39;\n&#39; || ch &#61;&#61; &#39;\r&#39;) {cursor_h &#61; 0;cursor_v &#43;&#61; 1;} else if (ch &#61;&#61; 8) {// Backspaceif (cursor_h &#61;&#61; 0) {cursor_h &#61; 24;cursor_v -&#61; 1;putchar(&#39; &#39; | 0xF00);cursor_h &#61; 24;cursor_v -&#61; 1;} else {cursor_h -&#61; 1;putchar(&#39; &#39; | 0xF00);cursor_h -&#61; 1;}} else if (ch &#61;&#61; 27) {for (int i &#61; 0; i < 4000; i&#43;&#43;) {putchar(&#39; &#39; | 0xF00);}cursor_h &#61; 0;cursor_v &#61; 0;} else {putchar(ch | 0xF00);}}
}void putchar(int ch) {volatile int *vram_offset &#61; 0x3000;vram_offset[cursor_v * 80 &#43; cursor_h] &#61; ch;cursor_h&#43;&#43;;if (cursor_h &#61;&#61; 80) {cursor_h &#61; 0;cursor_v&#43;&#43;;if (cursor_v &#61;&#61; 25) {cursor_v &#61; 0;}}
}int getchar() {// uart offsetvolatile int *uart_offset &#61; 0xFF00;int valid_mask &#61; 0x40000000;int byte_mask &#61; 0x000000ff;while ((*uart_offset & valid_mask) &#61;&#61; 0) {// Ready; do nothing//__asm__("nop");}int ret &#61; byte_mask & *uart_offset;*uart_offset &#61; (1 << 31);return ret;
}
VGACore.scala
可以看到&#xff0c;此处利用 object VGAConfig
有效减少硬编码&#xff0c;实现优雅的参数化。
// Thanks to iBug for its VGA Sources.package vgaimport chisel3._
import chisel3.util._/* hd: Horizontal Visible Area* hf: Horizontal Front Porch* hs: Horizontal Sync Pulse* hb: Horizontal Back Porch* vd: Vertical Visible Area* vf: Vertical Front Porch* vs: Vertical Sync Pulse* vb: Vertical Back Porch*/object VGAConfig {val config &#61; Map(// | hd | hf | hs | hb | vd | vf | vs | vb |"640x480&#64;60" -> List( 640 , 16 , 96 , 48 , 480 , 10 , 2 , 31 ),// Not widely supported, at 85Hz"720x400&#64;85" -> List( 720 , 36 , 72 , 108 , 400 , 1 , 3 , 42 ),"720x400&#64;70" -> List( 720 , 15 , 108 , 51 , 400 , 11 , 2 , 32 ),"800x600&#64;60" -> List( 800 , 40 , 128 , 88 , 600 , 1 , 4 , 23 ),"800x600&#64;72" -> List( 800 , 56 , 120 , 64 , 600 , 37 , 6 , 23 ),"1024x768&#64;60" -> List( 1024 , 24 , 136 , 160 , 768 , 3 , 6 , 29 ))val refresh_freq &#61; Map("640x480&#64;60" -> 25175000, // 25.175 MHz Pixel Freq"720x400&#64;85" -> 35500000, // 35.500 MHz Pixel Freq"720x400&#64;70" -> 28322000,"800x600&#64;60" -> 40000000, // 40.000 MHz Pixel Freq"800x600&#64;72" -> 50000000, // 50.000 MHz Pixel Freq"1024x768&#64;60" -> 65000000 // 65.000 MHz Pixel Freq)val mode_selected &#61; "720x400&#64;70"
}class VGASig extends Bundle {val r &#61; Output(UInt(4.W))val g &#61; Output(UInt(4.W))val b &#61; Output(UInt(4.W))val hsync &#61; Output(Bool())val vsync &#61; Output(Bool())
}class VGACore extends Module {val io &#61; IO(new Bundle {val row &#61; Output(UInt(32.W))val col &#61; Output(UInt(32.W))val ready &#61; Output(Bool())// Indicate that in next cycle// ready will assertval pre_ready &#61; Output(Bool())val color &#61; Input(UInt(12.W))val sig &#61; new VGASig()})val cfg_list &#61; VGAConfig.config(VGAConfig.mode_selected)val hd &#61; cfg_list(0).U(32.W) // Must have this, or compr will be buggyval hf &#61; cfg_list(1).U(32.W) // for h_tick >&#61; hs &#43; hb it&#39;ll do val hs &#61; cfg_list(2).U(32.W)val hb &#61; cfg_list(3).U(32.W)val vd &#61; cfg_list(4).U(32.W)val vf &#61; cfg_list(5).U(32.W)val vs &#61; cfg_list(6).U(32.W)val vb &#61; cfg_list(7).U(32.W)val h_tick &#61; RegInit(0.U(32.W))val v_tick &#61; RegInit(0.U(32.W))val h_max &#61; hs &#43; hb &#43; hd &#43; hfval v_max &#61; vs &#43; vb &#43; vd &#43; vf// Layered counterwhen (h_tick >&#61; h_max - 1.U) {h_tick :&#61; 0.Uwhen (v_tick >&#61; v_max - 1.U) {v_tick :&#61; 0.U} .otherwise {v_tick :&#61; v_tick &#43; 1.U}} .otherwise {h_tick :&#61; h_tick &#43; 1.U}// 0 ~ hs&#43;hb-1 &#61; total hs&#43;hb cyclesio.ready :&#61; (h_tick >&#61; hs &#43; hb) && (h_tick < hs &#43; hb &#43; hd) && (v_tick >&#61; vs &#43; vb) && (v_tick < vs &#43; vb &#43; vd)io.pre_ready :&#61; (h_tick >&#61; hs &#43; hb - 1.U) && (h_tick < hs &#43; hb &#43; hd - 1.U) && (v_tick >&#61; vs &#43; vb - 1.U) && (v_tick < vs &#43; vb &#43; vd - 1.U)io.sig.r :&#61; Mux(io.ready, io.color(3,0), 0.U)io.sig.g :&#61; Mux(io.ready, io.color(7,4), 0.U)io.sig.b :&#61; Mux(io.ready, io.color(11,8), 0.U)io.col :&#61; h_tick - hs - hb // BUG!!io.row :&#61; v_tick - vs - vbio.sig.hsync :&#61; h_tick >&#61; hsio.sig.vsync :&#61; v_tick >&#61; vs
}
MMap.scala
package multicpuimport chisel3._
import chisel3.util._import vga._// // Datapath perspective
// class MemPort extends Bundle {
// val mem_rdata &#61; Input(UInt(32.W))
// val mem_addr &#61; Output(UInt(32.W))
// val mem_wdata &#61; Output(UInt(32.W))
// val mem_wen &#61; Output(Bool())// // Extra reading port for debugging
// val mem_addr2 &#61; Output(UInt(32.W))
// val mem_rdata2 &#61; Input(UInt(32.W))
// }class MemCellPort extends Bundle {val mem_rdata &#61; Input(UInt(32.W))val mem_wdata &#61; Output(UInt(32.W))val mem_wen &#61; Output(Bool())
}class VRamPort extends Bundle {val mem_addr &#61; Output(UInt(32.W))val mem_wdata &#61; Output(UInt(16.W))val mem_wen &#61; Output(Bool())
}
// 0 ~ 3FF(1023) : Prog Mem, 1024 bytes (as 128 Words)
// 3000(12288) ~ 4F40(20288) : Disp Mem, 80*25 words (but upper 2 byte is always zero)
// FF00(65280) : UART Mem
class MMap extends Module {val io &#61; IO(new Bundle {// MMap -> Datapathval mmap_port &#61; Flipped(new MemPort)// Mem -> MMapval mem_port &#61; new MemPort// Uart -> MMapval uart_port &#61; new MemCellPort// VRam -> MMapval vram_port &#61; new VRamPort})val addr_in_mem &#61; ((io.mmap_port.mem_addr >&#61; 0.U) && (io.mmap_port.mem_addr < 1024.U))val addr_in_uart &#61; (io.mmap_port.mem_addr &#61;&#61;&#61; 65280.U)val addr_in_vram &#61; (io.mmap_port.mem_addr >&#61; 12288.U) && (io.mmap_port.mem_addr < 20289.U)io.mmap_port.mem_rdata :&#61; MuxCase(0.U, Seq(addr_in_mem -> io.mem_port.mem_rdata,addr_in_uart -> io.uart_port.mem_rdata,addr_in_vram -> 0.U))//io.mem_port.mem_rdataio.mem_port.mem_addr :&#61; io.mmap_port.mem_addrio.mem_port.mem_wdata :&#61; io.mmap_port.mem_wdataio.mem_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_mem)io.mem_port.mem_addr2 :&#61; io.mmap_port.mem_addr2io.mmap_port.mem_rdata2 :&#61; io.mem_port.mem_rdata2//io.uart_port.mem_rdataio.uart_port.mem_wdata :&#61; io.mmap_port.mem_wdataio.uart_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_uart)io.vram_port.mem_addr :&#61; (io.mmap_port.mem_addr - 12288.U) >> 2io.vram_port.mem_wdata :&#61; io.mmap_port.mem_wdata(15,0)io.vram_port.mem_wen :&#61; (io.mmap_port.mem_wen && addr_in_vram)
}
其余未尽事宜请参见压缩包内源码。
上面是全部导到 Vivado 后的仿真结果。限于篇幅&#xff0c;下面 VideoSys 和 CPU Sig 没有截出。
请参见同压缩包下的视频&#xff0c;非常清晰的展现了功能。
O(n^3)
&#xff09;&#xff1b;诊断这种问题的时候&#xff0c;就应该尝试注释代码 &#43; 记录时间&#xff0c;会得到很直观的认识。