此篇博客用于记录校内赛的技术细节和感悟(屎山预警!!!)
1.1 红外对管 排布见下图:
其中4 5作为循迹灯,其余全部用于路口判断。
1.1.1 循迹 利用左右循迹灯实现循迹,使用了PID算法,只使用了P和D,详细说明见2.1。
1.1.2 路口判断逻辑 利用1 2 3 0四个红外对管实现对路口的识别。
另外规定ADC的值>1900为完全踩上线,ADC的值小于lowx为完全没踩上线,ADC的值大于lowx+200(需要大些)为正在踩上线。
我们对路口的分类如下:
1.2 机械臂 构成:亚克力板,三个MG90S舵机,胶带,热熔胶,夹子,螺丝,螺母。
自由度:3
设计想法:为了便于能量块的夹取和定点投放
2. 软件 注:此部分主要内容及解释都在代码注释中
2.1 宏及全局变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include "stm32f10x.h" #include "Delay.h" #include "PWM.h" #include "bsp_usart.h" #include "AD.h" #include "stm32f10x_it.h" #include <stdio.h> # define kp 0.90 # define kd -80 # define high1 2100 # define low1 1800 # define high2 2400 # define low2 2000 # define delaytime 200 #define NOFCHANEL 12 #define FILTER_SIZE 5 int16_t filtered_AD0[FILTER_SIZE] = {0 };int16_t filtered_AD1[FILTER_SIZE] = {0 };int16_t filtered_AD2[FILTER_SIZE] = {0 };int16_t filtered_AD3[FILTER_SIZE] = {0 };int16_t filtered_AD4[FILTER_SIZE] = {0 };int16_t filtered_AD5[FILTER_SIZE] = {0 };extern __IO uint16_t ADC_ConvertedValue[NOFCHANEL];float ADC_ConvertedValueLocal[NOFCHANEL];int16_t et = 0 ;int16_t det = 0 ;int16_t temp = 0 ;int16_t wucha =0 ;
2.2 主函数及初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int main (void ) { USART_Config(); TIM1_CH4_PWM_Init(); PWM_Init(); AD_Init(); wucha =AD_Value[4 ]-AD_Value[5 ]; while (1 ) { if ('7' ==ucTemp) { while (1 ){ pid(); if (isTIntersection()) { handleTIntersection(); } else if (isLeftTurn()) { handleLeftTurn(); } else if (isRightTurn()) { handleRightTurn(); } else if (isRoundabout()) { handleRoundabout(); }else if (isRound()) { handleROUND(); } if ('6' ==ucTemp ) {Motor_Run(0 ,0 ); Motor_Run(1 ,0 ); break ;} } } if ('5' ==ucTemp) {while (1 ) { BlueTeeth(); if ('6' ==ucTemp ) {break ;} } } if ('8' ==ucTemp) {while (1 ) { pid(); if ('6' ==ucTemp) {break ;} } } } }
2.3 PID 我们的小车只使用了P和D,因为我们实现闭环的不是加速度,所以不需要用积分项去逼近目标位置。P好理解,就是正比于误差的量;但P也不能太小,否则P太小的话转弯太慢,导致S弯绕不过去。D作为斜率可以通过抑制P的趋势来实现消抖的功能。
理论学习如下:
整体思路:ADC采样—>对ADC采样数据的数学处理—>对得到的数据进行PID算法运算—>赋值给ccr调整pwm波输出占空比驱动电机。循环此过程则可实现速度闭环实现直角弯道除外的赛道。 方案:两灯and三灯。但考虑到两灯操作更简单,对硬件兼容性更高。最终采用两灯方案。 理论理解: 尽可能的扩大两个参数的值域:ADC数学处理获得值、小车识别黑线范围。针对第一个参数,当此值变化范围很大时,可以获得更高的精度调控;针对第二个参数,当此值变化范围很大时,可以扩大小车的“视野”,即使已经偏线也可以回归正轨。
归根结底,重点在于数学处理。
@针对采样:
重要参数: 单个采样范围及变化趋势,单个采样值。
单个采样范围及变化趋势: 非常有限,几乎不触及黑线采样值变化很小,但一接近黑线立即成指数增长。
单个采样值: 几百个体差异到约2300的原始值。但转化为电压值之后仅为 0~3.3,大量数据被压缩,损失了很多精度,最终决定采用原始值进行操作。
误差来源 :环境红外线影响,红外对管个体差异,红外对管彼此距离,红外对管对地距离。
环境红外线影响 :很大。非常影响红外对管采集数据。最终解决办法有两种,一是在每个红外对管四周缠上黑胶布,用以反射环境红外线并聚合红外对管发出的红外线;二是在代码中初始化过程中消除环境误差并不需要测得,但随着小车运动导致的位置改变,环境误差在变化,而我们只消除了开始的误差,我们选择使用动态消除误差。
红外对管个体差异: 不同红外对管的峰值大致相同,但最低值各有千秋。有的一两百,有的却五六百。考虑到对ADC采样数据进行处理时,个体误差的存在会影响很多计算结果造成两个灯调节作用差别很大。故必须消除。最终我们采用个体采样值域大的红外对管,以输出更加灵敏的值,并在代码中在初始化过程中,连同环境误差一起消除。
红外对管彼此距离: 主要影响两点,一是识别范围,二是数学处理时产生漏洞。对于第一点,两灯距离太小时,识别范围就太小,小车遇到稍抖一点的弯路时极易偏线,但两灯距离过大时,就相当于允许小车有一定程度的偏线,造成循迹不流畅。对于第二点,若两灯距离过小,此时可以忽略个体误差,但同时ADC数学处理获得值值域很小,调节精度或说范围很小,若两灯距离过大,两灯之间会留出一个处理值为0的空白区允许小车一定程度偏线,这是我们不希望看到的。
红外对管对地距离 :主要影响红外对管的采样值域。对地距离过低时红外对管接收不到反射光,采样值很低,对地距离过高时很受环境光干扰,接收红外线很多,采样值很高,通过打印波形图来观察何对地距离红外对管的检测距离最大,以及采样值域最大。
@针对数学处理:
处理参数: 识别范围,输出值。
识别范围: 即小车的“视野”,能够允许偏线程度最大的情况下依然可以回归正轨。
输出值: 作为偏差,进行PID运算后,改变ccr来改变电机占空比。
处理方法与消除误差:
缺少能够作为条件的判断,所以只有让两灯采样值相减,同时让两灯在同一环境下初始化,记录两灯差值,然后代入程序中来消除个体误差和环境误差,然后调用ADC数据处理函数。
@针对PID:
本次任务中采用位置式PID,且用到P与D参与运算。
采用PD系统调节原因:通过P进行主导线形控制,通过D来计算未来趋势抑制系统震荡,即通过PD系统获得更快的反应速度。符合循迹需求。
输入值:实时计算ADC数学处理获得值与目标值0的差值
输出值:除了初始占空比以外的用于控制电机的pwm
偏差:ADC数学处理获得值与目标值0的差值,用于P计算
二次偏差:前后两次偏差的差值,用于D计算
调参注意:P过小调节作用小,P过大造成过冲系统震荡。D过小调节作用小,D过大会放大系统趋势的影响,使系统震荡。
调参顺序:先设D=0,调P,从0逐渐增大,直到系统震荡。再调D使其逐渐增大,直到系统震荡。之后进行微调,直到系统稳定。
@针对电机控制:
调节方式:对两边电机附PID不同运算值,以达到使两侧不同性能电机具有相同效果的调节作用。
调节精度及限制:
考虑到可供调节的ccr范围为0~500( 有点小,占空比的相对调节精度小),但考虑到对循迹小车来讲500的调节范围够用,所以没改,但后来调车发现问题,发现有些曲率较大的弯转不过去,起初认为是P设的过小,但经过数学运算后发现问题是数值溢出,即能转过这个弯路的PID处理值没发挥出它的作用,因为溢出下限即占空比为0,溢出上限即占空比为100,所以最终采用【循迹电机反转函数】的使用,使溢出值得到充分利用。事实上增大ccr的可操作区间亦可。
初始值设置:一开始设置为50%占空比,因为考虑到想使PID调节具有对称性。事实效果很好,循迹很丝滑,但后来考虑到走直线速度较慢,故逐渐增大初始占空比,再具体进行调参。
详细解释见下方代码注释。
2.3.1 PID代码 2.3.1.1 PID基本原理 PID详解
比例增益(P): 比例部分根据当前误差的大小调整输出。如果误差较大,比例增益会产生更大的输出变化,以更快地减小误差。然而,如果比例增益设置得太大,系统可能会变得不稳定。积分时间(I): 积分部分考虑了误差随时间的积累。它用于消除系统稳态误差,因为它会持续增加控制输出,直到误差为零。但如果积分时间设置得太大,可能导致系统的超调或振荡。微分时间(D): 微分部分考虑了误差变化的速度。它可以帮助系统抑制振荡,因为它对误差变化的速度进行响应,减小输出的变化速度。然而,如果微分时间设置得太大,可能会导致系统对噪声敏感。PID算法基本原理
PID算法的执行流程是非常简单的,即利用反馈来检测偏差信号,并通过偏差信号来控制被控量。而控制器本身就是比例、积分、微分三个环节的加和。
根据上图我们考虑在某个特定的时刻t,此时输入量为rin(t),输出量为rout(t),于是偏差就可计算为err(t)=rin(t)-rout(t)。于是PID的基本控制规律就可以表示为如下公式:
其中Kp为比例带,TI为积分时间,TD为微分时间。
PID算法离散化
由于在计算机上应实现离散化问题,我们对比例,积分,微分特性做简单说明。
比例就是用来对系统的偏差进行反应,所以只要存在偏差,比例就会起作用。积分主要是用来消除静差,所谓静差就是指系统稳定后输入输出之间依然存在的差值,而积分就是通过偏差的累计来抵消系统的静差。而微分则是对偏差的变化趋势做出反应,根据偏差的变化趋势实现超前调节,提高反应速度。
在实现离散前,我们假设系统采样周期为T。假设我们检查第K个采样周期,很显然系统进行第K次采样。此时的偏差可以表示为err(K)=rin(K)-rout(K),那么积分就可以表示为:err(K)+ err(K+1)+┈┈,而微分就可以表示为:(err(K)- err(K-1))/T。于是我们可以将第K次采样时,PID算法的离线形式表示为:
这就是所谓的PID算法离散描述公式。还有一个增量型PID算法,下面来推导一下。 上面的公式描述了第k哥采样周期的结果,那么前一时刻也就是k-1哥采样周期可表示为:
那么我们再来说第K个采样周期的增量,很显然就是U(k)-U(k-1)。于是我们用第k个采样周期公式减去第k-1个采样周期的公式,就得到了增量型PID算法的表示公式:
当然,增量型PID必须记得一点,就是在记住U(k)=U(k-1)+∆U(k)
PID控制器基本实现
完成了离散化后,我们就可以来实现它了。已经用离散化的数据公式表示出来后,再进型计算机编程已经不是问题了。接下来我们就使用C语言分别针对位置型公式和增量型公式来具体实现。
位置型PID简单实现
位置型PID的实现就是以前面的位置型公式为基础。这一节我们只是完成最简单的实现,也就是将前面的离散位置型PID公式的计算机语言化。 首先定义PID对象的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct { float setpoint; float proportiongain; float integralgain; float derivativegain; float lasterror; float result; float integral; }PID;
接下来实现PID控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void PIDRegulation (PID *vPID, float processValue) { float thisError; thisError=vPID->setpoint-processValue; vPID->integral+=thisError; vPID->result=vPID->proportiongain*thisError+vPID->integralgain*vPID->integral+vPID->derivativegain*(thisError-vPID->lasterror); vPID->lasterror=thisError; }
这就实现了一个最简单的位置型PID控制器,当然没有考虑任何干扰条件,仅仅只是对数学公式的计算机语言化。
增量型PID简单实现
增量型PID的实现就是以前面的增量型公式为基础。这一节我们只是完成最简单的实现,也就是将前面的离散增量型PID公式的计算机语言化。
首先定义PID对象的结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct { float setpoint; float proportiongain; float integralgain; float derivativegain; float lasterror; float preerror; float deadband; float result; }PID;
位置型PID控制器的基本特点:
位置型PID控制的输出与整个过去的状态有关,用到了偏差的累加值,容易产生累积偏差。 位置型PID适用于执行机构不带积分部件的对象。 位置型的输出直接对应对象的输出,对系统的影响比较大。 增量型PID控制器的基本特点:
增量型PID算法不需要做累加,控制量增量的确定仅与最近几次偏差值有关,计算偏差的影响较小。 增量型PID算法得出的是控制量的增量,对系统的影响相对较小。 采用增量型PID算法易于实现手动到自动的无扰动切换。 2.3.1.2 最终代码实现 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void pid (void ) { int16_t filtered_AD4_value = getFilteredValue(filtered_AD4); int16_t filtered_AD5_value = getFilteredValue(filtered_AD5); et = filtered_AD4_value - filtered_AD5_value - wucha; det = et - temp; if (((kp) * et < 200 ) && ((kp) * et > -200 )) { MotorControl(FRONT, 1000 + (kp)/2 * et - kd * det); } else if (kp * et <= -50 ) { MotorControl(LEFT, -100 - kp * et + kd * det); MotorControl(FRONT, 1000 ); }else if (kp* et >= 50 ) { MotorControl(FRONT, 1000 ); MotorControl(RIGHT, kp * et - 100 - kd * det); } temp = et; updateFilter(filtered_AD4, AD_Value[4 ]); updateFilter(filtered_AD5, AD_Value[5 ]); }
2.4 路口识别 在本节中,详细讲讲判断环岛及真假环岛的逻辑(即连续两次左/右T型路口,左手定则为右T型,右手定则为左T型),其余路口都较为简单,不多赘述。
以左手定则走右环岛为例,首先不论环岛的真假,先判断连续两个右T型路口。我们设置了标志位flag(详见2.2主函数),在每次识别到非右T型路口时将其置零;在判断到右T型路口时将其取反,然后在下面写个若flag为0就进环岛函数的if,这样就实现了判断连续两个右T型路口。(详见2.4.1,2.4.3)
接着就是环岛的走法及判断环岛的真假。
(1) 环岛走法的大致逻辑就是,首先在第二个右T型路口右拐进环岛,然后识别环岛内第一个路口为右拐(第一个while,右拐后跳出),然后识别第二个路口为右拐(第二个while,右拐后跳出),然后识别第三个路口为T字型路口(第三个while,右拐后跳出),并return0,出函数再把flag置0,防止其一直走环岛。
(2) 判断环岛真假的大致逻辑是,在上述依次识别三个路口过程中,只要有一个识别到的路口与我们设定的路口不一致,就立马结束整个环岛函数并return相应的值(比如第一个路口不一致了就返回1,第二个不一致就返回2,第三个不一致就返回3),然后在循迹函数里做相应的处理。这里的处理也好理解,先让小车转180度,然后用switch case语句,从case3开始往case1写,内容便是按原路返回,然后在最初始的路口右拐。
比较有趣的是,我们发现在左手定则的大前提下,如果在同一个路口连续使用偶数次右手定则,则完全不会破坏左手定则的逻辑,换句话说,同一路口的偶数次右手定则等价于1次左手定则。
当然,从假环岛出来后还得把flag置1,这样又能解决走右T型+右环岛时进不了环岛的问题。
其代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 int isRound (void ) { return (AD_Value[7 ] < 2000 ); } void handleROUND (void ) { performAction(BACK_ACTION, 300 ); performAction(LEFT_ACTION, 1300 ); performAction(LEFT_ACTION, 900 ); } int isTIntersection (void ) { return (AD_Value[2 ] > high1 && AD_Value[0 ] > high1 && AD_Value[1 ] < low1); } void handleTIntersection (void ) { performAction(STOP_ACTION, 500 ); performAction(FRONT_ACTION, 500 ); } int isLeftTurn (void ) { return (AD_Value[2 ] > high2 && AD_Value[0 ] < low2 ); } int isRightTurn (void ) { return (AD_Value[2 ] < low1 && AD_Value[0 ] > high1); } void handleLeftTurn (void ) { performAction(STOP_ACTION, 500 ); performAction(LEFT_ACTION, 1400 ); performAction(FRONT_ACTION, 500 ); pid(); } void handleRightTurn (void ) { performAction(STOP_ACTION, 500 ); performAction(RIGHT_ACTION, 1500 ); performAction(FRONT_ACTION, 500 ); pid(); } int isRoundabout (void ) { return (AD_Value[2 ] > high1 && AD_Value[0 ] > high1 && AD_Value[0 ] > high1 && AD_Value[1 ] > high1 && AD_Value[4 ] > high1 && AD_Value[5 ] > high1); } void handleRoundabout (void ) { performAction(STOP_ACTION, 500 ); performAction(FRONT_ACTION, 500 ); } void CROSS_JUDG (void ) { int a=0 ; if (AD_Value[1 ]>2000 &&AD_Value[2 ]>2000 ) {a=1 ;} else if (AD_Value[1 ]>2000 &&AD_Value[0 ]>2000 ) {a=2 ;} else if (AD_Value[1 ]>2000 &&AD_Value[0 ]>2000 &&AD_Value[2 ]>2000 ) {a=3 ;} switch (a) { case 0 : break ; case 1 : MoveDirection(LEFT); Delay_s(1 ); break ; case 2 : MoveDirection(RIGHT); Delay_s(1 ); break ; case 3 : MoveDirection(RIGHT); Delay_s(1 ); break ; } }
2.5 蓝牙 蓝牙这块比较简单,只是一个简单的通信协议,其中给i赋的值都是ASCII码,然后在手机的按键里设置相应的字符即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void BlueTeeth () { if ('1' == ucTemp) { MoveDirection(FRONT); while (1 ) { if ('0' == ucTemp) { MoveDirection(STOP); ucTemp = '0' ; break ; } } } if ('2' == ucTemp) { MoveDirection(BACK); while (1 ) { if ('0' == ucTemp) { MoveDirection(STOP); ucTemp = '0' ; break ; } } } if ('3' == ucTemp) { MoveDirection(LEFT); while (1 ) { if ('0' == ucTemp) { MoveDirection(STOP); ucTemp = '0' ; break ; } } } if ('4' == ucTemp) { MoveDirection(RIGHT); while (1 ) { if ('0' == ucTemp) { MoveDirection(STOP); ucTemp = '0' ; break ; } } } }
2.6 电机控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 void MotorControl (Direction direction, int speed) { switch (direction) { case FRONT: left_up(); right_up(); Motor_Run(0 ,speed); Motor_Run(1 ,speed); break ; case BACK: left_down(); right_down(); Motor_Run(0 ,speed); Motor_Run(1 ,speed); break ; case LEFT: left_down(); right_up(); Motor_Run(0 ,speed); Motor_Run(1 ,speed); break ; case RIGHT: left_up(); right_down(); Motor_Run(0 ,speed); Motor_Run(1 ,speed); break ; case STOP: left_up(); right_up(); Motor_Run(0 ,0 ); Motor_Run(1 ,0 ); break ; } } void performAction (ActionType action, int duration) { switch (action) { case STOP_ACTION: MotorControl(STOP, 0 ); break ; case FRONT_ACTION: MotorControl(FRONT, 1000 ); break ; case BACK_ACTION: MotorControl(BACK, 1000 ); break ; case LEFT_ACTION: MotorControl(LEFT, 1300 ); break ; case RIGHT_ACTION: MotorControl(RIGHT, 1300 ); break ; } Delay_ms(duration); } void MoveDirection (Direction direction) { switch (direction) { case FRONT: MotorControl(FRONT, 1000 ); break ; case BACK: MotorControl(BACK, 1000 ); break ; case LEFT: MotorControl(LEFT, 1000 ); break ; case RIGHT: MotorControl(RIGHT, 1000 ); break ; case STOP: MotorControl(STOP, 0 ); break ; } } 轮子的直接控制
这一部分的代码主要是电机四个轮子的直接控制封装,由于左右两侧的轮子分别可以看作统一步调,故可以调整电机参数后统一输出PWM,达到同步效果。(底层封装略)
2.7机械臂 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 #include <Servo.h> #include "NeoSWSerial.h" NeoSWSerial mySerial (12 , 13 ) ; #define SERVOS (4) Servo arm_servos[SERVOS]; int servo_pins[SERVOS]; int servo_cur_angle[SERVOS]; int servo_min_angle[SERVOS]; int servo_max_angle[SERVOS]; int servo_init_angle[SERVOS]; int joystick_value[SERVOS];#define JOYSTICK_MIN_THRESH (300) #define JOYSTICK_MAX_THRESH (700) #define CLAW_OPEN_ANGLE (45) #define CLAW_CLOSE_ANGLE (5) #define CLAW_SERVO_INDEX (SERVOS-1) #define LEARN_MAX_ACTIONS (100) bool learning_mode = false ; bool repeat_mode = false ; int learn_actions[LEARN_MAX_ACTIONS][SERVOS]; int learn_action_count = 0 ; #define MAXSPEED (10) int demo_speed = 1 ; #define JOYSTICK_LEFT_BUTTON (2) #define JOYSTICK_RIGHT_BUTTON (4) #define JOYSTICK_LED (3) bool has_btn_been_pressed = false ; void setup () { Serial.begin(115200 ); pinMode(JOYSTICK_LEFT_BUTTON, INPUT_PULLUP); pinMode(JOYSTICK_RIGHT_BUTTON, INPUT_PULLUP); pinMode(JOYSTICK_LED, OUTPUT); digitalWrite(JOYSTICK_LED, HIGH); Serial.println("System Running..." ); Serial.print(digitalRead(JOYSTICK_LEFT_BUTTON)); Serial.print(", " ); Serial.println(digitalRead(JOYSTICK_RIGHT_BUTTON)); mySerial.begin(9600 ); delay(200 ); test_bluetooth_module(); initialization(); if (0 == digitalRead(JOYSTICK_LEFT_BUTTON)) { learning_mode = true ; has_btn_been_pressed = true ; learn_action_count = 0 ; delay(1000 ); digitalWrite(JOYSTICK_LED, LOW); Serial.println("Enter learning_mode!!" ); } else if (0 == digitalRead(JOYSTICK_RIGHT_BUTTON)) { play_demo_mode = true ; has_btn_been_pressed = true ; Serial.println("Enter play_demo_mode!!" ); play_demo(); } else { cut_cut(); Serial.println("Enter Bluetooth/JoyStick Control Mode!!" ); } } void loop () { move_by_bluetooth(); move_by_joystick_contrl(); learning_actions(); } void test_bluetooth_module () { Serial.print("\nTest Bluetooth Module: start\n" ); char cmd[16 ] = "AT+NAME\r\n\r\n" ; int len = strlen (cmd); Serial.println(cmd); Serial.println(len); for (int i = 0 ; i < len + 1 ; i++) { mySerial.write(cmd[i]); } delay(200 ); for (int i = 0 ; i < 16 ; i++) { if (mySerial.available()) { Serial.print(mySerial.read(), 0 ); } } Serial.print("\nTest Bluetooth Module: end\n" ); } void initialization () { servo_pins[0 ] = 11 ; servo_min_angle[0 ] = 0 ; servo_max_angle[0 ] = 180 ; servo_init_angle[0 ] = 90 ; servo_pins[1 ] = 10 ; servo_min_angle[1 ] = 10 ; servo_max_angle[1 ] = 140 ; servo_init_angle[1 ] = 90 ; servo_pins[2 ] = 9 ; servo_min_angle[2 ] = 40 ; servo_max_angle[2 ] = 170 ; servo_init_angle[2 ] = 90 ; servo_pins[3 ] = 5 ; servo_min_angle[3 ] = CLAW_CLOSE_ANGLE; servo_max_angle[3 ] = CLAW_OPEN_ANGLE; servo_init_angle[3 ] = CLAW_OPEN_ANGLE; for (int i = 0 ; i < LEARN_MAX_ACTIONS; i++) { for (int j = 0 ; j < SERVOS; j++) { learn_actions[i][j] = 0 ; } } init_servos(); } void init_servos () { for (int i = 0 ; i < SERVOS; i++) { arm_servos[i].attach(servo_pins[i], 500 , 2500 ); arm_servos[i].write(servo_init_angle[i]); joystick_value[i] = 0 ; } } void cut_cut () { delay(1000 ); for (int i = 0 ; i < 2 ; i++) { close_claw(true ); delay(150 ); close_claw(false ); delay(150 ); } } void close_claw (bool close) { if (close) { arm_servos[CLAW_SERVO_INDEX].write(CLAW_CLOSE_ANGLE); } else { arm_servos[CLAW_SERVO_INDEX].write(CLAW_OPEN_ANGLE); } } void move_by_joystick_contrl () { bool joy_changed = false ; for (int i = 0 ; i < SERVOS; i++) { joystick_value[i] = analogRead(i); #if 1 Serial.print("A[" ); Serial.print(i); Serial.print("]=" ); Serial.print(joystick_value[i]); Serial.print(", " ); #endif servo_cur_angle[i] = arm_servos[i].read(); if (joystick_value[i] > JOYSTICK_MAX_THRESH) { joy_changed = true ; if (servo_cur_angle[i] > servo_min_angle[i]) { --servo_cur_angle[i]; } if (i == CLAW_SERVO_INDEX) { servo_cur_angle[i] = CLAW_OPEN_ANGLE; } } else if (joystick_value[i] < JOYSTICK_MIN_THRESH) { joy_changed = true ; if (servo_cur_angle[i] < servo_max_angle[i]) { ++servo_cur_angle[i]; } if (i == CLAW_SERVO_INDEX) { servo_cur_angle[i] = CLAW_CLOSE_ANGLE; } } } Serial.println("" ); if (true == joy_changed) { for (int i = 0 ; i < SERVOS; i++) { #if 1 Serial.print("servo[" ); Serial.print(i); Serial.print("]=" ); Serial.print(servo_cur_angle[i]); Serial.print(", " ); #endif arm_servos[i].write(servo_cur_angle[i]); } Serial.println("" ); } delay(20 ); } void move_by_bluetooth () { bool joy_changed = false ; if (mySerial.available()) { char inByte = mySerial.read(); Serial.print(inByte); if ((inByte != (char )0xFF ) && (inByte != (char )0x0A ) && (inByte != (char )0x0D )) { joy_changed = true ; switch (inByte) { case 'a' : { if (servo_cur_angle[0 ] < servo_max_angle[0 ]) { ++servo_cur_angle[0 ]; } break ; } case 'b' : { if (servo_cur_angle[0 ] > servo_min_angle[0 ]) { --servo_cur_angle[0 ]; } break ; } case 'c' : { if (servo_cur_angle[1 ] < servo_max_angle[1 ]) { ++servo_cur_angle[1 ]; } break ; } case 'd' : { if (servo_cur_angle[1 ] > servo_min_angle[1 ]) { --servo_cur_angle[1 ]; } break ; } case 'e' : { servo_cur_angle[3 ] = CLAW_CLOSE_ANGLE; break ; } case 'f' : { servo_cur_angle[3 ] = CLAW_OPEN_ANGLE; break ; } case 'h' : { if (servo_cur_angle[2 ] > servo_min_angle[2 ]) { --servo_cur_angle[2 ]; } break ; } case 'g' : { if (servo_cur_angle[2 ] < servo_max_angle[2 ]) { ++servo_cur_angle[2 ]; } break ; } default : { joy_changed = false ; break ; } } } Serial.println("" ); } if (true == joy_changed) { for (int i = 0 ; i < SERVOS; i++) { #if 1 Serial.print("servo[" ); Serial.print(i); Serial.print("]=" ); Serial.print(servo_cur_angle[i]); Serial.print(", " ); #endif arm_servos[i].write(servo_cur_angle[i]); } Serial.println("" ); } } void move_from_to (int *action_from, int *action_to) { int max_angle = 0 ; int max_steps = 0 ; float step_angle[SERVOS]; adjust_speed(); max_angle = max(max(abs (action_to[0 ] - action_from[0 ]), abs (action_to[1 ] - action_from[1 ])), abs (action_to[2 ] - action_from[2 ])); max_steps = max_angle / demo_speed; max_steps = max_steps < 1 ? 1 : max_steps; for (int i = 0 ; i < CLAW_SERVO_INDEX; i++) { step_angle[i] = float (action_to[i] - action_from[i]) / float (max_steps); } for (int j = 1 ; j <= max_steps; j++) { for (int i = 0 ; i < CLAW_SERVO_INDEX; i++) { int new_angle = action_from[i] + j * step_angle[i]; if ((new_angle < servo_min_angle[i]) || (new_angle > servo_max_angle[i])) { continue ; } arm_servos[i].write(new_angle); } delay(20 ); } arm_servos[CLAW_SERVO_INDEX].write(action_to[CLAW_SERVO_INDEX]); delay(20 ); } void play_demo () { int counts = sizeof (demo_actions) / (SERVOS * sizeof (int )); Serial.print("demo_actions counts = " ); Serial.println(counts); move_from_to(servo_init_angle, demo_actions[0 ]); while (1 ) { for (int i = 0 ; i < counts - 1 ; i++) { if (is_btn_pressed()) { learning_mode = false ; repeat_mode = false ; play_demo_mode = false ; return ; } move_from_to(demo_actions[i], demo_actions[i + 1 ]); } } } void adjust_speed () { for (int i = 0 ; i < SERVOS; i++) { if (analogRead(i) > JOYSTICK_MAX_THRESH) demo_speed++; if (analogRead(i) < JOYSTICK_MIN_THRESH) demo_speed--; } demo_speed = demo_speed < 1 ? 1 : demo_speed; demo_speed = demo_speed > MAXSPEED ? MAXSPEED : demo_speed; } bool is_btn_pressed () { if ((false == has_btn_been_pressed) && ((0 == digitalRead(JOYSTICK_RIGHT_BUTTON)) || (0 == digitalRead(JOYSTICK_LEFT_BUTTON)))) { has_btn_been_pressed = true ; return true ; } else if ((1 == digitalRead(JOYSTICK_RIGHT_BUTTON)) && (1 == digitalRead(JOYSTICK_LEFT_BUTTON))) { has_btn_been_pressed = false ; } return false ; }
机械臂使用的是arduino生态,主要使用了arduinoUNO这个板子,同时也加装了蓝牙模块,代码封装函数和stm32略有不同。
2.8滤波器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void updateFilter (int16_t * buffer, int16_t newValue) { for (int i = FILTER_SIZE - 1 ; i > 0 ; i--) { buffer[i] = buffer[i - 1 ]; } buffer[0 ] = newValue; } int16_t getFilteredValue (int16_t * buffer) { int32_t sum = 0 ; for (int i = 0 ; i < FILTER_SIZE; i++) { sum += buffer[i]; } return sum / FILTER_SIZE; }
加入滤波器相关程序的考虑如下:
1.减少控制器的灵敏度: 滤波器可以减少PID控制器对高频噪声和干扰的敏感度。这有助于防止PID控制器对系统中不必要的快速变化作出响应,从而提高系统的稳定性。
2.滤波测量信号: 在PID控制中,测量信号可能受到噪声的影响。通过对测量信号应用滤波器,可以改善系统对实际状态的感知,提高控制性能。
3.避免控制器振荡: 滤波器有助于减小系统中可能引起控制器振荡的高频成分。这有助于防止控制器在某些条件下产生不稳定的振荡。
后来经过测试证明,我们的设计是有很大效果的,极大程度上降低了红外对管物种多样性的阴间影响
3. 总结及反思 3.1 软件方面 由于缺乏经验和想法,在最开始的直接控制上浪费了很多时间(and金钱),当代码完成后,可以发现很多封装其实是没有必要的,而且会降低循迹的反应速度和稳定性。 在封装控制函数的过程中出现了“各自为政的现象”,造成了大量的冗余。直到后来才发现有些参数几乎是可以复用的,完全可以通过定义少数的宏来节省大量空间,并增强代码的可读性,提高运行效率。 我们在最终的代码里尝试定义了一个类似于摩擦系数的宏,所有的参数可以根据这个宏计算来调节,理论上只要知道比赛场地的路况等信息就可以一键调节所有的速度、角度等参数,从而节省了大量的调参时间。但是在最终的结果中我们发现这种定义方式并不足以满足各种复杂情况,主要因素如下: 1.各个电机和红外对管的性能不同,统一调控会导致系统整体的兼容性和稳定性下降。
2.路况复杂的情况下,盲目地追求减少调参时间是不可取的。
因此,虽然宏的使用是一个很好的尝试,但在实践中,我们也意识到需要更为灵活和细致的参数调整,以适应不同的实际情况。这一过程是一个不断优化和润色代码的过程,通过反复实验和调整,逐步找到最优的参数配置。这也是在机器人控制领域中常见的挑战之一,需要不断学习和改进。
3.2 硬件方面 由于对新生板的使用不够熟悉,我们在参赛前最后一天先后损坏了数块板子的电源等模块,为正式比赛优化代码造成了很恶劣的影响。 由于对比赛规则的不熟悉,在选购机械臂时出现了严重失误,机械臂的抬升高度不足以支持能量块的投放,故在正式比赛时我们放弃了给机械臂通电,避免电能的无效浪费。 面对IO口稀少的情况,我们使用的焊接杜邦线的骚操作,我们承认这一行为给我们减少了很多麻烦,但是电流的减弱也曾一度让我们的电机面临驱动力不足等问题,好在可以通过更改PWM占空比缓解这一问题。 3.3 工程经验方面 工程经验的缺少使我们分工不够明确,效率不够高效。好在大家目标一致,最终较为圆满地完成了部分任务,离不开所有队员的倾力合作和集思广益。 4.感悟 肝了一个月,这是大上学以来第一次独立做出可使用的工程(虽然垃圾得一批 ),学到了很多以前自认为要大三甚至大四才能碰到的知识,像L298电驱,PWM波占空比的调节,PID控制算法,各种路口判断的设计,在现场实地修(乱)改参数等,碰到了各种各样逆天且玄学的问题,such as芯片的神奇短路,电源模块莫名暴毙,ADC接口的梦幻联动,(至今工位上还摆放着四块待救治的板子)。——特此衷心感谢硬件陈学长的力挽狂澜
与此同时明显感觉到C语言的能力得到了大幅度提升(尤其时造轮子的能力仿佛登峰造极),还练就了一身从屎里找吃的硬能力。
本篇博客就当成一个里程碑吧,纪念这疯狂熬夜的一个月(以及未来将要熬的无数个月),同时感谢718的各位学长(不)厌其烦的讲解和帮助。
外链:https://www.yuque.com/quanzhi-ndfvt/azx1dc/udzvfrcvol51mwr4?singleDoc# 《2023校内赛技术报告》