ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2024电赛H题自动驾驶小车:源代码与原理图完整拆解

2024电赛H题自动驾驶小车:源代码与原理图完整拆解

简介:这份资源是2024年全国大学生电子设计竞赛H题自动驾驶小车系统的完整工程,面向备战电赛的本科生、需要项目实战练习的计算机与自动化专业学习者,以及做课程设计或毕业设计的学生。包内共144个文件,以84个.h头文件与38个.c源文件为核心,涵盖MPU6050姿态解算、CAN通信、定时器与Flash控制等驱动模块,另附原理图PDF、工程配置与烧录文件、少量图片和说明文档,压缩包约10.26MB,结构清晰便于按模块查阅。资源来自导师指导并通过评审的高分设计,评审分99分,代码完整可运行,初学者也能按图索骥完成搭建与调试。目前已有2190人学习下载,适合需要完整赛题方案、硬件原理参考与排错思路的读者直接复用与二次开发。

1. 2024电赛H题自动驾驶小车:从源代码到原理图的完整拆解

2024年电赛H题要求做一辆能自主循线、识别路标、避障停车的自动驾驶小车,很多队伍卡在“代码跑不通、原理图对不上、电机一给PWM就复位”这三件事上。我拿到这套“源代码+原理图”的资料后,第一反应不是直接烧录,而是先把工程拆成三层:感知层(灰度/摄像头)、决策层(状态机)、执行层(电机+舵机)。这套方案适合正在备战电赛、想复现一辆能跑完全程的小车、或者想搞懂“为什么我的车一进弯就冲出赛道”的嵌入式新手和熟手。核心不是代码有多长,而是原理图和源代码之间的引脚映射、定时器分配、中断优先级有没有对齐——这才是高分项目和“能跑就行”之间的分水岭。

2. 先看懂原理图:电源树、电机驱动和最小系统怎么连

2.1 电源树:为什么你的单片机一给电机就复位

电赛小车最常见的翻车现场:电机一启动,单片机就重启,串口疯狂打印乱码。根源不在代码,在电源树。常见做法是单节锂电池(3.7V)升压到5V给单片机和外设,电机直接吃电池电压。但电机堵转瞬间电流能到2A以上,电池电压被拉低到3V以下,升压芯片输入不足,5V输出跟着掉,单片机欠压复位。

正确的电源树应该分三路:电池→电机驱动VM(直接供);电池→5V升压→单片机/灰度/编码器;5V→3.3V LDO→摄像头/OLED。关键参数:升压芯片选支持2A以上峰值电流的,比如TPS61088;5V和3.3V之间加磁珠或0Ω电阻隔离;电机驱动VM脚并一个470μF以上的电解电容,再并一个0.1μF陶瓷电容滤高频。

电源轨典型电压供电对象关键器件注意事项
VM7.4V/3.7V电机驱动电池直供并470μF电解+0.1μF陶瓷
5V5.0V单片机、灰度、编码器TPS61088峰值电流≥2A
3.3V3.3V摄像头、OLEDAMS1117-3.3输入输出加10μF

提示:原理图上电机驱动VM和单片机5V之间如果有共用走线,哪怕只有1cm,也要割断飞线。血泪经验:复位问题90%出在电源,不是代码。

2.2 TB6612电机驱动原理图:引脚映射和PWM频率

TB6612是电赛小车最常用的双路电机驱动,原理图连接不复杂,但引脚分配直接决定代码怎么写。典型接法:STM32的TIM1_CH1/CH2接PWMA/PWMB,GPIO接AIN1/AIN2/BIN1/BIN2,STBY接3.3V或GPIO拉高。关键参数:PWM频率建议10kHz~20kHz,太低电机会啸叫,太高驱动发热。ARR值按72MHz/10kHz=7200-1来设。

// TB6612 引脚定义(以STM32F103C8T6为例) #define MOTOR_A_PWM_TIM htim1 #define MOTOR_A_PWM_CH TIM_CHANNEL_1 #define MOTOR_A_IN1_PORT GPIOA #define MOTOR_A_IN1_PIN GPIO_PIN_4 #define MOTOR_A_IN2_PORT GPIOA #define MOTOR_A_IN2_PIN GPIO_PIN_5 #define MOTOR_B_PWM_TIM htim1 #define MOTOR_B_PWM_CH TIM_CHANNEL_2 #define MOTOR_B_IN1_PORT GPIOA #define MOTOR_B_IN1_PIN GPIO_PIN_6 #define MOTOR_B_IN2_PORT GPIOA #define MOTOR_B_IN2_PIN GPIO_PIN_7 #define STBY_PORT GPIOB #define STBY_PIN GPIO_PIN_0 // 电机方向控制:IN1=1, IN2=0 正转;IN1=0, IN2=1 反转 void Motor_Set(int16_t left, int16_t right) { // left/right 范围 -1000 ~ 1000 if (left >= 0) { HAL_GPIO_WritePin(MOTOR_A_IN1_PORT, MOTOR_A_IN1_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTOR_A_IN2_PORT, MOTOR_A_IN2_PIN, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(MOTOR_A_IN1_PORT, MOTOR_A_IN1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_A_IN2_PORT, MOTOR_A_IN2_PIN, GPIO_PIN_SET); left = -left; } __HAL_TIM_SET_COMPARE(&MOTOR_A_PWM_TIM, MOTOR_A_PWM_CH, left); // 右轮同理,略 }

逻辑说明:先判断方向,再设PWM占空比。参数说明:left/right用有符号整数,绝对值映射到ARR值。注意STBY必须拉高,否则TB6612输出高阻,电机不转。常见坑:TIM1_CH1和CH2如果用了互补输出,要关掉死区,否则PWM波形不对。

2.3 最小系统板和灰度传感器接口

STM32F103C8T6最小系统板原理图里,BOOT0/BOOT1跳线、复位电路、晶振电路是基础。灰度传感器常见5路或8路,输出数字量或模拟量。数字量接GPIO,模拟量接ADC。关键:灰度传感器的供电必须和单片机同一5V轨,否则电平不匹配。如果灰度输出是5V,STM32是3.3V容忍,但长期跑建议加电平转换或分压。

3. 源代码架构:状态机、PID和中断怎么排

3.1 主状态机:循线、识别、避障、停车

电赛H题的逻辑不复杂,但状态切换要干净。常见做法是用一个enum定义状态,主循环里switch-case。状态包括:IDLE、LINE_FOLLOW、CROSS_DETECT、OBSTACLE_AVOID、STOP。每个状态有进入动作、运行动作、退出条件。

typedef enum { STATE_IDLE, STATE_LINE_FOLLOW, STATE_CROSS_DETECT, STATE_OBSTACLE_AVOID, STATE_STOP } CarState_t; CarState_t g_state = STATE_IDLE; void Car_Update(void) { switch (g_state) { case STATE_LINE_FOLLOW: // 读灰度,算偏差,PID输出到电机 LineFollow_Update(); // 如果检测到十字或路标,切状态 if (Gray_IsCross()) { g_state = STATE_CROSS_DETECT; } break; case STATE_CROSS_DETECT: // 直行通过十字,计时或编码器计数 Motor_Set(600, 600); if (Encoder_GetDistance() > 200) { // 200mm g_state = STATE_LINE_FOLLOW; } break; case STATE_OBSTACLE_AVOID: // 超声波或红外触发,转向避障 Avoid_Update(); break; case STATE_STOP: Motor_Set(0, 0); break; default: break; } }

逻辑说明:主循环每1ms调用一次Car_Update。参数说明:Encoder_GetDistance返回累计脉冲换算的毫米数,200mm是十字宽度经验值。注意状态切换时清PID积分项,否则会“带记忆”冲出去。

3.2 PID循线:位置式还是增量式,参数怎么整定

灰度循线用PD就够了,I项容易积分饱和。常见做法:位置式PD,偏差=当前灰度加权位置-中心位置。P决定响应速度,D抑制震荡。整定顺序:先P后D,P从0.5开始加,直到小车能跟着线走但有点抖,再加D到不抖。

typedef struct { float Kp; float Kd; float last_error; float output; } PD_t; PD_t g_pd = { .Kp = 1.2f, .Kd = 0.8f }; float LineFollow_Calc(int16_t *gray, uint8_t n) { // gray: 灰度传感器权重,比如 -3,-2,-1,0,1,2,3 float error = 0; float sum = 0; for (uint8_t i = 0; i < n; i++) { error += gray[i] * (i - (n-1)/2.0f); sum += gray[i]; } if (sum == 0) return 0; // 丢线 error /= sum; float d = error - g_pd.last_error; g_pd.output = g_pd.Kp * error + g_pd.Kd * d; g_pd.last_error = error; return g_pd.output; }

逻辑说明:加权平均算偏差,PD算输出。参数说明:Kp=1.2,Kd=0.8是5路灰度的经验值,8路要重新整定。注意丢线时保持上一次输出或减速,不要直接归零。

3.3 中断优先级:编码器和串口谁先谁后

编码器用外部中断或定时器编码器模式,串口用DMA+空闲中断。优先级:编码器>串口>定时器。如果编码器中断被串口打断,丢脉冲会导致里程计算错。常见做法:编码器用TIM的编码器模式,不占中断;串口用DMA接收,空闲中断里处理。

4. 避坑与排查:电赛小车最常见的5个翻车点

4.1 现象:电机一转,OLED花屏或复位

原因:电源耦合,电机电流冲击5V轨。 解决:电机VM和5V分开走线,VM并大电容,5V加LC滤波。

4.2 现象:灰度循线直道还行,弯道直接冲出去

原因:PWM频率太低(比如1kHz),电机响应滞后;或者PD的D太大,过弯时反向刹车。 解决:PWM提到10kHz以上,D减半,速度降30%。

4.3 现象:编码器读数跳变,里程忽大忽小

原因:编码器A/B相接线太长没屏蔽,或者中断里做了浮点运算。 解决:A/B相用双绞线,中断里只做计数,浮点放主循环。

4.4 现象:十字识别误判,直道也停车

原因:灰度阈值没动态校准,环境光一变就误判。 解决:上电时先扫一遍地面,取黑白阈值中间值,或者用归一化。

4.5 现象:烧录后电机不转,但代码能跑

原因:TB6612的STBY没拉高,或者PWM通道没启动。 解决:检查STBY电平,HAL_TIM_PWM_Start必须调用。

5. 进阶:用编码器+IMU做里程校准和弯道预测

5.1 编码器里程校准:轮径和脉冲数怎么标定

编码器脉冲数×轮周长/每圈脉冲=距离。但轮径有误差,常见做法:让小车直行1米,读编码器脉冲,反推轮径。公式:轮径 = 1000mm × 每圈脉冲 / (脉冲数 × π)。标定一次,写进代码。

// 里程标定:直行1000mm,记录脉冲数 float wheel_diameter_mm = 65.0f; // 初始值 uint32_t pulses_per_rev = 13 * 4 * 30; // 编码器线数×倍频×减速比 float distance_per_pulse = (3.14159f * wheel_diameter_mm) / pulses_per_rev; void Calibrate_Distance(uint32_t actual_pulses) { wheel_diameter_mm = 1000.0f * pulses_per_rev / (actual_pulses * 3.14159f); distance_per_pulse = (3.14159f * wheel_diameter_mm) / pulses_per_rev; }

逻辑说明:用实际脉冲反推轮径。参数说明:pulses_per_rev根据编码器手册算,13线×4倍频×30减速比=1560。

5.2 IMU弯道预测:陀螺仪提前打角

MPU6050读角速度,积分得偏航角。弯道时,陀螺仪比灰度早10~20ms感知到转向,可以提前给舵机或差速。常见做法:陀螺仪Z轴角速度>阈值时,PD的Kp临时加大20%。

参数典型值作用
陀螺仪阈值50°/s触发提前打角
Kp增量+20%加快入弯响应
积分时间5ms陀螺仪读取周期

注意:陀螺仪零漂要上电校准,否则直道会慢慢偏。

5.3 一个具体技巧:用串口波形调PID

调PID最怕盲调。用串口把偏差、输出、灰度值打到上位机,看波形。常见做法:每10ms发一帧,格式“error,output\n”,上位机用匿名科创或VOFA+。看波形调Kp/Kd,比猜快10倍。

我自己的习惯:每次改完PID,先用手推小车看波形,再上电跑。这样能分清是代码问题还是机械问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进