智能车第20届Micropython组——从0到完赛~

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:
花了很多时间终于完赛了,本文章想记录一下自己做的过程和踩过的坑,同时也希望可以帮助到第一次接触智能车或者学校没有传承的同学们。本人也得到了很多前人大佬的文章的帮助,在此统一总结出来。(不要再去某鱼直接买现成的啦)


提示:以下是本篇文章正文内容,下面案例可供参考

1 所需硬件

  • 一个RT1021核心板 100Pin/144Pin(还是推荐144,毕竟管脚多)
  • 一个RT1021扩展学习板(以下简称“母版”)
  • 推荐两个或两个以上红孩儿线性CCD
  • 一个openart mini
  • 一到两个ips200屏幕(调试用)
  • 两个编码器(逐飞的绿色编码器,正交编码输出)
  • 一个IMU963RA
  • 一个DRV8701E双电机驱动板
  • 一个无线模块串口(调试用)
  • 一个tof测距(可用可不用)

需要自制的模块:母版、双电机驱动板

2 硬件制作

2.1 原理图

参考逐飞官方给的rt1021的原理图,自己不需要用到的部分可以选择性删掉,想加按键、拨码等,可以看看核心板有没有空余的IO口,尝试用用。电机驱动板的原理图基本不用改就行了。

2.2 PCB

参考逐飞学习板的布局,会比较简便,双层板和四层板都没问题。智能车对硬件的板子其实要求不高,能接通drc没问题、做好电源线(功率线也算,具体要看原理图的功率部分)的宽度就没问题啦 。板子的物理尺寸就看自己的车况去修改即可。可以利用好M3的孔,除了让母版定位在车身上,根据CCD的支撑座的孔位也可以用来固定支撑座。
想要完美一点,布局对称、均匀会好看一点,做个泪滴、电感等敏感器件做个禁止铺铜,适当增加缝合孔,让GND回流通畅。

原理图和PCB的库都是可以从逐飞淘宝客服或者官网上拿到的,不需要自己画封装什么的。

3 结构

在这里插入图片描述
@逐飞科技
可以参考逐飞提供的结构,那个三角斜拉杆要不要都行,有根竖直的杆就行。可以选择正跑也可以选择倒跑。尽量把重量集中中心,关注一下机械零点(就是车子在一个位置可以立住一会儿,或者是可能往前倒,也可能往候倒的那个状态),机械零点参考逐飞推文里那样找也可以。要确保在机械零点下前面的板件和后面的电池都不要太靠近地面,特别要考虑坡道的问题。还有就是其实车子真正跑的时候,一直稳定在的位置通常都不是你的机械零点,因为小车要前进,所以要靠这个向前倾倒的趋势去前进,所以机械零点不要卡的那么极限。

另外虽然说重量尽可能集中中间,但把所有东西放中间并且车身整个立起来跑,这种方案我个人是不太推荐的,因为虽然重量确实集中了,但是稍微一往前倾倒,车子的整个力矩就会特别大,需要很快的反应才能让它保持平衡,反正试过,不好调。搞成跷跷板的状态确实是比较好调的。

请添加图片描述
摄像头支撑座可以自己打印个电路板当固定件也可以(嘉立创白嫖板子)
在这里插入图片描述
openart的这些口不能一一对应屏幕,所以将openart上焊上排母,再画个嘉立创白嫖的转接板接上一块屏幕,就可以实时观看openart图像。用杜邦线接的话太鸡肋了。转接板电路就把对应孔连起来就好了,关注孔距和物理尺寸就好了。这也不涉及到比赛要的板子,所以转接板的PCB我就直接放在下方啦。另外这些加起来可能有点重量,建议别放太高。
ps:我这里用的openart v3.1和IPS200的屏幕,不同外设的接口可能不一样哦
请添加图片描述
通过网盘分享的文件:端口转接板.eprj
链接: https://pan.baidu.com/s/1bPmaXg-eElMnCYqu2T8w0A?pwd=7851 提取码: 7851

4、软件

4.1 RT1021

4.1.1 执行顺序

boot拨码开关的软启动那些建议去看说明书,那里写的更加详细清楚靠谱。

to小白:
函数先初始化完所有,一旦初始化到中断部分,中断就开始触发,每隔一段固定的时间就执行中断。即使开始了循环,在循环中一步步往下走,触发时间到了,就执行中断,中断执行完了,再回来循环刚刚的那个步骤继续执行。这个组别特别的地方在于这个中断没有优先级,谁先触发就先跑谁的程序。同时,因为是micropython在跑底层的C语言,所以其实这个中断时间不完全准确,但代码别太堵塞,是大差不差的。

4.1.2 变量的传递

我个人建议都把它写成类的形式,在类里面定义变量,这种是可以全局传递的。然后各部分的函数也写在各部分的类里面。
下面举个例子(这个代码内容是瞎写的只是举例子)

class ART:
    def __init__(self):
        self.flag = 0
        self.buf = 0

    def update(self, data):
        self.buf = (self.buf << 1) | data
        if self.buf == 0b10101010101010101010101010101010:
            self.flag = 1

art = ART()

建立一个ART的类,首先要实例化这个类(可以实例多个不一样的):art = ART(),有关于这里的变量就可以写在_init_里面用这种self.xxx的形式,然后如果同类的函数里用到这个变量,那就是self.xxx。如果在这个类外用到这个变量就是写成art.xxx。同理,在类里面用到类里的函数,那就说self.update;类外使用就是art.update。知道如何使用就足够啦

4.1.3 平衡思路

采用的是一阶互补滤波,确实已经够用了。平衡思路在逐飞浅析里已经讲过了,不过这里有几个点容易混淆。
在这里插入图片描述
加速度计有三个值嘛,陀螺仪也有三个值嘛,分别对应各自的xyz
这里的angle_m通常会有两个加速度计数据与小车的俯仰角有关,但是你需要去找那个往前倾倒和往后倾倒会增加和减少或者减少和增加的。如果没理解的话就把两个都试一试,哪个出来的波形比较像,就知道了。陀螺仪数据那个就比较好找了。然后就是这个return出来的结果,其实不是直接就是角度了,需要通过换算才能得到这个角度。但其实这里就不用纠结角度了,直接用这个return出来的值去做平衡就好了,就不用再除以在别的地方看到的那些16.4什么的了。
比如这个值在机械零点的时候是2000,那往前假设是2200,多于2000,多出的部分是不是就会进行PID计算然后控制啦,同理,往后是1800,少于的部分也会进行PID计算了。

在这里插入图片描述
这几个参数就靠自己跟着推文的教程走,将传入加速度计的值、陀螺仪的值以及return出来的伪角度通过无线串口发送到上位机上一边看、一边修改,去找到自己的那个参数就没问题的了。(串口的使用烧录一下wireless那个例程琢磨一下就会了)

滤出效果接近就行
在这里插入图片描述

4.1.4 PID控制

网上教程也很多了,这种看看网上的教程就好,最好能实现无线调参。
但对于这个D车模来说,还是有些技巧的。这里引用一下这位大佬的调参思路。

@
https://blog.csdn.net/Mark197/article/details/119486864?ops_request_misc=%257B%2522request%255Fid%2522%253A%25221cf5b4378af6e2521dfb3f954d168d8a%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=1cf5b4378af6e2521dfb3f954d168d8a&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-9-119486864-null-null.nonecase&utm_term=16%E5%B1%8A%E6%99%BA%E8%83%BD%E8%BD%A6&spm=1018.2226.3001.4450

在这里插入图片描述
在这里插入图片描述

我尝试过后,角速度环PI和PD其实都可以,建议先尝试调PI,很快就有平衡迹象。
各个PID参数正负要看个人实际传参。
每个人的参数都会不一样,大部分取决于你的执行中断的时间。

这个普通的PID的类应该大家都差不多,就直接放上来了

class PID:
    def __init__(self, kp, ki, kd, imax, target):#初始化
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.imax = abs(imax)  # 绝对值保证积分限幅有效性
        self.integrator = 0.0
        self.last_error = 0.0
        self.out_p = 0.0
        self.out_i = 0.0
        self.out_d = 0.0
        self.out = 0.0
        self.target = target

    def constrain(self, value, min_val, max_val):
        if value > max_val:
            return max_val
        elif value < min_val:
            return min_val
        return value

    def compute(self, error):

        # 累积误差
        self.integrator += error

        # 积分限幅
        if abs(self.integrator) > 10:
            self.integrator = self.constrain(self.integrator, -self.imax, self.imax)

        # 计算各个控制项
        self.out_p = self.kp * error
        self.out_i = self.ki * self.integrator
        self.out_d = self.kd * (error - self.last_error)

        # 更新最后一次误差
        self.last_error = error

        # 总输出
        self.out = self.out_p + self.out_i + self.out_d

        return self.out
pid = PID(0, 0, 0, 0.0, -0.0)

各个环的传参就根据推文的可以了。

4.1.5 电机控制

电机控制这种关键的重要步骤应放在中断去执行,我个人是速度环(20ms)—>方向环(10ms)—>角度环(5ms)—>角速度环(1ms),角速度环的输出就可以加上死区后赋值给两个电机了。
死区:每个电机都有个死区,不同电机的死区不一样,就是比如赋值给电机给400,轮子不动,420才动,那么死区就可能在410左右。

4.1.6 前进

平衡完以后就可以尝试前进了,速度环的目标值直接给就能往前跑啦,但建议刚开始慢慢加上去,感受一下你这个代码的目标值多少才是合适的。数值大了以后,小车会加速往前冲,直到达到目标速度,这里就有个问题就是小车很容易因为加速直接扑到了。可以用逐步加目标的办法解决。简单的来说就是第一次循环,你的目标速度只有300,下一次305,再下一次310,以此类推,这样缓慢起速效果很好。包括直到加速,弯道减速,入环减速都可以用这个方法。

    def speed_smoothing(self, target_speed):

        # 设置较小的加速度限制
        max_acceleration = 0.3  # 可以根据实际需求调整这个值
        
        # 计算期望变化量
        delta = target_speed - self._speed_out_prev

        # 限制速度变化量
        if abs(delta) > max_acceleration:
            if delta > 0:
                smoothed_speed = self._speed_out_prev + max_acceleration
            else:
                smoothed_speed = self._speed_out_prev - max_acceleration
        else:
            smoothed_speed = target_speed

        self._speed_out_prev = smoothed_speed
        
        return smoothed_speed

4.1.7 红孩儿线性CCD

我们使用的这款线性CCD一次读取128个数据,用两个CCD,其实也就只有两行数据,一个看远一个看近,通过改变两个CCD的权重可以适应不同的速度。
虽然处理CCD的速度较快,但CCD的缺点也很明显:

  • 不能看的太远,数据会不真实
  • 只有一行数据,信息量少
  • 边缘数据会比实际值小,比如第64位和第1位上的颜色色度就算是一模一样的,第1位读到的值却会比第64位的值小

我个人是采用了增益补偿的方法,就是将0-50和78-127的数据都乘上了一个数,这个数取决于第n位距离第64位的距离。

ccd.set_resolution(TSL1401.RES_12BIT),CCD的初始化可以选择12Bit或者8Bit,8Bit处理的比较快,但要求赛道光线较好。8Bit读出的数据应该是0-256,12Bit读出的数据应该是0-4096。

4.1.8 图像处理

两种方法进行图像处理:

  1. 原始数据:
    这个直接利用左右像素值的差比和就循迹基本没问题了
  2. 二值化:
    通过将所有的像素相加再除以128就是平均的像素,用这个像素乘上一个0.6~0.95的系数可以作为阈值,大于这个阈值就说明是赛道,反之则为赛道外,这样也可以自然而然地找出边线。(赛道背景如果是纯蓝色可能这个系数就选高一点哦)

得到的赛道误差分别给到方向环的传入参数即可,方向环的输出直接加在电机赋值就可以,两个电机一加一减,得到差速转弯的效果。速度越快,转向环的kp也需要越大。
这个转向环除了基本的误差pid,还可以引进二次项,如:误差×误差的结果的绝对值,这个可以作为第二个误差项,陀螺仪的偏航角的角速度可以作为抑制转向过度的传入值。
在这里插入图片描述
远端CCD看的远确实可以方便元素处理,但是会有看到另外一条赛道的情况,这个时候容易丢线,就需要做一下丢边处理,比如丢边了,就保持上一个状态的误差。

4.1.9 元素处理

  • 十字基本不用处理,转向环调的好,基本就能直接进了。

  • 障碍一般不会直接放在中间,只要做图像处理的时候从中间往两边找边线就行。

  • 坡道速度闭环好也没问题,也可以坡道放个art的识别,识别到了,提高速度环的kp,因为如果在正常赛道kp太高,会有磕头、拜年现象,一顿一顿的。

  • 斑马线也是直接放个art识别就好了,简单省事

  • 最难的就是圆环了

圆环处理:
采用多个状态机的形式,在标志位为0的时候,遇到什么条件就置1;在标志位为1的时候,遇到什么条件就置2,以此类推。
请添加图片描述
第一个状态机可以是这个红色框框的小尖角,CCD的左边线会外扩,或者是CCD左边读到了白黑白,右边边线基本不变,这样的情况。左边线外扩这种判断很容易和急弯的情况相同,容易误判;白黑白这种判断缺点就是万一赛道不干净,刚好在普通赛道的黑边外有个小白点,也会误判,包括速度如果太快,并且CCD的周期太长,就有可能读不到白黑白这个状况。CCD误判这个问题确实还是建议借助Openart辅助识别一下。
在这里插入图片描述

第二个状态机就是黄色框框这一部分,CCD左边线逐渐收回来,这里可以用个环形缓冲区,来获得最近的几个数据,右边线依然是基本保持不变。这里有个需要注意的就是,车速如果太慢,并且CCD读的很快,缓冲区里的几个数据有可能读到的是同一个位置的边线,这样就没有那个逐渐收回来的趋势了。

在这里插入图片描述

第三个状态机就是绿色框框这一部分,CCD左边线逐渐外扩,这里可以用个环形缓冲区,来获得最近的几个数据,右边线依然是基本保持不变。同理,车速不能太慢。

然后就开始入环了,因为平衡车,晃、加上转弯,CCD本来就一行数据,所以我选择第四个状态机用陀螺仪来判断,很靠谱。陀螺仪的偏航角转满一圈了就变成第四个状态机。(陀螺仪的偏航角=陀螺仪偏航角的角速度 * 对应陀螺仪读取周期,转满一圈的角度大概率不是360,大概率在200-300之间,可以自己实际转一圈就知道了)

注意:这里只是大概说了四个状态机,不代表就是4个标志位,在处理陀螺仪这个地方,标志位可能需要另外多几个,需要自己琢磨想一想了。
只能说CCD调完,确实感觉总钻风是真香了。

元素控制思路:

第1个和第2个状态机沿着右边线走
第3个状态机开始沿着左边线入环
第4个状态机沿右边线出环

4.1.10 菜单

菜单可以参考网上的修改一下,美不美观看个人,能实现基本的调试就行。
可以利用某个按键或者自增拨码开关来控制是否打开菜单,跑的时候关掉就行了,因为毕竟Lcd的打印很占用时间。

4.1.11 时间戳

首先要Import time

# 性能测试(调试用)
    test.t1 = time.ticks_us()
    sadasoiuhsioahosa
    sahoiusao
    sahoiusha
    ashbuoxahud
    sbanousxhao
    sabiuiodasb
    sahoiah
    test.t2 = time.ticks_us()
    print("耗时:", time.ticks_diff(test.t2,test.t1), "微秒")

这样就可以测出这一段代码执行一次用了多久时间了(当然里面可能会参杂了中断时间)
单独测中断时间的话,最好import utime,再把def time_pit_handler0(time): 里面的time换成utime。

4.2 openart

4.2.1 接收与发送:

openart一定是要接收到了RT1021的开始发送命令再开始发送,不然就会导致art已经在发送了,但RT1021没有去接收。这也是导致了很多种核心板卡死的问题的原因,比如什么后端有问题啊、什么溢出了啊、什么无法连接啊、无法识别端口啊,程序第一次运行没问题、第二次就卡死,程序有点变卡啊。这些都是我遇到过的问题,基本都是因为了这个原因,解决了以后就没有遇到过了。
RT1021的串口可以选择例程里有的串口,art就根据自己的型号和对应的固件示例去选择对应的串口,波特率那些都要一致。

具体解决的伪代码:

1在art上建立一个标志位,标志位收到一个“S”才开始让art开始发送
2在RT1021上初始化完串口(最好初始化完所有)后选择某一种方式给art发送“S”,并将自身的标志位置1,我这里用的是长按。
3同理,有开始就有结束,使用另外一个方式给art发送“E”,并将自身标志位置0。
4同理,art接收到“E”就停止发送
5同时,我建议在初始化串口后马上紧接一个给art发送"E"的指令
为什么要有步骤3呢?可以方便你调试。
为什么要有步骤5呢?因为有时候核心板会因为其他问题卡死或者突然报错,或者它就是出毛病了,这个时候art依然在发数据,你没有办法让art停止(这个时候核心板卡死,所以按键也没用了),art那边没有卡死,它会一直发一直发。如果不加这个步骤5,你即使通过刷新RES的方式刷新核心板解决了因为你的代码别的问题,但是又开始直接接收art数据了(RT1021并还没有准备好),就又会卡死了。这个时候一般就要去找逐飞刷新固件了。。有了这个步骤就能在每次RST就给art发个"E"了。
如何知道避免在写代码,尝试建立过程中又卡死了呢?让单片机和art分别通过串口转TTL与上位机连接,去看看各自与上位机的收发有没有问题,如果都没有问题,逻辑也理清了。那么,恭喜你,大概率就已经得到一个不会卡死的openart啦
# # 初始化串口
uart = UART(5)                         # 使用UART5
uart.init(115200, bits=8, parity=None, stop=1)  # 配置要和OpenMV一致
uart.write(b'E')
    if key_data[1] == 2 and not art.flag:  # 长按且未发送
        art.flag = 1
        uart.write(b'S')
        
    if key_data[2] == 2 and art.flag:  # 长按且发送
        art.flag = 0
        uart.write(b'E')

4.2.2 识别:

最简单的就是识别色块了,这个部分让AI直接写就行了,有啥具体需求,你想识别到啥,就发啥,具体看你自己就行了。
色块大部分用LAB,在openmv上可以根据实物图调整阈值。
在这里插入图片描述

LAB的阈值调整:在这里插入图片描述

防止在一条赛道上看到了隔壁赛道的色块,可以截取部分图像使用。sensor.set_windowing()的功能据说这个openart mini还不支持。可以使用ROI定义矩形框,加.copy的方式来获取你想要的部分图像。
ROI(x,y,w,h),x是起点,y是起点,w是宽度,h是高度。

*ps:识别色块本身是没问题的,但是要考虑赛场上会不会刚好看到路过的人衣服颜色和你的色块刚好一样哦!*所以考虑一下还是要不要训练一些简单的数字模型哦。

4.2.3 REST:

其余的就看官网提供的就某门台啦!

4.3 其他问题

  • 这个组别的micropython也不是所有的库都支持,有些可能别的micropython支持,但是这个不支持,可以自己试试(或者看看官网手册)
  • tof测距我个人没用来检测坡道,单纯用来防撞,防止小陀螺犯病疯跑、疯转,遇到障碍物就停下。
  • 电池电压过高,好像对车子影响还不小,参数感觉像超调了很多的样子,很抖。理论上可以或者电压,根据电压动态微调参数。但是我还没这样做,感觉也没啥必要,电池不冲太满就行,3S的保持个11.6-12V状态就比较好
  • 关于CCD的焦距和偏振片的问题,其实影响不大、偏振片可以去掉一些不均匀打进来的光,可以看看b站上如何调整。焦距的话直接看着图像随便扭一扭,看看怎么样才能让赛道更清晰明显就可以了。
  • 不知道大家有没有遇到过无线模块代码里已使用,如果实际不接上无线串口,就会有程序变卡的现象,接上就没问题了,或者不需要用到的话代码先注释应该就可以了。
  • 如何提速?是个好问题
  • 待补充…
if 点赞 and 收藏 and 关注:
	meet in hangdian
	good luck

欢迎各位大佬留言评论区,如有错误,欢迎指正,欢迎交流~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值