ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RISC-V开发板驱动视觉机械臂:YOLOE+Agent+MCP闭环实践

2026/9/8 13:13:49 拓冰建站 浏览量
RISC-V开发板驱动视觉机械臂:YOLOE+Agent+MCP闭环实践 1. 项目整体思路为什么把这四样拼在一起先直接回答标题里的问题RISC-V 确实能跑机器人但要看跑成什么样。我手里这块 VisionFive 2 是 StarFive 出的 RISC-V 开发板四核 Cortex-A558GB 内存带一个 2TOPS 左右的 NPU。说实话这个算力在机器人视觉领域算不上豪华但做桌面级机械臂的视觉闭环已经够用了。加上它把 PCIe、千兆网、USB 3.0 都给了外接摄像头、舵机控制板都很方便整套系统的扩展性比想象中好。这个项目我给它定的目标是用 VisionFive 2 作为唯一的主控跑 YOLOE 做目标检测识别桌面上的物体位置然后把坐标通过 Agent API 交给一个自动化决策层最后通过 MCP 协议把动作指令下发到机械臂完成“看到-判断-执行-验证”的完整闭环。消息中间件用的是 ROS 2但关键是整条链路的模型选择、接口设计和调试方法这些东西跟具体硬件绑定不算太深换到别的开发板上思路同样成立。为什么选 YOLOE 而不是老牌的 YOLOv8因为 YOLOE 是开放词汇检测模型不需要针对特定物体重训一个分类头你用文字描述目标类别它就能在画面里找出来。对机器人项目来说这个特性太关键了桌面上的物体种类是经常变的如果每次都重新标注数据、重新训练项目根本跑不起来。后面我会详细讲 YOLOE 在 RISC-V 平台上的移植和推理优化包括我改过的预处理代码和推理参数。至于 Agent API 和 MCP简单理解就是让机器人拥有“听懂指令”和“调用工具”的能力。我这边用了一个本地的 Agent 框架接收自然语言任务后解析成结构化的动作序列再通过 MCP Server 暴露的机械臂控制接口去执行。MCPModel Context Protocol可以把外部工具包成标准接口给大模型调用我在这块板上跑了一个轻量级 MCP Server负责把机械臂的坐标移动、夹爪开合、状态查询封装成工具Agent 拿到视觉检测结果后自己决定下一步做什么。这套组合最大的价值在于它把“视觉识别”和“任务决策”彻底解耦了。视觉部分专注输出物体类别和位置Agent 负责任务拆解MCP 负责执行哪一环出问题都能单独排查不用推倒重来。2. 视觉闭环的核心实现2.1 先跑通 YOLOE模型准备与推理环境搭建YOLOE 的推理在 PC 上不算难但放到 VisionFive 2 上就有讲究了。这块板子的 NPU 支持的算子有限直接拿 x86 上编译好的 ONNX 模型跑大概率会报错或者部分算子回落到 CPU速度会很难看。我的做法是先用官方仓库把 YOLOE 导出成 ONNX再用 npu-compiler 工具链做模型转换量化成 INT8。导出模型时有一个关键参数需要改输入分辨率。官方默认是 640x640但机械臂抓取场景里目标物体往往比较小我建议直接上 1280x1280。代价是推理时间增加但 VisionFive 2 的 NPU 跑 1280 输入时大概能到 4~6 FPS对于“物体基本静止、机械臂动作本身需要一两秒”的桌面场景完全够用。如果你对帧率有要求可以把输入降回 640但检测小物体的召回率会明显下降这个取舍得根据实际任务来。推理代码我用了 Python 版本的 rknn-toolkit2 兼容层VisionFive 2 的 NPU 接口与 RKNN 有相似之处但注意它不是完全兼容需要根据 StarFive 提供的 API 改写。核心的部分是这样的import numpy as np from starfive_npu import Model # 加载转换后的模型 model Model(yoloe_int8.nb) def preprocess(frame): # 保持宽高比缩放不足部分填充灰色 h, w frame.shape[:2] scale min(1280 / h, 1280 / w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(frame, (nw, nh)) canvas np.full((1280, 1280, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized # 归一化到 0~1并转为 NHWC 格式 tensor canvas.astype(np.float32) / 255.0 tensor np.expand_dims(tensor, axis0) return tensor, scale, nh, nw def infer(frame): tensor, scale, nh, nw preprocess(frame) # 推理结果: boxes, scores, classes, texts boxes, scores, classes, texts model.run(tensor) # 坐标映射回原始图像尺寸 boxes[:, [0, 2]] / scale boxes[:, [1, 3]] / scale return boxes, scores, classes, texts这里有个容易踩的坑YOLOE 的后处理跟 YOLOv5/v8 不一样它在模型输出里直接带了文本嵌入的相似度分支所以 classes 和 texts 是成对出现的。我在前期调试时一直用 YOLOv8 的 decode 方式去解析输出导致所有检测框都是乱的后来仔细看了输出张量的形状才定位到问题。如果你自己导模型务必确认输出节点是哪几个别想当然。2.2 从像素坐标到机械臂抓取坐标模型输出的是像素坐标系里的目标框而机械臂需要的是三维空间坐标这一步需要做坐标转换。我没有用复杂的标定工具而是用了一个相对粗暴但实际很稳的方法先固定摄像头位置然后让机械臂末端分别移动到画面中的几个已知点位记录下像素坐标和机械臂坐标的对应关系再用透视变换矩阵拟合。这个方法的前提是摄像头和机械臂底座的位置相对固定所以我在项目里设计了一个固定支架把摄像头装在机械臂正上方俯视工作台这样既减少了遮挡问题透视变换的误差也小。import cv2 import numpy as np # 标定数据: 像素坐标 - 机械臂底座坐标系坐标 pixel_pts np.array([[320, 240], [960, 240], [320, 720], [960, 720]], dtypenp.float32) robot_pts np.array([[150, 150], [450, 150], [150, 450], [450, 450]], dtypenp.float32) # 计算透视变换矩阵 M cv2.getPerspectiveTransform(pixel_pts, robot_pts) def pixel_to_robot(px, py): point np.array([px, py, 1.0], dtypenp.float32) rx, ry, rw M point return rx / rw, ry / rw注意这里的机械臂坐标是我简化后的二维平面坐标真实项目还要加上夹爪张开高度和物体高度估计。桌面高度固定所以 Z 轴基本是个常数但如果你抓的是圆柱体或球体需要额外估计物体的高度否则夹爪会撞到桌面上。我后来加了简单的激光测距模块直接读物体顶面的高度比纯视觉估计可靠得多。2.3 视觉闭环里面的关键延时控制视觉闭环最常见的问题不是识别不出来而是“识别出来了但手臂已经错过了”。这里最关键的是要控制整个闭环的延迟预算。我实测过在 VisionFive 2 上 YOLOE 单帧推理大约是 200 到 250 毫秒加上图像采集、坐标变换、MCP 消息转换全部走完接近 400 毫秒。如果机械臂动作本身要 1 秒这个延迟其实是可以接受的。但如果你的项目里物体是在移动的400 毫秒延迟就会导致明显的位置偏差。我是怎么解决的两个手段并举第一把检测频率降下来但控制频率提上去。检测模块跑 5 FPS但机械臂控制走单独的线程每 50 毫秒根据最近一次目标坐标做一次 PID 跟踪。第二给目标框加一个轻量级的卡尔曼滤波预测物体在下一个控制周期可能的位置这样即使视觉帧率低控制端也能平稳跟踪。视觉线程 : 摄像头 - YOLOE 检测 - 输出目标坐标约5Hz 控制线程 : 读取最新坐标 - 卡尔曼预测 - 发给 MCP Server约20Hz MCP 线程 : 接收动作指令 - 机械臂控制指令这个结构看着简单但实际工程里线程间的数据传输很容易出错。我的建议是不要直接共享可变对象用带锁的消息队列传递不可变数据。坐标数据量很小哪怕是 Python 的 queue.Queue 也够用关键是别在多个线程里同时改同一个 numpy 数组我因为这个问题排查了整整一个下午。3. Agent API 与 MCP 的衔接3.1 用 MCP Server 给机械臂开一个“工具接口”MCP 的核心思想是把外部设备或软件的能力抽象为标准的工具tools大模型通过标准协议去调用这些工具而不是每次都用自然语言把指令发给控制系统。我在这里做了一个轻量级的 MCP Server暴露了三个工具move_to、gripper、get_status。每个工具都有自己的输入参数说明Agent 调用时会自动带上参数。其实不用把 MCP Server 想得太复杂它的本质就是一个 JSON-RPC 服务。客户端发来一个请求服务端解析方法名和参数然后执行对应的 Python 函数把结果返回去。我在 VisionFive 2 上用的 FastAPI 实现因为开发效率高、调试方便运行时占用的资源也完全可以接受。from fastapi import FastAPI from pydantic import BaseModel import serial app FastAPI() arm serial.Serial(/dev/ttyUSB0, 115200) class MoveRequest(BaseModel): x: float y: float z: float speed: float 100.0 class GripperRequest(BaseModel): action: str # open or close app.post(/move_to) def move_to(req: MoveRequest): # 转换坐标并生成机械臂控制指令 cmd fG0 X{req.x} Y{req.y} Z{req.z} F{req.speed}\n arm.write(cmd.encode()) return {status: ok, cmd: cmd} app.post(/gripper) def gripper(req: GripperRequest): action 1 if req.action open else 0 cmd fM3 {action}\n arm.write(cmd.encode()) return {status: ok, cmd: cmd} app.get(/get_status) def get_status(): arm.write(bM114\n) resp arm.readline().decode().strip() return {status: ok, position: resp}这里需要说明MCP 协议和普通的 HTTP API 并不矛盾。MCP 更多是站在“大模型工具调用”这一层它规定了工具发现的格式、参数 schema 怎么描述、请求响应怎么封装。如果你做的系统不是给大模型用的直接用 HTTP API 也完全没有问题。我这边实际是两层都做了MCP Server 负责和大模型通信HTTP API 负责和前端控制台通信底层调用同一套函数。3.2 Agent 如何自主决定抓取哪个物体视觉部分把桌面上的物体都标记出来了但具体抓哪个、先抓哪个、抓完放哪里这些需要 Agent 来决策。我这里用的是一个简单的 Agent 框架给它一个系统提示词描述当前环境里有机械臂、夹爪、摄像头以及工作台上可能出现的物体类别然后让它接收自然语言任务。一个典型的任务流程是这样的用户说“把红色的杯子放到左侧托盘里”Agent 需要先从视觉模块拿最新的检测结果找到类别为“cup”且颜色接近红色的目标框然后调用 move_to 移动到该物体上方再调用 gripper 夹取最后移动到托盘位置放下。在这个流程里Agent API 的作用是把视觉模块的输出包装成语义化的环境状态。视觉模块只负责输出“画面中有哪些物体、坐标是多少”而“哪个是任务需要的目标”这个决策交给了 Agent。这里我用了一个小技巧把 YOLOE 输出的文本嵌入结果直接作为环境描述的一部分返回给 Agent这样 Agent 不需要依赖硬编码的物体列表。用户任务: 把红色杯子放到左侧托盘里 Agent 解析: 1. 查询环境状态 - 获取物体列表 [{id: 1, category: cup, color: red, pos: (320, 425)}] 2. 筛选目标 - 选择 id1 3. 调用 move_to(x320, y425, z50) 4. 调用 gripper(actionclose) 5. 调用 move_to(x100, y100, z50) 6. 调用 gripper(actionopen) 任务完成为了不让 Agent 乱调参数我在 MCP Server 里对坐标范围做了边界检查超出工作台的坐标直接返回错误。这个看似简单的校验在实际调试中帮了大忙否则机械臂很容易就去撞限位了。4. 硬件调试中踩过的坑4.1 算力、内存和 IO 的实际瓶颈VisionFive 2 的 CPU 在跑 YOLOE 预处理和后处理时性能比我想象的要好真正拖后腿的反而是内存带宽。1280x1280x3 的输入做归一化和 resize 时频繁的内存拷贝会把耗时拉得很高。我一开始直接用 Python 的 cv2.resize一帧预处理要 80 毫秒后来改成 OpenCV 的 UMat 走 GPU其实是 NPU 的加速路径预处理的耗时降到了 40 毫秒左右。另外一个容易被忽略的瓶颈是 USB 摄像头帧率。理论上是 30 FPS但在 RISC-V 板子上实际跑到 15 FPS 就不错了原因不是摄像头不行而是 USB 控制器和驱动栈对高带宽传输支持不太好。我的解决方法是把摄像头设成 MJPG 格式而不是 YUYV同样分辨率下带宽需求直接减半。代价是 CPU 解码要多花一些时间但总体帧率是明显提升的。如果你对实时性要求更高建议直接用 CSI 接口的摄像头走 MIPI 传输带宽和延迟都会好很多。不过我那边 CSI 摄像头驱动在 VisionFive 2 上还有点问题暂时先用 USB 顶着后面会换。4.2 MCP Server 超时与状态不一致我这边实际遇到的一个隐蔽问题Agent 调用 MCP 工具时如果超时机械臂可能已经执行了动作但 Agent 因为没收到返回结果就重试导致同一个动作被执行两次。这个问题非常危险轻则夹爪空夹重则机械臂直接撞到限位。我的解决思路是在每个工具接口里加了一个 request_id 参数。Agent 在调用时生成一个唯一 IDMCP Server 收到后如果发现相同 ID 已经执行过就直接返回上一次的结果不再重复执行。这个方案实现成本很低但让整个系统的容错性好了一个量级。还有一个问题是夹爪状态同步。机械臂执行完夹取动作后如果目标物体太滑或者太重夹爪未必真的夹住了。我在夹爪上加了一个电流检测模块夹紧瞬间电流会突变如果电流没有变化就说明没夹住这个状态会通过 get_status 接口返回给 AgentAgent 会决定是否需要重新尝试。这一步让我想起了做工业机器人的人常说的“末端执行器反馈”没有这一层反馈视觉闭环严格来说是不成立的。4.3 避坑速查表问题现象可能原因解决办法YOLOE 推理结果全是乱框后处理解析用了 YOLOv8 的格式确认输出节点YOLOE 有独立的文本相似度输出预处理耗时过高多次内存拷贝用 UMat 走硬件加速路径避免 numpy 中转USB 摄像头帧率低摄像头输出格式带宽占用过大改用 MJPG 格式降低 USB 带宽需求Agent 重复执行动作MCP 工具调用超时后重试工具接口增加 request_id 幂等控制机械臂抓取失败但无感知缺少夹爪状态反馈加电流检测或力传感器回传状态NPU 模型转换失败某些算子不支持换用更基础的算子组合或降级到 CPU5. 项目可以扩展的两个方向5.1 多自由度机械臂的避障规划我现在做的这套系统本质上是“点到点运动”没有考虑中间路径上的障碍物。如果要抓取的位置旁边有别的物体机械臂直接从初始位置直线运动过去很可能撞到旁边的物体。一个可行的扩展是在 Agent 层加入路径规划模块比如把工作台栅格化用 A* 或 RRT 算法生成一条无碰撞路径然后拆成多个航点传给机械臂。这个扩展对算力的要求不高但对代码结构会有一定改动因为运动控制要从“单点”改成“轨迹”模式。5.2 让多个 Agent 协作目前的 Agent 是单线程的一次只处理一个任务。如果把任务拆成“看”和“动”两个 Agent一个 Agent 只负责视觉分析和目标确认另一个 Agent 负责运动控制它们之间通过消息队列通信系统的模块化程度会更高。更进一步两个 Agent 可以通过 MCP 互相调用对方的工具这样一来整个系统就有点像一个微型的多智能体机器人集群了。我目前只在模拟环境里验证过这个思路离真正落地还有一段距离但值得继续搞。个人说点实在的做这个项目最大的收获不是跑通了某个框架而是把“视觉识别”和“硬件控制”这条链路上所有的细节都过了一遍。从模型选型到坐标标定从 MCP 协议到机械臂反馈每一步都有很多“想当然”和“实际不是那么回事”的地方。RISC-V 跑机器人这个题目现在看已经不是一个“能不能”的问题而是“怎么跑得更稳、更聪明”的问题。VisionFive 2 这块板子的算力上限摆在那里但通过合理的架构和优化桌面级视觉机械臂完全可以在它上面跑起来。后续如果再给我一次机会我会把重点放在夹爪的力反馈和路径规划上这两块是当前系统最薄弱的环节也是真正决定一个机器人能不能干活的最终底线。