尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA呼吸灯设计:基于Verilog的PWM控制与Vivado实现

FPGA呼吸灯设计:基于Verilog的PWM控制与Vivado实现 简介围绕Nexys4 DDR FPGA开发板实现的RGB呼吸灯控制项目面向FPGA初学者、数字电路设计爱好者及电子类课程实验者。通过一个可观测的LED渐变项目串起FPGA基本概念、硬件描述语言编程、GPIO引脚配置、时序逻辑设计、仿真验证、JTAG下载调试等关键环节。项目基于Xilinx Vivado IDE重点演示如何用VHDL或Verilog编写RGB三色亮度控制逻辑通过计数器与比较器构造呼吸渐变时序并完成综合、布局布线、比特流生成到板级验证的完整流程。压缩包大小约597KB文件总数与类型明细暂未提供但内容足以支撑一个完整的入门实践。目前已有482人学习下载。使用这份资源可学习到FPGA并行处理特性、Nexys4 DDR板载外设的调用方法以及利用示波器或板载按钮观察调试信号、排查时序逻辑问题的实用技巧同时还能理解PWM或占空比调节在LED亮度控制中的应用为后续更复杂的数字系统设计打下基础。对希望从理论走向硬件实操、快速积累FPGA开发经验的读者是一份值得反复研读的参考。1. 呼吸灯不是点灯而是把“亮度包络”做成硬件逻辑Nexys4 DDR 上那颗三合一 RGB LED 表面看只是三个引脚控制亮灭真正写“呼吸灯”时你会发现难点根本不在“亮”上而在“从暗到亮再到暗”这条包络线怎么产生。单片机可以靠定时器中断和 PWM 外设完成FPGA 则要用计数器、比较器和状态逻辑在硬件层面构造一个可调节的 PWM 源。这个项目把 FPGA 入门最常见的知识点包括时钟分频、时序控制、Vivado 仿真和 JTAG 下载压缩进一个十几秒的视觉循环里。无论你是刚开始写 Verilog还是已经做过模块但没碰过 Artix-7都值得把它完整走一遍先定呼吸灯的数学模型再写 RTL最后上板调参数整条链路不需要额外硬件。2. 从点灯到呼吸PWM 占空比、呼吸周期和通道相位2.1 呼吸灯真正需要的是“人眼看不见”的亮度调制直接给 LED 通电只能得到固定亮度。想让亮度连续变化必须让 LED 在一个很小的周期内快速开关用“开的时间占整个周期的比例”去模拟不同亮度这个比例就是 PWM 占空比。呼吸灯的信息藏在占空比随时间变化的曲线里占空比先单调上升再单调下降形成一个类似正弦的包络。人眼对足够快的变化感知是平滑的所以 PWM 频率不能太低工程上习惯用 1kHz 以上如果低于几百 Hz灯会在呼吸过程中出现明显闪烁甚至能看到 LED 在 PWM 周期内抖动的暗影。Nexys4 DDR 的系统时钟是 100MHz所有计数基准都从这 100MHz 展开后续调整周期和分辨率都很直接。2.2 用计数器与比较器构造 PWM 源FPGA 里没有现成的 PWM 外设最常规的做法是一个计数器加一个比较器。计数器从 0 计到PWM_MAX后回到 0输出信号在计数器小于当前亮度阈值时拉低、大于阈值时拉高。这样每过PWM_MAX个时钟周期输出端就完成一个完整的 PWM 周期。亮度阈值来自呼吸状态机阈值大LED 亮的时间长阈值小LED 基本熄灭。整个过程是纯并行逻辑不会占用 CPU也不会像软件轮询那样出现相位抖动。// pwm_cnt 是自由运行的 10 bit 计数器0~1023 循环 reg [9:0] pwm_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) pwm_cnt 10d0; else pwm_cnt pwm_cnt 10d1; end // 假设 LED 低电平点亮占空比越高灯越亮 wire [9:0] pwm_th {brightness, 2b00}; assign led_r (pwm_cnt pwm_th) ? 1b0 : 1b1;这里的{brightness, 2b00}等价于brightness左移两位把 8 bit 亮度阈值扩展到 10 bit 比较空间。100MHz 时钟下pwm_cnt每 1024 个周期循环一次PWM 频率约为 97.7kHz既避开闪烁区又保留 1024 级定时分辨率。想提高分辨率可以把计数器和阈值同时加宽到 16 bit占空比台阶会从 256 级变成 65536 级代价只是增加少量逻辑单元对 Artix-7 来说没有任何压力。2.3 三路通道之间不要各写一套状态机RGB 呼吸灯的有趣之处在于颜色会在红、绿、蓝之间连续漂移。如果为 R/G 和 B 各写一套递增递减状态机代码冗余不说相位对齐也容易出问题。更干净的做法是维护一个共享的呼吸相位计数器用同一个值计算出三路亮度。比如把整个呼吸周期切成 512 份红绿蓝分别取相位偏移 0、171、342三路之间固定相差 120 度。这样只需要一个计数器三个灯珠的呼吸周期天然保持同步颜色从红色渐变到青色再变回红色时不会出现某一路突然跳变的毛刺。参数含义本次取值影响clk开发板系统时钟100 MHz所有计数器的基础节拍PWM_MAXPWM 周期计数上限1023决定 PWM 频率和占空比分辨率DIV_LIMIT呼吸步进的时钟间隔586000步进越慢呼吸周期越长phase_cnt共享呼吸相位计数0~511三通道亮度的查找索引呼吸周期和DIV_LIMIT的关系是T 512 * DIV_LIMIT / fclk。按当前取值计算一次完整的渐亮渐暗需要512 x 586000 / 100MHz约 3.0 秒是人眼感觉最自然的节奏。想改成 2 秒只需要把DIV_LIMIT换成 390625想改成 1 秒换成 195312。这个公式在后面调参时还会继续用到。3. Verilog 实现一个共享相位、三路偏移的 RGB 呼吸控制器3.1 端口、参数和整体结构顶层模块只需要五个信号时钟clk、低有效复位rst_n、三根 LED 输出led_r/g/b。在 Nexys4 DDR 上100MHz 有源晶振接到专用时钟引脚板上 CPU_RESETN 按键可以作为复位输入。RGB LED 是共阳极接法阴极由 FPGA 引脚驱动所以输入高电平时灯灭输入低电平时灯亮输出极性要在 RTL 里提前规划好。3.2 完整 RTL 代码module rgb_breath ( input wire clk, input wire rst_n, output wire led_r, output wire led_g, output wire led_b ); localparam [19:0] DIV_LIMIT 20d586000; localparam [9:0] PWM_MAX 10d1023; reg [19:0] div_cnt; reg tick; reg [8:0] phase_cnt; // 0~511形成 512 步呼吸周期 reg [9:0] pwm_cnt; // 呼吸步进使能每 586000 个时钟产生一个 tick always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 20d0; tick 1b0; end else if (div_cnt DIV_LIMIT - 1) begin div_cnt 20d0; tick 1b1; end else begin div_cnt div_cnt 1; tick 1b0; end end // 共享呼吸相位 always (posedge clk or negedge rst_n) begin if (!rst_n) phase_cnt 9d0; else if (tick) phase_cnt phase_cnt 1; end // 自由运行的 PWM 计数器 always (posedge clk or negedge rst_n) begin if (!rst_n) pwm_cnt 10d0; else pwm_cnt pwm_cnt 1; end // 三通道取不同相位偏移模 512 回绕 wire [8:0] r_ph phase_cnt; wire [8:0] g_ph phase_cnt 9d171; wire [8:0] b_ph phase_cnt 9d342; // 相位 - 三角波亮度0~255 上升256~511 下降 wire [7:0] red_bright (r_ph 9d256) ? r_ph[7:0] : 9d511 - r_ph; wire [7:0] green_bright (g_ph 9d256) ? g_ph[7:0] : 9d511 - g_ph; wire [7:0] blue_bright (b_ph 9d256) ? b_ph[7:0] : 9d511 - b_ph; // PWM 比较输出低电平点亮 assign led_r (pwm_cnt {red_bright, 2b00}) ? 1b1 : 1b0; assign led_g (pwm_cnt {green_bright, 2b00}) ? 1b1 : 1b0; assign led_b (pwm_cnt {blue_bright, 2b00}) ? 1b1 : 1b0; endmodulediv_cnt每计数 585999 个时钟产生一个tick对应步进间隔约 5.86ms。phase_cnt在tick有效时加一从 0 循环到 511因此完整呼吸周期为512 x 5.86ms约 3.0 秒。r_ph、g_ph、b_ph分别是相位 0、171、342。因为三个表达式都限定在 9 bit 线网里加法溢出自动取模 512不需要手动判断边界。绿色通道落后红色 171 个步进蓝色通道又落后绿色 171 个步进合起来就是 120 度相位差。3.3 这个设计里值得注意的细节第一个细节是相位到亮度的映射用了简单的三角波而不是正弦波。三角波在 FPGA 里只是一个比较加一个减法不占 DSP 块正弦波则要维护查找表或调用 CORDIC IP。呼吸灯的视觉差异在慢节奏下并不明显三角波足够用。真正要关注的是上升沿和下降沿的步进大小red_bright每次 tick 变化 1 个亮度级在低亮度区人眼会更敏感如果希望暗部过渡更细腻可以把phase_cnt扩展到 10 bit亮度仍然取 8 bit但峰值附近需要重新压缩映射。第二个细节是 PWM 比较用了而不是。由于 LED 低电平点亮pwm_cnt大于等于阈值时输出 1 表示熄灭小于阈值时输出 0 表示点亮。如果把比较方向写反灯会绝大部分时间点亮呼吸包络倒置看起来像是“反向呼吸”并且整体亮度明显偏亮。换用高电平点亮的开发板时只需把这两个分支对调不需要动相位和分频逻辑。第三个细节是呼吸步进使用了单独的tick标志而不是在div_cnt 0的同一个 always 块里让phase_cnt累加。tick只维持一个时钟周期的脉冲避免div_cnt清零的时序和phase_cnt更新的时序在同一个时钟沿互相干扰。所有寄存器赋值都采用非阻塞赋值这样在 Vivado 综合后两段逻辑会在同一个时钟沿采样不会因为 always 块的执行顺序产生额外毛刺。4. Vivado 工程搭建、XDC 约束与仿真验证4.1 用 Tcl 脚本把工程建起来Vivado GUI 创建工程容易点乱尤其换电脑后要复现。我习惯把建工程流程写成 Tcl 脚本既方便重新跑也可以发给别人做对照。假设 RTL 文件放在./rtl/rgb_breath.v约束文件放在./constraints/nexys4ddr.xdc最小脚本如下set top rgb_breath set proj_dir ./rgb_breath_prj create_project $top $proj_dir -part xc7a100tcsg324-1 add_files -norecurse ./rtl/rgb_breath.v set_property top $top [current_fileset] read_xdc ./constraints/nexys4ddr.xdc set_property used_in_synthesis true [get_files ./constraints/nexys4ddr.xdc] launch_runs synth_1 -jobs 4 wait_on_run synth_1 launch_runs impl_1 -to_step write_bitstream -jobs 4 wait_on_run impl_1-part xc7a100tcsg324-1对应 Nexys4 DDR 使用的 Artix-7 器件资源规模对这个呼吸灯项目来说绰绰有余。launch_runs会按顺序完成综合、实现和 bitstream 生成最后在rgb_breath_prj.runs/impl_1/下找到.bit文件。如果工程文件已经存在需要先close_project再重新执行如果想重新综合先reset_run synth_1再跑后续否则 Vivado 会提示 run 已经完成。4.2 XDC 引脚约束别凭记忆填约束文件至少要有时钟、复位和三根 LED 引脚。Nexys4 DDR 的原理图引脚号不能凭印象填最稳妥的办法是在 Vivado 里打开Board窗口用图形化 I/O Planning 自动生成。以下是一个可替换引脚占位符的 XDC 模板set_property PACKAGE_PIN pin_clk [get_ports {clk}] set_property IOSTANDARD LVCMOS33 [get_ports {clk}] create_clock -period 10.000 -name sys_clk [get_ports {clk}] set_property PACKAGE_PIN pin_rst_n [get_ports {rst_n}] set_property IOSTANDARD LVCMOS33 [get_ports {rst_n}] set_property PACKAGE_PIN pin_r [get_ports {led_r}] set_property IOSTANDARD LVCMOS33 [get_ports {led_r}] set_property PACKAGE_PIN pin_g [get_ports {led_g}] set_property IOSTANDARD LVCMOS33 [get_ports {led_g}] set_property PACKAGE_PIN pin_b [get_ports {led_b}] set_property IOSTANDARD LVCMOS33 [get_ports {led_b}]把pin_clk等替换成原理图上的实际引脚即可。如果不想手动翻原理图直接打开 Digilent 官方维护的 Nexys4 DDR Master XDC 文件从里面复制时钟引脚和 RGB LED 引脚段。注意不要混用 Nexys4 和 Nexys4 DDR 的约束两者板载外设引脚并不完全一致。引脚写错了通常不会烧板子但会导致 bitstream 下载后看不到呼吸效果排查起来比改逻辑更费时间。4.3 仿真验证先看时序再看波形Vivado Simulator 免费集成在工具里不需要额外授权。写一个最小 testbench给clk生成 100MHz 激励rst_n先拉低再拉高然后跑约 50ms就能在波形窗口里看到三路 PWM 和呼吸包络的相位关系。module tb_rgb_breath(); reg clk 0; reg rst_n 0; wire led_r, led_g, led_b; always #5 clk ~clk; // 100MHz rgb_breath dut( .clk(clk), .rst_n(rst_n), .led_r(led_r), .led_g(led_g), .led_b(led_b) ); initial begin repeat(20) (posedge clk); rst_n 1; end initial begin #50_000_000; $finish; end endmodule50ms 仿真时间只覆盖呼吸周期的很小一段但足够确认三路 PWM 输出和相位偏移是否正确。重点观察phase_cnt在tick之后是否按预期累加以及red_bright、green_bright、blue_bright三者是否保持 171 步的固定间距。想让颜色从红到青完整转一圈需要跑到 3 秒以上仿真会明显变慢我一般只在确认包络函数有问题时才跑全周期。波形窗口里还可以把pwm_cnt放大到局部确认 PWM 计数器没有在复位释放后进入不确定状态。5. 上板调光JTAG 下载、周期修正和波形验证5.1 通过 Hardware Manager 下载 bitstream综合实现完成后Vivado 左侧打开 Hardware Manager连接 Nexys4 DDR 的 USB 口板载 CPLD 会把 FPGA 的 JTAG 链路映射成 USB 设备。点击Open Target后选择 Artix-7 器件再Program Device选择刚才生成的.bit文件。如果烧录后灯不亮先检查 XDC 里三根 LED 引脚是否和板上丝印对应另一处容易漏的是复位引脚rst_n接的是 CPU_RESETN 按键按键释放时是高电平按下才是低电平仿真里rst_n0的时序正好对应按键按下状态。5.2 修改呼吸周期和观察点想改变呼吸速度只需要改DIV_LIMIT。公式是DIV_LIMIT fclk * T_target / 512。如果目标周期是 2 秒则100_000_000 * 2 / 512 390625目标周期是 1 秒则195312。参数位宽 20 bit 可以覆盖最大 10 秒以上的呼吸周期再长就需要扩宽div_cnt。注意不要只改DIV_LIMIT而忽略pwm_cnt位宽PWM 频率当前约 97.7kHz如果为了减少逻辑把pwm_cnt缩到 8 bitPWM 频率会升到约 390kHz但亮度台阶只剩 256 个颜色渐变过程会出现肉眼可见的色阶断层呼吸效果反而不顺滑。5.3 呼吸不顺滑时的补救方法板上 RGB LED 阳极接电源、阴极接 FPGA 引脚因此熄灭电平是 1点亮电平是 0。如果你拿到的是别人改过的工程或者自己把比较逻辑写反了灯会在绝大多数时间亮着只在很短的时间里熄灭人眼看到的是“反向呼吸”而不是柔和渐变。遇到这种问题不要急着改 RTL先在 Hardware Manager 里把led_r的信号改成常 0看灯是否变亮确认硬件极性后再回到代码里调整比较符号。另一个视觉问题是三角波在峰值附近变化率最慢亮度停留在最高点附近的时间相对长体感比正弦更“憋气”。想获得更自然的呼吸曲线可以把第 3 章的三角波映射换成 32 点正弦查找表phase_cnt的高 5 位作为查表索引输出 8 bit 亮度值查找表用case语句或ROM实现都可以Artix-7 片上 BRAM 完全放得下综合时间也不会增加多少。本文还有配套的精品资源点击获取
返回列表