在 Arty A7 上用 LiteX 和 VexRiscv 跑 Linux

litex 安装 litex 安装过程按照 https://github.com/enjoy-digital/litex/wiki/Installation 进行,由于需要 pip install,建议用 venv 来开一个干净的环境: python3 -m venv venv source venv/bin/activate cd litex ./litex_setup.py --init --install 构建 bitstream litex-boards 已经内建了 Arty A7 的支持,直接运行下列命令,就可以得到 bitstream: python3 -m litex_boards.targets.digilent_arty --build --with-ethernet 这样就可以在 build/digilent_arty/gateware 目录下找到 bitstream。可以通过命令行参数来自定义需要的功能,详见 https://github.com/litex-hub/litex-boards/blob/f5e51d72bca6ed0325c1213791a78362326002f8/litex_boards/targets/digilent_arty.py#L162-L180。 如果想切换 CPU 为 Rocket Chip 的话,克隆并安装 https://github.com/litex-hub/pythondata-cpu-rocket,添加 --cpu-type rocket --cpu-variant small 参数即可。 下载 bitstream 最后,连接 microUSB 和网线到电脑,然后下载 bitstream: openFPGALoader -b arty digilent_arty.bit screen /dev/tty.usbserial-XXXXXXXXXXXXX 115200 就可以看到 litex 的输出: --=============== SoC ==================-- CPU: VexRiscv @ 100MHz BUS: WISHBONE 32-bit @ 4GiB CSR: 32-bit data ROM: 128.

Read More

C/C++ 数参数个数的特别方法

背景 群友上个月提了一个未知来源问题: 实现一个你自己的 printf(int, ...) 函数,该函数包含可变参数。为简便期间,假设所有参数均为 int 类型。 第一个参数是一个普通参数,不表示后续可变参数的数目 在 printf 中逐个输出所有传入的整数值(可使用系统自带的 kprintf 实现输出) 思考如何判定参数结束,是否有副作用 va_args 我们知道,传统的处理可变参数的方法是 va_args,但是它无法知道传入了多少参数,而要像 POSIX printf 那样,解析 format 参数,然后一个一个去取。 所以问题的关键是,如何获取参数的个数?一个思路是宏,尝试用宏的魔法来计算出参数个数,这个方法可能是可以的,但是没有深究。另一个思路是利用 ABI 的特点,例如 i386 上参数是通过栈传递的,那或许可以在栈上找到所有的 int,但是问题是无法确认参数在哪里结束。 __builtin_va_arg_pack_len 今天,另一位群友发了一个链接:https://gcc.gnu.org/onlinedocs/gcc/Constructing-Calls.html#Constructing-Calls,讲述了 GCC 中一些特别的 builtin 函数,用于函数调用相关的魔法,其中一段描述吸引了我的眼球: Built-in Function: int __builtin_va_arg_pack_len () This built-in function returns the number of anonymous arguments of an inline function. It can be used only in inline functions that are always inlined, never compiled as a separate function, such as those using __attribute__ ((__always_inline__)) or __attribute__ ((__gnu_inline__)) extern inline functions.

Read More

SCO OpenServer 6.0.0 虚拟机安装

安装过程 首先从 https://www.sco.com/support/update/download/product.php?pfid=12&prid=20 下载 SCO OpenServer 的安装 ISO。尝试过用 QEMU 启动,但是会卡在无法读取硬盘的错误上。 最后使用 VirtualBox 7.0.6 成功启动,注意创建虚拟机的时候不要给太多内存,例如 4GB 就起不来,2GB 可以。硬盘我也只给了 4GB 的空间。 安装过程中会询问 License number 和 License code,可以选择使用 Evaluation License,或者使用下面参考文档中提供的 License。按照流程一直走就可以了。如果重启出现无法 mount root 的问题,就 poweroff 再开机。 参考文档 本博客参考了以下文档中的命令: https://virtuallyfun.com/2020/11/21/fun-with-openserver-6-and-mergepro/

Read More

UnixWare 7.1.4 虚拟机安装

安装过程 在 https://www.sco.com/support/update/download/product.php?pfid=1&prid=6 可以看到 UnixWare 7.1.4 的相关下载,其中首先要下载 UnixWare 的安装 ISO:https://www.sco.com/support/update/download/release.php?rid=346,尝试过用 QEMU 启动,会遇到找不到 CD-ROM 的问题,虽然通过设置 ATAPI_DMA_DISABLE=YES 解决了,但是又遇到了找不到硬盘的问题。 最后换成了 VirtualBox 7.0.6。用 VirtualBox 创建虚拟机的时候,不要给太多内存,4GB 就会无法启动,2GB 可以,硬盘也不要给太多,4GB 就足够。 剩下就是按照安装界面一路默认即可,License 可以选择 Defer,使用 Evaluation License。 关机以后,修改启动顺序,把硬盘放到 CD 前,然后启动,就可以进入系统了。如果重启出现无法 mount root 的问题,就 poweroff 再开机。 参考文档 本博客参考了以下文档中的命令: https://virtuallyfun.com/2018/01/31/revisiting-a-unixware-7-1-1-install-on-qemu-kvm/

Read More

SCO OpenServer 5.0.7 虚拟机安装

安装过程 首先从 https://www.sco.com/support/update/download/release.php?rid=218 下载 SCO OpenServer 的安装 ISO 和从 https://www.sco.com/support/update/download/release.php?rid=187 下载 Supplement CD 5 ISO,然后用 QEMU 启动,这次需要用图形界面: qemu-system-i386 -accel kvm -m 16384 -serial mon:stdio -drive file=sco-hdd.qcow2,if=ide -cdrom ../../ISOs/OpenServer-5.0.7Hw-10Jun05_1800.iso 安装过程中会询问 License number 和 License code,按照 https://virtuallyfun.com/2020/11/03/fun-with-openserver-and-merge/ 进行输入。 安装的时候,在 hard disk setup 那一步,记得关掉 bad tracking,否则会把整个盘扫一遍,我一开始建了 20GB 的 qcow2,结果这一步跑了一晚上,而且把 qcow2 撑满了。 安装后,重新启动,这次打开网络,同时挂载 Supplement CD 5 ISO: qemu-system-i386 -accel kvm -m 16384 -serial chardev:mouse -drive file=sco-hdd.qcow2,if=ide -cdrom osr507suppcd5.iso -net nic -net tap,script=no,ifname=tap0 -chardev msmouse,id=mouse 启动以后,运行 custom 命令,然后从 CD-ROM 安装 Graphics and NIC Drivers。我尝试了安装 Maintenance Pack 5,但是启动以后会找不到硬盘,只好恢复之前的 qcow2 备份。可能是缺少了运行 /etc/conf/cf.

Read More

AIX 7.2 虚拟机安装

安装过程 宿主机环境是 Debian bookworm,不需要像其他教程那样自己编译 qemu,直接 apt install 即可。 通过 google 可以搜索到 AIX 7.2 的 ISO,下载第一个 ISO 到本地,然后在 QEMU 中启动安装镜像: qemu-img create -f qcow2 aix-hdd.qcow2 20G qemu-system-ppc64 -cpu POWER8 -machine pseries -m 16384 -serial mon:stdio -drive file=aix-hdd.qcow2,if=none,id=drive-virtio-disk0 -device virtio-scsi-pci,id=scsi -device scsi-hd,drive=drive-virtio-disk0 -cdrom aix_7200-04-02-2027_1of2_072020.iso -prom-env boot-command='boot cdrom:\ppc\chrp\bootfile.exe' -display none 进去以后,耐心等待,直到进入安装界面,按照提示进行安装,建议安装上 SSH Server,关掉图形界面,这样安装会比较快。安装需要几十分钟,安装完成后会进入 bootloop,关掉 QEMU。接着,准备好网络: sudo ip tuntap add tap0 mode tap sudo ip link set tap0 up sudo ip a add 10.0.2.15/24 dev tap0 再启动虚拟机,注意启动选项修改了,并且多了网络的配置:

Read More

RAM 读写冲突

背景 在 FPGA 或者 ASIC 中,通常都需要使用 RAM,通过读口、写口或者读写口来进行访问。常见的配置有单读写口(1RW),一读一写(1R1W)等等,读口通常有 1 个周期的延时。那么,如果在同一个周期内,读口和写口访问了同一个地址,会发生什么呢?可能会想到几种情况: 读和写都失败,读出的数据未定义,数据没写进去 数据写进去了,读出的数据未定义 数据写进去了,读出了写之前的旧数据 数据写进去了,读出了同一个周期写入的新数据 下面以具体的例子来看看,实际情况是什么样子。 Xilinx FPGA 首先测试的是 Xilinx FPGA 上的 RAM,测试的对象是 XPM,统一设置读延迟为一个周期,使用 Vivado 仿真。 一读一写 首先测试一读一写,也就是 xpm_memory_sdpram 模块。模块支持三种模式:NO_CHANGE(默认值)、READ_FIRST 和 WRITE_FIRST,因此我例化了三份,输入一样的信号,设置为三种不同的模式,然后比较输出结果。为了简化,读写使用一个时钟。下面是测试的波形: 图中第一个周期向地址 0 写入了 1111,然后第三个周期同时读写地址 0 的数据,此时 NO_CHANGE 和 WRITE_FIRST 两种模式中,写入成功,读取失败;READ_FIRST 模式读取成功,并且读取的是写入之前的数据。第四个周期时,读写没有出现冲突,三种模式都可以读出写入的新数据。 这有些出乎我的意料:之前在很多地方用过 XPM,但是都没考虑过读写地址相同的情况,而且默认设置(NO_CHANGE)下,输出结果是不确定的。实际上这个行为在 PG058 Block Memory Generator 里面提到了: Synchronous Write-Read Collisions: A synchronous Write-Read collision might occur if a port attempts to Write a memory location and the other port reads the same location.

Read More

firtool 尝试

背景 Chisel 3.6 很快就要发布了(目前最新版本是 3.6.0-RC2),这个大版本的主要更新内容就是引入了 CIRCT 的 firtool 作为 FIRRTL 到 Verilog 的转换流程: The primary change in Chisel v3.6.0 is the transition from the Scala FIRRTL Compiler to the new MLIR FIRRTL Compiler. This will have a minimal impact on typical Chisel user APIs but a large impact on custom compiler flows. For more information, please see the ROADMAP. 因此提前测试一下 firtool,看看其和 Scala FIRRTL Compiler 有哪些区别,是否有更好的输出。 使用 firtool 使用 firtool 有两种方法:

Read More

单核处理器的协同仿真

背景 今年的龙芯杯又开始报名了,我来写一篇关于协同仿真(cosim)的博客蹭蹭热度。下面的内容参考了一些已有的协同仿真的框架,例如 ibex co-sim 和 OpenXiangShan/difftest。 协同仿真 RTL 层次的协同仿真可以做不同层次的,这里讨论的是指令提交层次,具体来讲,就是把 CPU 和一个模拟器放在一起协同仿真,检查每条指令执行完以后的状态是否一致。基于代码样例的测试虽然可以覆盖很多情况,但是如果出了错误,报错的地方不一定是出现问题的地方,有些时候就需要往回找很久,才能找到刚出现问题的地方。软件上,大家经常苦于内存错误,经常找不到刚出现溢出的地方,所以要用 valgrind 或者 asan 等工具来直接定位第一次出错的地方。硬件上也是类似,为了精确定位到出错的波形,可以用 cosim。 cosim 是怎么工作的呢?模拟器是软件实现的,它原子地执行一条条指令,同时记录下当前的状态,例如寄存器的取值、内存的状态等等。如果可以让 CPU 和模拟器锁步运行,也就是 CPU 执行一条指令,模拟器执行一条指令,然后比对状态,一旦出现不一致,就直接报错。但实际上 CPU 可能会更加复杂,因为它指令的执行拆分成了很多部分,需要针对流水线进行一些修改,使得它可以生成一个匹配模拟器的原子的执行流。 整体的工作流程如下: 选择一个模拟器,自己写或者使用一个现成的。考虑到模拟器实现的功能和 CPU 不一定一致,有时候需要修改模拟器的源码,所以可以考虑使用一些现成的开源软件,如果是为了 cosim 设计的就更好了。 找到模拟器的单步执行接口,并且让模拟器可以把内部状态暴露出来。这一步可能需要修改源代码。 修改 RTL,把指令的提交信息、寄存器堆的内容通过一些方法传递出来。 修改仿真顶层,每当指令提交的时候,单步执行模拟器,然后比对双方的状态。 模拟器 选择模拟器,要根据你所实现的指令集来选择。下面以 Spike 为例,用来和 RISC-V CPU 进行协同仿真。spike 实现了比较完整的 RISC-V 指令集,并且以库的形式提供了它的 API,但还需要一些修改,让它更加适合协同仿真。这一部分参考了 ibex co-sim的文档。 首先,spike 提供了 step 函数,就是我们想要的单步执行。但是,spike 的 step 在遇到异常或者中断的时候也会返回,但实际上在处理器一侧,通常异常是单独处理的,所以这时候就要修改 spike 的 step 函数,如果遇到异常了,继续执行,直到执行了一条指令为止。与此同时,spike 没有记录最后一次执行的指令的 pc,只记录了下一个 PC,那么在发生异常的时候,就不会记录异常处理的第一条指令的 PC,这里也要进行针对性的修改。 state.last_inst_pc = pc; pc = execute_insn_logged(this, pc, fetch); advance_pc(); 做了这些修改以后,就足够在 cosim 中运行一些简单的程序了。

Read More

软硬件队列接口

背景 在网卡等场景下,经常会需要在软硬件之间传输大量的数据,通常的方法是建立循环队列,例如 H2C(Host to Chip)方向,是 Host 作为 Producer 增加数据到队尾,Chip 作为 Consumer 从队头读取数据。由于每次传输的数据不定长,为了方便,队列的项是一个定长的 Descriptor,Descriptor 指向了数据的地址。但具体的细节,不同的实现还不太一样。下面逐个案例进行分析。 AXI DMA 文档:https://docs.xilinx.com/r/en-US/pg021_axi_dma 如果在 Xilinx FPGA 上使用过以太网,那大概率会接触到 AXI DMA 这个 IP,它负责把以太网 MAC 的 AXI Stream 数据用 DMA 的形式通过内存来与操作系统交互。 发送队列 它的收和发各是一个队列,首先来看发送队列: 发送队列由一个头指针(MM2S_CURDESC)和一个尾指针定义(MM2S_TAILDESC),指针指向的是一个 Scatter Gather Descriptor,Descriptor 的内容包括: NXTDESC:队列下一项的地址 BUFFER_ADDRESS:要传输的数据的地址 CONTROL:控制信息 STATUS:状态信息 APP0 to APP4:附带的信息 可见这个发送队列实际上是一个链表: struct axi_dma_desc { struct axi_dma_desc *nxtdesc; void *buffer_address; // omitted }; 当 MM2S_TAILDESC 被更新的时候,硬件会从 CURDESC 开始逐个 Descriptor 处理,直到遇到 TAILDESC 为止: // when taildesc is changed void taildesc_changed() { do { dma_send(curdesc); if (curdesc !

Read More