1. 项目概述:智能车竞赛雁过留痕组技术方案解析
全国大学生智能汽车竞赛作为国内最具影响力的高校科技赛事之一,每年都吸引着众多高校学子参与。第二十一届赛事在规则设定和考核重点上进行了显著调整,特别是新增的"雁过留痕"组别,对参赛队伍的技术能力和实战水平提出了更高要求。作为一名参与过多届智能车竞赛的老队员,我将结合自身经验,为大家详细解析这一组别的技术方案。
雁过留痕组的核心挑战在于实现智能车对特定目标的精准识别与跟踪。与传统的循迹组别不同,该组别要求车模不仅能自主行驶,还需要完成对预设靶标的识别和标记任务。这种"识别-追踪-标记"的完整闭环,对参赛队伍的图像处理、控制算法和系统集成能力都提出了全新考验。
在本文中,我将从硬件搭建、软件环境配置、图像处理优化、控制算法设计等维度,全面剖析雁过留痕组的技术要点。特别针对STC32G144这款新引入的单片机平台,会重点讲解如何克服其性能限制,实现高效的图像处理和精准控制。通过本文,希望能帮助参赛队伍快速掌握核心技术要点,在有限备赛时间内达到最佳竞技状态。
2. 硬件系统设计与搭建
2.1 核心硬件选型解析
雁过留痕组的硬件系统设计需要兼顾图像采集、数据处理、运动控制和目标标记四大功能模块。经过多次实测验证,我们推荐以下硬件配置方案:
主控单元:STC32G144单片机作为本届赛事指定主控,其最大优势在于高性价比和丰富的外设资源。该芯片采用32位8051内核,主频可达35MHz,内置64KB Flash和5KB SRAM,支持硬件乘除法器,特别适合实时控制应用。在实际使用中,我们发现其ADC采样速率和PWM输出稳定性表现优异,完全能满足电机控制和传感器数据采集需求。
图像传感器:神眼摄像头(188×120分辨率)是我们的首选。相比传统OV系列摄像头,神眼在低照度环境下表现更稳定,且输出即为灰度图像,省去了色彩转换的计算开销。实测表明,在室内光照条件下,其帧率可达50fps以上,为后续图像处理提供了充足的数据源。
运动执行机构:
- 驱动电机:采用C车模配套的370电机,搭配DRV8701E双路电机驱动模块。该驱动芯片支持3.6A持续电流输出,内置电流检测和保护电路,能有效防止电机堵转损坏。
- 转向舵机:推荐使用20kg级数字舵机,响应时间小于0.1秒,可确保快速精准的转向控制。
测速反馈:LQ 1024线编码器提供了高精度的速度反馈,其正交解码信号可直接接入STC32G144的定时器接口,实现不占用CPU资源的脉冲计数。
2.2 硬件系统搭建要点
在实际组装过程中,需要特别注意以下几个关键点:
电源分配设计:
- 为数字电路(主控、传感器)和功率电路(电机驱动)分别供电,避免大电流波动影响信号稳定性
- 在电源输入端增加1000μF以上的电解电容,用于抑制电机启停时的电压波动
- 为每个数字模块配备0.1μF去耦电容,就近放置在供电引脚处
机械结构优化:
- 摄像头安装高度建议在20-25cm范围内,俯仰角10-15度,这个几何参数能兼顾远场识别和近场控制
- 编码器安装要确保轴系同心度,避免机械间隙导致速度反馈失真
- 整体重心应尽量压低并靠近车体几何中心,提升高速过弯稳定性
信号走线规范:
- 电机驱动信号线(PWM、方向)应采用双绞线布线,减少电磁干扰
- 编码器信号线需使用屏蔽线,长度不超过30cm
- 模拟信号(如陀螺仪输出)要远离功率线路,必要时使用磁珠隔离
提示:在初次上电前,务必用万用表检查所有电源线路的对地阻抗,避免短路损坏器件。建议采用分模块逐步上电的调试策略,先验证核心控制系统,再接入功率驱动部分。
3. 软件开发环境配置
3.1 工具链搭建全流程
Keil C251开发环境安装:
- 从Keil官网下载MDK-C251安装包(版本建议≥5.37)
- 安装时勾选"Add STC MCU Database"选项,便于后续器件选择
- 完成安装后,使用注册机激活软件(注意选择C251编译器)
STC烧录工具配置:
- 从STC官网下载最新版STC-ISP软件(当前版本v6.91)
- 连接USB-TTL适配器时,注意驱动安装正确(推荐使用CH340芯片版本)
- 在软件设置中启用"脱机下载"功能,方便赛场快速烧录
开发环境优化技巧:
- 在Keil的Options→Target中,将Memory Model设为Large,Code Banking设为Enabled
- 启用"Browse Information"选项,便于代码导航和调试
- 配置合适的Tab缩进(建议4空格)和代码自动补全
3.2 基础外设驱动开发
GPIO配置规范:
c复制// GPIO初始化示例
void GPIO_Init(void) {
P1M0 = 0x03; // P1.0/P1.1推挽输出
P1M1 = 0x00;
P3M0 = 0x00; // P3.2/P3.3上拉输入
P3M1 = 0x0C;
}
定时器配置要点:
- 系统时钟建议设置为24MHz(平衡性能和功耗)
- 使用Timer0作为系统时基,配置2ms中断周期
- Timer2用于PWM生成,频率建议10kHz(电机控制)和50Hz(舵机控制)
ADC采样优化:
c复制// ADC多通道采样示例
void ADC_Init(void) {
P1ASF = 0x07; // 启用P1.0-P1.2作为ADC输入
ADC_CONTR = 0x80; // 开启ADC电源
Delay_ms(1); // 等待电源稳定
}
uint16_t ADC_Read(uint8_t ch) {
ADC_CONTR = 0x80 | (ch & 0x07); // 选择通道
Delay_us(10); // 采样保持时间
ADC_CONTR |= 0x40; // 启动转换
while (!(ADC_CONTR & 0x20)); // 等待转换完成
return (ADC_RES << 8) | ADC_RESL;
}
4. 图像处理核心算法
4.1 大津法优化实践
原始大津法实现:
c复制uint8_t OtsuThreshold(uint8_t *image, uint16_t width, uint16_t height) {
uint32_t histogram[256] = {0};
uint32_t total = width * height;
// 统计直方图
for (uint16_t i = 0; i < total; i++) {
histogram[image[i]]++;
}
// 计算最佳阈值
uint8_t threshold = 0;
float max_var = 0;
for (uint8_t t = 0; t < 255; t++) {
float w0 = 0, w1 = 0, u0 = 0, u1 = 0;
for (uint8_t i = 0; i <= t; i++) {
w0 += histogram[i];
u0 += i * histogram[i];
}
if (w0 > 0) u0 /= w0;
for (uint8_t i = t + 1; i < 256; i++) {
w1 += histogram[i];
u1 += i * histogram[i];
}
if (w1 > 0) u1 /= w1;
float var = w0 * w1 * (u0 - u1) * (u0 - u1);
if (var > max_var) {
max_var = var;
threshold = t;
}
}
return threshold;
}
优化方案实施:
- 下采样优化:
c复制void DownsampleImage(uint8_t *src, uint8_t *dst, uint16_t src_w, uint16_t src_h) {
uint16_t dst_w = src_w / 2;
uint16_t dst_h = src_h / 2;
for (uint16_t y = 0; y < dst_h; y++) {
for (uint16_t x = 0; x < dst_w; x++) {
uint32_t sum = 0;
sum += src[(2*y)*src_w + (2*x)];
sum += src[(2*y)*src_w + (2*x+1)];
sum += src[(2*y+1)*src_w + (2*x)];
sum += src[(2*y+1)*src_w + (2*x+1)];
dst[y*dst_w + x] = sum / 4;
}
}
}
- 帧间采样优化:
c复制uint8_t dynamicOtsu(uint8_t *image, uint8_t lastThreshold) {
static uint8_t frameCount = 0;
if (frameCount == 0) { // 每10帧计算一次新阈值
frameCount = 10;
return OtsuThreshold(image, IMG_WIDTH, IMG_HEIGHT);
} else {
frameCount--;
return lastThreshold;
}
}
4.2 赛道识别算法对比
八邻域搜线法实现:
c复制void EightNeighborSearch(uint8_t *binaryImg, uint16_t width, uint16_t height,
uint16_t *leftBorder, uint16_t *rightBorder) {
// 初始化搜索起点(图像底部中心区域)
uint16_t startX = width / 2;
uint16_t startY = height - 1;
// 向左搜索初始左边界
uint16_t left = startX;
while (left > 0 && binaryImg[startY * width + left] == 0) left--;
leftBorder[startY] = left;
// 向右搜索初始右边界
uint16_t right = startX;
while (right < width - 1 && binaryImg[startY * width + right] == 0) right++;
rightBorder[startY] = right;
// 向上逐行搜索
for (int16_t y = startY - 1; y >= 0; y--) {
// 左边界搜索(逆时针)
uint16_t x = leftBorder[y + 1];
if (binaryImg[y * width + x] == 1) {
leftBorder[y] = x;
} else {
// 检查右侧
if (x < width - 1 && binaryImg[y * width + x + 1] == 1) {
leftBorder[y] = x + 1;
}
// 检查右上
else if (x < width - 1 && y > 0 && binaryImg[(y - 1) * width + x + 1] == 1) {
leftBorder[y] = x + 1;
}
// 检查上方
else if (y > 0 && binaryImg[(y - 1) * width + x] == 1) {
leftBorder[y] = x;
}
// 检查左上
else if (x > 0 && y > 0 && binaryImg[(y - 1) * width + x - 1] == 1) {
leftBorder[y] = x - 1;
}
// 检查左侧
else if (x > 0 && binaryImg[y * width + x - 1] == 1) {
leftBorder[y] = x - 1;
} else {
leftBorder[y] = x; // 保持上一行位置
}
}
// 右边界搜索(顺时针)
x = rightBorder[y + 1];
if (binaryImg[y * width + x] == 1) {
rightBorder[y] = x;
} else {
// 检查左侧
if (x > 0 && binaryImg[y * width + x - 1] == 1) {
rightBorder[y] = x - 1;
}
// 检查左上
else if (x > 0 && y > 0 && binaryImg[(y - 1) * width + x - 1] == 1) {
rightBorder[y] = x - 1;
}
// 检查上方
else if (y > 0 && binaryImg[(y - 1) * width + x] == 1) {
rightBorder[y] = x;
}
// 检查右上
else if (x < width - 1 && y > 0 && binaryImg[(y - 1) * width + x + 1] == 1) {
rightBorder[y] = x + 1;
}
// 检查右侧
else if (x < width - 1 && binaryImg[y * width + x + 1] == 1) {
rightBorder[y] = x + 1;
} else {
rightBorder[y] = x; // 保持上一行位置
}
}
}
}
最长白列优化方案:
c复制void WhiteColumnSearch(uint8_t *binaryImg, uint16_t width, uint16_t height,
uint16_t *leftBorder, uint16_t *rightBorder) {
uint16_t whiteCount[IMG_WIDTH] = {0};
// 统计每列连续白点数(隔两列统计一列)
for (uint16_t x = 0; x < width; x += 3) {
uint16_t count = 0;
for (int16_t y = height - 1; y >= 0; y--) {
if (binaryImg[y * width + x] == 1) {
count++;
} else {
break;
}
}
whiteCount[x] = count;
}
// 找左右最长白列
uint16_t maxLeft = 0, maxRight = width - 1;
uint16_t maxLeftCount = 0, maxRightCount = 0;
for (uint16_t x = 0; x < width / 2; x += 3) {
if (whiteCount[x] > maxLeftCount) {
maxLeftCount = whiteCount[x];
maxLeft = x;
}
}
for (uint16_t x = width - 1; x >= width / 2; x -= 3) {
if (whiteCount[x] > maxRightCount) {
maxRightCount = whiteCount[x];
maxRight = x;
}
}
// 边界搜索(隔一列扫一列)
for (int16_t y = height - 1; y >= 0; y--) {
// 左边界搜索
uint16_t x = maxLeft;
while (x > 0) {
if (binaryImg[y * width + x] == 0 &&
binaryImg[y * width + x - 1] == 0 &&
x > 1 && binaryImg[y * width + x - 2] == 1) {
leftBorder[y] = x - 2;
break;
}
x -= 2;
}
if (x == 0) leftBorder[y] = 0; // 到达图像边缘
// 右边界搜索
x = maxRight;
while (x < width - 1) {
if (binaryImg[y * width + x] == 1 &&
binaryImg[y * width + x + 1] == 0 &&
x < width - 2 && binaryImg[y * width + x + 2] == 0) {
rightBorder[y] = x;
break;
}
x += 2;
}
if (x == width - 1) rightBorder[y] = width - 1; // 到达图像边缘
}
}
5. 运动控制系统设计
5.1 电机速度闭环实现
增量式PID控制器:
c复制typedef struct {
float Kp, Ki, Kd;
float lastError, prevError;
float output;
float maxOutput;
} PID_IncTypeDef;
float PID_Incremental(PID_IncTypeDef *pid, float error) {
float delta = pid->Kp * (error - pid->lastError) +
pid->Ki * error +
pid->Kd * (error - 2 * pid->lastError + pid->prevError);
pid->output += delta;
// 输出限幅
if (pid->output > pid->maxOutput) {
pid->output = pid->maxOutput;
} else if (pid->output < -pid->maxOutput) {
pid->output = -pid->maxOutput;
}
pid->prevError = pid->lastError;
pid->lastError = error;
return pid->output;
}
速度环调参技巧:
- 先调P参数:逐步增大P值,直到电机出现轻微震荡,然后回调20%
- 再调D参数:增加D值抑制超调,通常设为P值的1/10到1/5
- 最后调I参数:微量增加I值消除静差,通常设为P值的1/100左右
- 测试不同速度下的响应曲线,确保全速域稳定性
5.2 舵机转向控制
位置式PID控制器:
c复制typedef struct {
float Kp, Ki, Kd;
float integral;
float lastError;
float maxIntegral;
} PID_PosTypeDef;
float PID_Positional(PID_PosTypeDef *pid, float error) {
pid->integral += error;
// 积分限幅
if (pid->integral > pid->maxIntegral) {
pid->integral = pid->maxIntegral;
} else if (pid->integral < -pid->maxIntegral) {
pid->integral = -pid->maxIntegral;
}
float derivative = error - pid->lastError;
pid->lastError = error;
return pid->Kp * error +
pid->Ki * pid->integral +
pid->Kd * derivative;
}
转向控制策略:
c复制float calculateSteering(uint16_t *leftBorder, uint16_t *rightBorder, uint16_t height) {
const uint16_t refRow = height / 3; // 取图像下1/3处作为参考行
float center = (leftBorder[refRow] + rightBorder[refRow]) / 2.0f;
float error = center - (IMG_WIDTH / 2.0f);
// 误差归一化
error = error / (IMG_WIDTH / 2.0f);
// 非对称响应:直道小增益,弯道大增益
float gain = 1.0f;
if (fabs(error) > 0.3f) {
gain = 1.5f; // 大偏差时提高增益
}
return PID_Positional(&steeringPID, error * gain);
}
6. 靶标识别与标记系统
6.1 靶标检测算法实现
方法1:黑白跳变边界检测
c复制typedef struct {
uint16_t x, y;
} Point;
uint8_t detectTarget(uint8_t *binaryImg, uint16_t width, uint16_t height, Point *center) {
const uint16_t searchBottom = height * 3 / 4; // 从图像下3/4处开始搜索
const uint16_t minWidth = width / 8; // 最小有效宽度阈值
// 水平方向搜索
uint16_t left = 0, right = 0;
for (uint16_t x = 0; x < width - 1; x++) {
if (binaryImg[searchBottom * width + x] == 0 &&
binaryImg[searchBottom * width + x + 1] == 1) {
left = x + 1;
break;
}
}
for (uint16_t x = left; x < width - 1; x++) {
if (binaryImg[searchBottom * width + x] == 1 &&
binaryImg[searchBottom * width + x + 1] == 0) {
right = x;
break;
}
}
// 有效性检查
if (right - left < minWidth) return 0;
uint16_t midX = (left + right) / 2;
if (binaryImg[searchBottom * width + midX] == 0) return 0;
// 垂直方向搜索
uint16_t top = 0, bottom = 0;
for (uint16_t y = searchBottom; y > 0; y--) {
if (binaryImg[y * width + midX] == 1 &&
binaryImg[(y - 1) * width + midX] == 0) {
top = y;
break;
}
}
for (uint16_t y = searchBottom; y < height - 1; y++) {
if (binaryImg[y * width + midX] == 1 &&
binaryImg[(y + 1) * width + midX] == 0) {
bottom = y;
break;
}
}
// 计算中心点
center->x = (left + right) / 2;
center->y = (top + bottom) / 2;
return 1;
}
方法3:最长白列特征法优化
c复制uint8_t detectTargetByColumn(uint8_t *binaryImg, uint16_t width, uint16_t height, Point *center) {
uint16_t maxCol = 0, maxCount = 0;
// 查找最长白列(隔列扫描)
for (uint16_t x = 0; x < width; x += 2) {
uint16_t count = 0;
for (uint16_t y = 0; y < height; y++) {
if (binaryImg[y * width + x] == 1) count++;
}
if (count > maxCount) {
maxCount = count;
maxCol = x;
}
}
// 有效性检查
if (maxCount < height / 4) return 0;
// 垂直方向边界检测
uint16_t top = 0, bottom = 0;
for (uint16_t y = 0; y < height; y++) {
if (binaryImg[y * width + maxCol] == 1) {
top = y;
break;
}
}
for (uint16_t y = height - 1; y > 0; y--) {
if (binaryImg[y * width + maxCol] == 1) {
bottom = y;
break;
}
}
// 水平方向边界检测
uint16_t left = 0, right = 0;
uint16_t midY = (top + bottom) / 2;
for (uint16_t x = maxCol; x > 0; x--) {
if (binaryImg[midY * width + x] == 1 &&
binaryImg[midY * width + x - 1] == 0) {
left = x;
break;
}
}
for (uint16_t x = maxCol; x < width - 1; x++) {
if (binaryImg[midY * width + x] == 1 &&
binaryImg[midY * width + x + 1] == 0) {
right = x;
break;
}
}
// 计算中心点
center->x = (left + right) / 2;
center->y = (top + bottom) / 2;
return 1;
}
6.2 标记执行机构控制
激光云台控制实现:
c复制void controlLaserTurret(Point target, Point imgCenter) {
// 计算偏差(像素坐标转云台角度)
float dx = target.x - imgCenter.x;
float dy = target.y - imgCenter.y;
// 比例控制(可根据需要改为PID控制)
float panAngle = dx * 0.1f; // 水平偏转系数
float tiltAngle = dy * 0.08f; // 垂直偏转系数
// 限幅保护
panAngle = constrain(panAngle, -30.0f, 30.0f);
tiltAngle = constrain(tiltAngle, -20.0f, 10.0f);
// 设置舵机角度
setServoAngle(PAN_SERVO, panAngle);
setServoAngle(TILT_SERVO, tiltAngle);
// 触发激光(脉冲宽度控制标记时间)
if (fabs(dx) < 5 && fabs(dy) < 5) { // 中心偏差小于5像素时触发
laserPulse(100); // 100ms脉冲
}
}
7. 系统集成与调试技巧
7.1 多任务调度设计
时间片轮转调度器:
c复制typedef struct {
void (*task)(void);
uint16_t interval;
uint16_t counter;
} TaskTypeDef;
TaskTypeDef taskList[] = {
{cameraCapture, 2, 0}, // 图像采集,每2ms执行
{imageProcess, 8, 0}, // 图像处理,每8ms执行
{motorControl, 4, 0}, // 电机控制,每4ms执行
{steeringControl, 6, 0}, // 转向控制,每6ms执行
{targetDetection, 10, 0},// 目标检测,每10ms执行
{debugOutput, 50, 0} // 调试输出,每50ms执行
};
void scheduler(void) {
for (uint8_t i = 0; i < sizeof(taskList)/sizeof(TaskTypeDef); i++) {
if (taskList[i].counter >= taskList[i].interval) {
taskList[i].task();
taskList[i].counter = 0;
}
taskList[i].counter++;
}
}
7.2 调试工具开发
无线调试协议设计:
c复制#pragma pack(1)
typedef struct {
uint8_t header; // 0xAA
uint8_t type; // 数据类型
uint16_t data[8]; // 数据载荷
uint8_t checksum; // 校验和
} DebugPacket;
#pragma pack()
void sendDebugData(uint8_t type, uint16_t *values) {
DebugPacket packet;
packet.header = 0xAA;
packet.type = type;
uint8_t sum = type;
for (uint8_t i = 0; i < 8; i++) {
packet.data[i] = values[i];
sum += (values[i] & 0xFF) + (values[i] >> 8);
}
packet.checksum = sum;
uartSend((uint8_t*)&packet, sizeof(DebugPacket));
}
常用调试数据类型:
c复制#define DEBUG_IMAGE_ROW 0x01 // 发送图像某行数据
#define DEBUG_BORDER 0x02 // 发送边界数据
#define DEBUG_PID_PARAM 0x03 // 发送PID参数
#define DEBUG_MOTOR_SPEED 0x04 // 发送电机速度
#define DEBUG_TARGET_POS 0x05 // 发送目标位置
7.3 典型问题排查指南
图像采集异常:
- 检查摄像头供电电压(实测应在3.0-3.6V之间)
- 确认同步信号(VSYNC/HREF)是否正常
- 测试时钟频率(典型值8-10MHz)
- 检查数据线连接(推荐使用20cm以内排线)
电机控制不稳定:
- 检查编码器信号是否正常(A/B相波形应正交)
- 测量电机驱动输出波形(PWM频率应为10kHz±5%)
- 检查电源电压(满电时应在7.4-8.4V之间)
- 观察电流变化(空载电流通常<0.5A)
靶标识别失败:
- 调整摄像头曝光参数(靶标区域灰度值应在180-220之间)
- 检查二值化阈值(推荐使用大津法动态阈值)
- 优化搜索区域(限制在赛道边界内)
- 增加形态学处理(可选开运算消除噪声)
在实际调试中,建议采用"分而治之"的策略,先确保各子系统独立工作正常,再进行系统集成。同时养成数据记录的习惯,通过对比不同参数下的运行效果,可以更快找到最优配置。