深圳市航顺芯片技术研发有限公司
直播中

小萃米

11年用户 801经验值
私信 关注
[问答]

教你怎样更快上手智能车

什么是双车规则?
怎样去设计一款基于传感器的智能车?

回帖(1)

江根磊

2021-10-18 17:40:23
  这次第十五届全国智能车大赛已经结束了,对于我而言,确实是我大学中一个不可多得的经历,我希望我的这段经历可以保存下来通过博客的形式,同时可以对于其他人有帮助,因为不论举办多少届,都还是会有大量的新人加入,刚开始的迷惘我也经历过,所以我明白大家做智能车开始的问题,下面我写的东西希望能对后来人有一个较好的抛砖引玉的效果,因为我经历过,明白在没有学长带的情况下,对于智能车的探索是极度缓慢的,甚至可能会导致你的放弃,所以,我希望,下面这篇能够对大家有所帮助。
  规则变化
  下面介绍一下第十五届的双车规则,智能车经过十几年的发展和变化,每一年的规则都在变化,我们每一届的智能车人都在不断适应和改变。当然如果你是在一个智能车的强校,那么,你可能有一份传承,可以让你更快上手智能车。
  好了,话不多说,下面介绍一下双车规则。
  1.车模限制
  车模可以使用竞赛指定车模中任何型号,车模运行方向不限,双车模中需要
  有一辆采用三轮车模。
  车模制作完毕后,车模宽度不超过 25 厘米,高度不超过 20 厘米,长度小于 30 厘米。
  2.微控制器和传感器
  双车车模中,其中一辆车模的微控制器使用 Infineon 公司的单片机,另外一辆车模可以使用 Infineon, STC, NXP 公司的单片机。
  允许使用各类电磁、红外光电、摄像头、激光传感器、超声传感器器件进行赛道和环境检测。
  3.双车比赛任务
  一辆车模在赛道中间指定位置停止。一辆车模从车库出发,到达中间车模位置时,将车上的一个尺寸不小于 40 厘米见方的球体传递给第二辆车模,第二辆车模带着球体返回车库。
  
  当然赛道肯定不是这样的,这是其中的一段交接过程。
  传递的球体可以使用用标准的乒乓球,或者高尔夫球。也可以另外自制其他材质的球体,只要其直径不小于 40 毫米即可。
  4.个人对于比赛的分析
  这次的比赛颠覆了双车中原来两辆会车不接触的传统,这次双车接力方案中,对大多数都是需要两车发生碰撞的。这次比赛的难点,在于赛道元素多了一个车库,同时因为接力,需要对于车的控制要有更高的要求,毕竟你也不希望天天撞车吧,做过智能车的人都知道智能车有多贵,怪心疼的。
  智能车四轮入门流程
  1.选择一款传感器
  从历届智能车来看,现在用来路径识别的传感器主要分为两种,一种是摄像头一种是电磁,以前早期还有激光识别装在车上像武器一样二十级根发射管,后来被线性ccd替代了。两种传感器都有各自的优点和缺点,摄像头这种传感器一般具有看得远,一般原理是摄像头会采集回来一个二维数组,里面都是灰度值(我这里用的逐飞的灰度摄像头总钻风),然后我们会用二值化算法替换掉灰度值,剩下0和1,这样就会成为一幅二值化图,然后用图像处理算法拟合出路径,赋值给舵机就可以实现自动转向的效果,这样可以达到预判的作用,但是也因为采集的数据量太大,导致对于芯片的要求比较高,误差会由于算法的不同而变大变小。另一种电磁传感器,制作简单,上手快,代码实现简单,原理其实就是赛道中间有电磁线,会再赛道上存在一个电磁场,一般电磁车的前端会有多路电感,至少两路电感左右对称,单片机检测adc值,看看两边adc的读值反馈就可以让你知道车子是在直线还是偏离了直线。缺点是电磁车的上限会受到限制,一般正常人摄像头车上两米五的速度很容易甚至是三米,但是电磁车很难上两米七,当然有一届杭电的电磁车“钱江一号”速度好像是上了四米,如果你是这种大佬,当我没说过。
  最有趣的是今年的AI电磁,通过AI算法的训练,居然让电磁车的短前瞻跑出了长前瞻的效果,其中原理我也不是很懂,只知道是结合了AI算法的优势,颠覆了传统pid,打破了电磁车的传统极限速度,相信这种跑法会在未来应用在更多的赛道和传感器中。
  2.单片机
  这部分是绝大多数人没什么意识的,前面十四届都是用的NXP的芯片,这一届采用了三足鼎立的形式,一部分是NXP的,一部分是infineon的,一部分是stc的。
  这次的比赛很有挑战,因为限制了单片机,大家都会从0开始学习新的单片机,会让你见识到单片机与单片机的不同,也会让你一定程度上打破传承。
  (我用的是infineon的TC264,双核主频200M芯片)
  注意:当你选定了一款芯片后,要去看看这个芯片有哪些外设,选择芯片一定要选择性能好并且学习资料多的,这样学习起来也会很大程度上解决自己不会的情况。
  3.任务顺序
  这里以摄像头四轮为例,第一步你可以用单片机点个灯之类的,毕竟这都是我们学习之路的常态。
  (1)单片机点灯
  (2)定时器中断和串口中断运用熟练
  (3)oled显示参数(或者其他的显示)
  (4)电机,熟练掌握PWM对于电机的输出,按键输出调速
  (5)舵机,知道转动的原理,可以用按键实现左转或者右转的现象
  (6)摄像头显示在显示屏上或者发送到上位机上,反正必须可以直观看到(可以根据例程改写)
  (7)学习图像处理的知识,不是那种高深的机器视觉的图像处理,而是简单的图像处理,类似于检测像素点的样子,只不过是用代码来实现的。
  (8)这个时候你可以搭建车模了,搭建一辆车子,然后把程序烧进去看看能不能用。不停排bug找原因,这个过程你会感觉到痛苦,但是对于我们自身的提升还是非常的显著的。
  (8)当车可以跑但是转向不灵活的时候,你就可以开始学习pid了,百度、csdn、b站都有很多的学习资料。
  (9)(写图像特殊元素和提高控制pid)
  (10)当你的小车速度上至少一米五的时候,可以利用编码器形成闭环控制,观测数据利用上位机和无线串口收发,可以更好调节pid
  最基本的图像处理入门
  基本概念:智能车的图像处理,并不是像PS的修图,所以大家不要以为图像处理很难,其实你只要明白了lcd显示的原理,理解这些图像处理很简单。
  首先,我们摄像头采集回来的数组是一个二维的数组,这些数组相当于一个矩阵,每一个矩阵的元素就是这个位置的像素点的灰度值,所有的像素点的集合就是一幅图像,对于这种图像,我们一般采取两种方法,一是二值化后处理图像,二是直接处理灰度。前者更常见,利用二值化又分为软件二值化和硬件二值化,这个要看你的摄像头支持哪种,个人觉得,硬件二值化速度快但是适应性差,软件二值化速度和算法有关但是适应性好,二值化最怕的是强光的照射,受到阳光的干扰很大。
  如果你想用灰度算法,先去了解一下差比和算法,这个算法的优点是可以极大程度的减少阳光对于摄像头的影响,适应性很好,就是会一定程度对单片机的运算速度会造成困扰。
  1.二值化
  我是用的二值化,所以我在这里和大家说说二值化,其实二值化很简单,最简单的方法就是全局二值化,设置一个阈值,大于这个阈值的像素点是白色1,小于这个阈值的像素点
  是黑色0。其他的二值化方法可以去网上查查迭代法二值化和大津法二值化(逐飞),我用的是迭代。原来的二维数组经过替换(双重for循环)后,就是一个只有0和1的二维数组。那么如果是个直道,二值化的图像会是中间白两边黑对吧,那么这个矩阵数组就会是中间为1两边都是0。
  2.扫线方式
  扫线方式有很多,这个需要看你怎么抉择,一般的方式是从两边往中间扫,或者从中间往两边扫,个人推荐从中间往两边扫描,这样可以减少大部分的噪点,减少干扰。这里要提醒后来的各位,写图像不要凭空想象,如果你有上位机就用上位机来模拟训练,如果没有,就像我一样,把图像显示到屏幕上,然后屏幕上打点画出赛道边界拟合出中线。这样你在手推赛道的时候可以更加直观的看到自己的图像处理是不是和想的一样。
  基本扫线:比如你从中间往两边扫,本来都是白的,一旦碰到这个像素点是黑的,那么标记并且跳出这个循环,表示找到边界,一行找两遍就可以找到左右边界了。
  图像修图:图像处理分为行处理和场处理,在行处理是每一行都在扫描,按顺序来扫完。场处理是在行处理所有行扫完的情况下具备行处理的特征后再进行扫描,其中场处理你可以不停的进行行处理,这样你可以重复扫描进行特征判断。(场处理一般都用在十字和侧环以及今年新出的元素车库)
  下面是图像处理的一个总体框架,其中包括行处理、场处理、截断处理。
  uint16 SignalProcess()//图像信息处理
  {
  for (RowNum = 133; RowNum 》10; RowNum--) //循环叠加
  {
  if (LcrlenBool != 0)
  {
  Lcr_LStart[RowNum] = Lcr_LBlack[RowNum] = Lcr_LEnd[RowNum] = Lcr_Center[RowNum] =
  Lcr_RStart[RowNum] = Lcr_RBlack[RowNum] = Lcr_REnd[RowNum] = 0xFF;//将截断的信息去除
  goto Line_ImageData_Process; //跳过行处理
  }
  ImageProcess(); //图像行处理 内联函数
  Line_ImageData_Process: ;
  }
  FieldProcess();//图像场处理函数内联
  }
  下面是基本扫线,对大家只是一个参考作用,重要的是这个思路,我这个思路是从中间往两边扫描,下一行的起始扫描点从上一行的左右边界计算出的中值开始。
  void ImageProcess()//图像行处理
  {
  if (RowNum == 133) //第一行设置左右的起始结束列
  {
  Lcr_LStart[RowNum] =first_start;
  Lcr_RStart[RowNum] = first_start;
  Lcr_REnd[RowNum] = 186;
  Lcr_LEnd[RowNum] = 2;
  for (lie = Lcr_LStart[RowNum]; lie 》=Lcr_LEnd[RowNum]; lie--)
  {//从左边的起始列扫到结束列
  if (P_Pixels[RowNum][ lie] == 1 && P_Pixels[RowNum][ lie - 1] == 1)
  {//如果左边黑点,则为左线,跳出循环
  Lcr_LBlack[RowNum] = lie;
  break;
  }
  }
  if (lie == Lcr_LEnd[RowNum]-1 )
  {//如果这个列超出结束列
  Lcr_LBlack[RowNum] = 2;
  if (Turn_left_flag == 0)
  {
  First_turn_left = RowNum;
  }
  Turn_left_flag++;
  }
  for (lie = Lcr_RStart[RowNum]; lie 《= Lcr_REnd[RowNum]; lie++)
  {//从右边起始列扫到结束列
  if (P_Pixels[RowNum][ lie] == 1 && P_Pixels[RowNum][ lie + 1] == 1)
  {//如果右边连续两个黑点,则为右线,跳出循环
  Lcr_RBlack[RowNum] = lie;
  break;
  }
  }
  if (lie == Lcr_REnd[RowNum] + 1)
  {//如果这个列超出结束列
  Lcr_RBlack[RowNum] = 186;
  if (Turn_right_flag == 0)
  {
  First_turn_right = RowNum;
  }
  Turn_right_flag++;
  }
  Lcr_Center[RowNum] = Pro_value_byte((uint8)((Lcr_RBlack[RowNum] + Lcr_LBlack[RowNum]) / 2));//计算中点
  first_start = (uint8)Lcr_Center[RowNum];//这一行的中点为下一行的起始点
  if (Lcr_LBlack[RowNum] == 95 && Lcr_RBlack[RowNum] == 95)//如果左右边界线重合在中点,则截断
  {
  LcrlenBool = 1;
  }
  }
  else
  {
  //这一行的左右起始列和左右结束列,根据上一行的左右边界线加减数值得到,并且需要加限幅函数
  Lcr_LStart[RowNum] =first_start;
  Lcr_RStart[RowNum] = first_start;
  Lcr_REnd[RowNum] = 186;
  Lcr_LEnd[RowNum] = 2;
  //***********扫描顺序的转变***************//
  //*******前面都是找到左右起始行和结束行设置不同的扫描方式********//
  //下面是基本扫线搜线的顺序//
  for (lie = Lcr_LStart[RowNum]; lie 》= Lcr_LEnd[RowNum]; lie--) //从左起始行扫线到左结束行
  {
  if (P_Pixels[RowNum][ lie] == 1 && P_Pixels[RowNum][ lie - 1] == 1)
  {
  Lcr_LBlack[RowNum] = lie; //找到左跳变点,跳出循环
  break;
  }
  }
  if (lie == Lcr_LEnd[RowNum]-1 ) //左边扫不到边界
  {
  Lcr_LBlack[RowNum] = 2;
  if (Turn_left_flag == 0)
  {
  First_turn_left = RowNum; //找到第一个左边扫不到的点,可能是拐点
  }
  Turn_left_flag++;//左边扫不到边界的次数
  }
  for (lie = Lcr_RStart[RowNum]; lie 《= Lcr_REnd[RowNum]; lie++) //寻找右跳变点
  {
  if (P_Pixels[RowNum][ lie] == 1 && P_Pixels[RowNum][ lie + 1] == 1)
  {
  Lcr_RBlack[RowNum] = lie; //找到右跳变点,跳出循环
  break;
  }
  }
  if (lie == Lcr_REnd[RowNum] +1) //找不到右边界
  {
  Lcr_RBlack[RowNum] = 186;
  if (Turn_right_flag == 0)
  {
  First_turn_right = RowNum; //找到第一个右边扫不到的点
  }
  Turn_right_flag++; //右边扫不到边界的次数
  }
  Lcr_Center[RowNum] = Pro_value_byte((uint8)((Lcr_RBlack[RowNum] + Lcr_LBlack[RowNum]) / 2)); //计算出中点
  }
  }
  车库识别最重要的是识别斑马线,给大家分享一位博主,他的斑马线识别成功率很高或者去csdn搜索智能车车库识别,找一个叫拾牙慧者的博主。这位博主写的图像处理很厉害,大家都可以学习借鉴,如果说小编的图像处理是带大家入门的话,他就是可以带大家起飞的。
  最基础的舵机转向
  相信很多智能车人入门搞不清楚如何让车子循着摄像头采集回来的数据自动转向,那么下面介绍一下舵机如何进行打角,我用的舵机是s3010,是赛方指定的舵机,
  舵机原理请大家自行上网搜索,就是调节占空比来调节打角的多少。首先我们图像处理中有很多的行数,每一行在经过处理后我们会拟合一个中线的数组,这个数组就是我们俗称路径了,那么打角一般分为静态打角和动态大角,一般都是静态打角,除非你的速度上了三米,不然静态打角足够了。那么我们一般会根据摄像头的角度和高度来定一个打角行,这个打角行根据你的速度来,越快越远,越慢越近。这一行的打角选定后,会有对应的打角行的中值,那么一幅图片都会有一个恒定的中值(特别提醒:摄像头需要你去矫正,最好是居中,不然你的视角也会偏),如果我们拿这个恒定的中值去减打角行的中值,那么会出现三种情况,一种是大于0(可能是左可能是右),一种是等于0(车身是正的),一种是小于0(情况和大于0相反)。根据这种原理就可以来判断你是要往左或者右打角了,然后可以用一些公式进行偏角值换算成占空比直接赋值到舵机上打角转向。
  这个时候你可能会发现打角的幅度很大,不是很稳,因为舵机打角并不是线性的,这个时候,你可以加一个kp值上去,加一个系数可以让你的打角更平坦,如果你的打角还是差距很大,那么可以考虑加上一个kd值,舵机一般都是位置式的打角,一般采取pd的赋值,可以有效的让你的打角更丝滑。
  这是我舵机的领路博客,感谢这位博主的分享可以让我入门,喝水不忘挖井人,分享给大家,这位博主有更详细的思路和代码结构可以给各位参考。希望“棒棒算法”可以被大家升级换代。
  总结
  如果你把上面的事情都做完了,那么相信你已经可以跑除了特殊元素的全图了,那么恭喜你,你的智能车已经入门了。如果大家还是迷惘不知道怎么做,建议你多去问问学长和老师,多交流,你会发现,他们有许多的见解是你不知道的,不要沉默,做电子技术的人需要敢问。加油各位,希望我的这篇博客可以对大家有一丝的帮助。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分