1. 项目概述与设计思路

最近在整理大学时期的FPGA课程设计,翻出来一个经典的交通灯控制器项目。这个项目可以说是数字逻辑设计和硬件描述语言(HDL)入门的“必修课”,它麻雀虽小,五脏俱全,涵盖了状态机设计、时钟分频、动态扫描显示、模块化设计等核心知识点。当时我用的是Altera的Cyclone系列FPGA,型号是EP1CQ240C8N,虽然现在看有点古老,但设计思路和方法论至今依然通用。这个项目的核心是实现一个智能十字路口交通灯控制,主干道和支干道根据车流情况动态调整通行时间,而不是简单的固定周期切换。对于刚接触FPGA的朋友来说,如果能吃透这个项目,对理解同步时序电路设计和Verilog编码风格会有质的飞跃。

简单来说,我们要做的是一个“感应式”交通灯控制器。它有一条长期繁忙的主干道和一条车流量较小的支干道。系统默认让主干道常绿,保证主干道畅通。只有当支干道检测到有车等待时(通过一个 car 信号),系统才会启动一个完整的红绿灯切换周期:主干道绿灯转黄灯再转红灯,同时支干道红灯转绿灯。通行时间上,主干道每次45秒,支干道每次25秒,每次绿灯转红灯前都有5秒黄灯作为缓冲,并且所有时间都需要用数码管进行倒计时显示。一个周期结束后,系统会再次检测支干道是否有车,有车则继续下一个周期,没车则让主干道恢复常绿状态。这个需求非常贴近实际应用,比简单的定时切换更有挑战性,也更能锻炼我们的系统设计能力。

2. 系统架构与模块化设计解析

面对这样一个项目,直接写一个大模块把所有功能塞进去是新手常犯的错误,这会导致代码难以调试、维护和复用。正确的做法是采用“自顶向下”的模块化设计思想。我们可以把整个系统拆解成几个功能明确、接口清晰的子模块,最后用一个顶层模块像搭积木一样把它们连接起来。这样做的好处是,每个模块可以独立设计、仿真和测试,极大降低了整体复杂度。

根据功能需求,我最终将系统划分为三个核心子模块和一个顶层模块:

  1. 时钟分频模块 ( fenpinqi ) :负责将外部高频系统时钟(如10kHz)分频产生我们需要的低频时钟(如1Hz,用于计时)。
  2. 交通灯控制及计时模块 ( control ) :这是整个系统的大脑,核心是一个有限状态机(FSM)。它根据 car 信号和计时器的值,决定交通灯的状态(红、黄、绿)和倒计时数值。
  3. 扫描显示译码模块 ( saomiao ) :负责驱动4位数码管,动态显示两个方向(主干道和支干道)的倒计时时间。
  4. 顶层模块 ( jiaotongdeng ) :实例化上述所有模块,并定义它们之间的连线关系。

这种划分方式使得每个模块的职责单一:分频模块只管时钟,控制模块只管逻辑和计时,显示模块只管驱动数码管。在顶层模块中,我们只需要关心端口映射和线网连接,结构非常清晰。在开始编码前,绘制一张系统框图是极其重要的,它能帮你理清数据流和控制流。下图展示了我设计时的系统详细框图,它清晰地标明了模块间的信号传递关系:

          +-----------------+
          |  顶层模块        |
          | jiaotongdeng    |
          |                 |
clk, rst, car |                 | led[5:0], sel[6:0], seg[3:0]
----------->|                 |----------------->
          |                 |
          |  +-----------+  |
          |  | 分频模块  |  |
          |  | fenpinqi  |  |
          |  +-----+-----+  |
          |        | clk_odd |
          |        v        |
          |  +-----------+  |
          |  | 控制模块  |  |
          |  | control   |  |
          |  +-----+-----+  |
          |        | count_H/L_1/2 |
          |        v        |
          |  +-----------+  |
          |  | 显示模块  |  |
          |  | saomiao   |  |
          |  +-----------+  |
          +-----------------+

注意 :在FPGA设计中,清晰的模块划分和接口定义是项目成功的第一步。务必在写代码前完成框图,并反复确认每个信号的位宽、方向和含义。我见过很多同学因为接口定义混乱,导致调试时信号对不上,浪费大量时间。

3. 核心模块设计与实现细节

3.1 时钟分频模块:精准的“心跳”发生器

任何时序电路都离不开一个稳定可靠的时钟。我们的系统需要两个不同频率的时钟:一个高频时钟(如10kHz)用于数码管的动态扫描,防止视觉闪烁;一个低频时钟(1Hz)作为状态机和计时器的基准“秒”信号。 fenpinqi 模块的任务就是把输入的10kHz时钟,转换成一个占空比为50%的1Hz方波。

这里的关键是分频系数的计算。输入时钟频率 f_in = 10kHz = 10,000 Hz ,输出时钟频率 f_out = 1Hz 。那么分频比 N = f_in / f_out = 10,000 。这意味着我们需要对输入时钟计数10,000次,才能让输出时钟翻转一次(产生半个周期)。为了得到50%占空比,我们通常在计数值达到 N/2 - 1 时进行翻转。因此,我们需要一个计数器,计数范围是0到 N/2-1 ,即0到4999。

module fenpinqi(clk, rst, clk_odd);
    input clk, rst;
    output reg clk_odd;
    reg [13:0] count; // 需要计数到4999,2^13=8192 > 4999,所以用14位宽足够
    parameter N = 10000;

    always @ (posedge clk) begin
        if(!rst) begin // 同步复位,低电平有效
            count <= 14'b0;
            clk_odd <= 1'b0;
        end
        else if (count < N/2 - 1) begin // 计数未到半周期
            count <= count + 1'b1;
        end
        else begin // 计数达到半周期,翻转输出时钟,计数器清零
            count <= 14'b0;
            clk_odd <= ~clk_odd;
        end
    end
endmodule

实操心得

  1. 计数器位宽选择 :计算 N/2 - 1 的最大值(这里是4999),然后确定需要多少位二进制数来表示它。 2^12 = 4096 不够, 2^13 = 8192 足够,所以计数器 count 需要至少13位。我习惯多留一点余量,这里用了14位( [13:0] )。
  2. 同步复位 :代码中使用了同步复位( always @ (posedge clk) ),复位信号 rst 只在时钟上升沿被检测。这是FPGA设计中的推荐做法,有利于时序收敛和避免复位信号上的毛刺引起问题。注意复位时要把所有寄存器和输出都置为确定值。
  3. 参数化设计 :使用 parameter N = 10000; 来定义分频系数,而不是直接把数字 5000 写在条件判断里。这样做的巨大好处是 可移植性 。如果将来时钟频率变了(比如板子换成了50MHz晶振),你只需要修改这一个参数 N ,而不需要去代码里寻找所有相关的数字进行修改,极大减少了出错概率。

3.2 交通灯控制及计时模块:系统的“大脑”与状态机设计

这是整个设计最核心、最复杂的部分。它需要实现需求中描述的所有智能控制逻辑:默认状态、有车感应、定时切换、黄灯过渡、周期循环等。用一堆 if-else 语句嵌套当然也能实现,但代码会像一团乱麻,难以阅读和调试。最佳实践是使用 有限状态机(Finite State Machine, FSM)

根据需求,我们可以抽象出4个有效状态(S0, S1, S2, S3)和一个隐含的初始/等待状态。让我们来定义它们:

  • S0 (主干道绿灯,支干道红灯) :这是系统的默认状态。只要支干道没车( car=0 ),就一直保持在这个状态。当检测到支干道有车( car=1 ),并且主干道绿灯时间(45秒)倒计时结束,才会离开此状态。
  • S1 (主干道黄灯,支干道红灯) :主干道绿灯时间到,进入5秒黄灯过渡期。此状态只持续5秒,时间一到立即切换。
  • S2 (主干道红灯,支干道绿灯) :支干道获得通行权,绿灯亮25秒。
  • S3 (主干道红灯,支干道黄灯) :支干道绿灯时间到,进入5秒黄灯过渡期。

状态之间的转换条件完全由时间和 car 信号决定。下图清晰地描绘了状态流转的全过程:

         [支干道无车/car=0]
               +---+
               |   |
               v   |
      +------> S0 <------------------+
      |        |                     |
      |        | [有车且主干道45秒倒计时结束]
      |        v                     |
      |        S1 (黄灯5秒)          |
      |        |                     |
      |        | [5秒倒计时结束]      |
      |        v                     |
      |        S2 (绿灯25秒)         |
      |        |                     |
      |        | [25秒倒计时结束]    |
      |        v                     |
      |        S3 (黄灯5秒)          |
      |        |                     |
      +--------+ [5秒倒计时结束]      |

状态机编码与计时逻辑融合 : 在Verilog中,我们通常用两个 always 块来描述状态机:一个时序逻辑块用于状态寄存器更新,一个组合逻辑块用于产生次态和输出。但在我们这个相对简单的Moore型状态机中(输出仅与当前状态有关),并且输出和计时器强相关,我选择用一个 always 块同时处理状态转移和计时。这样代码更紧凑。

module control(led, car, rst, clk, count_H_1, count_L_1, count_H_2, count_L_2);
    output reg [5:0] led; // 输出交通灯状态,例如 6‘b100010 可能表示:主干道红,支干道绿
    output reg [3:0] count_H_1, count_L_1, count_H_2, count_L_2; // 两个方向的倒计时十位和个位
    input clk, rst, car; // clk是1Hz时钟
    reg [1:0] state; // 状态寄存器,2位可表示4个状态

    // 状态编码,采用独热码(One-Hot)或二进制码。这里用二进制码。
    parameter S0 = 2'b00, // 主绿支红
              S1 = 2'b01, // 主黄支红
              S2 = 2'b10, // 主红支绿
              S3 = 2'b11; // 主红支黄

    always @(posedge clk or negedge rst) begin
        if(!rst) begin // 异步复位,初始化所有状态和输出
            led <= 6'b010100; // 假设:位[5:4]主干道,[3:2]黄,[1:0]支干道。010100可能表示主绿支红
            state <= S0;
            count_H_1 <= 4'd0; count_L_1 <= 4'd0;
            count_H_2 <= 4'd0; count_L_2 <= 4'd0;
        end
        else begin
            case(state)
                S0: begin
                    // 状态S0逻辑:主干道绿灯,支干道红灯
                    led <= 6'b010100; // 保持灯的状态
                    // 主干道倒计时设置:45秒
                    count_H_1 <= 4'd4; // 十位为4
                    count_L_1 <= 4'd5; // 个位为5
                    // 支干道倒计时在S0状态下通常不显示或显示固定值,这里我们让它也倒计时,但显示值可能被忽略
                    count_H_2 <= 4'd2; // 为下一个状态S2(支干道绿灯25秒)做准备?这里设计需要仔细推敲
                    count_L_2 <= 4'd5;

                    // 状态转移判断:如果支干道有车,并且主干道倒计时结束,则进入黄灯状态S1
                    // 注意:倒计时逻辑需要在每个时钟周期递减,这部分代码需要补充在下面
                    if(car && (count_H_1==0 && count_L_1==0)) begin
                        state <= S1;
                        // 进入S1前,重置黄灯倒计时为5秒
                        count_H_1 <= 4'd0; count_L_1 <= 4'd5;
                    end
                    // 如果支干道一直没车,就保持在S0状态,这是一个关键点!
                end
                // S1, S2, S3 状态逻辑类似,需要完整实现
                S1: begin
                    // 主干道黄灯,支干道红灯
                    led <= 6'b001100; // 假设编码
                    // 黄灯倒计时5秒
                    if(count_L_1 == 0) begin
                        if(count_H_1 == 0) begin // 5秒倒计时结束
                            state <= S2;
                            // 进入S2,设置支干道绿灯25秒,主干道红灯(倒计时可能显示为0或不变)
                            count_H_2 <= 4'd2; count_L_2 <= 4'd5;
                            led <= 6'b100010;
                        end
                        else begin
                            count_H_1 <= count_H_1 - 1'b1;
                            count_L_1 <= 4'd9; // 个位从9开始减
                        end
                    end
                    else begin
                        count_L_1 <= count_L_1 - 1'b1;
                    end
                end
                // ... S2和S3的状态逻辑需要类似地补充完整
                default: state <= S0; // 防止进入未知状态
            endcase
        end
    end
endmodule

重要提示 :上面的代码是一个 简化框架 ,用于说明思路。原始项目代码(在输入材料中)的计时逻辑更加复杂,它在一个状态中同时处理两个方向的倒计时,并且对 car 信号的判断逻辑嵌套在状态内部。在实现时,你必须非常小心地设计每个状态下的计时器加载和递减逻辑,确保状态转换的瞬间,计时器能正确加载新值,并且在状态保持期间能正确递减。这是调试中最容易出错的地方。

倒计时设计技巧 : 倒计时通常用两个4位BCD码计数器表示十位和个位。例如,45秒表示为 count_H_1=4 , count_L_1=5 。递减时,先判断个位是否为0,如果为0,则需要向十位借位(十位减1,个位变为9),否则个位直接减1。当十位和个位都为0时,表示倒计时结束。在状态转换的边界(如绿灯45秒结束,要进入5秒黄灯),需要将计时器重新加载为新的初值(如5秒黄灯,则加载为 count_H_1=0, count_L_1=5 )。

3.3 扫描显示译码模块:让信息“看得见”

FPGA开发板上的数码管通常是共阳或共阴的,并且为了节省IO口,多位数码管会采用 动态扫描 的方式驱动。原理是利用人眼的视觉暂留效应,依次快速点亮每一位数码管,只要扫描频率足够快(通常>60Hz),看起来就像是同时点亮的。

我们的系统需要显示4位数:主干道时间的十位和个位,支干道时间的十位和个位。 saomiao 模块的任务就是:

  1. 分频产生扫描时钟 :系统输入时钟可能是10kHz或更高,我们需要进一步分频得到一个几百Hz到1kHz左右的扫描时钟( scan_clk ),用于切换数码管位选。
  2. 位选扫描 :用一个2位计数器循环产生 00, 01, 10, 11 ,分别对应4个数码管。在 scan_clk 的驱动下,依次选中每一位。
  3. 数据选择与译码 :根据当前的位选信号,从 count_H_1, count_L_1, count_H_2, count_L_2 这四个输入中,选择对应的一位数据(一个4位BCD码)。
  4. 段码译码 :将选中的4位BCD码(0-9)翻译成7段数码管(a-g)的驱动信号(段码 sel[6:0] )。同时,将位选计数器的值译码为具体的位选控制信号( seg[3:0] ,假设是共阴数码管,低电平选中)。
module saomiao(rst, clk, count_H_1, count_L_1, count_H_2, count_L_2, sel, seg);
    input rst, clk; // clk是高频系统时钟,如10kHz
    input [3:0] count_H_1, count_L_1, count_H_2, count_L_2;
    output reg [6:0] sel; // 7段段码输出
    output reg [3:0] seg; // 4位位选信号
    reg [15:0] counter; // 用于产生扫描时钟的分频计数器
    reg scan_clk; // 扫描时钟
    reg [1:0] bit_sel; // 位选计数器,0~3循环
    reg [3:0] data_temp; // 临时存储当前要显示的数据

    // 第一部分:产生扫描时钟 (例如,从10kHz分频到约200Hz)
    always@(posedge clk or negedge rst) begin
        if(!rst) begin
            counter <= 16'd0;
            scan_clk <= 1'b0;
        end
        // 假设分频系数为 10000 / (2*200) = 25
        else if(counter == 16'd24) begin // 计数到N/2 -1
            counter <= 16'd0;
            scan_clk <= ~scan_clk; // 翻转,产生占空比50%的方波
        end
        else begin
            counter <= counter + 1'b1;
        end
    end

    // 第二部分:位选计数器循环
    always @(negedge rst or posedge scan_clk) begin
        if (!rst) begin
            bit_sel <= 2'b00;
        end
        else begin
            bit_sel <= bit_sel + 1'b1; // 0->1->2->3->0...
        end
    end

    // 第三部分:根据位选,选择要显示的数据,并产生位选信号
    always@(posedge clk) begin // 这里可以用系统时钟,保证响应速度
        if(!rst) begin
            seg <= 4'b1111; // 假设共阴数码管,位选高电平无效,全部关闭
            data_temp <= 4'b0000;
        end
        else begin
            case (bit_sel)
                2'b00: begin // 选中第1位数码管(例如主干道时间十位)
                    seg <= 4'b1110; // 点亮第1位
                    data_temp <= count_H_1;
                end
                2'b01: begin // 选中第2位数码管(主干道时间个位)
                    seg <= 4'b1101; // 点亮第2位
                    data_temp <= count_L_1;
                end
                2'b10: begin // 选中第3位数码管(支干道时间十位)
                    seg <= 4'b1011;
                    data_temp <= count_H_2;
                end
                2'b11: begin // 选中第4位数码管(支干道时间个位)
                    seg <= 4'b0111;
                    data_temp <= count_L_2;
                end
                default: begin
                    seg <= 4'b1111;
                    data_temp <= 4'b0000;
                end
            endcase
        end
    end

    // 第四部分:将数据译码为7段段码
    always @ (*) begin // 组合逻辑,data_temp或seg变化时立即更新sel
        case(data_temp)
            4'b0000: sel = 7'b1111110; // 显示数字0,段码abcdefg,假设共阴,1点亮
            4'b0001: sel = 7'b0110000; // 1
            4'b0010: sel = 7'b1101101; // 2
            4'b0011: sel = 7'b1111001; // 3
            4'b0100: sel = 7'b0110011; // 4
            4'b0101: sel = 7'b1011011; // 5
            4'b0110: sel = 7'b1011111; // 6
            4'b0111: sel = 7'b1110000; // 7
            4'b1000: sel = 7'b1111111; // 8
            4'b1001: sel = 7'b1111011; // 9
            default: sel = 7'b1111110; // 默认显示0
        endcase
    end
endmodule

避坑指南

  1. 扫描频率选择 :扫描时钟不能太慢,否则会看到闪烁;也不能太快,否则每个数码管点亮时间太短,亮度会不足。一般选择在60Hz~1kHz之间。例如,如果系统时钟是10kHz,分频到200Hz,那么每位显示时间约5ms,4位一个循环是20ms,刷新率50Hz,基本无闪烁。
  2. 段码与位选信号更新时机 :位选信号 seg 应该在扫描时钟 scan_clk 的驱动下切换。而段码 sel 应该在 data_temp 确定后立即更新(用组合逻辑 always @(*) )。确保在切换位选之前,新的段码数据已经稳定,否则会出现“鬼影”(上一个数字的残影)。
  3. 共阴与共阳 :务必查清你使用的开发板上的数码管是共阴(Common Cathode)还是共阳(Common Anode)。这决定了段码和位选信号的电平逻辑是相反的。上面的代码假设是共阴数码管(位选低电平选中,段码高电平点亮)。如果是共阳的,需要将所有段码和位选信号取反。

3.4 顶层模块:完成最后的“拼图”

顶层模块的设计相对简单,但至关重要。它不包含任何具体的逻辑功能,只做三件事:

  1. 声明与外部FPGA引脚对应的输入输出端口。
  2. 实例化我们之前设计好的三个子模块。
  3. wire 型线网正确连接这些模块。
module jiaotongdeng(clk, rst, car, led, sel, seg);
    // 1. 端口声明
    input clk;      // 10kHz系统时钟
    input rst;      // 复位信号
    input car;      // 支干道车辆检测信号
    output [5:0] led; // 6位交通灯控制信号
    output [6:0] sel; // 7段数码管段码
    output [3:0] seg; // 4位数码管位选

    // 2. 内部线网声明,用于模块间连接
    wire clk_1hz; // 连接分频模块输出和控制模块输入
    wire [3:0] count_H_1, count_L_1, count_H_2, count_L_2; // 连接控制模块输出和显示模块输入

    // 3. 模块实例化
    // 格式:模块名 实例化名 (端口连接);
    fenpinqi u_fenpinqi (
        .clk(clk),
        .rst(rst),
        .clk_odd(clk_1hz)
    );

    control u_control (
        .clk(clk_1hz), // 注意!控制模块使用1Hz时钟
        .rst(rst),
        .car(car),
        .led(led),
        .count_H_1(count_H_1),
        .count_L_1(count_L_1),
        .count_H_2(count_H_2),
        .count_L_2(count_L_2)
    );

    saomiao u_saomiao (
        .clk(clk), // 显示模块使用原始高频时钟
        .rst(rst),
        .count_H_1(count_H_1),
        .count_L_1(count_L_1),
        .count_H_2(count_H_2),
        .count_L_2(count_L_2),
        .sel(sel),
        .seg(seg)
    );

endmodule

连接要点

  • 时钟路径 u_fenpinqi 产生的 clk_1hz 精确地提供给 u_control 作为其时钟信号。而 u_saomiao 需要高频时钟进行动态扫描,因此直接使用输入的系统时钟 clk 绝对不要 把1Hz时钟给显示模块,否则扫描会慢得肉眼可见。
  • 数据路径 u_control 计算出的倒计时数值 count_H_1 等,通过内部线网直接传递给 u_saomiao 进行显示。
  • 命名规范 :实例化名 u_xxx 是一种常见的命名习惯, u 代表 instance 。清晰的命名在调试查看网表时会非常有帮助。

4. 关键问题排查与调试经验

即使代码写得再仔细,第一次上板调试也难免遇到问题。下面分享几个我在做这个项目时踩过的坑以及解决方法,希望能帮你节省时间。

4.1 数码管显示乱码或不亮

这是最常见的问题。

  • 检查段码和位选的真值表 :这是第一步,也是最重要的一步。确认你的 sel 段码和 seg 位选信号对应的电平逻辑(共阴/共阳)与开发板原理图完全一致。一个快速测试方法是写一个简单的测试程序,固定显示一个数字(如“8.”),看所有段是否能亮。再依次点亮每一位数码管,看位选是否正确。
  • 检查扫描频率 :用示波器或逻辑分析仪测量 scan_clk seg 信号的频率。如果频率太低(<50Hz)会闪烁,太高(>1kHz)可能亮度不均或驱动能力不足。调整分频计数器的值。
  • 检查“鬼影” :如果显示的数字有重影,通常是段码数据在位选切换时没有保持稳定。确保你的段码输出逻辑( always @(*) )是组合逻辑,并且其输入 data_temp seg 变化前已经稳定。有时在 seg 赋值和 sel 赋值之间加一个短暂的延时(用时钟同步)可以解决。
  • 检查复位信号 :确保显示模块在复位时, seg 位选信号处于“全部关闭”状态(例如共阴数码管, seg 赋值为全1)。否则可能一上电就导致多个数码管同时被选中,电流过大。

4.2 交通灯状态切换不正常或计时不准

  • 仿真!仿真!仿真! :在烧录到FPGA前,务必用ModelSim、Vivado Simulator等工具进行功能仿真。编写一个简单的测试平台(Testbench),给 clk car 信号施加激励,观察 state led count_* 等信号的变化波形。这是定位状态机逻辑错误最有效的方法。
  • 检查状态编码和转换条件 :对照状态转换图,在仿真波形中一步步跟踪。特别注意状态转换的边界条件,比如是否在倒计时 恰好为0 的那个时钟沿发生转换? car 信号是电平敏感还是边沿敏感?在需求中, car 信号在S0状态被检测,但它是持续检测还是在周期末检测?代码实现必须与设计意图严格一致。
  • 检查计时器加载值 :这是极易出错的地方。例如,从S0(45秒绿灯)切换到S1(5秒黄灯)时,主干道计时器应该从5开始倒计时,而不是从45继续减。仔细检查每个 state case 分支里,在什么条件下加载计时器初值。 建议 :在状态转换的瞬间( state <= next_state; ),同步地将对应计时器设置为新状态的初值。
  • 1Hz时钟是否准确 :用示波器测量 clk_1hz 信号的频率和占空比。如果分频模块计算错误,1Hz不准,那么所有计时都会同比加快或减慢。

4.3 车辆检测信号 car 的防抖处理

在实际中,车辆检测传感器(如地感线圈、红外)产生的 car 信号很可能带有毛刺。如果直接接到状态机,一次毛刺就可能误触发一次完整的红绿灯周期。

  • 解决方案:添加消抖模块 。这是一个简单的数字滤波器。其原理是,当检测到 car 信号变化时,并不立即响应,而是启动一个计数器(例如延时20ms),在此期间持续采样该信号。如果20ms后信号仍保持为新状态,则认为这是一个有效的车辆到来/离开信号,否则视为抖动忽略。
  • 如何添加 :你可以在 car 信号进入 control 模块前,插入一个消抖模块。或者更简单,在 control 模块的状态机中,对 car 信号进行边沿检测而非电平检测,也能在一定程度上避免长毛刺的影响。但对付密集抖动,专用的消抖模块更可靠。

4.4 资源占用与时序问题

对于这么小的设计,在EP1CQ240上资源肯定绰绰有余。但养成好习惯很重要。

  • 查看综合报告 :编译完成后,IDE(如Quartus II)会给出资源占用报告(Logic Elements, Registers, Memory bits)。检查是否在合理范围内。
  • 查看时序报告 :重点关注“Timing Analyzer”报告中的“Worst-case Slack”。如果为负值,说明存在时序违例,最坏路径的延迟超过了时钟周期。对于这个设计,时钟频率很低(1Hz和10kHz),基本不会有时序问题。但如果将来设计复杂了,这就是性能瓶颈的标志。解决方法包括流水线、重定时、优化关键路径逻辑等。

5. 项目扩展与优化思路

完成基础功能后,你可以尝试以下扩展,让项目更具挑战性和实用性:

  1. 增加紧急车辆优先通行 :增加一个输入信号 emergency 。当此信号有效时,无论当前处于何种状态,立即让主干道和支干道都亮红灯(或让主干道绿灯,支干道红灯),并持续一段时间,模拟救护车、消防车通过。
  2. 通行时间可配置 :通过拨码开关或按键,动态设置主干道和支干道的绿灯时间(如45秒、60秒、30秒)和黄灯时间。这需要增加输入接口,并在状态机中用变量代替固定的计时初值。
  3. 增加夜间模式 :增加一个模式选择信号。夜间模式下,两个方向可以都切换为黄灯闪烁状态,提醒司机谨慎通过。
  4. 使用更高效的状态机编码 :当前使用的是顺序二进制码(00,01,10,11)。可以尝试使用 独热码(One-Hot) ,例如用4位表示4个状态:S0=0001, S1=0010, S2=0100, S3=1000。独热码在FPGA中通常有更好的时序性能,因为状态译码更简单。
  5. 模块接口标准化 :可以考虑使用 总线或标准接口 (如简单的Valid/Ready握手信号)来连接模块,尤其是计时数据从控制模块传到显示模块。虽然对这个简单项目有点“杀鸡用牛刀”,但这是学习复杂系统互联的好机会。
  6. 加入LED流水灯效果 :在黄灯期间,让对应的黄灯以一定频率闪烁,而不是常亮,更符合一些地区的交通灯实际样式。

这个交通灯控制器项目虽然基础,但它串联起了FPGA开发的核心流程:需求分析、模块划分、HDL编码、功能仿真、综合实现、板级调试。希望这份详细的复盘和解析,能帮助你不仅仅是完成一个课程作业,更是真正理解数字系统设计的精髓。遇到问题时,多画图(状态图、时序图),多仿真,耐心调试,每一步的成长都是实实在在的。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐