资讯详情

STM32+OpenMV电赛小车:串口协议、串级PID与状态机联调

📅 2026/10/26 3:03:20 | 华诺云谱 👁 阅读
STM32+OpenMV电赛小车:串口协议、串级PID与状态机联调
简介这份面向2024电赛H题的巡线小车完整工程以STM32为主控、OpenMV摄像头巡线重点展示黑线与中心线偏差的闭环控制方案适合备赛学生和嵌入式控制初学者对照学习。压缩包共1053个文件、约27.45MB主体为570个C源文件、256个头文件及51个汇编文件并含工程配置、编译中间件、库文件与OpenMV Python脚本构成从底层驱动到控制逻辑的完整项目结构。已有4265人学习。工程围绕ABCD四点间的平滑衔接展开给出统一速度下的稳定行驶思路并提示可改用变速策略提升稳定性与缩短耗时同时记录MPU6050零漂、初始摆放角度对转向参考的影响以及四轮小车左右轮同步与参数微调的适配方法。读者可据此理解灰度传感器与摄像头巡线在偏差获取上的共性复盘控制难点、排错方向与优化空间。1. 2024电赛小车上STM32OpenMV到底谁指挥谁先定控制边界2024电赛小车题里STM32OpenMV这套组合最常见的翻车点不是识别不到目标而是图像坐标传到STM32后底盘追不上弯道已经过了轮子还在按上一帧的偏差打角。这个完整工程要解决的就是控制边界——OpenMV只做视野内的偏差提取STM32负责串口协议、速度闭环和状态切换两边用固定帧长和明确超时断开。适合正在搭竞赛小车、想把“能识别”推到“能稳定跑”的开发者也适合第一次做双控制器联调的团队。下面按协议、识别、闭环、排查、状态机五块拆开参数能给起始值的都给坑也按现象写清。2. STM32与OpenMV串口协议帧头、长度、校验和重发窗口怎么定联调第一道坎通常不是算法而是串口。OpenMV端用print(x%d,y%d\n)发字符串STM32端用sscanf解析实验室里能跑一到车上丢一个字节就错位小车会突然满舵。血泪经验是图像偏差这类周期性数据用定长二进制帧帧头、长度、序号、CRC8一个都别省参数配置这类低频数据再叠一层ACK和重发。先把协议定死后面PID和阈值才有意义。2.1 为什么控制帧不能直接发浮点帧头、长度和CRC8的取舍OpenMV和STM32的端序、对齐方式、浮点格式都可能出问题直接struct.pack(f, x)发4字节浮点在STM32端用指针强转代码看着短移植一次就翻车。小车控制需要的精度是像素级偏差QVGA下偏差范围大约-160到160int16_t完全够用一帧10字节115200波特率下传输时间不到1ms20ms控制周期绰绰有余。我一般用这样的帧结构帧头0xAA 0x551字节长度表示payload字节数1字节序号payload放int16 x_err int16 y_err uint8 flags共5字节最后1字节CRC8。CRC8只算长度、序号和payload多项式用0x07初值0。别小看这个CRC串口线上电机一启动就可能有误码没有校验时STM32会把乱码当偏差车会像被踢了一脚。字段长度说明帧头2固定0xAA 0x55用于快速对齐LEN1payload长度当前固定5SEQ10到255循环用来丢旧帧X_ERR2int16小端目标中心相对ROI中心的横向偏差Y_ERR2int16小端纵向偏差云台或测距用FLAGS1bit0目标有效bit1丢线bit2保留CRC81覆盖LENSEQpayload长度字段不要省。有人用固定帧长直接数字节一旦以后加一个标志位收发两端版本不一致就全乱。带上LEN解析状态机可以按长度收payload升级时只改长度和CRC范围帧头仍然能对齐。CRC8比CRC16省时间10字节帧在STM32的F4系列上算一次不到几微秒够用如果现场干扰特别大再换CRC16不要一上来就把协议做重。2.2 收包状态机从环形缓冲到CRC校验的C代码STM32端不要在串口中断里直接解析业务。常见做法是中断只把字节塞进环形缓冲主循环或20ms任务里取出来跑状态机。下面这段是可直接改的骨架vision_parse_byte()每收到一个字节调用一次解析成功返回1。#include stdint.h #include string.h #define RX_BUF_SIZE 128 typedef struct { uint8_t buf[RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuf; static RingBuf g_rx; static uint8_t g_payload[8]; typedef enum { S_H1 0, S_H2, S_LEN, S_SEQ, S_PAYLOAD, S_CRC } ParseState; typedef struct { uint8_t seq; int16_t x_err; int16_t y_err; uint8_t flags; uint8_t fresh; } VisionFrame; static VisionFrame g_vision; static uint8_t crc8_update(uint8_t crc, uint8_t data) { crc ^ data; for (uint8_t i 0; i 8; i) { if (crc 0x80) crc (uint8_t)((crc 1) ^ 0x07); else crc (uint8_t)(crc 1); } return crc; } uint8_t vision_parse_byte(uint8_t b) { static ParseState st S_H1; static uint8_t len 0, seq 0, idx 0, crc_calc 0, crc_recv 0; switch (st) { case S_H1: if (b 0xAA) st S_H2; break; case S_H2: st (b 0x55) ? S_LEN : S_H1; break; case S_LEN: len b; if (len 0 || len sizeof(g_payload)) { st S_H1; break; } crc_calc crc8_update(0, len); st S_SEQ; break; case S_SEQ: seq b; crc_calc crc8_update(crc_calc, seq); idx 0; st S_PAYLOAD; break; case S_PAYLOAD: g_payload[idx] b; crc_calc crc8_update(crc_calc, b); if (idx len) st S_CRC; break; case S_CRC: crc_recv b; st S_H1; if (crc_recv ! crc_calc) break; if (len 5) { g_vision.seq seq; g_vision.x_err (int16_t)(g_payload[0] | (g_payload[1] 8)); g_vision.y_err (int16_t)(g_payload[2] | (g_payload[3] 8)); g_vision.flags g_payload[4]; g_vision.fresh 1; return 1; } break; default: st S_H1; break; } return 0; }这段代码的逻辑是帧头不对就回到S_H1长度超过缓冲区就丢弃CRC不对就整帧丢掉只有校验通过才更新g_vision。参数上注意三个点RX_BUF_SIZE至少留两倍最大帧长的余量128字节对10字节帧很宽裕len sizeof(g_payload)是保护别让乱码把数组写穿CRC计算范围必须和OpenMV端完全一致少算一个SEQ就会永远校验失败。串口中断里只做环形缓冲写入类似g_rx.buf[g_rx.head] b;注意head回绕和主循环读tail的竞争。主循环每次取一个字节喂给状态机比在中断里跑find_blobs后果轻得多。如果字节间隔超过2ms还没有新字节可以在定时器里把状态机复位到S_H1防止半帧卡死。这个超时复位是后悔药平时用不上现场干扰时能救一次。2.3 序号与超时哪些帧要ACK哪些帧必须丢弃旧帧图像帧不要做ACK重发。OpenMV以50Hz发偏差STM32只关心最新一帧旧帧重发只会让车更迟钝。做法是STM32记录last_seq收到新帧后比较序号如果序号比当前旧超过一定范围就丢弃更简单的做法是只保留一个g_vision每次覆盖控制任务读之前看fresh标志读完清零。这样串口队列不会堆积弯道不会追着三帧前的偏差打角。参数配置帧要ACK。比如上位机或OpenMV要给STM32发“设置速度环Kp8.0”这种帧丢了小车不会立刻失控但调参效率会崩溃。我一般定义下行帧0xBB 0x66 LEN SEQ CMD DATA CRC8STM32收到后回一个ACK帧ACK里带回原SEQ。发送端等ACK最多20ms超时重发3次3次失败就置错误标志不无限重试。控制帧和参数帧走同一个串口时用CMD区分解析状态机里多一个分支别开两个串口浪费引脚。提示ACK帧也要带CRC别因为“只是应答”就省校验。现场见过ACK被干扰成别的CMD参数被反复写入调参调半天以为PID有问题。3. OpenMV端识别输出ROI、阈值、曝光和坐标映射的四个可调项OpenMV端最容易犯的错是“全图找色块”。QVGA全图find_blobs一次可能十几毫秒加上自动曝光抖动帧率看着有20fps实际偏差更新像心电图。竞赛小车不需要看懂整幅图它只需要知道目标在ROI里的横向偏差。把ROI锁死、曝光锁死、只发偏差OpenMV端就从一个“图像处理Demo”变成稳定的传感器。3.1 ROI先锁视野find_blobs的阈值和面积参数怎么给先确定目标在画面里的大致区域。小车摄像头通常朝前下方目标线或色块出现在画面下半部ROI可以取(60, 40, 200, 140)也就是从x60、y40开始宽200高140。ROI越小处理越快但太小会一出弯就丢目标。我一般让ROI覆盖目标可能出现的左右边界留10%余量别把ROI卡到刚好。阈值用LAB空间比RGB稳。比如追一条偏黄色的线阈值可能是(20, 100, -30, 50, -20, 60)六个数字分别是L Min、L Max、A Min、A Max、B Min、B Max。这不是万能值现场要用OpenMV IDE的阈值编辑器重新取。面积参数pixels_threshold200、area_threshold200意思是色块至少200个像素才认防止反光点被当成目标。mergeTrue把相邻色块合并适合线有断点的情况。import sensor, image, time, ustruct from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) sensor.set_auto_exposure(False, exposure_us6000) sensor.skip_frames(time500) uart UART(3, 115200) ROI (60, 40, 200, 140) THRESHOLD (20, 100, -30, 50, -20, 60) SEQ 0 def crc8(data): crc 0 for b in data: crc ^ b for _ in range(8): if crc 0x80: crc ((crc 1) ^ 0x07) 0xFF else: crc (crc 1) 0xFF return crc while True: img sensor.snapshot() blobs img.find_blobs([THRESHOLD], roiROI, pixels_threshold200, area_threshold200, mergeTrue) flags 0 x_err 0 y_err 0 if blobs: b max(blobs, keylambda x: x.pixels()) cx b.cx() cy b.cy() roi_cx ROI[0] ROI[2] // 2 roi_cy ROI[1] ROI[3] // 2 x_err int(cx - roi_cx) y_err int(cy - roi_cy) flags 1 else: flags 2 # 丢线标志 payload ustruct.pack(hhB, x_err, y_err, flags) body ustruct.pack(BB, len(payload), SEQ) payload frame b\xAA\x55 body bytes([crc8(body)]) uart.write(frame) SEQ (SEQ 1) 0xFF time.sleep_ms(20)这段OpenMV代码的关键不是find_blobs本身而是它只发x_err、y_err和flags不发票据也不发坐标数组。x_err是色块中心减ROI中心正数表示目标在右负数在左。flags2告诉STM32这一帧没找到目标STM32可以进搜索状态而不是继续用旧偏差。time.sleep_ms(20)对应50Hz和STM32控制周期对齐如果OpenMV处理一帧超过20ms实际帧率会掉STM32端必须靠序号判断新旧不能假设50Hz一定到。参数上pixels_threshold和area_threshold不要设太小太小会把反光、噪点认成目标也不要设太大线在远处只占几十个像素时会丢。常见做法是先设200现场看IDE里色块像素数取正常值的1/3作为下限。mergeTrue在线有断点时有帮助但两个相邻色块也会被合并如果目标旁边有同色干扰改回False并加roi限制。3.2 固定曝光与白平衡让阈值不在场地漂移的三个开关自动增益、自动白平衡、自动曝光是OpenMV端的三根“玄学旋钮”。实验室日光灯下调好的阈值到赛场窗户边就偏蓝自动白平衡一动作A和B通道全变阈值直接失效。我一般把三个都关掉set_auto_gain(False)、set_auto_whitebal(False)、set_auto_exposure(False, exposure_us6000)。曝光时间6000微秒是起始值光线暗就加到8000到10000光线亮就降到3000到4000但一旦定了比赛过程中不要再让它自动变。关掉自动曝光后图像亮度靠灯光和曝光时间保证。好处是阈值稳定坏处是换场地要重新标一次。标定方法不复杂把目标放在ROI中心看IDE里色块的LAB值取目标正常亮度和阴影亮度的交集作为阈值范围。比如L通道在30到90之间都算目标就不要卡到40到60否则一有阴影就丢。A、B通道同理范围太窄会挑光线太宽会认到背景。如果只追一条黑线可以直接用灰度图sensor.set_pixformat(sensor.GRAYSCALE)阈值只调一个灰度范围处理更快受色温影响更小。但灰度图识别彩色目标会丢信息所以追线用灰度、识别色块用RGB565这是常见分工。不要在同一套代码里频繁切像素格式切换本身有开销还会让曝光重新收敛。3.3 像素偏差到车体偏差标定表和打包代码x_err是像素偏差不是转向角。STM32拿到的x_err40到底该打多少度要靠标定。简单做法是把车放在目标左侧10cm、20cm、30cm记录x_err再放到右侧同样距离记录得到一张像素-厘米表。如果近似线性就在STM32里乘一个系数K_px比如K_px0.8表示1像素偏差对应0.8个转向单位如果不线性就分段查表。车体横向偏差记录x_err转向输出起始值备注左30cm-120-60大偏差限幅左10cm-40-20线性段中心00死区±3像素右10cm4020线性段右30cm12060大偏差限幅这张表不是让你抄数值是让你在场地里填。填完后把K_px写进STM32的转向环x_err乘系数再进PID。注意像素偏差有死区目标在中心±3像素内不用动否则车会左右抖。打包代码里ustruct.pack(hhB, x_err, y_err, flags)的表示小端STM32端按小端拼回两边必须一致。SEQ每帧加1STM32用它判断是否新帧别用时间戳时间戳在两边时钟不同步时更麻烦。4. STM32底盘闭环编码器测速、PWM限幅和串级PID整定表OpenMV给的是方向偏差STM32要把这个偏差变成左右轮的速度差。很多小车“能识别但跑不稳”问题出在底盘闭环编码器没读对、PWM死区没补、积分饱和把车憋死。底盘控制不需要多复杂关键是测速周期稳定、限幅明确、先内环后外环。下面给的参数是起始量级不是万能值换电机、换轮径、换电池电压都要重调。4.1 编码器测速定时器计数、M法测速和5ms控制周期编码器用定时器的编码器模式常见做法是__HAL_TIM_GET_COUNTER(htim3)读计数5ms读一次算差分。假设编码器线数ENCODER_LINES13四倍频后每转52个计数减速比GEAR_RATIO30轮子转一圈就是1560个计数。5ms内差delta个计数轮速delta / 1560 / 0.005转/秒。代码里把轮径乘进去就是线速度。#define CTRL_DT 0.005f #define ENCODER_LINES 13.0f #define GEAR_RATIO 30.0f #define WHEEL_DIAMETER 0.065f static int16_t last_cnt 0; float read_wheel_speed_mps(void) { int16_t now (int16_t)__HAL_TIM_GET_COUNTER(htim3); int16_t delta (int16_t)(now - last_cnt); last_cnt now; float counts_per_rev ENCODER_LINES * 4.0f * GEAR_RATIO; float rev_per_sec (float)delta / counts_per_rev / CTRL_DT; return rev_per_sec * 3.1415926f * WHEEL_DIAMETER; }这段代码的逻辑是M法测速固定时间读计数差。参数上CTRL_DT必须和定时器中断周期一致写5ms就真给5ms别在中断里做浮点除法和串口打印否则周期抖动速度环会抖。ENCODER_LINES和GEAR_RATIO按实际电机填填错一倍速度就错一倍。delta用int16_t强转计数溢出时会回绕短周期内没问题如果5ms内计数超过32767说明速度太高或线数填错先查参数。M法在低速时分辨率差。5ms内只有几个脉冲速度算出来一跳一跳。常见做法是低速改用T法测脉冲间隔或者把测速周期放到10ms牺牲一点带宽换稳定。小车竞赛速度不会太低5ms到10ms都有人用我一般先10ms跑通再压到5ms看抖动。编码器线不要和电机PWM线捆在一起否则计数会飞速度环直接疯。4.2 PWM限幅与死区积分抗饱和和电机死区补偿PWM限幅不是可有可无。电池满电时PID输出可能到100%电机堵转电流把电压拉低STM32复位OpenMV也重启。我一般把PWM上限设到定时器ARR的80%到90%留余量给电池跌落。比如ARR9000PWM_MAX7200。电机还有死区PWM小于120时轮子不转PID会以为输出不够继续加积分最后积分饱和车一松就窜出去。#define PWM_MAX 7200 #define PWM_DEAD 120 int16_t apply_motor_limit(int16_t pwm) { if (pwm 0 pwm PWM_DEAD) pwm PWM_DEAD; if (pwm 0 pwm -PWM_DEAD) pwm -PWM_DEAD; if (pwm PWM_MAX) pwm PWM_MAX; if (pwm -PWM_MAX) pwm -PWM_MAX; return pwm; } void set_motor_pwm(int16_t left, int16_t right) { left apply_motor_limit(left); right apply_motor_limit(right); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, (uint16_t)(left 0 ? left : 0)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, (uint16_t)(left 0 ? -left : 0)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, (uint16_t)(right 0 ? right : 0)); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_4, (uint16_t)(right 0 ? -right : 0)); }死区补偿放在限幅之后、写PWM之前正负都要补。积分抗饱和放在PID里积分项累加后先限幅到±out_max再参与输出输出再限一次。这样电机堵转时积分不会无限涨松开车不会猛冲。PWM频率常见10kHz到20kHz太低电机啸叫太高驱动发热按驱动板手册选别频繁改。4.3 串级PID整定表先内环后外环的起始参数串级结构是外环转向、内环速度。外环输入x_err输出左右轮速度差内环输入目标轮速输出PWM。整定顺序必须先内后外先让单轮速度跟得上阶跃再关外环调转向。内环P太大嗡嗡振I太大低速爬行D在编码器噪声大时先给0。外环P决定打角快慢D抑制过弯超调I一般很小甚至不给。typedef struct { float kp, ki, kd; float integ; float prev_err; float out_max; } PID; float pid_calc(PID *p, float err, float dt) { p-integ err * dt; if (p-integ p-out_max) p-integ p-out_max; if (p-integ -p-out_max) p-integ -p-out_max; float d (err - p-prev_err) / dt; p-prev_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; }环Kp起始Ki起始Kd起始周期输出限幅观察量速度内环8.00.50.05ms±7200轮速阶跃是否等幅振转向外环2.50.020.820ms±3000过弯是否超调丢线搜索1.20050ms±1500找回目标是否过冲表里的数只是量级不是抄了就完事。内环先给Kp8看轮速能不能在100ms内跟上目标跟不上加Kp等幅振就减Ki从0.5起消除静差但低速爬行就减到0.1。外环Kp2.5车头摆动大就减过弯懒就加Kd0.8抑制超调但图像噪声大时D会把噪声放大先给0.3试。所有参数改一次只改一个改完至少跑三圈别一边改PID一边改阈值最后不知道谁起的作用。5. 联调排查图像延迟、串口粘包、电机干扰和电源跌落的翻车记录小车在台架上跑得好落地就出问题多半不是算法错而是联调边界没查。下面五条是竞赛小车最常见的翻车记录每条按现象、原因、解决写。排查顺序我一般从电源和串口开始再查图像最后动PID反过来先调PID往往把机械和电源问题调成“参数玄学”。5.1 图像延迟导致弯道画龙曝光、ROI和最新帧策略现象直道正常进弯后车头左右摆像画龙弯越急越明显。看串口数据x_err变化比车体动作晚一截弯道过了才打到最大偏差。原因OpenMV端曝光时间太长自动曝光在暗处拉到20ms以上加上find_blobs十几毫秒一帧就30ms到40msSTM32端如果还按队列取旧帧控制周期20ms实际用的偏差可能滞后两帧。滞后在直道看不出来弯道相位一差就震荡。解决固定曝光到6000微秒左右ROI缩到目标可能出现区域find_blobs只算必要色块STM32端只保留最新帧g_vision覆盖写控制任务读fresh标志读完清零不排队。如果还慢把OpenMV分辨率降到QQVGA或把帧率降到30Hz让每帧处理时间稳定别让帧率忽高忽低。转向环的D项在延迟大时先减小D对滞后最敏感。5.2 串口粘包与CRC错位环形缓冲的读写下标现象小车偶尔突然满舵串口助手看数据帧有的粘在一起有的少一个字节复位后又正常跑几分钟再犯。原因串口中断里直接解析主循环又在读同一个缓冲区head和tail竞争或者中断里做了耗时操作新字节覆盖旧字节。还有一种情况是CRC计算范围两端不一致OpenMV算了帧头STM32没算平时能过干扰一来就错。解决中断只写环形缓冲主循环取字节喂状态机head和tail用volatile读写时关中断或保证单写单读缓冲区留足两倍最大帧长。CRC范围写死LENSEQpayload两端对照同一张表。加字节间隔超时2ms没有新字节就复位解析状态防止半帧卡死。改完用串口助手发乱码测试确认状态机能自己回到帧头。5.3 电机PWM干扰OpenMV走线、共地和复位现象现象电机一转OpenMV图像花屏、颜色跳变严重时OpenMV重启电机停就正常。示波器看PWM线上有振铃图像数据线附近有毛刺。原因电机PWM线和大电流线跟摄像头排线平行走耦合到数据线电机火花通过地线串进OpenMV供电共地阻抗大地电位跳动。OpenMV的USB线如果还插着电脑地环路更复杂。解决PWM线和摄像头排线分开走能垂直就垂直不能就拉开距离电机两端加续流和滤波PWM线双绞OpenMV单独一路稳压地线单点接到电池负极别和电机地串在一起。复位现象先查供电再查排线别一上来改代码。竞赛小车上线束固定比算法更影响稳定性。5.4 大电流启动STM32复位电源路径和电容位置现象小车起步或急转时STM32复位OLED黑屏串口断电池电压从8V掉到6V多电机堵转时更明显。原因电池内阻加上细线径大电流瞬间压降稳压芯片输入电容太小或离芯片太远响应不过来电机和MCU共用一路电机启动把MCU电压拉低。复位不一定是软件看门狗先看电源纹波。解决电机电源和MCU电源分开走MCU用独立稳压稳压输入输出电容靠近芯片引脚大电容放在电池出口线径按峰值电流选别用杜邦线给电机供电加低压检测电压低于阈值时降PWM限幅别硬撑。电容不是越大越好位置比容量重要离芯片远的1000uF不如近的100uF。5.5 换场地阈值翻车固定曝光后的二次标定现象实验室调好的阈值到赛场一半时间丢目标同一套代码上午下午识别率不一样。看IDE里色块LAB值L通道整体偏移。原因场地灯光色温和亮度不同固定曝光后图像亮度变了阈值范围没覆盖目标表面反光角度一变LAB值跳变ROI里背景颜色接近目标面积阈值把背景认进来。解决到场后先固定曝光再重新取阈值别用实验室值硬跑阈值范围取目标亮部和暗部的交集别卡太窄加ROI限制和面积阈值背景干扰大时用mergeFalse如果只追线切灰度图减少色温影响。标定花十分钟比现场调PID半小时有用。6. 用状态机和在线调参把小车从能跑推到能比赛一个可复用的调试习惯状态机是小车从“连续PID”变成“有判断”的关键。我一般分SEARCH、TRACK、ALIGN、STOP四个状态TRACK用图像偏差做转向连续丢帧超过5帧进SEARCHSEARCH用低速原地扫或按最后偏差方向找找回目标进ALIGN对齐稳定200ms再回TRACK。切换条件写成表别散在代码里。6.1 状态机骨架SEARCH、TRACK、ALIGN、STOP的切换条件typedef enum { S_SEARCH, S_TRACK, S_ALIGN, S_STOP } CarState; static CarState state S_SEARCH; static uint8_t lost_cnt 0; static uint16_t align_ms 0; void car_state_task(void) { switch (state) { case S_SEARCH: set_motor_pwm(-800, 800); // 低速原地找 if (g_vision.fresh (g_vision.flags 0x01)) { lost_cnt 0; state S_ALIGN; } break; case S_TRACK: if (!(g_vision.flags 0x01)) lost_cnt; else lost_cnt 0; if (lost_cnt 5) state S_SEARCH; else { int16_t turn (int16_t)(g_vision.x_err * 0.8f); set_motor_pwm(1500 turn, 1500 - turn); } break; case S_ALIGN: set_motor_pwm(0, 0); if (align_ms 200) state S_TRACK; break; case S_STOP: set_motor_pwm(0, 0); break; } g_vision.fresh 0; }这段骨架的重点是lost_cnt和align_ms两个计数器。lost_cnt 5对应100ms丢帧避免单帧误判就停车align_ms 200让对齐稳定后再走。参数按车重和舵机响应改轻车可以减到3帧和100ms重车加到8帧和300ms。状态机里不要做浮点大运算20ms任务里只做判断和PWM输出。6.2 在线调参帧用一条命令改PID和阈值在线调参能省掉反复烧录的时间。我一般留一个下行命令0xBB 0x66 LEN SEQ CMD DATA CRC8CMD0x01改速度环KpCMD0x02改转向KpCMD0x03改OpenMV阈值。STM32收到后写RAM参数并回ACK掉电不保存比赛前再固化。注意参数范围限幅别让一条乱码把Kp写成1000。CMD数据范围作用0x01float Kp0到20速度内环Kp0x02float Kp0到10转向外环Kp0x03uint8 阈值编号0到7切换预存阈值组我后来养成的习惯是先看串口波形和电源纹波再改PID先固定曝光和ROI再谈识别率每次只改一个参数跑三圈再记录。这样看着慢实际比反复推翻重来快。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑