一个人备赛智能车:STM32+PID控制完整实战记录

发布时间:2026/8/30 1:27:32
一个人备赛智能车:STM32+PID控制完整实战记录 一个人备赛智能车最开始的感觉其实不是热血沸腾而是有点空旷没有队友可以一起讨论控制方案没有人帮忙扶着车模读串口数据赛道是自己贴的坑是自己一个一个踩的。我在大一阶段就选择了这样一条路一个人把一辆车从散件攒成能跑完一圈的完整系统。我把这个阶段叫作“蚂蚁搬家”——每一次只搬一点东西一份引脚定义、一段滤波代码、一个调试参数攒上两三个月车才终于能稳定地通过第一个弯道。这篇不是获奖感言也不是鸡汤我更想把它写成一份能照着操作的备赛记录和入门教程。无论你是准备开始备赛还是正在一个人孤军奋战希望这篇文章能帮你少走一些弯路。1. 背景智能车竞赛与“蚂蚁搬家”1.1 智能车竞赛是什么全国大学生智能车竞赛是国内高校中影响力很大的电子设计类竞赛之一每年举办一届赛制通常分为分区赛和全国总决赛。参赛队伍需要在一辆标准车模硬件基础上完成传感器安装、控制电路设计、嵌入式软件编写、控制算法调优最终让小车沿着赛道自动跑圈计时。比赛每年都会设置不同组别常见的方向包括电磁引导组通过电感检测赛道中线铺线产生的电磁场信号最简单可靠。摄像头组通过摄像头采集赛道图像识别赛道边界和元素信息量最大。光电/线性CCD组使用线性CCD或灰度传感器识别赛道灰度分布处理量介于电磁和摄像头之间。平衡组、单车组、多车编队组、创意组等每年规则会调整需要以当年发布的规则文档为准。这套竞赛非常适合想系统学习嵌入式、自动控制、传感器信号处理的人。它把一个完整的闭环控制系统压缩到一辆小车上从底层寄存器到上层控制策略都要自己搞定学一轮下来单片机开发能力会有非常明显的提升。1.2 为什么一个人也要“蚂蚁搬家”“蚂蚁搬家”这个说法是我给大一备赛状态起的一个外号。别人看到的是“一个人参赛”我自己看到的其实是“一个人把知识一块块搬进自己脑子里的过程”。智能车备赛涉及的东西非常杂机械车模拼装、支架固定、重心调整。硬件单片机最小系统、电机驱动、稳压电源、传感器接口。软件外设驱动、中断、ADC采集、PWM输出、串口调试。算法滤波、PID控制、赛道元素识别、速度规划。有队友的队伍可以分工推进一个人备赛则必须所有环节都接触。但反过来说这也意味着每一个环节都会被迫真正“懂”一遍而不是只负责自己那一块。一个人备赛最需要避免的是“一次搬太多”也就是试图一个月内把整车从零搭到满速度跑完。正确的节奏是蚂蚁搬家式的小步迭代先让车能动再让车能直行然后再让它过弯每一步都验证完再进入下一步。1.3 本文适合谁正准备参加智能车竞赛的大一、大二学生。正在一个人备赛缺乏队友交流的嵌入式爱好者。对STM32、PID控制、传感器数据处理有基础了解但想把它落到项目里的人。本文会从备赛路线规划、环境搭建、硬件组装、软件框架、PID调参、常见问题排查几个方面展开尽量做到技术细节和工程经验兼顾。2. 备赛前的关键认知与技术路线选择2.1 一个人备赛选哪个组别一个人备赛时最需要思考的不是“哪个组别上限高”而是“哪个组别调试回路短、踩坑成本低”。从个人经验来看电磁组和线性CCD组相对适合刚接触单片机的大一新生进入电磁组传感器数量少数据处理量小核心工作是电感采集、差比和归一化以及赛道元素判断。前期不需要图像处理经验一个人完全可以闭环。线性CCD组数据是一维灰度数组比摄像头二维图像简单很多能直观感受到“传感器——位置计算——转向控制”这个闭环。摄像头组信息最丰富、上限最高但图像采集、畸变矫正、边界提取、丢线判断这些环节一个人调起来会占据大量时间。如果前期单片机基础还不够扎实很容易卡在“图像能出来但车却完全不会控制”的阶段。当然最终选哪个组别必须参考当年的竞赛规则。规则文档里会写明允许使用的传感器类型、单片机芯片范围、车模规格等这些信息一定要以组委会发布的正式文件为准不要只看往届经验。2.2 技术路线选择一个人备赛建议采用最稳妥的组合单片机以STM32F1系列为入门资料多、生态成熟、遇到问题容易搜索到解决方案。开发方式使用STM32CubeMX生成初始化工程再在Keil或IAR中编写业务逻辑。控制算法从PID开始转向环用PD速度环用PI或PID。调试工具串口打印传感器原始值配合串口可视化工具观察曲线。这个路线不是唯一正确的但对一个人备赛来说它最大化地降低了“不会用”和“找不到人问”的风险。2.3 第一个月的任务拆解一个人备赛如果前一个月没有一个清晰计划后面很容易陷入“每天都在装机却一直没有进展”的焦虑。建议第一个月按下面顺序拆解阅读竞赛规则文档确定组别和可用的传感器、主控芯片范围。完成车模机械拼装确保前后轮转向顺畅。焊接或排查主板、电机驱动、稳压模块用万用表测通断电。点亮开发板最小系统跑通一个PWM输出让舵机工作。跑通编码器采集能算出轮速。把传感器数据串口发出来观察原始数据是否稳定。完成直道开环测试让小车能沿直线跑一小段。这一步看起来简单实际上已经覆盖了嵌入式开发里最核心的几个外设。整个月不求跑得快只求链路完整。3. 环境准备与版本说明3.1 开发环境以最常见的STM32开发环境为例操作系统Windows 10/11虚拟机下也能用但调试体验略差。开发工具STM32CubeMX用于图形化生成初始化代码Keil MDK或IAR用于编译和下载。调试工具串口助手或支持串口波形显示的软件例如VOFA、匿名上位机等。版本管理Git用来给代码做备份和版本回滚。具体用什么版本的Keil、什么版本的STM32CubeMX其实不需要过度纠结因为不同电脑环境差异很大。重点是保证工程能正确下载程序并且串口能稳定输出数据。3.2 硬件清单以下是一辆入门智能车通常会用到的主要部件部件作用车模套件提供底盘、转向舵机、驱动电机和轮胎单片机核心板运行控制程序采集数据并输出控制信号电机驱动模块放大控制信号驱动电机以较大电流旋转稳压模块把电池电压稳定到单片机等工作所需的电压编码器测量车轮转速提供速度反馈舵机控制前轮转向角度电磁传感器/摄像头/线性CCD检测赛道信息电池与充电器为整车供电这里要特别提醒每一届比赛对主控芯片、传感器类型、车模型号都有不同限制以上清单只是通用的硬件组成实际采购前一定要对照当年规则文档确认。3.3 软件工程结构一个人备赛也要养成目录清晰的好习惯。一个简单的STM32智能车工程建议按照功能模块拆分User/ main.c // 主函数 pid.c / pid.h // PID控制器 motor.c / motor.h // 电机PWM与编码器读取 servo.c / servo.h // 舵机PWM控制 sensor.c / sensor.h // 传感器采集与归一化 control.c / control.h // 转向与速度决策 debug.c / debug.h // 串口调试输出模块化不是为了炫技而是为了调参和排查问题。当舵机不转时你只需要排查servo模块而不是在一份300行的主函数里大海捞针。4. 车模组装与硬件调试4.1 机械组装要点硬件部分如果装得不好后面软件调得再好也会被机械误差吃掉。组装车模时要注意底盘螺丝不要一次拧死先全部预装调整完虚位后再紧固。前轮转向机构要顺滑不能有明显的卡滞。电池尽量放在底盘较低位置降低整车重心减少过弯时侧倾。传感器支架要稳固车速上来后如果支架共振图像或电感信号会剧烈抖动。一个人组装时最好用手机拍照记录每步安装顺序。这样拆了重装时不用凭记忆猜测螺丝位置。4.2 电源分配与稳压电源问题是很多新手的第一个隐蔽大坑。车模通常使用动力电池直接给电机驱动供电而单片机、传感器、舵机需要稳定电压。如果所有负载都从一个稳压模块取电电机启动瞬间的大电流会导致电压跌落轻则传感器数据抖动重则单片机复位。推荐做法是电池输出分两路一路给电机驱动另一路经过稳压后给单片机、传感器、舵机供电。所有电路共地但电源走线尽量分开。使用万用表确认各点电压正常后再插主控板上电。这里要特别提醒电池电量过低时不要强行测试否则电机瞬间压降容易让单片机进入复位循环造成“跑一下停一下”的假性异常。4.3 传感器安装与固定电磁组的电感通常安装在车头左右两侧和中心位置形成一条横向阵列。安装高度、电感间距都会影响赛道的感应范围。摄像头或线性CCD则需要有一个合适的俯仰角让传感器视野覆盖到车前方一段赛道。固定传感器时不要只用双面胶车速起来后振动会让传感器位置缓慢漂移。可以用3D打印支架或金属支架加固并在安装完成后把传感器数据通过串口打出来确认信号方向与车模左右转向是否一致。4.4 电机、编码器、舵机接线接线必须遵循“先确认、后通电”的原则。电机驱动输入端接单片机的PWM引脚输出端接电机编码器输出接单片机定时器的编码器模式引脚舵机接一个频率为50Hz左右的PWM通道。接线完成后先不要装轮胎空转测试确认电机方向是否和预期一致。舵机也别急着连杆连接前轮先单独给中位信号确认舵机臂能回到中间位置再连接机械结构。5. 软件框架与核心控制代码5.1 初始化PWM输出舵机通常需要50Hz左右的PWM信号脉宽在1ms到2ms之间对应左右极限角度。下面以STM32的HAL库为例。// 文件路径User/servo.c #include servo.h void Servo_Init(TIM_HandleTypeDef *htim, uint32_t channel) { // 启动PWM输出 HAL_TIM_PWM_Start(htim, channel); // 1.5ms脉宽对应舵机中位具体占空比按定时器频率计算 __HAL_TIM_SET_COMPARE(htim, channel, 150); } void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t compare) { // 限制脉宽范围防止舵机堵转 if (compare 200) compare 200; if (compare 100) compare 100; __HAL_TIM_SET_COMPARE(htim, channel, compare); }这里要说明代码里的compare 150并不是通用值而是取决于定时器时钟频率和PWM周期寄存器配置。如果你的定时器配置不同150可能不是1.5ms必须根据实际计算。这也是新手最容易直接复制代码却跑不动的原因之一。5.2 增量式PID实现转向控制我用的是PD控制速度控制用了PI控制。下面给出一个通用PID结构体既能算位置式也能改造成增量式。// 文件路径User/pid.h #ifndef __PID_H #define __PID_H typedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float error; // 当前误差 float last_error; // 上一次误差 float integral; // 积分累计 float output; // 输出 float max_output; // 输出限幅 } PID_t; void PID_Init(PID_t *pid, float Kp, float Ki, float Kd, float max_output); float PID_Calc(PID_t *pid, float feedback); #endif// 文件路径User/pid.c #include pid.h void PID_Init(PID_t *pid, float Kp, float Ki, float Kd, float max_output) { pid-Kp Kp; pid-Ki Ki; pid-Kd Kd; pid-target 0.0f; pid-error 0.0f; pid-last_error 0.0f; pid-integral 0.0f; pid-output 0.0f; pid-max_output max_output; } float PID_Calc(PID_t *pid, float feedback) { pid-error pid-target - feedback; pid-integral pid-error; pid-output pid-Kp * pid-error pid-Ki * pid-integral pid-Kd * (pid-error - pid-last_error); if (pid-output pid-max_output) pid-output pid-max_output; if (pid-output -pid-max_output) pid-output -pid-max_output; pid-last_error pid-error; return pid-output; }积分项一定要加限幅处理。如果你用的是位置式PID积分饱和会让小车在入弯前冲出去或者出弯后回正极慢。很多“直线发抖、弯道过冲”的问题都和积分没有限幅有关。5.3 编码器读取与速度计算速度闭环需要知道当前车速。我们通过定时器编码器模式读取轮子转过的脉冲数再换算成实际速度。// 文件路径User/motor.c // 假设编码器接口和定时器2绑定 int16_t motor_speed_read(void) { // 读取定时器计数值并清空计数器 int16_t count (int16_t)__HAL_TIM_GET_COUNTER(htim2); __HAL_TIM_SET_COUNTER(htim2, 0); // 根据减速比、轮径、编码器线数换算成速度单位可根据需要调整 // 公式里的常量需要结合你使用的车模实际参数计算 float wheel_circumference 2 * 3.14159f * 0.04f; // 单位米 float speed_ms (float)count * wheel_circumference / 20.0f; return (int16_t)(speed_ms * 1000.0f); }上面的代码里除数20.0f是我个人测试时根据编码器线数和采样周期估算出来的。不同车模、不同减速比、不同采样周期这个系数完全不同。正确做法是先让车模低速转一圈记录实际脉冲数再反推换算关系不要直接套用别人代码里的常量。5.4 主循环控制逻辑智能车主循环可以写得很朴素关键是循环周期的稳定。// 文件路径User/main.c while (1) { // 1. 采集传感器数据 sensor_read(); // 2. 根据传感器数据计算赛道偏差 float error controller_calc_error(); // 3. 速度规划根据当前偏差和目标速度计算期望速度 float target_speed speed_plan(error); // 4. 速度环PID speed_pid.target target_speed; float motor_pwm PID_Calc(speed_pid, current_speed); // 5. 转向环PD steer_pid.target 0.0f; float servo_pwm PID_Calc(steer_pid, error); // 6. 输出 motor_set_pwm(motor_pwm); servo_set_pwm(servo_pwm); delay_ms(10); }这个循环结构很简单但它体现了一个重要思路把“算偏差”和“调PID”分开。调试时先单独看每次循环算出的偏差确认偏差方向和量级正常再让PID介入控制否则你根本无法判断是传感器的问题还是控制器的问题。6. 调试方法与实战迭代6.1 先开环后闭环很多人上来就把PID参数填进代码结果小车原地打转、疯狂抖动根本分不清是传感器装反还是参数不对。我的建议是先写一段开环代码固定输出一个舵机PWM确认前轮能保持一个角度。再用遥控方式或者通过串口发送指令手动控制舵机和电机确认转向和加减速响应正常。然后才进入闭环而且第一次闭环时速度一定要低最好用手把车提起来空转测试避免失控撞墙。开环测试阶段不要怕慢它帮你把“执行机构是否正常”这件事彻底确认掉后面所有问题都集中在传感器和控制算法上。6.2 传感器数据归一化电磁组最常见的数据处理流程是采集多个电感的AD值。对每个AD值做滤波去除偶发毛刺。计算差比和error (left - right) / (left right)。根据正负号判断车模在赛道中的左右偏移。差比和的优点是能消除部分共模干扰。当电池电压下降时所有电感值可能按比例变化差比和的比值相对稳定。但要注意电感值过小时差比和会变得很敏感这时需要设置一个判断阈值当所有电感的总和小于某个值时说明当前赛道信息不可靠可以强制保持上次偏差或减速处理。6.3 转向PID调参顺序转向环通常只用PD就可以理由是小车的转向目标是跟踪赛道中线不需要用积分去消除静差积分反而容易造成过冲。我的调参顺序是先给一个很小的P比如1.0。固定速度低速情况下看小车是否能大致沿赛道走。逐渐增加P直到小车在直线段出现小幅抖动再退回一点。增加D观察是否减轻过弯时的震荡和超调。反复在直线和弯道上测试找到“既不迟钝也不发抖”的折中值。D项对噪声非常敏感。如果传感器原始数据没有滤波D项会把噪声放大成高频抖舵这个时候要先处理滤波而不是一直减D。6.4 速度PID与转向PID配合转向和速度不是独立的速度越高转向环面临的挑战越大。一个实用思路是先让转向环在一档低速下表现稳定然后逐步提高目标速度。每次提速后如果发现入弯太冲就在速度规划里加入“根据偏差减速”的逻辑而不是只靠加大转向P来处理。简单速度规划可以写成float speed_plan(float error) { float base_speed 1.2f; // 直道目标速度 float decelerate 0.8f; // 减速系数 float ratio 1.0f - fabs(error) * decelerate; if (ratio 0.3f) ratio 0.3f; return base_speed * ratio; }这样在直线段偏差接近0时速度跑满入弯偏差增大时自动减速出弯偏差变小时重新加速。这个策略很朴素但一个人备赛时足够稳定。6.5 用串口数据定位问题一个人备赛最大的问题是“看不到小车内部状态”。串口是你最重要的眼睛。调试时至少打印以下信息电池电压或系统是否复位的标志。传感器原始值或归一化偏差。当前目标速度和实际速度。舵机输出占空比。主循环周期。通过串口波形观察数据很多问题会一目了然如果传感器偏差曲线毛刺很多说明滤波不足或支架振动。如果实际速度始终跟不上目标速度可能是电机驱动限流或速度PID积分太弱。如果舵机占空比在直线段来回跳说明P或者传感器灵敏度太高。7. 常见问题与排查思路一个人备赛遇到问题最怕的是不知道怎么定位。下面是我整理的高频问题排查表问题现象常见原因解决思路舵机不转或方向相反PWM引脚错、占空比不对、舵机限位错误先单独测试舵机中位再检查PWM频率和占空比范围电机不转驱动供电不足、PWM占空比为0、使能引脚没拉高用万用表测驱动输入输出再用固定占空比测试编码器读数为0编码器引脚接错、定时器编码器模式未配置、共地问题检查AB相接线确认供电和共地用手转轮子看计数传感器数据抖动严重电源纹波、支架共振、滤波不足使用稳压模块、加固支架、增加滑动平均滤波跑直线时频繁左右摆转向P过大、传感器灵敏度过高、机械虚位大降低P加死区检查转向机构虚位入弯冲出去转向P不足、没有提前减速、速度太快加大转向PD、在速度规划中加入偏差减速刹车后单片机复位电流倒灌或电池压降过大检查电源回路电机驱动是否反接必要时加保护二极管排查问题的通用心法是一次只改一个变量。很多新手喜欢同时调P、调D、调速度、改滤波器结果车突然变好了也不知道是哪一步起的作用突然变差了也不知道是哪一步引入的。一个人备赛更要克制每改一处都要有记录。8. 一个人备赛的工程实践建议8.1 用Git管理代码一个人写代码同样要使用版本管理。不是每次提交都要写多规范而是为了保证你可疑把车“调好”之后第二天改坏时能退回昨天的状态。建议在关键节点打上tag比如v0.1-开环直行v0.2-速度闭环v0.3-转向闭环v0.4-过弯成功以后每次改动优先保证当前版本能跑再尝试下一个小特性。我见过很多队伍代码里写满了“最终版”“最终版2”“改坏不慌版”这种命名方式在一个人备赛时尤其危险。8.2 建立备赛记录文档不要只把代码存下来还要把调试过程中的发现写下来。比如某个弯道为什么一直冲出赛道、换了什么参数后效果变好、传感器的安装高度是多少。记录的价值在第二天会体现出来。连续调试到深夜大脑记住的细节有限第二天再看记录能迅速恢复到上一阶段的调试状态。8.3 对数据负责智能车本质上是一个数据处理系统任何控制决策都建立在传感器数据之上。因此看到异常表现时不要急着调PID先确认传感器数据是否可靠。一个最简单的验证方法是把车拿在手里左右晃动车头观察串口输出的偏差是否平滑地左右变化。如果数据本身是乱的控制器再强也救不回来。8.4 注意调试安全和规则边界备赛时要注意几个安全事项拆装轮胎时先拔掉电机电源线防止意外启动。在赛道上测试时只留最低速度防止失控撞到人或设备。检查电池状态不要使用鼓包或电压过低的电池。任何生产环境或比赛现场的修改都要提前备份不要在开赛前最后一刻改动参数。同时一定要遵守当届竞赛规则。不要使用规则不允许的传感器或辅助设备否则即使跑得再快成绩也可能作废。8.5 时间与精力管理一个人备赛最容易出现的情况是某一周疯狂熬夜调车身体状态下滑然后连续几天没有进展。蚂蚁搬家的另一个含义是“持续而稳定”。建议制定一个可以坚持的节奏每周固定一个时间做硬件维护和测试。每次调试前写清楚本次要验证的目标。调试结束后简单记录结果。不要因为一次失败就反复熬夜大脑疲劳时往往只会制造更多Bug。9. 总结与下一步学习路线当我在大一选择一个人备赛时最大的收获不是车最后跑得多快而是我被迫成为了一整套系统的负责人机械、硬件、软件、算法、调试、记录都必须亲自完成。“蚂蚁搬家”式的学习方式让我在一次次微小的迭代中把零散的知识真正沉淀成了自己的方法。如果你也想走这条路这几点是我最想单独强调的先保证链路完整再追求速度。能直行永远比跑得快但总翻车更有价值。每一个参数都要有来路不要迷信网上的代码要理解它背后的原理。遇到问题先用串口还原数据再动手改代码。一个人也要写文档、用Git、留备份这能帮你长期保持可维护性。下一步可以按下面的路线继续深入学完PID之后去理解模糊控制、模型预测控制等更现代的控制方法。学习数据处理中的滤波算法例如卡尔曼滤波、互补滤波。了解RTOS的基本概念尝试在主控上引入多任务调度。把备赛中积累的调试经验整理成文档分享给后来者。智能车竞赛的终点是比赛但备赛过程本身已经足够让人把单片机、控制算法和工程思维完整地学一遍。如果你也正准备一个人开始备赛不用怕起点低不用怕进度慢。像蚂蚁搬家一样一次搬一段代码、一块结构、一个知识点攒上几个月你大概率能收获一辆属于自己的智能车。希望这篇备赛记录能帮你在出发时找到方向在焦虑时稳住节奏。