it编程 > 硬件开发 > stm32

第八章-PID 速度控制 PID控制 PID调参 PID温度控制 蓝桥杯 单片机 串级PID 模糊PID STM32f103c8t6最小系统板 STM32项目 STM32cubemx正点原子 江科大协

74人参与 2024-08-06 stm32

第八章-pid-速度控制

8.1-速度控制探索

前面我们已经能够通过编码器测量出速度值,下面我们来控制速度

我们先编写一个简单的控制方法

要求:讲转速控制再2.9-3.1转每秒

可以把中断里面不重要的输出注释掉
在这里插入图片描述

	if(motor1speed>3.1) motor1pwm--;
	if(motor1speed<2.9) motor1pwm++;
	if(motor2speed>3.1) motor2pwm--;
	if(motor2speed<2.9) motor2pwm++;
	motor_set(motor1pwm,motor2pwm);
	printf("motor1speed:%.2f motor1pwm:%d\r\n",motor1speed,motor1pwm);
	printf("motor2speed:%.2f motor2pwm:%d\r\n",motor2speed,motor2pwm);
	
	hal_delay(100);

开始实验
在这里插入图片描述
现象就开始电机没有到达3转每秒,pwm占空比逐渐增大,电机逐渐达到要求转速、到达要求转速后我们增加阻力,电机变慢,阻力大小不边pwm占空比逐渐更大转速逐渐更大

这样我们就把转速控制到我们想要的范围,但是我们并不满意、能够看出来控制的速度很慢,给电机一些阻力电机至少要2-3秒能够调整过来,这在一些场景是不允许的。

我们理想的控制效果是:在电机转速很慢的是时候能快速调整,在电机一直转的不能达到要求时候能够更快速度调整

8.2-准备工作-匿名上位机曲线显示速度波形方便观察数据

为了方便观察电机速度数据,我们通过上位机曲线显示一下。

这里我们使用的上位机是匿名上位机-大佬写的非常稳定功能也很多

我使用的版本是:匿名上位机v7.2.2.8版本推荐大家和我使用一样
匿名上位机官方下载链接:https://www.anotc.com/wiki/%e5%8c%bf%e5%90%8d%e4%ba%a7%e5%93%81%e8%b5%84%e6%96%99/%e8%b5%84%e6%96%99%e4%b8%8b%e8%bd%bd%e9%93%be%e6%8e%a5%e6%b1%87%e6%80%bb
在这里插入图片描述
我们要把stm32数据发送到匿名上位机,就要满足匿名上位机的数据协议要求

在匿名上位机资料下载链接,可以下载到协议介绍

0xaa:一个字节表示开始

0xff:一个字节表示目标地址

0xf1:一个字节表示发送功能码

1-40:一个字节表示数据长度

数据内容有多个字节如何发送

因为串口每次发送一个字节,但是数据可能是int16_t 16位的数据,或者int32_t 32位数据,每次发送16位数据,先发送数据低八位,还是先发送数据高八位那?

匿名协议通信介绍给出:data 数据内容中的数据,采用小端模式传送,低字节在前,高字节在后。

那么就要求,比如我们在发送16位数据0x2314我们要先发送低字节0x14,然后发送高字节0x23

那么如何解析出低字节或者高字节,就需要知道多字节数据在单片机里面是怎么存的,因为stm32是小端存储,所以低字节就在低位地址中,高字节高位地址中。

如果使用32单片机 小端模式,0x23高地址,0x14在低地址,所以我们要先发低地址,再发高地址。

下面就是对16位数据,或者32位数据的拆分

//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwtemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define byte0(dwtemp)  (*(char *)(&dwtemp))
#define byte1(dwtemp)  (*((char *)(&dwtemp) + 1))
#define byte2(dwtemp)  (*((char *)(&dwtemp) + 2))
#define byte3(dwtemp)  (*((char *)(&dwtemp) + 3))

拆分后我们按照协议要求发送数据就可以了
在这里插入图片描述
niming.c

#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];

//通过f1帧发送4个uint16类型的数据
void ano_dt_send_f1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
    uint8_t _cnt = 0;		//计数值
    uint8_t sumcheck = 0;  //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i = 0;
	data_to_send[_cnt++] = 0xaa;//帧头
    data_to_send[_cnt++] = 0xff;//目标地址
    data_to_send[_cnt++] = 0xf1;//功能码
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
	data_to_send[_cnt++] = byte0(_a);       
    data_to_send[_cnt++] = byte1(_a);
	
    data_to_send[_cnt++] = byte0(_b);
    data_to_send[_cnt++] = byte1(_b);
	
    data_to_send[_cnt++] = byte0(_c);
    data_to_send[_cnt++] = byte1(_c);
	
    data_to_send[_cnt++] = byte0(_d);
    data_to_send[_cnt++] = byte1(_d);
	 for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];//和校验
        addcheck += sumcheck;//附加校验
    }
    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}
//,通过f2帧发送4个int16类型的数据
void ano_dt_send_f2(int16_t _a, int16_t _b, int16_t _c, int16_t _d)   //f2帧  4个  int16 参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
   data_to_send[_cnt++] = 0xaa;
    data_to_send[_cnt++] = 0xff;
    data_to_send[_cnt++] = 0xf2;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = byte0(_a);
    data_to_send[_cnt++] = byte1(_a);
	
    data_to_send[_cnt++] = byte0(_b);
    data_to_send[_cnt++] = byte1(_b);
	
    data_to_send[_cnt++] = byte0(_c);
    data_to_send[_cnt++] = byte1(_c);
	
    data_to_send[_cnt++] = byte0(_d);
    data_to_send[_cnt++] = byte1(_d);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
	
	hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}
//通过f3帧发送2个int16类型和1个int32类型的数据
void ano_dt_send_f3(int16_t _a, int16_t _b, int32_t _c )   //f3帧  2个  int16 参数   1个  int32  参数
{
    uint8_t _cnt = 0;
    uint8_t sumcheck = 0; //和校验
    uint8_t addcheck = 0; //附加和校验
    uint8_t i=0;
    data_to_send[_cnt++] = 0xaa;
    data_to_send[_cnt++] = 0xff;
    data_to_send[_cnt++] = 0xf3;
    data_to_send[_cnt++] = 8; //数据长度
	//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
    data_to_send[_cnt++] = byte0(_a);
    data_to_send[_cnt++] = byte1(_a);
	
    data_to_send[_cnt++] = byte0(_b);
    data_to_send[_cnt++] = byte1(_b);
	
    data_to_send[_cnt++] = byte0(_c);
    data_to_send[_cnt++] = byte1(_c);
    data_to_send[_cnt++] = byte2(_c);
    data_to_send[_cnt++] = byte3(_c);
	
	  for ( i = 0; i < data_to_send[3]+4; i++)
    {
        sumcheck += data_to_send[i];
        addcheck += sumcheck;
    }

    data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;

	hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}

niming.h

#ifndef  niming_h
#define  niming_h
#include "main.h"
//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwtemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define byte0(dwtemp)  (*(char *)(&dwtemp))
#define byte1(dwtemp)  (*((char *)(&dwtemp) + 1))
#define byte2(dwtemp)  (*((char *)(&dwtemp) + 2))
#define byte3(dwtemp)  (*((char *)(&dwtemp) + 3))


void ano_dt_send_f1(uint16_t, uint16_t _b, uint16_t _c, uint16_t _d);
void ano_dt_send_f2(int16_t _a, int16_t _b, int16_t _c, int16_t _d);
void ano_dt_send_f3(int16_t _a, int16_t _b, int32_t _c );

#endif 

添加测试代码
在这里插入图片描述

	//电机速度等信息发送到上位机
	//注意上位机不支持浮点数,所以要乘100
	ano_dt_send_f2(motor1speed*100, 3.0*100,motor2speed*100,3.0*100);

下面设置上位机-数据解析
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
这个是控制效果,并不理想,后面我们介绍pid控制
在这里插入图片描述

8.3-p i d 逐个参数理解

在这里插入图片描述
加入的现在 过去 未来概念

p:现在

i:过去

d:未来
在这里插入图片描述
那么我们就开始写pid

pid的结构体类型变量、里面成员都是浮点类型

先在pid.h声明一个结构体类型、声明.c中的函数
在这里插入图片描述

#ifndef __pid_h
#define __pid_h

//声明一个结构体类型
typedef struct 
{
	float target_val;//目标值
	float actual_val;//实际值
	float err;//当前偏差
	float err_last;//上次偏差
	float err_sum;//误差累计值
	float kp,ki,kd;//比例,积分,微分系数
	
} tpid;

//声明函数
float p_realize(tpid * pid,float actual_val);
void pid_init(void);
float pi_realize(tpid * pid,float actual_val);
float pid_realize(tpid * pid,float actual_val);
#endif

然后在pid.c中定义结构体类型变量
在这里插入图片描述

#include "pid.h"

//定义一个结构体类型变量
tpid pidmotor1speed;
//给结构体类型变量赋初值
void pid_init()
{
	pidmotor1speed.actual_val=0.0;
	pidmotor1speed.target_val=0.00;
	pidmotor1speed.err=0.0;
	pidmotor1speed.err_last=0.0;
	pidmotor1speed.err_sum=0.0;
	pidmotor1speed.kp=0;
	pidmotor1speed.ki=0;
	pidmotor1speed.kd=0;
}
//比例p调节控制函数
float p_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	//比例控制调节   输出=kp*当前误差
	pid->actual_val = pid->kp*pid->err;
	return pid->actual_val;
}
//比例p 积分i 控制函数
float pi_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用pi控制 输出=kp*当前误差+ki*误差累计值
	pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum;
	
	return pid->actual_val;
}
// pid控制函数
float pid_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
	pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
	//使用pid控制 输出 = kp*当前误差  +  ki*误差累计值 + kd*(当前误差-上次误差)
	pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum + pid->kd*(pid->err - pid->err_last);
	//保存上次误差: 这次误差赋值给上次误差
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

然后在main中要调用pid_init();函数
在这里插入图片描述

  pid_init();

p调节函数函数只根据当前误差进行控制

//比例p调节控制函数
float p_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	//比例控制调节
	pid->actual_val = pid->kp*pid->err;
	return pid->actual_val;
}

主函数-可以估算当p=10 就有较好的响应速度

先看根据p比例控制的效果
在这里插入图片描述
p调节 电机稳态后还是存在误差。

下面加入i 调节也就是加入历史误差
pi的控制函数

//比例p 积分i 控制函数
float pi_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用pi控制
	pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum;
	
	return pid->actual_val;
}

因为实际值1.6的时候误差为1.4 上次偏差1.4和这次偏差1.4相加2.8 我们乘5 等于10点多就会有较好控制效果
这是pi 调节的控制效果
在这里插入图片描述
下面是pid调节的

// pid控制函数
float pid_realize(tpid * pid,float actual_val)
{
	pid->actual_val = actual_val;//传递真实值
	pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
	pid->err_sum += pid->err;//误差累计求和
	//使用pid控制
	pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum + pid->kd*(pid->err - pid->err_last);
	//保存上次误差:最近一次 赋值给上次
	pid->err_last = pid->err;
	
	return pid->actual_val;
}

8.4-加入cjson方便上位机调参

在这里插入图片描述
调大堆栈
在这里插入图片描述
软件开启中断
在这里插入图片描述
开启接收中断
在这里插入图片描述

 __hal_uart_enable_it(&huart1,uart_it_rxne);	//开启串口1接收中断

中断回调函数在这里插入图片描述

uint8_t usart1_readbuf[256];	//串口1 缓冲数组
uint8_t usart1_readcount = 0;	//串口1 接收字节计数
  if(__hal_uart_get_flag(&huart1,uart_flag_rxne))//判断huart1 是否读到字节
  {
		if(usart1_readcount >= 255) usart1_readcount = 0;
		hal_uart_receive(&huart1,&usart1_readbuf[usart1_readcount++],1,1000);
  }

编写函数用于判断串口是否发送完一帧数据
在这里插入图片描述

extern uint8_t usart1_readbuf[255];	//串口1 缓冲数组
extern uint8_t usart1_readcount;	//串口1 接收字节计数

//判断否接收完一帧数据
uint8_t usart_waitreasfinish(void)
{
	static uint16_t usart_lastreadcount = 0;//记录上次的计数值
	if(usart1_readcount == 0)
	{
		usart_lastreadcount = 0;
		return 1;//表示没有在接收数据
	}
	if(usart1_readcount == usart_lastreadcount)//如果这次计数值等于上次计数值
	{
		usart1_readcount = 0;
		usart_lastreadcount = 0;
		return 0;//已经接收完成了
	}
	usart_lastreadcount = usart1_readcount;
	return 2;//表示正在接受中
}

然后我们把cjson库放入工程里面

下载cjson新版

gtihub链接:https://github.com/davegamble/cjson

百度网盘链接:https://pan.baidu.com/s/1acnhtzuv5bokmq2f6qog7q

提取码:a422

和添加其他文件一样,加入工程,然后指定路径

编写解析指令的函数

在这里插入图片描述

#include "cjson.h"
#include <string.h>
   cjson *cjsondata ,*cjsonvlaue;

	if(usart_waitreasfinish() == 0)//是否接收完毕
	{
		cjsondata  = cjson_parse((const char *)usart1_readbuf);
		if(cjson_getobjectitem(cjsondata,"p") !=null)
		{
			cjsonvlaue = cjson_getobjectitem(cjsondata,"p");	
		    p = cjsonvlaue->valuedouble;
			pidmotor1speed.kp = p;
		}
		if(cjson_getobjectitem(cjsondata,"i") !=null)
		{
			cjsonvlaue = cjson_getobjectitem(cjsondata,"i");	
		    i = cjsonvlaue->valuedouble;
			pidmotor1speed.ki = i;
		}
		if(cjson_getobjectitem(cjsondata,"d") !=null)
		{
			cjsonvlaue = cjson_getobjectitem(cjsondata,"d");	
		    d = cjsonvlaue->valuedouble;
			pidmotor1speed.kd = d;
		}
		if(cjson_getobjectitem(cjsondata,"a") !=null)
		{
		
			cjsonvlaue = cjson_getobjectitem(cjsondata,"a");	
		    a = cjsonvlaue->valuedouble;
			pidmotor1speed.target_val =a;
		}
		if(cjsondata != null){
		  cjson_delete(cjsondata);//释放空间、但是不能删除cjsonvlaue不然会 出现异常错误
		}
		memset(usart1_readbuf,0,255);//清空接收buf,注意这里不能使用strlen	
	}
	printf("p:%.3f  i:%.3f  d:%.3f a:%.3f\r\n",p,i,d,a);

测试发送cjson数据就会解析收到数据
在这里插入图片描述
然后我们赋值改变一个电机的pid参数和目标转速
然后我们通过串口发送命令,就会改变pid的参数了
这么我们的第八章就弄好了,下篇我们进行第九章-pid整定

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

lwip:使用lwip UDP分包发送大量数据所遇问题以及解决方法(HPM6750、STM32)

08-06

英飞凌最新MCU之TC4XX系列量产介绍2024

08-06

【STM32-矩阵键盘】

08-06

STM32CubeMX使用说明

08-06

【STM32】GPIO口以及EXTI外部中断

08-06

J-Link:STM32使用J-LINK烧录程序,其他MCU也通用

08-06

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论