☰
2023电赛E题红点打靶与自动追踪:视觉标定、云台PID与双色闭环实践
2026/10/26 9:17:43 网站建设 项目流程

简介:围绕2023年全国大学生电子设计竞赛E题整理的参考工程包,面向准备电赛控制类赛题的高校学生与指导教师,核心示例为基于STM32F1的自动追光云台。资料以Keil工程为主线,包含STM32F10x标准外设库的C源文件与头文件、启动和链接配置、编译中间文件及可烧录的HEX/AXF产物,便于对照理解光敏传感器采样、电机驱动、PID调节与电源管理等环节的实现组织方式,也适合作为嵌入式系统课程设计与赛前训练的参考底稿。压缩包共199个文件,以h头文件、c源码、o/d/crf编译中间文件及少量s汇编和工程配置文件为主,整体约6.74MB,目录保留了较完整的开发过程痕迹。已有7810人学习,读者可从中获得赛题方案框架、工程配置范例与调试排错思路,用于搭建自己的追光云台原型并查漏补缺。

1. 红点打靶与自动追踪:2023 年电赛 E 题参考拆开看是四块活

2023 年电赛 E 题那套运动目标控制与自动追踪,表面上是"激光笔打靶",实际拆开是四件互不相同的活:视觉把红点从白靶纸上抠出来、坐标从像素换算到毫米、云台把角度闭环到毫秒级、绿光云台再去追红光。这四个环节任何一环抖一下,整套系统就在靶纸上画出一条歪歪扭扭的线。这份 2023 年电赛 E 题参考的价值不在于"给一份能跑的工程",而在于它把上面四块拼装顺序、标定方法和常见参数给固定下来了。适合两类人:一是正在备赛、需要一套能当基线改的队伍;二是把它当课程设计或选拔考核题的高校师生,照着复现一遍就能明白每一环为什么要这么接。

2. 视觉前端:红点坐标怎么稳定吐出来

视觉这一环最容易出问题,也最容易被低估。很多人以为把摄像头往上一架、调个阈值就能得到红点坐标,结果到了现场,日光灯一开、靶纸一反光,质心就开始乱跳。这一章把布局、取点、标定三件事拆开讲,顺序不要颠倒,因为布局决定了标定方式,标定方式决定了取点的精度要求。

2.1 固定俯视还是同轴随动:两种布局各有各的坑

E 题这类题目在视觉布局上只有两条主流路线,选择之前先想清楚一件事:你需要的是绝对位置还是相对误差。

固定俯视是把摄像头架在靶纸正上方一到两米的位置,视野覆盖整张靶纸,检测到的红点像素坐标经过标定直接变成靶面毫米坐标。好处是天然绝对定位,云台偏了多少、现在指到哪里,一目了然,做"沿边框走一圈""回到中心点"这类基础题非常省事。代价是视野大导致单个红点只有十几个像素,稍有噪声质心就飘一两个像素,换算到靶面可能就是几毫米。

同轴随动是把摄像头和激光笔一起装在云台上,光轴尽量与激光笔指向平行。此时红点在图像里的偏移量,本质上就是激光指向的偏差量,直接拿来做闭环误差,分辨率极高,红点能占几十个像素。但它只能做伺服,系统不知道自己在靶面的绝对位置,一旦积分累加出现偏差,走完一圈回到中心点时可能已经偏了两三厘米。

实际项目里我一般会做混合:主视觉用固定俯视拿绝对坐标做轨迹控制,追踪环节用跟随绿光云台的同轴摄像头做高分辨率误差反馈。两套镜头各干各的活,互不干扰。

对比项固定俯视同轴随动
输出量靶面绝对毫米坐标指向相对偏差
单点分辨率低(视野大)高(视野窄)
是否需要标定必须,四点或多点只需量比例系数
累积误差无有,靠零点复位消除
适合承担的题走边框、定点、画圆追踪对齐、末端精调
主要风险支架晃动、透视畸变长时间漂移、机械回差

支架这一项值得单独提一句。俯视摄像头如果用的是三脚架或者临时搭的铝型材,稍微碰一下就整体偏移,之前标定的单应矩阵全部作废。稳妥的做法是把摄像头和靶纸固定在同一块底板上,或者至少用夹具把支架脚锁死,标定完之后不要再动。

2.2 用 LAB 阈值把红点从白靶纸上抠出来

摄像头一上来就要关掉自动曝光、自动增益和自动白平衡。这三个"自动"在出厂设置里是为了让你拍风景好看,在光斑检测里全是灾难:激光笔一亮,自动曝光立刻把整幅图压暗,红点面积瞬间缩水;激光笔一移开,画面又亮回来,阈值全废。

锁定之后,阈值空间建议用 LAB 而不是 RGB。RGB 三个通道高度相关,光照一变三个值一起动,你得同时改三个上限三个下限;LAB 里 A 通道直接表示红绿方向,红色光斑的 A 值明显偏高,跟亮度解耦得比较干净,环境光变化时主要影响的只是 L 通道。

import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240:视野够、帧率稳,追踪够用 sensor.skip_frames(time=1000) # 等自动曝光跑完再锁 sensor.set_auto_gain(False) # 锁增益,否则光斑一亮面积就缩 sensor.set_auto_exposure(False, exposure_us=8000) # 曝光值按现场亮度手调 sensor.set_auto_whitebal(False) # LAB 阈值 (L_min, L_max, A_min, A_max, B_min, B_max) RED_TH = (25, 100, 30, 127, -25, 65) # 红激光:A 通道偏高 GREEN_TH = (25, 100, -75, -15, -20, 70) # 绿激光:A 通道偏低 def find_spot(img, th, min_pix=12): blobs = img.find_blobs([th], pixels_threshold=min_pix, # 小于这个像素数当噪声丢掉 area_threshold=min_pix, merge=True, # 光斑碎成几块时合并 margin=10) # 合并的像素间距 if not blobs: return None b = max(blobs, key=lambda x: x.pixels()) # 取最大块,滤掉反光点 return (b.cx(), b.cy(), b.pixels()) while True: img = sensor.snapshot() red = find_spot(img, RED_TH) if red: print("R", red)

这段代码里有三个参数需要按现场改:exposure_us、RED_TH的 A 下限、min_pix。曝光值调法是把激光笔关掉,让白靶纸在图像里呈中灰而不是纯白,此时红点打上去才不会过曝成一团。min_pix的取值跟摄像头高度直接相关,一米五左右的高度、QVGA 分辨率,红点通常在 20 到 60 像素之间,取 12 能滤掉绝大部分噪点又不会误杀远处的小光斑。

merge=True这个参数很多人不用,但打白靶纸时红点边缘经常因为过曝被切成两半,不合并的话质心会在两半之间来回跳,云台就跟着抽。

2.3 像素到毫米:四点单应标定比"量比例尺"靠谱

最省事的标定方式是量一下靶纸在图像里占多少像素,然后除一下得到 mm/px 的比例。这个做法只在摄像头光轴严格垂直于靶纸、且镜头畸变可以忽略时成立。实际架设时摄像头总有点倾斜,靶纸四角在图像里被拉伸的程度不一样,用单一比例尺换算,中心点误差可能只有 1mm,到边角就能到 8mm 以上,走边框的时候你会看到光斑在四个角上明显往外飘。

正确的做法是用单应变换。在靶纸上选四个已知物理坐标的点(比如四个角,或者四条边的中点),分别记录它们在图像里的像素坐标,解一个 3×3 的矩阵,之后任意像素点都能映射到毫米坐标。

import numpy as np # 四组对应点:像素坐标 (u,v) 与靶面物理坐标 (x,y),单位 mm px = np.array([[42, 36], [612, 40], [608, 452], [38, 448]], dtype=float) mm = np.array([[0, 0], [600, 0], [600, 400], [0, 400]], dtype=float) A, b = [], [] for (u, v), (x, y) in zip(px, mm): A.append([u, v, 1, 0, 0, 0, -x * u, -x * v]); b.append(x) A.append([0, 0, 0, u, v, 1, -y * u, -y * v]); b.append(y) h = np.linalg.lstsq(np.array(A), np.array(b), rcond=None)[0] H = np.append(h, 1.0).reshape(3, 3) def to_mm(u, v, H=H): p = H @ np.array([u, v, 1.0]) return p[0] / p[2], p[1] / p[2] # 反过来验证:把四个标定点再映射回去,残差超过 2mm 就重新采点 for (u, v), (x, y) in zip(px, mm): mx, my = to_mm(u, v) print(f"({x},{y}) -> ({mx:.1f},{my:.1f}) err={np.hypot(mx-x, my-y):.2f}mm")

代码逻辑很直白:把八元线性方程组堆出来最小二乘求解,矩阵H就是像素到毫米的映射。最后那段反算是必须做的,四个标定点的回代残差长期超过 2mm,说明要么点没点准,要么镜头畸变太大、四点模型撑不住,这时候就得加点到六点或九点,改用二次多项式拟合。

提示:标定用的四个点不要用激光笔去打,激光笔本身就有安装误差。用一张打印好的网格纸铺在靶面位置,手工量出四个角点,误差来源会少一半。

采点这一步如果靠人眼对图像找角点,重复性很差,同一个人两次采点能差三四个像素。常见做法是在靶面四个角贴黑色小方块,用色块识别自动找中心,把采点这件事也自动化,标定一次只要几秒钟,反复标定取平均能进一步压低噪声。

3. 云台闭环:从像素误差到两路 PWM

视觉把坐标吐出来之后,剩下的问题就是把它变成舵机角度。这一章的内容看起来是控制理论,实际绝大多数翻车都发生在"标定"和"限幅"这两个最土的地方,而不是 PID 参数本身。

3.1 舵机 PWM 与角度标定:先让"零位"和"水平"对上

标准舵机走 50Hz、0.5~2.5ms 脉宽的协议,对应大致 -90° 到 +90°。用定时器产生这个信号的关键是让计数器频率刚好是 1MHz,这样一个计数就是 1 微秒,脉宽直接等于比较寄存器的值,调试时肉眼可读。

/* TIM3: 72MHz / (71+1) = 1MHz 计数,ARR=19999 -> 20ms 周期 比较值 500~2500 即 0.5~2.5ms 脉宽 */ void servo_pwm_init(void) { /* 省略 GPIO/TIM 时钟与引脚复用配置,只保留关键计数值 */ TIM3->PSC = 71; TIM3->ARR = 19999; TIM3->CCR1 = 1500; /* 上电先给中位,防止舵机猛甩 */ TIM3->CCR2 = 1500; } /* 角度 -> 比较值:deg 为期望指向角,trim 为该轴的机械零位补偿 */ static uint16_t angle_to_ccr(float deg, float trim) { if (deg > 85.0f) deg = 85.0f; /* 软限位,防止顶死烧舵机 */ if (deg < -85.0f) deg = -85.0f; float us = 1500.0f + (deg + trim) * (1000.0f / 90.0f); return (uint16_t)us; }

trim这个参数就是每个舵机的机械零位补偿。同一型号的两个舵机,中位脉宽可能差 30~80 微秒,换算成角度就是三到七度,在靶纸上就是好几厘米。标定方法是:把deg固定为 0,调trim让激光点正好落在靶面中心,记下这个值写死到代码里,之后不要再用软件的方式去补偿机械误差。

软限位那两行看着不起眼,但它是防止烧舵机的最后一道保险。二维云台在走边界轨迹时,如果 PID 积分项冲起来,输出会远远超过机械行程,舵机顶在限位上持续堵转,几秒钟电流就能把齿轮箱里的塑料齿扫平。

3.2 位置式 PID 的起手参数与抗饱和写法

这题的被控对象是舵机,本质是个带滞后的位置伺服,用位置式 PID 就够,不需要上更复杂的东西。写法上有两个细节必须处理:积分限幅和微分项的 dt 保护。

typedef struct { float kp, ki, kd; float i_max; /* 积分项绝对值上限 */ float out_max; /* 输出限幅,单位:度 */ float integ; float last_err; } pid_t; float pid_calc(pid_t *p, float err, float dt) { if (dt < 1e-4f) dt = 1e-4f; /* 防止首帧或丢帧时微分爆掉 */ p->integ += err * dt; if (p->integ > p->i_max) p->integ = p->i_max; /* 抗积分饱和 */ if (p->integ < -p->i_max) p->integ = -p->i_max; float d = (err - p->last_err) / dt; p->last_err = err; float out = p->kp * err + p->ki * p->integ + p->kd * d; if (out > p->out_max) out = p->out_max; if (out < -p->out_max) out = -p->out_max; return out; }

起手参数的顺序建议是:先把ki和kd归零,kp从 0.05 开始往上加,加到系统开始等幅振荡时退回来取一半;再把ki从 0.01 慢慢加,消除到位后的静差;kd最后加,因为这题视觉噪声大,微分项会放大噪声,一般给 0.01 到 0.05 就够,甚至干脆不给。

i_max的取值跟out_max挂钩,经验值是i_max = out_max / ki * 0.3量级,保证积分项最多贡献三成输出。不设这个限幅,光斑被遮挡一两秒后重新出现时,积分已经攒了一大坨,云台会猛甩一下,这个现象在调试现场非常常见。

3.3 轨迹生成:边框、中心往返、画圆三套参数化函数

轨迹这部分的坑在于"用相对量还是绝对量"。如果轨迹是用角度增量一点点累加出来的,跑完一圈回到起点,累积误差可能已经偏了两三度;用靶面绝对坐标做参数方程,每一帧都重新算目标点,误差不会累积。

/* 靶面坐标系:原点左下角,x 向右,y 向上,单位 mm */ #define TARGET_W 600.0f #define TARGET_H 400.0f /* 沿边框走一圈,t 从 0 到 1,s 为走过的总周长比例 */ void traj_border(float s, float *x, float *y) { float w = TARGET_W, h = TARGET_H; float per = 2.0f * (w + h); float d = s * per; if (d < w) { *x = d; *y = h; } else if (d < w + h) { *x = w; *y = h - (d - w); } else if (d < 2*w + h) { *x = w - (d - w - h); *y = 0; } else { *x = 0; *y = d - 2*w - h; } } /* 画圆:中心 (cx,cy),半径 R,角速度 w(rad/s),t 为秒 */ void traj_circle(float cx, float cy, float R, float w, float t, float *x, float *y) { *x = cx + R * cosf(w * t); *y = cy + R * sinf(w * t); }

traj_border用周长比例做参数,好处是可以随时暂停、随时反向走,不用担心走到拐角处速度突变。实际调试时把s递增的速率跟题目要求的完成时间对齐,比如要求 30 秒走完一圈,就让s每秒增加 1/30,这样不用额外写定时逻辑。

画圆的时候要注意舵机的角速度跟随能力。半径越小、角速度越高,云台越跟不上,轨迹在四个象限交界的地方会被"切角"。参数上,600×400mm 的靶面画 R=100mm 的圆,角速度超过 1.5 rad/s 之后就明显跟不上,一般做到 1.0~1.2 rad/s 比较稳。

3.4 视觉板与主控的串口帧:定长、帧头、校验、超时

视觉板和主控之间的串口通信是最容易被忽视的一环。用printf("%d,%d\n")这种 ASCII 格式,解析简单但帧长不定,一旦丢字节整个包就错位,云台会突然抽一下。稳妥的做法是定长二进制帧。

字节内容说明
00xAA帧头 1
10x55帧头 2,双字节帧头抗误同步
2len载荷长度,固定 5
3type0=红点,1=绿点,2=双点
4-5x_lo, x_hi靶面 x 坐标,单位 0.1mm
6-7y_lo, y_hi靶面 y 坐标
8sum前三到七字节累加和低 8 位
/* 解析状态机:逐字节喂入,凑够一帧才更新目标点 */ void frame_feed(uint8_t byte) { static uint8_t buf[16], idx = 0; static uint32_t last_ms = 0; if (HAL_GetTick() - last_ms > 100) { /* 超时 100ms 丢弃残帧 */ idx = 0; } last_ms = HAL_GetTick(); buf[idx++] = byte; if (idx < 2) return; if (buf[0] != 0xAA || buf[1] != 0x55) { /* 滑动窗口重新对齐 */ memmove(buf, buf + 1, idx - 1); idx--; return; } if (idx < 9) return; uint8_t sum = 0; for (int i = 2; i < 8; i++) sum += buf[i]; if (sum == buf[8]) { g_target.x = (int16_t)(buf[4] | (buf[5] << 8)) * 0.1f; g_target.y = (int16_t)(buf[6] | (buf[7] << 8)) * 0.1f; g_target.valid = 1; } idx = 0; }

超时那一行是兜底逻辑。视觉板死机或者线松了,主控如果一直用最后一帧的目标点做 PID,云台会朝着一个方向慢慢走偏顶到限位。超时后把valid清零、输出回中位,是最省事的保护。

4. 绿光追红光:双色识别、滞后补偿和两个闭环

发挥部分要求绿光自动追踪红光,本质上是两个云台、两个闭环、一个共享视觉源的问题。很多队伍在这里卡住不是因为算法难,而是两个闭环的节拍没理清楚,互相踩。

4.1 一张图里同时抓红绿:面积、亮度、位置做判据

一张图里同时找红点和绿点,最直接的问题是阈值相互串。红激光过曝后在 LAB 空间里的 A 值会冲得很高,但边缘像素在 B 通道上也可能落进绿光的区间;反过来绿光打在白纸上,中心过曝区域接近白色,L 值高、A 和 B 都接近零,反而两个阈值都不命中。

实际处理方式是把两个色块的检测结果做一次交叉校验:如果绿光色块的中心落在红点色块内部,且面积明显小于红点,那多半是过曝串色,丢掉;如果两个色块的面积比超过 5 倍,也丢掉小的那个。

def pick_pair(img): red = find_spot(img, RED_TH, 12) grn = find_spot(img, GREEN_TH, 8) if red and grn: # 绿点面积远小于红点且位置重合 -> 判为串色,只保留红点 if grn[2] < red[2] * 0.2 and abs(grn[0]-red[0]) < 15 and abs(grn[1]-red[1]) < 15: grn = None return red, grn

面积判据之外,还有一个更稳的判据是"颜色饱和度"。红激光和绿激光的纯度都很高,反光杂点的饱和度低得多,在 LAB 里体现为 A/B 通道的绝对值偏小。把阈值的下限往上提一点,杂点会被天然滤掉,代价是远处的小光斑可能丢,需要配合曝光值一起调。

4.2 追踪滞后从哪来,前馈和死区怎么加

追踪系统的滞后是三段叠加起来的:摄像头曝光加读出大约 30~60ms,视觉算法一帧 10~30ms,串口传输和主控周期再吃 10ms,最后舵机本身响应 100ms 上下。总滞后两百毫秒量级,红光如果以 0.5 m/s 在靶面上移动,两百毫秒就是 10cm 的偏差,纯 PID 追不上,只能看到绿点一路落后。

最有效的补偿是前馈:把目标点(红点)的速度估计出来,直接叠加到绿光云台的角度指令上,让 PID 只负责修正残差。

/* 由视觉帧差分估计红点速度,前馈叠加到角度指令 */ static float ff_gain = 0.045f; /* 单位 deg/(mm/s),需实测标定 */ void tracking_update(float rx, float ry, float dt) { static float last_x = 0, last_y = 0, init = 0; if (!init) { last_x = rx; last_y = ry; init = 1; return; } float vx = (rx - last_x) / dt; float vy = (ry - last_y) / dt; last_x = rx; last_y = ry; /* 速度做一阶低通,避免差分噪声直接进前馈 */ static float vx_f = 0, vy_f = 0; vx_f = 0.7f * vx_f + 0.3f * vx; vy_f = 0.7f * vy_f + 0.3f * vy; float ang_h = pid_calc(&pid_h, rx - g_green_x, dt) + ff_gain * vx_f; float ang_v = pid_calc(&pid_v, ry - g_green_y, dt) + ff_gain * vy_f; servo_set(ang_h, ang_v); }

ff_gain这个系数没有理论值可抄,标定方法是让红光以恒定速度横穿靶面,手动调ff_gain直到绿点跟红点的水平距离最小。云台高度和靶面距离一变,这个值就得重标,所以现场调试时把它做成串口可调的全局变量,比反复烧程序快得多。

死区也值得加。追踪误差在 2~3mm 以内时,视觉噪声本身就超过真实误差了,继续让 PID 追只会让绿点在红点周围画小圈。加一个"误差绝对值小于阈值时输出保持上一帧"的死区判断,画面立刻安静下来。

4.3 两个云台的节拍:分时调度而不是互相打断

两个云台如果都放在主循环里同步跑,会出现一个隐蔽的问题:红点云台的控制周期被绿点云台的视觉处理拖长,PID 的 dt 忽大忽小,积分项算出来的值完全不可预期。

我的做法是把两路控制拆成不同频率的任务。红点云台的轨迹控制是开环或准闭环,跑 50Hz 就够了;绿点追踪需要快,跑到 100Hz;视觉板独立跑,按自己的帧率往串口发,主控收到就更新目标缓存,不阻塞主循环。

/* 分频调度:基准 1ms 中断 */ void systick_task(void) { static uint16_t cnt = 0; cnt++; if (cnt % 20 == 0) red_gimbal_task(); /* 50Hz:红点轨迹控制 */ if (cnt % 10 == 0) green_gimbal_task(); /* 100Hz:绿点追踪 */ if (cnt % 5 == 0) uart_poll_task(); /* 200Hz:串口收帧 */ }

任务里只做计算和写 PWM 寄存器,不做任何阻塞操作。串口用中断加环形缓冲收,主循环里取数据,这样波特的传输时间不会侵占控制周期。这套调度结构看着土,但现场的稳定性比"一个大循环里什么都干"要好一个量级。

5. 避坑清单:五条从调试现场捡回来的经验

这一章的五条,基本覆盖了从能点亮到能稳定跑之间会遇到的全部问题。每条按"现象—原因—解决"写,遇到问题时按顺序排查就行。

5.1 光斑质心乱跳,云台却纹丝不动

现象是串口打印出来的红点坐标每秒跳十几毫米,但盯着云台看,激光点稳稳地打在靶纸中心没动。

原因是自动曝光没锁。摄像头在光斑出现和消失之间不断调整曝光,红点的面积和形状每帧都在变,质心自然跟着漂。另一个可能是merge没开,过曝的红点被切成两块,质心在两块之间来回横跳。

解决办法是三道锁一起上:set_auto_gain(False)、set_auto_exposure(False, ...)、set_auto_whitebal(False),把曝光值手动调到白纸呈中灰。然后把merge=True和margin=8~12打开。做完这两步如果还跳,就该检查是不是有第二个红色物体进了视野,比如红色扎带、红色指示灯,把它们移出视野或者用pixels_threshold卡掉。

5.2 光斑走到靶纸边缘就"贴"住不动

现象是沿边框走的时候,光斑在四条边中段还好,一接近拐角就慢下来,甚至在边线附近来回摩擦,走不过去。

原因通常有两个。一是舵机分辨率不够,普通模拟舵机的理论分辨率约 0.3°,但实际有效分辨率受齿隙影响往往到 1° 以上,对应靶面上就是几厘米的死区,指令在小范围内变化时舵机根本不动。二是软限位设得太保守,angle_to_ccr里限到 ±85°,而实际机械行程可能只有 ±80°,走到边界时指令被限幅,光斑就卡在那里。

解决方式是先把软限位放宽到实测机械行程的 95%,再在靶面坐标上做一次"映射补偿":把期望的靶面范围映射到舵机能稳定覆盖的角度范围内,让轨迹计算时就不去碰限位。齿隙的问题换数字舵机最直接,如果换不了,就在控制上加一点抖动偏置,让舵机始终有微小的运动,避免停在齿隙里。

5.3 激光一开,整幅图像泛白、阈值全废

现象是激光笔点亮之后,画面上出现一大片白色亮斑,红点反而找不到了,或者找到的质心整个偏到亮斑中心。

这是典型的过曝。激光笔功率大、摄像头增益高,打在白纸上形成的亮斑中心区域三个通道全部饱和到 255,色相信息完全丢失,此时检测到的不是"红点"而是"白点"。

解决办法有三条,按优先级:先降曝光值,把exposure_us调到白纸呈中灰;再考虑在镜头前加一片中性密度滤光片或者一小片磨砂片,把光斑打散一点,让它不那么集中;最后才是软件上的补救,在阈值里把 L 上限压到 90 以下,让过曝区落不进阈值。软件补救的问题在于过曝区的形状随功率变化,阈值得反复调,不如从光学上解决。

5.4 云台到位后小幅摆动停不下来

现象是光斑到达目标点后,在目标位置周围做 1~3mm 的等幅或者衰减很慢的摆动,几秒钟都停不下来。

原因一般是三个叠加。微分项给得太大,视觉噪声被放大成控制量;积分项没有限幅,到位后积分还在慢慢累积,形成极限环;机械回差导致舵机在两度范围内反复跨越齿隙。

排查顺序是先去掉微分项看是否改善,如果摆动周期变长但幅度还在,就是积分在起作用,把i_max压到out_max的四分之一以内;如果两种都试过还在摆,就是机械问题,检查云台支架有没有松动、激光笔有没有夹紧、靶纸是不是被风扇吹得在动。最后这一条听着荒唐,但现场确实遇到过。

5.5 串口偶发丢帧,云台突然抽一下

现象是整体运行正常,但每隔十几秒云台会突然朝某个方向猛甩一下,然后又恢复。

原因是帧同步丢了。ASCII 格式或者单字节帧头的协议,一旦某个字节丢失或者被干扰,接收端会把后续数据错位解析,把坐标里的高位当成低位,解出来一个巨大的数值,PID 立刻给一个巨大的输出。

解决办法是把协议改成双字节帧头加定长加校验,就是第 3.4 节给的那套。另外在接收端加两重保护:一是超时丢弃,超过 100ms 没凑齐一帧就清空缓冲;二是数值合法性检查,解出来的坐标如果在靶面范围之外就直接丢弃,不要送进 PID。第二重保护尤其重要,它不解决协议问题,但能保证即使协议出错,云台也不会做出危险动作。

6. 现场三小时复现:一套固定的标定顺序与自检表

所有东西都调通之后,我给自己定了一套固定的上电顺序,每次搬到新场地都按这个顺序走一遍,能省掉大量临场抓瞎。顺序是:机械固定 → 摄像头锁定参数 → 四点标定 → 舵机零位 → 单轴闭环 → 双轴闭环 → 轨迹 → 追踪。这个顺序的核心逻辑是"先解决静态误差,再解决动态误差",任何一步跳过去,后面都会以更难排查的形式还回来。

现场自检表我一般贴在箱子内侧,逐项打勾:

检查项合格判据不达标先查
摄像头支架刚性手轻碰靶纸,图像不晃支架脚是否锁死
曝光锁定关激光时白纸呈中灰exposure_us是否合适
四点标定残差四点回代残差均 < 2mm采点是否准确、镜头畸变
舵机零位指令 0° 时光斑落靶心 ±5mmtrim补偿值
单轴阶跃响应阶跃到位时间 0.3~0.6s,无超调或仅一次kp与i_max
边框轨迹四角偏离边线 < 10mm软限位与机械行程匹配
圆轨迹圆度误差 < 8mm,无明显切角轨迹角速度是否过快
追踪稳态误差红光静止时绿点偏差 < 5mm死区阈值、PID 参数
追踪动态误差红光 0.3m/s 匀速时偏差 < 15mmff_gain前馈系数
串口健壮性拔插一次后 100ms 内自动恢复超时丢弃与合法性检查

表格之外还有一个技巧值得单独说:把trim、ff_gain、kp/ki/kd、i_max这几个参数全部挂到串口命令上,支持在线读写并掉电保存。这样现场调整不用重新编译烧录,一次调参从五分钟缩短到十秒钟。我在第二次现场调试时才加上这个功能,之前反复烧程序浪费的时间足够把整套轨迹跑通三遍。

最后一个习惯是记录。每次调整参数,无论成功失败,都在纸上记一行"时间 + 改了什么 + 现象变化"。追踪调不好的时候,凭记忆回想"上次那个参数是不是更好"百分之百会记错,翻记录两秒钟就能定位。

说个自己的教训:第一次跑完整流程时,我把所有参数都调到了"看起来最好"的状态,结果第二天换了个场地,日光灯位置一变,红点阈值全线失效,两个小时全花在重调阈值上。从那以后我每次换场地都强制走一遍上面这张自检表,从曝光开始重新锁一遍,哪怕看起来一切正常也不跳过。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询