学习STM32有一段时间了,完整的做了一套系统,记录一下。
一、目标功能:1、STM32 输出PWM控制直流电机转速。
2、采用增量式PID控制方法调节电机转速。
3、电机的转速值 和转速曲线实时显示在TFTLCD屏幕上。
4、电机的转速值 和PWM值每0.2s从串口输出。
5、通过串口输入,可以随时写入所需要达到的转速以及调整PID参数值。
二、系统组成:
1、STM32 ZET6 正点原子战舰版(带有TFTLCD屏幕)
2、L298N 直流电机驱动器
3、1000线 AB相单片机用编码器
4、12V直流电机
5、12V直流电源
6、杜邦线及导线若干
三、开发环境: KEIL-MDK ARM 软件
STM32 CUBE MX软件
四、接线说明:1、PWM 采用STM32 TIM2 CH1输出 ,输出频率10K HZ。
2、STM32 TIM2 CH1接入L298N 的BLA端,输出PWM到电机控制器。
2、编码器AB相 输入接入单片机TIM3 采用编码器模式,编码器AB相输入接入TIM3 CHI1 及CH2。
3、单片机PE2及PE3接入L298N 的IN1 及IN2。作为电机正反转控制信号。
4、电机接入L298N 的out1 及out2.
5、直流电源正极接入L298N +12V ,负极接入L298N GND并与单片机GND连接。
五、MX CUBE部分配置:
1、FSMC配置(PB0为背光接口,注意不要遗漏配置)
2、TFTLCD驱动程序
驱动程序借用了正点原子HAL库示例程序中的LCD.c及LCD.h文件。删除其中关于FSMC的配置程序行。
3、TIM2配置(产生PWM波,频率10KHz)
4、TIM3配置(编码器模式),用于编码器测速。
5、TIM6配置(定时0.2s) ,定时器中断用于每0.2s 计算转速值,输入PWM值。
6、NVIC优先级配置:这里建议将串口优先级最高。实际运行中发现,如果串口中断优先级不是最高,偶尔会出现输入的转速及PID值不正确的现象。
7、GPIO及串口配置:GPIO 口使用PE2 及PE3 用于控制电机正反转。串口配置略。
六、部分程序说明
1、LCD屏幕上速度网格绘制程序
2、增量式PID函数
typedef struct
{
float ActualSpeed ; //当前值
float error; //偏差
float lasterror; //前一次偏差
float last_lasterror; //前两次偏差
int_least16_t PWM; //输出PWM(有符号型)
int_least16_t PWM_ADD; //增量PWM(有符号型)
}PID;
/* 增量式PID 算法 */
int PID_SET(uint16_t actual_speed,uint16_t set_speed)
{
pid.error = set_speed - actual_speed; //设定值减当前值,计算本次偏差
pid.PWM_ADD=KP*(pid.error-pid.lasterror)+KI*pid.error+KD*(pid.error-2.0f*pid.lasterror+pid.last_lasterror);
pid.PWM+=pid.PWM_ADD;
pid.last_lasterror = pid.lasterror; //将此次偏差保留为上上次偏差
pid.lasterror = pid.error; //将此次偏差保留为上次偏差
if(pid.PWM > 7200) pid.PWM = 7200; //限副 PWM100% 为7200
if(pid.PWM < -7200) pid.PWM = -7200;
return pid.PWM;
}
/* 增量式PID 参数初始化 */
void PID_init(void)
{
pid.lasterror=0;
pid.last_lasterror = 0;
pid.PWM = 0;
}
3、速度值转换函数:用于将得到的速度值存入数组,在LCD屏幕上显示。
/* 速度值转化函数 */
//速度值分解千、百、十、个位后转成16进制ASC码 最后加上单位r/min
void speed_change(short motorspeed)
{
if(motorspeed<0) motorspeed=(-motorspeed); //反转转速为负数,转换成正数
motornum[0]= (motorspeed/1000)+0x30;
motornum[1] = (motorspeed%1000/100)+0x30;
motornum[2] = (motorspeed%1000%100/10)+0x30;
motornum[3] = (motorspeed%1000%100%10)+0x30;
motornum[4]=0x72;
motornum[5]=0x2F;
motornum[6]=0x6d;
motornum[7]=0x69;
motornum[8]=0x6e;
}
4、串口取PID值函数:用于将从串口取到的PID值取出来。PID值可以为小数。
/* 串口输入取PID参数数据函数 */
double Get_PIDDate(void)
{
double date=0;
double dat[Uart1_Rx_Cnt];
uint8_t i,j,k,n;
for(i=1;i<Uart1_Rx_Cnt;i++) //判断小数点的位置
{
if(RxBuffer[i]=='.')
{
n=i; //记录小数点位置
break;
}
else n= Uart1_Rx_Cnt-2; //如果没有输入小数点、则n= 数组位数减去PID开头 以及0x0a 0x0d
}
for(j=1;j<n;j++) //小数点前的数求和
{
dat[j]=(RxBuffer[j]-'0')*pow(10,n-j-1);
date+=dat[j];
}
for(k=n+1;k<Uart1_Rx_Cnt-2;k++) //加小数点后的数,如果没有小数点此函数不运行
{
dat[k]=(RxBuffer[k]-'0')*pow(10,n-k);
date+=dat[k];
}
return date;
}
5、串口取速度函数
/* 串口输入取速度数据函数 */
int Get_SpeedDate(void)
{
uint16_t date=0;
uint16_t dat[5];
uint8_t i;
for(i=1;i<Uart1_Rx_Cnt-2;i++) //位数减去S开头 以及0x0a 0x0d
{
dat[i]=(RxBuffer[i]-'0')*pow(10,Uart1_Rx_Cnt-3-i); //pow为几次方数学函数,将串口输入的数转化成十进制(-48为转化成十进制)存入数组dat
date+=dat[i]; //各位累加
}
return date;
}
6、串口中断回调函数:写了一个小协议,S开头输入代表设定所需转速,p开头输入代表设定PID的KP值,i开头输入代表设定PID的KI值,d开头输入代表设定PID的KD值。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/*此函数有如下BUG: 当发送超过数量超过267,触发数据溢出后仍然能够接收到超出部分的数据并发送出去*/
if(Uart1_Rx_Cnt>=255)
{
Uart1_Rx_Cnt=0;
memset(RxBuffer,0x00,sizeof(RxBuffer)); //将数组RxBuffer所有位清零
HAL_UART_Transmit(&huart1,(uint8_t*)"数据溢出",10,0xffff);
}
else
{
RxBuffer[Uart1_Rx_Cnt++]=aRxBuffer; //将接收到的数据依次放入数组
if((RxBuffer[Uart1_Rx_Cnt-1]==0x0a)&&(RxBuffer[Uart1_Rx_Cnt-2]==0x0d)) //判断已接收完成
{
switch (RxBuffer[0])
{
case 0x73: //s开头输入的为速度值
Set_Speed=Get_SpeedDate(); //从串口接收数组中取出设定速度值
printf("设定的速度值SetSpeed=%d\r\n",Set_Speed);
HAL_UART_Transmit(&huart1,(uint8_t *)&RxBuffer,Uart1_Rx_Cnt,0xffff); //将接收到的数据发送出去
while(HAL_UART_GetState(&huart1)==HAL_UART_STATE_BUSY_TX); //检测已发送完成
Uart1_Rx_Cnt=0; //接收数清零
memset(RxBuffer,0x00,sizeof(RxBuffer)); //接收数组清零
break;
case 0x70 : // p开头输入的为KP值
KP=Get_PIDDate(); //从串口接收数组中取出设定P值
printf("设定的P值KP=%f\r\n",KP);
HAL_UART_Transmit(&huart1,(uint8_t *)&RxBuffer,Uart1_Rx_Cnt,0xffff); //将接收到的数据发送出去
while(HAL_UART_GetState(&huart1)==HAL_UART_STATE_BUSY_TX); //检测已发送完成
Uart1_Rx_Cnt=0; //接收数清零
memset(RxBuffer,0x00,sizeof(RxBuffer)); //接收数组清零
break;
case 0x69: //i 开头的为KI值
KI=Get_PIDDate(); //从串口接收数组中取出设定P值
printf("设定的I值KI=%f\r\n",KI);
HAL_UART_Transmit(&huart1,(uint8_t *)&RxBuffer,Uart1_Rx_Cnt,0xffff); //将接收到的数据发送出去
while(HAL_UART_GetState(&huart1)==HAL_UART_STATE_BUSY_TX); //检测已发送完成
Uart1_Rx_Cnt=0; //接收数清零
memset(RxBuffer,0x00,sizeof(RxBuffer)); //接收数组清零
break;
case 0x64: // d开头的为KI值
KD=Get_PIDDate(); //从串口接收数组中取出设定P值
printf("设定的D值KD=%f\r\n",KD);
HAL_UART_Transmit(&huart1,(uint8_t *)&RxBuffer,Uart1_Rx_Cnt,0xffff); //将接收到的数据发送出去
while(HAL_UART_GetState(&huart1)==HAL_UART_STATE_BUSY_TX); //检测已发送完成
Uart1_Rx_Cnt=0; //接收数清零
memset(RxBuffer,0x00,sizeof(RxBuffer)); //接收数组清零
break;
default: printf("输入的参数有误,请重新输入!"); break;
}
}
}
HAL_UART_Receive_IT(&huart1,(uint8_t *)&aRxBuffer,1); //接收一次数据后再次开启接收中断
}
7、定时器中断回调函数:实现屏幕显示速度值及曲线,输出PWM等功能。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == (&htim6))
{
motorspeed = (short)__HAL_TIM_GET_COUNTER(&htim3)*0.075; //200ms内脉冲数转换成r/min(1000编码器。0.2秒收到的脉冲,编码器模式4倍频)
//先将__HAL_TIM_GET_COUNTER(&htim3)的计数值强制转换为short(-32768到32767)型,假如反转,转速从65535开始向下计数
// 转换成short后为-1,-2,-3...
__HAL_TIM_SET_COUNTER(&htim3,0); //编码器计数器清零
LCD_ShowString(10,20,210,16,16,"motorspeed="); //显示motorspeed=
speed_change(motorspeed); //速度值写入数组
LCD_ShowString(100,20,210,16,16,motornum); //将数组内的速度值显示出来
pwmVal = PID_SET(abs(motorspeed),Set_Speed); //20ms中断,执行PWM_SET函数 返回的PWM值给pwmVal
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_1,pwmVal); //将pwmVal值传递给TIM2通道的PWM输出
printf("PWMval=%d\r\n",pwmVal); //串口输出PWM值
printf("motorspeed=%d r/min\r\n",motorspeed); //串口输出电机速度值
LCD_DrawPoint(ntime++,motorspeed/Speed_n()); //屏幕显示速度点,最高4000r/min, 每一行代表4000/800=5r/min。
if (ntime >= 479) //列满后刷新屏幕
{
ntime=1;
LCD_Clear(WHITE);
LCD_SpeedDrawLine();
}
}
}
8、其它:略。
七、效果视频
STM32直流电机PID调速