nightt5879 / FPGA

该项目适用于中山大学(Sun Yat-sen University)数字集成电路前端设计与高层次综合 Front-end Design of Digital Integrated Circuits and High-level Synthesis
6 stars 0 forks source link

中文 | English

数字集成电路前端设计与高层次综合

1.写在前面

本课程由中山大学电子与信息工程学院(微电子学院)School of Electronics and Information Technology (School of Microelectronics) 开设。

这是一门实用主义的课程,可以学到很多东西,由于课程提供的lab板子和手上的板子不一样,所以对代码进行一些修改,以适应自己的板子。

我写这个项目的目的一来是完成6个lab与课程设计,用于提交作业。另外是为后来者可能有使用同一个板子的人提供一些参考(因为我看到这个板子在直接搜索FPGA时还是比较多人买的)。

1.1 软件硬件环境

AX7010资料

板子: 板子

ZedBoard资料

ZedBoard: ZedBoard

1.2 注意事项

这套实验除了实验1,余下的23456都需要使用到UART,通过XDC文件可以得知UART映射到的引脚是JA2与JA3,所以左上角USB的UART是没用的(这是PS配置的UART,不是PL的UART)。

正确的UART连线:

位置在板子的左下角,IO口可以看丝印

UART连线

2.实验内容

2.1 实验 1

重要文件: lab1 实验手册

具体的操作内容已经包含在上述的 PDF 中,本项目主要是将里面的 labX.vlab1_tb.vlab1.xdc 这三个文件进行修改,以适应自己的板子。

上述三个文件分别对应的是 Verilog 代码、Testbench 代码、约束文件。

实验结果:

以下图片和动画展示了实验的运行结果

行为仿真

行为仿真结果

项目概要

项目概要

综合后的电路图

综合后的电路图

设备实现

设备实现结果

后时序仿真

时序仿真结果

最终测试

最终测试

2.2 实验 2

重要文件: lab2 实验手册

实验2356都是同样的代码逻辑,实现教学的东西不一样,所以这里就留下一个实验2的实现,剩下的只要XDC选对了引脚一样能实现的 (正在开发中)

2.4 实验 4

重要文件: lab4 实验手册

原本的教程是导入所给的IP核,目的就是熟悉了解一下IP核心设计,这里我们直接利用IP核心配置实现PS的UART功能(也就是实验2的功能),也能学习使用IP核心的方法。

(正在开发中)

3.ZedBoard实验(课程所使用板子)

3.1 实验 1

重要文件: lab1 实验手册

代码解析

下面是 lab1 模块的代码(可以在lab1.v中找到),主要用来控制 ZedBoard 上的 LED 显示。每个 LED 的亮灭由相应的开关位置决定。具体逻辑如下:

仿真的代码类似,不赘述请自行查阅

代码实现如下:

module lab1(
    input [7:0] swt,
    output [7:0] led
    );

    assign led[0] = ~swt[0];
    assign led[1] = swt[1] & ~swt[2];
    assign led[3] = swt[2] & swt[3];
    assign led[2] = led[1] | led[3];

    assign led[7:4] = swt[7:4];

endmodule

引脚约束文件 lab1.xdc 用于将开关和 LED 连接到 ZedBoard 的物理引脚。以下是约束文件的内容:

由于 XDC 一般涉及较多的重复操作,我们分别举一 LED 和 swt 一个的例子,其余的逻辑相似。

代码实现如下:

set_property PACKAGE_PIN F22 [get_ports swt[0]]
set_property IOSTANDARD LVCMOS33 [get_ports swt[0]]

set_property PACKAGE_PIN T22 [get_ports led[0]]
set_property IOSTANDARD LVCMOS33 [get_ports led[0]]

实验结果

以下图片和动画展示了实验的运行结果 行为仿真

行为仿真结果

项目概要

项目概要

综合后的电路图

综合后的电路图

设备实现

设备实现结果

后时序仿真

时序仿真结果

最终测试

最终测试

3.2 实验 2

重要文件: lab2 实验手册

代码解析

这个实验相较于上一个实验较复杂,涉及了6个Verilog文件,我们先进行一个层级分类。

这就是这个lab的设计层级结果,可以理解成每个v文件调用了别的v文件中的模块,最终形成了一个完整的项目。

我们从上往下分析,首先是 uart_led.v 文件,该顶层文件主要是调用模块和传递参数,把uart和led拼接起来。具体可以自行查看代码

led_ctl.v 文件定义了一个led模块,并且可以将一个8位的数据传递给led模块,led会进显示。

主要实现的代码部分如下:

  always @(posedge clk_rx)
  begin
    if (rst_clk_rx)
    begin
      old_rx_data_rdy <= 1'b0;
      char_data       <= 8'b0;
      led_o           <= 8'b0;
    end
    else
    begin
      // Capture the value of rx_data_rdy for edge detection
      old_rx_data_rdy <= rx_data_rdy;

      // If rising edge of rx_data_rdy, capture rx_data
      if (rx_data_rdy && !old_rx_data_rdy)
      begin
        char_data <= rx_data;
      end

      // Output the normal data or the data with high and low swapped
      if (btn_clk_rx)
        led_o <= {char_data[3:0],char_data[7:4]};
      else
        led_o <= char_data;
    end // if !rst
  end // always

其中always @(posedge clk_rx)是每当clk_rx上升沿时就会进入该模块,首先利用if (rst_clk_rx)判断是否处于复位状态,如果是则将old_rx_data_rdychar_dataled_o都置为0(即重置所有状态),否则就会进入else部分

当不处于复位状态(进入else)时,首先将rx_data_rdy这个串口数据已经准备好的参数<=给old_rx_data_rdy

需要注意的是这里的<=是非阻塞赋值,该操作会在所有always模块执行完后才进行更新!!!

接下来进行if (rx_data_rdy && !old_rx_data_rdy)判断。

该判定逻辑是如果当前时钟周期的rdy是1(表示有rx数据)同时上一个时钟周期的rdy是0(表示上一个周期没有数据),则说明这是一个新的数据,此时更新char_datarx_data(即串口数据)

需要注意的是由于非阻塞赋值的情况存在,old_rx_data_rdy在if判断的时候还未更新,所以是上一个时钟周期的值。同时后续操char_data的更新最终也发生在always模块执行之后。

最后是led显示部分,首先检测btn_clk_rx(外部输入按钮)的值是否为1,如果是则将led_o的低4位和高4位交换,否则直接显示char_data


接下来说明一下meta_harden.vuart_baud_gen.v的作用

meta_harden.v主要实现了双重同步的功能,目的是将一个异步信号同步到目标时钟域

简单来说,假设这个异步信号是按键,按键被触发并不是同步于FPGA时钟信号的,当触发的时候刚好在时钟信号的上升沿时候,就会出现这个按键电平变得不稳定(亚稳态)导致后续读取问题。这是异步信号同步会发生的问题,所以需要一个双重同步的过程

实现代码:

  always @(posedge clk_dst)
  begin
    if (rst_dst)
    begin
      signal_meta <= 1'b0;
      signal_dst  <= 1'b0;
    end
    else // if !rst_dst
    begin
      signal_meta <= signal_src;
      signal_dst  <= signal_meta;
    end // if rst
  end // always

双重同步实现起来并不复杂,首先要理解前面提到的非阻塞赋值概念,即<=操作会在所有always模块执行完后才进行更新。所以在这个模块中,首先将signal_src赋值给signal_meta,下个周期才能再将signal_meta赋值给signal_dst

当然根据这个原理你也可以实现n重同步,多写几个<=赋值即可,具体取决于项目需求


uart_baud_gen.v主要是用来产生波特率,该部分重点是实现过采样

我们先首先要理解他的逻辑部分,稍后我们会进行分析如何得到这个频率计数数值。

逻辑代码如下:

assign internal_count_m_1 = internal_count - 1'b1;
  always @(posedge clk)
  begin
    if (rst)
    begin
      internal_count  <= OVERSAMPLE_VALUE;
      baud_x16_en_reg <= 1'b0;
    end
    else
    begin
      // Assert baud_x16_en_reg in the next clock when internal_count will be
      // zero in that clock (thus when internal_count_m_1 is 0).
      baud_x16_en_reg   <= (internal_count_m_1 == {CNT_WID{1'b0}});
      // Count from OVERSAMPLE_VALUE down to 0 repeatedly
      if (internal_count == {CNT_WID{1'b0}}) 
      begin
        internal_count    <= OVERSAMPLE_VALUE;
      end
      else // internal_count is not 0
      begin
        internal_count    <= internal_count_m_1;
      end
    end // if rst
  end // always 

首先我们得大概了解一下assign internal_count_m_1 = internal_count - 1'b1;的作用,这是一个连续赋值的语句,物理上是通过硬件组合逻辑实现,当internal_count变化时,internal_count_m_1也会马上变化,不需要等待时钟同步。

该模块always @(posedge clk)是在每一个时钟上升沿进入,当复位时候赋值internal_countOVERSAMPLE_VALUE,同时baud_x16_en_reg为0

上述逻辑部分实现了,每多少个时钟周期触发一次baud_x16_en_reg,假设时钟周期是1Khz,每10次触发一次就是进行100Hz的采样,如果波特率是10Hz那么就进行了10倍的过采样

实现代码:

  parameter BAUD_RATE    = 57_600;              // Baud rate
  parameter CLOCK_RATE   = 50_000_000;
  // The OVERSAMPLE_RATE is the BAUD_RATE times 16
  localparam OVERSAMPLE_RATE = BAUD_RATE * 16;
  // The divider is the CLOCK_RATE / OVERSAMPLE_RATE - rounded up
  // (so add 1/2 of the OVERSAMPLE_RATE before the integer division)
  localparam DIVIDER = (CLOCK_RATE+OVERSAMPLE_RATE/2) / OVERSAMPLE_RATE;
  // The value to reload the counter is DIVIDER-1;
  localparam OVERSAMPLE_VALUE = DIVIDER - 1;
  // The required width of the counter is the ceiling of the base 2 logarithm
  // of the DIVIDER
  localparam CNT_WID = clogb2(DIVIDER);

该部分就是实现了计算OVERSAMPLE_VALUE的值,也就是上述提到的每多少次触发一次baud_x16_en_reg的值

减去1是因为计数器是从0开始的

clogb2(DIVIDER)的作用是计算DIVIDER的二进制位数,这样就可以得到一个合适的计数器宽度,可以节省资源

实现代码:

  function integer clogb2;
    input [31:0] value;
    reg   [31:0] my_value;
    begin
      my_value = value - 1;
      for (clogb2 = 0; my_value > 0; clogb2 = clogb2 + 1)
        my_value = my_value >> 1;
    end
  endfunction

主要逻辑就是一直将value右移,直到为0,同时每一次右移的时候clogb2+1,最终得到的clogb2就是value的二进制位数


最后是uart_rx.vuart_rx_ctl.v的作用

uart_rx.v主要是调用与传递参数,类似于uart_led.v

重点部分是uart_rx_ctl.v,该部分主要是实现了一个串口接收控制器,主要是接收串口数据并且进行解码

这部分的代码比较长,里面有详细的注释,这里大概说明一下作用,详细可以对照代码查看

总共有五个always同步进行的逻辑部分,后续简称模块X

模块1实现的是状态机功能,在每一个时钟周期上升沿进行状态更新

模块2实现的是在判断到有数据时候,在第八个过采样读取当前bit的值,后续从起始位中间开始每16个过采样读取一次bit,也就是每次读取过采样的中间值,防止出现数据不稳定等情况

模块3实现的是记录当前读取的bit数

模块4实现的是判断bit位数是否正确产生rx_data_rdy信号

模块5是用于停止位校验,正常情况下停止位应该是1,如果不是则表示数据错误,通过frm_err传递结果

需要注意的是这些always是同步进行的,也就是在时钟沿上升的时候同步开始执行所有部分(读取的是上一个周期的数据)

实验结果

以下图片和动画展示了实验的运行结果

优化前电路图 电路原理图

设备视图 设备视图

功率报告 功率报告

项目概要 项目概要

优化后电路图 优化后电路图

时序分析报告(存在时序违规) 时序分析报告

检查点文件 检查点文件

最终测试

最终测试

3.3 实验 3

重要文件: lab3 实验手册

代码解析 代码部分和lab2一样,不赘述

实验结果

时序报告 时序报告

输出数据路径 输出数据路径

修改后的时序 修改后的时序

设备路径 设备路径

项目概要 项目概要

时序报告_imp 时序报告实现

*修改后的时序_imp" 修改后的时序实现

时钟路径 时钟路径

最终测试

和lab2现象一样,具体可以看lab2的最终测试

3.4 实验 4

重要文件: lab4 实验手册

实验4主要是教学使用IP核,但是这里对于新版本的vivado存在一个兼容性问题,需要打开IP Sources, 右键char_fifo,选择Upgrade IP,然后再进行IP核的配置。

更改前会显示红色的锁定标志:

更改前

右键打开的锁定原因:

锁定原因

升级后的char_fifo:

这里多了两个端口,具体作用可以自己检索,这两个端口由于之前没有会报错未连接,不影响本身实验,忽略报错即可。

升级后

报错内容:

报错内容

后续第五个大步骤的升级IP核心就不需要再次进行升级了,因为已经升级过了

实验结果

IP核心summary IP核心summary

device与utilization device与utilization

最终测试 最终测试

3.5 实验 5

重要文件: lab5 实验手册

该实验主要是教学使用IO Planing进行约束,代码和实验2是一样的。

需要注意的点:在top module改名后,在project summary中的top module也要改名,否则会出现找不到ios模块的错误。

*实验结果

找到Y9_PIN 时钟路径找到Y9_PIN

完成所有设置 完成所有设置

路径11 路径11

最终测试

和lab2现象一样,具体可以看lab2的最终测试

3.6 实验 6

重要文件: lab6 实验手册

该实验主要是教学使用Debug进行调试,具体内容和实验2是一样的。

实验结果

debug路径 debug路径

等待trigger 等待trigger

4.HLS_lab

4.1 实验1

这个实验可以注意到有很多个版本,首先是最原始的使用vivadoHLS的版本,这个适用较老的工具。其次是Vitis HLS的版本,这个适用于较新的工具。还有最新的Vitis版本,这个版本我使用跑完了实验1同时有一个教学文档。

Vitis的教学文档 Vitis开发lab1

4.2 实验2

可与使用强制命令打开Vitis_HLS软件的项目,使用代码:

vitis_hls -p <your prj name> -classic

请注意,要使用Vitis_HLS的命令行工具完成该操作。

打开会提示GUI未来将会弃用

本人遇到一定的小问题无法正常打开,但是这个流程操作应该是没有太大问题的

4.3 实验3

make之后可以使用类似于lab1的教程部分,使用Vitis进行操作,可以跟着跑完流程。

几乎一样 只是前面的选择文件还有top_function命名之类的不一样而已。

4.4 实验4

官方仓库有一个是直接打包好的prj,使用Vitis的terminal直接编译TCL文件即可直接输出最后的IP核文件。

5.课程设计(开发中)

勘误

错误1

lab2 的顶层文件 uart_led.v 中,存在一个关于 rst_clk 信号处理的错误: 该错误源于在设计中对 rst_clk 信号应用了反转操作,导致按键未按下时(默认低电平),rst_clk 被错误地反转为高电平,使得复位信号始终处于激活状态。由于复位信号持续有效,系统在无操作状态下无法正常工作,必须手动按下复位键,才能看到期望的实验现象。

具体出现在uart_led.v的69行:

    .signal_src   (~rst_pin),

相同代码逻辑的lab3、lab5、lab6中并未出现此问题,为正常的不加反转操作

错误的逻辑实验现象:

错误现象

错误2

在lab4中步骤2-2-42-2-5并不需要进行操作,因为本身的clk_core clk_core_i0已经存在了,不需要再次添加。

错误位置