4、脉冲输出模块设计(二):多通道脉冲输出、相位差控制、死区时间插入
上一节我们把单通道脉冲输出的基本功打扎实了。这一节,咱们来点真格的——多通道协同工作。
说实话,单通道脉冲输出,你只要把计数器玩明白,基本就搞定了。但一旦涉及到多通道,事情就变得有意思了。相位差、死区时间、通道间干扰……这些坑,我当年一个一个踩过来的。
4.1 多通道脉冲输出的架构设计
多通道脉冲输出,说白了就是在一个模块里同时管理N路脉冲信号。每路都有自己的频率、占空比、起始相位。
我习惯的做法是这样的:用一个全局计数器作为时间基准,每个通道独立配置自己的比较寄存器。这样所有通道都同步在同一个时间轴上,相位关系天然可控。
核心思路: 全局时基 + 通道独立配置 = 精确的通道间时序关系
来看看我常用的模块结构:
module multi_ch_pulse #(
parameter CH_NUM = 4, // 通道数
parameter CNT_WIDTH = 32 // 计数器位宽
)(
input wire clk,
input wire rst_n,
// 配置接口
input wire [CNT_WIDTH-1:0] period [CH_NUM-1:0], // 周期
input wire [CNT_WIDTH-1:0] duty [CH_NUM-1:0], // 占空比
input wire [CNT_WIDTH-1:0] phase [CH_NUM-1:0], // 相位偏移
input wire [7:0] dead_time [CH_NUM-1:0], // 死区时间
// 输出
output reg [CH_NUM-1:0] pulse_out
);
reg [CNT_WIDTH-1:0] cnt;
reg [CNT_WIDTH-1:0] cnt_phase [CH_NUM-1:0]; // 各通道相位调整后的计数值
// 全局计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 0;
else if (cnt >= period[0]) // 以通道0的周期为基准
cnt <= 0;
else
cnt <= cnt + 1;
end
// 各通道相位调整
generate
genvar i;
for (i = 0; i < CH_NUM; i = i + 1) begin : gen_phase
always @(*) begin
if (cnt >= phase[i])
cnt_phase[i] = cnt - phase[i];
else
cnt_phase[i] = cnt + period[0] - phase[i];
end
end
endgenerate
// 脉冲生成(含死区)
generate
for (i = 0; i < CH_NUM; i = i + 1) begin : gen_pulse
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pulse_out[i] <= 0;
else begin
// 这里只是示意,实际死区逻辑要更复杂
if (cnt_phase[i] < duty[i])
pulse_out[i] <= 1;
else
pulse_out[i] <= 0;
end
end
end
endgenerate
endmodule
我的经验: 全局计数器不要用所有通道中最大的周期,而是用通道0的周期。这样其他通道的相位偏移计算就简单了。当然,前提是通道0的周期是所有通道的公约数。
4.2 相位差控制——让脉冲们排好队
相位差控制,是电机控制、多相电源、步进驱动等场景的核心需求。你想想看,如果两路PWM同时跳变,那电流尖峰得多大?
相位差说白了就是:各路脉冲的起始点错开一定的时间。比如通道0在0°开始,通道1在90°开始,通道2在180°开始……
实现方式其实很简单:
- 方法一: 每个通道独立计数器,各自从不同的初始值开始计数。这是最直观的做法。
- 方法二: 共享一个全局计数器,每个通道配置一个相位偏移寄存器。我推荐这种做法,因为所有通道天然同步。
方法二的相位计算逻辑:
// 相位调整后的计数值
// 假设全局计数器 cnt 从 0 到 PERIOD-1
// 通道 i 的相位偏移为 phase[i]
// 则通道 i 的有效计数值为:
// cnt_eff = (cnt >= phase[i]) ? (cnt - phase[i]) : (cnt + PERIOD - phase[i])
嗯,这里要注意:相位偏移的单位是时钟周期。如果你需要更精细的相位控制,比如0.1°的精度,那就得用小数分频或者DDS技术了。这个我们后面章节会讲。
我曾经踩过的坑: 相位偏移值不能大于周期值。否则会出现相位绕回,导致脉冲时序完全乱掉。一定要在配置接口做合法性检查。
4.3 死区时间插入——保护你的功率管
死区时间,这是做电机驱动、逆变器、开关电源的朋友最熟悉的概念了。
为什么要插死区?因为功率管(MOSFET、IGBT)的开关速度不是无限快的。关断需要时间,开通也需要时间。如果上下桥臂同时导通,那就是直通短路,瞬间烧管子。
死区时间,就是在上管关断后、下管开通前,插入一段两者都关断的时间。反过来也一样。
我见过很多新手犯的错误:死区时间设得太短,觉得"我的管子开关速度很快,1ns就够了"。结果一上电,管子冒烟了。为什么?因为PCB走线寄生电感、驱动芯片的传播延迟、管子本身的米勒效应……这些都会让实际开关时间变长。
死区插入的典型实现:
// 死区插入逻辑(以一对互补PWM为例)
// pwm_high 和 pwm_low 是原始的互补信号
// dead_time 是死区时间(时钟周期数)
reg [7:0] dead_cnt_high, dead_cnt_low;
reg pwm_high_out, pwm_low_out;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pwm_high_out <= 0;
pwm_low_out <= 0;
dead_cnt_high <= 0;
dead_cnt_low <= 0;
end else begin
// 高边通道
if (pwm_high) begin
// 等待低边完全关断
if (dead_cnt_low == 0 && !pwm_low_out)
pwm_high_out <= 1;
end else begin
pwm_high_out <= 0;
dead_cnt_high <= dead_time; // 开始计时
end
// 低边通道
if (pwm_low) begin
if (dead_cnt_high == 0 && !pwm_high_out)
pwm_low_out <= 1;
end else begin
pwm_low_out <= 0;
dead_cnt_low <= dead_time;
end
// 死区计数器递减
if (dead_cnt_high > 0) dead_cnt_high <= dead_cnt_high - 1;
if (dead_cnt_low > 0) dead_cnt_low <= dead_cnt_low - 1;
end
end
关键点: 死区时间不是越长越好。太长的死区会导致输出波形失真,电机电流谐波增大,效率下降。一般建议在满足安全的前提下,尽量短。我常用的经验值是:IGBT用2-5μs,MOSFET用100-500ns。
4.4 多通道脉冲输出的时序图
光说理论不够直观,我画了一张时序图,帮你理解多通道相位差和死区的关系。
从这张图你可以清楚地看到:每个通道的脉冲波形完全一样,只是起始时间依次错开了90°。这就是四相步进电机驱动的基础波形。
4.5 实际项目中的避坑指南
做多通道脉冲输出,有几个坑我不得不提:
- 通道间串扰: 多个通道同时翻转时,电源和地平面会产生很大的瞬态电流。我建议在FPGA内部把各通道的输出寄存器分散放置,不要挤在一起。
- 死区时间的一致性: 不同通道的死区时间可能会有微小差异,这会导致桥臂不平衡。我习惯用一个统一的死区计数器,所有通道共享。
- 相位精度: 如果全局计数器位宽不够,相位偏移的精度就会受限。比如32位计数器,时钟100MHz,相位精度是10ns。如果需要更高精度,就得用DDS或者小数分频。
- 配置更新时机: 在运行中修改相位或死区参数时,一定要等当前周期结束再更新。否则会出现半个周期用新参数、半个周期用旧参数的混乱情况。
我的小技巧: 在调试多通道脉冲时,先用示波器看两两通道之间的相位关系。如果发现相位不对,先检查全局计数器是否溢出,再检查相位偏移寄存器是否被意外修改。这两个问题我各遇到过两次,每次排查都花了不少时间。
4.6 总结一下
多通道脉冲输出,核心就三件事:
- 全局时基: 所有通道共享一个计数器,保证同步性。
- 相位偏移: 每个通道独立配置起始相位,实现精确的通道间时序。
- 死区插入: 在互补信号之间插入安全间隔,保护功率器件。
这三件事做好了,多通道脉冲输出模块就稳了。下一节我们会聊到脉冲输出的高级话题——如何用DDS技术实现超高精度的频率和相位控制。嗯,那个更有意思。
公众号:蓝海资料掘金营,微信deep3321