📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列(共 12 篇)第 10 篇。
前面九篇,我们把”小车自己能跑稳、跑准”这件事讲透了:电机、电源、各种传感器,PID 从入门到串级再到调参实战。但你有没有发现,到现在为止小车的”眼睛”其实很弱——灰度传感器只能看脚下那条线,编码器只知道轮子转了几圈,IMU 只管自己歪没歪。要让小车看懂”前面有个二维码””那边是个 2 号路牌””这条线往左拐了 30 度”,就得请出真正的视觉模块。
这一篇聊两件事:一是 K230 这类视觉模块到底能干什么、怎么用;二是它怎么跟主控”对上话”——也就是通信协议。这两件事其实是一件事:视觉负责”看”,主控负责”动”,中间靠一根串口线把”看到的结论”传过去。把这条链路打通,你的小车才真正有了眼睛。
先把整条链路画出来,心里有个全局:

一、先想清楚:视觉模块在整车里是什么角色
新手最容易犯的一个错,是把视觉模块当”主脑”,想让它又看图、又算决策、又控电机。结果发现 K230 上用 Python 跑控制环又慢又难调,主控那边还拿不到干净数据,两头别扭。
正确的分工是这样的:
💡 一句话记住分工
视觉模块负责”看”,主控负责”动”,中间只传”结论”。
打个比方:视觉模块就像副驾上的导航员。副驾不会把整块挡风玻璃的画面塞给开车的你,他只会喊一句”左偏 10 度””前方是 2 号路牌”。你(主控)拿到这一句话就够打方向了。
所以视觉端只回传几个数——比如 line_error(线偏了多少)、line_angle(线斜了多少度)、flag(看到没看到)、id(二维码/路牌的编号)——绝不把整张图像通过串口发过来。
🔥 头号大坑:千万别传图
K230 千万别把整幅图像或大数组通过 UART 发给单片机。几百 KB 的图片走 115200 的串口要好几秒,带宽和主控算力都扛不住,延迟大到小车早就冲出去了。主控其实只需要 line_error/line_angle 这几个数就能闭环。真要传整幅图/点云那是极罕见的赛题,才考虑 SPI 或网口。
这种”把看的活和算的活分开”的设计,好处是两个人可以各写各的:视觉同学专心调他的巡线和识别,电控同学专心调 PID,只要事先把”传哪几个数、什么格式”约定死,最后联调时插上线就能跑。
二、K230/CanMV 能干什么
K230 是嘉楠(Kendryte)出的一颗带 6 TOPS NPU(神经网络算力)的视觉芯片,常见开发板有庐山派、01Studio CanMV 等。它跑的固件叫 CanMV,写法是 MicroPython,API 跟大家熟悉的 OpenMV 几乎同源——所以网上 OpenMV 的巡线、色块例程,K230 上稍改就能用。
📝 CanMV 是什么
CanMV = 在 K230 上跑的 MicroPython 视觉固件。你用 Python 写”找黑线””找色块”,它底层调硬件帮你算。会写几行 Python 就能上手,不用碰 C。
对电赛小车来说,常用的有这么几样活:
| 任务 | 用的 API | 回传给主控的”结论” |
|---|---|---|
| 巡线 | find_blobs 加权质心 / get_regression 线性回归 |
line_error、line_angle |
| 找色块 | find_blobs([色彩阈值]) |
色块中心坐标、颜色编号 |
| 二维码 | find_qrcodes() |
.payload() 内容字符串 |
| 机器码 AprilTag | find_apriltags() |
.id()、中心坐标、旋转角 |
| 数字/文字 OCR | OCR kmodel | 识别出的数字/字符串 |
巡线的两条路线
巡线是小车赛最高频的视觉任务,K230 上有两条主流做法,都只把”一个偏差 + 一个角度”发给主控。
路线一:灰度加权质心法。 把画面横向切成几条 ROI(感兴趣区域),近处权重大、远处权重小,每条 ROI 里用 find_blobs 找黑线最大的色块,加权平均出线的中心位置,再算出偏角。官方例程里近/中/远的权重是 0.7 / 0.3 / 0.1——理解一下:脚下的线最该听(权重大),远处的线只是”预判方向”(权重小),这其实就是我们在感知篇讲过的”前瞻”。
路线二:线性回归 get_regression。 直接对整幅二值图做一次直线拟合,返回一个 line 对象,里面有 theta()(角度)、rho()(到原点的距离,反映横向偏移),还有一个很有用的 magnitude()。
💡 magnitude 是"线靠不靠谱"的置信度
get_regression 的 magnitude 范围是 0 到正无穷,越大说明拟合出来的越像一条直线、越可信;接近 0 说明这堆点更像一团乱(圆),多半是噪点。就像你看一行字,写得越直越能确信”这是一条线”而不是一团花。可以拿它当门槛:magnitude 太小就认为”这帧没看清”,直接告诉主控”我没线”。
二维码、AprilTag、OCR——直接出”结论”
这三样的共同点是:K230 直接帮你把结果算成字符串或编号,你只要把那几个字节塞进串口发出去。
- 二维码
find_qrcodes():返回对象的.payload()就是二维码里的内容字符串。 - AprilTag(一种黑白方块机器码,专为机器识别设计)
find_apriltags():返回.id()、中心坐标.cx()/.cy()、旋转角。默认用 TAG36H11 这一族(6×6 方块,误识率低);还有 TAG16H5(4×4,看得更远但更容易认错)。 - OCR / 数字识别:用官方的 OCR kmodel,能识别中英文和数字;纯手写数字也可以走 MNIST 模型分类。
⚠️ 一个容易白调的坑
二维码识别率低时,常常要加 set_hmirror/set_vflip 做镜像校正(因为摄像头朝向可能把图照反了);但 AprilTag 不需要镜像校正。两者搞混会让你白白折腾半天,还以为是阈值问题。
分辨率怎么选
一句口诀:巡线用小图保帧率,识别用大图保精度。
巡线是几何任务、要的是控制环快(帧率就是控制频率的上限),所以常用 160×120(QQVGA)甚至更小的灰度图;二维码官方例程用 640×480 灰度;OCR/AI 识别最吃算力,但 K230 有 NPU 撑着,可以慢一点换清晰。K230 还支持多通道,甚至能一路给屏幕显示、一路给算法跑。
三、视觉端代码:把巡线结果发出去
下面给一段可以直接改的灰度加权质心巡线代码(K230 / MicroPython),算出偏差和角度后调 send_line 发给主控。发送函数 send_line/send_frame 的帧格式我们在第五节细讲,这里先把”算结论”的逻辑看清楚。
import math
GRAYSCALE_THRESHOLD = [(0, 64)] # 黑线的灰度阈值(现场要重标!)
ROIS = [ (0, 100, 160, 20, 0.7), # 近(权重大)
(0, 50, 160, 20, 0.3), # 中
(0, 0, 160, 20, 0.1) ] # 远
weight_sum = sum(r[4] for r in ROIS)
IMG_W, IMG_H = 160, 120
while True:
img = sensor.snapshot()
centroid_sum, found = 0, 0
for r in ROIS:
blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True)
if blobs:
b = max(blobs, key=lambda x: x.pixels()) # 取最大的黑块
centroid_sum += b.cx() * r[4]; found += 1
if found:
center_pos = centroid_sum / weight_sum
line_error = center_pos - IMG_W/2 # 居中=0,正负代表左右偏
angle = math.degrees(-math.atan((center_pos - IMG_W/2) / (IMG_H/2)))
send_line(line_error, angle) # 发给主控
else:
send_frame(0x00, b'') # 一条线都没找到 -> 告诉主控
如果走线性回归路线,核心就这几行:
img = sensor.snapshot().binary([(0, 64)]) # 先二值化
line = img.get_regression([(255, 255)], robust=True)
if line and line.magnitude() > 8: # 阈值自调,过滤不可信的拟合
rho_err = abs(line.rho()) - img.width()/2 # 横向偏移
theta = line.theta()
theta_err = theta - 180 if theta > 90 else theta # 角度,>90度要修正
send_frame(0x01, struct.pack('<hh', int(rho_err), int(theta_err * 10)))
else:
send_frame(0x00, b'') # 没拟合到可信的线
注意那行 theta - 180 if theta > 90:get_regression 返回的角度有时会跳到 90 度以上,需要减 180 折回来,否则角度会突变、车会猛打一下方向。这是 OpenMV 官方巡线工程 的标准处理。
❗ 阈值和 ROI 必须现场重标,不能照抄
上面的灰度阈值 [(0,64)] 和 ROI 是按别人的赛道、别人的光照定的。换个场地、换个灯,黑白的灰度值就变了。到了赛场第一件事就是对着真实赛道重新标阈值,否则”明明代码是对的,就是巡不到线”——多半就是阈值没标。
如果你用线性回归 + 主控双 PID 的方案,OpenMV 官方的经验 值得记住:横向偏移 rho 配一个 PID(经验 p ≈ 0.4),角度 theta 配另一个 PID(经验 p ≈ 0.001)。注意这俩 p 值差了两个数量级,必须分开调——不少人想用一个 PID 硬扛,结果怎么都不稳。具体怎么把这两路偏差喂进方向环、怎么整定,方向环和串级的细节见《PID 进阶:方向环、串级双环与工程补丁》一篇。
四、通信三选一:为什么是 UART
视觉端算好了结论,怎么送到主控?常见有三条总线:UART、SPI、I2C。对”只回传几个数”这个场景,结论很干脆——UART 几乎是唯一正确答案。
打个比方理解三者的取舍:
- UART 像乡间双车道:两根线(TX/RX)加地线,点对点,够用又省事。
- SPI 像高速公路:多车道全双工、速率最高(>10 Mbps),适合传图/大带宽,但要修匝道(每个从机一根 CS)、走线多、长线容易出错。
- I2C 像公交线:一条总线上挂一串站点,省 GPIO,但速度慢、要排班寻址(地址管理 + 上拉电阻),抗噪和距离最差。
给小车回传几个数,走”乡间双车道”最划算。详细对比:
| UART(主推) | SPI | I2C | |
|---|---|---|---|
| 线数 | 2(TX/RX)+ 地 | 4(CLK/MOSI/MISO/CS) | 2(SCL/SDA)+ 地 |
| 方式 | 异步全双工,点对点 | 同步全双工,主从 | 同步半双工,总线多从 |
| 速率 | 中(115200 够用) | 最高(>10 Mbps) | 低(100k~400k) |
| 适合 | 回传几个结论 | 传图/高带宽 | 一条总线挂多传感器 |
| 配套 | DMA+IDLE 零成本收整帧,库最成熟 | 走线多、CS 管理麻烦 | 上拉+地址管理麻烦、抗噪差 |
✅ 选型口诀
K230 只回传几个数 → UART;要回传整幅图/点云 → 才考虑 SPI;同一总线上挂多个传感器 → 才用 I2C。99% 的小车赛用 UART。
接线:3.3V 直连,别加多余的转换板
K230 所有 IO 都是 3.3V,STM32F4 的 IO 也是 3.3V,所以 TX 接对方 RX 交叉接、共地,就能直连,不需要电平转换芯片。波特率约定 115200、8 位数据、无校验、1 位停止(也就是常说的 8N1)即可。
⚠️ 接线两个高频翻车点
- 忘了共地:K230 和主控不共地 → 电平没有共同基准 → 全是乱码。两块板之间一定要拉一根 GND。
- 5V 主控直连:如果你的主控是 5V 系统(某些老板子、Arduino),它的 TX 是 5V,直接接到 K230 的 RX 可能把 K230 打坏。这种情况 K230 的 RX 要分压或加电平转换。STM32 是 3.3V 就没这问题。
K230 用串口前还有一步软件上的坑:必须先用 FPIOA 做引脚复用,纯软件 UART() 初始化是不够的。另外 K230 一共 5 个串口,UART0(小核 SH)和 UART3(大核 SH)常被系统占用,别选;用户可用 UART1/2/4,主推 UART2(GPIO11 = TX,GPIO12 = RX,对应庐山派的 GH1.25 接口①)。
from machine import UART, FPIOA
# 关键:必须先做引脚复用,否则口不通!
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS,
parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
引脚、API、电平这些硬事实,以 立创庐山派 K230 wiki 为准最权威。
五、帧协议:给数据装个”快递包裹”
确定了用 UART,下一个问题是:数据在线上是一串字节流,主控怎么知道”一帧从哪开始、到哪结束、有没有传错”?这就需要设计帧协议。
打个比方,一帧数据就像一个寄出去的快递包裹:
🧩 帧协议 = 寄快递
- 帧头(封口贴):告诉收件人”包裹从这开始了”
- 类型:里面装的是巡线结果,还是二维码 id
- 长度(运单上写几件):数据区有多少字节
- 数据:真正的货物(那几个数)
- 校验(防伪码):确认路上没被掉包/传错
- 帧尾(封箱胶带):到此结束
少了哪样,收件人都可能拆错包。
一个经过实战检验、可以直接用的格式:
帧头 0xAA 0x55 | 类型 1B | 长度 1B | 数据 N字节 | 校验 1B | 帧尾 0x0D 0x0A
几个设计点说一下:
- 帧头用双字节 0xAA 0x55,而不是单字节。因为在二进制数据流里,单个 0xAA 太容易被数据区里恰好相同的字节”撞上”导致错帧;双字节 + 长度 + 校验三重确认,误同步概率就压到可以忽略。顺便一提,0x55 = 01010101 在线上是个 50% 占空比的方波,最容易用示波器对齐,所以常被选作帧头。
- 长度字段让你能收变长数据(1 字节最大 255,扣掉校验后净载荷还有 200 多字节,对小车绰绰有余)。
- 校验可以用最轻量的累加和,也可以用更强的 CRC-8 / CRC-16。新手先用累加和就够。
- 帧尾 0x0D 0x0A(也就是
\r\n)的好处是:用串口助手或日志直接打印时能自动换行,调试时一眼能看清每一帧。
🔥 帧头/帧尾可能撞上数据
如果数据区里恰好出现了 0x0D 0x0A,而你又只靠帧尾来断帧,就会被误判成”帧结束了”。两种解法二选一:① 像本方案这样靠”帧头 + 长度 + 校验”整体判定,根本不依赖帧尾找边界;② 做转义(约定一个转义字符如 0x7D,遇到冲突字节就改写)。新手用方案①最省事。
视觉端发送代码
import struct
FRAME_HEAD = b'\xAA\x55'
FRAME_TAIL = b'\x0D\x0A'
# type: 0x01=巡线 0x02=二维码 0x03=AprilTag 0x04=数字 0x00=无目标
def send_frame(msg_type, payload: bytes):
length = len(payload)
body = bytes([msg_type, length]) + payload # 类型 + 长度 + 数据
checksum = 0
for b in body:
checksum = (checksum + b) & 0xFF # 累加和(可换成 CRC)
uart.write(FRAME_HEAD + body + bytes([checksum]) + FRAME_TAIL)
# 巡线 -> 回传 line_error 和 line_angle(放大10倍发,保留0.1度精度)
def send_line(line_error, line_angle_deg):
payload = struct.pack('<hh', int(line_error), int(line_angle_deg * 10))
send_frame(0x01, payload)
# 二维码/AprilTag/数字 -> 只回一个 id
def send_id(msg_type, id_val):
send_frame(msg_type, bytes([id_val & 0xFF]))
⚠️ 大小端要两边对上
上面用 struct.pack('<hh', ...) 发的是小端(低字节在前)。主控那边拼字节时也必须按小端还原(见下面 C 代码里的 data[0] | (data[1]<<8))。两边约定不一致,解出来就是乱值。
六、主控收包:DMA + 串口空闲中断的黄金组合
主控这边怎么收?新手常见的错法是:在串口中断里一个字节一个字节地收,还顺手把解析、甚至 PID 都塞进中断里。这样中断被占用太久,控制环都会被拖卡。
正确姿势是 DMA + 串口 IDLE(空闲)中断:
🧩 DMA + IDLE = 传送带 + 到货铃
DMA 是后台传送带,把串口来的字节一个个搬进缓冲区,全程不用 CPU 管;当一整帧到齐、线路空闲下来时,IDLE 中断就像”货到齐了按一下铃”,CPU 这才过来清点。平时 CPU 根本不用守着串口。
这套组合的好处是:零 CPU 占用 + 硬件自动断帧(天然支持变长收包)。现代 HAL 库写起来很省心,一个回调搞定:
#define RX_BUF_SIZE 64
static uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t frame_ready = 0;
volatile uint16_t frame_len = 0;
static uint8_t frame_copy[RX_BUF_SIZE];
// 初始化时调用一次(放在 DMA/UART Init 之后)
void Vision_UART_Start(UART_HandleTypeDef *huart){
HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buf, RX_BUF_SIZE);
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT); // 关半满中断,只要IDLE/收满
}
// 整帧到达回调:Size = 本帧字节数(收到指定长度或线路空闲触发)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){
if (Size > 0 && Size <= RX_BUF_SIZE){
memcpy(frame_copy, rx_buf, Size);
frame_len = Size; frame_ready = 1; // 只置标志,解析放主循环
}
HAL_UARTEx_ReceiveToIdle_DMA(huart, rx_buf, RX_BUF_SIZE); // 重启接收
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);
}
⚠️ DMA+IDLE 方案的第一坑:IDLE 中断要手动开
很多人用 HAL_UARTEx_ReceiveToIdle_DMA 却发现空闲中断死活不触发。原因是 IDLE 中断不像 DMA 完成中断那样自动开启,必须确认开了 UART 全局中断,且 DMA 配成 Normal 模式配合。如果你用老写法,记得 __HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE) 手动使能;老写法里算本帧长度用 rx_len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma)。
解析:一道道闸机的状态机
收到一整块字节后,怎么验证它是合法的一帧?用一个逐字节状态机最清晰。
🧩 状态机 = 过安检的闸机
一道道关卡:等帧头 → 读类型 → 读长度 → 收数据 → 验校验 → 等帧尾。每过一关进下一状态,任何一关不对就打回起点重新排队。最后放行的,一定是合规的完整帧。
typedef enum {S_H1,S_H2,S_TYPE,S_LEN,S_DATA,S_SUM,S_T1,S_T2} PState;
typedef struct { uint8_t type; uint8_t len; uint8_t data[32]; } Frame;
// 逐字节喂入,返回 1=完整有效帧, -1=出错, 0=进行中
int frame_feed(uint8_t byte, Frame *out){
static PState st = S_H1; static uint8_t idx=0, sum=0; static Frame f;
switch(st){
case S_H1: if(byte==0xAA) st=S_H2; break;
case S_H2: st = (byte==0x55)? S_TYPE : S_H1; break; // 双字节帧头
case S_TYPE: f.type=byte; sum=byte; st=S_LEN; break;
case S_LEN: f.len =byte; if(f.len>32){st=S_H1;break;} // 越界保护
sum+=byte; idx=0; st = f.len? S_DATA : S_SUM; break;
case S_DATA: f.data[idx++]=byte; sum+=byte;
if(idx>=f.len) st=S_SUM; break;
case S_SUM: if((sum&0xFF)!=byte){ st=S_H1; return -1; } // 校验失败,丢
st=S_T1; break;
case S_T1: st = (byte==0x0D)? S_T2 : S_H1; break;
case S_T2: st=S_H1;
if(byte==0x0A){ *out=f; return 1; } // 完整有效帧!
return -1;
}
return 0; // 进行中
}
📝 另一种常见写法:环形缓冲
如果你更习惯”DMA 整块搬进来、再批量找帧”,可以用环形缓冲区:循环找 0xAA 帧头、读长度、累加和校验,校验过才把整帧搬出、读指针前移。庐山派配套的 command.c 就是这套典型的嵌入式风格,K230-Study-Reference 里有现成代码。想要工业级带 CRC-8 和转义的 C 解析库,可以参考 uart-frame-parser。两种范式选一种用即可。
七、保命:超时降级,绝不让小车失控
视觉模块再好,赛场上也会掉链子——强光、反光、被手挡一下、线松了,丢帧是常态。这时候如果主控还傻等视觉数据,或者拿着一张过期的旧照片硬冲,小车就直接冲出赛道掉大分了。
所以必须有超时降级这道保命机制:
🧩 超时降级 = 备用降落伞
视觉是主伞(首选),但你一定要背个备用伞(灰度/IMU)。100ms 没收到有效数据就立刻开备用伞——切回灰度巡线,或用 IMU 锁住当前航向直行/缓停。宁可降落慢一点,也绝不自由落体(失控)。
volatile uint32_t last_valid_ms = 0;
int16_t line_error=0, line_angle=0; uint8_t vision_ok=0;
void Control_Loop(void){ // 固定周期调用(如 5ms)
if(frame_ready){
frame_ready=0; Frame f;
for(uint16_t i=0;i<frame_len;i++){
int r = frame_feed(frame_copy[i], &f);
if(r==1){ // 只有校验通过的整帧
last_valid_ms = HAL_GetTick(); // 才刷新超时计时!
if(f.type==0x01){ // 巡线
line_error = (int16_t)(f.data[0]|(f.data[1]<<8));
line_angle = (int16_t)(f.data[2]|(f.data[3]<<8));
vision_ok = 1;
} else if(f.type==0x00){ vision_ok = 0; } // 视觉报告:没线
// else 0x02/0x03/0x04: 二维码/Tag/数字 id = f.data[0]
}
}
}
if(HAL_GetTick() - last_valid_ms > 100){ // >100ms 超时
vision_ok = 0;
Fallback_GrayscaleOrIMU(); // 降级:灰度巡线/IMU航向
} else if(vision_ok){
Motor_PID(line_error, line_angle); // 正常:视觉闭环
}
}
这段里藏着几条容易被忽略、但赛场上很要命的细节:
❗ 降级机制的三条铁律
- 只有校验通过的帧才刷新超时计时。 坏帧(校验错、残帧)一律丢弃、不刷新——否则坏数据会维持”视觉还活着”的假象,该降级时不降级。
- 超时阈值要留余量。 100ms 大约是正常帧间隔的 2~3 倍,给抖动留空间,不会因为偶尔晚到一帧就误降级。
- 降级和恢复之间加迟滞(hysteresis)。 视觉时断时续会让小车在”正常↔降级”之间来回抖动切换。可以设成:连续丢 N 帧才降级,连续收到 M 帧好数据才切回视觉。
要让降级真正靠谱,前提是灰度、IMU 这些备选方案本身就已经能独立巡线/锁航向。这就回到了一条贯穿整个系列的最佳实践:
💡 先各模块单独调通,再合并
视觉、灰度、IMU、驱动、通信都先各自单独验证能跑,再合并到主控。2024 电赛 H 题的获奖方案就反复强调这种模块化——隐藏好处正是:视觉一掉线,主控能无缝切到”已经独立验证过”的灰度/IMU。赛前一定要专门做一次故障注入测试:故意拔掉视觉线,看小车会不会乖乖降级,而不是只测正常路径。
八、联调心法与一份避坑速查
最后给一句这一篇最重要的工作方法:
✅ 先把协议定死,双方各自开发,最后联调
视觉同学和电控同学一开始就坐下来,把”传哪几个数、什么类型、什么格式、大小端、波特率”全部写在一张纸上钉死。然后各写各的——视觉端发完整帧(哪怕暂时发假数据),电控端按协议解析。等两边都自测通过,插上线联调时往往一次就通。最怕的是边写边改协议,两头永远对不齐。
随手附一份本篇的避坑速查,赛前过一遍:
- ☐ 视觉端只发”结论”几个数,绝不发整图
- ☐ 帧头用双字节 0xAA 0x55 + 长度 + 校验三重确认
- ☐ K230 用串口前先 FPIOA 配引脚复用,且别用 UART0/UART3
- ☐ K230 与 STM32 都是 3.3V,TX-RX 交叉接、务必共地
- ☐ 主控收包走 DMA + IDLE 中断,IDLE 中断记得手动使能
- ☐ 中断里只置标志,解析和控制放主循环
- ☐ 大小端两边对齐(视觉
<hh小端 → 主控低字节在前还原) - ☐ 只有校验通过的帧才刷新超时计时,坏帧只丢弃
- ☐ >100ms 无有效帧立即降级到灰度/IMU,并加迟滞防抖动
- ☐ 巡线阈值和 ROI 到现场必须重新标定,别照抄例程
- ☐ 赛前做一次”拔视觉线”的故障注入,验证降级真的会触发
到这里,小车的”眼睛”和”神经”就接通了:视觉看到东西、算出结论、打包成帧发出去,主控收包、校验、闭环,还带着超时降级的保命绳。但你可能已经发现,主控这边的活越来越多了——既要跑控制环、解析串口,又要处理起步、过弯、停靠、丢线找回这些不同阶段的逻辑。这些怎么有条不紊地组织起来、各自按什么节奏跑,就是下一篇要解决的事:状态机与整车软件架构。
📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列(共 12 篇)第 10 篇。




