直立车想节能——2020全国大学生智能车车竞赛经验记录之我想要直立(平衡小车串级pid调参经验)

平衡车我相信很多电赛测控的入门的玩家都也玩过,很多朋友们也都听说过平衡小车之家这家淘宝店铺。而串级PID也是一个经典且牛批的方案而广为人知,所以在这样的比赛结束之后作为俩套方案都试过的我,谈一谈平衡小车之家的代码即并行的pid和串级pid的优缺点以及调参经验。

三环介绍

废话少说正题开始

我相信大家都应该很了解PID是什么,以及 P I D分别是干什么的,这里就不详细介绍了。反而我想谈一谈平衡小车的控制需要什么。

首先我觉得大家更为熟知的是角度环,速度环,和转向环。角度环是为了控制小车的一个直立,速度环是为了控制小车的速度,而方向环则是为了控制小车的转向。其实这三个环分别起到什么作用是非常好理解的。

角度环也叫直立环,就是机械中值的角度值为目标值,误差为目标值减去当前姿态传感器读到的角度即
误差角度 = 中值角度 - 当前角度

速度环,就是你设定速度为目标速度,如果想他站着不动,那么设定的目标速度就是0,误差就是编码器读到的车轮的值,如果是俩个车轮就将其加起来乘或不乘一个1/2(本质没区别)。
误差速度 = 设定速度 - (编码器左+编码器右)

转向环,转向环通过读取航向角的角速度来,其实本质上还是控制这个角速度为莫个值。比如你想小车走直线,那么你就需要设定值为0,误差值就是设定值减去角速度
误差转向 = 设定转向 - 航向角角速度

那么并行pid和串级pid有什么区别呢?

并行PID

如何做

并行的PID是将三个环分别独立计算,最后叠加起来最后输出到电机上面去即
电机PWM = 直立环 + 速度环
如果加上转向环就是

电机左PWM = 直立环 + 速度环 +(转向环/2)
电机左PWM = 直立环 + 速度环 - (转向环/2)

(加减得看你的极性)

为什么这样做

其实这三个环分别都很好理解,直立环控制直立,速度环控制你的速度,转向环控制你的转向。
至于为什么叠加起来就能直接赋值给电机好像是可以参考卓大大之前的那片文章,里面有对模型的详细介绍。

这样做有什么好处

其实并行pid最大的好处就是现象十分明显,为什么这么说呢?
直立环加上你就能看见小车能立起来了,速度环加上你就能看到小车停止或者按照指定的样子跑,转向环也是这样,你能看到小车转向很明显。明显带来的效果就是非常方便你调试PID你可以很清楚的看到现象,从而PID的参数整定就会十分顺利。由于参数调试十分简单这里就不详细介绍了,在很多淘宝店铺都提供了很多代码,包括b站也都有许多教程。

串级PID

因为串级pid有很多种串联的方法,我在这里仅介绍一下我的方法,和按照我的方法的效果,仅供参考

如何做

串级pid不同于上面的,会多出一个环叫做角速度环,而且串级pid会多出一些概念,如内环和外环。我的串级pid的构造为,角速度内环,速度外环,中间是角度环,叠加转向环。具体是怎么工作的呢?
速度环的输出结果作为角度环的设定值,角度环的输出结果作为角速度环的设定值,角速度环的输出结果累加上我们上面介绍的转向环作为我们的电机PWM的输出。

速度环——>角度环——>角速度——>输出 + 转向环 = PWM

为什么这样做

首先两点,多出了一个串起来和一个角速度环。

汉堡告诉我的是,串起来的目的是为了PID更快的收敛,但是我个人的感觉不止于此。我认为串级PID能给传感器很多信息分布不同的权重,并且让其有序,而不是简单粗暴的累加起来,就像煮大锅饭一样。

角速度环是我一开始最不能理解的一个东西,我队友也十分抗拒加上这个东西,可是我到最后才发现角速度环才是这个串级PID的灵魂。如果大家调试过直立环大家就会发现一个问题就是,如果直立环调的太软(p比较小)你会发现整个平衡小车会有点来回的低频振荡不能稳住,如果你的调的太硬(p比较大)你会发现整个车会在高频抖动,这种情况下一是容易烧电机驱动(来回转电流很大都是堵转电流,不要问我为什么知道容易烧,我板子经历了他这个年纪不改经历的惨痛),二是你会发现车非常抖,就包括你加上速度环之后,你的车在行进过程中十分抖动,这会非常耗电,对节能就不友好了,而且车子是不稳定的(太软车就容易倒,甚至一碰就倒)。你要调试到那种堪堪不抖动,而且车还不容易倒的状态下,这个pid的参数十分难调,而角速度环就能解决这个问题。我在调试之前问学长,角速度环的 目的是干什么的,他给我的解答是能排除扰动和干扰。我一开始一直认为排除的扰动可能是车在进行过程中装到旁边的路障,或者转向过大的时候带来的不稳定,可是我到最终发现其实角速度环能解决的一个很大问题的就是你直立环调的过硬的时候,所带来的高频抖动,角速度环能帮助你去掉这个高频抖动。所以从这一点来说,你的角速度环和速度环调的得较为适配,这样你的车很难倒下,而且车不会很抖。

这样做有什么好处

我在上一点上也大概说了串级pid的几点好处,首先你会直观的发现你的车非常非常非常稳,稳到什么地步呢,基本上你的电磁前瞻杆子你拍一下他能起来而且不会倒,我在侧面对着车踢一脚车也是不会倒的。其次你的加速会非常明显只要你的重心不要过分的往后,加速是很到位的。最后就是避免了高频抖动,避免了电机的堵转,也会进一步节能。

缺点也很明显

其实带来好处的同时就会带来缺点,缺点就是PID十分难调,因为你的pid是一环套一环,首先你出了问题,现象不容易看出来是哪个环的问题,主要是角速度环和角度环 ,然后就是怎么调整,而且中间某个环的参数没调好,后面的都不好使,这就带来了很多挑战和难题。

我该怎么调

在串级pid中,内环是直接输出的,对于这一点来说,外环粗调就ok,内环精调。对于我们这个模型来说,主调角度环副调角速度环,调参顺序是从内环到外环,最后调转向环。
首先调参角速度环,角速度环一般是pi控制,其实很多地方有用增量式也有用位置式,对于内环的角速度环,我们需要高频控制,大概每2ms运算一次,对于这样的运算我们可以选择增量式来减少我们的运算量,当然对于增量式不熟用位置式也是一样的。在调整这个参数的时候与以往不同,我们需要先调整i的参数,原因是陀螺仪的角速度跳动非常大,十分容易出现静差,我们需要加上i来消除那部分静差,调整的现象是加上i车就能大幅的低频来回晃,能勉强立住,立住的效果越好i的参数越好,i过大也会导致高频抖动。在调整完i的参数之后我们就需要调p的参数,其实加上p之后你会发现车就能勉强立住了,不会晃的很大,你需要增大p大概到车子会稍微抖动,还没要到高频抖动的感觉。效果就是小车基本上能立起来。
其次调角度环,角度环一般是pd控制,大概是10ms运算一次(串级pid 的级和级之间的运算时间是2-9倍之间),加上角度环的p之后你会发现小车基本上能稳稳的立住了,当然这是合适的p,但是区分这个的现象真的一点都不明显,我的方法是拍前瞻的杆子,看响应速度,力气越大 响应速度越快,说明效果越好,其实基本上车是会往前冲,来调整角度,你也可以看冲完之后小车稳定下来的pid收敛速度,来判断p的值是否合适。基本上p感觉调到差不多了的时候,可以加上d,d是提高车的响应速度,加上d之后你会发现小车向前冲的距离会不大,力气不大的时候,能看到小车立刻弹起来,这就是比较好的响应速度,d过大也会带来高频的抖动。加上合适pd之后你会发现小车基本是很稳定的立在地面上,除非特别暴力什么的,小车是不会倒的,哪怕是撞上什么物体,或者是碾过什么物体都不会倒。这个角度环得调的比较硬,这样你才能在加速的时候看起来效果比较好,不然你会看到小车在形式过车中,车身比较抖或者车容易倒,这是俩个极端。角度环是比较重要的一个环,需要我们耐心去调试。其实这也会面临一个问题,我大概调到什么样子才能到下一个环上面去调,这就得靠自己的尝试和感觉了,我个人觉得需要自己多调一调就会找到那个感觉,我调了三遍,第一遍调了大概五六个小时,第二遍调了大概2-3小时,第三遍一个小时就调完了,需要自己摸索找感觉。
最后速度环,速度环的调试其实比较粗糙了,感觉速度环的p就是小车的一个加速度,p越大加速度越大,甚至速度环不加i都是可以的。虽然速度环的i固然会积分导致一些问题出现,但是我们可以用多少次的一个清零尽可能的解决这个问题,如果不加i的话,感觉速度会不稳定,这就对你的直立环和角速度环提出了很大的考验,我看到的一个朋友就是没有加i,我看他加速的时候车屁股一直在点地,虽然规则是允许平衡车的第三点点地,但是这比如对我们的节能产生了很大的影响,节能的很重要的一个前提就是稳定的工作,以我们的电瓶车为例,频繁的加速减速你车的待电就不行。
转向环其实都是差不多的,但是有一点就是转向环需要和速度形成一定的联系,因为转向的多少是累加在我们角速度环的输出上,和并行那边是差不多的,如果你转向环是死的,你高速和低速状态下的转向环的值不一样,这就容易出问题。我看到人家的代码是 利用一个俩个的串级串到一起,是利用电磁的差比和作为当前值,设定值为0,进一个pid算出参数,当前的航向角的角速度为当前值,这个参数乘上速度作第二个pid的设定值,第一个pid还是一个动态pid,感觉挺巧妙的,由于时间关系没有尝试
在这里插入图片描述

END

调参很苦,熟能生巧,思考现象,方能直立
感觉踩了很多坑,也问了很多人,非常感谢这一路上许多学长耐心解答,我也希望我的文章能给别人带来一丝丝参考价值。节能直立挺好玩的,但是很可惜,在方向上走错了很多,只能怪我太菜了。愿大家都能做出三米神车,越来越牛批!

突然有些朋友问我要代码,其实我也没什么可以给的,pid都是一个样子,可以给大家看一下流程的样子(这是学长的代码)

void Balance_Control(void)
{
	if (Ang_Velocity_Flag)	// 直立角速度环	2ms
	{
		Ang_Velocity_Flag = 0;
		ICM_GetData(&GYRO,&ACC);
                //Data_Filter();//原始数据滤波
                Data_steepest(); 
                 IMU_update(0.008f,&(sensor.Gyro_deg), &(sensor.Acc_mmss),&imu_data); //匿名移植姿态解算
                icm_gyro_z = LPButterworth(icm_gyro_z, &Gyro_Butter, &Butter_20HZ_Parameter_Acce);//巴特沃斯30HZ滤波    Z轴和Y轴角速度都用原始数据,只经过滑动滤波和低通处理
                icm_gyro_y = LPButterworth(icm_gyro_y, &Gyro_Butter1, &Butter_30HZ_Parameter_Acce);
                //IMUupdate(GYRO.X*Gyro_Gr, GYRO.Y*Gyro_Gr, GYRO.Y*Gyro_Gr, ACC.X*AcceRatio, ACC.Y*AcceRatio, ACC.Z*AcceRatio);
                state = Long_street();
//                if (state)//障碍,长直道减速
//                {
//                  Speed_Set = 180;
//                }
//
//                else
//                {
//                  Speed_Set = 320;
//                
//                }
               //Middle_Err = LPButterworth(Middle_Err, &Middle_Err_Butter, &Butter_10HZ_Parameter_Acce); 
		Radius = -PlacePID_Control(&Turn_PID, Turn[Fre], Middle_Err, 0);//动态PID,动态范围0-0.04	,大误差时起作用,
                
		Theory_Duty += -PID_Increase(&Ang_gyro_PID, Ang_gyro, (int32)icm_gyro_y, (int32)(Tar_Ang_Vel.Y));										/* 角速度环作为最内环控制直立 */
		//abs +=PID_Increase(&Ang_gyro_PID, Ang_gyro, (int32)(GYRO_Real.Y*10), 0)*10;//(int32)(Tar_Ang_Vel.Y));	// 计算直立PWM
                //abs +=Theory_Duty;
                //Theory_Duty += Theory_Duty;
                //Theory_Duty = -Theory_Duty;
               //abs = abs * 10;
		Theory_Duty = LIMIT(Theory_Duty, -900, 900);
                //Direct_Parameter = -PID_Realize(&Turn_gyro_PID, Turn_gyro, (int32)(icm_gyro_z),0);
                Direct_Parameter = -PID_Realize(&Turn_gyro_PID, Turn_gyro, (int32)(icm_gyro_z),Radius*Speed_Min);	// 转向环左正右负
                Direct_Parameter = range_protect(Direct_Parameter, -800, 800);
		Direct_Parameter = Turn_Out_Filter(Direct_Parameter);//输出滤波
		
//		Direct_Parameter = Direct_Last*0.2 + Direct_Parameter * 0.8;	// 更新上次角速度环结果	
//                Direct_Last = Direct_Parameter;
		//Direct_Parameter = 0;
		MOTOR_Duty_Left  = Theory_Duty + Direct_Parameter;	// 左右电机根据转向系数调整差速
		MOTOR_Duty_Right = Theory_Duty - Direct_Parameter;	
		
		//get_angle();
               // Kalman_Filter(Mpu_date.roll, Mpu_date.groy);//卡尔曼滤波

                
               
	}
        if (AngleJS_Flag)
        {
          AngleJS_Flag = 0;
         
        }
	
	if (Angle_Flag)		// 直立角度环	10ms
	{
		Angle_Flag = 0;
		
		get_speed(&left_speed,&right_speed);// 获取当前速度
                //speed_measure(left_speed, right_speed);
		Speed_Now = (  left_speed-right_speed)/ 2;
                Distance = Speed_Now + Distance;
		Speed_Now = Speed_Now * 0.8 + Speed_Last * 0.2;
                Speed_Last = Speed_Now;
                //Long_Load();
                Tar_Ang_Vel.Y = -PID_Realize(&Angle_PID, Angle, (int32)(imu_data.pit*100), (int32)Target_Angle.Y);	
		Tar_Ang_Vel.Y = range_protect(Tar_Ang_Vel.Y, -1000, 1000);	// 注意正负号
 
                
                
	}
	if (Speed_Flag)		// 速度环	50ms
	{
		Speed_Flag = 0;
												/* 速度环加到角度环上串级控制 */
		Target_Angle.Y = -PID_Realize(&Speed_PID, Speed, Speed_Now, Tar_v+Speed_Set);	// 结果为放大1+00倍的目标角度
		Target_Angle.Y += Zero_Angle*100;	// 目标角度叠加在零点上
                //Target_Angle.Y = -Target_Angle.Y;
//                if (Middle_Err>=500 || Middle_Err<=-500)//一定程度上防止弯道减速
//                {
//                  Target_Angle.Y = 3500;
//                }
		Target_Angle.Y = range_protect((int32)Target_Angle.Y, 200, 3000);

		Speed_Min = Speed_Min * 0.1 + Speed_Now * 0.9;
		if (Speed_Min < 40)
		{
			Speed_Min = 40;
		}
                
                //oled_int16(84,0,Target_Angle.Y);
	}
        
}

以及位置式pid的代码

// 位置式动态PID控制
int32 PlacePID_Control(PID *sprt, float *PID, float NowPiont, float SetPoint)
{
	//定义为寄存器变量,只能用于整型和字符型变量,提高运算速度
	float iError;	//当前误差
	int  Actual;	//最后得出的实际输出值
	float Kp;		//动态P
	
	iError = SetPoint - NowPiont;	//计算当前误差
	sprt->SumError += iError*0.01;
	if (sprt->SumError >= PID[KT])
	{
		sprt->SumError = PID[KT];
	}
	else if (sprt->SumError <= PID[KT])
	{
		sprt->SumError = -PID[KT];
	}
	//二次函数是为了达到 误差越大  反应越快 回复力越大 其中 KI值是误差为0时的P 也就是直道上的P值
	Kp = 1.0 * (iError*iError) / PID[KP] + PID[KI];	//P值与差值成二次函数关系,此处P和I不是PID参数,而是动态PID参数,要注意!!!
	
	Actual = Kp * iError
		   + PID[KD] * ((0.8*iError + 0.2*sprt->LastError) - sprt->LastError);//不完全微分  
	sprt->LastError = iError;		//更新上次误差

	Actual = range_protect(Actual, -260, 260);

	return Actual;
}

仅供大家参考

  • 146
    点赞
  • 892
    收藏
    觉得还不错? 一键收藏
  • 43
    评论
下面是一个基于PID串级控制的直立平衡代码的示例: ```cpp // PID Constants float velocityKp = 0.1; // Velocity Loop Proportional Gain float velocityKi = 0.01; // Velocity Loop Integral Gain float velocityKd = 0.05; // Velocity Loop Derivative Gain float angleKp = 10; // Angle Loop Proportional Gain float angleKi = 0.1; // Angle Loop Integral Gain float angleKd = 0.5; // Angle Loop Derivative Gain // Variables float velocitySetpoint = 0; float angleSetpoint = 0; float velocityInput = 0; float angleInput = 0; float velocityOutput = 0; float angleOutput = 0; float velocityErrorSum = 0; float angleErrorSum = 0; float velocityLastError = 0; float angleLastError = 0; unsigned long loopTime = 0; void setup() { // Initialize motors, sensors, and other peripherals } void loop() { unsigned long currentTime = millis(); float dt = (currentTime - loopTime) / 1000.0; // Calculate time difference in seconds loopTime = currentTime; // Read encoder data and calculate velocity input float encoderData = getEncoderData(); // Replace with actual function to read encoder data velocityInput = encoderData / dt; // Calculate velocity by dividing change in encoder data by time // Velocity Loop float velocityError = velocitySetpoint - velocityInput; velocityErrorSum += (velocityError * dt); float velocityErrorRate = (velocityError - velocityLastError) / dt; velocityOutput = velocityKp * velocityError + velocityKi * velocityErrorSum + velocityKd * velocityErrorRate; velocityLastError = velocityError; // Read gyroscope data and calculate angle input float gyroData = getGyroData(); // Replace with actual function to read gyroscope data angleInput = integrateGyroData(gyroData, dt); // Integrate gyroscope data to get angle // Angle Loop float angleError = angleSetpoint - angleInput; angleErrorSum += (angleError * dt); float angleErrorRate = (angleError - angleLastError) / dt; angleOutput = angleKp * angleError + angleKi * angleErrorSum + angleKd * angleErrorRate; angleLastError = angleError; // Calculate motor PWM values based on velocity and angle outputs float leftMotorPWM = velocityOutput + angleOutput; float rightMotorPWM = velocityOutput - angleOutput; // Apply motor PWM values to control the motors // Delay for a short period of time delay(10); // Adjust the delay value as needed based on the control loop frequency } // Function to integrate gyroscope data and get angle float integrateGyroData(float gyroData, float dt) { static float integratedAngle = 0; integratedAngle += gyroData * dt; return integratedAngle; } // Function to get encoder data float getEncoderData() { // Replace with actual code to read encoder data return 0.0; } // Function to get gyroscope data float getGyroData() { // Replace with actual code to read gyroscope data return 0.0; } ``` 请注意,上述示例代码中的PID参数(如`velocityKp`、`velocityKi`、`velocityKd`、`angleKp`、`angleKi`、`angleKd`)需要根据实际情况进行调整和优化。此外,还需要替换示例代码中的函数`getEncoderData()`和`getGyroData()`来读取实际的编码器和陀螺仪数据。最后,确保按照实际控制循环的频率进行延迟,以使控制系统按照期望的频率运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 43
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值