news 2026/9/16 16:49:56

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 <= 10'd0; else pwm_cnt <= pwm_cnt + 10'd1; end // 假设 LED 低电平点亮,占空比越高灯越亮 wire [9:0] pwm_th = {brightness, 2'b00}; assign led_r = (pwm_cnt < pwm_th) ? 1'b0 : 1'b1;

这里的{brightness, 2'b00}等价于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 = 20'd586000; localparam [9:0] PWM_MAX = 10'd1023; 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 <= 20'd0; tick <= 1'b0; end else if (div_cnt == DIV_LIMIT - 1) begin div_cnt <= 20'd0; tick <= 1'b1; end else begin div_cnt <= div_cnt + 1; tick <= 1'b0; end end // 共享呼吸相位 always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_cnt <= 9'd0; else if (tick) phase_cnt <= phase_cnt + 1; end // 自由运行的 PWM 计数器 always @(posedge clk or negedge rst_n) begin if (!rst_n) pwm_cnt <= 10'd0; else pwm_cnt <= pwm_cnt + 1; end // 三通道取不同相位偏移,模 512 回绕 wire [8:0] r_ph = phase_cnt; wire [8:0] g_ph = phase_cnt + 9'd171; wire [8:0] b_ph = phase_cnt + 9'd342; // 相位 -> 三角波亮度:0~255 上升,256~511 下降 wire [7:0] red_bright = (r_ph < 9'd256) ? r_ph[7:0] : 9'd511 - r_ph; wire [7:0] green_bright = (g_ph < 9'd256) ? g_ph[7:0] : 9'd511 - g_ph; wire [7:0] blue_bright = (b_ph < 9'd256) ? b_ph[7:0] : 9'd511 - b_ph; // PWM 比较输出,低电平点亮 assign led_r = (pwm_cnt >= {red_bright, 2'b00}) ? 1'b1 : 1'b0; assign led_g = (pwm_cnt >= {green_bright, 2'b00}) ? 1'b1 : 1'b0; assign led_b = (pwm_cnt >= {blue_bright, 2'b00}) ? 1'b1 : 1'b0; endmodule

div_cnt每计数 585999 个时钟产生一个tick,对应步进间隔约 5.86ms。phase_cnttick有效时加一,从 0 循环到 511,因此完整呼吸周期为512 x 5.86ms,约 3.0 秒。r_phg_phb_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 endmodule

50ms 仿真时间只覆盖呼吸周期的很小一段,但足够确认三路 PWM 输出和相位偏移是否正确。重点观察phase_cnttick之后是否按预期累加,以及red_brightgreen_brightblue_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_n=0的时序正好对应按键按下状态。

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 bit,PWM 频率会升到约 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 完全放得下,综合时间也不会增加多少。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:49:14

三相感应电机动态建模与MATLAB仿真实践

1. 三相感应电机动态建模的必要性作为一名在电机控制领域摸爬滚打多年的工程师&#xff0c;我深刻理解三相感应电机启动电流问题带来的困扰。实验室里那台7.5kW电机每次启动时&#xff0c;电流表指针剧烈摆动的场景至今难忘——空载启动电流竟能达到额定值的5-7倍&#xff01;这…

作者头像 李华
网站建设 2026/9/16 16:45:51

纯跟踪算法原理与Matlab实现:路径跟踪关键技术解析

简介&#xff1a;基于Matlab实现纯跟踪&#xff08;Pure Pursuit&#xff09;算法的压缩包&#xff0c;定位清晰&#xff0c;面向自动驾驶路径跟踪、机器人导航和无人机飞行控制等典型场景&#xff0c;适合希望快速掌握该算法的初学者&#xff0c;也适合作为课程实验或工程验证…

作者头像 李华
网站建设 2026/9/16 16:45:41

STM32+OpenMV六轴机械臂颜色识别分拣系统实战解析

简介&#xff1a;基于STM32的六轴机械臂控制与OpenMV颜色识别分拣项目&#xff0c;涵盖运动控制与视觉识别两大核心模块&#xff0c;是一套完整的嵌入式视觉分拣方案&#xff0c;适用于毕业设计、课程设计及期末大作业。项目源码均经本地编译验证可运行&#xff0c;评审分达98分…

作者头像 李华
网站建设 2026/9/16 16:45:28

Pentagi:面向红队工程化的Docker+Neo4j智能协同框架

1. 项目概述&#xff1a;Pentagi 是什么&#xff1f;它不是“AI 渗透测试工具”&#xff0c;而是面向红队工程化的智能协同框架Pentagi 这个名字一出现&#xff0c;很多人第一反应是“又一个带 AI 的渗透测试工具”——毕竟搜索热词里“pentagi”和“penetration testing”“ai…

作者头像 李华
网站建设 2026/9/16 16:44:59

TanStack Query实战指南:高效管理前端服务端状态与缓存

搞前端的人&#xff0c;谁还没在项目里写过一堆useState加useEffect去拉接口的样板代码。数据加载中、报错了、成功拿到数据了、又要刷新了、又要翻页了&#xff0c;每个列表页都来一遍&#xff0c;写的时候不觉得&#xff0c;等项目大了就知道有多痛。TanStack Query&#xff…

作者头像 李华