1. 项目概述与设计思路

交通灯控制系统是FPGA学习的经典项目,能够综合运用计数器、状态机、数码管显示等多个知识点。我在实际项目中经常使用VHDL来实现这类系统,因为它能很好地描述硬件并行特性。这个设计需要处理50MHz时钟分频、交通灯状态转换、数码管动态显示,还要考虑紧急情况的特殊处理。

刚开始接触这个项目时,我觉得最头疼的是如何协调三个并行进程的工作。reg进程负责分频计数,com进程处理交通灯状态,smg进程控制数码管显示。每个进程都有自己的敏感信号列表,如果设置不当就会出现奇怪的时序问题。我记得第一次仿真时,数码管显示总是错乱,排查了好久才发现是敏感信号没设置正确。

对于初学者来说,建议先理解清楚每个模块的功能再开始编码。交通灯系统其实就是一个状态机,东西方向和南北方向的灯状态有固定的转换顺序。数码管显示则需要处理十进制转换和动态扫描,这部分对时序要求比较严格。

2. 系统架构与模块划分

整个系统我分为三个主要进程,这样结构清晰,也便于调试。reg进程负责将50MHz时钟分频到1Hz,这是整个系统的时间基准。在实际实现中,分频系数需要根据实际时钟频率调整。我通常会用两个计数器来实现:clk1计数到9999,clk2计数到4999,这样配合起来就能得到准确的1秒信号。

com进程是核心的状态控制部分。这里我定义了一个枚举类型来表示红绿灯的三种状态:s0(绿灯)、s1(黄灯)、s2(红灯)。通过counter的值来判断当前应该处于哪个状态。东西方向和南北方向的状态转换是相互关联的,一个方向绿灯时,另一个方向必须是红灯。

smg进程处理数码管显示,这是最容易出问题的地方。动态扫描需要足够快的刷新频率,否则会出现闪烁。我一般用20ms的刷新周期,这样人眼就看不到闪烁了。数码管显示需要处理十位和个位的分离,这里要注意当数字小于10时,十位应该显示空白而不是0。

type FSM is (s0, s1, s2);
signal LED_1, LED_2: FSM;
signal counter: integer range 0 to 49;
signal smg_1, smg_2: integer range 0 to 24;
signal change: bit;

3. 时钟分频与计数器设计

时钟分频是数字系统设计的基础,但也是容易出错的地方。50MHz到1Hz的分频系数很大,需要多级分频来实现。我采用两级计数器:clk1从0计数到9999,产生5kHz信号;clk2再从0计数到4999,最终得到1Hz信号。

在实际项目中,我建议使用更灵活的分频方法。可以定义一个常数来表示分频系数,这样需要修改频率时只需要改一个地方。另外,要注意计数器的位宽,确保能覆盖整个计数范围。

reg:process(clk, special)
variable clk1: integer range 0 to 9999;
variable clk2: integer range 0 to 4999;
begin
    if clk'event and clk='1' then
        if clk1 = 9999 then
            clk1 := 0;
            if clk2 = 4999 then
                clk2 := 0;
                if special = '0' then
                    change <= not change;
                    if counter = 49 then
                        counter <= 0;
                    else
                        counter <= counter + 1;
                    end if;
                end if;
            else
                clk2 := clk2 + 1;
            end if;
        else
            clk1 := clk1 + 1;
        end if;
    end if;
end process reg;

counter信号是整个系统的时序基准,它从0到49循环计数,对应50秒的完整周期。在每个状态中,我们需要根据counter的值来计算数码管应该显示的数字。比如东西方向绿灯时,数码管从20开始倒计时;黄灯时从5开始倒计时。

4. 状态机设计与交通灯控制

状态机设计是交通灯系统的核心。我采用摩尔型状态机,输出只与当前状态有关。东西方向和南北方向的状态机是相互关联的,需要仔细设计状态转换条件。

在实际道路中,交通灯的时序是有严格要求的。绿灯结束后必须经过黄灯过渡,然后才能切换到红灯。两个方向的绿灯时间不能重叠,否则会发生交通事故。在这个设计中,东西方向绿灯20秒,黄灯5秒,红灯25秒;南北方向红灯25秒,绿灯20秒,黄灯5秒。

com:process(counter, special)
begin
    if special = '1' then
        out_1 <= "100";  -- 红灯
        out_2 <= "100";  -- 红灯
    else
        if counter < 20 then
            LED_1 <= s0; smg_1 <= 20 - counter;  -- 东西绿灯
            LED_2 <= s0; smg_2 <= 25 - counter;  -- 南北红灯
        elsif counter < 25 then
            LED_1 <= s1; smg_1 <= 25 - counter;  -- 东西黄灯
            LED_2 <= s0; smg_2 <= 25 - counter;  -- 南北红灯
        elsif counter < 45 then
            LED_1 <= s2; smg_1 <= 50 - counter;  -- 东西红灯
            LED_2 <= s1; smg_2 <= 45 - counter;  -- 南北绿灯
        else
            LED_1 <= s2; smg_1 <= 50 - counter;  -- 东西红灯
            LED_2 <= s2; smg_2 <= 50 - counter;  -- 南北黄灯
        end if;
        
        -- 输出灯状态
        case LED_1 is
            when s0 => out_1 <= "010";  -- 绿灯
            when s1 => out_1 <= "001";  -- 黄灯
            when s2 => out_1 <= "100";  -- 红灯
        end case;
        
        case LED_2 is
            when s0 => out_2 <= "100";  -- 红灯
            when s1 => out_2 <= "010";  -- 绿灯
            when s2 => out_2 <= "001";  -- 黄灯
        end case;
    end if;
end process com;

状态机的设计要注意避免毛刺和竞争现象。我建议使用同步设计,所有状态转换都在时钟边沿进行。输出信号最好经过寄存器同步,这样可以改善时序特性。

5. 数码管显示与动态扫描

数码管显示是系统中比较复杂的部分,需要处理数字分解、段码转换和动态扫描。七段数码管的每个段对应一个比特位,需要根据要显示的数字输出相应的段码。

动态扫描的原理是利用人眼的视觉暂留效应,快速切换显示不同位。在这个系统中,有两个数码管,需要交替显示十位和个位。刷新频率要足够快,一般至少50Hz,否则会出现闪烁。

smg:process(clk, change, special)
variable a, b: bit;
variable c1, c2: integer range 0 to 10;
begin
    if clk'event and clk='1' then
        a := not a;
        b := not b;
        
        -- 东西方向数码管
        case a is
            when '0' => 
                l1 <= "01";  -- 显示个位
                c1 := smg_1 rem 10;
            when '1' => 
                l1 <= "10";  -- 显示十位
                c1 := smg_1 mod 10;
                if c1 = 0 then c1 := 10; end if;  -- 十位为0时不显示
        end case;
        
        -- 南北方向数码管
        case b is
            when '0' => 
                l2 <= "01";  -- 显示个位
                c2 := smg_2 rem 10;
            when '1' => 
                l2 <= "10";  -- 显示十位
                c2 := smg_2 mod 10;
                if c2 = 0 then c2 := 10; end if;  -- 十位为0时不显示
        end case;
        
        -- 特殊情况处理:数码管闪烁
        if special = '1' and change = '1' then
            c1 := 10;  -- 不显示
            c2 := 10;  -- 不显示
        end if;
        
        -- 段码转换
        case c1 is
            when 0 => display_1 <= "0111111";
            when 1 => display_1 <= "0000110";
            -- 其他数字的段码
            when others => display_1 <= "0000000";  -- 不显示
        end case;
        
        case c2 is
            when 0 => display_2 <= "0111111";
            when 1 => display_2 <= "0000110";
            -- 其他数字的段码
            when others => display_2 <= "0000000";  -- 不显示
        end case;
    end if;
end process smg;

数码管显示有几个常见的坑需要注意。首先是段码的极性,共阴和共阳数码管的段码是相反的。其次是刷新频率,太慢会闪烁,太快会亮度不足。最后是数字分解,要正确处理十位和个位,特别是当数字小于10时,十位应该显示空白。

6. 特殊情况处理与优化

交通灯系统必须考虑特殊情况处理,比如紧急车辆通过、交通事故等。在这个设计中,我使用special信号来处理特殊情况。当special为'1'时,所有方向都显示红灯,数码管闪烁提示。

数码管闪烁是通过change信号实现的。change每秒取反一次,当special为'1'且change为'1'时,数码管不显示任何内容,实现闪烁效果。这种设计既简单又有效,在实际项目中很实用。

-- 在reg进程中处理特殊情况
if special = '0' then
    change <= not change;
    if counter = 49 then
        counter <= 0;
    else
        counter <= counter + 1;
    end if;
end if;

-- 在smg进程中处理闪烁
if special = '1' and change = '1' then
    c1 := 10;  -- 不显示
    c2 := 10;  -- 不显示
end if;

除了特殊情况处理,我还对数码管显示进行了优化。原来的设计中,当数字小于10时,十位数码管会显示0,这不符合实际使用习惯。优化后,当十位为0时,数码管不显示,这样看起来更自然。

另一个优化点是状态机的输出寄存器。我建议所有输出信号都经过寄存器同步,这样可以改善时序特性,减少毛刺。在FPGA设计中,寄存器输出通常能获得更好的时序性能。

7. Quartus II仿真与调试

Quartus II是FPGA设计的主要工具,提供了完整的开发环境。在进行仿真前,需要先创建工程,添加VHDL文件,然后进行综合。综合通过后,才能进行功能仿真和时序仿真。

在仿真时,我通常先简化设计,比如将时钟分频部分注释掉,直接使用1Hz时钟进行仿真。这样可以加快仿真速度,也更容易发现问题。等基本功能验证通过后,再恢复完整的设计。

-- 仿真测试脚本示例
library ieee;
use ieee.std_logic_1164.all;

entity testbench is
end testbench;

architecture behav of testbench is
    signal clk: std_logic := '0';
    signal special: bit := '0';
    -- 其他信号声明
    
    component fisrt is
        port(
            clk: in std_logic;
            special: in bit;
            l1, l2: out std_logic_vector(1 downto 0);
            display_1, display_2: out std_logic_vector(6 downto 0);
            out_1, out_2: out std_logic_vector(2 downto 0)
        );
    end component;
begin
    -- 时钟生成
    clk <= not clk after 10 ns;  -- 50MHz
    
    -- 实例化设计
    uut: fisrt port map(
        clk => clk,
        special => special,
        -- 其他端口连接
    );
    
    -- 测试过程
    process
    begin
        special <= '0';
        wait for 100 ns;
        
        -- 测试正常情况
        wait for 50 sec;
        
        -- 测试特殊情况
        special <= '1';
        wait for 5 sec;
        special <= '0';
        
        wait;
    end process;
end behav;

仿真中常见的问题有时序违反、信号冲突、状态机死锁等。对于时序问题,需要检查时钟域交叉和信号路径。对于功能问题,需要仔细分析波形,查看状态转换是否正确。

8. 常见问题与解决方案

在实现交通灯系统的过程中,我遇到过各种各样的问题。最常见的是语法错误,比如忘记写end if、端口映射错误等。这些错误通常会在编译时被发现,但错误信息有时不太明确。

另一个常见问题是时序违规。当系统频率较高时,组合逻辑路径可能过长,导致建立时间或保持时间违规。解决方法包括插入寄存器、优化逻辑结构、降低频率等。

状态机设计也容易出现问题。比如状态编码不合适导致毛刺过多,或者状态转换条件不完整导致死锁。我建议使用独热码编码状态,这样既能减少毛刺,又能提高可靠性。

数码管显示问题也比较常见。比如闪烁、亮度不均、显示错误等。这些问题通常与刷新频率、段码极性、数字分解有关。需要仔细检查动态扫描的逻辑和时序。

在实际项目中,我还遇到过一个棘手的问题:按键抖动。如果使用按键输入special信号,必须进行消抖处理,否则会出现误触发。简单的消抖方法是用计数器和状态机实现。

-- 简单的按键消抖实现
process(clk)
    variable count: integer range 0 to 100000;
begin
    if clk'event and clk='1' then
        if key_in /= key_stable then
            count := count + 1;
            if count >= 100000 then  -- 消抖时间约2ms
                key_stable <= key_in;
                count := 0;
            end if;
        else
            count := 0;
        end if;
    end if;
end process;

9. 扩展功能与进阶设计

基本的交通灯系统实现后,可以考虑添加一些扩展功能。比如根据车流量自动调整绿灯时间,添加人行横道信号灯,实现夜间模式等。

车流量检测可以通过传感器实现,比如地感线圈或摄像头。检测到的车流量数据可以用来动态调整绿灯时间,提高交通效率。这需要更复杂的状态机和算法。

人行横道信号灯需要与机动车信号灯协调工作。通常在人行绿灯期间,机动车应该是红灯。还需要考虑行人过街时间,设置足够的绿灯时间。

夜间模式可以简化交通灯的工作方式,比如让主要方向常绿,次要方向常红,或者让所有方向闪烁黄灯提示减速慢行。

另一个有趣的扩展是添加远程监控功能。通过通信接口将交通灯状态发送到监控中心,实现集中管理和故障诊断。这需要添加通信模块,比如UART或以太网接口。

-- 简单的UART发送实现
process(clk)
    variable bit_count: integer range 0 to 7;
    variable state: uart_state;
begin
    if clk'event and clk='1' then
        case state is
            when idle =>
                if send_enable = '1' then
                    tx_data <= data_to_send;
                    tx_busy <= '1';
                    state := start_bit;
                    bit_count := 0;
                end if;
                
            when start_bit =>
                uart_tx <= '0';
                state := data_bits;
                
            when data_bits =>
                uart_tx <= tx_data(bit_count);
                if bit_count = 7 then
                    state := stop_bit;
                else
                    bit_count := bit_count + 1;
                end if;
                
            when stop_bit =>
                uart_tx <= '1';
                tx_busy <= '0';
                state := idle;
        end case;
    end if;
end process;

这些扩展功能可以让交通灯系统更加智能和实用,但也增加了设计的复杂性。建议在基本功能稳定后再逐步添加扩展功能,每次只添加一个功能,充分测试后再继续。

10. 实战经验与技巧分享

在多年的FPGA开发中,我积累了一些实战经验和技巧。首先是版本控制的重要性。即使是个人项目,也建议使用Git进行版本管理。每次修改前提交代码,这样出现问题可以快速回退。

其次是注释和文档。良好的注释能大大提高代码的可读性和可维护性。我习惯在每个模块开头注释功能说明,在重要代码段注释设计思路。仿真波形和测试用例也要妥善保存。

调试技巧也很重要。Quartus II提供了Signal Tap逻辑分析仪,可以实时抓取FPGA内部的信号波形。对于复杂问题,这是一种非常有效的调试手段。另外,好的仿真测试平台能大大减少调试时间。

代码风格方面,我建议遵循一些基本规范。比如使用有意义的信号名,模块化设计,避免过深的嵌套等。一致的代码风格能让项目更易于理解和维护。

最后是性能优化技巧。对于时序紧张的设计,可以考虑流水线设计、逻辑复用、资源共享等方法。面积优化则可以通过状态编码优化、逻辑压缩等手段实现。

在实际项目中,我还发现测试用例设计非常重要。好的测试用例应该覆盖正常情况、边界情况和异常情况。自动化测试能大大提高测试效率和可靠性。

这些经验和技巧都是在实际项目中慢慢积累的,有些是通过踩坑学到的。希望这些分享能帮助大家少走弯路,更高效地完成FPGA设计项目。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐