4. PWM生成与死区控制:高级定时器配置、互补PWM输出、死区时间计算、PWM更新策略
说到FOC控制,PWM生成绝对是绕不开的核心环节。你想想看,咱们的SVPWM算法算出来的占空比,最终都得靠定时器变成实实在在的开关信号,送到MOSFET栅极上去。GD32的高级定时器在这方面确实挺能打的,尤其是它的互补输出和死区控制,专门为电机驱动而生。
我个人习惯把这一章分成四个部分来讲:先搞定定时器的基本配置,再搞互补PWM输出,然后重点说说死区时间怎么算,最后聊聊PWM更新策略——这部分直接影响FOC环路的响应速度。
4.1 高级定时器配置
GD32的高级定时器(比如TIMER0、TIMER7)跟普通定时器最大的区别,就是多了互补输出、死区插入、刹车输入这些功能。说白了,它就是为桥式电路量身定做的。
先看一个最基本的配置流程:
// 定时器基本配置
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER0); // 使能时钟
timer_deinit(TIMER0);
// 配置时基
timer_struct_para_init(&timer_initpara);
timer_initpara.prescaler = 1; // 预分频,假设主频200MHz
timer_initpara.period = 1999; // 自动重装值,决定PWM频率
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0; // 重复计数器,用于产生更新事件
timer_init(TIMER0, &timer_initpara);
这里有个关键点:period值决定了PWM的载波频率。比如主频200MHz,预分频设为1(实际分频2),period设为1999,那么PWM频率就是200MHz / (2 * 2000) = 50kHz。我在项目中一般用16kHz到20kHz,既能避开人耳可听范围,又能保证电流环的更新速率。
4.2 互补PWM输出
互补PWM,就是同一对桥臂的上下管,一个导通时另一个必须关断。GD32的高级定时器支持通道0~2的互补输出,每个通道对应一对PWM引脚。
配置互补输出需要做三件事:
- 配置输出模式——设为PWM模式1或2
- 使能互补输出——通过TIMER_CTLR2寄存器的CCxE和CCxNE位
- 设置极性——决定高电平有效还是低电平有效
// 配置通道0互补输出
timer_oc_parameter_struct timer_ocinitpara;
timer_channel_output_struct_para_init(&timer_ocinitpara);
timer_ocinitpara.outputstate = TIMER_CCX_ENABLE; // 使能主输出
timer_ocinitpara.outputnstate = TIMER_CCXN_ENABLE; // 使能互补输出
timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH; // 主输出高有效
timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH; // 互补输出高有效
timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
timer_channel_output_config(TIMER0, TIMER_CH_0, &timer_ocinitpara);
// 设置比较值
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, 1000);
嗯,这里要注意极性配置。我见过不少新手在这里栽跟头——如果上下管都是高有效,那死区时间内两个管子都关断,没问题。但如果驱动芯片是低有效,极性就得反过来配。
4.3 死区时间计算
死区时间,就是上下管切换时,特意插入的一段「同时关断」的时间。为什么要加死区?因为MOSFET的关断速度比开通速度慢,如果不加死区,上管还没完全关断,下管就开了,这就是传说中的「直通短路」。
GD32的死区时间由TIMERx_CAR寄存器的DTCFG位控制。这个寄存器分成两段:
| 位段 | 功能 | 说明 |
|---|---|---|
| DTCFG[7:0] | 死区时间配置 | 具体数值由DTG[7:0]决定 |
| DTG[7] | 死区时间倍率 | 0:乘以1;1:乘以2或乘以16 |
死区时间的计算公式分三种情况:
- DTG[7] = 0:死区时间 = DTG[6:0] × Tck
- DTG[7] = 1, DTG[6:5] = 00:死区时间 = (64 + DTG[4:0]) × 2 × Tck
- DTG[7] = 1, DTG[6:5] = 01:死区时间 = (32 + DTG[4:0]) × 8 × Tck
- DTG[7] = 1, DTG[6:5] = 10或11:死区时间 = (32 + DTG[4:0]) × 16 × Tck
其中Tck是定时器的时钟周期。假设定时器时钟200MHz,Tck = 5ns。
举个例子:我想配500ns的死区时间,DTG[7] = 0,那么DTG[6:0] = 500ns / 5ns = 100。但DTG[6:0]最大只有127,所以100是合法的。配置代码:
// 配置死区时间500ns (T_ck = 5ns)
timer_channel_dead_time_config(TIMER0, TIMER_CH_0, 100);
如果死区时间需要更大,比如2μs,那就得用第二种模式:DTG[7] = 1, DTG[6:5] = 00,死区时间 = (64 + DTG[4:0]) × 2 × 5ns。算一下:(64 + 0) × 10ns = 640ns,不够。设DTG[4:0] = 31,则(64 + 31) × 10ns = 950ns。还是不够?那就换第三种模式,乘8倍。
4.4 PWM更新策略
FOC控制的核心是电流环,电流环的更新频率通常和PWM载波频率一致。每次PWM周期结束时,我们都要更新比较值,让下一个周期的占空比发生变化。
GD32支持两种更新方式:
- 立即更新:写入比较寄存器后,下一个计数周期立即生效
- 影子寄存器更新:写入比较寄存器后,等到更新事件(UEV)发生时再加载
在FOC应用中,我们通常使用影子寄存器模式,配合更新中断来实现同步更新。流程是这样的:
- 定时器产生更新中断(上溢或下溢)
- 在中断服务函数中读取电流采样值
- 执行电流环PI运算
- 计算新的占空比,写入比较寄存器
- 下一个周期开始时,新占空比自动生效
// 使能更新中断
timer_interrupt_enable(TIMER0, TIMER_INT_UP);
// 中断服务函数
void TIMER0_UP_IRQHandler(void) {
if(timer_interrupt_flag_get(TIMER0, TIMER_INT_UP) != RESET) {
// 1. 读取电流采样值(ADC已经转换完成)
float ia = get_current_a();
float ib = get_current_b();
// 2. 执行Clark/Park变换
// 3. 执行PI调节
// 4. 执行SVPWM,计算占空比
// 5. 更新比较值
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, duty_a);
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_1, duty_b);
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_2, duty_c);
timer_interrupt_flag_clear(TIMER0, TIMER_INT_UP);
}
}
这里有个性能问题:中断服务函数里做的事情太多,会影响最大PWM频率。我实测过,在200MHz主频下,如果PWM频率超过50kHz,中断里的运算时间就快赶上PWM周期了。这时候可以考虑用DMA来搬运比较值,或者把部分运算放到主循环里做。
最后,我把这一章的核心逻辑画成了一张流程图,方便你理解整个PWM生成与更新的链路:
这张图把整个链路串起来了。从定时器配置开始,到互补输出、死区插入、更新中断,最后输出波形。每一步都有对应的寄存器配置和注意事项。