简介:这份资源是2024年全国大学生电子设计竞赛H题「自动行驶小车」的完整参赛工程方案,面向参加电赛控制类赛题的高校学生及嵌入式开发者,可用于复盘赛题实现思路、搭建小车控制框架或作为MSPM0平台开发模板。压缩包共89个文件,约386KB,以C源码与头文件为核心,涵盖PWM、OLED显示、MPU6050与JY61P姿态传感、控制逻辑等模块,并附带CCS工程配置、链接脚本、makefile及编译中间文件,目录结构清晰,便于直接导入调试。资源中保留了完整的工程组织方式与模块划分,读者可据此理解小车循迹、姿态解算与电机控制的代码架构,并参考其外设驱动与工程配置快速复现。目前已有3173人学习下载,适合需要系统参考电赛控制题实现路径的读者。
1. 从零复现一台能跑完全程的自动行驶小车:这份2024电赛H题方案到底值不值得拆
如果你正在准备2024电赛H题,或者手头有一辆四轮小车却卡在循迹抖动、弯道丢线、停车不准这些玄学问题上,那这份自动行驶小车方案大概率能帮你省下至少两周的试错时间。它不是一个只跑通一次的演示工程,而是一套包含主控逻辑、电机闭环、灰度循迹、状态机切换和参数整定的完整实现思路。适合谁?适合已经搭好车体、烧录过基础PWM、但一上赛道就翻车的同学;也适合带队的指导者,想找一份结构清晰、模块解耦、方便二次修改的参考框架。核心解决三件事:第一,把“能走”变成“能稳定走完全程”;第二,把调参从盲试变成有依据的逐步逼近;第三,把赛题要求的几个关键动作——直线加速、弯道减速、路口判断、定点停车——拆成可独立验证的模块。下面按我实际拆解的顺序,从硬件选型到代码落地,再到最容易踩的坑,一层层说清楚。
2. 硬件选型与信号链:为什么灰度传感器加编码器电机是H题最稳的组合
2.1 主控与执行机构的匹配逻辑
2024电赛H题的核心约束是赛道宽度、弯道曲率和停车精度。很多队伍一上来就堆算力,选主频几百兆的MCU,结果发现瓶颈根本不在运算,而在传感器响应和电机死区。我一般会推荐主频在72MHz到168MHz之间的ARM Cortex-M4或M3内核主控,比如常见的某系列开发板,资源足够跑双闭环PID,价格也友好。电机必须带编码器,而且是AB相正交输出,这样你才能做速度闭环。没有编码器的方案在直道上靠PWM开环勉强能走,一进弯道就原形毕露——电池电压稍微掉一点,转速就变,循迹环直接振荡。
驱动模块选双路H桥,持续电流至少2A,峰值别低于3A。这里有个血泪经验:很多标称3A的驱动板,实际散热没做好,跑两分钟就热保护,小车直接停在赛道中间。所以驱动板一定要加散热片,或者干脆选余量更大的型号。电源部分,电机和主控分开供电,电机用2节18650串联或3节镍氢,主控用另一路稳压到3.3V或5V。共地是必须的,但电源走线要分开,否则电机启动瞬间的压降会让主控复位,现象就是小车跑着跑着突然重启,像见了鬼一样。
2.2 灰度循迹传感器的布局与阈值标定
灰度传感器是H题循迹的主力。常见的有5路、7路、8路甚至16路。我的建议是至少7路,间距根据赛道黑线宽度来定。假设黑线宽1.8cm到2cm,传感器间距可以取1.2cm到1.5cm,保证在直道上同时有2到3个探头压线,弯道时外侧探头能提前感知。布局上,中间探头对准车体中轴线,两侧对称展开。传感器离地高度很关键,一般控制在8mm到15mm之间,太高了反射光弱,太低了容易刮蹭赛道。
阈值标定不能偷懒。很多代码里写死一个阈值,结果换场地就废了。正确做法是上电后先做一次动态标定:让小车在黑白区域各停留一次,分别读取每个通道的最大值和最小值,然后取中值作为阈值。下面这段伪代码展示了标定逻辑:
// 灰度传感器动态阈值标定 #define SENSOR_NUM 7 uint16_t sensor_max[SENSOR_NUM] = {0}; uint16_t sensor_min[SENSOR_NUM] = {4095}; uint16_t sensor_threshold[SENSOR_NUM]; void calibrate_sensors(void) { // 阶段1:小车置于白色区域,采集最小值 for (int i = 0; i < 200; i++) { for (int ch = 0; ch < SENSOR_NUM; ch++) { uint16_t val = adc_read(ch); if (val < sensor_min[ch]) sensor_min[ch] = val; } delay_ms(5); } // 阶段2:小车置于黑色区域,采集最大值 for (int i = 0; i < 200; i++) { for (int ch = 0; ch < SENSOR_NUM; ch++) { uint16_t val = adc_read(ch); if (val > sensor_max[ch]) sensor_max[ch] = val; } delay_ms(5); } // 计算阈值,取中值并留一定裕量 for (int ch = 0; ch < SENSOR_NUM; ch++) { sensor_threshold[ch] = (sensor_min[ch] + sensor_max[ch]) / 2; } }逻辑说明:先扫白再扫黑,分别记录每个通道的极值,最后取中值。参数上,采样次数200次是经验值,太少容易受噪声干扰,太多浪费时间。延时5ms是为了让ADC稳定。注意,如果某个通道的最大值和最小值差距小于500(12位ADC),说明该探头可能坏了或者离地太高,需要在调试时打印出来检查。
2.3 编码器测速与电机闭环的必要性
编码器测速常用的是M法(单位时间计数)或T法(单位脉冲计时)。在低速时M法分辨率不够,高速时T法又太占资源。我一般用M/T混合法,或者简单点,固定10ms定时器中断,读编码器脉冲数,换算成速度。关键参数是编码器线数和轮径。假设编码器是13线,四倍频后每转52个计数,轮径65mm,那么每个计数对应约3.9mm位移。10ms内读到N个计数,速度就是N * 3.9mm / 0.01s = 0.39N m/s。这个换算要写进代码里,后面调PID全靠它。
速度闭环用增量式PI就够了,位置式PID容易积分饱和。下面是一个典型的增量式PI实现:
// 增量式PI速度控制器 typedef struct { float kp; float ki; float target; float actual; float err; float err_last; float output; float output_max; float output_min; } PI_Controller; float pi_calculate(PI_Controller *ctrl) { ctrl->err = ctrl->target - ctrl->actual; float delta = ctrl->kp * (ctrl->err - ctrl->err_last) + ctrl->ki * ctrl->err; ctrl->output += delta; // 输出限幅,防止积分饱和 if (ctrl->output > ctrl->output_max) ctrl->output = ctrl->output_max; if (ctrl->output < ctrl->output_min) ctrl->output = ctrl->output_min; ctrl->err_last = ctrl->err; return ctrl->output; }参数说明:kp决定响应速度,ki消除稳态误差。一般先调kp,让电机能快速跟上目标速度但不过冲,再加一点点ki。output_max和output_min对应PWM占空比的上下限,通常设成满占空比的90%和-90%,留一点余量给转向差速。注意,左右电机要分别建一个PI控制器,因为两个电机的摩擦和死区不可能完全一致。
3. 循迹控制与状态机:把直道、弯道、路口拆成独立可调的状态
3.1 位置式PID循迹与加权偏差计算
循迹的本质是让传感器阵列的中心始终对准黑线。7路传感器可以输出一个偏差值。常见做法是给每个探头分配一个权重,比如从左到右权重为-3、-2、-1、0、1、2、3。当黑线压在某个探头上时,该探头输出1,否则0。偏差 = Σ(权重 * 探头状态) / Σ(探头状态)。这样直道时偏差为0,偏左为负,偏右为正。如果黑线在两个探头之间,偏差就是两个权重的平均值,分辨率够用。
然后把这个偏差送给位置式PID,输出转向修正量。位置式PID公式是 output = kp * err + ki * integral + kd * derivative。循迹一般只用PD,因为积分项在弯道容易累积导致回正过慢。下面是一个典型的循迹PD:
// 循迹PD控制器 typedef struct { float kp; float kd; float err; float err_last; float output; } PD_Controller; float pd_calculate(PD_Controller *ctrl, float error) { ctrl->err = error; float derivative = ctrl->err - ctrl->err_last; ctrl->output = ctrl->kp * ctrl->err + ctrl->kd * derivative; ctrl->err_last = ctrl->err; return ctrl->output; }参数说明:kp决定回正力度,太小了弯道切不进去,太大了直道画龙。kd抑制振荡,一般取kp的5到10倍。注意,偏差计算时如果所有探头都没压线(比如冲出赛道),要保留上一次的偏差或者进入丢线保护,不能直接归零,否则小车会直冲出去。
3.2 状态机设计:直道、弯道、路口、停车的切换条件
H题的赛道不是一条简单的黑线,它有路口、有直角弯、有停车区。用单一PID跑全程,必然在路口翻车。所以必须上状态机。我一般定义四个状态:STRAIGHT、CURVE、CROSS、STOP。切换条件靠传感器组合和编码器里程。
- STRAIGHT:中间探头压线,两侧无信号。目标速度设为高速,比如0.8m/s。
- CURVE:外侧探头持续检测到黑线超过一定时间,或者偏差绝对值大于阈值。目标速度降到0.4m/s,同时kp加大。
- CROSS:所有探头或者大部分探头同时压线,说明遇到横向路口。此时保持直行,用编码器计程,走过路口宽度后恢复循迹。
- STOP:编码器累计里程达到设定值,或者检测到停车标志(比如特定传感器组合)。目标速度设为0,进入刹车逻辑。
状态切换要加去抖,比如连续3个控制周期满足条件才切换,否则会在临界点反复横跳。下面是一个简化的状态机框架:
typedef enum { STATE_STRAIGHT, STATE_CURVE, STATE_CROSS, STATE_STOP } CarState; CarState current_state = STATE_STRAIGHT; uint32_t cross_start_time = 0; float total_distance = 0; void state_machine_update(float error, uint8_t sensor_pattern) { switch (current_state) { case STATE_STRAIGHT: if (fabs(error) > 1.5) { current_state = STATE_CURVE; } else if (sensor_pattern == 0x7F) { // 假设7路全压 current_state = STATE_CROSS; cross_start_time = get_tick(); } break; case STATE_CURVE: if (fabs(error) < 0.5) { current_state = STATE_STRAIGHT; } break; case STATE_CROSS: // 保持直行,直到走过路口 if (get_tick() - cross_start_time > 300) { // 300ms current_state = STATE_STRAIGHT; } break; case STATE_STOP: // 停车逻辑 break; } }逻辑说明:sensor_pattern是一个位掩码,每一位对应一个探头。0x7F表示7路全黑。cross_start_time记录进入路口的时间,300ms是经验值,具体要根据车速和路口宽度调整。注意,状态机里不要放太多阻塞操作,所有判断都基于当前周期的传感器和里程数据。
3.3 速度规划与加减速曲线
直道高速、弯道低速,这个切换不能太突兀,否则电机响应不过来,小车会点头。我一般用梯形加减速或者S形曲线。简单点,在状态切换时给目标速度加一个斜坡函数,比如每10ms增加或减少0.05m/s。这样电机电流不会突变,电池电压也稳一些。编码器里程用来判断是否到达减速点,比如距离弯道还有20cm就开始减速。
4. 避坑与排查:调车现场最常见的五个翻车点
4.1 现象:小车直道画龙,左右摇摆
原因:循迹kp太大,或者kd太小,系统阻尼不够。也有可能是传感器阈值没标定好,导致偏差计算跳变。 解决:先把kp减半,观察振荡频率。如果振荡变慢但幅度没减,加kd。重新做动态阈值标定,确保每个探头在黑白区域的读数差异大于800。如果某个探头差异小,检查离地高度和表面反光。
4.2 现象:弯道直接冲出去,不转向
原因:弯道目标速度太高,或者状态机没及时切到CURVE,还在用直道的低kp。也有可能是外侧探头没检测到黑线,因为传感器间距太大。 解决:降低弯道目标速度到0.3m/s试一下。检查状态切换条件,把偏差阈值调小。如果外侧探头够不着,要么加宽传感器阵列,要么在弯道前提前减速,用中间探头预判。
4.3 现象:过路口时小车突然加速或停车
原因:路口所有探头压线,偏差计算变成0,循迹环以为在直道,但实际路口可能有干扰。或者状态机进入CROSS后没有保持直行,反而被其他逻辑打断。 解决:在CROSS状态里强制输出直行PWM,屏蔽循迹输出。用编码器计程走过路口,不要依赖传感器。路口宽度要实测,比如20cm,按当前速度算需要多少ms。
4.4 现象:停车位置每次都不一样,误差超过5cm
原因:刹车逻辑太简单,直接切PWM为0,小车靠惯性滑行,滑行距离受电池电压和地面摩擦影响。或者编码器累计里程有累积误差。 解决:用反向刹车,即电机短暂反转,然后切0。反转时间通过实验确定,比如50ms。编码器里程要定期清零,或者在起点用传感器重新校准。停车前最后一段用低速爬行,比如0.1m/s,到位后再刹停。
4.5 现象:跑两圈后电机发热,速度变慢
原因:驱动板散热不够,或者电机内部摩擦增大。也有可能是PID积分饱和,输出一直顶在最大值。 解决:加散热片,或者换更大电流的驱动。检查PI控制器有没有限幅,积分项有没有抗饱和。如果电机本身质量一般,跑两圈歇一下,或者换更好的电机。
5. 参数整定与赛道验证:从能跑到跑稳的最后一百米
参数整定没有捷径,但有顺序。我一般按这个流程走:先调电机速度环,让左右轮在空载和负载下都能稳定到目标速度;再调循迹环,用手推着车走,看偏差输出是否合理;最后联调状态机,先低速跑全程,再逐步提速。每个环节都要记录数据,比如用串口打印偏差、速度、状态,事后画曲线。没有数据,调参就是玄学。
赛道验证要分阶段。第一阶段,只跑直道,看能不能走直线,偏差是否收敛。第二阶段,加一个弯道,看减速和转向是否平滑。第三阶段,加路口,看状态切换是否果断。第四阶段,跑全程,记录每圈时间。如果某一圈突然变慢,检查电池电压和电机温度。我习惯在代码里加一个电压检测,低于阈值就报警,避免电压跌落导致复位。
最后说一个具体技巧:用蓝牙或者无线串口模块实时回传数据,比插着USB线跑方便得多。你可以一边跑一边看偏差曲线,哪个弯道振荡,哪个路口切换慢了,一目了然。下面是一个简单的数据回传格式:
// 每50ms通过串口发送一次调试数据 void send_debug_data(void) { printf("STA:%d,ERR:%.2f,SPD_L:%.2f,SPD_R:%.2f,DIST:%.1f,VOLT:%.2f\r\n", current_state, last_error, speed_left, speed_right, total_distance, battery_voltage); }参数说明:STA是状态枚举值,ERR是循迹偏差,SPD_L和SPD_R是左右轮实际速度,DIST是累计里程,VOLT是电池电压。用printf重定向到串口,上位机用任意串口助手都能看。注意,发送频率不要太高,50ms一次足够,否则占用CPU时间影响控制周期。
从那以后我每次调车都强制走一遍“空载速度环→手推循迹环→低速全程→逐步提速”的流程,再急也不跳过。希望帮到你。
本文还有配套的精品资源,点击获取