ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

平衡车项目复盘

平衡车项目复盘 对平衡车项目进行复盘总结项目设计思路、遇到问题的解决办法、以及改进方法还有调试记录等。1、电源电压读取第一步通过adc采集电压。第二步将采集到的电压进行处理根据电压的大小驱动3颗led灯闪烁用来指示电池电量。细节1ADC1的频率不超过14Mhz。上电时启动文件自动把Sys频率开到72Mhz再过APB1,1分频再经过6分频得到12Mhz给adc采样。细节2信号从pb0输入通道8.adc采样分成常规序列和注入序列用tim2的trgo触发注入序列存在4个jdr寄存器里这里用jdr1。eoc中断代表常规序列结束jeoc中断代表注入序列结束这里是注入序列jeoc。初始化函数里分成TIM2的trgo初始化adc的初始化还有led灯的初始化。细节3adc1和adc2的中断相应函数是共用的。up的代码是在中断里计算vbat的实际值其实可以优化一下在进程函数里面进行计算。这里可以算做一个优化点。细节4int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);main.c里面设置了中断优先级这里的意思是全部用作子优先级没有抢占优先级ADC 中断不能抢占任何别的中断别的中断也抢不了 ADC 中断 如果两个中断同时到来子优先级数字越小越优先响应 中断之间只能够「串行执行」不存在嵌套中断。细节5将led灯显示模块设定成进程函数用系统时间来完成延时不使用延迟函数。细节6由于使用了static关键字进行了封装无法从app_bat.h中读出电压的数据所以在app_bat.c文件中加入一个读取函数App_Bat_Get同时vbat变量还用volatile关键字必须从内存中重新加载防止中断程序而改变。语法代码细节见源码还有源文章。项目复盘仅用于理清思路。2、串口通信模块细节1//#1.对PA2和PA3引脚进行初始化PA2是复用给usart2用的PA2-Tx-AF-PP PA3-Rx-IPU。ipu输入上拉因为串口标准电平空闲状态总线应该是高电平Mark。初始化完毕后调用编写好的uart驱动程序通过串口向电脑发送电压数据。3、电机调速原理及其代码1、对于电机来说我么只需要借助空载最大转速加在电枢两端电压/反电动势常数即可所以我们只需要简单改变加在电枢两端电压的就行如图可以通过改变pwm的占空比改变Ua。2、用h桥的结构来驱动pwm波的产生h桥芯片结构如图STBY相当于芯片的总开关同时还用两颗led灯指示芯片工作状态。AIN1和AIN2控制电机旋转方向AIN1输入高电压AIN2输入低电压电机正转 AIN1输入低电压AIN2输入高电压电机反转。左右两组电机同理。设置按钮的初始化需要调用按钮驱动编写按钮初始化函数和按钮的进程函数还编写了按钮的回调函数调用对pwm控制stby引脚的函数。对于pwm相关的文件写控制pwm的cmd函数和设置左右电机占空比的函数以及初始化函数。初始化函数里面需要对左电机右电机连接芯片的引脚以及连接stby的引脚进行初始化。void App_PWM_Init(void); void App_PWM_Cmd(uint8_t on); void App_PWM_Set_L(float Duty); void App_PWM_Set_R(float Duty);STBY_Pin_Init();//STBY引脚的初始化 Motor_L_Init();//左电机的初始化 Motor_R_Init();//右电机的初始化三个引脚的初始化就是对相关GPIO口的设置。以及左右电机需要用定时器驱动对定时器的相关设置。两个电机分别用不同的定时器驱动产生pwm信号。细节1任何结构体创建都需要赋一个初值0否则容易出现问题。细节2对PWM的相关参数进行设置占空比等于电枢两端加的电压除以VBAT分辨率设置较高需要设置1000级可调保证调节的精度。下面是对定时器时基单元的设置。左边的输入捕获是输入一个信号测量占空比之类的右边的输出比较是用来产生pwm信号的本项目需要用到右边的部分。综合起来就是下面的结构。对于预分频器由于频率需要尽可能高填01分频保持最大频率72MHz对于自动重装寄存器因为设置1000级可调填999让ccr的调节范围变成0——1000重复寄存器取0即可。细节3由于电感的影响电流不突变电压也不突变会产生一定的纹波上下波动的电压增大pwm频率可以减小纹波所以周期越小越好。细节4输出比较有8种模式本项目使用的是PWM1模式当CNTCCR时输出高电平当CNTCCR时输出低电平。通过改变CCR的值就可以控制占空比。也就是说ccr就是一个比较的基准。对于模式选择选择上面的正常输出即可极性选择方面选择正极性闭合通道使能开关和主输出使能开关MOE就能通过CH1通道正常输出PWM波了。细节5根据我们设置的占空比duty的值首先要限制占空比在-100%~100%的范围内才有意义然后根据duty的正负判断旋转的方向控制相应的引脚输出高低电平然后根据duty的值反推出ccr的值ccrDuty/100.0f*1000这里乘1000是因为之前设置了1000级可调。有必要在开头加上保护若占空比超过100%容易引发芯片损坏判断占空比会不会超出范围若超出强制将占空比限制在边界上。// 占空比保护限制在±100超出强制拉到边界保护芯片 if(Duty 100.0f) Duty 100.0f; else if(Duty -100.0f) Duty -100.0f;细节6测试完成后发现一个问题两个轮胎的旋转方向不一致这是由于轮胎对称导致的相应地我们需要改变一下其中一个电机的正反转逻辑下面是左电机的判断逻辑。一边轮胎的顺时针旋转对于另一个轮胎就是逆时针旋转所以有必要修改成一致的逻辑。4、编码器 位置测量和电机测速1位置测量编码器的原理就是霍尔效应不具体解释了。磁环结构如下其中A相和B相的产生波形存在差异A相超前B相四分之一个周期。根据AB相当前电压判断电机正反转同时根据脉冲的个数判断转过的圈数。将AB相配置成EXTI中断的上升沿触发。上图是EXTI的结构图左边的AFIO用于选择GPIOABCD四个来源EXTI LIne0负责GPIOX_Pin_0的中断信号以此类推0—15都是这个规则剩下的16,17,18,19是专门负责其他模块的中断。根据这个规则配置AFIO和EXTI。细节1这些变量在声明的时候都应该加上volatile关键字因为都是在中断里会被修改volatile static int64_t encoder_L0;//volatile:告诉编译器/CPU此变量会被意外修改禁止优化缓存禁止指令重排每次都从内存读取最新值 volatile static int64_t encoder_R0;//上面是左电机的编码器的值这是右电机的编码器的值。 volatile static int8_t direction_l1;//左电机旋转的方向1 - 正转-1 - 反转 volatile static int8_t direction_r1;//右电机旋转的方向1 - 正转-1 - 反转 volatile static uint64_t t0_l0,t1_l0,t0_r0,t1_r0;//左右电机编码器发生的时间单位us对左右电机进行初始化static void Encoder_L_Init(void); static void Encoder_R_Init(void);将左电机的AB相对应引脚配置成输入上拉模式右电机同理。细节2我们需要对PB3,4和PB14,15进行初始化。其中PB3,4引脚的默认功能如下表默认是JTAG的下载调试接口所以我们需要对他们进行重映射使他们变成能用的引脚。RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);上面这段话需要加载PB3,4GPIO初始化之前。细节3这里其实可以用TIM的编码器模式但是受到硬件限制定时器数量紧张所以没有这样做。效果应该是一样的。细节4NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority0;//在main.c里没有分配抢占优先级这里填什么都不影响。 NVIC_InitStruct.NVIC_IRQChannelSubPriority0;//编码器相应很重要分配一个高的子优先级关于优先级子优先级要高前面没有分配抢占优先级所以无所谓。细节5其中左电机A相的EXTI_14需要额外加一条判断因为中断响应函数是15_10通用的判断一下是否有EXTI_14产生。在编写具体内容前先将中断相应标志位清0避免重复触发之后的逻辑判断参考之前的表格对编码器的值进行相应的增减。另外A相是否是上升沿还是下降沿由于中断相应函数对于检测上升沿和下降沿有延迟所以只需要判断A相是高电平还是低电平就可以间接判断出是上升沿还是下降沿。if(EXTI_GetFlagStatus(EXTI_Line14)SET) { EXTI_ClearFlag(EXTI_Line14);//对中断标志位清零2计算减速比由于电机转速很快所以需要减速箱减小转速增大扭矩使平衡车获得足够的动力。下面是平衡车电机部分的结构和up主对减速比的大致计算分析。上图反映测是等式第一个分式是刚才的减速比计算公式22则是因为编码器磁环有11对NS极每转过一个对极会产生一个脉冲前面的捕获双边沿也能说明这一点那么轮胎转一周会带动磁环转449个对极和447非常接近存在误差但是这并不是旋转的角度我们需要进行换算。细节1需要将旋转过的相数换算成角度// //简介读取左轮胎旋转的角度单位度 // float App_Encoder_GetPos_L(void) { return encoder_L/22.0f/(30613.0f/1500.0f)*360.0f; }在获取左右电机编码器的函数内重新编写计算公式首先除22.0f的代表计算编码器磁环旋转的圈数再除以减速比代表轮胎旋转的圈数最后乘上360就能正确地表示轮胎转过的角度。其中每一个数值后都加上.f用来表示浮点数的精确计算。同时对编程接口的返回值类型进行改写改成float类型,注意头文件和主文件同步改写。还有测试文件的相关参数也要改变。3M法测速M法固定时间ΔtM法是在一段很小的时间内计算转过的线数假设这段时间内的速度不变近似当成瞬时速度。经过公式换算得到转速。误差分析起点和终点的时间是由单片机决定电机带动编码器的示数变化和单片机时间不会完全同步如图Δt的起点和终点和编码器变化的边沿并不会完全的重合就会造成误差。假设第一个和最后一个的误差都达到最大的一个阶梯合起来就是两个阶梯除以编码器变化的总量M就得出了误差占整个测量结果的比例。电机的速度越快Δt时间内增加的M越大当M的值越大的时候误差所占的比例就越小所以M法适合高速测量。T法是在单位线数内转过的时间T角速度记为1/T。误差来源方面单片机使用输入捕获计算T的长度假设实际长度是10.5us但是单片机的分辨率只有1us那么测出的结果可能是10us因此产生了误差误差所占的比例就是0.5us/T。如果电机转速更慢上升一个阶梯的时间更长,即T更大那么误差占的比例就会更小所以T法适合低速测量。细节1用编码器输出的是方波信号得到的是阶梯状的角度变化曲线无法求导得出瞬时角速度如图测量代码如下float pos_lApp_Encoder_GetPos_L(); float pos_rApp_Encoder_GetPos_R(); float M_lpos_l-last_pos_l; float M_rpos_r-last_pos_r; float Omega_lM_l/0.001f; float Omega_rM_r/0.001f;细节2测量波形如下这张图解释了原因每1ms采样会在每一个阶梯上采集多次只有当阶梯发生阶跃时M的值才会发生变化从0变成1没有阶跃时M的值都是0就会出现实验结果中的波形。说明我们的项目确实不太适合M法测速。4T法测速对于正转和反转计算的结果需要用正负表示因此当检测到encoder的值上升时把direction设置成正1检测到下降时把direction设置成负1那么方向就可以统一用direction来表示。在encoder和encoder--的位置同步对direction进行更新增加就是1减少就是-1。接着定义两个时间节点初始量为0用于计算时间间隔volatile static uint64_t t0_l0,t1_l0,t0_r0,t1_r0;//左右电机编码器发生的时间单位us问题1速度不归0读取当前时间的速度需要利用上一个周期的速度进行计算所以总是滞后的。遇到轮胎突然归零的情况now处于上升阶梯的中心后面的上升沿再也不触发了导致值一直停留在上一个周期。解决的核心思想是基于Omega的值不会突变的原理反复对比now-t0即当前时间所在的阶梯和t0-t1即上一个阶梯。如果现在的阶梯小于上一阶梯基于不突变原理那么速度和上一周期应该是相近的所以取T为上一个周期的值如果现在的阶梯大于上一个阶梯也是基于不突变原理为了满足这一前提下一个上升沿应该尽快到来所以就把当前的时间记为T、表现在代码上也就是对两段时间取更大的值。float App_Encoder_Getspeed_L(void) { uint64_t nowGetUs(); float T0; if(t0_l-t1_lnow-t0_l) { T(t0_l-t1_l)*1.0e-6f; } else { T(now-t0_l)*1.0e-6f; } return direction_l/T/22.0f/(30613.0f/1500.0f)*360.0f;//微秒 -10e-6 }轮也能正常归0说明也没有问题。为什么这样写就能解决速度不归0的问题呢解释如下核心公式就一句话direction_l/T分母在下面分子在上面。车子转的时候 编码器不停地发脉冲中断里一直更新 T脉冲来了 → 记录时间 → T 很小 → 分母小 → 角速度大车子停了以后脉冲没了T 不再更新。但是 now当前时间一直在走脉冲没来 → T 从上次脉冲周期逐渐变成等了多久 → T 越来越大分母越来越大分子还是 ±1分数值越来越小 → 角速度趋近于 0。我在代码里又加入了if(T 1.0f) return 0.0f; // T超过100ms就认为静止直接返回0 return direction_l/T/22.0f/(30613.0f/1500.0f)*360.0f;//微秒 -10e-6这可以解决速度归0的时候角速度趋于0的问题。问题2穿越边界的波形有点乱图中三个红圈是换向点异常的地方。如图是把一个换向过程的相关参数以及计算出的Omega的值绘制成曲线的示意图发现确实在换向点8出现了异常的突起。根据Omega的计算公式在8点处的换向阶梯非常小也就是T很小导致计算结果异常大。代码实现部分以左轮为例在对direction赋值前加入对前一时刻的判断如果检测到换向了那么赋值-2作为区分。接着在读取函数前先对direction值做判断强制令Omega为0实现换向异常突起的抹平。float App_Encoder_Getspeed_L(void) { if(direction_l2 ||direction_l-2) { return 0.0f; } uint64_t nowGetUs(); float T0; if(t0_l-t1_lnow-t0_l) { T(t0_l-t1_l)*1.0e-6f; } else { T(now-t0_l)*1.0e-6f; } if(T 1.0f) return 0.0f; // T超过100ms就认为静止直接返回0 return direction_l/T/22.0f/(30613.0f/1500.0f)*360.0f;//微秒 -10e-6 }问题2拓展这里弹幕引出了一个讨论为什么direction不用直接变成0而是更加繁琐的-2.理论上0更加简介但是两者其实各有优势换向这一拍的速度本来就算不准所以关键不是算多少而是这一拍之后你还能不能知道我之前在往哪转。用 0的话这一拍就失忆了再下一拍你分不清我刚换向还是车真的停着没动用 ±2相当于在方向值里多塞了一个我刚换向的标记——2 表示现在正转、刚换过来-2 表示现在反转、刚换过来。多写一行 if(abs2) return 0 的代价很小但换来的是任何时候看一眼 direction 就知道方向状态不用再猜。对于后续PID 控制、里程累计、状态上报等额外功能更加友好。我个人倾向于-2的写法。问题3有毛刺可能是硬件问题或者中断相应的问题中断程序打断了常规程序对t0和t1更新了可能会出现t0跑到now的右边的情况对这种具体的情况进行分析。max中按照现实世界的角度看前面是-10后面是110貌似没有问题但是在程序里前面是无符号64位整数没办法表示负数所以发生溢出变成一个很大的正数那么max就取到一个很大的正数导致w的值非常小出现曲线图中的骤降的情况。float App_Encoder_Getspeed_L(void) { __disable_irq();//关闭单片机的总中断 int8_t direction_cpy direction_l; uint64_t t0_l_cpy t0_l; uint64_t t1_l_cpy t1_l; __enable_irq();//开启单片机的总中断 if(direction_cpy2 ||direction_cpy-2) { return 0.0f; } uint64_t nowGetUs(); float T0; if(t0_l_cpy-t1_l_cpynow-t0_l_cpy) { T(t0_l_cpy-t1_l_cpy)*1.0e-6f; } else { T(now-t0_l_cpy)*1.0e-6f; } if(T 1.0f) return 0.0f; // T超过100ms就认为静止直接返回0 return direction_cpy/T/22.0f/(30613.0f/1500.0f)*360.0f;//微秒 -10e-6 }以左轮胎为例先声明三个用于拷贝的变量临时保存三个数据。之后立刻恢复中断再用保存到的值进行运算避免异常数据的产生。我查了一下关中断和打开中断的总时长非常短不到100ns远比编码器的脉冲间隔几十微秒到几毫秒小很多不会有影响且中断是挂起机制不会丢。问题3拓展仍然有毛刺现在的曲线非常光滑但是实际上在我对数据进行保护后仍然产生了很多毛刺当时的截图没保存。这个现象是我的项目里的个例在我烧录老师的示例后发现示例的图像没有毛刺。于是我用AI进行分析比对文件的差异发现了如下问题以下是AI的原话 ---文件1my_lib/delay.c第100-119行这是模板工程自带的微秒延时驱动你之前没动过。改之前uint64_t GetUs(void){...SysTick-CTRL ~SysTick_CTRL_COUNTFLAG; // 清除COUNTFLAGtick ulTicks; // 第1步读毫秒mini_tick SysTick-VAL; // 第2步读微秒余数while(SysTick-CTRL SysTick_CTRL_COUNTFLAG) // 事后补救{mini_tick SysTick-VAL;tick ulTicks;}...}问题第1步和第2步之间不设防SysTick 可能在中间溢出。比如读完 tick100 后溢出、mini_tick读到新周期的值拼出来的时间就跳了一毫秒。后面的 while 补救也不是每次都管用。改之后__disable_irq(); // 关门谁也别打断while(1){tick ulTicks;mini_tick SysTick-VAL;if(SysTick-CTRL SysTick_CTRL_COUNTFLAG)ulTicks; // 溢出自己处理掉elsebreak;}__enable_irq(); // 开门关门 → 一口气读完 → 开门。读到的一定是同一个时刻。---文件2user/stm32f10x_it.c第161行和第163-169行这也是模板工程的中断服务文件。改之前extern __IO uint64_t ulTicks; // ← 声明为8字节void SysTick_Handler(void){ulTicks; // ← 但实际只有4字节}改之后extern __IO uint32_t ulTicks; // ← 修正为4字节void SysTick_Handler(void){if(SysTick-CTRL SysTick_CTRL_COUNTFLAG) // ← 确认是真的溢出{ulTicks;}}两个问题uint64_t 声明导致 ISR 每次写 8 字节踩踏相邻内存没有 COUNTFLAG检查可能在调试暂停等情况下误触发。都是模板工程的默认写法和你实际分配的变量不匹配。---这两处都不是你写的业务代码是模板工程底层驱动自带的问题只是在编码器高频中断 实时速度计算的场景下被暴露出来了。经过我的了解第一个问题大致是GetUs函数在读取时间的时候时钟的时间还在走导致时间读取不准确了。正确的做法就是先暂停时间再读取。第二个问题大致是delay.c里对ulTicks声明是4个字节但是在stm32f10x_it.c误说成8个字节会导致SysTick中断每次写8个字节多余的4个字节导致一些错误产生了毛刺。虽然具体原理我还不是很清楚但是借助AI工具还是帮我解决了一些问题。5.MPU6050本项目使用手动解算欧拉角并且采用互补滤波算法。1原理部分MPU6050获取数据原理略。2寄存器读写部分细节1打开数据手册 找到两个引脚的功能默认是TIM4的通道3和通道4使用重映射功能才可以复用成SCL和SDA。先进行重映射再配置PB8和PB9为输出开漏模式。接着设置I2C的初始化参数。细节2复用代码里的AFIO和EXTI前面的选择来源GPIOABCD。是一个外设注意区分细节3I2C的SCL和SDA要设置成开漏输出不能主动输出高低电平否则会造成信号混乱外接上拉电阻来拉高电平。根据已有驱动函数再次封装读函数和写函数#includei2c.h // //简介向寄存器写值 //参数 reg - 要写入寄存器的地址 value - 要写入的值 // static void reg_Write(uint8_t reg, uint8_t value) { uint8_t bytesToSend[]{reg,value}; My_I2C_SendBytes(I2C1,0xd0,bytesToSend,2); } // //简介向寄存器读值 //参数 reg - 要读取寄存器的地址 返回值表示读取到的值 // static uint8_t reg_Read(uint8_t reg) { uint8_t regValue; My_I2C_SendBytes(I2C1,0xd0,reg,1);//发送寄存器的地址 My_I2C_ReceiveBytes(I2C1,0xd0,regValue,1);//读取一个字节 return regValue; }细节4发送0xd0地址是数据手册里的规定解释略。修改寄存器量程的解释略核心是根据数据手册像寄存器写不同的数据。细节5以ax为例先读取高八位数据再读取第八位数据最后合并成带符号的16位数据。细节6得到原始数据后因为是带符号的16位整数范围从-32768——32768要进行单位转换。比如对加速度进行转换实际量程是-2g那么计算出的转换因子就是2/327686.1035e-5需要把原始数据都乘上这个数。角速度同理转换因子是2000/327686.1035e-2。温度值的转换用到数据手册的公式up主提到了实际上使用的芯片型号并不是6050而是6500其他地方区别不大但是在温度计计算上有区别。axax_raw*6.1035e-5f; ayay_raw*6.1035e-5f; azaz_raw*6.1035e-5f; temperaturetemperature_raw/333.87f21.0f;//MPU6500的计算公式区别于MPU6050 gxgx_raw*6.1035e-2f; gygy_raw*6.1035e-2f; gzgz_raw*6.1035e-2f;3解算欧拉角互补滤波陀螺仪部分欧拉角由三个角组成偏航角俯仰角翻滚角。细节1其中翻滚角因为芯片摆放的缘故变化的方向和翻滚角规定的方向刚好相反所以是减号。细节2对于裸机多任务模型 我们需要创建多个进程函数上述的计算过程也需要写成进程函数的形式通过对系统时间的比较来达到延时的目的而不是使用阻塞型的delay函数。创建一个陀螺仪解算欧拉角的进程函数根据上面的公式写代码void App_MPU6050_Proc(void) { //通过陀螺仪的测量结果计算欧拉角 App_MPU6050_Update();//更新传感器的值 PERIODIC(5)//进程函数每5s执行一次 // static uint32_t nxt0;//下次程序运行的时间 // // if(GetTick()nxt)return; yawyawgz*0.005; pitchpitchgx*0.005; rollroll-gy*0.005; }细节3注释掉的部分是一个判断系统时间来达到延时的效果下面把他替换成一个宏定义方便使用static void Usart2_Proc(void) { PERIODIC(10) float yawApp_MPU6050_Getyaw(); float pitchApp_MPU6050_Getpitch(); float rollApp_MPU6050_Getroll(); My_USART_Printf(USART2,%f,%f,%f\n,yaw,pitch,roll); }4加速度计部分细节1对重力进行分解就可以求出分力进而求出加速度。注意这里一般不计算偏航角因为重力始终垂直于偏航角的平面无法分解。细节2首先计算俯仰角原理图如下其中z轴上的重力加速度我们会认为是负方向理应是-g但是实际情况是MPU6050测出来的是g因为弹簧被拉伸的方向才是MPU6050加速度计的加速度方向。细节3但是这里的arctan涉及到了取值范围的问题arctanx这个函数在c语言里只能表示-90°-90°的角度如果是-135°tan-135°的值也是1但是在函数里只能表示成45°所以需要用atan2这个函数能把范围扩大到-180°-180°。虽然我认为这个小车项目里俯仰角的角度就是-90°-90°因为小车不可能倒过来但是用于飞行器之类的项目时是可能出现超过这个范围的情况的所以现在学习这个知识点仍然是有必要的。两个函数具体的区别在于atanx会丢失边长的符号信息还是以-135°举例对边和邻边都是负二分之根号二两者相除就变成1可能会错误识别成45°。atan2需要输入原始两个边长就可以准确判断象限了。另外还需要考虑到单位换算问题弧度和角度的互换这个知识点是高中数学的内容就不赘述了。接着算翻滚角示意图如下。另外正常的平地行驶也是用不到这个角的也是斜坡或者无人机会用上。5互补滤波原理不赘述了6快速三角函数首先介绍一下快速三角函数的背景因为普通的三角函数结算非常耗时间会影响时间片的厚度。本质是用空间换时间的操作。这些函数存在一个单独的文件。将原来的函数替换成快速三角函数即可。6.PID本项目采用的是建模调参不同于其他项目的调参。建立小车的物理模型然后写出传递函数最后画出传递函数的伯德图决定加在上面的PID参数数值最后用程序实现。第一部分 控制系统的结构和关键指标第二部分 电机的电学方程第三部分 电机的机械方程这就是两个方程电机的电学方程和机械方程。第四部分 拉普拉斯建模电学方程的拉普拉斯换算。机械方程的拉普拉斯换算。第五部分 结构图 化简此处忽略Tl为了计算方便再对两个方程联立求解传递函数消去Ias就可以得到Uas和ws的关系。结构图的解法见原记录。第六部分 电机参数测量这部分是测量基本参数的涉及大量公式可以不掌握直接获取参数即可。第七部分 波特图根据上节课跳过的数据把传递函数表示出来以后绘制电机模型的波特图。第八部分 PID参数整定理想的波特图的状况如上。低频段k越大w1对应的值越大越陡稳态误差越小。中频段尽量以-20db穿过且穿越的范围尽可能大范围越大超调量越小。wc也就是穿过x轴的交点的值也是越大越好振荡越快越容易趋于稳定。高频段尽量低下降越快越好抗噪声能力越好。利用PID控制器 叠加在原有电机上改进电机的性能如图。在低频段不仅变陡了而且w1的位置上升了。在中频段wc的位置右移了振荡加快且中频段穿越x轴两边的部分尽可能相等了。在高频段因为原本性能不错所以没什么变动。想画出PID的波特图步骤如下将PID传递函数画出波特图各种不同的波特图如图。设计电机的PID此处的数据为估算大致让改进后的系统达到要求而已。对于小车来说有3个现实缺陷导致电机控制非线性1.电机控制轮胎存在减速差因为变速齿轮存在间隙。2.编码器测速波形并非理想不光滑。3.启动扭矩很大。综上所述增益K需要减小一些这些数字都是up经过实验得出的。第九部分 PID控制器代码编写创建结构体的代码略包括修改设定值的函数。创建一个计算参数用于执行一次PID计算里面的参数都需要经过运算得出。将PID的比例积分微分项累积就可以得出结果了。但是积分和微分比较难实现需要借助误差的图像。注意在执行完一次计算后需要迭代更新下一次计算的值。迭代计算的函数略。第十部分 PID测试上图是PID控制的思路其中compute函数上一个部分已经完成编写了。把所有的电机调速代码写在app_motor.h和app_motor.c里面。依照先前的思路先对左右电机进行初始化再反复执行电机调速的进程函数。初始化和进程函数略其中设置电压的部分之前的学习中是通过设置占空比 来间接设置的。上图是占空比和Ua的转换关系。最后需要给用户留一个设置左右电机转速的接口。接口函数略。第十一部分 PID改进改进的方面主要有以上四个方面输出限幅主要限制CO的值要根据被控对象的现实物理条件进行合理的限制比如Ua的输出值由于Vbat的限制控制0~100%的占空比范围在-8.4~8.4超过这个范围的都应该被强制拉到最高或者最低值。上图是代码实现的具体思路。先在结构体里定义这两个变量在初始化的时候设置好上下限图上表示无穷大和无穷小。再加入一个设置上下限的接口。在计算PID的函数里再对输出值进行判断进行限幅。具体的代码呈现和上图代码差不多。积分限幅方面如下在compute函数里加入输出限幅和积分限幅。具体略PID复位就是通过小车上的按钮控制电机的开关时需要在小车重启的时候把相关参数复位避免残留数据对新的运行产生影响。并且在按下按钮的时候调用复位函数回到之前编写的按钮相关函数里面最后的改进首次运行时忽略积分和微分。第一次运行的时候只有积分项没有微分项和积分项所以要忽略。解决的办法是判断一下三个临时参数是不是0就知道是不是第一次运行了。7.倒立摆可以把小车看成倒立摆模型。小车往前倒让他向前加速就可以正常恢复后退同理。第一部分 串级PID因为θ点即绕x轴旋转的速度也就是gx图上出错了也是可以读取到的所以需要加在欧米伽和θ之间先通过欧米伽控制θ点在通过θ点间接控制θ。逐级整定内环作为外环的执行器。第二部分 电机闭环传递函数先计算最内环的传递函数细节1零点和极点非常接近斜率的增减很快被抵消同时很大的极点也可以忽略所以最终可以化简到上述结果。第三部分 倒立摆建模图上写反了1式子中的两个应该交换位置因为规定顺时针是正方向。化简过程略。第四部分 绘制系统结构框图现在需要找出欧米伽和x点点的关系。之后补齐θ点点和θ的关系也就是积一次分的关系从θ点到θ也只需要积分一次乘以s分之一就行了第五部分 倒立摆逆结算根据上一部分求解出的结构框图其中倒立摆的数学模型是带有三角函数的非线性模型是没办法求出传递函数的。我们可以通过逆结算交换输入和输出的位置。进而可以在单片机的部分加一个逆结算的结果抵消掉后面的倒立摆数学模型的物理影响。如下图的方法。将加入进去的传递函数的输出x点点和wref进行联立可以求出最左边的绿色框。最终图上的很多部分都可以相互抵消。第六部分 参数整定绘制波特图20lgkk1所以过1,0I型系统所以斜率是-20没有零点有一个极点40。画好的波特图如上。对于稳定性以-20db穿越横轴的范围比较宽没有问题。改进的地方可以让低频段斜率更陡一些然后和横轴的交点wc可以右移一点调节时间变短达到稳定的时间越快。可以加入如图所示的PID控制器叠加后的结果如绿线所示。可以加入如图所示的PID控制器叠加后的结果如绿线所示。推倒PID控制器 的表达式如图。因式分解过程用matlab过程略。内环整定如图具体过程略。接着整定外环的PID参数这里up为了不让系统的复杂度增加只使用了一个比例环节将曲线整体上移得到绿线。第七部分 编写代码1.为了区分单片机内部和物理世界的参数表达把单片机内部的θ点点和x点点都加上ref标志因为在单片机内部主要是设定值的计算。2.x点点的表达式的分子减号两边写反了。这个错误在前面已经说过了。具体不解释了。#ifndef APP_CONTROL_H #define APP_CONTROL_H #includestm32f10x.h void App_control_Init(void); void App_control_Proc(void); #endif#includeapp_control.h #includepid.h static PID_TypeDef pid_theta;//θ环的PID控制器 static PID_TypeDef pid_theta_dot;//θ点环的PID控制器 // //简介负责初始化平衡车系统 // void App_control_Init(void) { PID_Init(pid_theta,4.0f,0.0f,0.0f);//初始化θ环的PID PID_Init(pid_theta_dot,10.0f,10.0f,0.0f);//初始化θ点环的PID }编写pid文件调用之前的控制文件。分别初始化内环和外环。细节2接下去考虑输出限幅的问题对于控制器的输出在物理层面是有一定限制的估算一下两个输出的值θ点假设最快每秒转2圈就是4π弧度带单位正负。θ点点假设在0.1s内加速到最大的每秒两圈也就是4π弧度也就是40π带单位正负。单片机执行进程函数里面需要做大量的采集和计算up主告诉我们最合适的周而复始地执行进程函数的周期是5ms因为最短的传感器 采集时间是5ms所以遵循最短的时间来设置。这个周期的时长设置就是用到之前的PERIODIC的宏定义设置成5ms就行了。对于θ角我们希望他的设定值是0这样就能保持平衡。调用设置设定值的函数即可。然后根据MPU6050 传感器采集到的角度作为反馈计算两者的误差进行PID运算。我们直接在main.c里面对mpu6050进行初始化即可。代码见原文章。细节3这里的deltaT可以近似取5ms也可以实际测量一下时间需要在开头读取一下当前时间减去上次运行的时间再在最后更新一下上次的时间。8.速度环在上一个学习记录里能保持住小车的平衡之后小车会在原地不受控制的向前或者向后运动。这一部分我们要将给小车加上速度让小车跑起来。第一部分 绘制系统结构框图小车为什么会自主前进后退的原因如上。是实际重心和理想重心存在偏差这是没法避免的重力的分量会产生一个加速度所以小车始终会要向前或者向后跑来保持平衡。所以我们可以通过控制俯仰角来控制小车的加速和减速。把速度环的控制加在角度环的控制外面。只需要把θ和x点的关系表示出来就行了。细节1这样就可以把框图连接起来了出现了非线性 运算用之前的方法就是加入一个逆解运算抵消掉影响。需要把这个反解算的框图放在单片机内部实现第二部分 速度环悖论两种控制的控制量和被控量都是互相反过来的会产生问题w的计算原理如图。通过框图变换可以得出w的具体值。第三部分 计算闭环传递函数和参数整定上面的结构框图把相反部分抵消掉得到下面的框图。然后就可以在外面加一层速度环控制速度先得求出Gθs才能对PID的参数进行设置。蓝色框部分的传递函数已经求出来了。还原回去2次型过程略见原文章。细节1对于这个二阶振荡环节绘制波特图的方法如下以-20db的斜率穿过横轴穿越的范围宽稳定性好。低频段穿越的点wc1可以适当提高还可以让低频段的斜率更陡一些。高频段的衰减很大抗干扰能力强。第四部分 编写代码第一步先把速度环PID设置好初始化static PID_TypeDef pid_velocity;//速度环的PID控制器 void App_control_Init(void) { PID_Init(pid_velocity,10.0f,1.0f,0.0f);//初始化速度环的PID PID_LimitConfig(pid_velocity,0.5*g,-0.5*g);//将速度环的PID控制器输出限制在- 0.5g之间。 }接着设置速度的初始值先让小车保持静止为0。并且读取编码器 的值取左右轮胎的平均速度//#-1.改变速度环的设定值 PID_ChangeSP(pid_velocity,0.0f); //#-2.读取传感器的值 float omega (App_Encoder_Getspeed_L()App_Encoder_Getspeed_R())*0.5;之后按照上一个部分的思路减去w2的值//#-2.读取传感器的值 float omega (App_Encoder_Getspeed_L()App_Encoder_Getspeed_R())*0.5; float thetaApp_MPU6050_Getpitch()*0.0174533;//单位 rad,传感器采集到的是°需要进行单位换算 float theta_dotApp_MPU6050_GetGx()*0.0174533;//单位 rad,传感器采集到的是°需要进行单位换算 //#-3.计算速度环的反馈值x_dot float omega2-theta_dot * (lprw)/rw; float omega1omegaomega2;//此处应该是加 float x_dotomega1*rw; //#-4执行速度环的PID运算 float theta_refqatan(PID_Compute(pid_velocity,x_dot)/g); //#1.将外环的设定是SP设置为0 PID_ChangeSP(pid_theta,theta_ref);//上次是0.0f这里把传感器读取的值从2步移到了-2步为了方便读取数据。然后算出omega1的值乘上rw作为反馈值通过PID计算做三角函数运算得到θref的值当作倒立摆的设定值。写完以后up想进行演示但是发现小车转的飞快说明需要对上次运行的一些数据进行复位否则会出现问题。// //简介对控制系统进行复位 // void App_control_Reset(void) { //#1.复位暂存的值 Omega_ref 0.0f; lastTime0; //#2.复位PID控制器 PID_RESET(pid_velocity); PID_RESET(pid_theta); PID_RESET(pid_theta_dot); }细节1在计算轮胎转速的时候需要判断是不是第一次计算因为第一次计算的deltaT是无效的。//#7.计算轮胎转速 if(lastTime !0) { Omega_ref1.0f/rw*x_dot_dot_ref*deltaT; }在按下用户按钮的时候需要对控制器复位找到按钮相关的文件找到按钮按下的回调函数 加入复位函数。// //简介按键单击的回调函数 // static void OnUserKey_Clicked(uint8_t clicks) { if(clicks1) { App_control_Reset(); //翻转电机状态 if(pwm_on0) { pwm_on1; } else { pwm_on0; } App_motor_Cmd(pwm_on); } }9.遥控器第一部分 接受一行字符串这是手机端的app界面这个app由up主开发具体我没研究过。有控制前进和后退左右的摇杆。通过触发中断实现接收同时还有三个数组用于存储数据。接收一个字符触发一次中断放到中断程序的数组里。在接收结束的地方有\n在中断数组里存储\0将中断数组里的数据转存到暂存数组里面并且将lineReceiveFlag标志位置1。进程函数里不断查询标志位当标志位指示数组里有数据了就把数据转存到进程函数的数组里。写一个初始化函数和进程函数我们要对蓝牙芯片进行操作初始化USART3并且打开中断。连接到单片机的PB10和PB11引脚10-tx11-rx。接着声明3个数组。第一步当接受到的字符不是\n时接收一个字符用游标来移动存储的位置。接着编写中断相应函数的内容和进程函数的内容把数据搬到中转数组里面。第二部分 指令解析strnacase指令比较n个字符是否相等并且忽略大小写区别和strcmp有区别先判断开头是否是move空格再对后面的指令进行解析对cmdline里面的数据进行解析move空格是固定格式后面的%d %d是需要解析的内容把解析到的内容放到turnSpeed和moveSpeed里面解析成功一个就返回1解析成功两个就返回2。细节1这个函数包含在stdio.h的头文件里面使用前需要先引用头文件。第三部分 速度和转向控制控制平衡车的车速需要控制平衡车的速度环。编写两个函数一个控制移动速度一个控制转向速度。计算最大的移动速度先转换成m/s然后乘上一周的周长最后再给平衡车调节留一些余量小于0.7m/s。需要把手机APP摇杆上的范围映射到-0.7和0.7上细节2在main.c里面调用初始化函数和进程函数注意需要把app_control.c里对速度环强制设定成0的代码删掉接着设定转向的速度设置以左转为例只需要让小车左轮正转右轮反转即可控制转向的速度实际上是控制gz通过pid控制器调节加在左右电机上的速度就可以改变转向的速度了。KP1是up根据经验试出来的。还需要设置转向环的范围如下图结果大概是60rad/s只取四分之一15rad/s。°/s转换成rad/s,用前面的乘(π/180),约等于0,.017453。计算好PID的输出后加载左右电机上左电机加右电机减PID_Init(pid_turn,1.0f,0.0f,0.0f);//初始化转向环的PID PID_LimitConfig(pid_turn,15.0f,-15.0f);//将转向环的PID控制器输出限制在 - 15.0rad/s之间 //转向环 float gzApp_MPU6050_GetGz()*0.017453f;//°/s-rad/s float Omega_diffPID_Compute(pid_turn,gz); //#8.设置轮胎的转速 App_motor_SetOmega_L(Omega_refOmega_diff); App_motor_SetOmega_R(Omega_ref-Omega_diff);// //简介改变平衡车转向的速度 //参数TurnSpeed - 转向速度单位rad/s,最大转速15rad/s // void App_control_SetTurnSpeed(float TurnSpeed) { PID_ChangeSP(pid_turn,TurnSpeed); } if(strncasecmp(procBuf,move ,5)0) { int moveSpeed,turnSpeed; if(sscanf(procBuf,move %d %d,turnSpeed,moveSpeed)2)//第二个d后面不要写\n,因为已经被替换了 { //解析成功 //执行 App_control_SetMoveSpeed(-moveSpeed*0.01f*0.7f);//乘0.01是让-100映射到范围上 App_control_SetTurnSpeed(-turnSpeed*0.01f*15.0f);//用负的turnSpeed带入是因为-100~100。 } }
返回列表