脉冲输出模块设计(一):PWM原理、计数器设计、占空比与频率控制
各位同学,咱们今天正式开始啃脉冲输出这块硬骨头。说实话,PWM这东西,你随便找个单片机教程都能看到,但真正要在FPGA里把它做得稳、做得准,还是有不少门道的。我当年第一个项目就是做电机驱动,PWM频率没算对,电机嗡嗡响得像要起飞——嗯,那滋味记忆犹新。
PWM的本质:不就是个方波吗?
PWM,全称Pulse Width Modulation,脉冲宽度调制。说白了,就是输出一个方波,高电平的时间占比可以调节。这个占比,我们叫它占空比。
你想想看,一个方波周期T,高电平时间Ton,占空比就是Ton/T × 100%。频率就是1/T。就这么简单?对,原理就这么简单。但FPGA里怎么生成?靠计数器。
核心公式:
- 频率 f = 系统时钟频率 / (计数值 + 1)
- 占空比 = 比较阈值 / 计数值 × 100%
计数器设计:PWM的骨架
计数器是PWM模块的心脏。我习惯用递增计数器,从0计到设定的周期值,然后归零重新开始。这个周期值决定了PWM的频率。
举个例子:系统时钟50MHz,想要1kHz的PWM,周期计数值就是50000-1=49999。为什么减1?因为计数器从0开始,计到49999正好50000个时钟周期。
// 简单的PWM计数器模块
module pwm_counter (
input wire clk,
input wire rst_n,
input wire [15:0] period, // 周期值
input wire [15:0] duty, // 占空比阈值
output reg pwm_out
);
reg [15:0] cnt;
// 计数器逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 16'd0;
else if (cnt >= period)
cnt <= 16'd0;
else
cnt <= cnt + 1'b1;
end
// PWM输出比较
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pwm_out <= 1'b0;
else
pwm_out <= (cnt < duty) ? 1'b1 : 1'b0;
end
endmodule
这段代码我用了很多年,结构清晰,适合初学者理解。注意看:pwm_out在cnt小于duty时输出高电平,否则低电平。这就是占空比控制的精髓。
我的小技巧: 实际项目中,我习惯把period和duty做成可配置的寄存器,这样CPU可以通过总线动态调整频率和占空比。你想想看,如果硬编码死了,调试的时候改一次就要重新综合,多痛苦。
占空比与频率的独立控制
这里有个坑,我曾经踩过。频率和占空比是不是完全独立的?答案是:在固定时钟下,频率由period决定,占空比由duty决定,两者确实独立。但要注意,duty不能大于period,否则输出就一直高电平了。
| 参数 | 控制方式 | 取值范围 | 注意事项 |
|---|---|---|---|
| 频率 | 修改period值 | 1 ~ 系统时钟/2 | period越大,频率越低 |
| 占空比 | 修改duty值 | 0 ~ period | duty=0时输出常低,duty=period时输出常高 |
我个人习惯把占空比的分辨率做到16位,也就是0~65535。这样精度足够,又不浪费资源。如果你做LED调光,8位就够了;但如果是伺服电机控制,我建议至少12位。
PWM生成的核心逻辑流程图
下面这张图是我手绘风格的流程图,展示了PWM生成的完整数据流。从时钟输入到最终波形输出,每一步都很清楚。
从流程图可以清楚看到:时钟驱动计数器,计数器与duty比较,输出PWM波形。同时,period控制计数上限,也就控制了频率。整个逻辑是闭环的,没有复杂的状态机,纯粹的组合逻辑加时序逻辑。
实际项目中的避坑指南
我曾经踩过的坑:
- 计数器溢出问题: 有一次我忘了处理cnt ≥ period时的归零,结果计数器一直往上跑,PWM频率直接变成时钟频率的几分之一,电机抖得像筛子。切记:计数器必须周期归零。
- 占空比突变: 如果duty值在PWM周期中间被修改,当前周期可能会输出异常波形。我后来加了双缓冲机制,duty只在计数器归零时更新,完美解决。
- 频率分辨率: 系统时钟50MHz,想要1.000kHz和1.001kHz?period分别是49999和49950,差49。但如果时钟只有1MHz,同样频率差,period只能差1。所以时钟越高,频率分辨率越精细。
小结一下
PWM模块设计,说白了就是三件事:计数器、比较器、输出寄存器。频率靠period控制,占空比靠duty控制。代码量不大,但细节不少。
我个人建议,初学者先把这个基础模块跑通,用仿真看看波形。确认计数器归零正常,占空比切换平滑,再往里面加功能。比如多通道PWM、死区时间插入、故障保护等等,这些都是后面章节的内容。
嗯,今天就先聊到这儿。代码你们自己动手敲一遍,别光看。仿真波形出来了,你才算真正懂了。