第21届智能车竞赛“走马观碑”赛题的热度持续走高,很多队伍在寒假就开始焊车、调传感器、录运行视频。这里想分享一段“未加入视觉”状态下的车模运行视频背后,常用的一套调试思路。所谓“未加入视觉”,就是不依赖摄像头图像识别,车的感知来自电磁、红外、编码器等非视觉传感器,先让车跑起来,再把视觉算法叠加进去。这样的好处是,底盘响应、控制参数、传感器标定都可以在更少的变量下验证。本文会从赛题背景、硬件准备、最小可运行框架、感知与控制逻辑、运行视频录制、常见故障排查这几个角度展开,尽量让后来者少走弯路。
1. “走马观碑”与“未加入视觉”到底意味着什么
1.1 赛题名称背后的技术含义
“走马观碑”是历年智能车竞赛中比较考验综合能力的题目风格。单从字面上看,“走着马看碑”对应车模运行中识别路侧目标物;“碑”通常指向赛道上的特殊元素,可能是立柱、标牌或带有图形/字符的物体。竞赛规则经常会要求车模在高速巡线过程中,对这种特殊元素做出反应,例如变道、停车、绕行或只做短暂检测。
不同年份、不同组别的规则细节会有差异,但工程上解决的问题是一致的:车辆需要在一个连续的任务里同时完成稳定循迹和目标事件检测。对于很多队伍而言,第一步并不是直接上视觉,而是先把“稳定循迹”和“基础事件反应”做扎实。本文说的“未加入视觉”,就是指这一阶段。
1.2 为什么先不加视觉
很多新队员容易犯一个错误:一上来就想用摄像头跑完全程,最后陷入调曝光、调阈值、调神经网络的时间黑洞。摄像头视觉链路很长,包括图像采集、畸变矫正、透视变换、赛道线提取、目标物分类、串口通信或内存共享,每一步都可能引入新的问题。如果底盘本身都不稳定,摄像头画面里出现的每个抖动都会被放大,视觉程序根本分不清是车身姿态导致的图像运动,还是赛道真实变化。
先把视觉模块屏蔽掉,只用电磁或红外传感器进行赛道检测,能把问题规模缩小到“车能不能稳定跟着赛道走”。这个阶段,车模跑起来后的视频,可以直接看出机械结构、舵机响应、电机控制、传感器布局是否有问题。视频里的每次抖动、每次偏移、每次冲出赛道,都是后续视觉模块加入前必须解决的隐患。
1.3 未加入视觉时,感知与控制闭环如何组成
未加入视觉,不代表没有感知。一场完整的车辆运行闭环至少需要三类信息:
- 赛道位置信息:通过电磁传感器或红外对管判断车模相对赛道中心线的偏移。
- 车辆运动信息:通过编码器读取当前速度,通过陀螺仪/加速度计(如果加了)获取角速度和加速度。
- 状态切换信息:通过额外的传感器组合判断是否到达“碑”元素或特殊区域。
控制端则把这几类信息汇入一个有限状态机。默认状态是“巡线”,当满足一定条件时切换到“检测到碑”或“特殊处理”状态,执行动作后再回到巡线状态。
视频中看到的流畅运行,实际上是感知、决策、执行三个模块以毫秒级周期循环工作的结果。
2. 硬件与软件环境准备
2.1 车模平台与主控选型
第21届竞赛常见的车模平台有 A 型车、B 型车、C 型车等,不同车型的机械结构和允许改装程度不同。以常见的 C 型车模为例,车身自带两个驱动轮、一个或多个万向轮,转向由舵机控制,动力由直流电机提供。
主控芯片应根据队伍熟悉的平台选择。常见组合如下:
| 主控平台 | 特点 | 适合场景 |
|---|---|---|
| STM32F407 | 资料多,HAL库和标准库都有,ADC/PWM功能齐全 | 新手入门、快速验证 |
| 英飞凌 TC264 | 竞赛常用,性能强,但需要熟悉 AURIX 开发环境 | 需要更高实时性和复杂算法的队伍 |
| RT1064 | NXP 系列,主频高,适合视觉+控制同步运行 | 后期要加入视觉且希望单主控完成处理 |
无论选择哪块主控,都要先确认引脚数量、ADC 通道、PWM 定时器资源是否够用。很多队伍“未加入视觉”时只用了 4 路电磁、1 路编码器、1 路舵机、1 路电机,但后期加入视觉后会需要更多串口或 DMA 资源,选型时要预留余量。
2.2 传感器布局与连接
未加入视觉阶段的核心传感器是电磁或红外。
电磁传感器的原理是:赛道中心铺设有引导线,引导线中通有交变电流,会在周围产生磁场。车模前后左右放置多个电感线圈,通过感应电压的大小和差异判断车模与引导线的相对位置。常见布局是:
- 水平方向 2 到 4 个电感,横排布置在车头。
- 竖直方向 1 到 2 个电感,用于检测坡道或远距离信号。
- 每个电感经过运放放大、检波后接入主控 ADC。
红外传感器则常用在边界检测或特殊元素检测,例如使用对射式红外管判断是否经过立柱。红外信号受环境光影响较大,使用时要加遮光罩和固定阈值。
传感器连接建议:
| 传感器类型 | 信号类型 | 主控接口 | 注意点 |
|---|---|---|---|
| 电磁电感 | 模拟电压(已检波) | ADC 通道 | 需要滤波和归一化 |
| 红外对管 | 模拟电压或数字电平 | ADC 或 GPIO | 环境光干扰大 |
| 编码器 | 正交信号或单边沿脉冲 | 定时器编码器模式/外部中断 | 需要共地 |
2.3 软件环境与基础库
开发环境根据主控型号选择:
- STM32:Keil MDK 或 STM32CubeIDE。
- TC264:AURIX Development Studio 或 Tasking。
- RT1064:IAR 或 MCUXpresso。
除此之外,很多队伍使用逐飞科技提供的开源库,这些库封装了底层驱动,可以直接调用adc_init、pwm_init、encoder_init等函数。使用开源库能大幅缩短开发时间,但必须检查库版本和芯片型号是否匹配。
建议在项目文件夹下准备一份环境检查清单:
- 驱动库能否编译通过示例工程。
- 主控板能否正常下载程序。
- 电池电压是否在 7.2V 至 8.4V 范围内。
- 舵机和电机驱动是否共地。
- 串口能否打印数据。
- ADC 通道电压读数是否随传感器位置变化。
3. 最小可运行框架:先让车模动起来
很多队伍一上来就写完整控制算法,结果车一动就失控。正确做法是先做最小可运行框架,验证每一路输出都正确,再逐步加入感知和决策。
3.1 引脚初始化与基础输出
以 STM32 加常见驱动库为例,初始化主要包括三部分:PWM 输出、ADC 采样、编码器计数。
void Hardware_Init(void) { // 舵机控制 PWM,频率 50Hz,占空比由后续角度值决定 pwm_init(PWM_PIN_SERVO, 50, 7500); // 50Hz, 初始占空比 7.5% // 电机 PWM,频率 10kHz,初始占空比 0 pwm_init(PWM_PIN_MOTOR, 10000, 0); // ADC 初始化,用于读取电磁传感器 adc_init(ADC_GROUP_1, ADC_CH_A0); adc_init(ADC_GROUP_1, ADC_CH_A1); adc_init(ADC_GROUP_1, ADC_CH_A2); adc_init(ADC_GROUP_1, ADC_CH_A3); // 编码器接口初始化 encoder_init(ENCODER_TIMER_1); }这段代码的关键是明确区分舵机和电机的 PWM 频率。舵机通常使用 50Hz 方波,对应脉冲宽度 0.5ms 到 2.5ms 之间的角度范围;电机驱动则使用更高频率,例如 10kHz 到 20kHz,频率过低容易产生电流噪声和电机啸叫。
3.2 舵机中值与电机方向标定
在摄像头和复杂算法之前,先做两个最容易出错的手动标定。
第一个是舵机中值。舵机中值指的是舵机臂位于正中间时对应的 PWM 占空比。每个舵机安装后,齿轮角度和摆臂位置都不一定完全一致,必须通过调试确定。
// 手动标定舵机中值:通过按键或串口修改 Duty,直到舵机臂完全垂直 void Servo_Calibrate(uint16_t duty) { pwm_set_duty(PWM_PIN_SERVO, duty); }第二个是电机正反转方向。上电后给定一个恒定 PWM,观察电机转向是否和预期一致。如果方向反了,可以通过修改 PWM 引脚组合或换向函数解决。不要靠改硬件线来适应软件,秩序要清晰。
3.3 读取传感器与编码器并串口输出
为了让运行视频有“可复盘”数据,最少要能实时输出传感器值和速度值。
typedef struct { uint16_t adc[4]; int16_t speed; } CarData; void Data_Update(CarData *data) { >typedef struct { float norm[4]; float weight[4]; } SensorNormalize; float Sensor_CalculatePosition(uint16_t *raw) { SensorNormalize sn; float sum = 0.0f; float weighted = 0.0f; // 权重设定:左边为负,右边为正 float weight[4] = {-2.0f, -1.0f, 1.0f, 2.0f}; for (int i = 0; i < 4; i++) { sn.norm[i] = (float)raw[i] / 4095.0f; // 假设12位ADC sn.weight[i] = weight[i]; sum += sn.norm[i]; weighted += sn.norm[i] * sn.weight[i]; } if (sum < 0.001f) { return 0.0f; // 无信号时回中 } return weighted / sum; // 返回 -2 到 +2 之间的偏差 }这个偏差值position是后续方向控制的输入。position > 0表示车模中心偏左,需要向右修正;position < 0表示偏右,需要向左修正。
4.2 状态机:从巡线到“碑”检测
未加入视觉不代表无法处理“碑”元素。常见的非视觉检测方式有两种:
- 使用红外对管检测物体遮挡。当车模经过“碑”附近时,红外线被遮挡,GPIO 电平变化。
- 使用电磁信号突变。若“碑”附近没有引导线,或引导线形状变化,电磁传感器会检测到明显的信号波动。
状态机可以这样设计:
typedef enum { STATE_TRACK = 0, // 巡线状态 STATE_OBSTACLE, // 检测到碑/障碍物 STATE_RECOVER // 恢复状态 } CarState; CarState car_state = STATE_TRACK; void State_Machine_Update(float position, uint8_t obstacle_flag) { switch (car_state) { case STATE_TRACK: if (obstacle_flag) { car_state = STATE_OBSTACLE; } else { Servo_Control(position); } break; case STATE_OBSTACLE: // 执行减速或避让动作 Motor_SetSpeed(20); if (!obstacle_flag && position < 0.3f) { car_state = STATE_RECOVER; } break; case STATE_RECOVER: // 回到巡线状态 Servo_Control(position); Motor_SetSpeed(60); car_state = STATE_TRACK; break; default: car_state = STATE_TRACK; break; } }这里要注意,状态切换条件是整车稳定性的核心。不要只依赖单一阈值,要加入连续多帧判断或持续计数,避免传感器抖动导致误触发。
4.3 方向控制:从“差比和”到 PID
未加入视觉阶段的经典方向控制是“差比和”:
int16_t Servo_Control(float position) { int16_t servo_pwm = SERVO_MID; // 中值 float kp = 20.0f; // 比例系数 int16_t delta = (int16_t)(position * kp); servo_pwm += delta; // 限幅 if (servo_pwm > SERVO_MAX) servo_pwm = SERVO_MAX; if (servo_pwm < SERVO_MIN) servo_pwm = SERVO_MIN; pwm_set_duty(PWM_PIN_SERVO, servo_pwm); return servo_pwm; }比例控制实现简单,但弯道中往往存在稳态误差,表现为过弯时总是偏内或偏外。更平滑的方式是加入微分项,形成 PD 控制:
float error_prev = 0.0f; int16_t Servo_Control_PD(float position) { float kp = 18.0f; float kd = 6.0f; float error = position; float derivative = error - error_prev; int16_t servo_pwm = SERVO_MID + (int16_t)(error * kp + derivative * kd); error_prev = error; if (servo_pwm > SERVO_MAX) servo_pwm = SERVO_MAX; if (servo_pwm < SERVO_MIN) servo_pwm = SERVO_MIN; pwm_set_duty(PWM_PIN_SERVO, servo_pwm); return servo_pwm; }微分项能抑制车头来回摆动,但也会放大传感器噪声。建议在计算偏差前先对 ADC 做一次均值滤波或低通滤波,否则差分结果会非常尖锐。
4.4 速度控制:基于编码器的闭环
速度控制同样需要闭环。开环控制时,电池电压下降、电机温度升高都会让速度不满足预期。使用编码器读取实际转速,再通过 PID 调节 PWM 占空比,是稳定运行的基础。
typedef struct { float kp; float ki; float kd; float integral; float last_error; } PidObject; int16_t Speed_Control_Inner(int16_t target_speed, int16_t current_speed) { static PidObject speed_pid = {1.2f, 0.05f, 0.0f, 0.0f, 0.0f}; int16_t error = target_speed - current_speed; speed_pid.integral += error; // 积分限幅 if (speed_pid.integral > 100.0f) speed_pid.integral = 100.0f; if (speed_pid.integral < -100.0f) speed_pid.integral = -100.0f; float output = error * speed_pid.kp + speed_pid.integral * speed_pid.ki + (error - speed_pid.last_error) * speed_pid.kd; speed_pid.last_error = error; if (output > 100.0f) output = 100.0f; if (output < -100.0f) output = -100.0f; // 将百分比输出映射到 PWM 占空比 int16_t pwm = (int16_t)(output * MOTOR_PWM_FULL / 100.0f); pwm_set_duty(PWM_PIN_MOTOR, pwm); return pwm; }速度闭环的周期建议控制在 5ms 到 20ms 之间,不要和方向控制使用完全相同的周期。方向控制通常更快,例如 5ms;速度环可以慢一些,例如 10ms 或 20ms。
5. 运行视频录制与数据验证
5.1 为什么录运行视频比现场看一眼更重要
很多队伍调车时只看车跑了没有,没跑就停下来改代码,跑了就收工。这样无法定位问题。运行视频可以反复回放,而且可以在视频里叠加调试信息,把传感器状态和实际车身动作对齐。
录制“未加入视觉”阶段的车模运行视频,目标不是展示“跑得多快”,而是记录以下内容:
- 车模是否稳定地贴着赛道中线行驶。
- 过弯时是否出现明显甩尾或切弯。
- “碑”元素附近传感器是否产生预期的状态变化。
- 速度是否在直道和弯道有明显波动。
5.2 推荐录制视角与信息叠加
推荐两类视角:
| 视角 | 拍摄方式 | 用途 |
|---|---|---|
| 全景视角 | 手机固定在三脚架或场地外 | 看车模整体轨迹和姿态 |
| 车载视角 | 摄像头装在车模顶部或车头 | 看传感器和路面接触状态 |
为了让视频更有分析价值,可以把串口发送的数据通过 OLED、无线数传或上位机叠加显示。如果使用无线串口,可以边录视频边记录电脑上的数据流,后期用时间轴对齐。
5.3 视频中该观察的信号特征
不要只盯着车看,要同时关注以下特征:
- 车身是否周期性左右摆动。如果有,方向控制微分过大或传感器滤波不足。
- 直道是否速度均匀。如果不均匀,速度 PID 参数可能需要重新调整。
- 到“碑”前是否有明显减速或错误动作。如果没有反应,需要检查检测信号和状态机触发条件。
- 出弯后是否恢复到中线。如果恢复慢,的增益偏低。
可以建立一个表格,把每一圈视频编号,记录问题现象和对应参数,方便后续改动对比。
5.4 串口日志与上位机回放
运行视频只能给出“现象”,串口日志才能给出“数据”。推荐在代码中增加数据记录功能:
void Log_Record(CarData *data, float pos, int16_t servo, int16_t speed_target, int16_t speed_real) { // 将数据格式化后发送到无线串口 printf("POS:%.2f SERVO:%d TGT:%d REAL:%d\r\n", pos, servo, speed_target, speed_real); }在电脑端使用串口助手将数据保存为文件,然后用 Python 或 Excel 画曲线,可以清楚看到控制量是否平滑、执行机构是否饱和。
6. 未加入视觉阶段的高频问题排查
6.1 车模总是跑偏或画龙
现象:车模在直道上反复左右修正,幅度越来越大,视频里看像“画龙”。
可能原因:
- 传感器左右不对称。
- 舵机中值标定不准。
- 方向控制比例增益过大。
- 传感器数据没有滤波。
检查方式:
- 静止状态下读取 4 路 ADC 值,确认居中时左右对称。
- 打印舵机 PWM 中值,确认车模直线行驶时舵机臂方向。
- 减小
kp再测试。 - 对
position做低通滤波。
推荐做法:
float pos_filter = 0.0f; float pos_target = 0.0f; void Update_Position_Filter(float new_pos) { pos_target = new_pos; pos_filter = pos_filter * 0.7f + pos_target * 0.3f; }6.2 舵机不响应或 PWM 输出异常
现象:舵机不动或剧烈抖动。
可能原因:
- 舵机 PWM 频率错误,应使用 50Hz。
- PWM 占空比范围超出舵机有效范围。
- 舵机供电不足或主控与舵机未共地。
- 控制周期异常,导致舵机频繁收指令。
检查方式:
- 用示波器或逻辑分析仪看 PWM 波形。
- 单独写一个固定占空比程序,看舵机是否转到特定角度。
- 接上电池后测量舵机电源电压是否稳定。
6.3 ADC 值跳变严重
现象:串口打印的 ADC 值上下跳动,车模容易误判。
可能原因:
- 电感到主控的模拟线太长或没有屏蔽。
- 电机噪声通过电源耦合到 ADC。
- 采样时间没有避开 PWM 开关瞬间。
- 没有做软件滤波。
处理建议:
- 模拟线尽量短,使用双绞线。
- ADC 采样后做多次采样取平均。
- 在电源端并联去耦电容,例如 100nF 陶瓷电容和 220uF 电解电容。
6.4 车模到达“碑”区域时反应不符合预期
现象:车模检测不到“碑”,或者远远就误触发。
可能原因:
- 红外传感器的阈值设置不合理。
- 传感器安装高度、角度不合理。
- 状态机切换条件漏掉了持续帧判断。
- 电磁传感器在“碑”附近也发生了突变,没有和红外信号做联合判断。
处理建议:
- 用上位机标定传感器在“无碑”和“有碑”时的信号范围,设置中间阈值。
- 状态机中加入连续 3 到 5 帧满足条件才切换。
- 记录视频时,把触发标志位置为 1 或 0,方便对齐时间轴。
6.5 视频里看到电机转速忽快忽慢
现象:车模运行中速度有明显顿挫感。
可能原因:
- 速度 PID 积分参数过大,引起振荡。
- 编码器安装松动,信号忽高忽低。
- 电池电压波动导致开环 PWM 下转速不一致。
- 电机换向时死区补偿不完善。
检查方式:
- 打印实际速度曲线。
- 检查编码器固定螺丝。
- 在电池充满和快没电时分别测试相同速度指令。
7. 未加入视觉阶段的最佳实践与扩展方向
7.1 开发顺序与调试清单
建议按以下顺序推进:
- 舵机中值标定。
- 电机方向与速度开环验证。
- 串口输出功能。
- ADC 传感器信号读取与滤波。
- 方向闭环。
- 速度闭环。
- 状态机与特殊元素处理。
- 录制视频并回放。
每次改动只动一个变量。不要同时调比例系数和微分系数,否则出了问题无法定位。
7.2 运行视频录制前检查表
录制前建议检查一下:
- 电池电压是否足够跑完整圈。
- 赛道元素是否摆放完整。
- 摄像头/手机电量是否充足。
- 无线串口是否连接稳定。
- 程序编译时间是否已经注入了当天日期或版本号。
- 是否有备用轮胎和螺丝刀。
在视频开头用手势或白板写下版本号和日期,后期整理时会非常方便。
7.3 从“未加视觉”到“加入视觉”的过渡策略
当前阶段运行稳定后,视觉模块可以按以下方式切入:
- 先单独采集图像,不参与控制,只做记录和显示。
- 再用视觉计算出的偏差替代电磁偏差作为方向输入。
- 最后加入视觉目标检测,识别“碑”上的图形或文字。
过渡期间仍然保留电磁或红外数据,作为视觉异常时的兜底。摄像头图像处理周期通常比传感器读取周期长,因此控制周期需要重新规划,不能让控制任务等待图像结果。
7.4 对新手最有价值的练习建议
不要急着追求全组第一的速度。先跑完一圈不冲出赛道,再谈加速。每次调参后,录一段 10 到 15 秒的稳定运行视频,把视频和串口数据存档。这样即使第二天代码改坏了,也有一条明确的回退参考。
另外,建议给主控板增加一个 LED 或 OLED 显示当前状态机状态,这样录视频时不需要额外串口线也能看到程序到底认为自己在哪里。
“走马观碑”这类赛题的核心不在于某个传感器是否高级,而在于感知、决策和执行之间能否稳定配合。未加入视觉阶段正是检验这种配合的最好人手点。把这个阶段的问题解决干净,后续加入视觉时才会知道,哪些问题是图像算法引起的,哪些其实是底盘控制还没有到位。