前言:
本篇文章通过使用STM32F103C8T6作为主控驱动直流有刷减速电机,实现PWM驱动电机旋转,编码器测速,PID算法控制占空比等功能(注:本篇文章只是为记录本人的一个学习成果,如对某些东西理解有偏差,请谅解,欢迎评论区留言!!!)
目录:
一、概念
1.1 直流有刷电机
直流有刷电机在允许范围内,供电即可工作,只需要调整电压即可改变转速,具有良好的调速性能
**结构:**包括定子(产生固定磁场)、转子(若干绕组组成,受磁场力运动)、电刷(将外部电流输入到绕组上)和换向器(改变绕组中电流方向)。
**参数:**额定电压、额定电流、额定转速(r/min或PRM)、额定扭矩(kg*cm)、减速比(N:1)
**电机原理:**左手定则
左手定则判断方法:伸开左手,使拇指与其他四指垂直且在一个平面内,让磁感线从手心流入, 四指指向电流方向,大拇指指向的就是安培力方向(即导体受力方向)。 有刷直流电机在其电枢上绕有大量的线圈,所产生强大的磁场与外部磁场相互作用产生旋转运动。
有刷电机内部结构图
1.2 PWM介绍
PWM(Pulse Width Modulation,脉冲宽度调制)。是一种利用微处理器的数字输出来对模拟电路进行控制的技术,广泛应用在测量、通信、功率控制等诸多领域。
PWM是定时器输出比较的典型应用。除STM32的基本定时器(TIM6、TIM7)外,其它定时器都支持PWM输出,每个通用定时器(TIM2、TIM3、TIM4、TIM5)可以同时产生4路PWM,每个高级定时器(TIM1、TIM8)可以同时产生多达7路PWM。
举个例子,若当前设置计数器为向上计数,定时器重载值为TIMx_ARR,通道1的捕获/比较寄存器值为TIMx_CCR1。如下图,首先定时器从0开始计数,在0t1时间段,TIMx_CNT<TIMx_CCR1,输出低电平;在t1t2时间段,TIMx_CNT>TIMx_CCR1,输出高电平;
t2时,TIMx_CNT=TIMx_ARR计数器溢出,重新从0开始,如此循环。
由此可以看出,TIMx_ARR决定PWM的周期,TIMx_CCR1决定PWM的占空比,此时占空比计算公式为:
下图为向上计数模式图:
1.3 编码器简述
(1)什么是编码器
编码器是一种将角位移或者角速度转换成一连串电数字脉冲的旋转式传感器,我们可以通过编码器测量到底位移或者速度信息。编码器从输出数据类型上分可以分为增量式编码器和绝对式编码器。
从编码器检测原理上来分,还可以分为光学式,磁式,感应式,电容式。常见的是光电编码器(光学式)和霍尔编码器(磁式)。 光电编码器的采集精度远高于霍尔编码器但价格更贵。
(2)编码器工作原理
霍尔编码器是有霍尔马盘和霍尔元件组成。霍尔马盘是在一定直径的圆板上等分的布置有不同的磁极。霍尔马盘与电动机同轴,电动机旋转时,霍尔元件检测输出若干脉冲信号,为判断转向,一般输出两组存在一定相位差的方波信号。示意图如下:
1.4 PID 算法
(1)PID的一般形式
通过以上框图不难看出,PID控制其实就是对偏差的控制过程;如果偏差为0,则比例环节不起作用,只有存在偏差时,比例环节才起作用;积分环节主要是用来消除静差,所谓静差,就是系统稳定后输出值和设定值之间的差值,积分环节实际上就是偏差累计的过程把累计的误差加到原有系统上以抵消系统造成的静差;而微分信号则反应了偏差信号的变化规律,也可以说是变化趋势,根据偏差信号的变化趋势来进行超前调节,从而增加了系统的预知性;
(2)PID公式
-
连续公式
- Kp——比例增益,Kp与比例度成倒数关系
- Tt——积分时间常数
- TD——一微分时间常数
- u(t)——PID控制器的输出信号
- e(t)——给定值r (t)与测量值误差
-
离散公式与位置式公式
Kp : e(k)
Ki : ∑e(i) 误差的累加
Kd : e(k) - e(k-1) 这次误差-上次误差
离散公式也是位置式PID算法公式,同时也叫全量式PID
- 增量式公式
Kp: e(k)-e(k-1) 这次误差-上次误差
Ki: e(i) 误差
Kd : e(k) - 2e(k-1)+e(k-2) 这次误差-2*上次误差+上上次误差
增量式PID根据公式可以很好地看出,一旦确定了 KP、TI 、TD,只要使用前后三次测量值的偏差, 即可由公式求出控制增量
二、所用工具及模块说明
主控开发板:STM32F103C8T6(百问网的DshanMCU-F103)
电机:亚博智能的310编码减速电机
驱动板:亚博智能AT8236驱动板
STM32CubeMX软件
IDE: Keil5
ST_Link下载器
CH340串口模块
三、接线说明
3.1 ST-Link 接线图
3.2 CH340接线图
3.3 驱动板与电机
驱动板口 电机引脚 | 驱动板口 电机引脚 |
---|---|
AO1–>M1 | BO1–>M1 |
AO2–>M2 | BO2–>M2 |
GAD–>G | GAD–>G |
3V3–>V | 3V3–>V |
E1B–>B | E2B–>B |
E1A–>A | E2A–>A |
3.4 驱动板与主控板
驱动板 主控板 | 驱动板 主控板 |
---|---|
AIN1–>PA8 | E2A–>PB6 |
AIN2–>PB0 | E2B–>PB7 |
BIN1–>PA11 | E1A–>PA0 |
BIN2–>PB1 | E1B–>PA1 |
5V–>5V |
四、CubeMX配置
4.1 配置下载调试接口
进如配置页面首先点击System Core,在选择其下的SYS,最后右边Debug里的Serial Wire,如下图所示:
4.2 配置时钟树
首先同样是System Core下,选则RCC,再选择右边的高速时钟源,最后再选晶振模式,如下图所示:
接下来去时钟配置界面配置系统时钟频率。直接在HCLK时钟那里输入MCU允许的最高时钟频率。F103的最高频率是72Mhz,所以直接在那里输入72然后按回车,如图所示:
回车后CubeMX会自动计算得到各个分频系数和倍频系数,在提示框中点击“OK”,就开始自动配置时钟,配置成功后,结果如下图所示:
4.3 配置串口
点击Connectivity选择USART1,再点击右边Mode选择Asynchronous,如下图所示:
最后在配置参数,设置为115200、数据位8、奇偶校验位None、停止位1,如下图所示:
4.4 配置PWM
关于PWM能使用哪些定时器在上边概念一章中的PWM介绍中已经简单说过啦,这里PWM我选用的是TIM1,用的是互补输出,在TIM1中每个通道我都使用啦,为的是验证这样可不可,事实证明是可以的,大家选择通道不用我这么麻烦,选择其中两个通道就可以实现同样功能
接下来设置Configuration选项中Parameter Settings的参数 ,如下图所示:
只修改上图所画,其余保持默认即可!
4.5 配置编码器模式
开启编码器,点击TIM2,选择编码器模式,如下图所示:
接下再来在配置设置Configuration选项中Parameter Settings的参数,如下图所示:
最后点击NVIC,使能中断,如图所示:
4.6 芯片最终引脚图
五、代码部分
5.1 电机控制部分
moto.c
#include "moto.h"
#include "tim.h"
int32_t PWM_Duty = 0;
void SetMotorSpeed(int16_t Duty)
{
__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,Duty);
}
//启动电机
void SetMotorStart()
{
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_1); // 停止输出
}
//启动电机1输入1正转,输入2反转,输入0停止
void SetMotor1Dir(uint16_t dir)
{
if(dir == 1)
{
__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_1,PWM_Duty);
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_2); // 停止输出
}
else if(dir == 2)
{
__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_2,PWM_Duty);
HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_2); // 开始输出
}
else
{
HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_2); // 停止输出
}
}
//启动电机2输入1正转,输入2反转,输入0停止
void SetMotor2Dir(uint16_t dir)
{
if(dir == 1)
{
__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_4,PWM_Duty);
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_3); // 停止输出
}
else if(dir == 2)
{
__HAL_TIM_SetCompare(&htim1,TIM_CHANNEL_3,PWM_Duty);
HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_4);
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_3); // 开始输出
}
else
{
HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_4);
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_3); // 停止输出
}
}
添加完电机控制部分就可以在main中添加代码启动电机
5.2 验证串口控制电机旋转
在USART.C添加重定义C库函数printf 代码如下:
//重定向c库函数printf
int fputc(int c, FILE *f)
{
(void)f;
HAL_UART_Transmit(&huart1, (const uint8_t *)&c, 1, 10);
return c;
}
//重定向c库函数getchar,scanf
int fgetc(FILE *f)
{
uint8_t ch = 0;
(void)f;
/* Clear the Overrun flag just before receiving the first character */
__HAL_UART_CLEAR_OREFLAG(&huart1);
/* Wait for reception of a character on the USART RX line and echo this
* character on console */
HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/*
* 添加如下代码,则不需要在工程设置中勾选Use MicroLIB
*/
#pragma import(__use_no_semihosting)
struct __FILE
{
int a;
};
FILE __stdout;
FILE __stdin;
void _sys_exit(int x)
{
}
重定向后在main添加如下串口回调函数,即可控制,代码如下:
//测试串口控制
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)
{
if(data==1)
{
HAL_UART_Transmit(&huart1,(uint8_t *)&"电机正转!\r\n",sizeof("电机正转!\r\n"),HAL_MAX_DELAY);
HAL_UART_Receive_IT(&huart1,&data,sizeof(data));
}
else if(data==2)
{
HAL_UART_Transmit(&huart1,(uint8_t *)&"电机反转!\r\n",sizeof("电机反转!\r\n"),HAL_MAX_DELAY);
HAL_UART_Receive_IT(&huart1,&data,sizeof(data));
}
else if(data==0)
{
HAL_UART_Transmit(&huart1,(uint8_t *)&"电机停止!\r\n",sizeof("电机停止!\r\n"),HAL_MAX_DELAY);
HAL_UART_Receive_IT(&huart1,&data,sizeof(data));
}
else
{
HAL_UART_Transmit(&huart1,(uint8_t *)&"error!\r\n",sizeof("error\r\n"),HAL_MAX_DELAY);
HAL_UART_Receive_IT(&huart1,&data,sizeof(data));
}
HAL_UART_Receive_IT(&huart1,(uint8_t*)&data,sizeof(data));//打开下一次串口接收中断
}
}
5.3 编码器部分
//编码器实验
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t i = 0;
i++;
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2))
OverflowCount--; //向下计数溢出
else
OverflowCount++; //向上计数溢出
if(i == 1000)/* 1000ms计算一次 */
{
/* 电机旋转方向 = 计数器计数方向 */
Motor_Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2);
/* 当前时刻总计数值 = 计数器值 + 计数溢出次数 * ENCODER_TIM_PERIOD */
Capture_Count =__HAL_TIM_GET_COUNTER(&htim2) + (OverflowCount * 65535);
/* 转轴转速 = 单位时间内的计数值 / 编码器总分辨率 * 时间系数 */
Speed = (float)(Capture_Count - Last_Count) / 13 * 20 * 4 * 10 ;
printf("电机方向:%d\r\n", Motor_Direction);
printf("单位时间内有效计数值:%d\r\n", Capture_Count - Last_Count);/* 单位时间计数值 = 当前时刻总计数值 - 上一时刻总计数值 */
printf("电机转轴处转速:%.2f 转/秒 \r\n", Speed);
printf("电机输出轴转速:%.2f 转/秒 \r\n", Speed/20);/* 输出轴转速 = 转轴转速 / 减速比 */
/* 记录当前总计数值,供下一时刻计算使用 */
Last_Count = Capture_Count;
/* 数据清零,等待下一秒重新记录数据 */
i = 0;
}
}
5.4 PID部分
pid.c
/* PID结构体 */
PID_TypeDef sPID; // PID参数结构体
PID_TypeDef *ptr = &sPID;
void PID_ParamInit()
{
sPID.LastError = 0; // Error[-1]上一次的误差
sPID.PrevError = 0; // Error[-2]上上次的误差
sPID.Proportion = SPD_P_DATA; // 比例常数 Proportional Const
sPID.Integral = SPD_I_DATA; // 积分常数 Integral Const
sPID.Derivative = SPD_D_DATA; // 微分常数 Derivative Const
sPID.SetPoint = TARGET_SPEED; // 设定目标Desired Value
}
/**
* 函数名称:速度闭环PID控制设计
* 输入参数:当前控制量
* 返 回 值:目标控制量
* 说 明:无
*/
int32_t SpdPIDCalc(float NextPoint)
{
float iError,iIncpid;
iError = (float)sPID.SetPoint - NextPoint; //偏差
/* 给恒定的占空比的时候,0.5r/m 的跳动范围是正常的 */
if((iError<0.5f )&& (iError>-0.5f))
iError = 0.0f;
iIncpid=(sPID.Proportion * iError) //E[k]项
-(sPID.Integral * sPID.LastError) //E[k-1]项
+(sPID.Derivative * sPID.PrevError); //E[k-2]项
sPID.PrevError = sPID.LastError; //存储误差,用于下次计算
sPID.LastError = iError;
return(ROUND_TO_INT32(iIncpid)); //返回增量值
}
pid.h
typedef struct
{
int32_t SetPoint; //设定目标 Desired Value
float SumError; //误差累计
float Proportion; //比例常数 Proportional Const
float Integral; //积分常数
float Derivative; //微分常数
int LastError; //上一次的误差
int PrevError; //上上次的误差
} PID_TypeDef;
/* 四舍五入 */
//将浮点数x四舍五入为int32_t
#define ROUND_TO_INT32(x) ((int32_t)(x)+0.5f)>=(x)? ((int32_t)(x)):((uint32_t)(x)+1)
#define SPD_P_DATA 5.5f // P参数
#define SPD_I_DATA 1.56f // I参数
#define SPD_D_DATA 0.0f // D参数
#define TARGET_SPEED 100.0f // 目标速度 15r/m
extern __IO uint32_t uwTick;
void PID_ParamInit(void) ;
int32_t SpdPIDCalc(float NextPoint);
#endif
5.5 pid速度闭环PID控制系统
滴答定时器中断回调函数
void HAL_SYSTICK_Callback(void)
{
__IO int32_t Spd_Pulse = 0; // 编码器捕获值 Pulse
__IO int32_t Spd_PPS = 0; // 速度值 Pulse/Sample
__IO float Spd_RPM = 0; // 速度值 r/m
/* 速度环周期100ms */
if(uwTick % 100 == 0)
{
Spd_Pulse = (OverflowCount*65535) + (int32_t)__HAL_TIM_GET_COUNTER(&htim2);
Spd_PPS = Spd_Pulse - Last_Count;
Last_Count = Spd_Pulse ;
/* 11线编码器,30减速比,一圈脉冲信号是13*20*4 */
/* 放大10倍计算r/s,放大60则是计算r/m*/
Spd_RPM = (float)(Spd_Pulse - Last_Count) / 13 * 20 * 4 * 10 ;
/* 计算PID结果 */
if(Start_flag == 1)
{
PWM_Duty += SpdPIDCalc(Spd_RPM);
/* 判断当前运动方向 */
if(PWM_Duty < 0)
{
Motor_Dir = 1; //顺时针
{HAL_TIM_PWM_Stop(&htim1,TIM_CHANNEL_1);\
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);}
/* 限制占空比 */
if(PWM_Duty < -(4199-100))
PWM_Duty = -(4199-100);
/* 直接修改占空比 */
SetMotorSpeed(-PWM_Duty);
}
else
{
Motor_Dir = -1;
{HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);\
HAL_TIMEx_PWMN_Stop(&htim1,TIM_CHANNEL_1);}
if(PWM_Duty > (4199-100))
PWM_Duty = (4199-100);
SetMotorSpeed(PWM_Duty);
}
}
}
}