简介:OV7670摄像头循迹程序源码是一份面向智能小车/机器人视觉导航开发的完整Keil工程,基于AVR或ARM(STM32F1xx)嵌入式平台,解决利用OV7670图像传感器采集环境图像,经灰度/黑白二值化、去噪、边缘检测后识别赛道边界,并通过PID控制电机实现自主循迹的问题。资源共204个文件,压缩包4.91MB;核心为47个C源文件与50个H头文件,除主控逻辑外还覆盖STM32定时器、FLASH、RCC、ADC、I2C等驱动以及ILI93xx LCD显示模块;另含axf/hex烧录文件、o/d/crf等编译中间文件和Keil工程配置,可直接打开编译、烧录调试。该方案已尝试由CameraBlackWhiteWarShip移植到纪伟最小系统板,代码流程涵盖OV7670初始化与SCCB配置、图像预处理、赛道边界识别、路径规划、电机控制等环节,能帮助读者理解从传感器数据到控制输出的完整链路。目前已有3688人学习下载,适合嵌入式学习者和智能车竞赛队伍用于快速搭建原型、对照移植或扩展二次开发。
1. OV7670摄像头循迹源码落地:多数人第一步卡在出图而不是循迹
这次要聊的是OV7670摄像头循迹的程序源码,一个听起来「做完就能跑」、实际先把图像调出来就劝退一半人的方向。OV7670不是USB摄像头,插上线就能在屏幕上看画面——它是一颗靠寄存器配置输出 RGB565 / YUV 的CMOS数字传感器,上电默认输出的是测试彩条,不是真实画面。循迹任务本身不复杂:灰度图里找深色线、算横向偏差、给转向控制量;真正让新手反复翻车的是图像链——SCCB时序、帧同步、DMA缓存、二值化阈值,每一环都能让摄像头输出雪花或者全黑。这篇按「先让摄像头出图、再做找线算法、最后控车」的顺序写,适合正在做智能车竞赛、课设或毕设的开发者,目标是用最少的调试时间跑出一套能稳定走线的循迹demo。
2. 循迹系统的图像链路:从OV7670到二值化后的赛道
2.1 帧同步与DMA缓存:为什么循迹必须用整帧采样
OV7670输出的是一行一行的像素数据,伴随 HREF(行有效)、VSYNC(帧有效)和 PCLK(像素时钟)三个同步信号。很多人一开始只盯着 PCLK 采样,结果发现图像左右撕裂、上下错位——原因很简单:PCLK 只告诉你「当前这位是像素」,不告诉你「这一行从哪开始、这一帧从哪开始」。
循迹算法需要的是完整的、连续的一帧画面,不是零散像素流。所以第一步是搭帧同步采集:用 VSYNC 下降沿标志一帧的开始,在 HREF 有效期间按 PCLK 逐像素读取。常见做法是用定时器配合 GPIO 模拟时序,或者在带 DCMI 接口的单片机上用硬件捕获。DMA 双缓冲是最省 CPU 的方式:一帧数据写入缓冲区A时,CPU 处理缓冲区B,下一帧到来时交换,避免采集和处理互相等待。
// DMA 双缓冲采集配置示意(以 GPIO 模拟时序的采集方案为例) #define IMG_W 320 #define IMG_H 240 #define IMG_SIZE (IMG_W * IMG_H * 2) // RGB565 每像素2字节 static volatile uint8_t frame_buf[2][IMG_SIZE]; // 双缓冲 static volatile uint8_t active_buf = 0; // 当前写入的缓冲 void start_frame_capture(void) { // 等待 VSYNC 下降沿,表示新的一帧开始 while (READ_VSYNC_PIN() == 1); // 重置行计数,准备采集一整帧 line_count = 0; // 启动 DMA:把 PCLK 驱动的像素数据写入 active_buf DMA_Config(frame_buf[active_buf], IMG_SIZE); DMA_Enable(); } void DMA_TransferComplete_IRQHandler(void) { // 一帧采集完成:交换缓冲 uint8_t done_buf = active_buf; active_buf ^= 1; // 把刚采完的帧交给处理函数,同时启动下一帧采集 process_frame(frame_buf[done_buf]); start_frame_capture(); }这个代码的关键在于 DMA 传输完成中断里做缓冲切换。active_buf在每次帧完成后翻转,保证采集和处理互不踩踏。IMG_SIZE用 320×240×2 是因为 OV7670 在 QVGA 模式下输出 RGB565,每像素两个字节。如果你用 VGA 模式 640×480,缓冲区要扩大到原来的四倍,普通单片机会非常吃紧,循迹场景建议始终用 QVGA。
这里有一个高频踩坑点:DMA 的传输方向配置。OV7670 的数据引脚是 8 位的,但 RGB565 一个像素占两个字节,如果你的 DMA 配置成 16 位宽度、地址没按 2 字节对齐,图像会出现「每个像素错半字节」的诡异花屏。我一般会把 DMA 配置成 8 位传输,保证逐字节落地,再在算法层按(high << 8) | low拼出 RGB565。参数上,PCLK在 24MHz 主频下大约 6~12MHz,DMA 的传输速率要匹配,过快会丢像素、过慢会拉长帧间隔导致帧率下降。
2.2 RGB565转灰度:循迹真正处理的是亮度而不是色彩
循迹的核心是区分「线」和「地面」,本质是亮度的差异,不是颜色的差异。OV7670 直接输出的 RGB565 里,每个像素是 5 位红、6 位绿、5 位蓝,直接用这个数据做阈值分割会非常不稳定——因为光照变化时,三个通道的值会同时漂移,而且摄像头自动增益和白平衡也会参与变化。把 RGB565 转成一个 8 位灰度值,后续的阈值、找线、偏差计算全部基于灰度,稳定性和实时性都比基于彩色通道好得多。
// RGB565 转灰度:提取 R/G/B 分量并加权合成 uint8_t rgb565_to_gray(uint16_t pixel) { uint8_t r = (pixel >> 11) & 0x1F; // 高5位是红色分量 uint8_t g = (pixel >> 5) & 0x3F; // 中间6位是绿色分量 uint8_t b = pixel & 0x1F; // 低5位是蓝色分量 // 把5位/6位分量扩展到8位范围,再做加权灰度 r = (r << 3) | (r >> 2); g = (g << 2) | (g >> 1); b = (b << 3) | (b >> 2); // 标准灰度加权:人眼对绿色最敏感 return (uint8_t)((r * 30 + g * 59 + b * 11) / 100); }这个转换函数是循迹代码里被调用次数最多的一个,每一帧 320×240 共 76800 个像素都要过一遍。计算量不小,所以在单片机上常见做法不是用浮点除 100,而是用移位近似:(r * 77 + g * 150 + b * 29) >> 8,误差很小但速度快很多。参数上,如果你发现灰度图偏亮或偏暗,可以微调三路权值,但不建议把绿色权值调得太低——循迹场景里绝大多数地面是灰色或浅色,绿色通道的信息量最大。
转灰度这一步还有个细节容易被忽略:OV7670 的 RGB565 色彩格式是可以配置成 RGB444 或 RGB555 的。如果你在寄存器里设成 RGB444,这里的分量提取逻辑就全错了。配置时确认COM7寄存器选择了 RGB565,否则你会在「颜色对但灰度图花掉」这种问题上白耗半天。
2.3 大津法阈值:光照变化时最稳的灰度分割方式
灰度图拿到手,下一步是把它变成二值图——线是黑的(或白的),地面是另一个颜色。多数教程会直接写if (gray < 80) line = 1;,但固定阈值在循迹场景里几乎必翻车:上午十点的阳光和下午五点的夕阳光照强度差好几倍,同一个 80 阈值,早上能正确分割,下午整幅图全黑或全白。更别提穿过树荫时,同一帧画面里不同区域的亮度都不同。
固定阈值不行,就得用自适应阈值。最经典的是大津法(Otsu):遍历所有可能的灰度阈值,找一个让「前景类内方差 + 背景类内方差」最小的值。这个值能自动适应当前画面的亮度分布,光照变了它也跟着变。
// 大津法求最佳分割阈值:输入灰度图,输出 0~255 的阈值 uint8_t otsu_threshold(uint8_t *gray, uint32_t size) { uint32_t hist[256] = {0}; uint32_t total_pixels = size; // 统计灰度直方图 for (uint32_t i = 0; i < size; i++) { hist[gray[i]]++; } // 计算全局灰度均值 uint64_t sum = 0; for (int i = 0; i < 256; i++) { sum += (uint64_t)i * hist[i]; } // 遍历阈值 t:将像素分为背景(0~t)和前景(t+1~255) uint64_t sumB = 0; // 背景灰度累加值 uint32_t wB = 0, wF = 0; // 背景/前景像素数 double max_var = 0.0; uint8_t best_t = 0; for (int t = 0; t < 256; t++) { wB += hist[t]; // 背景像素数增加 if (wB == 0) continue; wF = total_pixels - wB; // 前景像素数 if (wF == 0) break; sumB += (uint64_t)t * hist[t]; uint64_t sumF = sum - sumB; // 前景灰度累加值 double mB = (double)sumB / wB; // 背景均值 double mF = (double)sumF / wF; // 前景均值 double var = (double)wB * wF * (mB - mF) * (mB - mF); if (var > max_var) { max_var = var; best_t = t; } } return best_t; }大津法计算里最耗时的部分是直方图统计,O(n) 遍历一遍 76800 个像素,在 72MHz 的单片机上大约要 10~20ms,整体帧率会掉一些。实际工程里我一般不会整帧做大津法,而是取画面中间一行或中间区域的一块 64×48 小窗口做统计,因为赛道线绝大部分在中部区域,阈值代表的是「当前主视野的亮度分布」。这个优化能把计算时间压到 1ms 以内,循迹实时性明显更好。
用大津法时有一个方向性的问题:你要确保前景对应的是赛道线而不是背景。如果场地是深色地面、浅色线,那大津法分出来的前景是线,直接用;如果场地是浅色地面、深色线,前景是地面,要取反。代码里我会把best_t后接一层判断——统计前景像素的占比,如果超过 50%,说明前景是背景,取反处理。这个判断逻辑在场地颜色和线色固定的前提下永远不会变,属于写一次就一劳永逸的事。
3. 循迹算法落地:找线、算偏差、控制转向
3.1 逐行扫描找线:从二值化图像提取路径坐标
二值图不是循迹的终点,只是中间产物。真正要交给控制算法的,是「线在哪里、车偏离了多少」。常见做法是逐行扫描二值图,对每一行找出深色像素的左右边界,取中点作为该行的路径中心点;然后从远到近(或从近到远)取几行中心点,做加权平均得到整条线的横向位置。
这里有一个新手容易犯的错:扫描整幅图所有行,计算量爆炸,而且画面最上方通常是远景、线很细甚至消失,干扰大。我会只扫描一个感兴趣区域(ROI),比如取画面下半部分从第 100 行到第 230 行,每 10 行扫一次,总共 13 行左右。这个范围内的线距离车近、宽度大、特征稳定,计算量也小。
// 逐行扫描二值图:从 ROI 内每行找黑线中心 #define ROI_ROW_START 100 // ROI 起始行 #define ROI_ROW_END 230 // ROI 结束行 #define ROW_STEP 10 // 每 10 行采样一次 typedef struct { uint8_t found; // 该行是否找到线 uint16_t center; // 该行线中心 x 坐标 } LinePoint; LinePoint line_points[14]; // (230-100)/10 ≈ 13 行,留一个余量 void scan_line_centers(uint8_t *binary_img, uint16_t img_w, uint16_t img_h) { uint8_t idx = 0; for (uint16_t row = ROI_ROW_START; row < ROI_ROW_END; row += ROW_STEP) { uint8_t *line = binary_img + row * img_w; uint16_t left = 0, right = 0; uint8_t found = 0; // 先从左往右找第一个深色像素 for (uint16_t col = 0; col < img_w; col++) { if (line[col] == 0) { // 假设线是黑色(0),地面是白色(255) left = col; found = 1; break; } } // 如果找到左边界,从右往左找右边界 if (found) { for (uint16_t col = img_w - 1; col > left; col--) { if (line[col] == 0) { right = col; break; } } line_points[idx].found = 1; line_points[idx].center = (left + right) / 2; } else { line_points[idx].found = 0; } idx++; } }这段代码里有两个参数值得说。ROW_STEP决定扫描密度,10 行一个采样在直线和缓弯上完全够用,但在连续急弯路段,10 行的间距可能导致相邻两个采样点之间跨越了弯道拐点,丢失曲率信息。这时候把步长改成 5,13 行变 26 行,计算量翻倍但循迹精度明显提升。另一个是line[col] == 0的判断假设线为黑色,如果你把线做成白色、地面是深色,改成== 255即可,不用动其他逻辑。
丢线处理是这个函数的隐藏难点。当车出界或线被阴影吞掉,某一行可能找不到边界。我的做法是:不把那行的 center 当作 0 参与计算,而是跳过该行,只用两侧有效行的数据做插值。如果连续三行都丢线,就判定为「出线」,进入状态机里的恢复逻辑,而不是继续盲目算平均。
3.2 偏差量计算与转向控制:从像素偏差到舵机角度
找到线的中心位置后,转向控制的本质是算「线中心在画面中的位置」和「目标位置」的差。目标位置通常取画面宽度的一半——摄像头正对前方时,理想情况下线应该在正中央。偏差就是line_center - img_width/2,正值表示线偏右、负值表示线偏左。
这个原始偏差不能直接给舵机用:画面是透视的,远处的像素偏移和近处的像素偏移对应的实际横向距离完全不同——同样的 20 像素偏差,在近处只偏了几厘米,在远处可能偏了半米。所以常见做法是对多行中心点做加权,近处的行权重大、远处的行权重小,相当于优先保证车头附近的准确性。
// 计算加权偏差:近处行权重大,远处行权重小 int16_t calc_weighted_error(LinePoint *points, uint8_t count, uint16_t img_w) { int32_t weighted_sum = 0; int32_t weight_total = 0; for (uint8_t i = 0; i < count; i++) { if (!points[i].found) continue; // 跳过丢线行 int16_t err = (int16_t)points[i].center - (int16_t)(img_w / 2); uint8_t weight = i + 1; // 越靠近画面底部(索引越大)权重越高 weighted_sum += (int32_t)err * weight; weight_total += weight; } if (weight_total == 0) { return 0; // 完全丢线,由状态机处理 } return (int16_t)(weighted_sum / weight_total); } // PID 控制:输出舵机 PWM 占空比 int16_t pid_steering(int16_t error) { static int16_t last_error = 0; static int32_t integral = 0; float kp = 0.35f, ki = 0.001f, kd = 0.08f; integral += error; // 积分限幅,防止 windup if (integral > 1000) integral = 1000; if (integral < -1000) integral = -1000; int32_t output = (int32_t)(kp * error + ki * integral + kd * (error - last_error)); last_error = error; // 输出限幅到舵机 PWM 范围 if (output > 60) output = 60; if (output < -60) output = -60; return (int16_t)output; }calc_weighted_error里的权重逻辑值得细看:索引越大的行越靠近画面底部(车头方向),权重越大。这个设计对应的是「近处误差可信度高、远处误差噪声大」的物理直觉。你可以把权重从线性改成平方关系,让近处行占据绝对主导,但要注意这会让车对急弯的反应变迟钝,因为弯道信息主要来自远处行。
PID 参数在循迹场景里没有万能组合。上面这组kp=0.35、ki=0.001、kd=0.08是从典型舵机响应需求推出来的起点:转向响应要快但不能震荡,所以比例系数居中、微分有抑制超调作用、积分只用很小一点消除稳态误差。如果你发现车在直道上左右画龙,是kd太小或kp太大;如果过弯转向不足、冲出赛道,是kp太小或kd过大(微分把转向拉住了)。我不会在代码里写死这三项,而是放到编译宏里,方便现场调参——这一步会在第 5 章展开。
3.3 循迹主循环状态机:启动、正常循迹、丢线恢复
很多人写的循迹代码是一个while(1)里顺序执行「采集→处理→控制」,出了问题就整个流程崩掉。实战里我会给循迹加一个简单的状态机,至少区分「启动」「正常」「丢线」「恢复」四个状态。为什么需要状态机?因为摄像头是周期性出帧的,丢线不是异常,是一个必然会发生的普通情况——比如车压到线上转弯时,视角里线可能短暂消失半秒钟。
状态机的核心逻辑:正常状态下,如果连续 N 帧找不到线,进入「丢线」状态;丢线状态下不执行正常 PID 输出,而是执行一个预设的「回搜」动作——保持上次的转向方向并减速,持续 M 帧后如果找回线,恢复正常循迹;如果一直找不回,停车。这个机制比单帧丢线就原地停车要实用得多。
// 循迹状态机简版 enum TrackState { TRACK_START, TRACK_NORMAL, TRACK_LOST, TRACK_STOP }; void track_loop(void) { static enum TrackState state = TRACK_START; static uint8_t lost_count = 0; const uint8_t LOST_THRESHOLD = 10; // 连续10帧丢线才判定为真丢线 const uint8_t RECOVER_FRAMES = 30; // 丢线后回搜最多30帧 uint8_t line_found = check_line_visible(); // 当前帧是否有有效线 switch (state) { case TRACK_START: // 起步阶段:先让电机低速前进,等线进入视野 motor_set_speed(20); if (line_found) { state = TRACK_NORMAL; lost_count = 0; } break; case TRACK_NORMAL: line_found = check_line_visible(); if (!line_found) { lost_count++; if (lost_count >= LOST_THRESHOLD) { state = TRACK_LOST; lost_count = 0; } } else { lost_count = 0; int16_t err = calc_weighted_error(line_points, POINT_COUNT, IMG_W); int16_t steer = pid_steering(err); steering_set(steer); motor_set_speed(30); } break; case TRACK_LOST: // 保持上次转向方向回搜,同时减速 steering_set(last_steer_output); motor_set_speed(15); lost_count++; if (line_found) { state = TRACK_NORMAL; lost_count = 0; } else if (lost_count >= RECOVER_FRAMES) { state = TRACK_STOP; motor_set_speed(0); // 放弃,停车 } break; case TRACK_STOP: // 停车后需要手动复位才能重新启动 break; } }这个状态机的精妙处不在代码结构,而在两个阈值参数:LOST_THRESHOLD = 10和RECOVER_FRAMES = 30。前者太大会让「真出线」后车继续盲目前冲好几米,太小会让车在正常过弯的短暂丢线中频繁误判、动作一顿一顿。后者太大会让车在错误方向上越走越远,太小则可能还没转回正确方向就放弃了。这两个值没有理论最优解,和车速、弯道半径直接相关,只能实跑微调。我一般会先用小半径场地测出参考值,再放到大场地上验证。
状态机里还有一个隐含的工程经验:丢线状态下不要使用本次的 PID 输出,而是沿用last_steer_output。原因是丢线那一刻的偏差计算往往是无效的(线都找不到,偏差就是垃圾数据),用它更新 PID 会让积分项累积一个巨大的错误值,等重新找到线时,积分项的残留会让车猛打方向,非常危险。
4. OV7670循迹最常见的5个翻车点:现象、原因与排查顺序
4.1 屏幕全是雪花或条纹,GEN_CLK 和 PCLK 配置不对
现象:上电后采集到的图像像是老式电视的雪花点,或者横条纹从头到尾扫过,完全看不出任何物体轮廓。原因:OV7670 的时钟系统非常敏感。它的内部很多模块依赖PCLK分频,寄存器0x11(CLKRC)设置的是 PLL 倍频和分频系数,如果你给它的主时钟是 24MHz,但 CLKRC 配成了 1:1 直通,PCLK会飙到十几甚至几十 MHz,单片机的 GPIO 采样速度根本跟不上,采进来的全是随机电平。解决:先确认 OV7670 模块上的晶振频率(常见 24MHz 或 12MHz),然后按官方寄存器配置表设CLKRC,目标是把PCLK压到 8MHz 以下。用示波器测PCLK引脚频率是最直接的诊断手段——如果你没示波器,就按「能出图的最小 PCLK」原则去配置,宁可图像帧率低一点,也不要雪花。
4.2 图像左右翻转或上下颠倒,镜像寄存器没设
现象:图像能出来,但物体左右是反的,比如车往左动,画面里的线往右跑,而且转向控制完全乱套。原因:OV7670 传感器在模块上的安装方向不同,加上镜头是正装还是倒装,直接影响输出图像的镜像关系。有的模块出厂默认是「镜像 + 翻转」输出,你在算法里按正像算偏差,必然反着来。解决:读寄存器0x1E(MVFP),把Mirror和Vflip位按你的安装方向设好。这个参数没法盲调——我的做法是在画面里放一个左上角有缺口标记的纸板,采集一帧图像,看缺口实际出现在哪个角,再反推需要设哪几个位。这一步必须在写找线算法之前做,否则后面所有偏差方向都是错的。
4.3 二值化后线是断的或整片黑,自动增益和曝光白平衡作怪
现象:灰度图看着正常,但二值化后赛道线断成一截一截,或者地面和线分不开,整个下半屏全是黑的。原因:OV7670 默认开启自动曝光(AEC)和自动白平衡(AWB),这两个自动控制在光照渐变时会持续调整增益和色彩,导致同一段地面在不同时刻的灰度值差别很大。你在大津法阈值之上加一个固定偏移,恰好落在线的灰度范围内,就会把线切碎;落在背景范围内,就会把背景误判成线。解决:循迹场景一定要关闭或锁定 AEC 和 AWB。操作上常做的是把寄存器0x13(COM8)的 AGC 位设为 0,并把曝光值手动固定到合理区间(比如0x3A~0x3D四个寄存器配合定曝光行数和每行曝光时间)。画面会稍微变暗或变亮,但灰度分布会稳定很多,大津法也能稳定工作。
4.4 只有半屏图像或画面斜向撕裂,DMA 缓冲区没按行长对齐
现象:图像上半部分正常,下半部分是上一帧的残留,或者画面中间有一条斜向的撕裂线。原因:DMA 在传输一帧中途被别的任务打断,或者缓冲区的行宽和实际图像行宽不一致。OV7670 在 QVGA 模式下每行输出 320 像素,但实际 HREF 有效宽度受寄存器HSTART/HSTOP配置影响,如果你把显示窗口设成了 320 但寄存器实际给的是 304 或 336,DMA 按 320 读就会错位。解决:用示波器对比 HREF 的脉冲高电平持续时间,算出实际每行像素数,再修正 DMA 的行存储宽度。还有一种情况是 DMA 缓冲区的首地址没有按 32 字节对齐,在部分单片机的 DMA 控制器上会触发总线错误或数据错位,给 buffer 加上__attribute__((aligned(32)))往往能解决。
4.5 车直道正常、弯道必冲出赛道:ROI 只看近处不看远处
现象:直道上走得很稳,一到半径稍大的弯道就打不满方向,直接从切线方向冲出赛道。原因:这是典型的「信息不足」问题。如果你 ROI 只选近处 100 行内,画面里的线大部分是斜线,能给出的偏差量不足以推导出弯道曲率;而远处弯道的弧线信息,恰恰是预判转向角度的关键。解决:把 ROI 的起始行往上延伸到画面的 1/3 处甚至更高,让远处的弯道信息进入偏差计算。同时注意,远处线的像素宽度只有近处的几分之一,找线时的形态学处理(比如先做一次膨胀再做腐蚀)能增强微弱信号。另一个实用技巧是给「远端行的线位置」加一个衰减系数参与 PID 计算,既能预判弯道,又不会让远处噪声干扰近处的精确控制。
5. 现场调试三板斧:把黑匣子变成可见的变量
循迹调参最绝望的状态是「车歪了,但我只知道它歪了,不知道为什么」。OV7670 的代码运行起来是个黑匣子——摄像头数据进 CPU,舵机输出出去,中间发生了什么,不通过调试接口根本看不见。我的习惯是给循迹源码加三个调试出口,全部通过串口输出到上位机。
第一个出口是灰度图和二值化结果的可视化。把摄像头采集的一帧图像经过灰度转换和二值化后,按简单的字符画协议通过串口发到电脑——每像素映射成一个字符,黑色线用#、白色地面用空格,上位机用等宽字体显示。这个画面不需要多清晰,能看出线的位置、粗细、断裂情况就够用。做法是在process_frame里加一个调试宏,#define DEBUG_IMAGE_OUTPUT 1时每 10 帧输出一次,正常跑的时候关掉。
第二个出口是偏差值的实时曲线。串口每帧输出一次calc_weighted_error的返回值,加上 PID 输出的舵机值,两个数字用逗号分隔,上位机用虚拟示波器软件或串口绘图工具显示成波形。这个波形能直接看出调参效果:kp太大时波形震荡幅度大且频率快,kd太大时波形过冲后回不到零点,积分项异常时波形会有持续的直流偏移。我就是靠这个波形把 PID 从「玄学调参」变成了「看波形改参数」,效率提升了一个量级。
第三个出口是状态机的跳转记录。每当track_loop里的状态发生切换,通过串口打印一条带状态编号和时间戳的记录,比如LOST: frame=1234、RECOVER: frame=1256。跑完一圈后回看记录,如果发现车在某个固定弯道频繁丢线,说明那个弯道的曲率超出了当前 ROI 和 PID 的适应范围,要么调大远端权重、要么降低入弯速度。
我的习惯是先把三个调试出口全部打开,在场地里跑一圈拿到所有数据,再关掉前两个、保留状态机记录,最后上场比赛或演示时全部关闭。这样做的原因是串口输出本身会占用 CPU 时间,尤其是图像字符画输出,一帧的内容能占好几毫秒,开着调试跑出来的循迹表现和关掉后不完全一致——你永远不要在开了调试的模式下去验证最终稳定性。这算是我从几次「调试时好好的、关掉调试就翻车」当中总结出来的血泪经验,希望帮到你:源码不只是一个能编译的 demo,把调试出口设计进去,才真正具备上路跑的价值。
本文还有配套的精品资源,点击获取