BLDC电机开发之一---六步换相驱动方法讨论
了解BLDC开发的人都知道通过TIMER生成PWM 控制半桥电路导通电流,实现对电机的驱动。不过许多初学者驱动的方法可能是上半桥timer_pwm,下桥GPIO的方式,这种方式符合直接,但却不是一个好的选择, 现在MCU的TIMER在硬件上已经针对电机控制做了非常多的优化,本文在将展示如何利用timer的特性更好的驱动电机。BLDC电机驱动可以参考本人开源项目 https://gitee.com/cyytx/bldc_-pid_-control 另外本文讨论仅限于讨论六步换相也就是方波驱动,如果是FOC的话其实不需要讨论,因为上臂pwm,下臂gpio的方法根本就不可能实现的了,必须得上互补pwm
上臂PWM下臂GPIO
通常而言,当一个嵌入式软件开发初步接触三相逆变电路,并知道上桥臂通过pwm来控制输出电压到电机,并通过下桥臂接地实现电流闭环时,以此驱动电机时。那么我相信他第一时间想到的驱动方式就是上桥臂通过一个TIMER来产生一个PWM,下桥臂通过GPIO来控制拉高拉低从而实现导通和不导通。这种方式实际上在资源受限时可以使用,譬如类似于下面的,如U相上桥臂导通和V相下桥臂导通的情况,就可以设置U相为特定占空比的PWM,其他占空臂为0,V相下臂拉高导通,其他的拉低截止。不过这种控制方法需要使用者对换相时序和逻辑把控更加精准,否则会有上下臂直通的风险
void m1_uhvl(void)
{
g_atimx_handle.Instance->CCR1 = g_bldc_motor1.pwm_duty; /* U相上桥臂PWM */
g_atimx_handle.Instance->CCR2 = 0;
g_atimx_handle.Instance->CCR3 = 0;
HAL_GPIO_WritePin(M1_LOW_SIDE_V_PORT, M1_LOW_SIDE_V_PIN, GPIO_PIN_SET); /* V相下桥臂导通 */
HAL_GPIO_WritePin(M1_LOW_SIDE_U_PORT, M1_LOW_SIDE_U_PIN, GPIO_PIN_RESET); /* U相下桥臂关闭 */
HAL_GPIO_WritePin(M1_LOW_SIDE_W_PORT, M1_LOW_SIDE_W_PIN, GPIO_PIN_RESET); /* W相下桥臂关闭 */
}

上臂pwm下臂gpio的一些隐患
如果是HALL出现问题导致采样不对,或者是在电机转向发生改变时,没有做好处理,就可能导致上下臂直通,导致过流 我以我之前遇到的一种情况给大家做个例子,当然出现这个问题的原因之一也是我当时做电机开发时,设置TIMER的更新参数设置为htim1.Init.RepetitionCounter = 19;也就是20个pwm周期才做更新,导致了这个问题的概率增大。
如下图电机的电机正反转 控制逻辑表。比如在在010时,是V+U-, 也就是V+是PWM导通的,这个时候突然切到反转的命令,如果是低速时,可能不用直接停止而是可以直接反转,那么就要将V+关闭截止,V-打开导通,但是这里有个问题,V+是timer 控制的,它要等更新事件来之后它才改变,而GPIO是即时的,它会立即起作用,这样就可能导致一个问题,V+还没关闭,V-先导通了,导致上下臂导通

通过逻辑分析仪也可以看到该过程。不过需要注意的一点是,我当时的一个配置是20个pwm周期才一个更新事件(Update Event, UEV),导致了它的概率极大的增加,如果1个周期一个UEV,那么因为CCR/ARR 比例比较小,遇到直通的概率也比较小,另一方面也完全可以通过软件上的一些设置来解决这个问题,比如在反转时先暂停,并且通过强制产生更新事件htim1.Instance->EGR |= TIM_EGR_UG; 来解决

事实上我认为上臂PWM下臂GPIO的方式通过良好的逻辑和规划也可以避免上下臂直通问题,但没有必要。因为对于现代的MCU来说却是有着更好的解决方案,这是一种不推荐的方式,通过硬件的方法来解决这个问题,通过高级定时器的互补pwm +死区时间+原子更新 的方式是更好的方案。
互补PWM+死区时间+原子更新方式
许多现代的MCU的高级定时器都支持互补PWM+死区时间的功能,比如stm32系列,另外因为我驱动电机的MCU是STM32G4,所以我下面文章以它举例。
什么是互补pwm呢,就是把同一桥臂的上、下管分别用两路极性相反的 PWM 信号驱动。比如如下图就是一组互补pwm

那么什么是死区时间呢,它的定义是在上下桥臂互补 PWM 信号切换过程中,强制让两个功率开关器件都保持“关断”状态的一段固定时间间隔.如下图,按理既然叫互补就应该是你高我低,我高你低,这个叫互补,但是这里有个问题,就是如果是同时拉高拉低的,那它可能会在某个极限的瞬间有导通的可能,因为timer控制的只是MCU的GPIO,它要到控制电机,需要先控制MOSFET导通/关闭,这里面有些延迟。所以为了保证不会有瞬间导通的情况,在切换时,下臂先提前一段时间先拉低截断。

下面是有关MOSFET延时的一些数据
| 功率等级 | 管芯面积 | 栅极电荷 Qg | 驱动电阻/电流 | 关断延迟 t_fall | 系统杂散电感 | 需要的死区 |
|---|---|---|---|---|---|---|
| 小功率 MOSFET | 小 | 几 nC | 驱动电阻小、驱动电流大 | 10-30 ns | 几 nH | 200-500 ns 足够 |
| 中功率 MOSFET/IGBT | 中 | 数十 nC | 驱动能力中等 | 50-200 ns | 10-20 nH | 0.5-2 µs |
| 高压大电流模块 | 非常大 | 数百 nC | 栅极驱动需更大电流或更高门极电阻 | 200-1000 ns | 50-100 nH | 2-5 µs |
如何配置互补pwm
下面展示如何设置互补pwm及死区时间,以stm32g4为例子,下面只是挑一些不好理解的地方来说,完整代码请参考本人的开源项目代码 https://gitee.com/cyytx/bldc_-pid_-control/blob/master/motor_control_stm32g4/Drivers/BSP/src/bldc.c
互补pwm主要的配置有两个:
1、设置互补通道的相位逻辑,让它和主通道相位形成相反的关系,
2、设置主输出和互补输出在空闲状态(Main Output Enable MOE=0) 也就是定时器通道停止输出时,两个通道都为低电平,防止导通
sConfigOC.OCMode = TIM_OCMODE_PWM1; // 模式1,计数器值 < CCR:输出 active,>=CCR低电平输出 inactive
sConfigOC.Pulse = 0; // 设置比较值(占空比)的初始值为0。
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 设置主输出的有效电平(active)极性为高电平。
// 设置互补输出的有效电平极性也为高电平,当OCPolarity为 active,则它为OFF,当OCPolarity为inactive,反之亦然,这里设置的是
//它为active时的电平,所以互补通道它也是TIM_OCNPOLARITY_HIGH才形成互补。
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; // 禁用快速模式
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET; // 设置定时器在空闲模式(如停止或刹车)时,主输出引脚强制为低电平(RESET)。
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; // 设置定时器在空闲模式时,互补输出引脚也强制为低电平(RESET)。
如何配置死区时间
死区时间的配置如下,另外死区时间和刹车的配置是在一起的,刹车和死区都是为了防止同一个相的上下桥臂同时导通导致大电流烧毁器件,因为我目前使用的板子刹车pin是没有使用的,所以我目前并没有开启它的刹车功能,因为我的驱动是另一种方式来做过流保护,我只在这里做一个大概的对刹车的介绍,
1、timer 的刹车功能是一种硬件功能,它会有专门的pin,可以复用为timer的刹车pin ,也就是配置时要将它做一个复用到timer.
2、它起作用的方式是,一旦这个刹车引脚检测到有效的“刹车信号”(电平变化),定时器硬件会 立即、自动地、无需CPU干预,将所有的PWM输出引脚强制设置为一个预先定义好的“安全状态”。
3、通常硬件连接方法是,电机相线上串联一个低阻值采样电阻,通过运放放大采样电阻上的电压。将放大后的信号输入到一个硬件比较器(的同相端,比较器的反相端接一个代表电流阈值的参考电压。比较器的输出引脚连接到TIM1的BKIK引脚。,这样的话在过流时可以立即实现刹停。

我对代码如下,设置死区时间为800ns,另外因为我的硬件上实际没有连接刹车功能,所以我的刹车功能是DISABLE的,这个要根据实际情况配置。
#define DEAD_TIME_NS 800 // 死区时间 800ns
// 配置死区时间和刹车功能
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE; // 运行模式下关断状态:立即设为非活动状态
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE; // 空闲模式下关断状态:立即设为非活动状态
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_1; // 锁定级别1:防止意外修改死区时间等关键参数
sBreakDeadTimeConfig.DeadTime = (uint32_t)(GET_TIM1_CLOCK_FREQ() / 1000000000.0f * DEAD_TIME_NS); // 死区时间:根据配置和时钟动态计算
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE; // 不启用刹车1:提供硬件级安全保护
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH; // 刹车1极性:高电平有效
sBreakDeadTimeConfig.BreakFilter = 3; // 刹车1滤波:防止噪声误触发
sBreakDeadTimeConfig.Break2State = TIM_BREAK_DISABLE; // 不启用刹车2
sBreakDeadTimeConfig.Break2Polarity = TIM_BREAK2POLARITY_HIGH; // 刹车2极性:高电平有效
sBreakDeadTimeConfig.Break2Filter = 3; // 刹车2滤波:3级滤波,防止噪声误触发
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE; // 禁用自动输出恢复:需要软件手动恢复
上下臂都靠TIMER来控制了,而不靠GPIO那么控制逻辑就需要改变。主要有一下几个点如下面,
1、需要输出pwm的相,他的模式要这种为PWM Mode x 模式,能够输出互补pwm
2、需要下臂导通的相,让主通道(上臂)强制低电平,因为是互补的原因,下臂自然会变成高电平导通
3、对于不导通的相禁止它的输出,让它变为高阻态,无法输出电流也就无法导通。

具体代码如下
void m1_uhvl(void)
{
// --- W 相: 高阻态 --- 禁用 W 相输出
TIM1->CCER &= ~(TIM_CCER_CC3E | TIM_CCER_CC3NE);
// --- U 相: PWM 输出 ---
TIM1->CCMR1 &= ~(TIM_CCMR1_OC1M_Msk); // 清除 U 相 (CH1) 的输出比较模式位
TIM1->CCMR1 |= (6U << TIM_CCMR1_OC1M_Pos); // 设置为 PWM1 模式 (0b110)
TIM1->CCER |= (TIM_CCER_CC1E | TIM_CCER_CC1NE);//使能U相
// --- V 相: 强制非活动电平 ---
TIM1->CCMR1 &= ~(TIM_CCMR1_OC2M_Msk); // 1. 清除 V 相 (CH2) 的输出比较模式位
TIM1->CCMR1 |= (4U << TIM_CCMR1_OC2M_Pos); // 2. 设置 V 相 (CH2) 为 "Force Inactive" 模式 (0b100)
TIM1->CCER |= (TIM_CCER_CC2E | TIM_CCER_CC2NE);// 3. 使能 V 相输出,让强制电平生效
}
另外有一点需要注意的是,高阻态pin的电平可以认为和浮空输入一样,电压都是不确定的,且都无法输出电流,虽然无法输出电流,但是MOSFET是靠电压驱动的,不需要栅极输出电流,而高阻态的电压是不确定,如果它直接接到MOSFET,实际上是有导通或半导通风险的,所以在实际上芯片的Pin脚不会直接连接到MOSFET而是至少隔了一层。
如下图是我所用驱动板的电路图,它连接了高速光电耦合器,当变为高阻态时,没有电流,发光二极管瞬间熄灭,输出电平瞬间拉低,MOSFET就不导通了。所以我上面高阻态能实现不导通的原理就在这里。

原子更新方式
另外还有一点需要非常注意点是如下图所,timer支持同时更新CCxE, CCxNE and OCxM bits ,如果你仔细看过代码就会发现,我们的六步换相调制三相pin的输出时,刚好就是用上这三个bit,

那这是一种巧合吗?显然不是巧合,实际上这是一种硬件对特定应用场景的深度适配和优化,就像systick 可pendsv专为RTOS而设计一样。实际上看代码就知道两个问题,
1、时序问题,就是我们对三相的配置按照软件顺序串行完成的,这存在一个短暂的时间窗口。在这个窗口内,TIM1的输出状态是一个无效的、中间的、不期望的状态低速时固然没有问题,但高速时可能存在问题。
2、原子性问题,如果这中间被一个更高优先级的中断打断了,TIM1被“冻结”在一个错误的配置上。当中断返回时,换向的后半部分才继续执行,但这已经导致了严重的定时错误。
所以这个时候,芯片厂商使用了硬件的方法解决,这个其实也是一种芯片设计哲学:通过专用的硬件特性,来解决纯软件实现时会遇到的效率和时序难题,这是一种硬件对特定应用场景的深度适配和优化。
通过硬件的同时更新方案,实际上实现了下面的效果
实现无毛刺的换向 (Gitch-Free Commutation):确保从一个状态到另一个状态的转换是瞬时、干净的。
保证换向的原子性:让整个换向配置的更新成为一个不可中断的硬件操作,解决了被外部中断打断的风险。
那么在软件配置上要怎么做呢,首先是在初始化时,允许预加载
LL_TIM_CC_EnablePreload(TIM1);
//它的实现是
__STATIC_INLINE void LL_TIM_CC_EnablePreload(TIM_TypeDef *TIMx)
{
SET_BIT(TIMx->CR2, TIM_CR2_CCPC);
}
其实就是配置下面这个位

第二是在换相后触发更新
LL_TIM_GenerateEvent_COM(TIM1);
//他的实现是
__STATIC_INLINE void LL_TIM_GenerateEvent_COM(TIM_TypeDef *TIMx)
{
SET_BIT(TIMx->EGR, TIM_EGR_COMG);
}
也就是我之前贴的图的寄存器
