news 2026/9/29 2:01:17

智能车竞赛开源实录:硬件设计、图像处理与PID角速度控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智能车竞赛开源实录:硬件设计、图像处理与PID角速度控制

1. 开源目录的由来和整体结构

1.1 为什么赛后才把开源目录整理出来

21届智能车竞赛结束后,很多朋友来问我们要工程文件,说“比赛都完了,能不能把代码和电路板资料分享一下”。说实话,比赛期间我们只想把车调快,根本没空整理这些。但问的人多了,我也意识到,智能车竞赛每年都有大量新手在重复造轮子:画同样的电源板、写同样的图像处理、调同样的PID。如果有一个相对规范的开源目录,后来的人至少能少走几个月的弯路。

这个开源目录对应的就是我们队——疯狂电路组soberup战队——在21届备赛期间沉淀下来的全部内容。它不是一个独立文件,而是一个有组织的仓库,包含硬件原理图、PCB源文件、软件工程代码、调试日志和赛道元素处理记录。不管你是报名了22届、23届的新队伍,还是单纯对嵌入式控制感兴趣的开发者,都可以从这里面找到值得参考的东西。特别是如果你正在纠结“摄像头图像怎么处理”“PID输出角速度怎么定”“环岛怎么识别”这三个问题,我建议你先把文档里的QuickStart看完再开始改代码。

所谓“疯狂电路组”,并不是官方分组。我们队内部有几名成员特别痴迷硬件,画板子风格比较野,经常为了一个电源方案熬到凌晨,时间久了大家就这么叫。这个外号后来也延续到了开源目录的命名上。我觉得挺好的,疯狂不代表乱来,而是在保证可靠的前提下,把电路设计做到极限。

1.2 仓库顶层结构和阅读顺序

开源仓库的目录设计遵循一个原则:任何人都能按顺序读完并跑起来。所以我把项目拆成了六个部分,每个部分对应一类需求。

soberup-smartcar21/ ├── README.md ├── LICENSE ├── docs/ │ ├── 0_QuickStart.md │ ├── 1_HardwareBuildGuide.md │ ├── 2_SoftwareArchitecture.md │ ├── 3_DebugLogs.md │ └── 4_RulesNotes.md ├── hardware/ │ ├── power_board/ │ ├── motor_driver_board/ │ ├── sensor_board/ │ └── main_board/ ├── software/ │ ├── drivers/ │ ├── algorithms/ │ ├── control/ │ ├── tasks/ │ └── project/ ├── tools/ │ └── debug_scripts/ └── media/ ├── photos/ └── videos/
  • README.md是总入口,里面有比赛成绩、硬件照片、视频链接和快速开始指引。第一次进入仓库的人先读这个文件。

  • docs/是文档区。我强烈建议按照数字顺序阅读:0_QuickStart.md教你如何准备环境,1_HardwareBuildGuide.md讲解焊接和接线,2_SoftwareArchitecture.md描述每个任务线程的逻辑,3_DebugLogs.md记录我们实测过程中出现过的奇怪现象,4_RulesNotes.md整理了对卓晴老师发布的21届规则文档的解读和注意事项。

  • hardware/里是四块板子的工程源文件。每块板子都有原理图和PCB源文件,还有制造工厂需要的Gerber文件。如果你只想打样,直接把Gerber文件夹扔给板厂就行。

  • software/是MDK5工程目录。我们用的是STM32F407VET6主控,代码分成drivers、algorithms、control、tasks和project。project下面才是完整的Keil工程,其它目录是模块化源码。

  • tools/是调试时用的上位机脚本和解析工具,可以配合串口把实时数据导出来画曲线。

  • media/是照片和视频,方便查看实物效果。

1.3 复现这个项目需要准备什么

硬件方面,复制我们的方案需要准备:STM32F407系列核心板(VET6最佳)、一颗OV7725摄像头、一个标准数字舵机、一个直流减速电机、一块两串锂电池。如果你做的是电磁组,板载的传感器板上我们已经预留了电磁运放电路,只用焊接四个工字电感就可以切换。

软件环境上,我们使用MDK5编译,配置工程用STM32CubeMX生成底层初始化。上位机推荐山外调试助手或者VOFA+,这两个都能直接显示灰度图像和PID曲线。调PID的时候,把误差值和时间序列通过串口发出来,看着曲线调参比盲调快五倍。

提示:仓库里不放编译好的bin文件,因为每队的主板型号和引脚映射不一样。拿到源码后先打开software/project/下的工程,根据你自己的接线修改sys_config.h里的引脚宏定义,再编译下载。

2. “疯狂电路组”的硬件遗产:四块板子的设计与取舍

2.1 电源板:把能量管好,车就成功了一半

智能车对电源的要求比较苛刻。我们用的电池是两串锂电,满电电压8.4V,放到7.4V左右就没多少余量了。舵机启动瞬间电流很大,摄像头和单片机又需要稳定低纹波的电源。如果整套系统只有一个电源模块,急加速时很可能会导致摄像头图像跳动,甚至单片机复位。

所以电源板的设计思路是分轨独立供电。电机驱动直接从电池供电,舵机单独用一路5V,逻辑电路再用另一路5V转3.3V。这样舵机抖动的电流变化不会串到摄像头和单片机上。

电压轨来源芯片负载对象电流需求关键电容
7.4V直供电池直连电机驱动板峰值8A470uF电解电容
5V舵机轨TPS5430或降压模块数字舵机启动3A220uF+100nF
5V逻辑轨低压差线性稳压摄像头、编码器500mA10uF+100nF
3.3V主控轨AMS1117-3.3STM32、SD卡300mA10uF+100nF

布局上有一条铁律:功率地和信号地单点连接。PCB上把功率地画成一块完整铺铜,信号地单独走线,最后在电源板入口处通过一颗0欧电阻相连。这能有效防止大电流在地上产生压差,把噪声传导到ADC采样和图像信号上。

2.2 电机驱动板:H桥电路与续流保护

我们用的电机是RS380或540级别的直流减速电机,正常电流两安左右,堵转可以到十安。驱动板选择了BTN7971B半桥芯片,两个芯片组成一个全桥,单颗芯片连续电流能力足够,封装还自带大面积散热焊盘,不需要额外加散热片。

驱动板上有三个关键点值得新手注意。

第一,续流二极管必须靠近桥臂放置。电机是感性负载,PWM关断瞬间会产生反向电动势,如果续流路径太长,尖峰电压会把驱动芯片打穿。很多驱动板烧毁都是因为这个原因,不是芯片电流不够,而是PCB布局太差。

第二,PWM频率不要乱设。我们最终用的是18kHz,既能避开音频噪声,又不会让开关损耗过高。如果你用的是其他MOS管,建议查一下栅极电荷和开关延迟,再决定频率。

第三,驱动板的逻辑电源要与功率电源分开走线,特别是在铺铜时不要为了省事把逻辑地直接大面积覆盖到功率地。否则单片机输出的PWM信号会跳动,车速控制不稳。

2.3 传感器板:摄像头和电磁信号的调理电路

摄像头板相对简单,就是给OV7725供电,并把D0-D7、PCLK、VSYNC、HREF、SIO_C、SIO_D这些信号用短排线引到主控板。这里有个非常容易踩的坑:摄像头排线不要太长。我们一开始为了布板方便,用了20cm长的杜邦线,结果图像在高速时频繁出现横纹,后来改成10cm软排线并加屏蔽层,问题才消失。

电磁组方案在传感器板上集成了四路工字电感调理电路。工字电感感应到的信号是几十毫伏级别的微弱交流信号,先经过放大电路放大,再做峰值检波。运放选型用的TLV2372,轨到轨输出,单电源3.3V供电就能满足需求。放大增益控制在20倍左右,太高会饱和,太低则远距离丢信号。

2.4 四块板子的分层布局心得

我们的硬件结构是三层堆叠:最底层是电机驱动板和电源板,中间是主控底板,最上层是核心板和摄像头接口板。这样做的目的是让大电流电路远离主控和传感器,同时方便维修。如果你只是想做一块集成板,也不是不行,但一定要保证驱动部分有足够的散热和隔离。

焊接顺序也建议标准化:先焊电源板,再焊驱动板,然后焊传感器板,最后才是主控板。每焊完一块都要独立上电测试。我记得我们当时焊完电源板,直接接了一个舵机反复打角,确认电压稳定才继续下一步。这种分段验证方法,比整机焊完再查故障节省太多时间。

实操心得:如果你打算复刻我们的电源板,建议把测试点做成清晰的过孔和排针。调试时万用表表笔直接插上去,比夹在芯片引脚上安全得多。

3. 图像处理链路与PID角速度输出:从摄像头到执行机构的完整流向

3.1 图像数据到底怎么变成控制量

很多新手拿到摄像头的第一反应是“我先把图像显示出来”。这个思路没错,但看完图像之后,要回答的核心问题是:图像里的哪几个像素点决定了小车的转向。

我们的处理流程是这样一条链:摄像头采集灰度图像 → 二值化 → 提取左右边线 → 计算中线偏差 → 方向PID → 输出目标角速度 → 映射到舵机角度或电机差速。

二值化阶段选用动态阈值,而不是固定阈值。因为赛道在不同光照条件下,灰度值变化很大,固定阈值会导致边线提取不稳定。动态阈值就是取图像中心区域的一个矩形窗口,统计窗口内的灰度均值,再根据均值乘以一个系数得到阈值。这个方法简单且鲁棒。

之后是边线提取。我们从图像底部往上扫描每一行,找出左跳变点和右跳变点。再把跳变点连续成线。这里的核心是处理丢线:赛道元素变化时,边线会缺失,我们采用近端线优先的补线策略,即用最近三行有效边线的斜率推测缺失位置。

3.2 为什么要把PID输出定义成角速度

这是我认为本项目最有参考价值的设计点之一。传统做法是直接把图像中线偏差送进PID,输出一个舵机PWM占空比。这在小曲率赛道没什么问题,但到了环岛和S弯,同样的偏差在不同车速下需要完全不同的舵机角度,PWM与路径之间的映射关系会因为速度变化而崩溃。

我们的做法是让PID输出期望角速度ω,单位是rad/s。原因在于,智能车运动学里,前轮转角与角速度、车速满足近似关系:ω ≈ V / R,其中R是转弯半径。图像处理阶段算出的偏差其实隐含了当前路径的曲率信息,而曲率的倒数就是转弯半径。以角速度为目标值,相当于直接对运动学量做控制,再去分配执行器,控制链条更干净。

对于舵机转向车,映射关系是:

// control.c 中方向环的核心片段 float error = get_lateral_error(); // 像素偏差,归一化到[-1, 1] float derivative = (error - last_error) / dt; // 误差变化率 float integral += error * dt; // 积分项,带限幅 float pid_out = kp * error + kd * derivative + ki * integral; // 把PID输出限幅为目标角速度 float omega = limit(pid_out, -max_omega, max_omega); // 通过增益和偏置映射到舵机角度 float target_angle = mid_angle + angle_k * omega; set_servo_pwm(angle_to_pwm(target_angle));

对于麦克纳姆轮或者差速转向车,角速度输出更直观,只需分配左右轮速:

float left_speed = speed_target - HALF_TRACK * omega; float right_speed = speed_target + HALF_TRACK * omega;

这样做之后,速度环和方向环就解耦了。你调速度时不需要重新调转向,因为角速度目标值只反映路径弯曲程度,不随车速变化。

3.3 方向PID三个参数的实际整定顺序

智能车方向PID我们只用了PD,很少开积分。原因是赛道偏差期望值基本为零,积分作用反而会引起过冲。具体整定时按这个顺序来:

现象参数调整方法原因说明
直道蛇形摆动增大kd,适当减小kp微分项抑制误差变化率,摆动通常意味着阻尼不足
S弯切弯不足增大kp比例项直接决定响应急度,S弯需要快速反应
入弯点头减小angle_k,限制max_omega目标角速度太大,舵机跟不上
弯道内侧剪裁路肩减小kd,增大kp微分项在弯道入口处有过预测,导致提前打角过猛
坡道后偏差恢复过慢加一点ki,限幅0.05坡道上下坡时重心变化产生恒定偏差,积分可以消除稳态误差

实测参考数值:图像误差归一化到[-1,1]后,kp=0.8、kd=1.2、ki=0,max_omega=4.0,angle_k=8。这组参数在室内赛道和室外强光环境下都能稳定跑完。但每队机械结构、舵机响应速度不一样,参数必须重新标定。

3.4 速度环与角速度限幅的配合

速度控制我们用的是增量式PI,输出直接作用到电机的PWM占空比。核心是要根据赛道曲率规划目标车速。这里的曲率信息可以直接从方向PID输出获得——PID输出越大,说明路径越弯,就应该降速。

// 弯道降速:根据方向环输出查表 float omega_abs = fabsf(omega); float speed_target = speed_base - speed_reduce(omega_abs); speed_target = limit(speed_target, min_speed, max_speed);

降速曲线是一个查表函数,我们把角速度分成几个区间,每个区间对应不同的目标速度。这样做比传统一条公式更稳,尤其适合场地光线变化导致图像误差突变的场景。角度限幅值max_omega我建议设为3.5到4.5rad/s。太大舵机物理上打不过来,输出反而饱和;太小弯道又转不过去。

4. 环岛、十字和坡道:赛道元素识别与实车调试记录

4.1 环岛在图像里到底长什么样

环岛是21届规则里最容易翻车的地方。很多队伍在仿真里跑得很顺,一上真车就在环岛冲出去。核心原因是没有理解环岛在图像传感器里的真实形态。

以右环岛为例,车在环岛入口看到的画面特征是:远端的右边界线突然消失,左边界线正常延伸,画面右侧出现一大片赛道内部区域。随着车辆前进,可以看到一个圆弧形的内边沿从下方出现并向左上弯曲,这就是环岛的内侧路肩。在二值化图像里,这个内边沿和正常赛道边界之间存在明显的高度差和斜率突变。

我们通过三个特征联合判断:

  • 丢线比率:右侧连续丢失边线的行数占上半屏总行数的比例超过阈值。
  • 中线斜率:左侧正常边线拟合出的斜率发生剧烈偏转,指向内侧。
  • 弧线检测:在远端区域内,检测到一段曲率半径显著小于正常赛道的白色弧线。

三点同时满足,才判定为环岛入口,避免把十字或大S弯误判成环岛。

4.2 用状态机管理环岛运行逻辑

环岛处理我们不搞复杂的机器学习,而是用一个简洁的有限状态机。状态迁移清晰,调参也方便。

状态触发条件控制策略
NORMAL默认状态正常巡线,方向环输出直接映射舵机
SEARCHNORMAL下检测到疑似入口保持当前方向,连续10帧确认
ENTERSEARCH确认强制向左或向右补偿目标角速度,切入环岛
INSIDE进入内弧区域用内侧弧线作为主参考,维持恒定的向心偏移
EXIT检测到出口直线段恢复NORMAL,清除状态标志

状态切换的时机非常重要。ENTER阶段不能过早,否则会沿着切线路肩冲出去;又不能不晚,否则会错过入口。我们用一个入口检测计数器和弧线曲率连续判断,只有当弧线的起点横坐标进入画面内侧三分之一区域才算真正进入。

4.3 十字和坡道的处理方式

十字路口在图像里通常表现为左右边线同时大面积丢失,或者四条边界交错。我们的策略很简单:如果当前状态是NORMAL且检测到左右同时丢线持续较短时间,则维持上一帧的方向输出,不进行额外转向,直接穿过十字。

坡道则要讲究速度。上坡时车身俯仰,摄像头视野变高,远处图像信息减少,这时候需要主动降一点速度,让车辆稳定贴坡;下坡时车头下压,图像里赛道突然放大,误差容易突变,我们会在下坡出口限制方向环的kd,防止出现抖动。

4.4 一条真实的调车流水账

我挑一段调试日志写在这里,正好反映调车的真实节奏。

第一次下地,车在直道正常,但进环岛必飞。我们用调试助手回放图像,发现环岛入口判断其实触发了,只是ENTER状态里目标角速度补得不够,车冲进环岛外侧草地。于是把补偿角速度从2.0提高到3.0,结果又出现环岛内车身抖动,因为方向环在INSIDE状态仍在用普通巡线参数,对弧线太敏感。后来单独为INSIDE状态做了一组保守参数:kp降低30%,kd提高20%,车身稳定下来。

随后又遇到一个奇怪现象:同一套代码,在下午室外场地直道频繁摆头。查了半天,不是算法问题,而是太阳角度变化导致动态阈值整体漂移,边线提取出现锯齿。我们增设了一行“曝光自适应”代码,每次采集图像后统计有效像素平均亮度,动态调整摄像头寄存器曝光值,这才解决。

5. 开源这件事本身:许可证选择、文档协作和托管经验

5.1 许可证选什么,直接影响别人敢不敢用你的代码

开源不是把代码丢到网上就结束了。如果你不声明许可证,别人拿到了代码也不敢合规使用,因为你保留了版权。在Gitee或GitHub建仓库时,一定要选一个LICENSE文件放进仓库根目录。

对智能车这类学生项目,我推荐MIT或Apache-2.0。两者的区别在于Apache-2.0多了一个专利授权条款,如果你使用了一些可能有专利风险的方法,它能在一定程度上保护贡献者和使用者。GPL-3.0不建议用在智能车开源目录里,因为它要求任何使用了该代码的项目也必须整体开源,后续队伍很可能因为赛道创意或学校项目需要保密而放弃使用你的代码。

许可证允许商用修改后必须开源必须保留版权声明含专利授权适合场景
MIT是否是否通用代码、学生项目
Apache-2.0是否是是涉及算法的完整项目
GPL-3.0是是是否纯学习交流、社区驱动项目

我们在仓库里最终选择了Apache-2.0,因为代码里有我们自己的图像处理算法实现,多个学校可以直接参考它做二次开发,不需要因为协办单位或队伍政策被迫全量开源。

5.2 README怎么写才有人看

开源目录的README就是门面。我见过太多仓库只有一个README语法模板,根本没人愿意看。合格的README应该包含:项目一句话介绍、演示视频链接、硬件照片、目录结构、快速开始步骤、常见问题、许可证声明。

快速开始步骤必须写到“傻瓜级”。举个例子,不要说“配置好编译环境”,而是写“安装MDK5.38 → 安装F407芯片包 → 打开project下的uvprojx文件 → 修改sys_config.h里的引脚定义 → 编译下载”。每一句话都应该能直接被执行。

另外,建议添加一个CHANGELOG.md文件,记录每次版本更新的内容。哪怕只是“修改了环岛判断阈值”“更新了供电板丝印”,也要记上。开源一段时间后你会发现,后续使用者反馈最多的就是版本问题,有changelog能省大量沟通成本。

5.3 Gitee和GitHub的托管方案

国内队伍首选在Gitee建主仓库,因为下载速度快,用的顺手。但考虑到很多来自海外的人在GitHub上关注了智能车项目,我们同时把仓库同步到GitHub上。同步方式不需要多复杂,两个平台各自建仓库,手动推送或者写一个简单的脚本都可以。不需要介绍多余的工具,直接备份推送三连:git add、git commit、git push。

发布的时候记得使用Release功能。每次打完一个稳定版本,打一个tag,附上发行说明。这样做既能回溯,也能让别人下载到某一阶段对应的源码和电路板文件,而不是永远面对当前最新的未稳定分支。

5.4 给下一届参赛者的协作建议

开源目录整理过程本身,也是团队协作能力的一次锻炼。建议队伍在比赛开始时就建立一个内部文档仓库,不用追求完整,只要有随手记的习惯。画板子时拍下关键调试波形,调车时录下异常现象,这些原始资料最后稍微整理就是高质量的开源文档。我们这次很多内容,其实就是把当时随手拍的视频和聊天记录里的关键信息抽出来重写。

如果你正在准备下一届智能车竞赛,我的建议是:前期多读几个成熟的开源项目,不要急着抄特定电路,而是看别人的系统结构,理解每个模块存在的意义。开源项目不像教科书,它会告诉你很多实际取舍,比如电源轨怎么分、图像处理放哪个任务优先级、参数要留哪几份备份。读懂了这些,你再去画第一版电路,踩坑数量能明显减少。

我在整理这个开源目录时,最大的感受是:比赛成绩是短暂的,但一套规范化的工程习惯能带得很远。希望这些资料能帮到后面的人,也希望看到这个目录的你,能在自己队伍里也养成记录和开源的习惯。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:01:16

C++ std::string的\0真相:data()与c_str()的本质区别

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:01:15

DC/DC恒压输出环路设计:控制架构、补偿网络与实测调环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:01:02

UE5.1角色移动实战:慢走/快跑/蹲伏的动画蓝图与增强输入实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:01:00

JavaScript字符串拼接5种方法详解:性能对比与工程选型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:59:58

C++手写希尔、快排、堆排、归并排序:从原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华