三、E906移植----FPGA生成可用的比特流并实现串口发送

三、E906移植----FPGA生成可用的比特流并实现串口发送

书接上回,第二篇把基本工程搭建了起来,跑了下综合看了看。本文就开始具体的修改了,连蒙带猜,修修补补,最终完成了板上串口发送“Hello world !”的功能。本文涉及到很多移植的具体细节,有些通用,有些需要根据不同需求作修改,我尽量把修改的目的讲清楚,不清楚的可以多看源码。

另外,把E906的amba 3.0 soc总线换成AXI4.0就基本上是E907的思路了,然而E907是不开源的,授权才能使用的,所以开源这事还是值得思考的,但是另一方面如果把E906的朝着支持AXI4.0总线的方向修改,也就是比较能够实用、卖钱的方向了。

1. 跑通布局布线、生成比特流

1.1 平台切换成zynq7020,资源对比

先贴个zynq7020资源方便比对
三、E906移植----FPGA生成可用的比特流并实现串口发送_第1张图片
53k lut,106k FF, Block rams 140

上一篇我们原封不动的在zynq7045上跑了一遍,资源占用如下,是够的,如果你的板子芯片资源差不多,那资源优化部分暂时不需要修改,如果你的板子芯片资源不够或者即使够也想手动优化一下资源占用,就需要好好分析各个模块的资源占用了,后面默认是基于zynq7020来继进行修改。

z7045下:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第2张图片

·························
·························
z7020下:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第3张图片
光比较LUT是够的,但是bream是严重不够的。bram不够,编译就会调用lut ram,然而lut ram又需要很多额外的lut,最后是整个资源占用都会爆掉

1.2 降低bram 资源占用

ram18e 包含 18Kbit 存储资源 —— (8+1)x 2 Kb —— 2KB —— reg[7:0] mem[0:2047]
一个ram18e 差不多是2KB的ram资源,一个ram36 差不多就是4KB。

z7045 下bram占用分布:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第4张图片

需要注意memory占用的几个地方:

i . x_dahb_mem_ctrl 占了192个bram ,是大头,里面挂了的ram相当于所谓的DTCM,紧耦合data ram;

ii. x_iahb_mem_ctrl 占了64个, 是所谓的ITCM,存放指令的紧耦合ram;

iii. x_smem_ctrl 占32个, 是soc外设总线上挂着的ram,相当于模拟外部接入ram;

iv. soc内部控制模块的,指令icache、

v. 数据dcache

vi. 用于分支预测BHT 的ram。

i、ii和iii的修改方式差不多,都是调用同一个memory生成子模块,修改其传入的数据位宽和深度即可;iv、v和 vi 的修改方式也是一致的,都是在头文件 “cpu_cfg.h” 修改对应的宏定义即可。另一个不太好回答的问题是,各部分具体降低多少bram? 我的思路是内部的cache和外部的ram是一种映射关系,最直白的方式就是大家统一乘上1/3或者1/4(默认E906占用bram大概是zynq7020的3倍),但是会造成利用不充分,x_smem_ctrl和x_dahb_mem_ctrl 内的ram其实在现有的例程中没有使用,因此又可以适当给iahb_mem 多加点。

具体修改:

① iv、v和 vi

iv、v和 vi 的修改相对比较集中一些,打开 cpu_cfg.h ,慢慢往下翻,会分别找到 define BHT_8K 、define ICACHE_16K、define DCACHE_16K几个宏定义。这就是这几个cache的占用了,其实也不是很多,一个ram16e是2KB, 一个ram32额是4KB,soc内部的cache一种大概也就占了一二十个bram。这里我们分别注释掉后替换成 define BHT_2K、define ICACHE_4K、define DCACHE_4K 。

保存再跑一下综合:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第5张图片
对比前面的lut 271%和 lut ram 497% 还是有明显下降的,注意10%是整个器件的1/10,53k lut下就降了5k , 5k lut还是能做很多事的;另一方面,大头还是在ITCM、DTCM上。

② x_iahb_mem_ctrl

打开 x_iahb_mem_ctrl 文件,目前占用64个,修改目标是占用降低一半。

首先可以看到 parameter IMEM_WIDTH = 18 的宏定义,从命名上理解应该是定义一个存储深度的量;
翻到末尾,可以看到如下定义

assign lite_mem_dout[31:0] = {ram3_dout[7:0], ram2_dout[7:0], ram1_dout[7:0], ram0_dout[7:0]};

soc_fpga_ram #(8, IMEM_WIDTH-2) ram0(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[0]),
  .PortADataOut(ram0_dout));

soc_fpga_ram #(8, IMEM_WIDTH-2) ram1(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram1_din),
  .PortAWriteEnable(ram_wen[1]),
  .PortADataOut(ram1_dout));

soc_fpga_ram #(8, IMEM_WIDTH-2) ram2(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram2_din),
  .PortAWriteEnable(ram_wen[2]),
  .PortADataOut(ram2_dout));

soc_fpga_ram #(8, IMEM_WIDTH-2) ram3(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram3_din),
  .PortAWriteEnable(ram_wen[3]),
  .PortADataOut(ram3_dout));

再看这个soc_fpga_ram.v是如何描述mem的

parameter  DATAWIDTH = 2;
parameter  ADDRWIDTH = 2;
parameter  MEMDEPTH = 2**(ADDRWIDTH);
reg [(DATAWIDTH-1):0] mem [(MEMDEPTH-1):0];

所以DATAWIDTH 就是8,ADDRWIDTH 是18-2=16,MEMDEPTH 就是65536, 共有4个ram块,
就是2 ^18 Byte = 2**16*4Byte = 65536 * 32b ,
这里插播一下tb.v是如何初始化ITCM的:


initial
begin
  $display("\t******START TO LOAD PROGRAM******\n");
  $readmemh("./case.pat", mem_inst_temp);

  for(i=0;i<65536;i=i+1)
  begin
    `RTL_IAHBL_MEM.ram0.mem[i][7:0] = ((^mem_inst_temp[i][31:24]) === 1'bx ) ? 8'b0:mem_inst_temp[i][31:24];
    `RTL_IAHBL_MEM.ram1.mem[i][7:0] = ((^mem_inst_temp[i][23:16]) === 1'bx ) ? 8'b0:mem_inst_temp[i][23:16];
    `RTL_IAHBL_MEM.ram2.mem[i][7:0] = ((^mem_inst_temp[i][15: 8]) === 1'bx ) ? 8'b0:mem_inst_temp[i][15: 8];
    `RTL_IAHBL_MEM.ram3.mem[i][7:0] = ((^mem_inst_temp[i][ 7: 0]) === 1'bx ) ? 8'b0:mem_inst_temp[i][ 7: 0];
  end

都是65536 ,是不是就恰好对上了!
另外这里提醒一下,tb的ram0对应的是[31:24]是高8bit,后面重写soc_fpga_ram模块时会有用。

回到主题上来,x_iahb_mem_ctrl.v里parameter IMEM_WIDTH = 18 改成17,占用就减半了,
另外有两个地方就是addr_hoding信号和ram_addr两个信号的位宽都定义成16位,会发现和IMEM_WIDTH 紧相关,所以也对应改成15位,其他模块大体上完成的功能就是在ahb总线上进行读写响应来实现系统与模块内ram的数据交互。

再跑综合:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第6张图片
又降了不少。

③ dahb_mem_ctrl

dahb_mem_ctrl本来是占用大头,放到后面主要是因为目前还不是很明白其中一些写法的用意,整个模块的功能和iahb_mem_ctrl差不多,都是通过apb_lite总线来与外部进行数据交互。

文件开局也定义了一个parameter DMEM_WIDTH = 20 ,但是整个文件没有出现第二次,拉到文件末尾会发现传入参数默认是写固定了的,共定义了8个ram块,ram0-ram3构成一组是17bit地址位,而ram4-ram7构成另一组地址是16bit,通过地址ram_addr[17]也就是18位来分别选通使能两组memory。

// memory unit is in DPTHx8 size, 4 units are instanced
soc_fpga_ram #(8, 17) ram0(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[16:0]),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[0]),
  .PortADataOut(ram0_dout));
  
soc_fpga_ram #(8, 16) ram4(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[15:0]),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[4]),
  .PortADataOut(ram4_dout));

其实我不是很明白这种写法的用意,是为了模拟多个sram块吗? 打开tb.v ,可以看到相关初始化操作:

for(i=0;i<=65536;i=i+1)
  begin
    `RTL_DAHBL_MEM.ram0.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram1.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram2.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram3.mem[i][7:0]  = 8'b0;
  end
  for(i=0;i<=65536;i=i+1)
  begin
    `RTL_DAHBL_MEM.ram4.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram5.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram6.mem[i][7:0]  = 8'b0;
    `RTL_DAHBL_MEM.ram7.mem[i][7:0]  = 8'b0;
  end

可以看出:①例程代码是没用DTCM的,都是赋初始值0; ②ram4-ram7是6553648bit 和RTL是对应的。
但是ram0-ram3的操作是对不上的,RTL给的是17bit,初始化只做了一半16bit,这点也是比较疑惑的。

但是好在,跑hello_world可能暂不需要DTCM,所以和上小节一样套路修改也没什么问题。

具体操作:
首先是把宏定义 DMEM_WIDTH = 20 用上,方便修改,就是 dahb_mem_ctrl 文件末尾涉及到memory的四五个模块中有关地址的地方,如 17 用 DMEM_WIDTH -3代替 ,16用DMEM_WIDTH -4代替,我下面贴一下改过后的后面代码,然后再把DMEM_WIDTH 改成19 ,memory占用就减半了,另外和上节不同的是文件开始定义的wire [29:0] ram_addr 就不用改了,明显这是把2^32 byte的索引 弄成2**30 * 32bit的索引。

//memory
always @(posedge pll_core_cpuclk)
begin
  if(!lite_mem_cen)
    addr_holding[29:0] <= lite_mem_addr[31:2];
end

assign ram_clk = pll_core_cpuclk;
assign ram_addr[29:0] = lite_mem_cen ? addr_holding[29:0] : lite_mem_addr[31:2];
assign ram_wen[0] = (!lite_mem_cen && !lite_mem_wen[0]) && !ram_addr[DMEM_WIDTH -3];
assign ram_wen[1] = (!lite_mem_cen && !lite_mem_wen[1]) && !ram_addr[DMEM_WIDTH -3];
assign ram_wen[2] = (!lite_mem_cen && !lite_mem_wen[2]) && !ram_addr[DMEM_WIDTH -3];
assign ram_wen[3] = (!lite_mem_cen && !lite_mem_wen[3]) && !ram_addr[DMEM_WIDTH -3];

assign ram_wen[4] = !lite_mem_cen && !lite_mem_wen[0] && ram_addr[DMEM_WIDTH -3];
assign ram_wen[5] = !lite_mem_cen && !lite_mem_wen[1] && ram_addr[DMEM_WIDTH -3];
assign ram_wen[6] = !lite_mem_cen && !lite_mem_wen[2] && ram_addr[DMEM_WIDTH -3];
assign ram_wen[7] = !lite_mem_cen && !lite_mem_wen[3] && ram_addr[DMEM_WIDTH -3];

assign ram0_din[7:0] = lite_mem_din[7:0];
assign ram1_din[7:0] = lite_mem_din[15:8];
assign ram2_din[7:0] = lite_mem_din[23:16];
assign ram3_din[7:0] = lite_mem_din[31:24];
assign lite_mem_dout[31:0] = addr_holding[DMEM_WIDTH -3] ? {ram7_dout[7:0], ram6_dout[7:0], ram5_dout[7:0], ram4_dout[7:0]}
                                              : {ram3_dout[7:0], ram2_dout[7:0], ram1_dout[7:0], ram0_dout[7:0]};

// memory unit is in DPTHx8 size, 4 units are instanced
soc_fpga_ram #(8, DMEM_WIDTH -3) ram0(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -4:0]),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[0]),
  .PortADataOut(ram0_dout));

soc_fpga_ram #(8, DMEM_WIDTH -3) ram1(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -4:0]),
  .PortADataIn (ram1_din),
  .PortAWriteEnable(ram_wen[1]),
  .PortADataOut(ram1_dout));

soc_fpga_ram #(8, DMEM_WIDTH -3) ram2(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -4:0]),
  .PortADataIn (ram2_din),
  .PortAWriteEnable(ram_wen[2]),
  .PortADataOut(ram2_dout));

soc_fpga_ram #(8, DMEM_WIDTH -3) ram3(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -4:0]),
  .PortADataIn (ram3_din),
  .PortAWriteEnable(ram_wen[3]),
  .PortADataOut(ram3_dout));

soc_fpga_ram #(8, DMEM_WIDTH -4) ram4(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -5:0]),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[4]),
  .PortADataOut(ram4_dout));

soc_fpga_ram #(8, DMEM_WIDTH -4) ram5(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -5:0]),
  .PortADataIn (ram1_din),
  .PortAWriteEnable(ram_wen[5]),
  .PortADataOut(ram5_dout));

soc_fpga_ram #(8, DMEM_WIDTH -4) ram6(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -5:0]),
  .PortADataIn (ram2_din),
  .PortAWriteEnable(ram_wen[6]),
  .PortADataOut(ram6_dout));

soc_fpga_ram #(8, DMEM_WIDTH -4) ram7(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr[DMEM_WIDTH -5:0]),
  .PortADataIn (ram3_din),
  .PortAWriteEnable(ram_wen[7]),
  .PortADataOut(ram7_dout));

再跑一下综合,可以看到现在是够用了,但是lut 与lut ram占用有点过高,这会导致后续的时序跑不起来。
三、E906移植----FPGA生成可用的比特流并实现串口发送_第7张图片
再看lut ram主要分布模块:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第8张图片
还是这个 dahb_mem_ctrl,所以 再减1/2,把DMEM_WIDTH 改成18,综合后如下:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第9张图片
bram 都没有用完,lut占用70%左右,差不多是个合适区间吧,后面那个x_smem_ctrl也懒得改了。

至此,综合部分算是基本调完了。

1.3 添加时钟块,约束引脚与时序,完成 Implemention 与 Generate Bitstream步骤

top文件只是调用了soc模块,时钟IP没添加,复位、uart引脚没绑定。。。。。。

① 添加时钟IP

悲观起见,输出50M、80M、100M 这3路时钟:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第10张图片
时钟例化进top文件中去,引用一下pll输出时钟。

②引脚约束

创建xcd约束文件,根据自己的板子约束对应引脚,提供一份仅供参考:

set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]

#timming
create_clock -period 20 -name clk50M -waveform {0 10} [get_ports clk_fpga]

#IO
#clk_fpga
set_property -dict { PACKAGE_PIN U18    IOSTANDARD LVCMOS33 } [get_ports { clk_fpga }]
#rstn
set_property -dict {PACKAGE_PIN J15  IOSTANDARD LVCMOS33}  [get_ports rstn_fpga  ]

#####               MCU JTAG define           #####

#set_property PACKAGE_PIN R17 [get_ports jtg_clk]
#set_property PACKAGE_PIN C20 [get_ports jtg_trstn]
set_property PACKAGE_PIN P19 [get_ports jtg_tdi]
set_property PACKAGE_PIN R16 [get_ports jtg_tdo]
set_property PACKAGE_PIN N18 [get_ports jtg_tms]

#set_property IOSTANDARD LVCMOS33 [get_ports jtg_clk]
#set_property IOSTANDARD LVCMOS33 [get_ports jtg_trstn]
set_property IOSTANDARD LVCMOS33 [get_ports jtg_tdo]
set_property IOSTANDARD LVCMOS33 [get_ports jtg_tdi]
set_property IOSTANDARD LVCMOS33 [get_ports jtg_tms]

set_property KEEPER true [get_ports jtg_tms]
#set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets jtg_clk_IBUF] 

#####               UART                        #####
## UART TX
set_property PACKAGE_PIN P15  [get_ports uart_tx_fpga ]
set_property IOSTANDARD LVCMOS33 [get_ports uart_tx_fpga]

## UART RX
set_property PACKAGE_PIN P16  [get_ports uart_rx_fpga]
set_property IOSTANDARD LVCMOS33 [get_ports uart_rx_fpga]

#####               GPIO port A                 #####
#
#A7:
#A6:
#A5:    key_in PL key1
#A4:    PL_LED1
#A3:    PL_LED0
#A2:
#A1:
#A0:
set_property PACKAGE_PIN M18  [get_ports {gpio_porta[7]}]
set_property PACKAGE_PIN M17  [get_ports {gpio_porta[6]}]
set_property PACKAGE_PIN J20    [get_ports {gpio_porta[5]}]
set_property PACKAGE_PIN H18   [get_ports {gpio_porta[4]}]
set_property PACKAGE_PIN J18     [get_ports {gpio_porta[3]}]
set_property PACKAGE_PIN M20   [get_ports {gpio_porta[2]}]
set_property PACKAGE_PIN D20    [get_ports {gpio_porta[1]}]
set_property PACKAGE_PIN K14    [get_ports {gpio_porta[0]}] 

set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {gpio_porta[0]}]

##ignore unconnected pins
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
set_property BITSTREAM.CONFIG.UNUSEDPIN Pullup  [current_design]

以及top文件:

`timescale 1ns / 1ps

module e906_top(
    clk_fpga,     //jing zhen clk input  50Mhz
    rstn_fpga,         
    uart_tx_fpga,
    uart_rx_fpga,
    gpio_porta,
    jtg_tdi,
    jtg_tdo,
    jtg_tms
 );
    input clk_fpga;
    input rstn_fpga;
    input uart_rx_fpga;
    output uart_tx_fpga;
    inout[7:0] gpio_porta;
    input jtg_tdi;
    output jtg_tdo;
    input jtg_tms ;  
    
wire clk_soc;
clk_wiz_0 u_clk_wiz_0
 (
  // Clock out ports
        .clk_out1( clk_soc ),     //50M
        .clk_out2(  ),     //80M
        .clk_out3(  ),     //100M
 // Clock in ports
        .clk_in1(  clk_fpga )
 );
    //instantiate soc    
soc u_soc(
  .i_pad_clk          ( clk_soc                ),
  .i_pad_uart0_sin    ( uart_rx_fpga           ),
  .o_pad_uart0_sout   ( uart_tx_fpga           ),
  .i_pad_jtg_tclk     ( clk_soc                ),
  .i_pad_jtg_trst_b   ( rstn_fpga              ),
  .i_pad_jtg_nrst_b   ( rstn_fpga              ),
  .b_pad_gpio_porta   ( gpio_porta             ),
  .i_pad_jtg_tdi      ( jtg_tdi                ),
  .o_pad_jtg_tdo      ( jtg_tdo                ),
  .i_pad_jtg_tms      ( jtg_tms                ),   
  .i_pad_rst_b        ( rstn_fpga              )
);
    
endmodule

③ 先跑布局布线

等待一二十分钟后,查看timing,
三、E906移植----FPGA生成可用的比特流并实现串口发送_第11张图片
可以看到50M时钟下,setup time slack 仅为0.06ns,关键路径逻辑层级来到26,速度基本也就到头了。

但是就速度来说不得不说的是,①ch2601 芯片能跑200Mhz ②平台换成z7045 同样是-2等级,速度能上80Mhz。 说明器件速率和资源占用都会影响最终速率(严谨点可能z7045的-2比起z7020的-2速率也有提升,没去实际看手册比较)。

④ 跑比特流

跑一下比特流生成,修改报错,确保整个流程是通畅的,方便后面正式修改。
三、E906移植----FPGA生成可用的比特流并实现串口发送_第12张图片
···························································
································手动分割···············
···························································

2. 修改结构、加载程序、修改例程运行串口输出

经过这么久的铺垫,基本平台算是搭建完成,就可以慢慢深入了。
这一节涉及到软硬件交互协调,实际调试过程应该是硬件-软件-硬件-软件多次交互,一点点改动适配的过程,为了方便写,我这里会先介绍一下总体要干那些事、需要哪些文件、要改哪些部分,然后再分别从软件、硬件两方面把要修改的逐一罗列出来。

2.1 必要步骤

要把基本的程序跑起来,我们要做的事情大致包括:生成硬件平台,生成软件代码,然后软件装载进硬件。

关于硬件,确切的说是可综合的、可自动加载软件程序的、行为与tb中的激励行无本质差别的RTL构成的硬件平台。参照tb的行为描述:
一是前面搭建的基本硬件平台存在最致命的问题——无法加载软件代码进系统,对应的就是tb中读入文件并循环赋值初始化iahb_mem_ctrl中的memory那部分,而且E906相较其他更为复杂一点的是,它的32bit的ram是由4个8bit的soc_fpga_ram模块并行起来组成的,所以不能一次性把软件二进制数据读入reg数组中,不能像仿真那样一个一个循环给数据进ram;所以需要手动将tb中那个读入的文件分割成4份,分别装入ram块中。

二是电源管理pmu模块,tb中进行了强制赋值操作,可能有影响可能没有,改了肯定可以,不改行不行将在最后解答(因为第一次调试时,先发现的这个没有修改,后才进一步发现内存初始化步骤有问题)。

关于软件,一是官方没有直接的uart测试例程,但是提供了uart的库函数,需要读懂并添加调用;二是可以先在smart_run上跑通验证功能正确性,而不必每次都上板验证,但是又要注意跑仿真验证和实际板上的区别。

2.2 软件代码

借助smart_run平台的hello_world例程,备份后自行修改,可以参考下面我的:

#include "datatype.h"
#include "stdio.h"
#include "uart.h"

t_ck_uart_device uart0 = {0xFFFF};

//sys 50Mhz
void delay_ms(int time){
	int i,j;
	for(i=0;i<time;i++){
		for(j=0;j<50000;j++){
			;
		}
	}
}

int main (void)
{
   int a;
  //--------------------------------------------------------
  // setup uart
  //--------------------------------------------------------
  t_ck_uart_cfig   uart_cfig;

  uart_cfig.baudrate = BAUD;       // any integer value is allowed
  uart_cfig.parity = PARITY_NONE;     // PARITY_NONE / PARITY_ODD / PARITY_EVEN
  uart_cfig.stopbit = STOPBIT_1;      // STOPBIT_1 / STOPBIT_2
  uart_cfig.wordsize = WORDSIZE_8;    // from WORDSIZE_5 to WORDSIZE_8
  uart_cfig.txmode = ENABLE;          // ENABLE or DISABLE
  // open UART device with id = 0 (UART0)
  ck_uart_open(&uart0, 0);
  // initialize uart using uart_cfig structure
  ck_uart_init(&uart0, &uart_cfig);

//Section 1: Function
  printf("printf test!\n");

//Section 2: Uart
while(1){
	for(a=0;a<10;a++){
		ck_uart_putc(&uart0, a+48);
		delay_ms(10);
	}	
}
  return 0;
}

然后 make runcase CASE=helloworld CASE=vcs命令,执行仿真,查看仿真结果。
三、E906移植----FPGA生成可用的比特流并实现串口发送_第13张图片
根据前面踩的坑,这里有三点有必要着重说明一下

i. 仿真结果时间比程序预设执行时间慢很多,从printf test! 到后面的01234567…之间我等待了至少5分钟。

ii. 硬件工程中的uart_mnt()模块是具备监测uart引脚状态的功能的,尽管是仿真代码,但是是一个脉冲一个脉冲的把uart 的sout信号解析出来的。

iii. 程序里的printf()函数目前还没有重映射,所以实际上该函数是调用的PC端上提供的打印功能,只有第二排开始后面的红色字体才是真正的uart输出经过解析后的值,且是正确的。 所以会出现第一行打印很快,第二行打印很慢,因为后面是真的在一个指令一个指令的模拟仿真,而第一行直接调用后台输出。

打印成功后,最终想要的只是work目录下的 case.pat文件:
三、E906移植----FPGA生成可用的比特流并实现串口发送_第14张图片

2.3 pat文件拆分

可以看到case.pat文件的数据默认每32bit(4byte)存放一个地址(@地址),我们需要将其拆分成4个文件,每个文件分别存放其中所有行的字节0、字节1、字节2、字节3,然后将未用到的地址对应的数据补0 。

我这里使用 questasim来进行数据的转换操作,贴一下参考文件,自己注意路径根据实际更改:
操作文件:

//split .pat file into 4 files
`timescale 1ns/100ps

module top();

parameter DATAWIDTH=8;
parameter ADDRWIDTH=17;	//byte addrress width
parameter MEMDEPT = 2**(ADDRWIDTH-2);	//divided by 4---- 32768

integer fid0,fid1,fid2,fid3;
reg[31:0] i;
reg[31:0] mem_temp [(MEMDEPT-1):0];	

initial 
begin 
	$readmemh("/home/ICer/ic_prjs/opene906-main/smart_run/work/case.pat",mem_temp);	
end

initial begin
	fid0 = $fopen("case0.pat");
	fid1 = $fopen("case1.pat");
	fid2 = $fopen("case2.pat");
	fid3 = $fopen("case3.pat");

	for(i=0;i<MEMDEPT;i=i+1) begin
		if((^mem_temp[i][31:24]) === 1'bx ) 
			$fdisplay(fid3,"@%h %h",i,8'h00);
		else 
			$fdisplay(fid3,"@%h %h",i,mem_temp[i][31:24]);
		if((^mem_temp[i][23:16]) === 1'bx ) 
			$fdisplay(fid2,"@%h %h",i,8'h00);
		else 
			$fdisplay(fid2,"@%h %h",i,mem_temp[i][23:16]);
		if((^mem_temp[i][15:8]) === 1'bx ) 
			$fdisplay(fid1,"@%h %h",i,8'h00);
		else 
			$fdisplay(fid1,"@%h %h",i,mem_temp[i][15:8]);
		if((^mem_temp[i][7:0]) === 1'bx ) 
			$fdisplay(fid0,"@%h %h",i,8'h00);
		else 
			$fdisplay(fid0,"@%h %h",i,mem_temp[i][7:0]);
	end
	
	$fclose("case0.pat");	
	$fclose("case1.pat");	
	$fclose("case2.pat");		
	$fclose("case3.pat");	
end

endmodule

以及questaSim运行do脚本

quit -sim
vlib work
vmap work work
vlog -work work top.v
vsim -voptargs=+acc work.top

add wave  -color Green -height 20 /*
run 1000000 ns

具体操作:
① 在smart_run路径下新建一个文件夹,把上述两个文本文档复制进去。
三、E906移植----FPGA生成可用的比特流并实现串口发送_第15张图片
②该目录下打开终端,输入vsim进入questaSim仿真器,运行do脚本
三、E906移植----FPGA生成可用的比特流并实现串口发送_第16张图片
③得到输出结果文件
三、E906移植----FPGA生成可用的比特流并实现串口发送_第17张图片
三、E906移植----FPGA生成可用的比特流并实现串口发送_第18张图片

2.4 硬件更改

实在懒得解释了,看一遍应该都能理解。

① ram初始化适配

i. 在FPGA的/source/logical/mem的目录下,把soc_fpga_ram.v复制一份,重命名为soc_fpga_ram2.v;

ii. 编辑soc_fpga_ram2.v文件,主要区别在于多了一个字符串宏参数FILE,方便外部调用时调用不同文件。

module soc_fpga_ram2(
  PortAClk,
  PortAAddr,
  PortADataIn,
  PortAWriteEnable,
  PortADataOut
);
parameter  DATAWIDTH = 2;
parameter  ADDRWIDTH = 2;
parameter  FILE="file.pat";
parameter  MEMDEPTH = 2**(ADDRWIDTH);

input                     PortAClk;
input   [(ADDRWIDTH-1):0] PortAAddr;
input   [(DATAWIDTH-1):0] PortADataIn;
input                     PortAWriteEnable;
output  [(DATAWIDTH-1):0] PortADataOut; 

reg [(DATAWIDTH-1):0] mem [0:(MEMDEPTH-1)];
reg [(DATAWIDTH-1):0] PortADataOut;

initial 
begin
	$readmemh(FILE, mem);
end

always @(posedge PortAClk)
begin
  if(PortAWriteEnable)
  begin
    mem[PortAAddr]  <= PortADataIn;
  end
  else
  begin
    PortADataOut    <= mem[PortAAddr];
  end
end
endmodule

iii. 修改iahb_mem_ctrl.v最后几行调用ram的代码,如下,注意:传递路径根据实际写 ;ram0----pat3对应关系。

soc_fpga_ram2 #(8, IMEM_WIDTH-2,"/home/ICer/ic_prjs/opene906-main/smart_run/create_patfile/case3.pat") ram0(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram0_din),
  .PortAWriteEnable(ram_wen[0]),
  .PortADataOut(ram0_dout));

soc_fpga_ram2 #(8, IMEM_WIDTH-2,"/home/ICer/ic_prjs/opene906-main/smart_run/create_patfile/case2.pat") ram1(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram1_din),
  .PortAWriteEnable(ram_wen[1]),
  .PortADataOut(ram1_dout));

soc_fpga_ram2 #(8, IMEM_WIDTH-2,"/home/ICer/ic_prjs/opene906-main/smart_run/create_patfile/case1.pat") ram2(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram2_din),
  .PortAWriteEnable(ram_wen[2]),
  .PortADataOut(ram2_dout));

soc_fpga_ram2 #(8, IMEM_WIDTH-2,"/home/ICer/ic_prjs/opene906-main/smart_run/create_patfile/case0.pat") ram3(
  .PortAClk (ram_clk),
  .PortAAddr(ram_addr),
  .PortADataIn (ram3_din),
  .PortAWriteEnable(ram_wen[3]),
  .PortADataOut(ram3_dout));

iiii. 打开FPGA工程,把soc_fpga_ram2 .v文件添加进源文件中,重新综合、布局布线、生成比特流。
这里又遇到个坑,前面新建工程时,千万不要勾选copy源文件进工程,否则修改是分开的,不同步,很麻烦。

第一次下载验证,此时未修改pmu中的相关信号电平,通过ch340模块接上串口助手,波特率调到9600,
结果显示——串口上位机有信号!!
三、E906移植----FPGA生成可用的比特流并实现串口发送_第19张图片
如果接收有乱码,一是波特率设置减半问题。二是串口发送太快,上位机接收后把两个字符合成一个扩展ASCII,这种情况最好就是相邻uart发送字符串加个延时函数,然后最好是再发送个“\n”字符,因为上位机软件的uart缓存机制,大多都是读一行进行一次触发处理(可以参看我前面分享的一个QT写的串口上位机)。

为什么c文件中舒适化波特率19200,这里实际接收选择9600?因为,E906默认的时钟频率是100Mhz,从uart_mnt模块以及c代码库中的config.h也能找到相关描述。但是我们实际给的是50Mhz,soc系统默认是无法察觉自身时钟频率的,因此实际最终波特率应该减半!

② pmu中的信号修改

暂时不做修改,理由见上。但是把相关信号的具体位置记录一下,以便后续可能需要用到。

corec_pmu_sleep_out
pmu_corec_isolation
pmu_corec_sleep_in

  1. corec_pmu_sleep_out 在cpu_sub_system_ahb 定义 output
    添加 assign =0;

  2. pmu.v中

corec_pmu_sleep_out拉高 -->状态机进入PG_POWER_OFF -> pmu_corec_sleep_in 和pmu_corec_isolation拉高 没有其他模块直接交互

2.5 上板验证

见2.4结果,不再赘述,至此基本跑通整个移植过程。

3.总结

这应该是移植最核心的一篇,内容也比较多,做完后我觉得算是入门了。还是那句话,自己看官方源码才是最终解决方案。因为分享经验时效性不好说的,官方指不定后面更新一下源码库,有些直接复制过去就用不了。

这里留了一个小悬疑,就是printf函数的问题,其实这是我踩的一个大坑,因为仿真的时候总是发现调试窗口有输出的,我误以为也是uart的解析输出,所以明明自己写的测试c例程实际上没有通过uart发送数据,调试时被判定发送了,排错浪费了些时间。下一篇我将介绍如何在平台上将printf进行重映射到uart上去,便于调试。

你可能感兴趣的:(玄铁RISCV核--E906,CPU的FPGA移植,fpga开发,RISCV,E906)