程序和开发板原理图的下载地址:https://pan.baidu.com/s/1gSrfLV3KZ4wMw1B5LAwrXQ(提取码:t8pf)
本程序通过方波驱动联控智能的24V无刷电机。开发板为联控智能低压板,使用的单片机为STM32F405RG,晶振的大小为8MHz,如下图所示。程序实现了电机正转、反转、停转、速度增加和速度减小这五种功能,分别由按键1~5控制。程序为开环控制,没有实现PID速度闭环,也没有利用开发板上的各种电路保护器件(如MOS管过流保护、总电源过压保护等等)。所以,在运行程序前,一定要保证开发板供电电压为24V,而且电机没有被堵转。
以下是电机参数:
从电机参数中,我们可以直接看到什么HALL值下该换什么相,方便编写程序。
【程序代码】
main.c:
#include
#include
#include "common.h"
#include "Keyboard.h"
#include "Motor.h"
int main(void)
{
int key;
HAL_Init();
clock_init();
usart_init(115200);
printf("STM32F405RG Motor\n");
printf("SystemCoreClock=%u\n", SystemCoreClock);
Keyboard_Init();
Motor_Init();
while (1)
{
if (key == -1)
{
// 等待按键按下
key = Keyboard_Read();
if (key != -1)
{
// 按键处理
switch (key)
{
case 1:
Motor_Start(MOTORDIRECTION_FORWARD);
break;
case 2:
Motor_Start(MOTORDIRECTION_BACKWARD);
break;
case 3:
Motor_Stop();
break;
case 4:
Motor_SpeedUp();
break;
case 5:
Motor_SpeedDown();
break;
}
}
}
else
{
// 等待按键松开
if (Keyboard_Read() == -1)
key = -1;
}
}
}
Motor.c:
#include
#include
#include
#include "Motor.h"
// HALL信号
#define HALL_U (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_6) == GPIO_PIN_SET)
#define HALL_V (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET)
#define HALL_W (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_SET)
// 上管控制
#define UH_0 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0)
#define UH_1 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, abs(motor_speed))
#define VH_0 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, 0)
#define VH_1 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, abs(motor_speed))
#define WH_0 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 0)
#define WH_1 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, abs(motor_speed))
// 下管控制
#define UL_0 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET)
#define UL_1 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET)
#define VL_0 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET)
#define VL_1 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET)
#define WL_0 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_RESET)
#define WL_1 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET)
// 霍尔值表
static const uint8_t motor_halltable[2][6] = {
{2, 6, 4, 5, 1, 3}, // 正转
{5, 1, 3, 2, 6, 4} // 反转
};
TIM_HandleTypeDef htim1;
static int8_t motor_lasthall = -1;
static int16_t motor_speed;
static uint32_t motor_lasttime;
void Motor_Init(void)
{
GPIO_InitTypeDef gpio;
TIM_OC_InitTypeDef tim_oc = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_TIM1_CLK_ENABLE();
// IR2101S的HIN和LIN上面是没有横线的, 一定要把PDF放大了看, 不要看错了
// HIN与HO, LIN与LO始终都是同向的
// U,V,W上管 (高电平导通MOS管)
gpio.Alternate = GPIO_AF1_TIM1;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOA, &gpio);
// HALL信号
gpio.Mode = GPIO_MODE_IT_RISING_FALLING;
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &gpio);
// U,V,W下管 (高电平导通MOS管)
UL_0;
VL_0;
WL_0;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);
htim1.Instance = TIM1;
htim1.Init.Period = 12000;
htim1.Init.Prescaler = 0;
HAL_TIM_PWM_Init(&htim1);
// 这里的Pulse就是寄存器中的CCRx
// 在PWM1模式下, 若OCPolarity=TIM_OCPOLARITY_HIGH, 只要CNT=CCR时输出低电平
// 若CCR=0则一直输出低电平, 因为CNT永远不会小于CCR
// 若CCR=ARR+1则一直输出高电平, 因为CNT永远小于CCR (ARR就是Init.Period)
tim_oc.OCMode = TIM_OCMODE_PWM1;
tim_oc.Pulse = 0;
HAL_TIM_PWM_ConfigChannel(&htim1, &tim_oc, TIM_CHANNEL_1);
HAL_TIM_PWM_ConfigChannel(&htim1, &tim_oc, TIM_CHANNEL_2);
HAL_TIM_PWM_ConfigChannel(&htim1, &tim_oc, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
uint8_t Motor_ReadHall(void)
{
return (HALL_W << 2) | (HALL_V << 1) | HALL_U;
}
int Motor_SpeedDown(void)
{
int16_t new_speed;
if (motor_speed == 0)
return Motor_Start(MOTORDIRECTION_BACKWARD);
new_speed = motor_speed - 1000;
if (motor_speed > 0 && new_speed <= 0)
return Motor_Stop();
else if (abs(new_speed) > htim1.Init.Period)
return -1;
motor_speed = new_speed;
printf("Motor speed: %d\n", motor_speed);
return 0;
}
int Motor_SpeedUp(void)
{
int16_t new_speed;
if (motor_speed == 0)
return Motor_Start(MOTORDIRECTION_FORWARD);
new_speed = motor_speed + 1000;
if (motor_speed < 0 && new_speed >= 0)
return Motor_Stop();
else if (abs(new_speed) > htim1.Init.Period)
return -1;
motor_speed = new_speed;
printf("Motor speed: %d\n", motor_speed);
return 0;
}
int Motor_Start(MotorDirection direction)
{
if (motor_speed != 0)
return -1;
motor_speed = 1000 * direction;
__HAL_GPIO_EXTI_GENERATE_SWIT(GPIO_PIN_6); // 软件触发HALL中断
printf("Motor is started! speed=%d\n", motor_speed);
return 0;
}
int Motor_Stop(void)
{
if (motor_speed == 0)
return -1;
motor_speed = 0;
motor_lasthall = -1;
UH_0;
VH_0;
WH_0;
UL_0;
VL_0;
WL_0;
printf("Motor is stopping...\n");
motor_lasttime = HAL_GetTick();
while (HAL_GetTick() - motor_lasttime < 250);
printf("Motor is stopped!\n");
return 0;
}
void Motor_Switch(void)
{
uint8_t direction, hall;
hall = Motor_ReadHall();
if (motor_speed == 0)
{
// 电机停转时, 手动旋转电机, 可以读取HALL值
printf("Hall value: %u\n", hall);
motor_lasttime = HAL_GetTick();
return;
}
else if (motor_speed > 0)
direction = 0; // 正转
else
direction = 1; // 反转
// 忽略重复的中断
if (hall == motor_lasthall)
return;
motor_lasthall = hall;
// 霍尔换相
if (hall == motor_halltable[direction][0])
{
// AB (A的上管导通, B的下管导通)
// U=A, V=B, W=C
UH_1;
VH_0;
WH_0;
// 先关闭其他下管, 再开需要的下管
UL_0;
WL_0;
VL_1;
}
else if (hall == motor_halltable[direction][1])
{
// AC (A的上管导通, C的下管导通)
UH_1;
VH_0;
WH_0;
UL_0;
VL_0;
WL_1;
}
else if (hall == motor_halltable[direction][2])
{
// BC
UH_0;
VH_1;
WH_0;
UL_0;
VL_0;
WL_1;
}
else if (hall == motor_halltable[direction][3])
{
// BA (B的上管导通, A的下管导通)
UH_0;
VH_1;
WH_0;
VL_0;
WL_0;
UL_1;
}
else if (hall == motor_halltable[direction][4])
{
// CA
UH_0;
VH_0;
WH_1;
VL_0;
WL_0;
UL_1;
}
else if (hall == motor_halltable[direction][5])
{
// CB
UH_0;
VH_0;
WH_1;
UL_0;
WL_0;
VL_1;
}
}
void EXTI9_5_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_6);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_7);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
switch (GPIO_Pin)
{
case GPIO_PIN_6:
case GPIO_PIN_7:
case GPIO_PIN_8:
Motor_Switch();
break;
}
}
Motor.h:
#ifndef _MOTOR_H
#define _MOTOR_H
typedef enum
{
MOTORDIRECTION_FORWARD = 1,
MOTORDIRECTION_BACKWARD = -1
} MotorDirection;
void Motor_Init(void);
uint8_t Motor_ReadHall(void);
int Motor_SpeedDown(void);
int Motor_SpeedUp(void);
int Motor_Start(MotorDirection direction);
int Motor_Stop(void);
void Motor_Switch(void);
#endif
【程序详解】
关于无刷电机的驱动原理请参阅:https://www.zhihu.com/question/20363326/answer/721897772
程序使用motor_speed全局变量保存电机的目标速度,电机的实际速度为HALL中断的触发频率。motor_speed=0为停转,motor_speed>0为正转,motor_speed<0为反转。motor_speed的取值范围为-12000~12000。
电机的驱动电路由6个MOS管和3个预驱芯片IR2101S构成。MOS管的上管为Q1、Q3和Q5,MOS管的下管为Q2、Q4和Q6。每对上下管由一个IR2101S预驱动。
Q1和Q2控制A相(也叫U相),Q3和Q4控制B相(也叫V相),Q5和Q6控制C相(也叫W相)。A、B、C相线的颜色分别为黄、绿、蓝。
其中Q1和Q2的电路如下图所示(其他两路类似):
由于MOS管栅极的导通电压过高,超过了单片机的电平输出能力(3.3V),所以需要使用预驱芯片IR2101S进行升压,升压后再接到MOS管的栅极上。
值得注意的是,IR2101S芯片手册里面的图片,缩小了看,好像HIN和LIN上面有横线,但其实放大了之后根本没有:
所以HIN和HO,LIN和LO是同相的。单片机给HIN(或LIN)输入什么电平,HO(或LO)就输出什么电平。上下管采用的是同型号MOS管,都是N-MOS管。由于采用了同型号管,当下管截止的时候,上管源极的电压就由芯片的VB和VS来提供,这两个电压分别叫做High side floating supply和High side floating supply return,具体不用深究。电路的接法就是参考的IR2101S手册上的Typical Connection,VB和VS上面接有二极管和电容。
我们只需要知道,只要单片机给高电平,相应的MOS管就导通就行了。并且要注意,同一路的上管和下管绝不允许同时导通,否则会发生短路,烧毁MOS管。
我们用“AB”表示A相的上管通,B相的下管通。也就是左边字母的上管通,右边字母的下管通。“BA”就表示B相的上管通,A相的下管通。
HALL值我们用WVU的顺序表示,U,V,W=0,0,1表示为二进制的100也就是十进制的4。十进制的3转换为二进制是011,所以表示的是U,V,W=1,1,0。
根据前面的电机参数图,可以表示出每个HALL值的换向顺序:
HALL4(HA=0,HB=0,HC=1,倒过来是二进制的100,等于十进制的4)=>BC(B的上管通B相就是24V高电平,C的下管通C相就接地了,对应到表中就是V输出1,W输出0,而U的上下管都不通,悬空)
HALL5=>BA(这回是U接地了,不再是W接地,所以C的下管要截止,A的下管要导通)
HALL1=>CA
HALL3=>CB
HALL2=>AB
HALL6=>AC
这就是正转的换相顺序。在换相函数中,我们是从AB开始判断,所以正转的霍尔顺序就是264513,用motor_halltable全局数组保存:
// 霍尔值表
static const uint8_t motor_halltable[2][6] = {
{2, 6, 4, 5, 1, 3}, // 正转
{5, 1, 3, 2, 6, 4} // 反转
};
反转就是把右边的字母左右对调,AB换成BA,AC换成CA等等,于是:
HALL4=>CB
HALL5=>AB
HALL1=>AC
HALL3=>BC
HALL2=>BA
HALL6=>CA
字母对调就相当于把电压反了过来,本来AB是A相24V,B相0V,反过来BA就是A相0V,B相24V,电机就可以反转了。
于是反转的霍尔值表就是513264。(从AB开始判断,第一个HALL值就是5)
从表中还可以发现,同一列正转的HALL值和翻转的HALL值相加,刚好等于7。比如2+5=7、6+1=7等等。
在电机停转的情况下,手动旋转电机,可以在串口中看到每转一定角度HALL值就变化一次,正转和反转的变化序列刚好相反。我们在程序中要做的,正是电机在转起来的情况下,遇到每个HALL值该怎么换相。
HALL信号接的单片机I/O口为PB6~8,我们在这三个引脚上打开EXTI外部中断,任何一个引脚上的电平发生变化,都将触发中断,读取HALL值并换相。上管采用定时器1的PWM波输出,控制电机的转速。下管直接输出高低电平。
上管控制:TIM1_CH1~3(高电平导通)
下管控制:PB13~15(高电平导通)
定时器选择的是PWM1模式,CCR(tim_oc.Pulse以及__HAL_TIM_SET_COMPARE的第三个参数)决定了占空比。
在PWM1模式下,若OCPolarity=TIM_OCPOLARITY_HIGH:
(1)只要CNT
(2)若CCR=0则一直输出低电平,因为CNT永远不会小于CCR。
(3)若CCR=ARR+1则一直输出高电平,因为CNT永远小于CCR (ARR就是Init.Period)。
所以,要想关闭上管,只需要将对应通道的CCR的值设为0就可以了。
启动电机时,先设置motor_speed的值,然后用__HAL_GPIO_EXTI_GENERATE_SWIT(GPIO_PIN_6)这条语句强制触发外部中断,根据当前HALL值开启MOS管,电机就开始转起来了。
关闭电机时,先关闭所有的MOS管,然后用while语句等待HALL值不再变化,电机就停止了。更改电机转速只需要修改motor_speed全局变量就可以,但是如果要改变电机转向,则一定要先让电机停下来,再进行反向转动。也就是说motor_speed一定不能从正值直接改为负值,或者从负值直接改为正值。电机在转动过程中,motor_speed也不允许直接清零,以免HALL中断停止工作后某些MOS管一直导通。要想停转必须调用Motor_Stop函数。
电机在高速转动下可以立即停下来:
Motor is started! speed=1000
Motor speed: 2000
Motor speed: 3000
Motor speed: 4000
Motor speed: 5000
Motor speed: 6000
Motor speed: 7000
Motor speed: 8000
Motor speed: 9000
Motor speed: 10000
Motor speed: 11000
Motor speed: 12000
Motor is stopping...
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Hall value: 1
Hall value: 5
Hall value: 4
Hall value: 6
Hall value: 2
Hall value: 3
Motor is stopped!
更换了轮子之后,HALL值表可能需要进行修改。比如下面这个大轮子,霍尔值表就是:
// 霍尔值表
static const uint8_t motor_halltable[2][6] = {
{1, 3, 2, 6, 4, 5}, // 正转
{6, 4, 5, 1, 3, 2} // 反转
};
这个大轮子的霍尔值表仍然满足每列相加等于7的规律。