
很多工科生的大学四年里都有一个绕不开的夜晚实验室的灯还亮着赛道上小车一遍遍试跑摄像头采回来的图像在屏幕上闪烁。那句“谨以此片纪念这段智能车生涯”记录的不只是比赛结果更是无数个调参、焊板、改代码的瞬间。这篇文章不想只做纪念我想把智能车开发这条路上的核心知识整理成一套可以照着做的实战教程包括硬件选型、环境配置、图像处理、PID 控制、代码调试和常见问题排查送给正在准备比赛或者打算入门智能车开发的你。智能车是一个很综合的嵌入式项目它把单片机、传感器、电机驱动、控制算法压缩在一个小车平台上。很多新手刚开始会觉得无从下手网上资料虽然多但比较零散。本文以最常见的摄像头循迹方案为主线从零开始梳理整个开发流程。无论你是参赛队员、毕业设计选了这个方向还是单纯想折腾嵌入式这篇文章都能帮你少走不少弯路。1. 背景与核心概念1.1 智能车到底在做什么智能车竞赛本质上是让一辆小车在没有遥控器的情况下依靠车载传感器识别赛道元素完成一圈又一圈的自主循迹。这里面最核心的输入是“赛道长什么样”最核心的输出是“左右轮各应该转多快”。把这句话拆开你会发现它涵盖了几个技术模块感知层用摄像头采集赛道图像或者用电感采集电磁信号。处理层单片机对采集到的数据进行二值化、边界提取计算出车相对于赛道的偏差。决策层根据偏差计算转向量根据速度反馈计算期望 PWM。执行层电机驱动芯片把 PWM 信号转换成电机转速编码器再把转速读回来形成闭环。一辆能稳稳跑完赛道的车靠的不是某一个环节特别强而是这四个模块的配合。很多队伍车跑不稳往往不是硬件坏了而是图像处理和 PID 参数不匹配。1.2 智能车常见的技术分组在正式动手之前先了解主流的几种方案方便你判断自己应该从哪个方向切入。摄像头组是最热门的方向。它用灰度摄像头采集赛道图像通过软件识别赛道边界。优点是信息量丰富能提前看弯道、识别十字路口和坡道缺点是需要花较多时间调图像处理算法。电磁组则用工字电感感应赛道下的交变磁场对硬件的依赖更大算法相对简单适合刚接触单片机的新手。此外还有单车组、越野组等它们对机械结构和控制算法的要求更高。对于大多数想入门智能车的人来说我的建议是先做摄像头的简单循迹不追求高速先把“图像采集 → 图像处理 → 转向控制 → 速度闭环”这条路跑通。这条链路跑通以后再考虑复杂元素识别和高速过弯。1.3 智能车开发为什么值得认真学智能车不是一个“玩具项目”它包含了嵌入式开发中最重要的一组能力资源受限下的编程、传感器数据的实时处理、控制理论的工程落地以及软硬件联合调试。这些能力在后续的嵌入式岗位、自动驾驶领域、机器人开发中都是通用的。更重要的是智能车项目倒逼你养成工程习惯。代码要版本管理参数要记录调参表硬件接线要有规范遇到问题要能根据现象反推原因。这些习惯不在课本里但比赛结束后你会发现它们比奖项本身更有价值。2. 环境准备与版本说明2.1 硬件清单智能车的硬件方案很多本文以最常见的摄像头后驱车为例列出基本组成。模块常见型号作用主控 MCUSTM32F407、K60、TC264图像处理与控制逻辑摄像头总钻风 OV7725、山外灰度摄像头采集赛道图像电机带编码器直流减速电机驱动后轮电机驱动DRV8701、BTN7971、MOS 全桥放大 PWM 驱动电机编码器Mini 编码器、欧姆龙编码器测量车轮转速电源7.2V 镍铬电池或 2S 锂电池整车供电稳压模块5V、3.3V DC-DC 模块给单片机、传感器供电具体型号要根据你所在队伍的主控方案来定不同学校常用的库不一样。本文重点讲思路代码层面以常见嵌入式 C 语言风格为例你需要根据自己的 SDK 做适配。2.2 软件工具链软件环境需要准备下面几类工具开发环境如果你用 STM32一般用 Keil MDK 或 STM32CubeIDE如果使用逐飞库开发的 K60、TC264则常用 IAR 或 CodeWarrior。摄像头调试工具逐飞串口助手、山外多功能调试助手用来查看摄像头采集的原始图像和处理后的二值图。波形查看工具VOFA 或者逐飞虚拟示波器用来观察 PID 输出、编码器速度、偏差值等数据曲线。串口工具任何支持十六进制收发功能的串口助手都可以。版本方面Keil MDK 建议使用 5.x 以上版本芯片支持包单独安装。IAR 需要根据芯片厂商选择对应的版本。由于各厂家库函数接口差异较大本文的代码以逻辑可移植为主平台相关部分会标注清楚你替换成自己 SDK 的对应接口即可。2.3 项目目录结构建议嵌入式工程的目录结构决定了后期维护效率。下面给出一个比较干净的划分方式。smart_car_project/ ├── doc/ # 原理图、芯片手册、调参记录 ├── firmware/ │ ├── app/ # 应用层图像处理、控制逻辑 │ ├── bsp/ # 板级驱动摄像头、电机、编码器、串口 │ ├── libraries/ # 官方库或逐飞库 │ └── main/ # 主函数入口 ├── tools/ # 串口助手、虚拟示波器 └── README.md # 工程说明很多队伍习惯把所有代码堆在 main.c 里前期跑通倒是快但一旦接入复杂元素识别代码会越来越难改。建议一上来就按“应用层”和“驱动层”分离的思路组织代码。3. 核心语法、配置或原理拆解3.1 图像处理从灰度图到赛道偏差摄像头输出的原始数据是一幅灰度图。控制车转向并不需要识别出物体是什么只需要知道“赛道在哪”。因此第一步通常是把灰度图转成二值图也就是把每个像素点标记为“白色赛道”或者“黑色背景”。二值化的关键在于阈值的选取。固定阈值最简单但不同光照下赛道灰度会变化效果不稳定。稍微进阶一点的做法是动态阈值常用的有最大类间方差法Otsu它根据图像灰度直方图自动计算区分前后景的最佳阈值。得到二值图之后下一步是提取赛道边界。逐行扫描图像从中间向两边寻找黑白跳变点就能得到左右边界位置。左右边界的中间点就是当前行赛道的中心线。将中心线位置与图像底部的预期位置做差得到转向偏差。下面给出一段伪代码风格的边界提取核心逻辑它不依赖具体硬件重点展示思路// 假设 image 是已经二值化的图像 // image[row][col] 1 表示白色赛道0 表示背景 // 输入二值图像输出当前行赛道中心偏移量 int16_t get_center_offset(uint8_t image[IMAGE_H][IMAGE_W]) { int row IMAGE_H - 1; // 从最靠近车身的一行开始扫描 int left_boundary -1; int right_boundary -1; for (int col IMAGE_W / 2; col 0; col--) { if (image[row][col] 1) { left_boundary col; break; } } for (int col IMAGE_W / 2; col IMAGE_W; col) { if (image[row][col] 1) { right_boundary col; break; } } if (left_boundary -1 || right_boundary -1) { return 0; // 本行没找到完整赛道返回 0 } int center (left_boundary right_boundary) / 2; return center - IMAGE_W / 2; // 返回相对图像中心的偏差 }这里有几个容易被忽略的细节扫描方向非常重要。如果车身压线图像底部一行容易整行都是黑的此时应从略高的行开始扫描。每行都计算边界会浪费算力。实际工程中通常只取底部若干行计算偏差或者对边界做“由近及远”的线性化处理。只取单行偏差的缺点是弯道信息不足后期可以提取多行边界拟合出赛道曲率。3.2 转向控制增量式 PIDPID 是智能车转向和速度控制中最常用的算法。它的三个参数各有分工。比例项 P 根据当前偏差大小给出与偏差成正比的输出。偏差越大转向越猛。积分项 I 累积过去的偏差用来消除稳态误差但智能车上使用不当容易造成积分饱和导致过弯抖动。微分项 D 根据偏差变化趋势提前刹车减少超调对弯道动态性能影响很大。智能车常用的是增量式 PID它并不直接计算目标 PWM 的绝对值而是计算每次输出相对于上一次输出的增量然后叠加到上一次输出上。这样做的好处是执行机构本身带有一定的记忆性输出变化更平滑不容易产生突变。一个典型的增量式 PID 计算函数如下typedef struct { float kp; float ki; float kd; float target; // 目标值 float last_error; // 上一次误差 float prev_error; // 上上次误差 float out; // 当前输出 } PidController; void pid_calc(PidController *pid, float current, float dt) { float error pid-target - current; float delta_out pid-kp * (error - pid-last_error) pid-ki * error pid-kd * (error - 2.0f * pid-last_error pid-prev_error); pid-out delta_out; pid-prev_error pid-last_error; pid-last_error error; }在转向控制中current是摄像头计算出的偏差target通常设为 0即希望车始终对准赛道中心。在速度控制中current是编码器读回的实时速度target是期望速度。增量式 PID 的两个输出限幅非常重要。如果不限幅弯道上偏差突然变大PWM 增量会瞬间冲到极限车就会猛地打方向甚至甩尾。一般做法是给out设置一个最大最小值并限制每次delta_out的变化幅度。3.3 速度闭环让车跑得稳速度环是智能车控制中相对容易被忽视的部分。很多新手一开始只做转向环车速靠开环 PWM 硬顶结果车越跑越慢或者忽快忽慢。速度闭环的结构是编码器读取两个后轮的实时转速 → 与期望速度做差 → 经过速度 PID → 输出电机 PWM。电机的响应比图像慢所以速度环的控制周期可以设置得比图像处理周期长一些常见做法是用定时器每 10ms 到 20ms 执行一次速度控制。需要注意的是编码器读数往往带有噪声直接用于 PID 会导致输出抖动。解决办法是在读取编码器后做一次简单的滤波。// 一阶低通滤波alpha 取值 0~1越小越平滑 float speed_filter(float raw_speed, float last_speed, float alpha) { return alpha * raw_speed (1.0f - alpha) * last_speed; }alpha 太大会导致速度滞后明显太小则噪声滤不干净。具体取值要看实际波形一般从 0.3 到 0.7 之间尝试。4. 完整实战案例前面把原理拆开了这一节用一个最小可运行的结构把整个控制链路串起来。由于不同开发板的 SDK 差异较大下面代码以 Core 逻辑为核心平台相关部分用函数名代替你可以在自己的工程里找到对应的函数填入。4.1 创建项目结构假设你已经用对应开发环境新建了工程先规划模块文件app/ ├── image_process.c # 图像二值化、边界提取 ├── image_process.h ├── control.c # 转向 PID、速度 PID ├── control.h └── main.c # 主循环4.2 定义数据结构为了让代码逻辑更清晰先把需要跨模块共享的数据封装成结构体。// control.h #ifndef CONTROL_H #define CONTROL_H #include stdint.h typedef struct { float kp; float ki; float kd; float target; float last_error; float prev_error; float out; float max_out; float max_delta; } Pid_t; typedef struct { float left_speed; // 左轮实时速度 float right_speed; // 右轮实时速度 float set_speed; // 期望速度 float steer_pwm; // 转向控制量 float motor_pwm; // 速度控制量 } CarState_t; void pid_init(Pid_t *pid, float kp, float ki, float kd, float max_out, float max_delta); float pid_calc(Pid_t *pid, float current); #endif4.3 编写图像处理逻辑图像处理模块的职责是输入一幅灰度图输出赛道偏差。下面把二值化和边界提取合并到一个函数里。实际工程中为了性能一般会直接在原始灰度图上做行扫描避免二次拷贝图像。// image_process.c #include image_process.h #define IMAGE_W 188 #define IMAGE_H 120 // 固定阈值二值化实际推荐使用动态阈值 static uint8_t binary_image[IMAGE_H][IMAGE_W]; void image_to_binary(uint8_t gray_image[IMAGE_H][IMAGE_W], uint8_t threshold) { for (int row 0; row IMAGE_H; row) { for (int col 0; col IMAGE_W; col) { binary_image[row][col] (gray_image[row][col] threshold) ? 1 : 0; } } } int16_t get_track_offset(void) { int row IMAGE_H - 15; // 取靠近车身区域的某一行 int left -1; int right -1; for (int col IMAGE_W / 2; col 0; col--) { if (binary_image[row][col] 1) { left col; break; } } for (int col IMAGE_W / 2; col IMAGE_W; col) { if (binary_image[row][col] 1) { right col; break; } } if (left 0 || right 0) { return 0; } return (left right) / 2 - IMAGE_W / 2; }这个实现简单但有效。真实场景中出场率最高的坑是某一行只有单边边界。比如车在急弯中车身内侧压线会先找到一条边界另一条始终找不到这时候返回 0 会导致车短暂失去转向后续可以用“记忆上一次有效偏差”的方式改善。4.4 编写 PID 控制逻辑PID 模块需要同时支持转向环和速度环。两者结构相同只是参数不同。// control.c #include control.h void pid_init(Pid_t *pid, float kp, float ki, float kd, float max_out, float max_delta) { pid-kp kp; pid-ki ki; pid-kd kd; pid-target 0.0f; pid-last_error 0.0f; pid-prev_error 0.0f; pid-out 0.0f; pid-max_out max_out; pid-max_delta max_delta; } float pid_calc(Pid_t *pid, float current) { float error pid-target - current; float delta pid-kp * (error - pid-last_error) pid-ki * error pid-kd * (error - 2.0f * pid-last_error pid-prev_error); // 限制单次变化量 if (delta pid-max_delta) { delta pid-max_delta; } else if (delta -pid-max_delta) { delta -pid-max_delta; } pid-out delta; // 限制输出幅值 if (pid-out pid-max_out) { pid-out pid-max_out; } else if (pid-out -pid-max_out) { pid-out -pid-max_out; } pid-prev_error pid-last_error; pid-last_error error; return pid-out; }调试时建议先把max_out设小一点比如 200 到 500让车在低速下先跑起来再逐步放开限幅。上来就给大舵量车会非常敏感很难判断是 P 太大还是 D 不够。4.5 编写主循环主循环要完成“采集图像 → 处理图像 → 计算转向 → 更新速度 → 输出 PWM”这一整套流程。不同方案的主频和摄像头帧率不同这里给出常见框架。// main.c 核心示意 #include control.h #include image_process.h static Pid_t steer_pid; static Pid_t speed_pid; static CarState_t car; void setup(void) { // 初始化摄像头、编码器、PWM 输出、串口 camera_init(); encoder_init(); pwm_init(); uart_init(); // 转向 PID 和速度 PID 初始化为不同参数 pid_init(steer_pid, 65.0f, 0.0f, 3.5f, 380.0f, 40.0f); pid_init(speed_pid, 25.0f, 0.8f, 0.2f, 4200.0f, 300.0f); car.set_speed 1500.0f; // 初始期望速度单位与编码器一致 } void loop(void) { // 1. 摄像头图像回调已经在 DMA 中断里完成这里直接取最新帧 uint8_t gray[IMAGE_H][IMAGE_W]; camera_get_frame(gray); // 2. 二值化并提取偏差 image_to_binary(gray, 128); int16_t track_offset get_track_offset(); // 3. 转向 PID目标 0当前偏差 steer_pid.target 0.0f; float steer pid_calc(steer_pid, (float)track_offset); car.steer_pwm steer; // 4. 差速输出转向量叠加到左右电机 set_left_duty((int)(car.motor_pwm - steer)); set_right_duty((int)(car.motor_pwm steer)); // 5. 速度环在定时器中断里周期性执行 } void timer_isr_10ms(void) { float left encoder_get_left_speed(); float right encoder_get_right_speed(); speed_pid.target car.set_speed; car.motor_pwm pid_calc(speed_pid, (left right) / 2.0f); }这个框架省略了具体芯片的寄存器操作因为不同 SDK 的函数名差异很大但流程是一致的。4.6 运行与验证把程序下载到小车后先不要放到赛道上跑。用串口把偏差值track_offset和转向输出steer打印出来观察两个数据摄像头正对直道时偏差应该接近 0并且抖动很小。用手把车在赛道上左右移动steer应该跟随偏差变化方向比偏差慢半拍。如果发现偏差方向反了最常见的原因是摄像头安装方向颠倒。如果速度环不工作先检查编码器读数是否正常可以用串口打印编码器的原始脉冲数。速度读取有问题时PID 调得再好也没有意义。5. 常见问题与排查思路智能车调试过程中很多问题看起来复杂实际原因就那么几个。下面汇总一些高频问题问题现象常见原因解决思路图像全黑或全白摄像头初始化失败、阈值设置不合适用串口助手查看原始灰度图调节曝光和阈值电机不转电源没接通、PWM 引脚接错、使能脚未拉高先用开发板输出固定占空比排除软件问题转向方向反了摄像头安装方向反、差速符号错误检查图像坐标系与转向输出的映射关系车左右摆动剧烈转向 P 过大、图像偏差抖动明显减小 P图像做滤波或降低扫描行分辨率速度环跑不上去编码器读数偏低、PWM 满占空比检查编码器供电和接线确认占空比范围过弯严重外抛转向 D 太大或速度环响应慢降低速度期望调整转向环 D 参数上电反复重启电源纹波大、稳压模块电流不足单独给舵机和电机供电避免与单片机共地干扰摄像头帧率低选择的输出模式不对单片机处理耗时过长使用硬件二值化摄像头或降低分辨率排查时我建议遵循一个原则每次只改一个变量。很多同学出问题后同时调整了 PID 参数、换了阈值、改了车速最后车出了问题根本不知道是哪一步引起的。正确做法是恢复到一个已知稳定状态然后只改变一个条件验证它的影响。6. 最佳实践与工程建议6.1 代码与调参规范智能车代码的复杂度看起来不高但它对实时性要求高而且需要在现场频繁改参数。没有规范的队伍往往到最后代码一团糟。参数配置建议集中管理。把所有 PID 参数、图像阈值、目标速度都放在一个config.h里不要散落在各个源文件中。调参时用串口指令实时修改避免每次烧录程序。// config.h #define STEER_KP 65.0f #define STEER_KI 0.0f #define STEER_KD 3.5f #define SPEED_KP 25.0f #define SPEED_KI 0.8f #define SPEED_KD 0.2f #define BASE_SPEED 1500.0f调参记录表是很多人忽略但非常关键的习惯。每次修改参数后记录下赛道类型、车速、现象哪怕只是拍一张屏幕照片。比赛前几天你会因为有记录而节省大量时间。6.2 硬件与供电安全硬件上最常出问题的是供电。智能车电机启动电流很大如果单片机、摄像头、编码器与电机共用同一组电源电机启动瞬间会把电压拉低导致单片机复位或者摄像头掉帧。规范的做法是驱动电源与控制电源分开。电机单独用动力电源单片机与传感器用稳压模块供电并且在电源入口并联一个大容量电解电容。共地处理也要注意必须在主控板处单点共地避免地线环路引入干扰。线径和接插件也不能将就。比赛整车剧烈震动杜邦线很容易松脱。建议直接把关键信号线焊死在 PCB 上或者使用带锁扣的接插件。6.3 团队协作与备赛节奏智能车比赛通常不是一个人能完成的。三个人如果都去写代码不如明确分工一个人负责机械与硬件一个人负责图像算法一个人负责控制与调试。每个人的代码需要放到同一个仓库里用 Git 管理避免互相覆盖。节奏上建议第一版车跑起来的时间尽量提前。很多队伍第一个月都在纠结摄像头选型迟迟没有跑起来。其实最快的路径是用最简单的方案哪怕只跑 1m/s先把整个链路打通。有了这个基础后面每一步优化都有明确目标提高一点速度观察哪里先撑不住再针对性地改进那个环节。6.4 不断用数据驱动调参很多同学调 PID 完全是靠感觉觉得转向太猛就减小 P觉得过弯慢就加大 P。这样调出来的参数换一段赛道可能就失效了。更可靠的做法是使用虚拟示波器观察曲线让数据告诉你哪里出了问题。转向环重点看偏差曲线和输出曲线是否平滑有没有明显的高频抖动。速度环重点看编码器速度是否紧跟目标速度超调量有多大。当你把曲线和现象对应起来之后调参的盲目性就会大大降低。7. 总结与学习路线智能车开发是一条上手门槛不低、但回报很实在的技术路线。它没有太多花哨的概念核心就是图像处理 PID 控制 嵌入式工程化这三件事。新手建议先照着本文的思路把最小系统跑通再逐步添加弯道识别、十字处理、坡道检测等元素。当你把一辆车调试到稳定跑完赛道时回头再看那些资料你会发现很多当时看不懂的细节都有了答案。这篇文章更像是一份“技术复盘笔记”。如果你正在准备自己的第一辆智能车可以先收藏备用把环境搭建和基础代码跑通然后在调试中慢慢补充属于你自己的参数经验。硬件方案和库版本会变但“感知 → 决策 → 执行”的链路和工程调试的思路是通用的。希望这篇整理能帮你少熬几个改代码的夜也希望你的智能车生涯不只是在比赛的录像里更留在你认真写过、认真调过的每一段代码里。