[Mini-F5375-OB开发板评测] 普通定时器&MindPwm控制编码电机

前言:

在当下的生活中,除了步进电机,最常用到的就是减速电机,带个霍尔编码器就成为了编码电机,这种电机的优点在于只需要使用定时器发出的PWM波形配合H桥电路即可实现电机的驱动,所以在这里演示了通过普通定时器驱动和使用MM32这款MCU的新功能MindPwm--一款专门用于产生专业的PWM波形的定时器,也叫做高级定时器,主打顶尖。
在这里使用了TB6612FNG驱动芯片作为H桥的芯片,这款驱动芯片由于其体积小,带载能力强被我看重,其体积小的特点适合于驱动我手上的这款微型编码电机,所以就以这款mcu为例,展示普通定时器控制和MindPwm控制下的能力比较吧。
由于在H桥里如果两个通道都是高电平,会导致直通现象,导致烧毁H桥,但是这款驱动芯片在都为1的情况下,会让电机慢速衰减停止,其阻尼比比正常都为0要小得多,所以这里还是当作其为正常的H桥,插入死区并且配置刹车功能来让其更加的完善。

效果图:

 title=

TB6612FNG驱动芯片简介:

TB6612FNG驱动芯片是一款微型全桥驱动芯片,最大驱动电压10v,单H桥最大电流1.5A,适合驱动小型的编码电机和步进电机,而且驱动电平为0-5V,适合大部分MCU来控制,是一款不可多得的驱动芯片,功能十分的强大。
这款驱动芯片带有两个H桥,可以驱动两个电机,总电流不推荐超过2.5A(大部分也不会这样),而且芯片自带睡眠低功耗和高温停机功能,整体十分的强大。
而且这款芯片的H桥经过特殊设计,保证了直通的情况下不会烧毁芯片,而是会让电机缓慢停下,但是这毕竟不是H桥的推荐做法,所以最好不要让通道同时为高电平。
 title=
这款芯片支持低功耗,在Standby引脚接低电平或者悬空时芯片处于休眠模式,此时对外部动作没有响应,只有当Standby为高电平时才会让芯片启动,对外界有所响应。
没有电机输出有单独的使能引脚,高电平使能(电机响应通道变话),低电平失能(电机对互补通道无响应,输出两个通道都是低电平),这里虽然可以把MCU的定时器一个PWM接入这个使能引脚,频繁使失能通道让其实现调速,但是也不是一个推荐的做法,这里使用互补通道实现电机调速的功能(本例使用通道A)。
 title=

实现方法:

首先,使用官方推荐的方法把TB6612芯片根据电路图连接好,然后连接上电机,由于我们采用互补通道的方法实现H桥电路,所以,把PWMA通道给高,使能通道A,接着使用死区来防止直通现象导致的烧毁(虽然这颗芯片不会烧),然后调整CCR寄存器的占空比来调整电机的转速,转速来自于CCR的占空比,这样就可以实现改变速度的效果。
 title=

高级定时器产生互补通道驱动:

普通定时器产生互补通道信号,插入死区然后开启刹车模式,这里在开启一个定时器作为输入捕获测量编码器频率,至于我为啥不使用PWM模式,是因为没有找到定时器TRGO的对应表,想使用TRGO复位编码器没办法,懒得开中断手动复位了,反正需求就是测量速度和转速,所以在这里直接使用输入捕获来测量速度即可,使用内部的触发复位定时器是一个很方便的选择,所以这里使用输入捕获测速度。
通过定时器产生的互补通道来驱动电机,然后插入死区,设置刹车功能,就可以暂时实现电机的驱动功能,实现调速和转度反馈。
代码如下:
首先开启一个普通定时器,开启中断按键检测功能,让按键的触发来改变电机速度和旋转方向,代码如下,首先初始化时钟,然后配置定时器时基单元,最后开启中断和配置NVIC,定时器启动就算开启了定时器中断。(配置一个基础定时器,不要占用其他复杂定时器的功能)。

void TIMER6_Config(void)//普通定时器6,作为按键检测
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
    //RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
    TIM_OCInitTypeDef TIM_OC_INIT = {0};
    NVIC_InitTypeDef NVIC_InitStruct;
    RCC_ClocksTypeDef *rcc_clock_get = (RCC_ClocksTypeDef *)malloc(sizeof(RCC_ClocksTypeDef));
    RCC_GetClocksFreq(rcc_clock_get);
    TIM_TimeBaseInitTypeDef TIMER_Config = {0};
    TIMER_Config.TIM_Prescaler = rcc_clock_get ->PCLK1_Frequency / 10000 - 1;       
    TIMER_Config.TIM_CounterMode = TIM_CounterMode_Up;      
    TIMER_Config.TIM_Period = 200-1;           
    TIMER_Config.TIM_ClockDivision = TIM_CKD_Div1;    
    TIMER_Config.TIM_RepetitionCounter = 0x00;
    TIM_TimeBaseInit(TIM6,&TIMER_Config);       
    TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
    NVIC_InitStruct.NVIC_IRQChannel = TIM6_IRQn;
  NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStruct);
    TIM_Cmd(TIM6,ENABLE);
}  

void TIM6_IRQHandler(void)
{
   static bool state = false;
   static uint16_t K1_status,K1_last_status = RESET,\
   K2_status,K2_last_status = RESET;
   if(TIM_GetITStatus(TIM6,TIM_IT_Update) == SET)
   {
      TIM_ClearITPendingBit(TIM6,TIM_IT_Update);
      K1_status = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0);
      K2_status = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1);
      if(K1_status== RESET && K1_last_status == SET)
      {
         Freq -= 25;
         if(Freq <0)
         {
            Freq =500;
            TIM_SetCompare2(TIM1,Freq-1);
         }
         else
         {
            TIM_SetCompare2(TIM1,Freq);
         }
    }
    K1_last_status = K1_status ;
    if(K2_status== RESET && K2_last_status == SET)
    {
       if(state)
       {
           TIM_OC_INIT.TIM_OCIdleState = TIM_OCIdleState_Reset;
           TIM_OC_INIT.TIM_OCMode = TIM_OCMode_PWM1;
           TIM_OC_INIT.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
           TIM_OC_INIT.TIM_OCNPolarity = TIM_OCNPolarity_High;
           TIM_OC_INIT.TIM_OCPolarity = TIM_OCPolarity_High;
           TIM_OC_INIT.TIM_OutputNState = TIM_OutputNState_Enable;
            TIM_OC_INIT.TIM_OutputState = TIM_OutputState_Enable;
            TIM_OC_INIT.TIM_Pulse =Freq;
            TIM_OC2Init(TIM1,&TIM_OC_INIT);
            state = false;
            Dir = true;
       }
        else
        {
            Dir = false;
            TIM_OC_INIT.TIM_OCIdleState = TIM_OCIdleState_Reset;
            TIM_OC_INIT.TIM_OCMode = TIM_OCMode_PWM1;
            TIM_OC_INIT.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
            TIM_OC_INIT.TIM_OCNPolarity = TIM_OCNPolarity_Low;
            TIM_OC_INIT.TIM_OCPolarity = TIM_OCPolarity_Low;
            TIM_OC_INIT.TIM_OutputNState = TIM_OutputNState_Enable;
            TIM_OC_INIT.TIM_OutputState = TIM_OutputState_Enable;
            TIM_OC_INIT.TIM_Pulse =Freq;
            TIM_OC2Init(TIM1,&TIM_OC_INIT);
            state = true;
        }

    }
    K2_last_status = K2_status ;
  }
}

接下来就可以开启配置互补通道的定时器了:

void Motor_Config(void)//配置PA1,PA2作为 
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
    RCC_ClocksTypeDef *rcc_clock_get = (RCC_ClocksTypeDef *)malloc(sizeof(RCC_ClocksTypeDef));
    RCC_GetClocksFreq(rcc_clock_get);
    GPIO_InitTypeDef GPIO_Initstructer;
    TIM_BDTRInitTypeDef TIM_BDTR_Config = {0};
    TIM_TimeBaseInitTypeDef TIMER_Config = {0};
    TIMER_Config.TIM_Prescaler = rcc_clock_get ->PCLK2_Frequency / 1000000 - 1;       
    TIMER_Config.TIM_CounterMode = TIM_CounterMode_Up;      
    TIMER_Config.TIM_Period = 1000-1;           
    TIMER_Config.TIM_ClockDivision = TIM_CKD_Div1;    
    TIMER_Config.TIM_RepetitionCounter = 0x00;
    TIM_TimeBaseInit(TIM1,&TIMER_Config); 
    TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable);
    TIM_OC2PreloadConfig(TIM1,TIM_OCPreload_Enable);
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_13;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_Middle;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_6 ;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_8 ;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource13,GPIO_AF_25);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_1);
    GPIO_WriteBit(GPIOA,GPIO_Pin_8,Bit_SET);
    TIM_OC_INIT.TIM_OCIdleState = TIM_OCIdleState_Reset;
    TIM_OC_INIT.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OC_INIT.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    TIM_OC_INIT.TIM_OCNPolarity = TIM_OCNPolarity_High;
    TIM_OC_INIT.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC_INIT.TIM_OutputNState = TIM_OutputNState_Enable;
    TIM_OC_INIT.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OC_INIT.TIM_Pulse =500;
    TIM_OC2Init(TIM1,&TIM_OC_INIT);
    TIM_CCxCmd(TIM1,TIM_Channel_2,TIM_CCx_Enable);
    TIM_CCxNCmd(TIM1,TIM_Channel_2,TIM_CCxN_Enable);
    TIM_CtrlPWMOutputs(TIM1,ENABLE);
    TIM_BDTRStructInit(&TIM_BDTR_Config);
    TIM_BDTR_Config.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
    TIM_BDTR_Config.TIM_Break = TIM_Break_Enable;
    TIM_BDTR_Config.TIM_BreakPolarity = TIM_BreakPolarity_High;
    TIM_BDTR_Config.TIM_DeadTime = 0x69;
    TIM_BDTR_Config.TIM_LOCKLevel = TIM_LOCKLevel_OFF;
    TIM_BDTR_Config.TIM_OSSIState = TIM_OSSIState_Enable;
    TIM_BDTR_Config.TIM_OSSRState =TIM_OSSRState_Enable ;
    TIM_BDTRConfig(TIM1,&TIM_BDTR_Config);
    TIM_BreakInputFilterConfig(TIM1,TIM_IOBKIN_BKIN1,TIM_BKINF_2);
    TIM_BreakInputFilterCmd(TIM1,ENABLE);
    TIM_Cmd(TIM1,ENABLE);
}

这里开启互补通道和死区功能。这样就可以配置电机的H桥电路,配置刹车电路的目的在于当出现突发情况,可以及时停止PWM的生成,保护电机和外围电路。
接下来就可以配置一个定时器作为编码器的TRGO信号,注意,由于使用的是定时器触发信号复位定时器,所以不可以把驱动PWM的定时器和编码器的输入捕获定时器放在一起,触发复位会打断PWM的生成。(这里使用TIM3)

void Encoder_Config(void) //Based on TIM3
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    GPIO_InitTypeDef GPIO_INIT = {0};
    TIM_ICInitTypeDef TIM3_IC = {0};
    RCC_ClocksTypeDef *rcc_clock_get = (RCC_ClocksTypeDef *)malloc(sizeof(RCC_ClocksTypeDef));
    RCC_GetClocksFreq(rcc_clock_get);
    GPIO_InitTypeDef GPIO_Initstructer;
    TIM_TimeBaseInitTypeDef TIMER_Config = {0};
    TIM_OCInitTypeDef TIM_OC_INIT = {0};
    TIMER_Config.TIM_Prescaler = rcc_clock_get ->PCLK1_Frequency / 1000000 - 1;       
    TIMER_Config.TIM_CounterMode = TIM_CounterMode_Up;      
    TIMER_Config.TIM_Period = 65535;           
    TIMER_Config.TIM_ClockDivision = TIM_CKD_Div1;    
    TIMER_Config.TIM_RepetitionCounter = 0x00;
    TIM_TimeBaseInit(TIM3,&TIMER_Config); 
    TIM3_IC.TIM_Channel = TIM_Channel_2;
    TIM3_IC.TIM_ICFilter = 0x0f;
    TIM3_IC.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM3_IC.TIM_ICPrescaler = TIM_ICPSC_Div1;
    TIM3_IC.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM3,&TIM3_IC);
    TIM_SelectInputTrigger(TIM3,TIM_TS_TI2FP2);
    TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
    TIM_SelectMasterSlaveMode(TIM3,TIM_MasterSlaveMode_Enable);
    GPIO_INIT.GPIO_Mode = GPIO_Mode_FLOATING;
    GPIO_INIT.GPIO_Pin = GPIO_Pin_7;
    GPIO_INIT.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_INIT);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource7,GPIO_AF_2);
    TIM_SetCompare2(TIM3,0);
    TIM3 -> CNT = 0;
    TIM3 -> CCR2 = 0;
    
    TIM_Cmd(TIM3,ENABLE);
    
}

这样就可以开启定时器的输入捕获,其实说实话,这样有个缺陷,就是不能获取到旋转方向,但是这里由于电机的旋转方向是我们主机产生的,不会出现旋转方向和实际方向不同的现象,所以不需要获取方向也行的。
这样就可以实际的配置定时器3的捕获比较通道为触发,复位定时器,这样就可以实现速度测量。
最后配置一下OLED的显示模块:

void Show(void)
{
    uint32_t Circle;
    uint8_t Buffer[40];
    static uint32_t i = 0;
    sprintf((char *)Buffer,"Time:%d ",i++);
    OLED_ShowString(2,1,(char *)Buffer);
    sprintf((char *)Buffer,"Freq:%d%%  ",(int)((500.0f - (float)Freq )/ 5.0f));
    OLED_ShowString(3,1,(char *)Buffer);
    Circle = 1000000/(TIM_GetCapture2(TIM3)*7) % 1000 ;
    sprintf((char *)Buffer,"Speed:%d c/s %c ",Circle>350?0:Circle,Dir ? 'N':'S');
    OLED_ShowString(4,1,(char *)Buffer);
    PLATFORM_DelayMS(500);
}

这里显示速度使用了三元运算符,是因为第一次读取,由于电机没有转动,所以读取到的数据是0,这样会导致转速无穷大,导致显示错误,所以这里判断一下速度是数值,因为速度不会大于300,为了保险起见设置为350,这样在开机时会显示为0,令数据显示正确,这里使用一个变量在换向里改变以下旋转方向,就可以实现方向的显示。
最后在main函数里启动一下:

int main(void)
{
      PLATFORM_Init();
        LED_Config();
        Key_Init();
        PLATFORM_DelayMS(500);
        OLED_Init();
        GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
        OLED_ShowString(1,1,"Motor_Test");
        TIMER6_Config();
        Motor_Config();
        Encoder_Config();
    for(;;)
    {
            GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_SET);
            Show();
            GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
            Show();
    }
}

效果如下,控制起来和预想的一样,可以看出高级定时器产生的互补PWM波形还是很精确的,可以满足我们的需求:

补充:刹车中断:

在发生刹车事件时,如果开启中断,那么刹车中断回向CPU申请中断,这个中断在触发时会直接阻塞程序运行,手册里说当发生刹车事件时,BLF标志位不会被清除,也就是说中断不会停止,直到刹车事件结束才可以由软件清除,这样可以在发生事件之后直接停止刹车并且中断不会被忽略,以确保电机可以及时停下来,防止产生其他的危害行为。
代码如下:
在电机控制里配置NVIC和定时器中断,这样可以触发刹车中断,这样在发生刹车事件可以触发中断,这个中断会一直触发并且不能被软件清除标志位停止,这样虽然会阻塞程序,但是提高了对刹车事件响应的安全性(一般使用的时候把中断优先级放低一点,防止阻塞其他中断)。

void Motor_Config(void)//配置PA1,PA2作为 
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
    RCC_ClocksTypeDef *rcc_clock_get = (RCC_ClocksTypeDef *)malloc(sizeof(RCC_ClocksTypeDef));
    RCC_GetClocksFreq(rcc_clock_get);
    NVIC_InitTypeDef NVIC_InitStruct;
    GPIO_InitTypeDef GPIO_Initstructer;
    TIM_BDTRInitTypeDef TIM_BDTR_Config = {0};
    TIM_TimeBaseInitTypeDef TIMER_Config = {0};
    TIMER_Config.TIM_Prescaler = rcc_clock_get ->PCLK2_Frequency / 1000000 - 1;       
    TIMER_Config.TIM_CounterMode = TIM_CounterMode_Up;      
    TIMER_Config.TIM_Period = 1000-1;           
    TIMER_Config.TIM_ClockDivision = TIM_CKD_Div1;    
    TIMER_Config.TIM_RepetitionCounter = 0x00;
    TIM_TimeBaseInit(TIM1,&TIMER_Config); 
    NVIC_InitStruct.NVIC_IRQChannel = TIM1_BRK_IRQn;
        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
        NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
        NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
        NVIC_Init(&NVIC_InitStruct);
    TIM_ITConfig(TIM1,TIM_IT_Break,ENABLE);
    TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable);
    TIM_OC2PreloadConfig(TIM1,TIM_OCPreload_Enable);
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_13;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_Middle;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_6 ;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    
    GPIO_Initstructer.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_Initstructer.GPIO_Pin = GPIO_Pin_8 ;
    GPIO_Initstructer.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_Initstructer);
    
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_1);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource13,GPIO_AF_25);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource6,GPIO_AF_1);
    GPIO_WriteBit(GPIOA,GPIO_Pin_8,Bit_SET);
    TIM_OC_INIT.TIM_OCIdleState = TIM_OCIdleState_Reset;
    TIM_OC_INIT.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OC_INIT.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    TIM_OC_INIT.TIM_OCNPolarity = TIM_OCNPolarity_High;
    TIM_OC_INIT.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC_INIT.TIM_OutputNState = TIM_OutputNState_Enable;
    TIM_OC_INIT.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OC_INIT.TIM_Pulse =500;
    TIM_OC2Init(TIM1,&TIM_OC_INIT);
    TIM_CCxCmd(TIM1,TIM_Channel_2,TIM_CCx_Enable);
    TIM_CCxNCmd(TIM1,TIM_Channel_2,TIM_CCxN_Enable);
    TIM_CtrlPWMOutputs(TIM1,ENABLE);
    TIM_BDTRStructInit(&TIM_BDTR_Config);
    TIM_BDTR_Config.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
    TIM_BDTR_Config.TIM_Break = TIM_Break_Enable;
    TIM_BDTR_Config.TIM_BreakPolarity = TIM_BreakPolarity_High;
    TIM_BDTR_Config.TIM_DeadTime = 0x69;
    TIM_BDTR_Config.TIM_LOCKLevel = TIM_LOCKLevel_OFF;
    TIM_BDTR_Config.TIM_OSSIState = TIM_OSSIState_Enable;
    TIM_BDTR_Config.TIM_OSSRState =TIM_OSSRState_Enable ;
    TIM_BDTRConfig(TIM1,&TIM_BDTR_Config);
    TIM_BreakInputFilterConfig(TIM1,TIM_IOBKIN_BKIN1,TIM_BKINF_2);
    TIM_BreakInputFilterCmd(TIM1,ENABLE);
    TIM_Cmd(TIM1,ENABLE);
}

void TIM1_BRK_IRQHandler(void)
{
        if(TIM_GetITStatus(TIM1,TIM_IT_Break) == SET)
        {
            TIM_ClearITPendingBit(TIM1,TIM_IT_Break);
            RES = true;
            if(Break_status)
            {
                Break_status = false;
                GPIO_WriteBit(GPIOB,GPIO_Pin_14,Bit_RESET);
                OLED_ShowString(1,1,"Break_Locking!");
                sprintf((char *)Buffer,"Speed:%d c/s %c ",0,Dir ? 'N':'S');
                OLED_ShowString(4,1,(char *)Buffer);
            }
        }
}

 title=
 title=
PS:演示视频在本文末尾。

MindPwm定时器产生的互补通道驱动:

Mindpwm是灵动微电子为MM32驱动电机特意添加的一个外设,其特点就是可以产生精度更高的PWM波形,可以帮助我们更好地控制电机,由于精度更高,所以配置的PWM波形的分度值更高,对电机的运行速度有着更高的控制精度,所以使用MindPwm来控制电机的互补通道的效益更好,所以这里来测试一下MindPwm产生的互补通道的效果是否真的可以达到更好地控制精度。
这里使用MindPwm产生互补通道来驱动这个电机,方法和上面高级定时器一样,但是需要单独配置一些其他的部分。
首先,注意到MindPWM有四个模块,每个模块都可以产生独立的PWM波形,每个模块有两个输出口,可配置为PWM互补通道的输出,所以,在这个程序里,使用Module0的PWM互补输出来实现电机调速的功能。
首先配置一下输出的引脚,注意AF重定向的问题,无数名将折戟在此,所以需要看准了配置,然后配置MindPWM的时钟,Reload的逻辑和方向,最后配置单独模块的具体细节,包括周期和占空比,Active极性等,最后加载到寄存器里,开启模块即可使用。
在按钮改变占空比和旋转方向时,由于没找到(或者没时间找),所以写了这么不简洁但是能跑的代码,效果和预计的一样,其他的编码器沿用上面的高级定时器部分。
需要注意的是MindPwm模块不支持刹车功能,但是支持故障模式,具体实现类似于刹车,但是有更多的功能。
剩下的配置没啥好说的,配置如下:
先设置引脚的AF属性,配置好引脚的功能,然后开始配置MindPWM部分:
设置时钟位BUS也就是使用内部时钟来为提供时钟信号;
Bebug模式指的是当系统在Debug时,MindPwm是否受Debug模式影响停下待命还是不管Debug;
团体触发:设置为本地触发(不使用TRGO)
初始化控制:本地初始化控制,不受其他模块影响(虽然Module0是发送信号的一方,不接受信号)
配对操作:指的是指定哪一个通道位互补通道,这里指定通道A作为互补主通道,改变PWMA的占空比会影响到PWMB互补的占空比,但是改变PWMB的占空比不会有任何变化。
预分频器:给8分频;
重装载时机:每一个时机重装载;
重装载逻辑:在一个周期结束以后重装载,当然有半周期,这里为了完整性选择一个整周期。
选择重装载来源:这里选择本地寄存器重装载,毕竟是改变自本地寄存器。
初始化以后开始配置Module0模块的细节:
选择通道号:A.B都使用,所以都选,需要一个结构体数组,每个数组放一个通道配置。
水平:指的是在通道被激活以后输出的电平,这里当通道需要输出(激活)以后,输出高电平。
占空比:是浮点数,可以设置小数占空比,这也是MindPWM的优势所在。
死区时间,在互补PWM中插入死区时间,来让互补PWM的波形有时间间隔的使能,防止H桥烧掉。
默认状态:这里没啥要求,使用默认状态0;
通道使能:当然得使能这个通道,要不然用不了。
PWMB的配置也是如此。
最后就可以把配置使能并且锁定到寄存器里,然后开启MindPWM的模块开始输出PWM波形。

/*----------以上是高级定时器互补通道产生的PWM驱动------------*/
/*----------以下是MindPwm驱动互补通道产生的PWM驱动-----------*/
PWM_InitTypeDef MindPwm_Config;
void Mind_Pwm_Config(void)
{
    RCC_ClocksTypeDef RCC_Clocks;
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA | RCC_AHBPeriph_GPIOC,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_MDPWM, ENABLE);
    GPIO_InitTypeDef GPIO_INIT = {0};
  GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_11);  //MDPWM_CHA1
  GPIO_INIT.GPIO_Pin  =  GPIO_Pin_6;
    GPIO_INIT.GPIO_Speed = GPIO_Speed_High;
  GPIO_INIT.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOC, &GPIO_INIT);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource1,GPIO_AF_17);
    GPIO_INIT.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_INIT.GPIO_Pin = GPIO_Pin_1;
    GPIO_INIT.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_INIT);
    GPIO_INIT.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_INIT.GPIO_Pin = GPIO_Pin_8;
    GPIO_INIT.GPIO_Speed = GPIO_Speed_High;
    GPIO_Init(GPIOA,&GPIO_INIT);
  RCC_GetClocksFreq(&RCC_Clocks);
    APB2_CLOCK = RCC_Clocks.PCLK2_Frequency;
    MindPwm_Config.clockSource =PWM_BusClock ;
    MindPwm_Config.enableDebugMode = true;
    MindPwm_Config.forceTrigger = PWM_Force_Local;
    MindPwm_Config.initializationControl = PWM_Initialize_LocalSync;
    MindPwm_Config.pairOperation = PWM_ComplementaryPwmA;
    MindPwm_Config.prescale = PWM_Prescale_Divide_8;
    MindPwm_Config.reloadFrequency =PWM_LoadEveryOportunity ;
    MindPwm_Config.reloadLogic = PWM_ReloadPwmFullCycle;
    MindPwm_Config.reloadSelect =PWM_LocalReload ;
    PWM_Init(MindPWM,PWM_Module_0,&MindPwm_Config);
    pwmSignal[0].pwmChannel       = PWM_PwmA;
    pwmSignal[0].level            = PWM_HighTrue;
    pwmSignal[0].dutyCyclePercent = 50.0f; 
    pwmSignal[0].deadtimeValue    = 0x69;
    pwmSignal[0].faultState       = PWM_PwmFaultState0;
    pwmSignal[0].pwmchannelenable = true;
    pwmSignal[1].pwmChannel = PWM_PwmB;
    pwmSignal[1].level      = PWM_HighTrue;
    pwmSignal[1].dutyCyclePercent = 0.0f;
    pwmSignal[1].deadtimeValue    = 0x69;
    pwmSignal[1].faultState       = PWM_PwmFaultState0;
    pwmSignal[1].pwmchannelenable = true;
    PWM_SetupPwm(MindPWM, PWM_Module_0, pwmSignal, 2, PWM_UpwardCenterAligned, 1000.0f, APB2_CLOCK);
    PWM_SetPwmLdok(MindPWM, PWM_Control_Module_0,true);
    PWM_StartTimer(MindPWM, PWM_Control_Module_0);
    GPIO_WriteBit(GPIOA,GPIO_Pin_8,Bit_SET); //这里使能H桥的输出。
}
/*---------------------------E-N-D--------------------------*/

按键更改PWM占空比和电机选转方向:
这里没啥好说的,但是很不简洁,由于时间关系,等下一篇给出一个简洁的更改方案。
逻辑就是更改占空比(由于是以PWMA为主通道,所以更改PWMA的占空比即可同步更改互补PWMB的占空比)。
换向就是更改占空比的状态数值实现的。
电机在0%时正向转速最快,然后递减到50%停止,然后从50%开始反向旋转,转速逐渐上升,到100%达到最大值,所以合理设置占空比即可是实现换向和调速的功能。

uint32_t APB2_CLOCK;
pwm_signal_param_t pwmSignal[2] ={0};
void TIM6_IRQHandler(void)
{
    static bool state = false,go = false;
    static uint16_t K1_status,K1_last_status = RESET,\
        K2_status,K2_last_status = RESET;
        if(TIM_GetITStatus(TIM6,TIM_IT_Update) == SET)
        {
            TIM_ClearITPendingBit(TIM6,TIM_IT_Update);
            K1_status = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0);
            K2_status = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1);
            if((K1_status== RESET && K1_last_status == SET) || go == true)
            {
                go =false;
                if(Dir)
                {
                    Freq -= 25;
                    if(Freq <0)
                    {
                        Freq =500;
                        pwmSignal[0].pwmChannel       = PWM_PwmA;
                        pwmSignal[0].level            = PWM_HighTrue;
                        pwmSignal[0].dutyCyclePercent = ((float)Freq )/ 10.0f ; 
                        pwmSignal[0].deadtimeValue    = 0x69;
                        pwmSignal[0].faultState       = PWM_PwmFaultState0;
                        pwmSignal[0].pwmchannelenable = true;
                        PWM_SetupPwm(MindPWM, PWM_Module_0, pwmSignal, 1, PWM_UpwardCenterAligned, 1000.0f, APB2_CLOCK);
                        PWM_SetPwmLdok(MindPWM, PWM_Control_Module_0,true);
                    }
                    else
                    {
                        pwmSignal[0].pwmChannel       = PWM_PwmA;
                        pwmSignal[0].level            = PWM_HighTrue;
                        pwmSignal[0].dutyCyclePercent = ((float)Freq )/ 10.0f; 
                        pwmSignal[0].deadtimeValue    = 0x69;
                        pwmSignal[0].faultState       = PWM_PwmFaultState0;
                        pwmSignal[0].pwmchannelenable = true;
                        PWM_SetupPwm(MindPWM, PWM_Module_0, pwmSignal, 1, PWM_UpwardCenterAligned, 1000.0f, APB2_CLOCK);
                        PWM_SetPwmLdok(MindPWM, PWM_Control_Module_0,true);
                    }
                }
                else
                {
                    Freq -= 25;
                    if(Freq <0)
                    {
                        Freq =500;
                        pwmSignal[0].pwmChannel       = PWM_PwmA;
                        pwmSignal[0].level            = PWM_HighTrue;
                        pwmSignal[0].dutyCyclePercent = (500.0f - (float)Freq) / 10.0f+50.0f ; 
                        pwmSignal[0].deadtimeValue    = 0x69;
                        pwmSignal[0].faultState       = PWM_PwmFaultState0;
                        pwmSignal[0].pwmchannelenable = true;
                        PWM_SetupPwm(MindPWM, PWM_Module_0, pwmSignal, 1, PWM_UpwardCenterAligned, 1000.0f, APB2_CLOCK);
                        PWM_SetPwmLdok(MindPWM, PWM_Control_Module_0,true);
                    }
                    else
                    {
                        pwmSignal[0].pwmChannel       = PWM_PwmA;
                        pwmSignal[0].level            = PWM_HighTrue;
                        pwmSignal[0].dutyCyclePercent =(500.0f - (float)Freq) / 10.0f+50.0f  ;
                        pwmSignal[0].deadtimeValue    = 0x69;
                        pwmSignal[0].faultState       = PWM_PwmFaultState0;
                        pwmSignal[0].pwmchannelenable = true;
                        PWM_SetupPwm(MindPWM, PWM_Module_0, pwmSignal, 1, PWM_UpwardCenterAligned, 1000.0f, APB2_CLOCK);
                        PWM_SetPwmLdok(MindPWM, PWM_Control_Module_0,true);
                    }
                    
                    
                    
                    
                }
                
            }
            K1_last_status = K1_status ;
        if(K2_status== RESET && K2_last_status == SET)
        {
            if(state)
            {
                Freq += 25;
                go = true;
                Dir = true;
                state = false;
            }
            else
            {
                Freq += 25;
                go = true;
                Dir = false;
                state = true;
            }
            
        }
        K2_last_status = K2_status ;
    }
}

最后在main函数里调用一下MindPwm的配置函数即可运行MindPWM:

int main(void)
{
      PLATFORM_Init();
        LED_Config();
        Key_Init();
        PLATFORM_DelayMS(500);
        OLED_Init();
        GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
        GPIO_WriteBit(GPIOB,GPIO_Pin_14,Bit_SET);
        OLED_ShowString(1,1,"Motor_Test");
        TIMER6_Config();
        //Motor_Config();
        Mind_Pwm_Config();
        Encoder_Config();
    for(;;)
    {
            GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_SET);
            Show();
            GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
            Show();
    }
}

效果如下,和高级定时器的功能是一样的。
效果如下:
【极术社区MM32电机测评-哔哩哔哩】
可以看这个视频来获取我的测评结果。

总结:

这里使用高级定时器和MindPWM功能驱动电机,但是在MindPWM驱动的换向,还没找到可以更换占空比的函数,所以准备在下一篇把更改后的方案给出。
MindPwm的精度和效果还是很不错的,这个外设很值得我们去学习和使用,在电机控制方面有很大的应用场景。
MindPWM在使用效果上虽然灵活度没有高级定时器高,但是其特有的FAULT处理和浮点占空比以及自己算频率的特点还是很值得我们去使用这个外设的。
/*作者:是小枼大人🍡*/
/*如果喜欢我的文章,欢迎点个免费的赞,您的赞是我更新的动力*/
/*---Clannad---*/

推荐阅读
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息