基于STM32+ICE40电赛训练平台的RISCV软核移植
本项目使用硬禾课堂基于ICE40UP5K FPGA以及STM32G031设计的电赛训练平台,实现了一款RISCV软核FemtoRV的移植,并成功运行由汇编代码编写的LED流水灯程序以及ADC-DAC直通程序。
标签
嵌入式系统
FPGA
数字逻辑
2023寒假在家练
电子卷卷怪
更新2023-03-28
南京大学
594

一、项目概述

   本项目使用硬禾课堂基于ICE40UP5K FPGA与STM32G031设计的电赛训练平台,以及配套的扩展版,实现了开源RISCV软核FemtoRV的移植。移植的软核能够运行示例的流水灯程序,以及自己使用汇编代码编写的ADC-DAC直通程序。

 

二、项目思路与结构

   本项目在Ubuntu20.04(WSL2)环境下开发,使用了包含yosys,nextpnr-ice40在内的icestorm开源项目工具链,以及bin2rbt,来完成Verilog源码的编译和烧录文件的生成;使用了FemtoRV项目配套的RISCV交叉编译工具链对代码进行交叉编译,并最终放在FPGA的BRAM中运行。

   本项目移植的软核源码是FemtoRV的step20.v,采用状态机实现,支持RV32I指令集;本项目的测试程序使用汇编语言编写。

 

三、实现过程

1.开源工具链安装

   yosys,nextpnr-ice40,icestorm的安装都很简单,只需要从github上拷贝源码,在本地编译之后就可以使用。bin2rbt是群里的moo同学编写的将bin文件转换为rbt文件的工具。由于WSL2在连接USB时问题比较多,且核心板的设计是通过上位机连接虚拟U盘,而不是上位机直接连接FPGA芯片的方式来下载的,因此不能使用iceprog工具来下载。

   生成rbt文件的方式有两种,一种是在IDE下直接配置生成rbt文件的选项,一种是开源工具链生成bin文件之后,再转为rbt文件。

 

2.开源项目

   FemtoRV的作者团队为RISCV的入门级爱好者(像我)提供了一套非常完善且通俗易懂的教程。该教程除了指导读者如何做移植和平台适配,还提供了一整套使我震惊的“FROM_BLINKER_TO_RISCV”讲义,作者团队在其中以最易懂和风趣的方式(p.s. 这样的英文读着是真爽),讲述了他们从一个流水灯模块开始逐步写成一个RISCV软核的过程。

   因此,本次选择该开源项目进行移植,一方面是看中其教程的完整,另一方面也是想要从一个“最小系统”的实例入手,对RISCV软核的设计有一个整体的认识。

 

3.具体实现

   该项目可以直接适配到IceStick和IceBreaker等开发板,但在本平台上需要做些适配。

   第一,先从教程提供的开发步骤入手,明确项目结构。

   以流水灯例程为例,最简单的编译命令(在该项目提供的环境下)为:

cd FROM_BINKER_TO_RISCV/FIRMWARE
make blinker.bram.hex
cd ..
BOARDS/run_xxx.sh step20.v

   其中FIRMWARE文件夹下的Makefile文件提供了对riscv交叉编译工具链的封装,可以直接对blink.S文件进行修改,而编译命令不用变。

   再观察BOARDS文件夹下的run_xxx.sh文件(以icebreaker为例,它采用的是与本平台一样的芯片):

PROJECTNAME=SOC
BOARD=icebreaker
BOARD_FREQ=12
CPU_FREQ=20
FPGA_VARIANT=up5k
FPGA_PACKAGE=sg48
VERILOGS=$1
yosys -q -DICE_BREAKER -DNEGATIVE_RESET -DBOARD_FREQ=$BOARD_FREQ -DCPU_FREQ=$CPU_FREQ -p "synth_ice40 -abc9 -device u -dsp -top $PROJECTNAME -json $PROJECTNAME.json" $VERILOGS  || exit
nextpnr-ice40 --force --json $PROJECTNAME.json --pcf BOARDS/$BOARD.pcf --asc $PROJECTNAME.asc --freq $BOARD_FREQ --$FPGA_VARIANT --package $FPGA_PACKAGE --pcf-allow-unconstrained || exit
icetime -p BOARDS/$BOARD.pcf -P $FPGA_PACKAGE -r $PROJECTNAME.timings -d up5k -t $PROJECTNAME.asc
icepack $PROJECTNAME.asc $PROJECTNAME.bin || exit
iceprog $PROJECTNAME.bin || exit
echo DONE.

   这里面其实是对开源工具的顺序调用,唯一存在变数的宏(变量)只有BOARD。进一步讲,只有$BOARD.pcf是因开发板而异的。除此之外,-DICE_BREAKER参数也是需要改变的。由此可见:

   第二,我们需要做的就是编写.pcf文件,该文件里包含了顶层模块输入输出引脚的约束。为此,要关注我们所需用到的引脚的连接关系。

   根据原理图可以找出ADC和DAC与FPGA连接的22个引脚(20数据线+2时钟线)。通过查阅数据手册并观察ADC和DAC模块,可以确定数据引脚的LSB与MSB。

   在直通程序(ADC采样直接bypass到DAC输出)中,还需启用核心板上的RGB_LED,通过它的定时闪烁作为系统正常运行的标志。

   RST复位线可以连接到板子上任意一个带上拉的按键。

   适配后的.pcf文件如下:

set_io CLK 44

set_io SYS_LED[0] 39
set_io SYS_LED[1] 40

set_io ADC[0] 26
set_io ADC[1] 27
set_io ADC[2] 28
set_io ADC[3] 31
set_io ADC[4] 32
set_io ADC[5] 34
set_io ADC[6] 2
set_io ADC[7] 36
set_io ADC[8] 25
set_io ADC[9] 48
set_io ADC_CLK 47

set_io DAC[0] 46
set_io DAC[1] 3
set_io DAC[2] 4
set_io DAC[3] 6
set_io DAC[4] 9
set_io DAC[5] 10
set_io DAC[6] 11
set_io DAC[7] 12
set_io DAC[8] 13
set_io DAC[9] 18
set_io DAC_CLK 45

set_io RESET 43

   至此,已经完成了硬件连接上的适配。

   第三,需要对锁相环进行配置。刚才run_xxx.sh里的-DICE_BREAKER参数实际上在femtoPLL.v文件中生效:

`ifdef PASSTHROUGH_PLL
module femtoPLL #(
 parameter freq = 60
) (
 input  pclk,
 output clk
);
   assign clk = pclk;
endmodule
`else
 `ifdef ICE_STICK
  `include "pll_icestick.v"
 `elsif ICE_BREAKER
  `include "pll_icebreaker.v"
 //......
 `endif
`endif

显然我们也需要生成一个适用于本平台的.v文件,并添加一个对应的宏定义(该宏可以在文件中定义,也可以在命令行中以-D的方式像-DICE_BREAKER这样子传入)。如果不想用锁相环的话也很简单,只需要在命令行中传入参数-DPASSTHROUGH_PLL。FemtoRV项目中就包含一个自动为我们生成对应型号FPGA锁相环.v文件的脚本,但在生成之后我们需要把SB_PLL40_PAD改回SB_PLL40_CORE,把PACKAGEPIN改回REFERENCECLK——而不是像项目中附带的教程中说的那样。

   并且,如果启用了锁相环,则需要将-DCPU_FREQ参数(也就是CPU_FREQ变量)改成锁相环的实际输出频率,否则至少在使用uart模块时会遇到错误。

   第四,总线分析。

   在实现外设挂载时,很重要的一点就是:要去适应和理解作者的思路,这样就能很快摸清代码中信号的层次。并且,应该先理解作者的思路,再去顺着这个思路写自己的模块。

   在step20.v中,作者已经为我们提供了一个挂载uart外设的例子,在该SOC上挂在外设所需了解的一切,在教程和下面这段代码中全都讲清楚了:

  assign mem_wstrb = |mem_wmask;
//......
   localparam IO_UART_DAT_bit  = 1;  // W data to send (8 bits)
   localparam IO_UART_CNTL_bit = 2;  // R status. bit 9: busy sending
//......
   wire uart_valid = isIO & mem_wstrb & mem_wordaddr[IO_UART_DAT_bit];
   wire uart_ready;

   corescore_emitter_uart #(
      .clk_freq_hz(`CPU_FREQ*1000000),
//      .baud_rate(115200)
        .baud_rate(1000000)
   ) UART(
      .i_clk(clk),
      .i_rst(!resetn),
      .i_data(mem_wdata[7:0]),
      .i_valid(uart_valid),
      .o_ready(uart_ready),
      .o_uart_tx(TXD)
   );
   wire [31:0] IO_rdata =
               mem_wordaddr[IO_UART_CNTL_bit] ? { 22'b0, !uart_ready, 9'b0}
                                                                          : 32'b0;
   assign mem_rdata = isRAM ? RAM_rdata :
                              IO_rdata ;

   在我看来,讨论如何挂载一个外设,本质上就是在讨论CPU如何经总线寻址并将数据发到该外设模块。

   在这里,mem_wdata[7:0]显然是32位数据总线的低8位(因为串口就是以Byte为单位的),而valid信号指示着写操作是否对该模块(从机)有效。而valid信号取决于三点(就是assign的那三个信号):当前指令是否在外设区域寻址?当前写数据线是否有效?当前写地址是否针对该外设?(作者在教程中提到,为了避免使用大位宽的比较器,采用独热编码对外设区域0x400000~0x4fffff内的外设进行编址)

   至于读取,作者的逻辑是先判断当前寻址区域是内存还是外设,如果是外设,再看是具体那个外设,并把这个外设的数据route到“外设数据总线”上。

   至此,作者设计的整个总线协议已经非常清晰了。

   这里需要说一句,作者把uart的busy信号(也就是ready)单独引出来,并给他独立安排了一个独热编码地址,这在我看来不够简洁——既然32位的数据线远远没有占满,那么在他设计的这个总线读写协议下,他完全可以赋予一个地址以完全独立的读写行为(就像我挂载ADC外设时做的那样),从而省下一个地址。

第五,外设挂载。

   在了解了作者设计总线的思路之后,就可以挂载我们自己的ADC和DAC驱动模块:

module adc_3pa1030(
        input wire i_clk,
        input wire i_rst,
        input wire [10:0] i_data,
        input wire i_valid,
        output wire o_clk,
        output reg [9:0] o_data
);
wire en;
reg en_r = 1'b0;
assign en = i_rst & i_data[10];
//enable logic
//write 0x01 to enable adc block

always@(posedge i_clk)begin
        en_r <= i_valid ? en : en_r;
end

//o_clk feeds directly to adc chip
assign o_clk = en_r ? i_clk : 1'b0;

always@(negedge i_clk)begin
        o_data <= i_data[9:0];
end

endmodule

在ADC模块中,我采用了同一个地址具有独立读写行为的设计:一方面,i_data[10]连接到mem_wdata[0],这样子的话,向ADC地址写0x01就表示使能ADC;i_data[9:0]连接到3PA1030的数据引脚,这10个引脚和CPU没什么关系。另一方面,o_data[9:0]则被route到外设数据总线,只需在作者原先的代码上做一些修改:

   wire [31:0] IO_rdata =
               mem_wordaddr[IO_UART_CNTL_bit] ? { 22'b0, !uart_ready, 9'b0}
              :mem_wordaddr[IO_ADC_bit]       ? {22'b0,adc_rdata}
                                              : 32'b0;

   此外,由于ADC和DAC的数据均为正沿同步,因此最好在负沿存储和更新数据。

   DAC模块的设计与ADC类似:

module dac_3pd5651(
        input wire i_clk,
        input wire i_rst,
        input wire [10:0] i_data,
        input wire i_valid,
        output wire o_clk,
        output reg[9:0] o_data
);
wire en;
reg en_r = 1'b0;
assign en = i_rst & i_data[10];
//write 0x0400 to enable dac block

always@(posedge i_clk)begin
        en_r <= i_valid ? en : en_r;
end

//o_clk feeds directly to dac chip
assign o_clk = en_r ? i_clk : 1'b0;

always@(negedge i_clk)begin
        o_data <= i_valid ? i_data[9:0] : o_data;
end

endmodule

唯一不同的是:由于对DAC的操作全是“写”,因此需要写入0x400才能对DAC使能。

 

第六,firmware编写

直通测试程序的main函数如下:

.equ IO_BASE, 0x400000
.equ IO_LEDS, 4
.equ IO_ADC, 32
.equ IO_DAC, 64

.section .text

.globl main

main:
        li   t2, 0x01
        sw   t2, IO_LEDS(gp)
        li   t0, 0x01
        sw   t0, IO_ADC(gp)
        li   t0, 0x400
        sw   t0, IO_DAC(gp)
        li   t1, 0
.L0:
        bnez t1, .L1
        li   t1, 1
        slli t1, t1, 18
        xori t2, t2, 0x03
        sw   t2, IO_LEDS(gp)
.L1:
        lhu  t0, IO_ADC(gp)
        ori  t0, t0, 0x400
        sh   t0, IO_DAC(gp)
        addi t1, t1, -1
        j .L0

其中,start.S已经规定gp为0x400000,并禁止编译器将gp用作他用。

IO_LEDS(gp)的意思就是gp + IO_LEDS,即带偏移的寻址。在主循环中,CPU不断从ADC的地址读取数据,然后与0x400(DAC的使能位)相或,并写入DAC的地址。与此同时,寄存器t1(这里的代码都是按照ABI标准写的,寄存器的“真名”不会出现)作为一个计数器,使得RGB_LED的R和G引脚以“01,10,01,10……”的规律更新,表现为RGB_LED以“红,绿,红,绿……”的规律交替。

最后,执行修改之后的编译脚本:

PROJECTNAME=SOC
BOARD=ice40stepv2
BOARD_FREQ=12
CPU_FREQ=40
FPGA_VARIANT=up5k
FPGA_PACKAGE=sg48
VERILOGS=$1
yosys -q -DSTEP_FPGA -DNEGATIVE_RESET -DBOARD_FREQ=$BOARD_FREQ -DCPU_FREQ=$CPU_FREQ -p "synth_ice40 -abc9 -device u -dsp -top $PROJECTNAME -json $PROJECTNAME.json" $VERILOGS  || exit
nextpnr-ice40 --force --json $PROJECTNAME.json --pcf BOARDS/$BOARD.pcf --asc $PROJECTNAME.asc --freq $BOARD_FREQ --$FPGA_VARIANT --package $FPGA_PACKAGE --pcf-allow-unconstrained || exit
icetime -p BOARDS/$BOARD.pcf -P $FPGA_PACKAGE -r $PROJECTNAME.timings -d up5k -t $PROJECTNAME.asc
icepack $PROJECTNAME.asc $PROJECTNAME.bin || exit
#iceprog $PROJECTNAME.bin || exit
python3 bin2rbt.py --binfile $PROJECTNAME.bin --rbtfile $PROJECTNAME.rbt
cp -f $PROJECTNAME.rbt /mnt/c/<保密>/Documents
echo DONE.

然后通过虚拟U盘界面烧录即可。

附录:开源工具链输出的资源占用报告。

Info: Packing constants..
Info: Packing IOs..
Info: Packing LUT-FFs..
Info:     1078 LCs used as LUT4 only
Info:       67 LCs used as LUT4 and DFF
Info: Packing non-LUT FFs..
Info:       59 LCs used as DFF only
Info: Packing carries..
Info:       10 LCs used as CARRY only
Info: Packing indirect carry+LUT pairs...
Info:        0 LUTs merged into carry LCs
Info: Packing RAMs..
Info: Placing PLLs..
Info:   constrained PLL 'CW.genblk1.pll.pll' to X12/Y31/pll_3
Info: Packing special functions..
Info: Packing PLLs..
Info: Promoting globals..
Info: promoting clk (fanout 158)
Info: promoting RESET_SB_LUT4_I3_O_SB_DFFR_R_Q_SB_LUT4_I0_O_SB_LUT4_I0_O[1] [reset] (fanout 34)
Info: promoting RESET_SB_LUT4_I3_O [reset] (fanout 16)
Info: promoting CPU.instr_SB_DFFE_Q_E [cen] (fanout 32)
Info: promoting CPU.state_SB_DFFESR_Q_D_SB_LUT4_O_1_I3_SB_LUT4_O_I2_SB_LUT4_I1_O[2] [cen] (fanout 30)
Info: Constraining chains...
Info:        3 LCs used to legalise carry chains.
Info: Checksum: 0x76026bc2

Info: Annotating ports with timing budgets for target frequency 12.00 MHz
Info: Checksum: 0xe5901ee4

Info: Device utilisation:
Info:            ICESTORM_LC:  1219/ 5280    23%
Info:           ICESTORM_RAM:    16/   30    53%
Info:                  SB_IO:    28/   96    29%
Info:                  SB_GB:     5/    8    62%
Info:           ICESTORM_PLL:     1/    1   100%
Info:            SB_WARMBOOT:     0/    1     0%
Info:           ICESTORM_DSP:     0/    8     0%
Info:         ICESTORM_HFOSC:     0/    1     0%
Info:         ICESTORM_LFOSC:     0/    1     0%
Info:                 SB_I2C:     0/    2     0%
Info:                 SB_SPI:     0/    2     0%
Info:                 IO_I3C:     0/    2     0%
Info:            SB_LEDDA_IP:     0/    1     0%
Info:            SB_RGBA_DRV:     0/    1     0%
Info:         ICESTORM_SPRAM:     0/    4     0%

 

四、项目总结

   这次项目的经历是充满交易的。因为寒假的时候刚刚经历过蜂鸟E203的“折磨”,使我这个初学者对RISCV开源CPU多少产生了一些阴影和恐惧。但FemtoRV项目作者却在生动形象、风趣幽默的讲解中,不知不觉就把他的blinky变成了一个实实在在的RISCV core。FROM_BLINKER_TO_RISCV的讲义显然很对我”从最小系统出发“的胃口,它在使我明白RISCV CPU”也就是那么回事儿“的同时,也使我明白,即使是像我这样初涉RISCV(但又满脑子骚操作)的初学者,也完全有能力去进行自己的开发和尝试。

 

五、附录

硬件框图:

Fheg51fSVk16skDUCadHApJYO515

软件框图:

FlQ5B7BwlOWENxuv2_5rbez_gpbY

注:演示阶段选择了比流水灯难度更高的ADC-DAC直通程序进行演示,因为流水灯程序是这个开源项目自带的,直接抄过来就能跑。而由于ADC和DAC的引脚和板载LED的引脚冲突,因此只能以RGB-LED交替亮起“红”“绿”的效果(见B站视频)来代替项目要求中的“流水灯”。RGB-LED驱动和流水灯LED驱动事实上是一模一样的,而既然ADC-DAC直通都能完成,流水灯必不在话下。

演示效果1:正弦波

FoHjQkbeOc9KiI7hQ936hC-9jRW1

演示效果2:三角波

FnmFLOyHwauuZ82ENe0IFZ5klK5a

演示效果3:方波

FlZpyAUvJaDyWtQtCp3l28mfW_jg

 

附件下载
ice40.zip
团队介绍
本项目为2023年寒假在家练项目,单人独立完成,成员为南京大学电子学院2019届本科生。
团队成员
赵雨飞
南京大学电子学院2019级本科生,研究生方向为人工智能芯片设计
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号