news 2026/2/28 0:10:04

使用vivado2018.3完成PWM信号生成实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用vivado2018.3完成PWM信号生成实战指南

用 Vivado 2018.3 打造一个真正能用的 PWM 信号发生器

最近带学生做 FPGA 实验,发现很多人写完代码、生成比特流下载到板子上,示波器一接——没波形。问起来都说:“我照着例程写的啊?” 结果一看,要么时钟没约束,要么引脚根本没绑对,更有甚者连复位逻辑都没处理好。

今天我们就以Vivado 2018.3为平台,从零开始完整走一遍:如何在 Xilinx 7 系列 FPGA 上实现一个可调频率、可调占空比、稳定可靠的 PWM 模块,并确保它能在真实硬件上跑得起来。

这不是一份“理论正确但实测翻车”的文档,而是一份基于多年调试经验总结出的实战手册。我们不堆术语,只讲你真正需要知道的事。


为什么是 PWM?它到底解决了什么问题?

在嵌入式控制领域,我们经常要解决一个问题:怎么用数字的方式,精准控制模拟的效果?

比如:
- 让 LED 明暗渐变;
- 控制电机转速;
- 调节电源输出电压。

传统做法是用 DAC 输出模拟电压,成本高、抗干扰差。而 PWM 提供了一种优雅的替代方案——通过快速开关来等效出连续电压

它的核心思想很简单:

如果一个信号在一个周期内高电平的时间越长,那么它的平均电压就越高。

这个“高电平时间占比”,就是所谓的占空比(Duty Cycle)。而整个波形重复的速度,就是频率(Frequency)。

举个例子:
- 使用 3.3V 供电;
- 生成 1kHz 的 PWM 信号;
- 占空比设为 60%;

那这个信号的等效直流电压就是3.3V × 60% = 1.98V

更妙的是,在 FPGA 中实现 PWM 几乎不消耗额外硬件资源,只需要几个寄存器和比较器就够了。


为什么选 Vivado 2018.3?

虽然现在最新版 Vivado 已经更新到 2023.x,但很多工业项目仍在使用Vivado 2018.3,原因很现实:

  • 官方长期支持 Zynq-7000 和 Artix-7 等主流器件;
  • 对老开发板兼容性最好(如 Basys3、Nexys4 DDR、ZedBoard);
  • 编译稳定性强,不像新版偶尔出现 IP 加载失败的问题;
  • 很多企业已有成熟流程固化在此版本。

所以如果你正在参与实际产品开发或课程设计,掌握 2018.3 的使用是非常实用的技能。


PWM 是怎么在 FPGA 里“造”出来的?

最常见也最高效的实现方式是:计数器 + 比较器结构。

基本原理一句话说清:

我们让一个计数器每拍加一,当它小于某个设定值时输出高电平,否则输出低电平。计数满后自动归零,周而复始,就形成了固定频率的 PWM 波。

假设系统时钟是 100MHz,我们要产生 1kHz 的 PWM 信号:

  • 一个周期需要计数 $ \frac{100\,000\,000}{1\,000} = 100\,000 $ 次;
  • 所以用一个至少 17 位的计数器($2^{17} = 131072 > 100000$);
  • 当前值小于duty_cycle_reg时,pwm_out = 1
  • 达到最大值后回 0,重新开始。

这样,只要改变duty_cycle_reg的值,就能动态调节占空比。

关键参数怎么算?

参数含义示例
CLK_FREQ输入时钟频率100_000_000 Hz
PWM_FREQ目标 PWM 频率1_000 Hz
COUNTER_WIDTH计数器位宽$\lceil \log_2(100M / 1k) \rceil = 17$

公式如下:

$$
COUNTER_WIDTH = \left\lceil \log_2\left(\frac{CLK_FREQ}{PWM_FREQ}\right) \right\rceil
$$

⚠️ 注意:不要手动计算后硬编码!建议将COUNTER_WIDTH设为参数,方便移植到不同平台。


Verilog 实现:不只是“能编译”,更要“能工作”

下面这段代码,是你能在大多数教程里看到的标准实现。但它有几个隐藏陷阱,稍不留神就会导致波形抖动甚至锁死。

module pwm_generator #( parameter COUNTER_WIDTH = 17 )( input clk, input rst_n, input [COUNTER_WIDTH-1:0] duty_cycle, output reg pwm_out ); reg [COUNTER_WIDTH-1:0] counter; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 0; pwm_out <= 0; end else begin counter <= counter + 1'b1; if (counter == {COUNTER_WIDTH{1'b1}}) counter <= 0; pwm_out <= (counter < duty_cycle) ? 1'b1 : 1'b0; end end endmodule

看起来没问题?其实有三个关键点必须注意:

🔥 问题 1:duty_cycle更新是异步的 → 可能产生毛刺!

如果外部模块直接修改duty_cycle寄存器,而没有同步机制,会导致比较结果在单个时钟周期内突变,从而引起输出跳变不稳定。

解决方案:加入双缓冲机制,或者确保duty_cycle来自同步写入的寄存器文件。

🔥 问题 2:pwm_out是组合逻辑输出 → 存在亚稳态风险!

当前设计中,pwm_out直接受(counter < duty_cycle)控制,属于纯组合逻辑。这意味着每当counter变化时,输出都会立刻响应,容易受到布线延迟影响,尤其是在高速路径上。

改进方法:将输出改为寄存器打一拍:

reg pwm_next; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 0; pwm_next <= 0; pwm_out <= 0; end else begin counter <= counter + 1; if (counter == {COUNTER_WIDTH{1'b1}}) counter <= 0; pwm_next <= (counter < duty_cycle); pwm_out <= pwm_next; // 寄存一次,消除毛刺 end end

虽然延迟了一个周期,但换来的是干净稳定的边沿,值得!

🔥 问题 3:全‘1’判断虽简洁,但可读性差且不易扩展

{COUNTER_WIDTH{1'b1}}这种写法虽然省事,但在综合时可能无法被工具识别为“自然回滚”,影响优化。

✅ 更清晰的做法是定义最大值常量:

localparam MAX_COUNT = (1 << COUNTER_WIDTH) - 1; ... if (counter == MAX_COUNT) counter <= 0;

不仅语义明确,还能避免因位宽变化导致的溢出错误。


在 Vivado 2018.3 中一步步搭建工程

别急着仿真,先确保你的工程结构是对的。

第一步:创建新工程

打开 Vivado 2018.3,选择Create Project→ 设置工程名(不要含中文!)→ 选择 RTL Project → Skip添加源文件(后面手动加)。

目标器件选你手上的开发板型号,例如:

  • Basys3xc7a35tcpg236-1
  • Nexys4 DDRxc7a100tcsg324-1
  • ZedBoardxc7z020clg484-1

第二步:添加设计源文件

右键Design Sources→ Add Sources → Add or create design modules → 创建或导入pwm_generator.v

第三步:写测试平台(testbench),提前发现问题

别跳过仿真!这是排查逻辑错误最快的方式。

module tb_pwm; parameter COUNTER_WIDTH = 17; reg clk; reg rst_n; reg [COUNTER_WIDTH-1:0] duty_cycle; wire pwm_out; // 实例化待测模块 pwm_generator #(.COUNTER_WIDTH(COUNTER_WIDTH)) uut ( .clk(clk), .rst_n(rst_n), .duty_cycle(duty_cycle), .pwm_out(pwm_out) ); // 生成 100MHz 时钟 initial begin clk = 0; forever #5 clk = ~clk; // 10ns 周期 = 100MHz end // 复位与激励 initial begin rst_n = 0; duty_cycle = 0; #20 rst_n = 1; // 测试不同占空比 duty_cycle = 17'd50000; // ~50% #1000000; duty_cycle = 17'd80000; // ~80% #1000000 $finish; end endmodule

运行仿真(Run Simulation → Run Behavioral Simulation),观察波形是否符合预期:

  • 周期 ≈ 1ms(对应 1kHz)
  • 初始低电平持续约 500μs,后变为 200μs
  • 边沿整齐,无抖动

✅ 成功后再进入下一阶段。


引脚约束与时钟设置:决定你能不能“看到波形”

太多人卡在这一步却不知道原因。

管脚分配(XDC 文件)

Constraints下新建 XDC 文件,写入:

# PWM 输出引脚(以 Basys3 的 JA[1] 为例) set_property PACKAGE_PIN U10 [get_ports pwm_out] set_property IOSTANDARD LVCMOS33 [get_ports pwm_out] # 输入时钟(板载 100MHz 晶振) set_property PACKAGE_PIN W5 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk]

📌 必须查开发板原理图确认 PIN 名称!不同板子不一样。

时钟约束(SDC 格式)

在 XDC 中添加:

create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} [get_ports clk]

这告诉 Vivado:“我的输入时钟是 100MHz”,否则时序分析会按默认 1GHz 处理,导致布局布线失败或实际频率不准。


下载验证:用示波器说话

生成比特流 → 连接 JTAG 下载器 → Open Hardware Manager → Program Device。

接上示波器探头到指定引脚(比如 U10),你应该能看到:

  • 清晰的方波;
  • 频率接近 1kHz;
  • 占空比随duty_cycle改变而变化。

如果看不到信号,请按以下顺序排查:

检查项方法
是否绑定了物理引脚?查 XDC 文件中的PACKAGE_PIN
时钟是否锁定?若用了 PLL/MMCM,检查 LOCKED 信号
复位是否释放?确保rst_n接了上拉电阻或由按键控制
电源是否正常?测 FPGA 核心电压(通常 1.0V 或 1.2V)

高级技巧与常见坑点

✅ 技巧 1:多路 PWM 共享计数器,节省资源

如果你要做 RGB LED 调光或三相逆变器,可以共享同一个计数器,只为每个通道保留独立的duty_cycle和比较器:

genvar i; generate for (i = 0; i < NUM_CHANNELS; i = i + 1) begin : gen_pwm assign pwm_out[i] = (counter < duty_cycle[i]); end endgenerate

大幅减少 FF 和 LUT 消耗。

✅ 技巧 2:加入使能控制端口

增加一个enable输入,允许动态启停 PWM:

input enable, ... pwm_next <= enable ? (counter < duty_cycle) : 1'b0;

适用于节能模式或故障保护。

❌ 坑点提醒:千万不要用阻塞赋值混在时序逻辑里!

尤其是初学者容易写出:

pwm_out = (counter < duty_cycle) ? 1 : 0; // 错!应使用 <=

这可能导致综合失败或行为异常。


总结:什么样的 PWM 才算是“可用”的?

回顾一下,一个真正能在项目中使用的 PWM 模块应该满足:

  • ✅ 参数化设计,适配多种频率需求;
  • ✅ 输出经过寄存器同步,消除毛刺;
  • ✅ 支持动态调节占空比且无跳变;
  • ✅ 有时钟约束和正确引脚绑定;
  • ✅ 经过仿真验证 + 硬件实测确认。

掌握了这套方法,你就不再只是“会写代码”,而是具备了完整的 FPGA 数字控制系统开发能力。

下一步,你可以尝试:
- 把 PWM 封装成 AXI-Lite 外设,由 MicroBlaze 动态控制;
- 实现 SPWM(正弦脉宽调制)用于逆变器;
- 结合 ADC 采样做闭环调光或调速。

这些高级应用,都建立在今天这个“小小 PWM”的基础之上。

如果你也在用 Vivado 2018.3 做控制类项目,欢迎留言交流踩过的坑。毕竟,最好的学习,永远来自实战。

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

免费音乐标签编辑终极指南:一键整理您的音乐库

免费音乐标签编辑终极指南&#xff1a;一键整理您的音乐库 【免费下载链接】music-tag-web 音乐标签编辑器&#xff0c;可编辑本地音乐文件的元数据&#xff08;Editable local music file metadata.&#xff09; 项目地址: https://gitcode.com/gh_mirrors/mu/music-tag-web…

作者头像 李华
网站建设 2026/2/26 7:05:49

12、.NET 并行编程中的同步原语

.NET 并行编程中的同步原语 1. 同步原语概述 在并行编程中,当并发任务在没有适当同步机制的情况下对变量进行读写操作时,可能会出现竞态条件。竞态条件会导致程序结果不一致,并且难以检测和纠正。例如,有两个并行任务 task1 和 task2,它们都尝试读取并递增一个公共变量的…

作者头像 李华
网站建设 2026/2/27 14:47:28

14、多线程编程中的同步原语与调试工具使用

多线程编程中的同步原语与调试工具使用 1. 使用 SemaphoreSlim 限制资源访问 SemaphoreSlim 是一个轻量级的信号量,用于限制可以同时访问资源的线程数量。它通过维护一个计数器来工作,每次线程获取信号量时,计数器减少;线程返回信号量时,计数器增加。 以下是使用 Semap…

作者头像 李华
网站建设 2026/2/28 4:37:35

5分钟搞定Mac鼠标滚动优化:Mos终极平滑方案

5分钟搞定Mac鼠标滚动优化&#xff1a;Mos终极平滑方案 【免费下载链接】Mos 一个用于在 macOS 上平滑你的鼠标滚动效果或单独设置滚动方向的小工具, 让你的滚轮爽如触控板 | A lightweight tool used to smooth scrolling and set scroll direction independently for your mo…

作者头像 李华
网站建设 2026/2/21 9:18:15

WindowResizer终极教程:3分钟学会强制调整任意窗口尺寸

WindowResizer终极教程&#xff1a;3分钟学会强制调整任意窗口尺寸 【免费下载链接】WindowResizer 一个可以强制调整应用程序窗口大小的工具 项目地址: https://gitcode.com/gh_mirrors/wi/WindowResizer 还在为无法调整大小的应用程序窗口而烦恼吗&#xff1f;WindowR…

作者头像 李华
网站建设 2026/2/28 3:48:42

Windows系统APK安装神器:让你的电脑变身安卓应用平台

Windows系统APK安装神器&#xff1a;让你的电脑变身安卓应用平台 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想在Windows电脑上轻松安装安卓应用吗&#xff1f;告别…

作者头像 李华