在电子设计竞赛(电赛)中,小车类题目是控制类赛题的常客,从基础的循迹、避障到复杂的平衡、滚球控制,每年都吸引着大量参赛者。对于大一新生而言,面对“电赛飞线小车”这样的项目,挑战不仅在于理解单片机、传感器和电机驱动,更在于如何将一堆零散的模块通过“飞线”(即杜邦线)可靠地连接起来,并编写出稳定、高效的控制程序。这个过程充满了从硬件连接到软件调试的实战细节,任何一个环节的疏忽都可能导致小车“罢工”。
本文将以一个典型的电赛小车项目为背景,假设目标是完成一个具备循迹与动态避障功能的智能小车。我们将从零开始,梳理硬件选型、电路连接(飞线技巧)、核心代码编写、参数调试到最终系统集成的完整流程。文章不仅会提供可运行的代码框架,更会重点解释为什么这样连接、为什么这样编程,以及当小车不按预期运行时,应该如何一步步排查问题。无论你是初次接触电赛的大一同学,还是希望巩固嵌入式开发基础的开发者,都能通过本文构建一个清晰、可复现的小车开发与调试思路。
1. 理解电赛小车的核心系统构成
一个典型的电赛智能小车,其本质是一个移动的嵌入式系统。它通过传感器感知环境(如地面黑线、前方障碍物),由微控制器(如STM32、Arduino)作为大脑进行决策,最后通过执行器(电机)来驱动车轮行动。理解这个信息流的闭环,是设计和调试一切功能的基础。
1.1 硬件模块分解与选型依据
电赛小车通常由以下几个核心模块构成,选型时需要平衡性能、成本、易用性和电赛题目要求。
| 模块类别 | 常见型号/类型 | 核心作用 | 选型考量与电赛常见要求 |
|---|---|---|---|
| 主控MCU | STM32F103C8T6 (蓝桥杯/电赛常用)、Arduino Uno/Nano、ESP32 | 系统大脑,运行控制算法,处理传感器数据,产生PWM驱动电机。 | STM32:性能强,外设丰富(多定时器、ADC),适合复杂控制(如PID、平衡)。Arduino:生态好,库丰富,上手快,适合快速验证。ESP32:集成Wi-Fi/蓝牙,适合需要无线通信的题目。电赛控制类题目通常对实时性有要求,STM32系列是更主流的选择。 |
| 电机驱动 | L298N、TB6612FNG、DRV8833 | 接收MCU的小电流控制信号,驱动电机所需的大电流。 | L298N:经典、皮实、易购,但发热大,效率较低。TB6612FNG:效率高,发热小,支持待机模式,体积小,是当前更优选择。DRV8833:集成度更高,适合小型小车。选型需计算电机工作电流,确保驱动芯片峰值电流留有裕量。 |
| 直流减速电机 | TT马达(带编码器/不带编码器)、N20减速电机 | 提供车轮动力,减速齿轮箱提供更大扭矩。 | 需关注电压(如3-6V)、空载转速、减速比(决定最终转速和扭矩)、是否带编码器。编码器用于精确测速和闭环控制,在需要精确速度控制或里程计算的题目中是必备的。 |
| 循迹传感器 | TCRT5000红外反射模块、灰度传感器阵列 | 发射红外光并接收反射光强度,用于识别地面黑线与白底。 | TCRT5000价格低廉,需调节电位器改变灵敏度。电赛中常使用多个(如3-5个)组成阵列,以检测小车相对于黑线的位置偏差。数字输出(DO)简单,模拟输出(AO)可提供更精细的灰度值。 |
| 避障传感器 | HC-SR04超声波模块、红外避障模块 | 检测前方障碍物距离。 | HC-SR04:测量距离精确(2cm-400cm),但测量有最小盲区,响应速度相对慢。红外避障:检测距离短(几cm到几十cm),易受环境光干扰,但响应快,成本低。根据题目障碍物特性选择或融合使用。 |
| 电源管理 | 18650锂电池组、降压模块(如LM2596)、升压模块 | 为整个系统提供稳定、合适的电压。 | 电机启动电流大,会导致电源电压瞬间跌落,可能造成MCU复位。关键实践:必须为MCU和逻辑电路(传感器、驱动芯片逻辑供电)提供独立于电机动力的稳压电源。常用方案:锂电池(如7.4V)->降压模块(降至5V)供逻辑部分;电机直接由锂电池或通过驱动模块供电。 |
1.2 “飞线”连接的艺术与隐患
“飞线”指用杜邦线连接各个模块。它灵活,但也是项目中最主要的故障来源之一。
- 连接逻辑:必须严格遵循“共地”原则。即所有模块的GND引脚必须最终连接到电源的负极,形成一个统一的参考零电位。任何地线环路缺失或接触不良,都会导致信号紊乱、传感器读数不准、单片机程序跑飞。
- 电源分配:如前所述,强烈建议将电机动力电源与控制逻辑电源在物理上分离或通过电感、磁珠隔离。最简单的做法是使用两套电池,或者使用一个电池但通过两个独立的稳压电路输出。
- 线缆整理:杂乱无章的飞线极易被车轮绞住或互相拉扯导致脱落。使用扎带、热熔胶或绝缘胶带将线缆分组固定在小车底盘上,是保证长期运行稳定的必要步骤。
- 接触可靠性:杜邦线接头反复插拔容易松动。在比赛前,对关键连接点(如电源、电机线)可以用热熔胶轻微加固(注意不要影响后续拆卸)。
2. 从零搭建硬件系统与最小软件测试
我们以STM32F103C8T6(核心板)+TB6612FNG电机驱动+带编码器的TT马达+TCRT5000循迹模块+HC-SR04超声波模块为例,演示硬件连接和基础驱动软件。
2.1 硬件连接清单与原理
以下连接示意基于STM32标准库开发环境。请务必先对照原理图或引脚定义表进行操作。
电源部分:
- 将一组7.4V 18650电池的正负极接入TB6612FNG的
VM(电机电源)和GND。 - 使用一个降压模块(如LM2596),从电池正负极取电,输出稳定的5V,连接到核心板的
5V或VIN引脚(如果核心板有5V稳压),并为TB6612FNG的VCC(逻辑供电)、循迹模块、超声波模块供电。 - 所有模块的GND最终都必须连接到这个5V电源的GND上。
- 将一组7.4V 18650电池的正负极接入TB6612FNG的
电机与驱动连接:
- 左电机线接TB6612的
A01和A02,右电机线接B01和B02。 - TB6612控制引脚连接STM32:
STBY(使能)接STM32任一GPIO(如PC13),高电平有效。AIN1,AIN2控制电机A(左)方向,接PA1,PA2。BIN1,BIN2控制电机B(右)方向,接PA3,PA4。PWMA,PWMB接收PWM信号控制速度,接STM32的定时器通道引脚(如TIM2_CH1 (PA0)和TIM2_CH2 (PA1),注意避免与方向引脚冲突)。
- 左电机线接TB6612的
编码器连接(如果电机带编码器):
- 编码器一般输出A、B两相正交信号。分别接到STM32的定时器编码器模式引脚(如
TIM3_CH1 (PA6)和TIM3_CH2 (PA7)用于左电机,TIM4_CH1 (PB6)和TIM4_CH2 (PB7)用于右电机)。
- 编码器一般输出A、B两相正交信号。分别接到STM32的定时器编码器模式引脚(如
循迹传感器连接(以3个为例):
- 三个TCRT5000的
VCC接5V,GND接GND。 - 数字输出
DO引脚分别接STM32的GPIO,如PB12,PB13,PB14。也可以使用模拟输出AO接STM32的ADC引脚,实现灰度循迹。
- 三个TCRT5000的
超声波模块连接:
VCC接5V,GND接GND。Trig(触发)接STM32 GPIO(如PB8)。Echo(回响)接STM32 GPIO(如PB9),该引脚应支持输入捕获或外部中断,以精确测量高电平时间。
2.2 基础驱动代码:让小车动起来
在开始复杂算法前,先确保每个底层模块都能被正确驱动。这里以STM32 HAL库为例,展示关键初始化代码。
电机驱动初始化(PWM和GPIO):
// motor.c #include “motor.h” TIM_HandleTypeDef htim2; // PWM定时器 GPIO_InitTypeDef GPIO_InitStruct = {0}; void Motor_Init(void) { // 1. 初始化控制方向的GPIO引脚 (AIN1, AIN2, BIN1, BIN2, STBY) __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // STBY 引脚 __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_13; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // 使能驱动芯片 // 2. 初始化PWM定时器 TIM2, Channel1 (PA0) 和 Channel2 (PA1) // 注意:这里PA0和PA1与上面的方向引脚冲突,实际项目需根据板子原理图选择不冲突的引脚,例如使用PA8和PA9。 // 此处仅为示例流程。 htim2.Instance = TIM2; htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1000 - 1; // 1MHz / 1000 = 1kHz PWM频率 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始占空比0 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); } // 设置左电机速度和方向 void Set_LeftMotor(int16_t speed) { // speed范围 -1000 ~ 1000 if (speed >= 0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // AIN1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); // AIN2 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, speed); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, -speed); } } // 右电机函数类似...关键解释:PWM频率通常选择1kHz-10kHz。频率太低电机噪音大,太高可能超过驱动芯片响应能力。
Period寄存器值决定了PWM分辨率,这里设为999,则速度可设0-999。方向控制是通过给AIN1/AIN2高低电平组合实现的,具体需参考TB6612数据手册。
超声波测距基础函数:
// ultrasonic.c #include “ultrasonic.h” uint32_t high_level_time = 0; uint8_t measure_done = 0; void Ultrasonic_Trigger(void) { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); // 触发信号至少10us高电平 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); } // 在Echo引脚上升沿/下降沿中断中计算高电平时间 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { static uint32_t start_time = 0; if (GPIO_Pin == ECHO_Pin) { if (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET) { // 上升沿,开始计时 start_time = HAL_GetTick(); // 使用毫秒计时,精度有限。高精度应用需用定时器输入捕获。 } else { // 下降沿,结束计时 high_level_time = HAL_GetTick() - start_time; measure_done = 1; } } } float Get_Distance_CM(void) { Ultrasonic_Trigger(); measure_done = 0; uint32_t timeout = HAL_GetTick() + 100; // 100ms超时 while (!measure_done && HAL_GetTick() < timeout) { // 等待测量完成或超时 } if (measure_done) { // 距离 = (高电平时间 * 声速) / 2 // 注意:HAL_GetTick()单位是ms,声速340m/s = 0.034cm/us // 高电平时间单位ms,换算:distance = high_level_time * 0.034 * 1000 / 2 return (high_level_time * 17.0f); // 简化计算,实际误差较大,建议使用定时器微秒级计时。 } return -1.0f; // 超时或测量失败 }注意:上述代码使用
HAL_GetTick()毫秒定时器测量,对于超声波测距(厘米级精度)误差很大,仅用于演示流程。实际项目必须使用定时器的输入捕获功能来测量微秒级的高电平时间,才能获得准确距离。
3. 核心算法实现:循迹与动态避障
当底层驱动验证无误后,便可以开始实现上层逻辑。循迹的核心是PID控制,动态避障的核心是状态机决策。
3.1 循迹算法:PID控制
假设我们使用3个数字式TCRT5000(左、中、右)。定义传感器检测到黑线为1,白底为0。
// track.c #define SENSOR_LEFT 0 #define SENSOR_MIDDLE 1 #define SENSOR_RIGHT 2 uint8_t sensor_values[3]; // 存储三个传感器状态 int32_t line_position = 0; // 虚拟的“线位置”,用于PID计算 int32_t last_error = 0, integral = 0; // 读取传感器并计算位置偏差 void Read_Line_Sensors(void) { sensor_values[SENSOR_LEFT] = HAL_GPIO_ReadPin(SENSOR_L_GPIO_Port, SENSOR_L_Pin); sensor_values[SENSOR_MIDDLE] = HAL_GPIO_ReadPin(SENSOR_M_GPIO_Port, SENSOR_M_Pin); sensor_values[SENSOR_RIGHT] = HAL_GPIO_ReadPin(SENSOR_R_GPIO_Port, SENSOR_R_Pin); // 简单的权重法计算偏差。假设传感器间距相等。 // 定义:左传感器为-100,中传感器为0,右传感器为+100 line_position = 0; if (sensor_values[SENSOR_LEFT]) line_position -= 100; if (sensor_values[SENSOR_MIDDLE]) line_position += 0; if (sensor_values[SENSOR_RIGHT]) line_position += 100; // 如果都没检测到(出界),可以根据上一次偏差进行恢复或停车 } // 增量式PID计算(输出为速度调整量) float PID_Calculate(int32_t target, int32_t current) { float Kp = 0.5f, Ki = 0.01f, Kd = 0.2f; // PID参数,需实际调试 int32_t error = target - current; // 目标位置是0(中线) float P_out = Kp * error; integral += error; float I_out = Ki * integral; float D_out = Kd * (error - last_error); last_error = error; float output = P_out + I_out + D_out; // 输出限幅 if (output > MAX_OUTPUT) output = MAX_OUTPUT; if (output < -MAX_OUTPUT) output = -MAX_OUTPUT; return output; } void Line_Following_Task(void) { Read_Line_Sensors(); float pid_output = PID_Calculate(0, line_position); // 目标偏差为0 int base_speed = 500; // 基础速度 int left_motor_speed = base_speed - pid_output; int right_motor_speed = base_speed + pid_output; Set_LeftMotor(left_motor_speed); Set_RightMotor(right_motor_speed); }PID调试心得:
- 先P后I再D:先将
Ki和Kd设为0,增大Kp直到小车沿着线摆动但能大致跟上。 - 加I消静差:如果小车在直道上始终无法稳定在中心(存在静态误差),适当加入较小的
Ki。 - 加D抑振荡:如果小车摆动过于剧烈(振荡),加入
Kd可以抑制这种振荡,使曲线更平滑。 - 实时调整:最好能通过蓝牙串口等工具,在电脑上实时修改参数并观察小车响应。
3.2 动态避障决策:状态机
避障逻辑需要与循迹逻辑结合。一个简单的状态机可以是:默认循迹 -> 发现障碍 -> 停车/绕行 -> 恢复循迹。
// avoidance.c typedef enum { STATE_LINE_FOLLOWING, STATE_OBSTACLE_DETECTED, STATE_TURNING, STATE_RECOVERING } SystemState_t; SystemState_t current_state = STATE_LINE_FOLLOWING; uint32_t obstacle_timer = 0; void Main_Decision_Loop(void) { float distance = Get_Distance_CM(); switch (current_state) { case STATE_LINE_FOLLOWING: Line_Following_Task(); if (distance > 0 && distance < SAFE_DISTANCE_CM) { // 例如20cm current_state = STATE_OBSTACLE_DETECTED; obstacle_timer = HAL_GetTick(); Stop_Motors(); // 先停车 } break; case STATE_OBSTACLE_DETECTED: // 停车观察一段时间,或立即决策 if (HAL_GetTick() - obstacle_timer > 500) { // 停车500ms // 简单策略:向右转90度 Set_LeftMotor(600); Set_RightMotor(-600); current_state = STATE_TURNING; obstacle_timer = HAL_GetTick(); } break; case STATE_TURNING: // 执行转弯,例如通过编码器判断转过一定角度,或定时转弯 if (HAL_GetTick() - obstacle_timer > 800) { // 假设800ms转90度 Stop_Motors(); // 进入恢复状态,尝试重新寻线 current_state = STATE_RECOVERING; obstacle_timer = HAL_GetTick(); } break; case STATE_RECOVERING: // 原地缓慢旋转或前进,直到中间传感器检测到黑线 Set_LeftMotor(200); Set_RightMotor(200); Read_Line_Sensors(); if (sensor_values[SENSOR_MIDDLE]) { current_state = STATE_LINE_FOLLOWING; } // 超时处理 if (HAL_GetTick() - obstacle_timer > 3000) { Stop_Motors(); // 进入错误状态或重新尝试 } break; } }这个状态机非常基础。更复杂的策略可能包括:根据障碍物在左/右选择绕行方向,绕行时记录路径以便回归,或者使用多个传感器(左、前、右)进行更精细的环境感知。
4. 系统集成、调试与故障排查
将各个模块的代码整合到一个工程中,并合理设计任务调度(如使用定时器中断周期性地执行传感器读取、PID计算和决策循环),是最后一步。
4.1 常见问题与排查清单
当小车无法正常工作时,请按照以下清单逐项检查,可以解决90%以上的“飞线小车”问题。
| 问题现象 | 可能原因 | 排查步骤 | 解决方案与建议 |
|---|---|---|---|
| 上电后单片机无反应,LED不亮 | 1. 电源未接通或电压不对。 2. 核心板损坏。 3. 电源线接触不良。 | 1. 用万用表测量核心板VCC和GND间电压是否为3.3V或5V(视核心板而定)。 2. 检查降压模块输出。 3. 轻摇杜邦线接头,看是否有接触不良。 | 确保电源正确连接。使用质量好的MicroUSB线或稳压电源供电。焊接关键电源线。 |
| 电机不转或只抖动 | 1. 电机驱动未使能(STBY引脚为低)。 2. PWM信号未输出或频率不对。 3. 电机电源(VM)电压不足或电流不够。 4. 电机线接反或接触不良。 | 1. 用逻辑分析仪或示波器检查PWM引脚是否有波形。 2. 检查STBY引脚电平。 3. 测量电机驱动VM端电压,带载时是否跌落严重。 4. 直接将电机接电池,看是否正常转动。 | 确认软件中已拉高STBY。检查PWM初始化代码和引脚配置。为电机动力使用独立电池或大容量电源。拧紧电机接线端子。 |
| 循迹传感器始终触发或不触发 | 1. 传感器供电错误。 2. 距离地面高度不合适。 3. 环境光干扰(特别是红外传感器)。 4. 阈值电位器未调节。 | 1. 测量传感器VCC电压。 2. 观察传感器指示灯,遮挡测试。 3. 在预期环境下(如比赛场地),调节传感器上的电位器,使其在黑白地面上输出状态翻转。 4. 使用单片机ADC读取模拟值,观察黑白差异。 | 为传感器提供稳定5V供电。调整传感器安装高度(通常距地面1-2cm)。在传感器头部加装遮光罩。进行现场校准。 |
| 超声波测距值不准或不变 | 1. Echo引脚未正确配置中断/输入捕获。 2. 测量时序错误,Trig触发太频繁。 3. 物体超出量程或表面不反射声波。 4. 电源噪声干扰。 | 1. 用示波器同时观察Trig和Echo引脚波形。 2. 确保两次测距间隔大于60ms(HC-SR04要求)。 3. 对固定距离物体(如20cm)测试,校准计算式中的声速系数。 | 使用定时器输入捕获模式精确计时。增加测量间隔。避免测量柔软、倾斜的物体。在VCC和GND间并联10uF和0.1uF电容滤波。 |
| 小车运行时单片机突然复位 | 1. 电机启动/制动瞬间产生大电流,导致电源电压跌落。 2. 电机产生的反电动势干扰。 3. 杜邦线松动。 | 1. 用示波器监控单片机VCC电压,观察电机动作时是否出现跌落毛刺。 2. 检查所有GND连接是否牢固。 | 最有效的方案:电机动力电源与控制逻辑电源完全分离(两套电池)。或在电机电源输入端并接大容量电解电容(如1000uF)缓冲。在电机两端并联续流二极管。 |
| PID循迹摆动严重或跑飞 | 1. PID参数不合适。 2. 传感器安装间距不合理。 3. 小车机械结构不对称,左右轮转速不一致。 4. 电池电压下降导致电机性能变化。 | 1. 单独测试左右轮在相同PWM下的实际转速,进行校准。 2. 将PID输出和传感器偏差值通过串口打印到上位机,观察曲线。 3. 在直线赛道上反复调试P、I、D参数。 | 实现电机速度闭环(使用编码器)。加入死区控制,微小偏差不调整。定期校准电机速度差。使用稳压电源或监测电池电压进行补偿。 |
4.2 进阶优化与扩展方向
当基础功能实现后,可以考虑以下优化来提升小车的性能和竞赛表现:
- 速度闭环控制:使用电机编码器测量实际转速,并用另一个PID控制器来使得电机转速精确跟随目标值。这能极大提升小车直线行驶的稳定性和抗干扰能力。
- 运动学模型与路径规划:建立小车的差分驱动运动学模型。这样,你可以更精确地控制小车移动特定距离或旋转特定角度,对于需要定点停车的任务至关重要。
- 多传感器融合:结合超声波、红外、甚至摄像头(如OpenMV)的数据,构建更全面的环境感知。例如,用红外检测近处障碍,用超声波测量精确距离。
- 无线调试与参数整定:通过蓝牙(如HC-05/06)或Wi-Fi(ESP32)将单片机数据(传感器值、PID输出、电机速度)实时发送到电脑上位机(如Serial Plotter、匿名上位机、自己写的Qt程序),可以极大提高调试效率。
- 状态记录与脱机运行:对于需要记忆路径的题目,可以让小车在第一遍运行时记录下关键动作(如直行时间、转弯角度),第二遍时脱机复现。
对于大一同学,完成一个能稳定循迹并避障的“飞线小车”已经是巨大的成功。这个过程中学到的硬件连接、电源管理、单片机编程、传感器使用、PID调试和系统调试的思维,远比比赛名次更重要。从最小系统开始,逐个模块验证,最后集成调试,遇到问题按照电源、地线、信号、配置、代码的逻辑层层排查,这种工程化解决问题的方法,将是你们后续所有软硬件项目中最宝贵的财富。