STM32麦轮小车各运动模式编写中的“趣事”与项目总结
1. 避障模式
今天尝试编写避障模式,
常规思路就是读取 HC-SR04 的值进行判断,如果读到的数值小于某个值(比如10cm), 车子就后退;如果数值大于这个值,那么车子就停止。
在这个过程中我犯了两个错误。
一个是忘记了做BSP测试的时候,HC-SR04 用到定时器和电机输出的 PWM 定时器是同一个定时器(TIM2)(调试的时候发现 HC-SR04 读数一直特别小,还以为硬件有问题。。。。)
也就是忘记定时器分配,造成混用的问题。
随后将HC-SR04 改到TIM3 了。
另外发现 UP主设计PCB 的引脚分配的非常好,应该学习,做了一个引脚分配资源的总结。
犯的第二个错误,是没选择好车子停止的函数。
想要一个车子停止,有多种方式。
- 第一种,最直接的就是把“总闸”关了,这里指的是 STBY 引脚设为0, DRV8833 就不工作了。
- 第二种,不把“总闸”关掉,而是把 IN1 和 IN2 都设为0, 或者都设为1。(可以参考DRV8833的逻辑表)
一开始写的时候,选择了第一种,因为进入模式之后不断循环,不断判断,我并没有在要倒退的时候开启“总闸”,导致车子没有反应。于是,我改为第二种:
void Car_Brake(void) { /*-------左上轮子----*/ //FL_IN1(0); TIM_SetCompare4(TIM2,0); FL_IN2(0); /*-------右上轮子----*/ //FR_IN1(0); TIM_SetCompare3(TIM2, 0); FR_IN2(0); /*-------左下轮子----*/ //BL_IN1(0); TIM_SetCompare2(TIM2, 0); BL_IN2(0); /*-------右下轮子----*/ //BR_IN1(0); TIM_SetCompare1(TIM2, 0); BR_IN2(0); }
另外,我希望车子(PCB)不会下载进去就直接转动,因为这时候还连着 SW 的下载调试线(动起来有一种头疼的感觉)
因此我设置了一个 test_flag 的标志位,并通过手机(再通过蓝牙)想单片机发送一个“@”后,才能将 test_flag 置为1. 下面的语句在 while()循环中不断检测,这样手机就可以动态设置小车的启动了
在默认状态(复位的时候),test_flag = 0,这时候车子处于停止状态,就不会乱动了。
1 if(test_flag == 0) 2 { 3 STBY1(0); //PB9 后面两个轮子 4 STBY2(0); //PB4 前面两个轮子 5 } 6 else 7 { 8 STBY1(1); //PB9 后面两个轮子 9 STBY2(1); //PB4 前面两个轮子 10 }
2.无线模式
我发现我轮子的安装方式不对。下面文章推荐的方式为 ABBA 型。但我安装的时候没搞懂什么是 A 轮,什么是 B 轮。
https://blog.csdn.net/weixin_42108484/article/details/122090548
更加贴切的表达是安装成 X 型还是 O 型。(参考自文章 https://www.guyuehome.com/34376 )
则合理的安装方式共有一下两种(均为俯视看小车,而不是仰视看车底):
图1 X形布局
根据上面文章的力学分析,X形布局(仰视小车车顶)在转向的时候受力情况比 O形更好,因此尽量采用X形布局。
目前遇到了一个大挑战——用摇杆控制小车的程序该怎么写。这涉及到由遥杆的采集到的电位值转换到小车车轮的pwm值的问题。
看了B站UP的视频,左边的摇杆用于将车子平移,右边的遥杆用于车子的旋转。这也是这段代码的要最终实现的目的。
因为电机驱动选择了 DRV8833,因此在编写代码的时候一定要十分清楚调速时各引脚的状态:
这也是第一个难点:
难点①:DRV8833驱动的熟悉掌握
图3 DRV8833驱动电机
由文章:https://www.cnblogs.com/warcraft/p/16516486.html
图4 占空比(pwm)与转速关系
由图4可以得知,当xIN中有一个为高电平的时候,占空比和转速是成反比的。这和一般常识不同,需要特别注意。
因此在程序编写的情况中,需要时刻记住这一点。
我的程序经过实验测试,如下所示(v越大,转速越大;dir=1时,轮子向前转):
(我的 TIM 初始化部分OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_Low)
1 void FL_Forward_Roll(uint32_t v, u8 dir) 2 { 3 if(dir==1) {FL_IN2(1); TIM_SetCompare4(TIM2, v);} 4 else { FL_IN2(0); TIM_SetCompare4(TIM2, 500-v);} 5 } 6 7 void FR_Forward_Roll(uint32_t v,u8 dir) 8 { 9 10 if(dir==1) {FR_IN2(0); TIM_SetCompare3(TIM2, 500-v);} 11 else { FR_IN2(1); TIM_SetCompare3(TIM2, v);} 12 } 13 14 void BL_Forward_Roll(uint32_t v, u8 dir) 15 { 16 17 if(dir==1) {BL_IN2(1); TIM_SetCompare2(TIM2, v);} 18 else { BL_IN2(0); TIM_SetCompare2(TIM2, 500-v);} 19 } 20 21 22 void BR_Forward_Roll(uint32_t v, u8 dir) 23 { 24 if(dir==1) {BR_IN2(0); TIM_SetCompare1(TIM2, 500-v);} 25 else { BR_IN2(1); TIM_SetCompare1(TIM2, v);} 26 }
可见在我的程序中,
当有一个引脚为低电平的时候,若想让实际转速和v值成正比,需要设置TIM 的比较器为500-v。
而在有一个引脚为高电平时,这时是呈现正比的。
这与前述似乎矛盾,但是因为我在TIM初始化中,有效电平设置为低电平,即大于比较器后,电平由高变低(OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_Low) ,所以这并不矛盾了。
难点②:摇杆电位值转换到小车车轮的pwm值
接下来就是正题部分,解决摇杆采集到的电位值转换到小车车轮的pwm值的问题。
在编写程序的时候,首先需要明确几个准则:
1. 限幅问题:摇杆电位转换成pwm,势必要进行加减乘除等变换操作,但是范围不能超过 pwm的满值(这里是500),也不能低于0,同时希望消除一些抖动。
2. 方向问题:希望所得到的pwm值能够反映方向。经过变换最终得到的pwm值被用来设定电机速度,但由于希望摇杆能控制小车四处运动,所以希望得到的间接的 pwm 值能够反映方向。
例如:可将间接的pwm范围设为[-500, 500],以0为分界线,将大于0的设定为轮子正转,小于0的设定为轮子反转。
另外,因为 NRF24L01 传递过来的是 ASCII 码(为了方便调试,也为了和一些字符同时传递过来),因此还涉及到由 ASCII 码到普通数字的转换问题。
1 u8 Joy_RxBuf[20];//摇杆接收数据缓冲区 2 //@params: pwm1:FL pwm2:FR pwm3:BR pwm4:BL 3 void Conv_Joy_To_Pwm(int *pwm1, int *pwm2, int *pwm3, int* pwm4) 4 { 5 int Joy_Lx=50, Joy_Ly = 50, Joy_Rx = 50, Joy_Ry = 50; 6 int Map_Lx, Map_Ly, Map_Rx, Map_Ry; 7 //1. 接收 Joy_RxBuf 将其从 ASCII 转换为数字 8 //转换为4个值, Lx,Ly,Rx,Ry 9 Joy_Lx = (Joy_RxBuf[2] - '0') * 10 + (Joy_RxBuf[3] - '0'); 10 Joy_Ly = (Joy_RxBuf[5] - '0') * 10 + (Joy_RxBuf[6] - '0'); 11 12 Joy_Rx = (Joy_RxBuf[9] - '0') * 10 + (Joy_RxBuf[10] - '0'); 13 Joy_Ry = (Joy_RxBuf[12] - '0') * 10 + (Joy_RxBuf[13] - '0'); 14 15 //2.将Joy_Lx,Ly,Rx,Ry 从10~90的范围映射到-127~127的范围的 Map_Lx,Ly 16 Map_Lx = Map(Joy_Lx, 10, 90, -127, 127); 17 Map_Ly = Map(Joy_Ly, 10, 90, -127, 127); 18 Map_Rx = Map(Joy_Rx, 10, 90, -127, 127); 19 Map_Ry = Map(Joy_Ry, 10, 90, -127, 127); 20 21 //3. 通过 Map_Lx, Map_Ly 将其转换为 pwm1,2,3,4 22 *pwm1 = -Map_Ly - Map_Lx - Map_Ry - Map_Rx; 23 *pwm2 = -Map_Ly + Map_Lx - Map_Ry + Map_Rx; 24 *pwm3 = -Map_Ly - Map_Lx - Map_Ry + Map_Rx;//BR 25 //*pwm3 = -Map_Ly + Map_Lx - Map_Ry + Map_Rx;// my version 26 *pwm4 = -Map_Ly + Map_Lx - Map_Ry - Map_Rx; 27 //*pwm4 = -Map_Ly - Map_Lx - Map_Ry - Map_Rx; //my version 28 29 //4. 将pwm1,2,3,4的范围从-127~127 映射为 -499~499。 30 *pwm1 = Map(*pwm1, -127, 127, -499, 499); 31 *pwm2 = Map(*pwm2, -127, 127, -499, 499); 32 *pwm3 = Map(*pwm3, -127, 127, -499, 499); 33 *pwm4 = Map(*pwm4, -127, 127, -499, 499); 34 35 //5. 将 pwm1,2,3,4限制范围。 36 if (*pwm1 < 20 && *pwm1 >-20)*pwm1 = 0; 37 if (*pwm2 < 20 && *pwm2 >-20)*pwm2 = 0; 38 if (*pwm3 < 20 && *pwm3 >-20)*pwm3 = 0; 39 if (*pwm4 < 20 && *pwm4 >-20)*pwm4 = 0; 40 41 if (*pwm1 > 499)*pwm1 = 499; 42 if (*pwm2 > 499)*pwm2 = 499; 43 if (*pwm3 > 499)*pwm3 = 499; 44 if (*pwm4 > 499)*pwm4 = 499; 45 46 if (*pwm1 < -499)*pwm1 = -499; 47 if (*pwm2 < -499)*pwm2 = -499; 48 if (*pwm3 < -499)*pwm3 = -499; 49 if (*pwm4 < -499)*pwm4 = -499; 50 }
3. 重力模式
首先,要获取数据,就是从 MPU6050 那里得到数据。
MPU6050有很多数据产生,但是控制小车,主要用的是欧拉角:pitch, roll, yaw。
更确切的说,只需要 pitch 和 roll 两个数据。
接下来就是数据的编码、转换问题了。
期望的效果是:
前倾的时候,小车前行;后仰的时候,小车后退;(这是 pitch 参数需要起作用)
左倾的时候,小车左移;右倾的时候,小车右移;(这是 roll 参数需要起作用)
因此需要深入了解以下 pitch 和 roll 的数据范围了。
由文章:https://www.cnblogs.com/darren-pty/p/10280796.html
pitch角 –绕Y轴(俯仰) 范围:±90° ,与旋转方向相反转是增大 -- 抬头为正,低头为负
roll角 –绕X轴(横滚) 范围:±180° ,与旋转方向相反转是增大 -- 右滚为正,左滚为负.
但 roll 一般使用的时候,应该范围在-90~90之间(因为大于90°的时候,手已经碰不到摇杆了吧。。。。。。)
图5 MPU6050丝印
从丝印也可得知 pitch 的正方向(绕Y轴的箭头)、roll 的正方向(绕X轴的箭头)。
接下来就步入正题,如何将MPU6050的数据转换为 pwm 的信号呢?
下面这张图解释的很清楚了,只要将 pitch 代为前后量(向前为正),roll代为左右量(向右为正),就能很好地实现小车的重力模式了。
图6 麦轮求移动公式问题
4. 模式菜单
问题①:小车开机就不由自主地转动
这个问题一开始很奇怪,后来很清楚。
因为我的遥控器(发送端)返了一个“模式展示--确认选择”的错误。
所谓“模式展示--确认选择”是遥控器上选择小车模式的先后步骤,
如果在模式展示的时候,就将模式的代码发送给小车(此时还没确认进入该模式),
那么只要遥控器上电,就直接回发送模式的代码给小车。小车这时就会进入这个模式中,从而发生乱转的情况。
解决办法很简单,在遥控器(发送端),程序从起始开始运行,直到当确认进入该模式后,才会发送该模式的代码。
例如,无线模式的“模式展示--确认选择"中确认选择部分的代码如下
(属于遥控器端代码)
1 if(mode == 2) //一层:遥控模式 2 { 3 if(R_Dir==R_RIGHT)//二层:确定进入该模式的动作 4 { 5 while(!Mode_Send(mode)); //直到确认进入该模式,才会发送该模式的代码 6 while(1)//三层:具有出口的死循环:程序到这里后只执行死循环内的代码,直到有条件打开出口 7 { 8 Wireless_Mode_Send(); 9 mpu_dmp_get_data(&pitch, &roll, &yaw); 10 if(pitch>=50) 11 { 12 while(!Mode_Send(8)); 13 mode = 1; 14 break; 15 } 16 delay_ms(10); 17 }//3 18 }//2 19 }//1
另外,从上面的代码中也可以看到,一种模式下有三层的代码层次:前两层就是模式展示、确认进入,最后一层就是该模式的循环体。
问题②:退出一种模式后,小车确仍然保持运动
可以在这个模式退出的时候加入小车刹车的程序,如下:
(属于小车端代码)
1 if(mode_ok && mode == 3)//mode=3 表示重力模式 2 { 3 while(1) 4 { 5 if(Gravity_Mode()) //当函数体返回1,表示退出 6 { 7 mode_ok = 0; 8 Car_Brake();//退出的时候,小车刹车 9 break; 10 } 11 delay_ms(10); 12 } 13 }
问题③(未解决):退出一种模式后,小车确仍然保持运动
(后面解决了)
现在我写的程序出现了一个奇怪的问题:
就是处于重力模式的时候,有时候退出会出现两种情况:
第一种情况:是正常退出,不会对遥控模式造成影响
第二种情况:就是退出后小车仍然运动,遥控模式受到影响(右摇杆失去作用,左摇杆控车方向改变)
以上是一开始重力模式;如果一开始选择了无线模式,然后切换到重力模式的时候,也会出现两种情况:
第一种情况:是正常退出,不会对重力模式造成影响
第二种情况:就是退出后小车仍然运动,重力模式受到影响
一种可能的猜想是:
在退出后,模式确认标志为被置为0,重新接收数据,但是因为发送和接收可能存在延迟,因此下面的代码可能陷入一种陷阱。
就是明明没有收到合法的模式的代码,但是mode_ok 被置为1,此时再配合不明确的 mode,进入特定的mode中去了。
1 //1. 将 NRF 配置为接收模式 2 NRF24L01_RX_Mode(); 3 //2. 接收数据,将其存储在 Cmd_Rx_Buf 中(用于存储表示模式的字符) 4 stat = NRF24L01_RxPacket(Cmd_Rx_Buf); 5 //3. 确认数据接收成功,并产生标识 6 if(stat == 0)//表示接收到了数据,根据接受到的数据,设置 mode 的值 7 { 8 if(Cmd_Rx_Buf[0] != ' ') //确认缓存不为空 9 { 10 if(Cmd_Rx_Buf[0]=='L' && Cmd_Rx_Buf[1]=='Y') mode = 1; //mode=1 表示蓝牙模式 11 if(Cmd_Rx_Buf[0]=='W' && Cmd_Rx_Buf[1]=='X') mode = 2; //mode=2 表示无线模式,已经完成 12 if(Cmd_Rx_Buf[0]=='Z' && Cmd_Rx_Buf[1]=='L') mode = 3; //mode=3 表示重力模式 13 if(Cmd_Rx_Buf[0]=='B' && Cmd_Rx_Buf[1]=='Z') mode = 4; //mode=4 表示避障模式,已经完成 14 if(Cmd_Rx_Buf[0]=='G' && Cmd_Rx_Buf[1]=='S') mode = 5; //mode=5 表示跟随模式,已经完成 15 if(Cmd_Rx_Buf[0]=='C' && Cmd_Rx_Buf[1]=='D') mode = 6; //mode=6 表示彩灯模式, 已经完成 16 if(Cmd_Rx_Buf[0]=='S' && Cmd_Rx_Buf[1]=='T') mode = 0; 17 mode_ok =1;//没有确保 mode 的合法性,就将 mode_ok 置为1 18 } 19 }
应该改为以下代码:
1 if(Cmd_Rx_Buf[0] != ' ') //确认缓存不为空 2 { 3 if(Cmd_Rx_Buf[0]=='L' && Cmd_Rx_Buf[1]=='Y') mode = 1; //mode=1 表示蓝牙模式 4 if(Cmd_Rx_Buf[0]=='W' && Cmd_Rx_Buf[1]=='X') mode = 2; //mode=2 表示无线模式,已经完成 5 if(Cmd_Rx_Buf[0]=='Z' && Cmd_Rx_Buf[1]=='L') mode = 3; //mode=3 表示重力模式 6 if(Cmd_Rx_Buf[0]=='B' && Cmd_Rx_Buf[1]=='Z') mode = 4; //mode=4 表示避障模式,已经完成 7 if(Cmd_Rx_Buf[0]=='G' && Cmd_Rx_Buf[1]=='S') mode = 5; //mode=5 表示跟随模式,已经完成 8 if(Cmd_Rx_Buf[0]=='C' && Cmd_Rx_Buf[1]=='D') mode = 6; //mode=6 表示彩灯模式, 已经完成 9 if(Cmd_Rx_Buf[0]=='S' && Cmd_Rx_Buf[1]=='T') mode = 0; 10 else mode=0; 11 if(mode != 0) mode_ok =1; 12 }
后来发现,这是小车接收不到停止代码的特殊情况
这个情况是遥控器发送停止代码,小车也接收到,但是就是没有退出该模式。
也就是说明,NRF24L01传递数据,有时候并不可靠。即使发送成功了,但是也不一定接收成功。(非常奇怪,但实验出来就是这样)
因此出现了上述的情况:
就是处于重力模式的时候,有时候退出会出现两种情况:
第一种情况:是正常退出,不会对遥控模式造成影响
第二种情况:就是退出后小车仍然运动,遥控模式受到影响(右摇杆失去作用,左摇杆控车方向改变)
以上是一开始重力模式;如果一开始选择了无线模式,然后切换到重力模式的时候,也会出现两种情况:
第一种情况:是正常退出,不会对重力模式造成影响
第二种情况:就是退出后小车仍然运动,重力模式受到影响
上面的情况之所以会发生,就是因为,小车根本就没有退出!!!!
当遥控器退出遥控模式,进入重力模式,但是小车仍然处在遥控模式下,这时候小车在用遥控模式的算法在计算遥控器处于重力模式下发送的数据!!!!
同理,遥控器退出重力模式进入遥控模式的时候也会出现这个问题。
另外,还有一个很重要的BUG:就是如果遥控器突然断电(偶然或者人为故意),小车仍然会卡在那个模式:
因为小车在一个模式的循环体中,退出的条件只有一个:NRF24L01接收成功,并且接收到的是停止代码。(‘S’,‘T')
因此,为了解决上面问题,新增了一个全局变量 g_try,用于记录 NRF24L01 接收失败的次数。
当g_try大于一个阈值的时候,小车直接退出;当NRF24L01接收成功,将g_try归0———这样,就能保证小车在接收无效或者遥控器没电的情况下,及时退出一个死循环。
(这样做的前提在于:遥控器NRF24L01的发送周期是远小于g_try到达阈值的时间,因此保证小车不会在遥控器进入模式状态下退出该模式,保证了安全性。
同时,如果小车的NRF24L01一直没接收到数据,也间接表明了遥控器也退出了这个模式,即使小车接收停止命令无效,在没有受到命令一段时间后,小车也会退出这个模式)
5. 项目总结
① 麦轮小车项目概括
项目简介:使用遥控器控制麦轮小车,通讯方式为2.4G无线信号,控制方式为通过遥控器上的摇杆或者陀螺仪,控制小车的方向和速度。
项目难点:
- 用NRF24L01实现无线通讯,编码解码的实现。
- DRV8833的驱动实现。
- 摇杆控制、重力模式控制小车的算法的实现。
② 项目时间历程:
第一阶段:4月9日~4月17日 编写遥控器的LED、OLED、JDY-23、MPU6050、摇杆的驱动。
- 4月9日:编写LED和OLED的驱动测试。
- 4月11日~12日:测试JDY-23,开始学习NRF24L01的通讯知识。
- 4月13日~17日:编写摇杆 、MPU6050的驱动
第二阶段:4月18日~4月25日 进行遥控器NRF24L01发送接收消息的测试,
并同步开始编写小车的电机等驱动,尝试编写避障、跟随模式以及 2.4G 通讯的编码和解码。
- 4月18日:完成小车的LED、JDY23、HC-SR04的驱动。
- 4月20日~23日:NRF24L01发送消息的测试,完成DRV8833, WS2812B驱动的初步编写。
- 4月24日:测试麦轮的前进与后退。编写小车避障模式、跟随模式程序。并测试遥控板的编码和解码。
这里的编码和解码:指的是模式的数字和代码之间的转换。在遥控板中,模式的切换是通过一个 mode 的整型变量,通过摇杆改变的;在通讯的时候,发送的是这种模式的代码(例如:避障模式取 'B' 'Z' 两个字母作为代码),而不是模式的数字序号。在接收端(小车)接收的时候,再将这种代码转换为数字(或者宏定义)。
另外,在传输摇杆数据和陀螺仪的数据也需要编码和解码。即数字与 ASCII 码之间的转化。这样是方便调试的(如果通讯不转为ASCII码,那么在蓝牙终端看到的是乱码)。
第三阶段:4月26日到5月6日 完成小车的无线、重力模式,调试模式切换功能;编写遥控器贪吃蛇的程序。
- 4月28日~5月1日:实现摇杆控制小车,陀螺仪控制小车。
- 5月2日:实现 OLED 的UI设计。
- 5月4日:测试完善小车模式切换的功能,并进行调试。
- 5月6日:编写贪吃蛇的程序。
③ 学习软件外的收获
另外,在第三阶段也学习到了一些软件之外的知识,比如:
- 麦轮小车的排布(仰视看为X型更合理),
- 锂电池的选型
- 电机接线硅橡胶和PCB上插座的使用(比原来做过的面包板平衡小车好用不少)
- 提升了对于 STM32F103C8T6 这个 MCU 的认识(5大最小系统:时钟、BOOT、电源、复位、下载调试)
电源模块简介:https://www.htxw-tech.com/post/39400.html
拿STM32F103C8T6这个型号来说,总共有5个接电源正极和4个接地引脚,分别给内部不用的模块供电。
VDD:单片机的数字电源正极,共有5个VDD引脚
VSS: 数字电源负极,共有5个VSS引脚。
VDDA:VDD后面有个A,A=Analog,表示模拟的意思,就是芯片内部模拟器件的工作电压正极。
VSSA:表示模拟器件的公共端地(模拟电源负极)。
VBAT:给后备区域供电,维持RTC/BKP寄存器这些数据掉电保存,一般是接纽扣电池,如果不需要可以直接接电源。
VREF+是参考电压输入引脚正极,VREF-是参考电压输入引脚负极。
上一段提到了ADC和DAC模块,这两种模块是数字与模拟的结合,负责数字信号和模拟信号的转换。在某些应用中,对信号的噪声要求很高,这就需要把数字信号和模拟信号分开,采取一定的措施连接,避免相互影响。所以单片机会有数字电源和模拟电源引脚。由于模拟电源需要一个很标准的电压信号。所以就有了VREF引脚。但是,作为开发板,只是用来学习单片机用的,所以对噪声要求不高,我们就只需要做一个简单的隔离措施:在VDD和VDDA之间接一个0欧姆的电阻,同理,在VSS和VSSA之间接一个0欧姆的电阻。
把VREF+与VDDA连接,把VREF-与VSSA连接。(在实际应用中,VREF+用来连接标准的电压输出,比如REF3133,可以产生标准的3.300V。前面说到,开发板是用来学习的,没有必要给VREF连接一个标准的3.3V,如果你非要连一个,我也不拦着。)
软技能上:
是学习到了驱动和应用分层的思想:增加了APP层以及文件夹(原来只有 HARDWARD,SYSTEM都是应用和驱动混合在一起)。