简介:本资源是一套面向嵌入式AI开发者的端侧智能控制完整实践方案,聚焦Jetson Nano与STM32协同实现垃圾分类模型部署及舵机闭环控制,适用于具备C语言基础与嵌入式开发经验的进阶学习者。资源共110个文件,涵盖38个C源码(含STM32外设驱动如usart、tim、rcc等)、40个头文件(h)、8个汇编文件(s),以及Paddle Lite模型文件(pdmodel/pdparams等)、Keil工程配置(uvprojx/uvoptx)、Python推理脚本、系统说明文档(md)和实操演示视频(mp4),总大小142.82MB,结构清晰,软硬协同模块分明。已有1812人学习下载,提供从垃圾图像数据预处理、CNN模型训练与轻量化优化(含量化部署细节),到Jetson Nano端推理输出、UART串口通信协议实现、STM32端PWM舵机精准控制的全链路代码与配置,附带Keil工程与Linux端部署脚本,可直接复现智能分拣硬件响应流程。
1. 项目缘起:当AI视觉遇到物理执行
最近在做一个挺有意思的机器人小项目,核心需求是让一个搭载了摄像头的Jetson Nano能够“看懂”环境,然后指挥一个由STM32控制的机械臂(舵机组)去执行动作。听起来像是把“眼睛”和“手”连接起来,但真做起来,从数据准备、模型训练、模型部署到最后的跨平台通信控制,每一步都藏着不少细节和坑。网上关于Jetson Nano跑深度学习的教程不少,STM32控制舵机的资料也一大堆,但把这两者串起来,形成一个从感知到执行的完整闭环,并且把每一步的选型理由、避坑经验讲清楚的,还真不多见。
这个项目的核心逻辑很清晰:Jetson Nano负责“看”和“想”,STM32负责“动”。Jetson Nano利用其GPU算力运行一个轻量级的深度学习模型(比如目标检测或分类模型),识别出摄像头画面中的特定目标或状态。然后,它需要将这个“想法”(例如,“目标在画面左侧”)转化为一个STM32能理解的指令,通过某种通信方式发送过去。STM32收到指令后,解析并生成相应的PWM信号,驱动舵机转动到指定角度,从而完成抓取、指向、避障等物理动作。
整个过程涉及嵌入式AI、边缘计算、单片机控制、通信协议等多个领域。我折腾了一圈下来,发现最大的挑战不在于单个环节,而在于如何让这些异构的、资源禀赋完全不同的设备(一个跑Linux的AI计算卡,一个资源紧张的微控制器)高效、稳定地协同工作。这篇文章,我就把自己从准备数据集开始,到最终在Jetson Nano上部署模型并与STM32通信控制舵机的完整流程、核心技术和踩过的坑,系统地梳理一遍。无论你是想复现类似项目,还是对其中某个环节(如模型部署、串口通信)感兴趣,希望都能从中找到有价值的参考。
2. 核心架构设计与技术选型思考
在动手写第一行代码之前,花点时间把整体架构和技术选型想清楚,能避免后期大量的返工。这个项目的架构可以抽象为“感知-决策-执行”三层,每一层的技术选型都直接关系到项目的成败和开发体验。
2.1 感知层:Jetson Nano与模型选型
Jetson Nano作为边缘AI计算设备,是我们的“大脑”。它的选型理由很充分:拥有128核Maxwell架构GPU,支持CUDA,能流畅运行经过优化的轻量级神经网络;功耗低(5W-10W),适合移动或嵌入式场景;原生支持JetPack SDK,包含了TensorRT、CUDA、cuDNN等深度学习加速库,部署环境友好。
模型选型是第一个关键决策。你不能在Jetson Nano上跑一个庞大的ResNet-50或YOLOv5l去做实时检测,那会严重卡顿。我们的目标是轻量、快速、精度可接受。经过对比,我主要考虑了以下几个方向:
- MobileNet系列 + SSD:经典组合。MobileNet使用深度可分离卷积极大减少了参数量和计算量,SSD(Single Shot MultiBox Detector)实现单次前向传播即可完成检测,速度上有优势。TensorRT对这类结构优化支持很好。
- YOLO系列轻量版:如YOLOv5s、YOLOv8n。YOLO系列在速度和精度上平衡得很好,社区活跃,从PyTorch导出到ONNX再到TensorRT的流程非常成熟。YOLOv5s/v8n在Jetson Nano上跑到15-30 FPS是可行的。
- 专门为边缘设计的模型:如NanoDet、YOLOX-Nano等。这些模型设计之初就考虑了极致的轻量化,在资源受限的设备上表现可能更优。
我最终选择了YOLOv8n。原因有几个:一是其PyTorch实现训练和导出非常方便;二是其ONNX导出和TensorRT加速的教程和工具链最完善,社区支持最好;三是对于我项目中需要识别的几种简单物体(比如不同颜色的方块、特定手势),YOLOv8n的精度完全够用。如果你的目标类别更少、场景更固定,甚至可以考虑自己设计一个几层卷积的小网络,速度会更快。
2.2 通信层:连接大脑与四肢的“神经”
Jetson Nano(Linux系统)和STM32(裸机或RTOS)之间的通信,是整个系统的“大动脉”。选型需要考虑稳定性、实时性、开发复杂度和硬件资源。
- UART串口通信:这是最经典、最直接的选择。几乎所有的STM32和Jetson Nano都自带UART外设。它简单、可靠、对单片机资源占用极小。缺点是速度相对较慢(通常到1Mbps左右),但对于传输“角度值”、“指令码”这类小数据包(几个到几十个字节)的控制场景,完全绰绰有余。在绝大多数类似项目中,UART都是首选,因为它能极大降低两端的开发复杂度。
- USB转串口(CDC):本质上还是串口,只是物理层换成了USB。Jetson Nano把STM32识别为一个串口设备(如
/dev/ttyACM0)。好处是连线简单(一根USB线搞定供电和通信),速率可能更高。但需要在STM32端实现USB CDC协议栈,会占用一些Flash和RAM,且驱动稳定性有时需要调试。 - SPI/I2C:这两种是板级短距离高速通信协议。它们速度比UART快,但通常用于主从设备间距离很近(同一块板子)的场景,需要连接更多物理线(时钟、数据等)。如果你的STM32是作为Jetson Nano的协处理器,焊接在同一块载板上,可以考虑。但对于分立的两个设备,接线麻烦,抗干扰能力也不如UART。
- 网络通信(TCP/UDP):如果STM32搭载了以太网或Wi-Fi模块(如W5500、ESP8266),那么可以通过Socket通信。这种方式非常灵活,距离远,甚至可以无线化。但会给STM32带来额外的复杂度和成本,且实时性受网络状况影响。
我的选择是UART。理由非常务实:首先,我的应用场景对实时性要求是毫秒级,UART的延迟完全满足;其次,STM32和Jetson Nano的UART开发都极其简单,有大量成熟代码参考;最后,硬件上只需要三根线(TX, RX, GND),连接可靠。我使用了Jetson Nano的/dev/ttyTHS1(这是它的一个硬件串口)与STM32的USART1相连,波特率设置为115200。这个速率对于传输“ID, X, Y, Angle”这样的指令帧,每秒可以发送上千次,远远过剩。
2.3 执行层:STM32与舵机控制
STM32在这里扮演一个“忠实执行者”的角色。它不需要复杂的逻辑,核心任务就两个:可靠地解析来自Jetson Nano的指令,并精确地生成PWM信号控制舵机。
舵机控制原理:市面上最常见的舵机(如SG90、MG996R)采用位置伺服控制。它需要接收一个周期通常为20ms(50Hz)的PWM信号,其中高电平的脉宽(脉冲宽度)决定了舵机转动的角度。例如,0.5ms脉宽对应0度,1.5ms对应90度,2.5ms对应180度。这是一个线性关系。
STM32的PWM实现:STM32的定时器(TIM)外设是生成PWM的利器。以通用定时器TIM2/3/4/5或高级定时器TIM1/8为例,配置为PWM输出模式后,可以通过调节CCR(捕获/比较寄存器)的值来改变输出波形的占空比,从而精确控制脉宽。例如,如果定时器时钟为72MHz,预分频后计数频率为1MHz(即每个计数1us),那么要产生1.5ms的高电平,只需将CCR设置为1500即可。
指令协议设计:为了通信可靠,必须定义一个简单的应用层协议。我设计了一个非常简单的文本协议,便于调试:$<servo_id>,<angle>;\n例如,$1,90;\n表示让1号舵机转到90度。STM32端通过串口中断接收字符,在收到\n后解析整条指令,校验格式,然后更新对应舵机PWM通道的CCR值。文本协议的好处是,你甚至可以直接用串口调试助手手动发送指令测试,非常直观。
3. 从零开始:数据集准备与模型训练
虽然项目标题是“部署”,但没有好的模型,部署就是无源之水。对于嵌入式部署,数据集和训练需要更有针对性。
3.1 数据集采集与标注
我的目标是让机械臂识别并抓取两种不同颜色的积木(红色和蓝色)。数据采集就在实际的工作场景中进行,使用了Jetson Nano连接的USB摄像头。
- 采集设备与环境:就使用项目最终要用的摄像头和光照条件。这能保证训练数据和实际推理数据分布一致,减少领域偏移。我在不同的光照(开灯/关灯)、不同的背景、积木不同的摆放角度和距离下,拍摄了大约300张图片。对于简单的目标,这个量基本够用。
- 标注工具:使用
labelImg或更现代的CVAT、Roboflow进行标注。标注格式选择YOLO格式(.txt文件),因为后续训练YOLOv8最方便。每个.txt文件里存储的是归一化后的中心坐标和宽高:<class_id> <x_center> <y_center> <width> <height>。 - 数据增强:为了提升模型鲁棒性,防止过拟合,必须做数据增强。我直接在训练时利用YOLOv8框架内置的增强功能,包括随机旋转(±15度)、亮度对比度调整、模糊、马赛克增强等。对于嵌入式场景,特别要注意增强的幅度不能太夸张,要符合实际物理可能(比如物体不会上下颠倒出现)。
3.2 模型训练与优化
我使用Ultralytics的YOLOv8进行训练,这是目前最省事的方案。
环境搭建:在一台有NVIDIA显卡的电脑上(我的开发机),创建Python虚拟环境,安装
torch(CUDA版本)、ultralytics包。pip install ultralytics训练配置:YOLOv8的命令行接口非常强大。我准备了一个
dataset.yaml文件,定义训练集、验证集路径和类别名称。path: /path/to/your/dataset train: images/train val: images/val names: 0: red_block 1: blue_block然后使用以下命令开始训练
YOLOv8n模型:yolo train data=dataset.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16epochs=100:对于小数据集,100轮通常足够。imgsz=640:输入图像尺寸。更小的尺寸(如320)速度更快,但可能损失精度。640是一个较好的平衡点。batch=16:根据你的GPU显存调整。
训练监控与调参:训练过程中,使用TensorBoard或Ultralytics自带的日志查看损失曲线和精度指标(mAP50)。重点关注验证集损失是否持续下降,验证集mAP是否收敛。如果出现过拟合(训练损失下降,验证损失上升),可以增加数据增强强度、使用更小的模型(如
yolov8n)或添加正则化(如权重衰减)。模型导出:训练得到最好的模型权重(如
best.pt)后,需要将其导出为Jetson Nano上TensorRT可用的格式。标准流程是:PyTorch -> ONNX -> TensorRT Engine。yolo export model=best.pt format=onnx imgsz=640 simplify=True这条命令会生成一个
best.onnx文件。simplify=True会应用ONNX简化,去除一些冗余操作,对后续转换有好处。
4. 模型在Jetson Nano上的部署与优化
这是项目的核心环节,目标是将ONNX模型转换成TensorRT引擎,并编写高效的推理代码,最终达到实时性能。
4.1 Jetson Nano环境准备
首先确保你的Jetson Nano刷好了最新的JetPack系统(包含CUDA, cuDNN, TensorRT)。我使用的是JetPack 5.1.2。然后安装一些必要的Python包:
sudo apt-get update sudo apt-get install python3-pip libopenblas-dev libomp-dev pip3 install --upgrade pip # 安装PyTorch for Jetson (版本需与JetPack中的CUDA匹配) # 从NVIDIA官方论坛或仓库获取对应版本的.whl文件安装 pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 # 示例,需核对版本 # 安装其他依赖 pip3 install numpy opencv-python pillow pycuda # 安装用于ONNX解析和TensorRT转换的库 pip3 install onnx onnxruntime # TensorRT通常已随系统安装,Python接口可通过安装对应的wheel文件4.2 使用TensorRT加速推理
手动使用TensorRT的C++或Python API进行模型转换和推理有一定复杂度。我推荐使用NVIDIA的torch2trt或trt工具链,或者使用ONNX Runtime的TensorRT Execution Provider。这里我采用后者,因为它相对简单,且ONNX Runtime对动态形状支持较好。
安装ONNX Runtime for Jetson:需要安装支持TensorRT的版本。
# 根据你的JetPack版本,从ONNX Runtime发布页下载对应的.whl文件 # 例如:onnxruntime_gpu-1.xx.x-cp38-cp38-linux_aarch64.whl pip3 install onnxruntime_gpu-*.whl编写推理脚本:创建一个Python脚本,使用ONNX Runtime加载ONNX模型并指定TensorRT作为执行提供者。
import cv2 import numpy as np import onnxruntime as ort class YOLOv8Detector: def __init__(self, onnx_path, conf_thres=0.5, iou_thres=0.5): # 创建TensorRT推理会话 providers = ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'] self.session = ort.InferenceSession(onnx_path, providers=providers) self.input_name = self.session.get_inputs()[0].name self.output_name = self.session.get_outputs()[0].name self.conf_thres = conf_thres self.iou_thres = iou_thres # 获取输入尺寸 (例如:640x640) self.input_shape = self.session.get_inputs()[0].shape[2:] # [H, W] def preprocess(self, img): # 将BGR图像转换为RGB,调整大小,归一化,并转换为NCHW格式 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized = cv2.resize(img_rgb, self.input_shape) img_normalized = img_resized.astype(np.float32) / 255.0 img_chw = np.transpose(img_normalized, (2, 0, 1)) # HWC -> CHW img_batched = np.expand_dims(img_chw, axis=0) # CHW -> NCHW return img_batched def postprocess(self, outputs, img_shape): # outputs: [1, 84, 8400] 对于YOLOv8 # 这里需要实现YOLOv8的后处理:解码边界框,应用置信度阈值和NMS # 具体实现涉及矩阵操作,篇幅所限不展开,可参考Ultralytics官方代码 # 最终返回一个列表,每个元素为 [x1, y1, x2, y2, conf, cls_id] detections = [] # ... (后处理逻辑) return detections def detect(self, img): input_tensor = self.preprocess(img) outputs = self.session.run([self.output_name], {self.input_name: input_tensor}) detections = self.postprocess(outputs[0], img.shape[:2]) return detections # 使用示例 if __name__ == "__main__": detector = YOLOv8Detector("best.onnx") cap = cv2.VideoCapture(0) # 打开摄像头 while True: ret, frame = cap.read() if not ret: break detections = detector.detect(frame) # 在frame上绘制检测框... # 根据检测结果,生成控制指令(例如,计算目标中心位置,映射为舵机角度) # 指令生成逻辑... # 通过串口发送指令给STM32 cv2.imshow("Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()
注意:上述代码中的后处理(
postprocess)是YOLO推理的核心和难点之一。YOLOv8的输出格式需要正确解析。强烈建议你参考Ultralytics官方仓库中的utils/ops.py文件,将其中的non_max_suppression等函数移植过来,或者直接使用ultralytics包(如果能在Jetson上成功安装)进行推理,那样最简单。为了性能和简化,我最终选择将后处理逻辑用C++实现并编译成Python扩展,但这属于进阶优化。
- 性能调优:
- 首次运行慢:ONNX Runtime + TensorRT Provider在第一次推理时会构建TensorRT引擎并优化,耗时较长(可能几十秒)。构建完成后,引擎会缓存下来,后续推理就很快了。可以将预热(跑几次空白推理)放在初始化阶段。
- FP16精度:在Jetson Nano上,使用FP16(半精度浮点数)可以显著提升推理速度,且对精度损失很小。可以在创建
InferenceSession时通过provider_options参数启用。providers = [('TensorrtExecutionProvider', {'trt_fp16_enable': True, 'trt_engine_cache_enable': True, 'trt_engine_cache_path': './trt_cache'})] - 输入尺寸:确保推理脚本中的输入尺寸与导出ONNX时的
imgsz参数一致。
4.3 从检测结果到控制指令
模型输出了检测框,我们需要将其转化为STM32能理解的舵机角度指令。这需要一些简单的数学计算。
假设我们的场景是:摄像头固定在机械臂上方,舵机控制机械臂末端执行器在二维平面上移动(例如,一个XY平台)。我们需要将检测到的目标中心像素坐标(cx_pixel, cy_pixel)映射为舵机X和舵机Y的角度。
坐标映射:这是一个简单的线性映射。首先,你需要标定摄像头视野范围与实际物理位置的对应关系。
- 已知:图像宽度
W_img,高度H_img。 - 已知:舵机X的物理运动范围对应像素范围
[X_pixel_min, X_pixel_max],舵机Y同理。 - 已知:舵机X的角度范围
[Angle_X_min, Angle_X_max](例如0-180度),舵机Y同理。
那么,对于检测到的目标中心
cx_pixel:angle_x = Angle_X_min + (cx_pixel - X_pixel_min) / (X_pixel_max - X_pixel_min) * (Angle_X_max - Angle_X_min)同理计算angle_y。这里X_pixel_min/max需要你通过实际测量获得,比如让舵机转到最小和最大角度,分别记录下图像中某个固定参考点的像素位置。- 已知:图像宽度
指令生成:得到
angle_x和angle_y后,将其四舍五入为整数,按照之前定义的协议格式组装成字符串。def generate_command(servo_id, angle): # 简单限制角度范围 angle = max(0, min(180, int(angle))) cmd = f"${servo_id},{angle};\n" return cmd.encode('ascii') # 转换为字节串
5. Jetson Nano与STM32的串口通信实战
通信的稳定性和可靠性直接决定了整个系统是否可用。这里既有软件配置,也有硬件连接上的坑。
5.1 Jetson Nano端串口配置与编程
Jetson Nano的40针GPIO扩展接头上有多个串口,最常用的是/dev/ttyTHS1(UART1)。首先需要确保系统已启用该串口,且权限正确。
硬件连接:
- Jetson Nano Pin 8 (UART1_TX) -> STM32 USART1_RX 引脚
- Jetson Nano Pin 10 (UART1_RX) -> STM32 USART1_TX 引脚
- Jetson Nano Pin 6 (GND) -> STM32 GND切记:TX接RX,RX接TX,地线共地。
软件配置:
- 检查串口设备:
ls -l /dev/ttyTHS*。应该能看到/dev/ttyTHS1。 - 默认情况下,
ttyTHS1可能被系统控制台占用。我们需要禁用这个服务。sudo systemctl stop nvgetty sudo systemctl disable nvgetty - 修改串口权限,或者将当前用户加入
dialout组。sudo usermod -a -G dialout $USER # 然后需要注销重新登录生效 - 也可以每次用
sudo运行脚本,但不推荐。
- 检查串口设备:
Python串口通信代码: 使用
pyserial库,非常方便。pip3 install pyserialimport serial import time class STM32Communicator: def __init__(self, port='/dev/ttyTHS1', baudrate=115200, timeout=1): self.ser = serial.Serial(port, baudrate, timeout=timeout) if not self.ser.is_open: self.ser.open() print(f"Serial port {port} opened.") def send_command(self, servo_id, angle): cmd = f"${servo_id},{angle};\n" self.ser.write(cmd.encode('ascii')) print(f"Sent: {cmd.strip()}") def read_response(self): # 如果STM32有数据返回,可以在这里读取 if self.ser.in_waiting: response = self.ser.readline().decode('ascii', errors='ignore').strip() return response return None def close(self): self.ser.close() # 在主循环中集成 if __name__ == "__main__": comm = STM32Communicator() try: # 假设从检测逻辑中获得了目标角度 target_angle_x = 90 target_angle_y = 45 comm.send_command(1, target_angle_x) time.sleep(0.05) # 短暂延时,避免指令淹没 comm.send_command(2, target_angle_y) # ... 可以读取STM32的应答进行确认 except KeyboardInterrupt: print("Exiting...") finally: comm.close()重要提示:串口发送后,建议增加一个微小延时(如
time.sleep(0.02))。STM32的串口中断处理和指令解析需要时间,连续快速发送可能导致缓冲区溢出或指令丢失。对于舵机控制,几十毫秒的延时完全不影响效果。
5.2 STM32端串口接收与PWM生成
STM32端的代码相对独立,核心是串口中断服务程序和定时器PWM配置。
串口接收与解析(以HAL库为例):
// 定义指令缓冲区 #define CMD_BUF_SIZE 32 char cmd_buffer[CMD_BUF_SIZE]; uint8_t cmd_index = 0; bool cmd_received = false; // 串口中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uint8_t rx_char; HAL_UART_Receive_IT(&huart1, &rx_char, 1); // 重新开启接收中断 if (rx_char == '\n') { // 指令结束符 cmd_buffer[cmd_index] = '\0'; // 字符串结束符 cmd_received = true; cmd_index = 0; } else if (cmd_index < CMD_BUF_SIZE - 1) { cmd_buffer[cmd_index++] = rx_char; } else { // 缓冲区溢出,清空缓冲区 cmd_index = 0; memset(cmd_buffer, 0, CMD_BUF_SIZE); } } } // 在主循环中解析并执行指令 void parse_and_execute_command() { if (cmd_received) { cmd_received = false; // 解析指令 "$1,90;" int servo_id, angle; if (sscanf(cmd_buffer, "$%d,%d;", &servo_id, &angle) == 2) { // 验证ID和角度范围 if (servo_id >= 1 && servo_id <= NUM_SERVOS && angle >= 0 && angle <= 180) { set_servo_angle(servo_id - 1, angle); // 更新对应舵机角度 } } // 清空缓冲区 memset(cmd_buffer, 0, CMD_BUF_SIZE); cmd_index = 0; } }PWM配置与角度设置: 假设使用TIM3的通道1和2控制两个舵机。
// 初始化PWM void PWM_Init(void) { TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz -> 1us计数周期 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 20000 - 1; // 周期 20000us = 20ms (50Hz) htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 1500; // 初始脉宽1.5ms (90度) sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); } // 设置指定舵机角度 void set_servo_angle(uint8_t servo_idx, uint16_t angle) { // 将角度(0-180)转换为脉宽(500-2500us) // 注意:不同舵机范围可能略有差异,需校准 uint16_t pulse_width = 500 + angle * (2000 / 180); // 线性映射 // 确保脉宽在安全范围内 pulse_width = (pulse_width < 500) ? 500 : ((pulse_width > 2500) ? 2500 : pulse_width); switch(servo_idx) { case 0: __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse_width); break; case 1: __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, pulse_width); break; default: break; } }
5.3 通信稳定性保障与调试技巧
在实际联调中,通信问题是最常见的。
共地与电平匹配:务必确保Jetson Nano和STM32的GND连接在一起。Jetson Nano的GPIO是3.3V电平,大多数STM32也是3.3V,所以电平匹配。如果是5V的STM32(如某些老型号),则需要电平转换模块,否则可能损坏Jetson Nano的GPIO。
波特率一致:双方设置的波特率必须完全相同。常用的有9600, 115200, 921600等。115200在稳定性和速度之间是个好平衡。
硬件流控:如果通信数据量较大或环境干扰强,可以考虑启用RTS/CTS硬件流控。但我们的指令很短,通常不需要。
软件纠错:
- 添加校验和:在指令协议中加入校验和,例如
$1,90,78;\n,其中78是前几个字节的累加和。STM32端收到后重新计算并比对,不一致则丢弃。 - 增加应答机制:STM32收到有效指令后,回传一个确认帧,如
ACK1\n。Jetson Nano端如果在一定时间内没收到应答,则重发指令。这能显著提升可靠性。 - 指令队列与去重:Jetson Nano端不要每帧图像都发送新指令。可以设置一个角度死区(例如,目标角度变化小于2度时不发送),或者以固定频率(如10Hz)发送最新指令,避免串口拥堵。
- 添加校验和:在指令协议中加入校验和,例如
调试方法:
- 独立测试:先用USB转TTL模块和PC上的串口调试助手,分别测试Jetson Nano的发送和STM32的接收解析是否正常。
- 打印日志:在STM32端,可以将解析到的指令ID和角度通过另一个串口(或重映射到同一个串口的TX)打印出来,用调试助手查看,这是最直接的调试手段。
- LED指示:在STM32板上加一个LED,收到指令时闪烁一下,可以快速判断通信是否发生。
6. 系统集成、测试与性能优化
当各个模块都调通后,将它们集成在一起,并优化整体性能与稳定性。
6.1 集成与主循环设计
Jetson Nano上的主程序需要将摄像头采集、推理、指令生成、串口发送等环节串联起来,形成一个稳定的循环。
import cv2 import time from your_detector_module import YOLOv8Detector from your_serial_module import STM32Communicator def main(): # 初始化 detector = YOLOv8Detector("best.onnx") comm = STM32Communicator('/dev/ttyTHS1', 115200) cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 控制频率 inference_fps = 10 # 目标推理帧率 frame_interval = 1.0 / inference_fps last_time = time.time() # 角度滤波(减少抖动) last_angle_x = 90 last_angle_y = 90 filter_factor = 0.3 # 一阶低通滤波系数 print("System started. Press 'q' to quit.") while True: current_time = time.time() if current_time - last_time < frame_interval: time.sleep(0.001) # 短暂休眠,降低CPU占用 continue last_time = current_time # 1. 捕获帧 ret, frame = cap.read() if not ret: print("Failed to grab frame") break # 2. 推理 detections = detector.detect(frame) # 3. 指令生成(假设只处理第一个检测到的目标) target_angle_x, target_angle_y = last_angle_x, last_angle_y # 默认保持上次角度 if detections: # 假设detections[0]包含[x1,y1,x2,y2,conf,cls] box = detections[0] cx = (box[0] + box[2]) / 2 cy = (box[1] + box[3]) / 2 # 坐标映射到角度 (这里需要你的标定参数) raw_angle_x = map_pixel_to_angle(cx, 'x') raw_angle_y = map_pixel_to_angle(cy, 'y') # 低通滤波,使运动更平滑 target_angle_x = last_angle_x * (1-filter_factor) + raw_angle_x * filter_factor target_angle_y = last_angle_y * (1-filter_factor) + raw_angle_y * filter_factor last_angle_x, last_angle_y = target_angle_x, target_angle_y # 4. 发送指令 comm.send_command(1, int(target_angle_x)) time.sleep(0.02) # 指令间延时 comm.send_command(2, int(target_angle_y)) # 5. 可视化(可选,会消耗资源) # draw_detections(frame, detections) # cv2.imshow('Demo', frame) # if cv2.waitKey(1) & 0xFF == ord('q'): # break # 清理 cap.release() cv2.destroyAllWindows() comm.close() if __name__ == "__main__": main()关键设计点:
- 固定频率循环:使用
time.time()控制主循环频率,避免无节制地运行导致CPU占用率100%和帧率不稳定。 - 角度滤波:对计算出的角度进行一阶低通滤波(
filter_factor越小越平滑),可以极大减少因检测框抖动导致的舵机高频颤动,让运动更柔和。 - 资源管理:可视化(
cv2.imshow)非常消耗资源,在最终部署时可以关闭,通过SSH或无头模式运行。
6.2 性能瓶颈分析与优化
在Jetson Nano上跑实时AI应用,性能永远是关注焦点。你需要知道时间花在哪里。
性能剖析:使用Python的
time模块或更专业的cProfile来测量各阶段耗时。import time start = time.time() # ... 执行代码段 end = time.time() print(f"阶段耗时: {end-start:.3f}s")通常,耗时排序为:模型推理 > 图像预处理/后处理 > 串口通信。
推理优化:
- 使用TensorRT FP16:如前所述,这是最有效的提速手段,通常能带来1.5-2倍的提升。
- 调整输入尺寸:将模型输入从640x640降到320x320,速度会快很多,但精度会下降。需要权衡。
- 使用更轻的模型:如果YOLOv8n还是慢,可以尝试NanoDet或自己设计的超轻量网络。
- 批处理:如果一次处理多帧图像,TensorRT的利用率会更高。但对于实时视频流,这通常不适用。
预处理/后处理优化:
- 使用GPU加速:OpenCV的一些操作(如
cv2.resize,cv2.cvtColor)可以通过cv2.cuda模块或使用CUDA版的PyTorch张量操作来加速,但会引入额外复杂度。 - 简化后处理:如果你的检测目标单一且固定,可以简化NMS和非极大值抑制的逻辑,甚至用一些启发式规则代替。
- 使用C++扩展:将最耗时的后处理部分用C++实现,并编译为Python扩展模块,可以显著提升速度。这是进阶优化手段。
- 使用GPU加速:OpenCV的一些操作(如
系统级优化:
- 设置Jetson Nano运行模式:使用
sudo nvpmodel命令可以切换功耗模式。nvpmodel -m 0是最大性能模式(10W),-m 1是5W模式。在供电充足的情况下,使用模式0。 - 关闭图形桌面:如果通过SSH操作,可以关闭桌面环境以释放更多CPU和内存资源。
sudo systemctl set-default multi-user.target然后重启。 - 使用
jetson_clocks脚本:这个工具可以强制让CPU和GPU运行在最高频率,避免动态调频带来的延迟波动。sudo jetson_clocks。
- 设置Jetson Nano运行模式:使用
6.3 稳定性与鲁棒性提升
一个演示成功的系统和一个能长期稳定运行的系统之间,还有很大距离。
异常处理:
- 摄像头断连:在
cap.read()失败后,尝试重新初始化摄像头或等待一段时间重试。 - 串口断开:在
serial.Serial初始化或发送失败时,捕获异常,记录日志,并尝试重新打开串口。 - 模型推理失败:如果ONNX Runtime抛出异常,可以尝试重新加载模型,或者降级到使用一个简单的备用逻辑(如让舵机回到安全位置)。
- 摄像头断连:在
看门狗与自恢复:
- 可以编写一个简单的看门狗脚本,定时检查主程序是否在运行,如果卡死则重启它。
- 在STM32端,也可以启用硬件看门狗(IWDG),防止程序跑飞。
电源管理:
- Jetson Nano供电:务必使用官方推荐的5V4A电源。供电不足会导致系统不稳定、随机重启,这是最常见的问题之一。
- 舵机供电隔离:舵机在启动和堵转时电流很大,会产生电源噪声,可能干扰Jetson Nano和STM32。强烈建议为舵机单独供电,并与控制板的电源地线共地。在舵机电源线上并联一个大电容(如1000uF)也可以吸收电流冲击。
机械结构考量:
- 舵机有最大扭矩限制,不要让它在极限位置长时间堵转,否则容易烧毁。
- 为机械臂的运动范围设置软件限位,防止超程损坏结构。
- 在STM32代码中,可以加入角度渐变函数,让舵机平滑运动到目标位置,而不是瞬间跳变,这对机械结构和舵机寿命都有好处。
从准备数据集到模型部署,再到跨设备通信控制,这个项目就像一次微缩版的机器人系统开发。它涉及软件、硬件、算法、调试等多个方面。最大的体会是,“跑通”只是第一步,让系统“跑稳”、“跑好”需要花费数倍的时间去处理边界条件、优化性能和提升鲁棒性。例如,串口通信中那个微小的延时time.sleep(0.02),就是经历了多次指令丢失后才加上的;角度滤波的参数也是反复调试才找到既能快速响应又不抖动的平衡点。
如果你也打算做类似的项目,我的建议是分而治之,逐步集成。先把Jetson Nano的推理Demo跑起来,再用串口调试助手测试STM32控制舵机,最后把两者连起来调通信。每步都确保稳定了,再往下走。遇到问题,多用打印日志、LED指示灯这种最直接的方法来定位。这个过程虽然繁琐,但当你看到机械臂随着你手中的物体流畅移动时,那种成就感绝对是值得的。
本文还有配套的精品资源,点击获取