第十九届全国大学生智能汽车竞赛 | 技术报告 - 清华鼠鼠

引 言
目 录
Contents
智能车大赛背景
气垫组比赛规则
本文结构
车模整体设计思路
硬件设计
大芯片系统电
路与引脚分配
小芯片系统电
路与引脚分配
电源
电机驱动
蓝牙
摄像头
IMU
USB-TTL
光流传感器
第一版PCB
第二版PCB
第三版PCB
第四版PCB
机械设计
第一版机械设计
第二版机械设计
第三版机械设计
软件设计
整体流程
底层配置
图像处理
调试过程
开发调试工具
空心杯电机选型问题
摄像头噪声问题
总 结
附 件
附录A:硬件原
理图与版图
附录B:软件代码
附录D:联系方式

可爱的掌中宝船

 

01   言


  引言部分,对于智能汽车制作情况进行概述, 对于相关联的文献进行综述。阐明后面报告内容框架安排。

一、智能车大赛背景

  为了加强学生实践、创新能力和培养学生团队精神,从2006年开始,由教育部高等教育司委托高等学校自动化类教学指导委员会举办全国大学生智能车竞赛。在继承和总结前十八届比赛实践的基础上,竞赛组委会拓展新的竞赛内涵,设计新的竞赛内容,创造新的比赛模式,在第十九届比赛中开设气垫组赛道,进一步激发大学生的想象力和创造力。

二、气垫组比赛规则

  气垫组比赛赛道为白色平整PVC材料,边缘由黑色电工胶带固定到蓝色背景布,四周有深色挡板以屏蔽环境的影响。赛道包含直线赛道、曲线弯道、交叉路口、环岛、路障五种赛道元素,车模需要从赛道起跑线出发,绕行赛道一周后,返回到起跑线。气垫组允许车模冲出赛道,但不允许在弯道内侧冲出赛道抄近道行进。

  气垫组要求参赛队伍自行制作气垫船,运行过程中分别使用两组风扇,分别提供车模垂直方向的悬浮力以及水平运动的推力,不允许依靠改变车模姿态将悬浮力转换成水平推动力,也不允许车模有轮子对车模产生支撑和推动作用力。

  气垫组对传感器没有限制,但要求必须只使用STC(专科)或WCH的RISC-V架构(本科)的微控制器进行信息处理和控制。

三、本文结构

  我们将从硬件电路、机械结构、软件代码三个维度,介绍第一版、第二版、第三版、第四版小车的技术细节,带领读者体验我们队气垫船的完整的设计过程。

 

02 模整体设计思路


  们的气垫船本着小型化的理念,主打原创和培养新生力量,对车模进行了多次迭代,利用风扇驱动,光流传感器和陀螺仪获取车身姿态,根据摄像头的图像信息完成跑赛道的任务。

  电路设计使用嘉立创EDA,电路板由嘉立创工厂生产和贴片,塑料的机械结构零件由拓竹的3D打印机打印,材料为PLA+。气垫布裁自普通垃圾袋,摄像头通过3D打印结构/碳纤维杆支撑,固定在车身/电路板上。

  控制算法采用基础的基于二元化图像的像素差值的算法,使用PID对小车在赛道上的偏移进行纠正。

  我们的小车将会全部开源,设计图纸和资料可以在文末的链接下载,也可以联系作者获取。

 

03 件设计


  定的硬件电路是智能车平稳快速行驶的基础。我们在整个系统设计过程中坚持完全原创,把小型化作为设计目标,在满足比赛需要和不多的预算下,尽可能利用芯片硬件资源,简化外围电路。

一、“大芯片”系统电路与引脚分配

  单片机是智能车的核心,根据比赛要求,我们在沁恒RISC-V单片机中选择了CH32V307VCT6,因为它是唯一一颗拥有并行图像接口(DVP)的微控制器。因为我们的系统中包含两颗微控制器,为了便于区分,我们称这个芯片为“大芯片”。关于“大芯片”的原理图如图3.1所示。

图3.1:“大芯片”相关电路与引脚分配

  图3.1右侧列出了“大芯片”与其他模块连接的情况:

  • 蓝牙通过UART6通讯,由PE2控制AT与透传模式
  • USB插口的虚拟COM经过CH340转为TTL信号,连到“大芯片”的UART2
  • 摄像头的并行视频接口与“大芯片”的DVP接口一一对应
  • PA12控制1.3V电源使能
  • SPI3和PD7负责与IMU芯片通讯
  • ADC0与电池分压相连,用来监测电量
  • PD11控制所有电机驱动芯片使能
  • TIM4与TIM8分别负责平移的风扇和产生气垫的风扇控制

  值得一提的是,我们在设计电路时选用12MHz晶振是方便用PLL倍频后使用GTM模块分频获得摄像头所需的24MHz的XCLK时钟(图5.2),以及合适的电机控制信号(25kHz)。可以看到,摄像头的XCLK连接到“大芯片”的MCO引脚,如此可以省去一个晶振。

  考虑到“大芯片”需要进行图像处理,而采用任何方式读取光流传感器数据有可能影响图像处理效率和产生异常,我们在小车系统中添加了第二块处理器芯片,为了开发方便,通过UART7使用串口完成控制器间通信。

二、“小芯片”系统电路与引脚分配

  按照比赛要求,小车上允许使用多颗指定的微控制器芯片。因此,我们使用高性价比的CH32V203C8T6来采集光流传感器数据。我们称这个芯片为“小芯片”,它相关的原理图在图3.2中有所展示。

图3.2:“小芯片”相关电路以及引脚分配

  每个光流传感器模块都与“小芯片”的其中一个I2C接口相连,如此既可以提高传输速率,又能保证数据同步。

三、电源

  本着小型化迷你化的想法,我们采用单节锂电池供电。为了最大化压榨小容量电池的最后一滴能量,我们允许供电电压在3.2V~4.2V波动。这样一来,稳压后的电源保持在3.3V就不划算了,于是在查阅所有在用的芯片后,我们决定采用2.8V逻辑电平。图3.3展示了电源相关的电路。

图3.3:电源相关电路

2.8V稳压芯片选用TI的高效DC-DC同步降压芯片TLV62569,其供电电压为2.5V~5.5V,刚好符合单节锂电池的电压波动范围,其同步降压又能减小损耗,对调试过程的续航有一定帮助。

1.3V供电采用ME6219C13,它是一款高精度、低噪音、超快响应的低压差LDO,因为希望从输入端就减小纹波,于是选择使用2.8V稳压输出作为它的输入。

  考虑到用的都是低压直流有刷电机,我们用一个滑动开关控制两个并联的PMOS管来开关总电流,如图3.3中央。考虑到电机在减速时可能产生电动势,在PMOS源漏之间外接了一个把倒灌回电池的高速肖特基二极管。

  因为使用单节锂电池,所以用来调试的USB接口电源(VBUS)可以用来给电池充电。这里我们选择经典的TP4057线性充电芯片给电池充电。

四、电机驱动

  电机驱动芯片的选择不多,大多数支持如此低电压的芯片只支持数百毫安的电流,这对我们来说是不够的。出于面积的考虑,我们最终放弃了外置晶体管的控制方案,选择TI的DRV8837驱动芯片,集成了连续1.8A的低导通电阻H桥。

图3.4:驱动(左上)、蓝牙(右上)、摄像头(右中)、IMU(左下)、USB-TTL(右下)电路

五、蓝牙

  为了方便调试,我们的小车提供蓝牙功能。CH9141K是沁恒的一款通用蓝牙透传芯片,非常适合用在这次的设计中。从图3.4中可以看到,蓝牙部分也采用2.8V供电,但用一个大电感与其余的电路作了交流隔离。蓝牙部分有它自己的电源指示灯,这是为了方便在比赛期间澄清没有使用蓝牙遥控,这一点在PCB版图设计中还会提到。

六、摄像头

  摄像头选用OV2640,用FPC连接器与主板相连。图3.4中展示了两个连接器,分别是立贴和卧贴的版本,我们在每个PCB版本中只用了其中一个。

七、IMU

  有时候,光流传感器可能不能够提供稳定的位移数据,于是我们在电路板上添加了六轴姿态传感器来获得角速度和加速度,辅助解算当前姿态。我们选用LSM6DS3TR-C是因为它在移动设备上广泛应用,属于成熟方案,而且价格比较低。

八、USB-TTL

  为了方便调试与充电,我们在板子上添加了一个Micro-USB连接器,并使用沁恒的CH340芯片进行转换。

九、光流传感器

  所谓气垫船,自然是要完全离开地面行驶才能够被称为气垫船。于是我们开始思考,有没有一种传感器,能够在不接触的前提下,像辅助轮那样把地面编码为光电信号呢?机缘巧合下,我们想到了鼠标。

  于是在拆了几个鼠标后,我们决定把鼠标的传感器应用到气垫船上。

  我们选择的传感器是PAW3205,它的工作原理是以2400FPS的速度拍摄40×40像素图片,再通过DSP电路对比相邻两帧内像素点的位移,从而获得相对位移信息。理论上讲,这样获得的位移信息比使用辅助轮获得的信息要拥有非常高的精度。

图3.5:PAW3205俯视图

  传感器需要与配套的透镜一起使用才能发挥作用,而且需要放在里地面很近的地方,因此我们围绕PAW3205做了单独的电路板,用排线与主板相连。这类芯片方案十分成熟,已经高度集成化,如图3.6所示,外围元件很少。

图3.6:PAW3205电路

  然而,我们最终没有把这个它应用在正式比赛中,主要有两方面考虑。

  首先,这类传感器要求赛道表面拥有一定不规则的纹路,才能够进行识别与跟踪。虽然在水泥地上在1~3mm高度能够稳定识别,但是我们曾将赛道当作鼠标垫时有过失灵的情况,所以我们担心在正式比赛的光滑赛道上不能够稳定工作。

  其次,光流传感器中包含一个DSP电路,尽管不属于另外的MCU,但做的其实是高效的图像处理。在无人机中,有时会用到嵌入高性能处理器的光流传感器以追踪目标或感知自身速度。尽管PAW3205内的DSP功能与IMU内的DSP效果类似,也就是利用算法解算出芯片输出,并没有可编程的功能,但我们担心鼠标光流传感器中的DSP电路会因为功能强大被判定为不符合大赛要求,因此只预留了接口,并没有在比赛中用到。

十、第一版PCB

  由于硬件电路和机械全部由同一人完成,因此在PCB布局上会和机械设计高度相关。成品气垫船外观请见第四章机械设计部分和附录C。
  第一版PCB没有采用光流传感器和“小芯片”,而将电源、主控、驱动分开,做成三个板子,如图3.7和图3.8展示。

图3.7:主控板顶面(左)和底面(右)3D图

图3.8:驱动板顶面(左1)和底面(左2)、电源板顶面(左3)和底面(左4)

  具体的原理图、PCB版图请见附录A。

十一、第二版PCB

  整体的布局主要考虑光流传感器、蓝牙天线、电池连接器、摄像头连接器的位置。可以看到,摄像头的FPC连接座位于下侧,光流传感器位列两侧来获得旋转信息,电池连接器方向朝内以方便装备电池,USB口放在左上角来避开气垫布,等等。为了节约成本并提供一个光滑的底面,这一版PCB的元件都在顶层。图3.9展示了第一版PCB的3D外观示意图,因为懒得画光流传感器的3D模型,所以它只有一层丝印。

图3.9:第二版PCB的3D图

  电机的输出焊盘在摄像头连接器左侧,只是迫不得已,这是一个致命的缺陷,在后续的版本中会换到其他地方优化掉。
  为了减少自重,所有电源铺铜采用网格形状。另外,PCB上有三处方形开槽,是为了将风扇的气流导到气垫中。

  成品气垫船外观请见第四章机械设计部分和附录C。

  具体的原理图、PCB版图请见附录A。

十二、第三版PCB

  这是最终带到华北赛区现场的版本,图3.10是它的3D图。与上一版本“PCB作底板”的想法不同,这一版PCB作顶盖板。

  原本我们在讨论如何在上一版的基础上增加腔体体积,打算继续把PCB作底板并加一些塑料柱来支撑顶面板,最终因为FR-4材料的高硬度,决定用PCB作顶板来保持整“船”的稳定。

  值得一提的是,前文讲到蓝牙供电有单独的指示灯,并且在PCB上也有一些特别的地方。图3.10中展示的顶面图中,圆形挖槽区域右侧有一个电感。这个电感是连接蓝牙电源和稳压电源的唯一通路,因此如果需要绝对禁用蓝牙相关功能,只需拆焊或剪断这个电感。

图3.10:第三版PCB顶面(左)和底面(右)的3D图

  我们选择给气垫风扇开一个口,并打了许多的定位孔用于固定其他部分,用于设计机械结构。

  随着版本的迭代,硬件-机械同步设计碰撞出了前所未有的火花,成品请见机械设计部分和附录C。

  具体的原理图、PCB版图请见附录A。

十三、第四版PCB

  针对第三版PCB在使用中出现的问题,以及为了兼容更多功能,我们在保持PCB形状基本不变的前提下完成了第四版PCB。
  因为PCB盖在顶上,不再要求底面凭证,所以这一版本也利用了底面进行布局。这一版主要有以下几点改进:

  • 电路支持1S、2S、3S电池供电,连接器改用XT30
  • 新增1S、2S、3S充电均衡电路(可选部分不贴)
  • 增强驱动电路。采用预驱动器+外置晶体管的方式驱动电机
  • 新增OLED屏幕用于显示debug信息
  • 对高频、大电流布线,芯片接地、去耦特别照顾,解决串扰问题

图3.11:第四版PCB的3D图

  所有大功率器件均放在顶层以达到最好的散热效果。

  目前这一版本还没有完整的机械模型,但具体的原理图、PCB版图可以在附录A中找到。

 

04 械设计


  们小车的特点是迷你,这需要机械和硬件同步设计,完美结合。从最初的想法到赛场上展现的成品,我们花费了相当一部分时间用来讨论船身形状、摄像头固定方法等,也做了一些调研,从前人和龙邱的设计中获得灵感与想法,一步步完善自己的车模。

  我们使用Solidworks建模,利用嘉立创EDA导出的STEP文件初步验证,所有机械支撑材料使用3D打印机打印。这也是许多零件设计得形状有些奇怪的原因,在设计的时候需要考虑到FDM打印机的能力范围,例如无支撑的最大倾角、桥的长宽等。

一、第一版机械设计

图4.1:第一版设计的模型示意图(顶面);后方是学生卡/身份证大小的卡片用于对比

  第一版设计比较原始,主要是想把所有东西捏到尽可能小的提及里。图4.1和图4.2是第一代的示意图,可以看到主控板下塞着电池和电源板,而驱动板因为面积非常小(10mm×7.6mm)在图中没有标出,计划焊上线后用热缩管把它变成导线的一部分。

  第一版设计中还有一块底板没有展示在图中,用铜柱连接在盖板周边的六边形凹槽中(图4.2),螺丝螺母固定。两块盖板之间设计了凹槽和突起,用于夹住气垫布。

图4.2:第一版设计的模型示意图(底面);后方是学生卡/身份证大小的卡片用于对比

  盖板上方插有电机固定支架,每个电机支架支撑两个电机,电机放在支架的套筒中并用胶水固定,后方留有导线的小孔。
  第一版设计的成品图展示在附录C中,但考虑到自重和面积不匹配、短宽体的底面不稳定、固定柱难以安装等问题,我们改进设计,完成了第二版。

二、第二版机械设计

  吸取第一版的教训,第二版整个外壳使用3D打印成型,省去了支撑柱的安装。我们使用螺丝将PCB板固定在外壳的底面,同时让PCB板卡住气垫布。如图4.3,外壳立面的开口提供气垫布膨胀的气流,电路板上的开口提供“船”下气垫的气流。这个设计还有一个打印的盖板,可以固定电机和摄像头,并利用凹槽和突起的摩擦卡住气垫布的另一边。

图4.3:第二版设计的模型示意图

  但在实际尝试使用的时候又遇到了许多问题,很遗憾没有留下任何完全组装的实物照片,但第二版设计的外壳、PCB实物图展示在附录C中。考虑到短宽体的底面不稳定等问题,我们再次改进设计,完成了第三版。

三、第三版机械设计

  既然把PCB放在底面会带来各种问题,那不如拿它当盖板。因为我们团队中机械和硬件设计是同一人完全负责,所以将二者结合,同步设计才有可能。

  我们在PCB布局时就考虑了电机固定问题,于是预留了比较大的面积,为后续组装提供不少便利。但是,摄像头的安装位置完全没有预留,只得在最后见缝插针挖几个固定孔,再设计几个奇形怪状的支撑架来插固定摄像头的碳纤维杆。

图4.4:第三版机械设计图(顶面)

  图4.4和图4.5中可以看到,船身上设计、有与PCB连接的孔位和螺母的槽位(外壁上的小开口),这大大简化了组装的流程和零件。
  如果把每个零件都展开描述一遍,这篇技术报告会显得冗长,所以感兴趣的老师、同学可以私信联系我,联系方式在文末,也可以通过链接下载获取所有的工程文件和资料。
  第三版机械设计借鉴了龙邱在气垫船底用双面胶带把气垫布粘在一个“环”上的想法,并将光流传感器引入设计中,如图4.5所示,底部开槽正好卡住光流传感器使用的透镜。更多图片请转至附录C。

图4.5:第三版机械设计图(底面)

  第三版机械设计是出现在华北赛区比赛现场的版本,获得了大家的喜爱,对此我们表示十分开心,也衷心感谢大家的欣赏!

  当然了,第三版也有一些缺陷,例如摄像头固定问题、电池摆放问题等。第四版PCB与第三版机械设计兼容,但针对第四版PCB的新功能,我们正在进行第四版机械设计的讨论,希望在国赛赛场能够呈现出一个更加完美的车模。

 

05 件设计


一、整体流程

  开机后,对产生PWM的定时器及GPIO口、给摄像头提供时钟的MCO引脚、与摄像头通讯的SCCB引脚及8位并行数据引脚、与子芯片通讯的串口进行初始化配置,随后通过SCCB协议与摄像头通讯配置摄像头寄存器,并配置DVP中断。随后在主循环中不断计算摄像头数据,通过位置式PD算法控制风扇的PWM占空比。

二、底层配置

1、电机控制输出

  电机的控制PWM信号产生自TIM4和TIM8,使用96MHz主频下周期为3840刻的计时器输出25kHz的PWM信号。我们选择25kHz是因为如此人耳听不到电机产生的噪声。

  初始化代码的逻辑分为五步:

  • 使能对应GPIO、TIM、AFIO模块的时钟
  • 配置对应的GPIO引脚,并使用AFIO进行重映射
  • 设置TIM模块的周期、预分频、运行模式
  • 配置每个PWM输出通道的占空比刻数并将通道使能
  • 设置TIM模块的PWM输出总使能,并将TIM模块使能

  具体代码参见附录B或GitHub链接,这部分在“huansic_motor.h/.c”。

2、周期性中断与时间轮

  考虑到更高层运行的PID函数,底层需要提供一个定时执行的函数。我们选择使用SysTick计时器是因为它是唯一一个64位计时器,且其它计时器都是16位的,无法做到在96MHz的主频下既做到每毫秒中断一次又提供最大精度。配置SysTick寄存器的过程十分简单,只需要按照说明写入四个寄存器,然后使能中断即可。

  需要指出的是,沁恒官方的硬件抽象库中没有明显的中断函数声明,所以要开发人员自己声明并定义中断向量指向的函数。但是声明的函数还需要和编译器内部定义的函数名一致,并且需要添加__attribute__((interrupt(“WCH-Interrupt-fast”)))的属性。

  在此,我们希望官方能够添加一个预先声明好的中断函数文件,学习STM32官方的硬件抽象库,使用weak属性定义默认中断函数,这样至少开发人员在写对函数名后能够跳转,知道自己写的是对的。

  在完成电路原理图后,我们在写底层库的时候觉得功能有些单一,想给自己添加一项挑战,于是就有了一个简陋的时间轮。考虑到气垫船上应该没有什么需要伪并行运行的程序,所以这个时间轮更像是一个事件队列。

  大体的原理十分简单:每个事件绑定一个函数,以时间由近至远将即将发生的事件进行排队,每毫秒检查队列的第一个事件是否应该发生,如果时间到了或者过了,就运行其函数。当两个事件非常接近时,因为中断函数无法中断自己,所以会等前一个运行完才会运行下一个。反正这个队列只是用来处理一些琐碎的事情,例如亮个LED或计算一下PID之类的,索性也就不考虑它堵塞delay函数的问题了。

  具体代码参见附录B或GitHub链接,这部分在“huansic_chronos.h/.c”。

3、图像数据读取

  OV2640的输出格式是DVP,也就是广义的并行图像接口,具体的图像传输大小(CIF、VGA等)可以通过配置寄存器进行设置。

  在行结束中断中清除对应中断标志位,读取DMA传到指定内存地址上的数据并按照RGB565的格式解码(由于一系列的寄存器配置导致OV2640采用了小端对齐的方式发送数据,所以在解码时还要额外翻转每个字节),将计算得到的灰度值写到另一片指定的内存空间上,行计数器加1。

  在帧接收完成中断中清除对应中断标志位,对DVP的DMA相关寄存器进行设置使其指回摄像头数据内存地址的开头,清零行计数器,同时代表摄像头读取状态的变量置0。

  在帧开始中断中清除对应中断标志位,帧计数器加1,同时代表摄像头读取状态的变量置1。

图5.1:DVP中断代码
  ### 4、“大芯片”时钟配置

图5.2:ch32v30x.h中更改HSE值为12MHz (右);system_ch32v30x.c中设定SYSCLK为96MHz

  在系统文件中(如图5.2)根据硬件配置更改时钟信息,并设置MCO功能输出24MHz给OV2640。

图5.3:OV2640的时钟要求

三、图像处理

1、图像二值化

  二值化阈值计算使用大津法,该函数在主循环中调用以通过灰度值分布更新二值化阈值,该阈值随后被用来对每个像素的灰度值进行判断并将结果写道存储图像二值化值的另一片内存上。

2、降噪

  若该像素点的周围四点中三点同黑或同白,则将此像素点改为与周围三点或四点相同的颜色。

3、决策

  将降噪后的数据进行统计,定义画面左半边白色像素的数量减去画面右半边白色像素的数量为误差并输入PID算法。

4、运动控制

  运动控制采用经典的PID算法,根据图像中白色像素数量差控制电机输出。PID算法是最为广泛应用的控制器之一。它结构简单、稳定性好、方便调整的特点,使其成为智能车大赛中几乎必备的、新手友好的控制算法。

  PID控制器基于误差的大小、误差的变化量、误差的累积量的线性组合得到输出量,完成闭环。更详细的原理这里不再赘述。

 

06 试过程


一、开发调试工具

  软件开发工具选用的是沁恒的IDE MounRiver Studio,可以对一键编译代码并烧录到芯片中。

图6.1:MounRiver Studio

  图像显示用的是I2C协议的0.96寸OLED屏,只需占用2个GPIO口就可以显示摄像头拍摄的图像。

图6.2:使用OLED屏幕显示图像

二、空心杯电机选型问题

  淘宝上在售的716电机虽然卖家各异,但大体上基本分为三种,分别标注了9000转,40000转和60000转。9000转对于气垫船的风力需求而言太低,故不详细展开。我们测试了40000转和60000转的电机特性如下

图6.3:4.2V下40000转空心杯716电机空载(左)、负载(中)、堵转(右)电流

4.2V电压下40000转电机空转电流约为0.2A,带负载转动电流约为0.8A,堵转电流约为1.2A。

图6.4:4.2V下60000转空心杯716电机空载(左)、负载(中)、堵转(右)电流

4.2V电压下60000转电机空转电流约为0.2A,带负载转动电流约为1.9A,堵转电流约为3.0A。

  由于我们气垫船上驱动芯片能输出的电流有限,当安装上60000转电机并将PWM占空比设置为20%以上时,由于电机启动时电流超过2A,因此电机无法正常启动。对此,我们修改程序使得电机启动时占空比缓慢增加,以减小电机的启动电流。这种做法能使得电机成功启动,但是当电机根据PID算法不断调速时仍会产生超出驱动芯片承受能力的电流,造成意外停机并且对摄像头的信号产生巨大干扰,所以我们最终还是选择了采用40000转的电机为气垫船提供升力和动力。

三、摄像头噪声问题

  摄像头噪声问题是最困扰我们的一个问题。我们发现当电机运行时,读到的图像数据中含有大量沿行方向分布的条纹噪声,且不具有规律性,无法通过软件过滤。使用60000转电机高转速运行时,我们甚至读不到任何有效的图像信息,而使用40000转电机时,情况有所改善但没有完全消除,图像中仍包含无规则的条纹噪声,因此在图像识别时依然无法进行搜线计算。我们怀疑是高频的PWM电机驱动信号对高速图像数据产生了干扰,数字与模拟的隔离欠佳也可能是摄像头噪声产生的原因。

图6.5:OLED屏幕显示受到严重干扰的图像

  在不修改硬件电路的前提下,能做的改善工作比较有限。对于噪声,我们加入了数字滤波器,希望通过牺牲响应速度而得到更稳定的数据。另外,将电机PWM和DVP采集数据的任务在时间上错开,通过控制PWM信号的上升沿和下降沿位置来使得DVP传输数据时不受电机驱动干扰也是可能的解决方案。

 

07   结


  份技术报告从我们起草的第一版设计到还在改进的第四版设计的硬件、机械、软件方面进行介绍,展示了完整的备赛历程。我们展示了独特的迷你车模设计理念和逐渐精进的硬件-机械同步融合的设计方法,并希望以锻炼为目的参赛的同学能够将重心放在探索各种可能性、培养自己的能力,大胆创新。正如智能车大赛的理念所言,立足培养,重在参与,鼓励探索,追求卓越!

  自2023年得知即将新增气垫组以来,我们便着手调研和设计。在完成了三大版本的设计后,终于做出了像样的气垫船。尽管最终无缘国赛,但这一路走来的收获对我们来说比奖项本身更珍贵。

  我(吴宗桓)同时负责指导本校完全模型赛道的两队进行备赛,在队内培训时得知廖宇翚想要参加气垫组。考虑到我需要进行暑期实习和保研夏令营,原本计划一人单推气垫组玩一玩,不想连累其他人,于是建议他加入完全模型的其中一组。一段时间后宇翚依然坚持一起参赛,并且拿出了十二分努力学习智能车相关的知识。要知道,清华大学的大一年级竞争是十分激烈的,我被他的决心打动,于是踏上了互相激励的备赛之路。

  备赛的过程自然不是一帆风顺,每一个大版本的设计都需要经历数次小改,特别是机械部分。我们在杯赛过程中遇到过的难题主要有器件选型、硬件-机械同步融合设计、算法实现。

  器件选型对于完全原创的设计来说永远是迈不过去的一道坎。额定电压、额定电流、面积、体积、芯片封装类型、电机功率等等,都是需要考虑的因素。我们需要在众多选项中组合出一套能够完成比赛任务的、成本合适的方案。很多时候,我会想,如果像其他队伍那样选择用2S或3S的电池,车模再做大一些,好多问题都将不复存在……但既然已经选择小型化,就要坚持到底!辅助轮肯定是不能用了,于是我们想到了拆鼠标;不能用无刷电机提供大功率了,于是我们尽可能选用小电机,降低自重。现在的方案肯定不是最佳的,但这是我们一步一步改进而来的成果。

  这是我第一次尝试硬件-机械同步融合设计,其路途也是艰难无比。以往的设计一般是给定机械结构去设计电路,例如完全模型组,或是使用已有的电路去制作小车底板,例如校内的电设比赛。这一次的锻炼让我机械设计能力得到了极大的提升,同时在设计PCB时也能时刻思考如何与机械部分相互配合。我相信这是我打破自己爱好之间壁垒的开始,以后会碰撞出更加绚丽的火花!

  这辆小车的软件算法部分全部由宇翚完成。我相信打过两年前第一届完全模型组的老同学们应该知道,第一次面对陌生的赛道规则会有多么劝退。在此,我想再一次赞扬宇翚的坚韧;入学不到一年便能够联系学长学姐自学图像处理和控制算法,同时请教我有关电路和MCU内部的知识,这份求知若渴然后亲自下手实践的劲头值得包括我的所有人学习。

  正如第六章中提到的,目前的方案主要存在两个问题:电机和摄像头。我们计划尝试不同的空心杯电机以及不同的供电电压(保留电池的小体积),选出最合适的方案进行进一步开发。摄像头的信号线已经重新布线,希望第四版的电路板不再有串扰问题。
  比较遗憾的是,我需要进行暑期实习,最后冲刺阶段没能和宇翚一起努力。好在我们的车模获得大家的喜爱,能够得到老师和同学的认可,对我们来说是非常重要的一件事!

  最后,我们祝愿晋级国赛的队伍bug统统解决,比赛现场不给车磕也能顺利跑圈;祝遗憾落选的队伍重振旗鼓,来年国赛现场见!

 

  件 ※


一、附录A:硬件原理图与版图

1、第一版

图A.1:第一版原理图

图A.2:第一版PCB版图
  ### 2、第二版 ![](https://i-blog.csdnimg.cn/direct/72fe7243ed734928a5172685dea064f4.png#pic_center =720x)


图A.3:第二版原理图


图A.4:第二版PCB版图

3、第三版


图A.5:第三版原理图
  ![](https://i-blog.csdnimg.cn/direct/179d61d1c8034166b955a3c2d6cdf39d.png#pic_center =640x)    ![](https://i-blog.csdnimg.cn/direct/b37ad22c033b42d8a1e09340e28bdb3e.png#pic_center =640x)

4、第四版


图A.7:第四版原理图

图A.8:第四版PCB版图

二、附录B:软件代码

huansic_util.c
/*
 * huansic_uitl.c
 *
 *  Created on: May 21, 2024
 *      Author: ZonghuanWu
 */

#include "huansic_util.h"

int32_t huansic_map(int32_t input, int32_t old_floor, int32_t old_ceil, int32_t new_floor, int32_t new_ceil) {
	float temp = old_ceil - old_floor;
	float percentage = input - old_floor;
	percentage /= temp;
	temp = new_ceil - new_floor;
	percentage *= temp;
	percentage += old_floor;
	return (int32_t) percentage;
}

void huansic_led_init() {
	// PWM pin
	GPIO_InitTypeDef GPIO_InitStructure = { 0 };
	TIM_OCInitTypeDef TIM_OCInitStructure = { 0 };
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = { 0 };

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init( GPIOB, &GPIO_InitStructure);

	TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = SystemCoreClock / 1000 - 1;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit( TIM3, &TIM_TimeBaseInitStructure);

	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStructure.TIM_Pulse = 0;	// turn off for now
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_OC3Init( TIM3, &TIM_OCInitStructure);

	TIM_CtrlPWMOutputs(TIM3, ENABLE);
	TIM_OC3PreloadConfig( TIM3, TIM_OCPreload_Disable);
	TIM_ARRPreloadConfig( TIM3, ENABLE);
	TIM_Cmd( TIM3, ENABLE);

	// GPIO
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOE, ENABLE);
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = 0xFC00;		// PE10~15
	GPIO_Init(GPIOE, &GPIO_InitStructure);

	// turn off for now
	GPIO_WriteBit(GPIOE, 0xFC00, Bit_RESET);
}

void huansic_led1_set(float bri) {
	if (bri < 0)
		bri = 0;
	if (bri > 1)
		bri = 1;
	TIM3->CH3CVR = (uint16_t) (bri * 1000);
}

void huansic_led2_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_10, bri < 0.5 ? 0 : 1);
}

void huansic_led3_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_11, bri < 0.5 ? 0 : 1);
}

void huansic_led4_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_12, bri < 0.5 ? 0 : 1);
}

void huansic_led5_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_13, bri < 0.5 ? 0 : 1);
}

void huansic_led6_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_14, bri < 0.5 ? 0 : 1);
}

void huansic_led7_set(float bri) {
	GPIO_WriteBit(GPIOE, GPIO_Pin_15, bri < 0.5 ? 0 : 1);
}

void huansic_led1_turn() {
    TIM3->CH3CVR = (uint16_t) (TIM3->CH3CVR > 500 ? 0 : 1000);
}

void huansic_led2_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_10, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_10) ? 0 : 1);
}

void huansic_led3_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_11, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_11) ? 0 : 1);
}

void huansic_led4_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_12, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_12) ? 0 : 1);
}

void huansic_led5_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_13, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_13) ? 0 : 1);
}

void huansic_led6_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_14, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_14) ? 0 : 1);
}

void huansic_led7_turn() {
    GPIO_WriteBit(GPIOE, GPIO_Pin_15, GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_15) ? 0 : 1);
}
 
huansic_motor.c
/*
 * huansic_motor.c
 *
 *  Created on: Jul 3, 2024
 *      Author: ZonghuanWu
 */

#include "huansic_motor.h"

void huansic_motor_init(void) {
	GPIO_InitTypeDef GPIO_InitStructure = { 0 };
	TIM_OCInitTypeDef TIM_OCInitStructure = { 0 };
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = { 0 };

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_TIM8, ENABLE);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &GPIO_InitStructure);
	AFIO->PCFR1 |= AFIO_PCFR1_TIM4_REMAP;		// remap required!!!
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 | GPIO_Pin_14 | GPIO_Pin_13 | GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_Init(GPIOD, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;		// PD11
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOD, &GPIO_InitStructure);

	TIM_TimeBaseInitStructure.TIM_Period = 3840 - 1;	// 96MHz / 25kHz - 1
	TIM_TimeBaseInitStructure.TIM_Prescaler = 0;	// full speed!!
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStructure);
	TIM_TimeBaseInit(TIM8, &TIM_TimeBaseInitStructure);

	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	TIM_OCInitStructure.TIM_Pulse = 0;
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
	TIM_OC1Init(TIM4, &TIM_OCInitStructure);
	TIM_OC2Init(TIM4, &TIM_OCInitStructure);
	TIM_OC3Init(TIM4, &TIM_OCInitStructure);
	TIM_OC4Init(TIM4, &TIM_OCInitStructure);
	TIM_OC1Init(TIM8, &TIM_OCInitStructure);
	TIM_OC2Init(TIM8, &TIM_OCInitStructure);
	TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
	TIM_OC2PreloadConfig(TIM4, TIM_OCPreload_Enable);
	TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);
	TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable);
	TIM_OC1PreloadConfig(TIM8, TIM_OCPreload_Enable);
	TIM_OC2PreloadConfig(TIM8, TIM_OCPreload_Enable);

	TIM_ARRPreloadConfig(TIM4, ENABLE);
	TIM_ARRPreloadConfig(TIM8, ENABLE);
	TIM_CtrlPWMOutputs(TIM4, ENABLE);
	TIM_CtrlPWMOutputs(TIM8, ENABLE);
	TIM_Cmd(TIM4, ENABLE);
	TIM_Cmd(TIM8, ENABLE);
}

void huansic_motor_set(MotorName motor, float val) {
	val = val > 1 ? 1 : (val < -1 ? -1 : val);		// constrain
	if (motor & Fan) {
		if (val < 0) {
			TIM8->CH1CVR = 0;
			TIM8->CH2CVR = (uint16_t) (-val * 3840);
		} else {
			TIM8->CH1CVR = (uint16_t) (val * 3840);
			TIM8->CH2CVR = 0;
		}
	}
	if (motor & LeftProp) {
		if (val < 0) {
			TIM4->CH1CVR = 0;
			TIM4->CH2CVR = (uint16_t) (-val * 3840);
		} else {
			TIM4->CH1CVR = (uint16_t) (val * 3840);
			TIM4->CH2CVR = 0;
		}
	}
	if (motor & RightProp) {
		if (val < 0) {
			TIM4->CH3CVR = 0;
			TIM4->CH4CVR = (uint16_t) (-val * 3840);
		} else {
			TIM4->CH3CVR = (uint16_t) (val * 3840);
			TIM4->CH4CVR = 0;
		}
	}
}

inline void huansic_motor_enable(void) {
	GPIO_WriteBit(GPIOD, GPIO_Pin_11, 1);
}

inline void huansic_motor_disable(void) {
	GPIO_WriteBit(GPIOD, GPIO_Pin_11, 0);
}

uint16_t T8CH1, T8CH2, T4CH1, T4CH2, T4CH3, T4CH4;

void huansic_motor_suspend(){
    T8CH1 = TIM8->CH1CVR;
    T8CH2 = TIM8->CH2CVR;
    T4CH1 = TIM4->CH1CVR;
    T4CH2 = TIM4->CH2CVR;
    T4CH3 = TIM4->CH3CVR;
    T4CH4 = TIM4->CH4CVR;
    TIM8->CH1CVR = 0;
    TIM8->CH2CVR = 0;
    TIM4->CH1CVR = 0;
    TIM4->CH2CVR = 0;
    TIM4->CH3CVR = 0;
    TIM4->CH4CVR = 0;
}

void huansic_motor_resume(){
    TIM8->CH1CVR = T8CH1;
    TIM8->CH2CVR = T8CH2;
    TIM4->CH1CVR = T4CH1;
    TIM4->CH2CVR = T4CH2;
    TIM4->CH3CVR = T4CH3;
    TIM4->CH4CVR = T4CH4;
}

 
huansic_chronos.c
/*
 * huansic_chronos.c
 *
 *  Created on: May 20, 2024
 *      Author: ZonghuanWu
 */

#include "huansic_chronos.h"

volatile uint32_t milliseconds;
volatile uint32_t microseconds;

struct Reminder {
	uint32_t scheduled_ms;
	void (*thread)(uint32_t ms);
};

#define	REMINDER_LENGTH	32
struct Reminder reminders[REMINDER_LENGTH];
uint8_t wheel[REMINDER_LENGTH];		// will be set to 255 if there's no upcoming reminder
uint8_t next;
void (*fp)(uint32_t ms);

void huansic_chronos_init(void) {
	uint8_t i;
	milliseconds = 1;
	next = 0;
	for (i = 0; i < REMINDER_LENGTH; i++) {
		wheel[i] = 255;
		reminders[i].thread = 0;
		reminders[i].scheduled_ms = 0;
	}

	// clear status register and counter
	SysTick->SR = 0;
	SysTick->CNT = 0;
	// update every millisecond
//	SysTick->CMP = SystemCoreClock / 1000 - 1;
	SysTick->CMP = SystemCoreClock / 1000000 - 1;
	// start SysTick
	SysTick->CTLR = 0xF;
	// enable interrupt
	NVIC_SetPriority(SysTicK_IRQn, 1);
	NVIC_EnableIRQ(SysTicK_IRQn);
}

void huansic_delay_ms(uint32_t duration) {
	uint32_t start = milliseconds;
	while(milliseconds - start < duration);
}

//void huansic_delay_us(uint16_t duration) {
//	uint32_t temp = duration, i=0;
//	temp *= SystemCoreClock / 1000000;	// total CPU cycles
//	if (temp < 30)
//		return;		// can't make it that precise, probably already elapsed
//	temp -= 30;		// idk, feel like it
//	temp /= 5;		// for loop count
//
//	for (i = 0; i < temp; i++)
//		;
//}

//void huansic_delay_us(u16 time)
//{
//   u16 i=0;
//   while(time--)
//   {
//      i=10;
//      while(i--) ;
//   }
//}

void huansic_delay_us(uint32_t duration) {
    uint32_t start_ms = milliseconds;
    uint16_t duration_ms = duration / 1000;
    uint16_t duration_us = duration % 1000;
    while(milliseconds - start_ms < duration_ms);
    uint32_t start_us = 0; microseconds = 0;
    while(microseconds - start_us < duration_us);
}

uint32_t huansic_chronos_milliseconds() {
	return milliseconds;
}

int8_t huansic_chronos_schedule(uint32_t scheduled_ms, void (*thread)(uint32_t ms)) {
	// if time already elapsed, return -1
	if (scheduled_ms <= milliseconds)
		return -1;

	// find a place to hold the information
	uint8_t i, target = 255, replc = 255, temp, holding;
	for (i = 0; i < REMINDER_LENGTH; i++) {
		if (!reminders[i].thread) {
			target = i;
			break;
		}
	}
	// if there's no place for it, return -2
	if (target == 255)
		return -2;

	// add the reminder
	reminders[target].scheduled_ms = scheduled_ms;
	reminders[target].thread = thread;

	// insert into the time wheel
	for (i = 0; i < REMINDER_LENGTH; i++) {
		// if it is empty
		if (wheel[(next + i) % REMINDER_LENGTH] == 255) {
			wheel[(next + i) % REMINDER_LENGTH] = target;
			return target;
		}

		// otherwise find the one to be inserted before
		if (reminders[wheel[(next + i) % REMINDER_LENGTH]].scheduled_ms <= scheduled_ms)
			continue;
		replc = (next + i) % REMINDER_LENGTH;
	}
	if (replc == 255)
		return -3;
	// shift everything after it backwards
	i = 0;
	holding = target;
	while(holding != 255) {		// only break when an empty slot is replaced
		// if somehow it reached a loop, return -3
		if((replc + i) % REMINDER_LENGTH == next) {
			return -4;
		}

		// replace
		temp = wheel[(replc + i) % REMINDER_LENGTH];
		wheel[(replc + i) % REMINDER_LENGTH] = holding;
		holding = temp;
		i++;
	}

	// return index on success
	return target;
}

void __huansic_systick_update_irq(void) {
    microseconds++;
    if(microseconds >= 1000){
        milliseconds++;
        microseconds = 0;
    }
	// push the wheel
	if (wheel[next] < REMINDER_LENGTH) {
		while (reminders[wheel[next]].scheduled_ms <= milliseconds) {
			fp = reminders[wheel[next]].thread;		// store the function pointer
			// condition the reminder
			reminders[wheel[next]].thread = 0;
			reminders[wheel[next]].scheduled_ms = 0;
			wheel[next] = 255;
			next = (next + 1) % REMINDER_LENGTH;
			// call the thread
			if (fp) {
				fp(milliseconds);}
		}
	}
}

__attribute__((interrupt("WCH-Interrupt-fast"))) void SysTick_Handler(void) {
	if (SysTick->SR) {
		SysTick->SR = 0;
		__huansic_systick_update_irq();
	}
}

 
其它
完整代码请移步GitHub: https://github.com/HUANsic/ICAR_Hover2024
或云盘:https://cloud.tsinghua.edu.cn/d/816506e0547a47b4a756/

附录C:车模照片
第三版车模图片
 

三、附录D:联系方式

  • 硬件相关: 吴宗桓 wu-zh20@mails.tsinghua.edu.cn
  • 机械相关: 吴宗桓 wu-zh20@mails.tsinghua.edu.cn
  • 底层代码相关:吴宗桓 wu-zh20@mails.tsinghua.edu.cn
  • 软件相关: 廖宇翚 liaoyh23@mails.tsinghua.edu.cn


评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卓晴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值