前言
本次板球系统作为2024年电赛做准备的练习项目,选用的是与比赛原题尺寸60*60的亚克力板,我主要负责32代码级调参,先是采用了单环位置环,后面选择内环速度环,外环位置环,项目最终没能调参成功,但对pid也有所了解,特意此篇作为项目总结及反思。
一、板球系统解题思路
- 根据题目要求,采用OLED显示屏,独立按键,opencv,蓝牙以及舵机等外设,用蓝牙选取小球的目标位置。摄像头用于图像采集,将九个点的数据以及小球坐标发送给单片机,单片机通过单片机计算小球当前位置和目标位置的距离,利用PID算法进行控制,使小球朝着要求的目标位置运动。
二、电路设计思路
电路原理图如下:
三、代码实现过程
单片机接收摄像头数据
opencv以每秒三十帧的速度向32传输数据,开机识别9个点的坐标传输给32(只传输一次),后面实时传输小球数据,32用二维数组储存数据
#include "Serial.h"
#include "oled.h"
#include "stdio.h"
char CX=0,Xb=0,Xs=0,Xg=0,CY=0,Yb=0,Ys=0,Yg=0;
int BB;
#define MAX_VALUE 3//定义一维数组最大行数//球/点,x,y
#define MAX_row_VALUE 10 //定义二维数组最大行数
#define MAX_col_VALUE 3 //定义二维数组最大列数
//定义一个一维数组用于储存串口处理完的数据
int count_x_y[MAX_VALUE]={0};
//count_x_y[0]:小球为数字0 1-9区域为黑色圆
//count_x_y[1]:x坐标范围在0-640之间
//count_x_y[2]:y坐标范围在0-480之间
//定义一个二维数组用于储存二维坐标并且初始化所有位置为0
int coor_count_x_y[MAX_row_VALUE][MAX_col_VALUE] = {0};
//此处省略
void USART1_Init(void)
//USART1 全局中断服务函数
void USART1_IRQHandler(void)
{
char com_data;
u8 i;
// u8 X,Y;
static u8 RxCounter1=0;
static u16 RxBuffer1[16]={0};
static u8 RxState = 0;
static u8 RxFlag1 = 0;
if( USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) //接收中断
{
USART_ClearITPendingBit(USART1,USART_IT_RXNE); //清除中断标志
com_data = USART_ReceiveData(USART1);
USART_SendData(USART1,com_data);
// OLED_ShowChar(3,3,data);
// char com_data = (char)data;
if(RxState==0&&com_data==0x53) //0x2c帧头
{
RxState=1;
RxBuffer1[RxCounter1++]=com_data;
}
else if(RxState==1&&com_data==0x42) //0x12帧头
{
RxState=2;
RxBuffer1[RxCounter1++]=com_data;
}
else if(RxState==2)
{
RxBuffer1[RxCounter1++]=com_data;
if(RxCounter1>=12||com_data == 0x43) //RxBuffer1接受满了,接收数据结束
{
RxState=3;
RxFlag1=1;
BB=RxBuffer1[RxCounter1-10];
CX=RxBuffer1[RxCounter1-9];
Xb=RxBuffer1[RxCounter1-8];
Xs=RxBuffer1[RxCounter1-7];
Xg=RxBuffer1[RxCounter1-6];
CY=RxBuffer1[RxCounter1-5];
Yb=RxBuffer1[RxCounter1-4];
Ys=RxBuffer1[RxCounter1-3];
Yg=RxBuffer1[RxCounter1-2];
int Bb = BB - '0';
int xb = Xb - '0';
int xs = Xs - '0';
int xg = Xg - '0';
int yb = Yb - '0';
int ys = Ys - '0';
int yg = Yg - '0';
int X = xb * 100 + xs * 10 + xg;
int Y = yb * 100 + ys * 10 + yg;
count_x_y[0] = Bb;
count_x_y[1] = X;
count_x_y[2] = Y;
//条件判断如果count_x_y[0]等于0~8之间的数
if(count_x_y[0]>=0&&count_x_y[0]<=MAX_row_VALUE)
{
coor_count_x_y[count_x_y[0]][0] = count_x_y[0];//将count_x_y[0]给到coor_count_x_y的对应位置球/点的坐标上
coor_count_x_y[count_x_y[0]][1] = count_x_y[1];//将count_x_y[0]给到coor_count_x_y的对应位置 的x坐标上
coor_count_x_y[count_x_y[0]][2] = count_x_y[2]; //将count_x_y[0]给到coor_count_x_y的对应位置 的y坐标上
}
}
}
else if(RxState==3) //检测是否接受到结束标志
{
if(RxBuffer1[RxCounter1-1] == 0x43)
{
USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);//关闭DTSABLE中断
if(RxFlag1)
{
}
RxFlag1 = 0;
RxCounter1 = 0;
RxState = 0;
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
}
else //接收错误
{
RxState = 0;
RxCounter1=0;
for(i=0;i<12;i++)
{
RxBuffer1[i]=0x00; //将存放数据数组清零
}
}
}
else //接收异常
{
RxState = 0;
RxCounter1=0;
for(i=0;i<12;i++)
{
RxBuffer1[i]=0x00; //将存放数据数组清零
}
}
}
}
//2C 12 31 58 31 32 33 59 33 32 31 69 5B
/*************************************
*函 数 名 Get_x()
*函数功能 :输出x轴的值
*输 入 :无
*输 出 :x轴坐标
*************************************/
int Get_x(void)
{
return coor_count_x_y[0][1];
}
/*************************************
*函 数 名 Get_y()
*函数功能 :输出y轴的值
*输 入 :无
*输 出 :y轴坐标
*************************************/
int Get_y(void)
{
return coor_count_x_y[0][2];
// coor_count_x_y[count_x_y[0]][2];
}
按键对舵机进行调平
KeyNum = Key_GetNum();
if(KeyNum == 1)
{
servo_x += 50;
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
}
if(KeyNum == 2)
{
servo_x -= 50;
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
}
if(KeyNum == 3)
{
servo_y += 50;
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
}
else if(KeyNum == 4)
{
servo_y -= 50;
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
}
蓝牙选取目标值
void USART3_IRQHandler(void)
{
if (USART_GetITStatus(USART3, USART_IT_RXNE) == SET)
{
data = USART_ReceiveData(USART3);
Serial_RxFlag = 1;
USART_SendData(USART3,data);
// 假设data的值在1到9之间,且count_x_y数组足够大且已正确初始化
if (data >= '1' && data <= '9')
{
data_int=data-'0';
// 直接使用data来访问coor_count_x_y数组,避免额外的count_x_y数组查找
target_x = coor_count_x_y[data_int][1];
target_y = coor_count_x_y[data_int][2];
// OLED_ShowNum(3, 7,123,3);
}
else
{
// 处理data不在1到9之间的情况,可以设置为默认值或进行错误处理
target_x = coor_count_x_y[5][1]; // 定义DEFAULT_X_VALUE为合适的默认值
target_y = coor_count_x_y[5][2]; // 定义DEFAULT_Y_VALUE为合适的默认值
}
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
}
主函数代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "Serial.h"
#include "PID.h"
#include "PWM.h"
#include "usart3.h"
#include "mpu6050.h"
#define MAX_VALUE 3//定义一维数组最大行数
#define MAX_row_VALUE 10 //定义二维数组最大行数
#define MAX_col_VALUE 3 //定义二维数组最大列数
extern int count_x_y[MAX_VALUE];
extern int coor_count_x_y[MAX_row_VALUE][MAX_col_VALUE];
extern char data;
extern char CX,Xb,Xs,Xg,CY,Yb,Ys,Yg;
extern int BB;
float ax,ay,az;
float pitch,roll,yaw;
uint8_t KeyNum;
//float Angle_x = 60.0, Angle_y = 60.0;
int x,y;
extern int target_x,target_y;
int ballx,bally;
//int target_x=236,target_y=203; //当前坐标
float servo_x = 1570.0; //yellow 加大下降 //舵机目标值
float servo_y = 1560.0;//
float X_Vel_Target, X_Vel_PWM; //X轴速度环的PWM
float X_Pos_Target, X_Pos_PWM; //X轴位置环
float Y_Vel_Target, Y_Vel_PWM; //Y轴速度环的PWM
float Y_Pos_Target, Y_Pos_PWM; //Y轴位置环
float X_Vel_Kp = 4, X_Vel_Ki = 0, X_Vel_Kd = 0;
float X_Pos_Kp = 0, X_Pos_Ki = 0, X_Pos_Kd = 0; //X轴位置环
float Y_Vel_Kp =4, Y_Vel_Ki = 0, Y_Vel_Kd = 0; //Y轴速度环
float Y_Pos_Kp = 0, Y_Pos_Ki = 0, Y_Pos_Kd = 0; //Y轴位置环
int previousValuex = 0;
int receivedValuex =0;
int previousValuey = 0;
int receivedValuey =0;
int main(void)
{
float X_PWM,Y_PWM;
HSE_sysclock_config();
OLED_Init();
Key_Init();
Serial_Init();
PWM_Init();
USART1_Init();
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
// Servo_SetAngle_x(Angle_x);
// Servo_SetAngle_y(Angle_y);
OLED_ShowString(1, 1, "Angle:");
while (1)
{
OLED_ShowChar(1, 1,data);
/*串口获取x轴与y轴坐标*/
x = Get_x();
y = Get_y();
// /*OLED显示当前xy轴坐标*/
OLED_ShowNum(2, 1,x,3);
OLED_ShowNum(2, 7,y,3);
OLED_ShowNum(1, 3,target_x,3);
OLED_ShowNum(1, 8,target_y,3);
// OLED_ShowSignedNum(3, 7,ballx,3);
// OLED_ShowSignedNum(4, 7,bally,3);
// 如果坐标不等于(000, 000)
if (x != 000 )
{
previousValuex = receivedValuex;
receivedValuex = x;
previousValuey = receivedValuey;
receivedValuey = y;
ballx = receivedValuex - previousValuex;
bally = receivedValuey - previousValuey;
X_Pos_PWM=0,Y_Pos_PWM=0;
//速度环闭环控制,位置环的输出为速度环的输入,形成串级PID
// X_Pos_PWM = PID_position_x(target_x, x); //位置环--外环
X_Vel_PWM = PID_velocity_x(ballx,X_Pos_PWM)/2; //速度环--内环
// Y_Pos_PWM = PID_position_y(target_y, y); //外环
Y_Vel_PWM = PID_velocity_y(bally,Y_Pos_PWM)/2; //内环
X_Vel_PWM=constrain_float(X_Vel_PWM, -200, 200); //1550
Y_Vel_PWM=constrain_float(Y_Vel_PWM, -200, 200);//1560
X_PWM = X_Vel_PWM+ servo_x; //X轴舵机的PWM+机械中值
Y_PWM = Y_Vel_PWM+ servo_y;
/*限制pwm大小*/
// X_PWM=constrain_float(X_PWM, 1450, 1650); //1550
// Y_PWM=constrain_float(Y_PWM, 1460, 1660);//1560
//
/*输出xy舵机的驱动值*/
PWM_SetCompare_x(X_PWM);
PWM_SetCompare_y(Y_PWM);
/*OLED显示当前xy的pid后的值*/
OLED_ShowSignedNum(3, 1,X_PWM,4);
OLED_ShowSignedNum(4, 1,Y_PWM,4);
OLED_ShowSignedNum(3, 7,X_Vel_PWM,4);
OLED_ShowSignedNum(4, 7,Y_Vel_PWM,4);
OLED_ShowSignedNum(3, 11,ballx,4);
OLED_ShowSignedNum(4, 11,bally,4);
}
if (x == 0 )
{
PWM_SetCompare_x(servo_x);
PWM_SetCompare_y(servo_y);
}
}
}
// // /*串口获取x轴与y轴坐标*/
// x = Get_x();
// y = Get_y();
OLED_ShowChar(2,1,'X');
OLED_ShowChar(3,1,'Y');
OLED_ShowNum(4,1,1234,4);
// /*OLED显示当前xy轴坐标*/
// OLED_ShowNum(1, 1,x,4);
// OLED_ShowNum(2, 1,y,4);
// OLED_ShowNum(1, 7,Serial_flag(),1);
// if(Serial_flag() == 1)
// {
// /*将当前的xy轴坐标带入PID中*/
// sp_x = PID_x(target_x, x, kp_x, ki_x, kd_x);
// sp_y = PID_y(target_y, y, kp_y, ki_y, kd_y);
// sx = servo_x + sp_x;
// sy = servo_y + sp_y;
// }
// /*输出xy舵机的驱动值*/
// PWM_SetCompare_x(sx);
// PWM_SetCompare_y(sy);
//
// /*OLED显示当前xy的pid后的值*/
// OLED_ShowNum(3, 1,servo_x + sp_x,4);
// OLED_ShowNum(4, 1,servo_y + sp_y,4);
//
// OLED_ShowSignedNum(3, 7,sp_x,4);
// OLED_ShowSignedNum(4, 7,sp_y,4);
//
//
// }
/******************蓝牙通信***************************/
// if (Serial_GetRxFlag() == 1)
// {
// Serial_RxData = (char)Serial_GetRxData();
// OLED_ShowChar(2, 8, Serial_RxData);
// Serial_SendByte(Serial_RxData);
// }
/******************球、点坐标***************************/
// OLED_ShowNum(1,1,coor_count_x_y[count_x_y[0]][0],1);
// OLED_ShowChar(2,1,'(');
// OLED_ShowNum(2,2,coor_count_x_y[count_x_y[0]][1],3);
// OLED_ShowChar(2,5,',');
// OLED_ShowNum(2,6,coor_count_x_y[count_x_y[0]][2],3);
// OLED_ShowChar(2,9,')');
// OLED_ShowNum(3,1,coor_count_x_y[count_x_y[0]][3],1);
//
// OLED_ShowNum(4,1,coor_count_x_y[3][0],1);
// OLED_ShowChar(4,2,'(');
// OLED_ShowNum(4,3,coor_count_x_y[3][1],3);
// OLED_ShowChar(4,6,',');
// OLED_ShowNum(4,7,coor_count_x_y[3][2],3);
// OLED_ShowChar(4,10,')');
// }
/******************舵机测试**************************/
// KeyNum = Key_GetNum();
// if (KeyNum == 1)
// {
// OLED_ShowNum(4, 1,123,4);
// Angle_x -= 5;//20下,
//
// Servo_SetAngle_x(Angle_x);
// if (Angle_x < 40)
// {
// Angle_x = 40;
// }
// if (Angle_x > 80)
// {
// Angle_x = 80;
// }
// }
// else if(KeyNum == 2)
// {
// OLED_ShowNum(4, 5,456,4);
// Angle_y -= 5;//25下,
// Servo_SetAngle_y(Angle_y);
// if (Angle_y < 40)
// {
// Angle_y = 40;
// }
// if (Angle_y > 80)
// {
// Angle_y = 80;
// }
// }
//
// OLED_ShowNum(2, 3, Angle_x, 4);
// OLED_ShowNum(3, 3, Angle_y, 4);
// constrain_float(Angle_x, 40, 80); //输出限幅
// constrain_float(Angle_y, 40, 80);
四、pid的学习
pid的学习可以参考下方链接,此链接详细的介绍里pid的介绍。此处我仅写写我自己的理解以及学习中遇到的问题。
学习链接:http://t.csdnimg.cn/yivMv
以位置环为例,pid计算主要为系统指定一个目标值,PID将目标值(目标位置)与被控对象当前的反馈量(小球当前位置)作差得到误差(将目标值与当前值作为输入量需已知),PID将误差值分别经过三个环节计算得到输出分量,三个分量加起来得到PID的输出,将PID的输出施加到被控对象即驱动器(舵机/电机)上,使反馈量向目标值靠拢
PID三个环节的作用
PID三个环节各自的主要作用和效应:
比例环节:起主要控制作用,使反馈量向目标值靠拢,但可能导致振荡
积分环节:消除稳态误差,但会增加超调量
微分环节:产生阻尼效果,抑制振荡和超调,但会降低响应速度
现在就PID控制算法进行剖析。
- 比例控制:
假设小球初始位置(0,0),目标位置(30,30),距离我们的目标是有很大的差距的,这个时候就通过比例控制的方式加大调节力度,让小球更快到达目标位置
这个时候的比例系数的选取就非常的重要:
比例系数如果太小,调节的力度不够,使系统输出量变化缓慢,调节所需的总时间过长。
增大比例系数:可以使系统反应灵敏,调节速度加快,还可以减小稳态误差。但比例系数如果过大,调节力度太强,容易造成调节过头,严重的甚至使小球无法到达目标位置,超调量也会增大,这不是我们希望看到的。
所以,单纯的比例控制很难让确保调节的稳定,也难以做到完全消除误差。
2.积分控制
在控制系统中,输出量与设定值两者的差值就是控制的偏差,有偏差就说明输出量并未稳定在期望值上,需要继续调节。
具体来说,如果小球当前位置低于目标位置,那么偏差就是正的。在积分控制下,积分项会累积这个正的偏差,导致调整量(比如施加给小球向上的力或调整其运动轨迹的装置的角度)逐渐增大,使小球向上移动,接近目标位置。
反之,如果小球当前位置高于目标位置,那么偏差就是负的。积分项会累积这个负的偏差,导致调整量逐渐减小(比如减小施加给小球向上的力或调整其运动轨迹的装置的角度),使小球向下移动,远离当前位置,逐渐接近目标位置。
通过这种方式,积分控制能够持续根据小球与目标位置之间的误差来调整作用力或运动轨迹,使小球最终稳定地停留在目标位置上。这种控制方法在处理需要精确到达目标位置的任务时非常有效。
总而言之,PID控制中,积分作用就是对偏差的累积,用于消除静差。
3.微分控制
PID(比例-积分-微分)控制系统中,微分项(Derivative)确实用于反映偏差(Error)的变化速率。具体来说,当系统的实际输出与期望输出之间的偏差变化越快时,微分项的绝对值就越大;反之,如果偏差变化缓慢,微分项的绝对值就较小。
串级pid:串级pid以期望的速度转动到期望的位置
五、踩过的坑:
- 问题:与摄像头的串口通信出现问题,无法将数据准确传入二维数组,中间其实有很多问题,看下方解决方式
解决:
- 数据传输混乱,有几个点传入错误,排查了好久,后面发现是因为我们只用了一个起始帧,导致有几个点没能录入
- 虽然先约定了串口格式,但后面还是改变了发送格式,二维数组行存储忘记改变,后面也是排查了好久
- 从树莓派改成香橙派,树莓派不知道什么原因3.3V直接打通了,真的太痛啦,后面只能采用现有的香橙派。
2.问题:数据类型给错啦!!!大坑,还踩了两次,一次是uint8_t 装小球的坐标,结果坐标有的大于255,直接就超范围了,超过255就不显示了。一次是
int ballx = (receivedValuex - previousValuex )/10;去计算小球速度,结果/10之后是小数,存不到整数型里面,所以pid出问题。
解决方法:1.直接将uint8_t改为int
2.数据类型不变,不要/10,直接丢到pid里面计算
3.问题:机械结构不稳,都说机械结构是半辟江山,这个板球系统其实拖了很久,四月初搞的,中间因为比赛再加自己偷懒拖得太久了,机械结构本身就有问题,一开始是底座不稳,舵机一转底座也跟着翘起来,后面虽然把他黏在一个够重的板子上了,但是舵机动底板还是会动一点。再加上拖太久,亚克力板压弯了,只能说惨不忍睹,所以最后还是放弃了,或者后面再找个时间重新做机械结构再调参。
六、未能解决的问题:
为什么引入串级pid?
因为当时无论怎么调小球都无法在目标值来回震荡,将小球放在目标点附近,板子抬升,小球动起来,但是位置差越来越大,小球离目标点越来越远,速度也越来越大,小球最终滚出板子。后面发现,只用位置环,假如位置差一样,但是小球的速度不一样,但是pid两次输出是一样的,舵机的抬升值一样,好像就是无法解决。
其次,就是我对舵机的PWM进行限幅,控制板子大约在30度内来回倾斜,但是会出现下列情况,很头疼,可能是我对pid的理解有偏差,但是我确实解决不了,甚至到后面单速度环也会出现这种情况。