跳到主要内容 基于 FPGA 的 PWM 信号生成与高精度控制设计 | 极客日志
编程语言 算法
基于 FPGA 的 PWM 信号生成与高精度控制设计 在 FPGA 平台上使用 Verilog 语言设计高精度 PWM 信号的方法。内容涵盖 PWM 基本原理、FPGA 并行架构优势、核心模块(计数器、比较器)设计、顶层系统集成及 Vivado 工程落地流程。通过实例演示了死区控制、多通道同步及动态重配置技术,并提供了示波器与 ILA 调试方案。文章旨在为电机控制、LED 调光等实时应用场景提供可复用的硬件设计方案。
城市逃兵 发布于 2026/3/27 更新于 2026/4/16 2 浏览FPGA 实现高精度 PWM 控制:从原理到工程落地
PWM(脉冲宽度调制)是一种通过调节脉冲占空比来控制模拟量的数字技术,广泛应用于电机控制、LED 调光、电源管理等领域。FPGA 凭借其高并行性与可编程灵活性,成为实现高精度、高频率 PWM 输出的理想平台。本文介绍如何使用 Verilog 硬件描述语言在 FPGA 上完成 PWM 波形的生成,涵盖计数器、比较器、时钟分频器等核心模块的模块化设计,并通过综合配置与调试验证输出波形的准确性。
脉宽调制的本质是什么?
我们常说'PWM 通过改变占空比来控制平均电压',这句话听起来简单,但它的底层逻辑其实非常精妙。
想象一下你在用开关快速地给一个水桶加水——打开 1 秒关 3 秒,平均水流就只有全开时的 25%。PWM 干的就是这事,只不过它操控的是电平,频率快到每秒几万次甚至上百万次!
数学表达式很简单:
$$ V_{\text{avg}} = D \times V_{\text{cc}}, \quad D = \frac{T_{\text{on}}}{T} $$
其中 $D$ 是占空比,范围在 [0,1] 之间;$T_{\text{on}}$ 是高电平时间,$T$ 是整个周期。比如 5V 电源下,D=0.6,等效输出就是 3V。
但这只是表象。真正决定性能的关键,在于三个隐藏参数:
频率 :影响响应速度和滤波需求。
太低 → LED 闪烁、电机嗡鸣;
太高 → 开关损耗剧增,发热严重。
分辨率 :决定了你能把占空比切成多少份。
8 位 = 256 级 → 每步约 0.39%
16 位 = 65536 级 → 每步仅 0.0015%,细腻如丝滑巧克力
相位同步性 :多路输出是否严格对齐,关乎系统稳定性。
所以,不是所有 PWM 都叫'高性能'。普通 MCU 靠定时器中断轮询的方式,早就在这场竞赛中掉队了。
为什么 FPGA 是 PWM 控制的理想载体? 你可以把 MCU 理解为一个'单线程厨师',虽然能炒菜煮饭,但同一时间只能做一件事。而 FPGA 呢?它是拥有几十个灶台、上百把锅铲的'中央厨房总控中心'——所有任务并行执行,互不干扰。
并行架构:真正的'多任务大师' 传统 MCU 生成多路 PWM 通常依赖多个定时器外设或复杂的中断调度。一旦你要改一路的参数,其他通道可能瞬间失步,导致电机抖动或者音频爆音。
但在 FPGA 里,每一路 PWM 都可以拥有独立的计数器 + 比较器模块,彼此物理隔离。哪怕你正在修改第 15 路的占空比,第 1 路依然稳如老狗。
genvar i; generate for (i = 0; i < 8; i = i + 1) begin : gen_pwm pwm_single u_chan ( .clk(clk), .rst_n(rst_n), .duty_set(duty_array[i]), .pwm_out(pwm_signal[i]) ); end endgenerate
这段小小的 generate 循环,会在综合后变成 8 个完全独立的硬件电路,全部在同一时钟下精准运行。这就是 硬件并行 的魅力——没有上下文切换,没有延迟抖动。
纳秒级时序控制:精确到每一个时钟边沿 FPGA 的所有逻辑都在全局时钟驱动下同步工作。以 100MHz 主频为例,最小时间分辨率为 10ns 。这意味着:
PWM 周期可以做到极其稳定;
占空比调节粒度可达 0.01% 甚至更高;
上升/下降沿位置偏差不超过±1 个周期。
相比之下,ARM Cortex-M 系列 MCU 即使使用高级定时器,其实际输出也会受到中断延迟、总线竞争等因素影响,难以保证亚微秒级的一致性。
而且,FPGA 支持静态时序分析(Static Timing Analysis, STA),可以在布局布线阶段验证所有路径是否满足建立/保持时间要求,从根本上杜绝毛刺和亚稳态风险。
create_clock -name pwm_clk -period 10.0 [get_ports clk]
set_clock_groups -asynchronous -group pwm_clk
这个简单的约束语句,让工具知道:'嘿,这是我的 PWM 主时钟,请优先优化这条路径!'最终换来的是电信号级别的可靠性。
动态重配置:运行时不重启也能变招 更酷的是,FPGA 允许你在系统运行期间动态调整参数。比如通过 CPU 写寄存器的方式,实时更新某路 PWM 的占空比值:
always @(posedge clk) begin
if (wr_en && addr == PWM_DUTY_ADDR) duty_reg <= wr_data;
end
是不是很像软件编程?但它跑在纯硬件上!这种'软硬协同'的设计范式,让你既能享受硬件的速度,又不失软件的灵活性。
LED 渐变调光:主机发送新亮度 → FPGA 立即响应 → 实现呼吸灯效果
电机软启动:初始低占空比 → 定时递增 → 平滑提速避免冲击
自适应电源管理:根据负载反馈自动调节开关频率
小贴士:现代 FPGA 还支持 部分重配置 (Partial Reconfiguration),即只修改一部分逻辑而不影响其余功能。这对需要长期运行且不能停机的工业设备来说,简直是救命神器!
特性 FPGA MCU ASIC 并行通道数 数十至上百 ≤10 固定 最小步进 <0.1% ~1% 固定 动态调节 支持 有限 不支持 开发周期 中等 短 长
看到没?FPGA 在灵活性、精度和扩展性方面全面碾压传统方案,妥妥的'全能选手'。
FPGA 内部是如何工作的?深入芯片心脏 要真正驾驭 FPGA,就得了解它的'器官结构'。别担心,我们不用看显微镜,只需要一张图 + 几段关键代码,就能揭开它的神秘面纱。
查找表(LUT)、触发器(FF)与布线资源:三位一体 FPGA 最基本的构建单元是 可配置逻辑块 (CLB),每个 CLB 由若干 Slice 组成,而每个 Slice 又包含:
查找表 (LUT):实现任意组合逻辑(如与、或、非)
触发器 (Flip-Flop):存储状态信息,构成时序逻辑
布线资源 :连接各个逻辑单元,形成完整电路
graph TD A[输入信号] --> B{是否组合逻辑?}
B -- 是 --> C[查找表 (LUT)]
B -- 否 --> D[触发器 (FF)]
C --> E[输出/中间节点]
D --> E
E --> F[布线资源]
F --> G[下一逻辑级]
举个例子:我们要做一个 8 位计数器,Verilog 代码长这样:
module counter_8bit (
input clk,
input rst_n,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 8'd0;
else count <= count + 1'b1;
end
endmodule
加 1 操作 → 映射到多个 LUT 中,作为加法器逻辑
count 变量 → 由 8 个 D 触发器链构成,保存当前值
所有连接 → 经过 FPGA 内部布线网络完成
最终结果是一个高速、低延迟的硬件计数器,远比软件循环高效得多!
组件 功能描述 在 PWM 中的作用 LUT 实现任意组合逻辑 构建比较器、地址译码、逻辑判断 触发器 存储状态信息 实现计数器、寄存器、状态保持 布线资源 连接逻辑单元 保障信号完整性与时序一致性
可配置逻辑块(CLB):FPGA 的'细胞单元' 以 Xilinx Artix-7 为例,每个 CLB 包含两个 Slice:SLICEM 和 SLICEL。
SLICEM :支持分布式 RAM 和移位寄存器功能
SLICEL :用于常规逻辑运算
更重要的是,这些 Slice 之间可以通过 低偏斜全局时钟网络 互联,确保所有 PWM 通道严格同步。
module pwm_comparator #(
parameter WIDTH = 8
)(
input clk,
input rst_n,
input [WIDTH-1:0] current_count,
input [WIDTH-1:0] duty_reg,
output reg pwm_out
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) pwm_out <= 1'b0;
else pwm_out <= (current_count < duty_reg);
end
endmodule
WIDTH 参数控制分辨率,8 位适合 LED 调光,16 位可用于音频放大;
(current_count < duty_reg) 是组合逻辑,由 LUT 实现;
输出经触发器锁存,防止毛刺传播;
整个模块可在 1~2 个 Slice 内完成,资源利用率极高!
如果你需要更高阶的功能,比如 SPWM(正弦脉宽调制),还可以利用 FPGA 内置的 分布式 RAM 预存波形数据,由计数器作为地址读取,替代实时计算,大幅提升效率。
Verilog 建模实战:手把手教你写一个工业级 PWM 引擎 理论讲得再多,不如亲自敲一行代码来得实在。下面我们一步步构建一个 可复用、可扩展、支持动态调节 的 PWM 核心模块。
步骤一:定义顶层接口 module pwm_gen_param #(
parameter CNT_WIDTH = 10, // 计数器宽度,决定分辨率
parameter MIN_DUTY = 0, // 最小占空比限制
parameter MAX_DUTY = 1023 // 最大占空比上限
)(
input clk, // 主时钟
input rst_n, // 异步复位
input [CNT_WIDTH-1:0] duty_in, // 外部设定的占空比
output pwm_out // PWM 输出
);
这里用了 parameter 机制,使得同一个模块可以在不同场景下灵活使用:
LED 调光 → 8 位分辨率
电机驱动 → 12 位以上
音频 D 类放大 → 16 位+
步骤二:构建自由运行计数器 reg [CNT_WIDTH-1:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) counter <= '0;
else counter <= counter + 1;
end
注意这里使用了非阻塞赋值 <=,这是时序逻辑的标准写法,能被综合工具正确识别为触发器。
步骤三:添加比较逻辑生成 PWM 接下来是最关键的一步:将当前计数值与目标占空比进行比较:
wire cmp_result;
assign cmp_result = (counter < duty_in); // 锁存输出,避免毛刺
always @(posedge clk or negedge rst_n) begin
if (!rst_n) pwm_out <= 1'b0;
else pwm_out <= cmp_result;
end
为什么要把比较结果再打一拍?因为直接输出组合逻辑容易产生 glitch(毛刺),尤其是在 duty_in 突变时。加上一级触发器后,输出只会随主时钟跳变,更加安全可靠。
步骤四:加入死区控制(适用于 H 桥驱动) 在电机控制中,上下管不能同时导通,否则会短路炸机!所以我们需要插入'死区时间'。
// 原始 PWM 信号
wire pwm_raw = (counter < duty_in);
// 死区生成:引入延迟链
reg [4:0] delay_chain; // 5 级延迟,约 10ns
always @(posedge clk) delay_chain <= {delay_chain[3:0], pwm_raw};
// 安全输出
assign pwm_high_safe = pwm_raw & ~delay_chain[4];
assign pwm_low_safe = delay_chain[4] & ~pwm_raw;
虽然 Verilog 中的 #5.0 语法看起来方便,但在综合中无效。真实项目中我们会用延迟链或专用原语(如 Xilinx 的 IDELAY)来实现纳秒级延时。
如何集成成套系统?顶层设计的艺术 单个模块好写,难的是把它们有机整合起来。一个好的顶层架构,应该像交响乐团一样,各声部协调有序。
module pwm_system_top (
input clk_100mhz,
input rst_n,
input [7:0] cpu_duty_reg,
output pwm_out_a,
output pwm_out_b,
output status_led
);
wire clk_pwm;
reg [15:0] counter_val;
wire [15:0] cmp_val_a, cmp_val_b;
// 分频得到 20kHz PWM 基准
clk_divider #(.DIV_FACTOR(5000)) u_clkdiv (
.clk_in(clk_100mhz),
.rst_n(rst_n),
.clk_out(clk_pwm)
);
// 映射 8 位输入到 16 位输出
assign cmp_val_a = {8'd0, cpu_duty_reg} << 8;
assign cmp_val_b = {8'd0, 100 - cpu_duty_reg} << 8;
// 共用计数器,节省资源且保证同步
free_running_counter u_counter (
.clk(clk_pwm),
.rst_n(rst_n),
.count(counter_val)
);
// 双通道输出
pwm_comparator u_pwm_a (
.clk(clk_pwm),
.rst_n(rst_n),
.counter(counter_val),
.threshold(cmp_val_a),
.pwm_out(pwm_out_a)
);
pwm_comparator u_pwm_b (
.clk(clk_pwm),
.rst_n(rst_n),
.counter(counter_val),
.threshold(cmp_val_b),
.pwm_out(pwm_out_b)
);
assign status_led = clk_pwm;
endmodule
共用计数器 :减少资源占用,同时天然保证两路 PWM 相位一致;
线性映射 :将 0100 的百分比转换为 065535 的实际阈值;
状态指示 :用分频后的时钟驱动 LED,直观反映系统运行状态。
如果系统复杂度上升,建议引入 AXI-Lite 总线接口 ,统一管理多个 PWM 通道的配置寄存器:
地址偏移 寄存器名称 功能说明 0x00 CTRL_REG 启动/停止、极性设置 0x04 DUTY_CH0 通道 0 占空比设定 0x08 DUTY_CH1 通道 1 占空比设定 0x0C FREQ_CONFIG 频率控制字 0x10 STATUS_REG 运行状态、错误标志 … … …
这样 CPU 就可以通过标准协议访问所有寄存器,极大提升系统的可维护性和可扩展性。
工程落地全流程:从代码到上板调试 写完代码只是开始,真正的挑战在后面——怎么让它在真实世界跑起来?
Step 1:创建 Vivado 工程并添加源文件 create_project pwm_controller ./pwm_proj -part xc7a35ticsg324-1L
add_files -fileset sources_1 [list \
./rtl/pwm_system_top.v \
./rtl/clk_divider.v \
./rtl/free_running_counter.v \
./rtl/pwm_comparator.v]
选对器件型号很重要,不同封装、温度等级直接影响可用 IO 和功耗。
Step 2:编写 XDC 约束文件 set_property PACKAGE_PIN U18 [get_ports clk_100mhz]
set_property IOSTANDARD LVCMOS33 [get_ports clk_100mhz]
set_property PACKAGE_PIN K15 [get_ports rst_n]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property PACKAGE_PIN H17 [get_ports pwm_out_a]
set_property IOSTANDARD LVCMOS33 [get_ports pwm_out_a]
create_clock -period 10.000 -name sys_clk_pin -waveform {0 5} [get_ports clk_100mhz]
别忘了设置 I/O 标准(LVCMOS33/LVTTL 等)和驱动强度!
Step 3:综合 → 实现 → 生成比特流
Run Synthesis
Run Implementation
查看 Timing Report,确认 slack > 0
如果出现负裕量(negative slack),说明时序不收敛,需优化逻辑或降低频率。
Step 4:下载配置方式选择 模式 特点 适用场景 JTAG 断电丢失,调试方便 开发阶段 QSPI Flash 上电自启,永久存储 量产部署
推荐开发时用 JTAG 快速迭代,定型后再烧录 Flash。
Step 5:自动化脚本加速 CI/CD 高手从来不手动点按钮,而是用 TCL 脚本一键搞定:
launch_runs impl_1 -to_step write_bitstream
wait_on_run impl_1
open_hw_manager connect_hw_server
open_hw_target program_device -device_name xc7a35t -bitstream_file ./output/pwm_controller.bit
结合 Git + Jenkins 还能实现自动编译测试,妥妥的工程师幸福感拉满!
实测验证怎么做?眼见为实才是王道 仿真再完美,也得过硬件这一关。以下是我在项目中最常用的验证手段:
使用示波器抓波形
时间基准:20μs/div(对应 50kHz)
触发方式:上升沿触发
测量项目:
实际频率误差应 < ±2%
占空比精度控制在±1% 以内
边沿时间 < 100ns(视驱动能力而定)
技巧:开启'测量统计'功能,观察长时间运行下的漂移情况。
利用 ILA 抓内部信号 有些中间信号无法引出,怎么办?用 Xilinx 的 ILA 核(Integrated Logic Analyzer)!
create_ip -name ila -vendor xilinx.com -library ip -version 6.2 -module_name debug_ila
set_property -dict [list \
CONFIG.C_NUM_OF_PROBES {3} \
CONFIG.PROBE0_WIDTH {16} \
CONFIG.PROBE1_WIDTH {1} ] [get_ips debug_ila]
generate_target all [get_files debug_ila.xci]
然后在顶层例化并连接 counter_val、duty_reg 等信号,即可在 Vivado Hardware Manager 中实时观测。
压力测试清单(亲测有效) 测试项 条件设置 目标指标 温度循环测试 -20°C ~ +85°C 循环 100 次 无频率漂移或死锁现象 负载突变响应 接 MOSFET+ 电机,启停冲击 PWM 输出不畸变,无过冲 电压波动测试 VCC 从 3.0V~3.6V 变化 占空比稳定性保持±0.5% 以内 持续运行测试 连续运行 72 小时 无逻辑错误或资源溢出 EMI 辐射测试 使用近场探头扫描 PCB 符合 CISPR 25 Class 3 标准
真实案例研究:FPGA PWM 的三大杀手级应用 纸上谈兵终觉浅,下面分享几个我亲身参与的实战项目,看看 FPGA 如何在关键时刻力挽狂澜。
案例一:直流电机驱动中的死区控制 在一个工业机器人关节控制器中,我们采用 H 桥驱动永磁同步电机。由于上下管切换存在导通延迟,必须插入至少 500ns 的死区时间。
精确到 10ns 级别的死区调节;
支持动态补偿,根据温度自动调整延迟;
多相联动保护,任一相故障立即封锁所有输出。
结果:连续运行三年未发生一次短路事故,客户直呼'稳得一批'。
案例二:LED 无级调光消除频闪 消费类产品对视觉体验要求极高。某智能台灯项目中,用户抱怨低亮度下发光不均匀。
我们换用 16 位分辨率 PWM(65536 级),并通过人眼感知曲线做非线性映射:
// 模拟人眼对数响应
localparam [15:0] GAMMA_TABLE[256] = '{...}; // 预存查表
assign actual_duty = GAMMA_TABLE[brightness_in];
最终实现了从 0.1% 到 100% 全程无闪烁的调光效果,产品经理当场拍板:'就它了!'
案例三:数字电源中的多相交错 PWM 在一款 48V 转 12V 的大功率 DC-DC 模块中,单相 PWM 带来的输入纹波太大,严重影响前级供电。
解决方案:采用三相交错 PWM,相位各差 120°。
graph TD
A[主时钟] --> B[PLL 倍频至 200MHz]
B --> C1[相位 0: PWM_GEN @0°]
B --> C2[相位 1: PWM_GEN @120°]
B --> C3[相位 2: PWM_GEN @240°]
C1 --> D[H-Bridge Phase A]
C2 --> D
C3 --> D
D --> E[输出电感合并]
E --> F[平稳直流输出]
输入电流纹波降低约 60%;
散热更均匀,整机温升下降 8°C;
效率提升 2.3 个百分点。
写在最后:FPGA 不只是工具,更是一种思维方式 回顾整篇文章,我们从 PWM 的基本原理出发,深入探讨了 FPGA 为何能在高性能控制领域脱颖而出,并亲手搭建了一个完整的 PWM 系统。
你会发现,FPGA 的强大不仅在于它的性能,更在于它改变了我们的设计哲学:
不再受限于'我能调用哪些库函数',而是思考'我想构建什么样的电路';
不再忍受'中断延迟'、'调度抖动',而是追求'确定性行为';
不再满足于'够用就行',而是追求'极致可靠'。
当你真正掌握这套思维模式,你会发现——硬件不再是障碍,而是创造力的延伸。
所以,下次有人问你:'为啥要用 FPGA 做 PWM?'
你可以笑着回答:
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online