1. BASYS3开发板开箱初体验

第一次拿到BASYS3开发板时,我像个拿到新玩具的孩子一样兴奋。这块由Xilinx推出的FPGA开发板特别适合初学者,板载资源丰富但又不至于复杂到让人望而生畏。板子正面最显眼的就是那8颗排列整齐的LED灯,它们将成为我们第一个实验的主角。

BASYS3采用的是Artix-7系列的FPGA芯片,具体型号是xc7a35tcpg236-1。这个35T的芯片对于入门学习来说绰绰有余,它包含了33280个逻辑单元、1800Kb的块RAM和90个DSP切片。板子上还集成了USB转JTAG编程接口、VGA接口、USB HOST接口等实用外设,不过我们今天只需要关注最简单的LED部分。

开发环境我选择了Vivado 2019.1,这是Xilinx官方提供的集成开发环境,从设计输入到生成比特流文件都能一站式完成。安装过程比较常规,但要注意的是Vivado体积较大,建议预留至少50GB的硬盘空间。安装完成后,第一次启动可能会有点慢,这是正常现象。

2. 流水灯设计方案详解

2.1 功能需求分析

流水灯看似简单,但作为第一个FPGA项目,我们需要把它拆解清楚。基本需求是让8颗LED灯依次点亮,全亮后再依次熄灭,如此循环。每个状态保持1秒,这样人眼才能清晰看到变化过程。

这里有几个关键点需要考虑:

  1. 时间控制:如何精确产生1秒的时间间隔
  2. 状态切换:如何实现LED灯的依次点亮和熄灭
  3. 循环控制:如何让这个过程无限循环

2.2 系统架构设计

我决定采用模块化设计,将系统分为两个主要部分:

  1. 计时模块:负责产生精确的1Hz时钟信号
  2. 译码模块:根据当前状态控制LED灯的亮灭模式

这种分模块的设计方法在FPGA开发中很常见,每个模块专注于单一功能,最后通过信号连接起来。这样做不仅逻辑清晰,也便于后续调试和功能扩展。

3. RTL代码实现过程

3.1 计时模块编码

计时模块的核心任务是利用板载的100MHz时钟产生1Hz的信号。100MHz意味着每秒钟有100,000,000个时钟周期,要得到1Hz信号,我们需要对时钟进行分频。

module leds(
    input clk,        //100Mhz系统时钟
    input rst,        //复位信号
    output reg [2:0] ctrl_data, //控制数据
    output led_clk,   //1Hz时钟
    output led_cs     //片选信号
);
    
    reg[27:0] cnt;    //计数值
    parameter cnt_max = 28'd99_999_999; //计数最大值
    parameter cnt_half = 28'd49_999_999; //计数中值
    
    reg led_clk_r;    //1Hz方波时钟
    reg led_cs_r;     //片选信号
    
    // 产生1Hz方波
    always @ (posedge clk or posedge rst) begin
        if(rst) cnt <= 28'd0;
        else if(cnt < cnt_max) cnt <= cnt+1'b1;
        else cnt <= 28'd0;
    end
    
    always @ (posedge clk or posedge rst) begin
        if(rst) led_clk_r <= 1'b0;
        else if(cnt < cnt_half) led_clk_r <= 1'b0;
        else led_clk_r <= 1'b1;
    end
    
    assign led_clk = led_clk_r;
    
    // 在1Hz时钟下计数
    always @ (negedge led_clk or posedge rst) begin
        if(rst) begin
            ctrl_data <= 3'd0;
            led_cs_r <= 1'b0;
        end
        else if(ctrl_data < 3'd7) ctrl_data <= ctrl_data+1'b1;
        else begin
            ctrl_data <= 3'd0;
            led_cs_r <= ~led_cs_r;
        end
    end
    
    assign led_cs = led_cs_r;
endmodule

这段代码有几个关键点:

  1. 使用28位计数器cnt来累计时钟周期
  2. 当计数达到49,999,999时翻转led_clk_r信号,产生占空比50%的方波
  3. 在1Hz时钟的下降沿触发状态切换

3.2 译码模块实现

译码模块的作用是根据计时模块提供的控制信号,输出对应的LED亮灭模式。这里我借鉴了3-8译码器的思路,但做了些调整以适应我们的需求。

module code(
    input clk,         //100Mhz系统时钟
    input rst,         //复位信号
    output reg [7:0] leds_out //输出LED信号
);
    
    wire [2:0] ctrl;   //控制地址
    wire cs;           //片选信号
    
    // 实例化计时模块
    leds myleds(
        .clk(clk),
        .rst(rst),
        .ctrl_data(ctrl),
        .led_cs(cs)
    );
    
    // 类3-8译码器设计
    always @ (ctrl or cs or rst) begin
        if(rst) leds_out = 8'b0000_0000;
        else begin
            if(!cs) begin  // 依次点亮
                case(ctrl)
                    3'b000: leds_out = 8'b0000_0001;
                    3'b001: leds_out = 8'b0000_0011;
                    //...其他状态
                    3'b111: leds_out = 8'b1111_1111;
                endcase
            end
            else begin    // 依次熄灭
                case(ctrl)
                    3'b000: leds_out = 8'b1111_1110;
                    3'b001: leds_out = 8'b1111_1100;
                    //...其他状态
                    3'b111: leds_out = 8'b0000_0000;
                endcase
            end
        end
    end
endmodule

这个模块通过case语句实现了真值表功能,根据片选信号cs的不同,分别处理点亮和熄灭两个过程。这种组合逻辑设计简单直接,非常适合我们的需求。

4. 仿真验证与调试

4.1 测试平台搭建

在将代码烧录到FPGA之前,仿真是必不可少的步骤。我编写了两个测试模块,分别验证计时模块和整个系统的功能。

module leds_sim;
    reg clk;
    reg rst;
    wire [2:0] ctrl_data;
    wire led_clk;
    wire led_cs;
    
    leds text_leds(
        .clk(clk),
        .rst(rst),
        .ctrl_data(ctrl_data),
        .led_clk(led_clk),
        .led_cs(led_cs)
    );
    
    initial begin
        clk = 0;
        rst = 0;
        #1000 rst = 1;
        #1000 rst = 0;
    end
    
    always #5 clk = ~clk; // 100MHz时钟
endmodule

这个测试平台主要验证计时模块能否正确产生1Hz信号。由于仿真时不可能真的运行1亿个周期,在实际项目中我们会调整参数来缩短仿真时间。

4.2 仿真结果分析

通过Vivado的仿真工具,我们可以清晰地看到各个信号的变化情况。计时模块的仿真显示:

  1. clk是100MHz的系统时钟
  2. led_clk确实被分频成了1Hz
  3. ctrl_data在led_clk的每个周期递增
  4. 当ctrl_data达到7后,led_cs信号会翻转

系统级仿真则验证了LED输出信号的正确性,能够按照预期依次点亮和熄灭。

5. 硬件实现与上板调试

5.1 管脚约束配置

仿真通过后,接下来就是硬件实现的关键步骤——管脚分配。BASYS3的管脚定义可以在官方文档中找到,我们需要将设计中的信号映射到具体的物理管脚上。

set_property PACKAGE_PIN W5 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]

set_property PACKAGE_PIN T17 [get_ports rst]
set_property IOSTANDARD LVCMOS33 [get_ports rst]

set_property PACKAGE_PIN U16 [get_ports {leds_out[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds_out[0]}]
//...其他LED管脚类似

这些约束告诉Vivado如何将逻辑信号连接到实际的物理管脚。特别注意电平标准要设置为LVCMOS33,这是BASYS3使用的3.3V电平标准。

5.2 生成比特流与下载

在Vivado中完成综合、实现和生成比特流后,就可以通过USB连接开发板进行下载了。第一次使用时可能需要安装驱动,Vivado通常会提示相关操作。

下载完成后,按下开发板上的复位按钮,就能看到LED灯开始按照预期流动起来。如果效果不理想,可能需要检查:

  1. 管脚分配是否正确
  2. 时钟频率设置是否准确
  3. 复位信号极性是否正确

6. 常见问题与解决经验

在实际操作中,我遇到过几个典型问题,这里分享下解决经验:

  1. 时序不满足:第一次综合时出现了时序违例警告。解决方法是在Vivado中增加时序约束,明确告诉工具我们的时钟频率要求。

  2. LED灯不亮:检查发现是管脚分配错误,将LED输出分配到了错误的管脚。通过仔细核对原理图解决了这个问题。

  3. 流水速度过快:最初设计时计数器位宽不够,导致流水速度比预期快很多。通过仿真发现并修正了这个错误。

  4. 复位信号无效:由于BASYS3的按钮是按下为高电平,我的代码中复位逻辑写反了。通过测试平台发现了这个问题。

这个简单的流水灯项目虽然基础,但涵盖了FPGA开发的完整流程:从需求分析、架构设计、代码实现、仿真验证到最终的上板调试。通过这个实践,我对Verilog语言和Vivado工具链都有了更深入的理解。

更多推荐