1. 项目概述:FPGA数字交通灯设计
在嵌入式系统开发领域,FPGA因其高度可编程性和并行处理能力,成为数字电路设计的理想平台。这次我们要实现的是一个典型的数字交通灯控制系统,通过VHDL和Verilog两种硬件描述语言分别实现,并最终在FPGA开发板上运行验证。
这个项目看似简单,但涵盖了数字系统设计的核心要素:时钟管理、状态机控制、外设驱动以及跨语言实现。我在实际教学中发现,很多初学者在实现这类基础项目时,往往会在状态机设计、时序约束和硬件调试等环节遇到问题。本文将结合我多年FPGA开发经验,详细解析每个关键环节的实现要点。
2. 系统架构设计
2.1 功能需求分析
一个完整的交通灯控制系统需要满足以下核心需求:
- 两路信号灯控制(主干道和支干道)
- 符合标准交通灯时序:主干道绿灯→黄灯→红灯,同时支干道红灯→绿灯→黄灯
- 可配置的时序参数(各状态持续时间)
- 硬件复位功能
在实际道路中,交通灯的时序设计需要考虑车流量、行人安全等多重因素。我们的FPGA实现虽然简化了这些复杂因素,但保留了最核心的状态切换逻辑。
2.2 模块化设计
将系统分解为三个关键模块:
-
时钟分频模块:
- 将FPGA板载高频时钟(通常50-100MHz)分频为1Hz低频时钟
- 提供系统基本时间基准
- 支持同步复位
-
状态控制模块:
- 实现四状态有限状态机(FSM)
- 控制两路信号灯的状态转换
- 每个状态持续时间可配置
-
显示驱动模块:
- 将状态信号转换为实际LED控制信号
- 可扩展支持数码管倒计时显示
提示:模块化设计不仅便于调试,也方便后续功能扩展。例如可以单独修改时钟分频参数而不影响状态逻辑。
3. VHDL实现详解
3.1 时钟分频模块
vhdl复制library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity clk_divider is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
clk_out : out STD_LOGIC);
end clk_divider;
architecture Behavioral of clk_divider is
signal counter : STD_LOGIC_VECTOR (23 downto 0) := (others => '0');
begin
process(clk, reset)
begin
if reset = '1' then
counter <= (others => '0');
elsif rising_edge(clk) then
if counter = 24999999 then -- 50MHz→1Hz分频
counter <= (others => '0');
else
counter <= counter + 1;
end if;
end if;
end process;
clk_out <= counter(23); -- 取最高位作为分频输出
end Behavioral;
关键点解析:
-
分频系数计算:50MHz时钟要分频到1Hz,需要50,000,000/1=50,000,000次分频。由于我们使用计数器最高位输出,2^24=16,777,216 < 50,000,000 < 2^25=33,554,432,因此需要25位计数器。但实际代码中使用24位计数器并比较24999999,这是因为计数从0开始。
-
复位设计:同步复位确保计数器在已知状态开始,避免亚稳态问题。
-
输出选择:使用counter(23)而非比较输出,节省了组合逻辑资源。
3.2 状态控制模块
vhdl复制library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
entity traffic_fsm is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
main_red : out STD_LOGIC;
main_yellow: out STD_LOGIC;
main_green : out STD_LOGIC;
sub_red : out STD_LOGIC;
sub_yellow : out STD_LOGIC;
sub_green : out STD_LOGIC);
end traffic_fsm;
architecture Behavioral of traffic_fsm is
type state_type is (S0, S1, S2, S3);
signal current_state, next_state : state_type := S0;
signal timer : integer range 0 to 15 := 0;
begin
-- 状态寄存器
process(clk, reset)
begin
if reset = '1' then
current_state <= S0;
timer <= 0;
elsif rising_edge(clk) then
if timer = 5 then -- 每个状态持续6个时钟周期
current_state <= next_state;
timer <= 0;
else
timer <= timer + 1;
end if;
end if;
end process;
-- 下一状态和输出逻辑
process(current_state)
begin
case current_state is
when S0 => -- 主干道绿灯,支干道红灯
main_green <= '1'; main_yellow <= '0'; main_red <= '0';
sub_red <= '1'; sub_yellow <= '0'; sub_green <= '0';
next_state <= S1;
when S1 => -- 主干道黄灯,支干道红灯
main_green <= '0'; main_yellow <= '1'; main_red <= '0';
sub_red <= '1'; sub_yellow <= '0'; sub_green <= '0';
next_state <= S2;
when S2 => -- 主干道红灯,支干道绿灯
main_green <= '0'; main_yellow <= '0'; main_red <= '1';
sub_red <= '0'; sub_yellow <= '0'; sub_green <= '1';
next_state <= S3;
when S3 => -- 主干道红灯,支干道黄灯
main_green <= '0'; main_yellow <= '0'; main_red <= '1';
sub_red <= '0'; sub_yellow <= '1'; sub_green <= '0';
next_state <= S0;
end case;
end process;
end Behavioral;
状态机设计要点:
- 采用Moore型状态机(输出仅依赖当前状态)
- 明确的状态编码(S0-S3)
- 添加了定时器控制状态持续时间
- 输出信号互斥(同一方向不会同时亮多个灯)
注意:实际应用中,各状态持续时间应该参数化,便于后期调整。这里简化为固定6个时钟周期。
4. Verilog实现对比
4.1 时钟分频模块
verilog复制module clk_divider(
input wire clk,
input wire reset,
output reg clk_out
);
reg [24:0] counter; // 25位计数器
always @(posedge clk or posedge reset) begin
if (reset) begin
counter <= 25'd0;
clk_out <= 1'b0;
end else begin
if (counter == 25'd24999999) begin
counter <= 25'd0;
clk_out <= ~clk_out; // 翻转输出
end else begin
counter <= counter + 1;
end
end
end
endmodule
与VHDL的主要差异:
- Verilog使用always块替代process
- 输出时钟生成方式不同(直接翻转vs取最高位)
- 复位时同时清零counter和clk_out
4.2 状态控制模块
verilog复制module traffic_fsm(
input wire clk,
input wire reset,
output reg main_red,
output reg main_yellow,
output reg main_green,
output reg sub_red,
output reg sub_yellow,
output reg sub_green
);
typedef enum reg [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10,
S3 = 2'b11
} state_type;
state_type current_state, next_state;
reg [3:0] timer;
// 状态寄存器
always @(posedge clk or posedge reset) begin
if (reset) begin
current_state <= S0;
timer <= 4'd0;
end else begin
if (timer == 4'd5) begin
current_state <= next_state;
timer <= 4'd0;
end else begin
timer <= timer + 1;
end
end
end
// 下一状态和输出逻辑
always @(*) begin
case (current_state)
S0: begin
main_green = 1'b1; main_yellow = 1'b0; main_red = 1'b0;
sub_red = 1'b1; sub_yellow = 1'b0; sub_green = 1'b0;
next_state = S1;
end
S1: begin
main_green = 1'b0; main_yellow = 1'b1; main_red = 1'b0;
sub_red = 1'b1; sub_yellow = 1'b0; sub_green = 1'b0;
next_state = S2;
end
S2: begin
main_green = 1'b0; main_yellow = 1'b0; main_red = 1'b1;
sub_red = 1'b0; sub_yellow = 1'b0; sub_green = 1'b1;
next_state = S3;
end
S3: begin
main_green = 1'b0; main_yellow = 1'b0; main_red = 1'b1;
sub_red = 1'b0; sub_yellow = 1'b1; sub_green = 1'b0;
next_state = S0;
end
endcase
end
endmodule
语言特性对比:
- Verilog使用typedef定义枚举类型
- always @(*) 表示组合逻辑
- 状态编码显式指定为二进制值
- 输出赋值使用阻塞赋值(=)
5. 仿真验证与调试
5.1 测试平台设计
verilog复制`timescale 1ns / 1ps
module tb_traffic_fsm();
reg clk;
reg reset;
wire main_red, main_yellow, main_green;
wire sub_red, sub_yellow, sub_green;
traffic_fsm uut (
.clk(clk),
.reset(reset),
.main_red(main_red),
.main_yellow(main_yellow),
.main_green(main_green),
.sub_red(sub_red),
.sub_yellow(sub_yellow),
.sub_green(sub_green)
);
initial begin
clk = 0;
reset = 1;
#100 reset = 0;
#1000 $finish;
end
always #10 clk = ~clk; // 50MHz时钟
endmodule
仿真要点:
- 初始复位信号置高,100ns后释放
- 时钟周期设置为20ns(50MHz)
- 观察各信号灯输出是否符合状态转换图
- 检查状态持续时间是否为6个时钟周期
5.2 常见仿真问题
-
信号未初始化:
- 现象:仿真开始时信号显示为红色(未定义)
- 解决:确保所有寄存器变量都有复位值
-
状态机锁死:
- 现象:状态机卡在某个状态不转换
- 检查:状态转换条件和定时器逻辑
-
时序违例:
- 现象:在时钟边沿附近信号变化
- 解决:添加适当的时序约束
经验分享:在ModelSim中,使用"run -all"命令前先"restart"可以避免一些缓存问题。波形窗口中合理分组信号(如按模块或功能)能显著提高调试效率。
6. 硬件实现与调试
6.1 FPGA引脚约束
以Xilinx Artix-7为例,约束文件(.xdc)关键内容:
tcl复制# 时钟引脚
set_property PACKAGE_PIN E3 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
# 复位按钮
set_property PACKAGE_PIN C12 [get_ports reset]
set_property IOSTANDARD LVCMOS33 [get_ports reset]
# 主干道信号灯
set_property PACKAGE_PIN D4 [get_ports main_red]
set_property PACKAGE_PIN E4 [get_ports main_yellow]
set_property PACKAGE_PIN D3 [get_ports main_green]
# 支干道信号灯
set_property PACKAGE_PIN F4 [get_ports sub_red]
set_property PACKAGE_PIN F3 [get_ports sub_yellow]
set_property PACKAGE_PIN G3 [get_ports sub_green]
6.2 实际调试技巧
-
LED亮度问题:
- FPGA输出电流有限,建议使用晶体管驱动高亮度LED
- 添加限流电阻(通常220Ω-1kΩ)
-
按键消抖:
- 硬件:在复位按钮两端并联0.1μF电容
- 软件:添加消抖计数器(检测稳定20ms以上)
-
时钟稳定性:
- 确保时钟引脚走线尽量短
- 必要时添加时钟缓冲器
-
功耗考虑:
- 未使用的IO引脚设置为高阻态
- 根据LED数量评估电源功率需求
7. 项目扩展方向
-
倒计时显示:
- 增加数码管显示各状态剩余时间
- 需要增加BCD计数器和显示驱动
-
自适应控制:
- 通过传感器检测车流量
- 动态调整各状态持续时间
-
夜间模式:
- 检测环境光强度
- 夜间切换为黄灯闪烁模式
-
多路口联动:
- 多个FPGA通过通信接口同步
- 实现绿波带控制
vhdl复制-- 倒计时显示模块示例
entity countdown is
Port ( clk : in STD_LOGIC;
reset : in STD_LOGIC;
seg : out STD_LOGIC_VECTOR (6 downto 0);
an : out STD_LOGIC_VECTOR (3 downto 0));
end countdown;
architecture Behavioral of countdown is
signal counter : integer range 0 to 9 := 5;
begin
process(clk, reset)
begin
if reset = '1' then
counter <= 5;
elsif rising_edge(clk) then
if counter = 0 then
counter <= 5;
else
counter <= counter - 1;
end if;
end if;
end process;
-- 简化的七段译码
with counter select
seg <= "1000000" when 0, -- '0'
"1111001" when 1, -- '1'
"0100100" when 2, -- '2'
"0110000" when 3, -- '3'
"0011001" when 4, -- '4'
"0010010" when 5, -- '5'
"0000010" when 6, -- '6'
"1111000" when 7, -- '7'
"0000000" when 8, -- '8'
"0010000" when 9, -- '9'
"1111111" when others;
an <= "1110"; -- 只使用第一个数码管
end Behavioral;
8. 经验总结与避坑指南
-
状态机设计:
- 明确状态编码方案(二进制/独热码)
- 确保所有状态都有明确的转换条件
- 添加默认状态处理避免锁死
-
时序约束:
- 添加适当的时钟约束
- 跨时钟域信号使用同步器
- 关键路径添加流水线
-
资源优化:
- 共享计数器资源
- 合理选择信号位宽
- 使用常量替代魔数
-
调试技巧:
- 使用嵌入式逻辑分析仪(如Xilinx ILA)
- 分模块验证功能
- 添加调试输出信号
-
代码规范:
- 统一命名规则(如低有效信号加_n后缀)
- 添加详细注释
- 参数化设计(使用generic/parameter)
在实际项目开发中,我建议先用仿真验证核心逻辑,再逐步添加外设驱动。遇到问题时,采用"二分法"排查——先确认时钟和复位信号正常,再检查状态机转换,最后验证输出驱动。
这个项目虽然基础,但涵盖了FPGA开发的多个核心概念。通过VHDL和Verilog的双重实现,可以更深入理解两种语言的异同。建议初学者在完成基础功能后,尝试上述扩展功能,逐步提升设计能力。