D6 D阶段流片准备

流程处于内测阶段

目前本页面处于内测阶段, 流程可能会有所更新, 请保持关注.

建议创建新分支

如果你不打算参加D阶段流片, 你可以直接跳过本页面的内容.

由于D阶段流片的准备工作需要对NPC进行调整, 这些调整可能会在某种程度上提升C阶段的实现难度, 因此我们建议你在ysyx-workbench中创建一个新分支, 并在新分支中进行代码的调整. 如果你想继续进行C阶段的开发工作, 可以切换回旧分支.

此外, 为了达到流片的基本功能要求, 我们需要引用C阶段和B阶段中的少数学习内容.

在NPC中添加CSR

阅读C阶段异常处理和RT-Thread在NPC中添加CSR这一小节的内容, 并完成其中的必做题, 添加mcycle和学号CSR.

实现总线

  1. 阅读B阶段总线总线 - 硬件模块间的通信协议这一小节的内容, 学习总线的基本概念.
  2. 阅读B阶段总线系统总线这一小节的内容, 并完成其中的必做题, 在NPC中实现支持有效信号的SimpleBus总线. 直到你看到如下提示框:

温馨提示

如果你是为了D阶段流片而学习总线, 学习到此处即可.

接入ysyxSoC

我们提供一个可以在verilator上运行的SoC环境, 称为ysyxSoC. 你可以SoC环境中测试你的NPC, 从而更早地暴露NPC的问题, 帮助你进行调试.

获取ysyxSoC的代码

你需要克隆ysyxSoC在新窗口中打开项目:

cd ysyx-workbench
git clone git@github.com:OSCPU/ysyxSoC.git

需要注意的是, ysyxSoC与最终流片使用的SoC仍有一定差异. 因此, 通过ysyxSoC的测试并不代表最终也能通过流片SoC仿真环境的测试. 但即使这样, 也可以借助ysyxSoC项目提前暴露一部分问题.

ysyxSoC项目中包含较多细节, 我们会在B阶段再深入学习SoC的相关内容. 我们为D阶段的流片专门准备了一份经过简化的SoC代码, 包含如下设备:

设备地址空间
UART165500x1000_0000~0x1000_0fff
SPI master0x1000_1000~0x1000_1fff
Flash0x3000_0000~0x3fff_ffff
SDRAM0x8000_0000~0x81ff_ffff
Reverse其他

这份简化后的SoC代码中还包含一个转接桥模块, 来将SimpleBus转换成AXI. 有了这个转接桥, 就可以将NPC接入到AXI接口的SoC中, 并与各种设备进行通信.

接入ysyxSoC

依次按照以下步骤将NPC接入ysyxSoC:

  1. 调整NPC顶层接口, 使其与ysyxSoC/ready-to-run/D-stage/cpu-interface.md中的接口命名规范完全一致, 包括信号方向, 命名和数据位宽
    • 其中io_lsu_size用于与外设进行通信, 由LSU根据访存指令的数据位宽来设置: 位宽为1字节时设置为2'b00, 为2字节时设置为2'b01, 为4字节时设置为2'b10
  2. 将NPC中的PC复位值修改为0x3000_0000, 即复位后从Flash取指令
  3. ysyxSoC/perip目录及其子目录下的所有.v文件加入verilator的Verilog文件列表
  4. ysyxSoC/perip/uart16550/rtlysyxSoC/perip/spi/rtl两个目录加入verilator的include搜索路径中
    • 具体如何加入, 请RTFM(man verilator或verilator的官方手册)
      • 如果你从来没有查阅过verilator有哪些选项, 我们建议你趁这次机会认真阅读一下手册中的argument summary, 你很可能会发现一些新的宝藏
  5. 在verilator编译选项中添加--timescale "1ns/1ns"--no-timing
  6. ysyxSoCFull模块(在ysyxSoC/ready-to-run/D-stage/ysyxSoCFull.v中定义)设置为verilator仿真的顶层模块
    • 如果你不知道如何加入, RTFM
  7. ysyxSoC/ready-to-run/D-stage/ysyxSoCFull.v中的ysyx_00000000模块名修改为你的学号, 例如ysyx_26000001
  8. 在仿真的cpp文件中加入如下内容, 用于解决链接时找不到flash_read的问题
    extern "C" void flash_read(int32_t addr, int32_t *data) { assert(0); }
    
  9. 通过verilator编译出仿真可执行文件
    • 如果你遇到了组合回环的错误, 请自行修改你的RTL代码
  10. 尝试开始仿真, 你将观察到代码触发了flash_read()中的assert(0)错误, 我们接下来再解决这个问题
    • 如果没有触发这个错误, 请检查总线的实现

在ysyxSoC上运行程序

在仿真的cpp文件中定义一个16MB的数组作为Flash存储器, 然后实现flash_read()函数, 根据地址addr返回Flash存储器中的相应数据. 然后, 将NPC需要运行的.bin文件读入到Flash存储器中, 从而让NPC从Flash中取出程序的第一条指令.

我们准备了一个hello程序, 位于ysyxSoC/ready-to-run/D-stage/hello-minirv-ysyxsoc.bin, 你可以按照上述方式让NPC运行这个程序. 如果你的实现正确, 你将看到程序输出Hello World!后陷入死循环.

运行更多程序

针对ysyxSoC, 我们只提供了一个hello程序. 接下来, 你需要自己将程序编译到ysyxSoC上并运行.

在真实的计算机中, 一般的存储器是易失存储器(volatile memory), 例如SRAM和DRAM, 它们在上电时并没有存放有效数据. 如果上电后CPU直接从内存中读取指令执行, 存储器读出什么数据是未定义的, 因此整个系统的行为也是未定义的, 从而无法让CPU执行预期的程序. 因此, 需要使用一种非易失存储器(non-volatile memory)来存放最初的程序, 使其内容能在断电时保持, 并在上电时能让CPU马上从中取出指令.

在ysyxSoC中, 非易失存储器由Flash充当, 易失存储器由SDRAM充当. 接入ysyxSoC后, NPC复位后将从位于0x30000000的Flash中取出第一条指令. 但由于对处理器来说, Flash无法直接通过访存指令写入, 因此通常需要将程序从Flash中加载到可写入的非易失存储器(如SDRAM)后再执行.

上述的hello程序已经包含了这个过程. 事实上, 上述的.bin文件中还包含一个加载器(loader)程序, 它位于地址0x30000000的位置. NPC执行这个.bin文件时, 首先执行的是加载器. 加载器的工作是将真正的hello程序从Flash中加载到位于0x80000000的SDRAM中, 因此你会看到程序通过串口输出loading to memory region [0x80000000, 0x80048650)的信息. 加载完成后, NPC将会跳转到hello程序的入口, 即0x80000000, 然后取出hello程序的指令并执行.

由于加载器在Flash中运行, 而Flash对NPC来说无法通过访存指令写入, 因此在加载器进行链接的时候需要进行一些额外的处理. 为了减轻大家的负担, 目前我们不要求大家实现加载器, 而是复用上述.bin文件中的加载器. 我们提供了一个脚本, 位于ysyxSoC/ready-to-run/D-stage/gen.sh, 用于将其他程序和这个加载器拼接成一个新的.bin文件. 通过这种方式, 你就可以在ysyxSoC中加载并运行其他程序了.

更新minirv的构建过程

我们在2025/08/30 22:30:00更新了minirv的构建过程. 如果你在上述时间之前获取abstract-machine的代码, 请按照以下说明更新文件:

--- a/tools/minirv/minirv-common.sh
+++ b/tools/minirv/minirv-common.sh
@@ -29,3 +29,5 @@

 src_dir=`dirname $src
 riscv64-linux-gnu-gcc -I$src_dir $flags -D_LUT_BIN_PATH=\"$lut_bin_path\" -Wno-trigraphs -c -o $dst $dst_S
+
+/bin/echo -ne '\x80' | dd of=$dst bs=1 seek=39 count=1 conv=notrunc 2> /dev/null

脚本的用法示例如下:

cd am-kernels/tests/cpu-tests
make ARCH=minirv-npc ALL=dummy
cd ysyxSoC/ready-to-run/D-stage
bash gen.sh am-kernels/tests/cpu-tests/build/dummy-minirv-npc.elf
ls new.bin

运行其他程序

根据上文的介绍, 尝试在ysyxSoC中运行dummy程序. 由于dummy程序没有输出, 加载器跳转到dummy程序后将会输出HIT GOOD TRAP的信息.

运行hello程序

用类似的方式在ysyxSoC中运行hello程序, 你将看到NPC输出若干字符. 不过你发现NPC并没有输出全部字符, 尽管这并不是我们所期望的, 但目前来说这是预期行为, 我们接下来将会修复这个问题.

在真实的芯片中, 程序在通过串口输出之前, 还需要对串口进行初始化. 具体地, 初始化过程需要设置串口收发参数, 包括波特率, 字符长度, 是否带校验位, 停止位的位宽等.

  • 波特率指每秒传送的字符数. 不过通常并非直接在寄存器中设置波特率, 而是设置一个与波特率成反比的除数: 除数越小, 波特率越大, 传输速率越快, 但受电气特性的影响, 误码率也越高, 字符传送成功的概率越低; 相反, 除数越大, 波特率越小, 传输速率越慢, 软件等待的时间也越长. 除数的值还与串口控制器的工作频率有关, 后者即串口每秒传送的比特数, 可RTFM了解两者的具体关系. 串口的手册位于ysyxSoC/perip/uart16550/doc/UART_spec.pdf.
  • 串口收发端的参数配置要完全一致, 才能正确发送和接收字符. 通常用形如115200 8N1等方式来描述一组参数配置, 它表示波特率是115200, 字符长度是8位, 不带校验位, 1位停止位.

正确实现串口的初始化

你需要在TRM中添加代码, 设置串口的除数寄存器. 由于ysyxSoC本质上还是一个仿真环境, 没有串口接收端, 也没有电气特性的概念, 因此目前可随意设置上述除数, 不必担心误码率的问题. 当然, 在真实的芯片中, 除数寄存器的设置是需要仔细考量的.

具体如何设置除数, 你可以RTFM了解UART IP的功能, 也可以RTFSC, 结合UART16550寄存器的RTL实现, 帮助你理解设置的除数如何工作.

如果除数寄存器设置得足够小, 你会观察到hello程序多输出了一些字符, 但仍然会出现字符丢失的情况. 为了解决这个问题, 我们需要在向串口写入字符之前, 保证其发送队列一定有空闲位置可以写入. 这可以通过查询串口的状态寄存器实现: 软件可以轮询相关寄存器, 直到确保写入的字符不会丢失为止.

输出前轮询串口的状态寄存器

你需要修改putch()的代码, 在输出前先查询串口发送队列的情况. 具体如何查询, 同样地, 你可以RTFM了解UART IP的功能, 也可以RTFSC, 结合UART16550寄存器的RTL实现, 帮助你理解相关的功能.

实现后, 重新将hello程序编译到ysyxSoC上并运行, 你会发现hello程序可以正确输出所有字符.

最后, 你还需要修改AM时钟的实现. 具体地, 你需要访问mcycle, 并通过某个系数将计数器的值换算成时间. 将来在真实的处理器芯片上运行程序时, 这个系数和处理器的工作频率相关. 不过, 在仿真环境中并没有频率的概念, 而且仿真环境中时间流逝的速率和真实时间并不一致, 因此你可以选择一个合适的系数, 让程序读出的时钟流逝速率接近真实时间. 由于这个换算过程是在软件中完成的, 将来在真实的处理器芯片上运行时, 可以调整系数并重新编译程序.

运行时钟测试

修改AM时钟的实现, 并运行am-testsreal-time clock test测试, 使得测试程序输出信息的间隔接近1秒.

运行字符版本的超级玛丽

尝试在ysyxSoC中运行字符版本的超级玛丽.

待续未完

综合

将代码上传到ECOS Studio云平台

最近更新时间:
贡献者: Zihao Yu