
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或降压模块数字舵机启动3A220uF100nF5V逻辑轨低压差线性稳压摄像头、编码器500mA10uF100nF3.3V主控轨AMS1117-3.3STM32、SD卡300mA10uF100nF布局上有一条铁律功率地和信号地单点连接。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]后kp0.8、kd1.2、ki0max_omega4.0angle_k8。这组参数在室内赛道和室外强光环境下都能稳定跑完。但每队机械结构、舵机响应速度不一样参数必须重新标定。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 给下一届参赛者的协作建议开源目录整理过程本身也是团队协作能力的一次锻炼。建议队伍在比赛开始时就建立一个内部文档仓库不用追求完整只要有随手记的习惯。画板子时拍下关键调试波形调车时录下异常现象这些原始资料最后稍微整理就是高质量的开源文档。我们这次很多内容其实就是把当时随手拍的视频和聊天记录里的关键信息抽出来重写。如果你正在准备下一届智能车竞赛我的建议是前期多读几个成熟的开源项目不要急着抄特定电路而是看别人的系统结构理解每个模块存在的意义。开源项目不像教科书它会告诉你很多实际取舍比如电源轨怎么分、图像处理放哪个任务优先级、参数要留哪几份备份。读懂了这些你再去画第一版电路踩坑数量能明显减少。我在整理这个开源目录时最大的感受是比赛成绩是短暂的但一套规范化的工程习惯能带得很远。希望这些资料能帮到后面的人也希望看到这个目录的你能在自己队伍里也养成记录和开源的习惯。