简介:这是一套面向嵌入式AI初学者与课程设计者的端侧智能控制实战项目资源,聚焦Jetson Nano与STM32协同完成图像识别→指令下发→舵机精准响应的完整闭环。资源覆盖从数据集采集标注、YOLOv5s模型训练、TensorRT加速优化,到STM32通过UART/I2C接收Jetson指令并驱动舵机转动的全链路实现,适用于毕业设计、大创竞赛、工程实训等实践场景。压缩包共111个文件(142.82MB),含38个C源码与40个头文件构成的STM32底层驱动工程(涵盖TIM、USART、ADC、I2C等关键外设),3个Paddle Lite模型文件(model、params、pdiparams)及配套YAML配置、Python部署脚本、Keil工程(uvprojx/uvoptx)和实操演示MP4视频。已有1339人学习下载,所有代码经真机测试可直接烧录运行,附带keilkill.bat一键清理脚本与详细README说明,大幅降低复现门槛。
1. 项目整体思路与系统架构设计
做这个项目的起因其实很直接:我一直想搞一个能“看懂”物体并做出响应的桌面级机械臂,但市面上的成品方案要么是纯视觉但控制封闭,要么是可编程但完全没有AI能力,两者能打通并且留有足够二次开发空间的方案几乎没有。所以我决定自己搭一套:用Jetson Nano当“大脑”负责跑深度学习模型做目标检测,再用STM32当“小脑”负责实时控制舵机。这套组合做完之后,功能上完全可以扩展成视觉抓取、人脸追踪云台、巡检小车等,而且每一个环节都是可以独立复用的。
1.1 核心需求解析
先说这个项目到底解决了什么问题。单独用Jetson Nano去控制舵机不是不行,但实时性很难保证——Jetson Nano跑的是Linux系统,进程调度、Python解释器开销、USB转舵机控制板驱动延迟,这些加在一起会让舵机响应出现几十到几百毫秒的抖动。而单独用STM32去接摄像头做目标检测,算力又完全不够,跑个轻量级模型都勉强。
所以架构上最合理的分工是:Jetson Nano只负责“看见”和“思考”,也就是摄像头采集、模型推理、目标坐标解算;STM32只负责“行动”,也就是解析指令、生成PWM波形、驱动舵机转到位。两者之间用串口UART通信,数据量小、协议简单、实时性有保证。这个思路在很多实际产品里都是这么做的,比如扫地机器人的视觉导航模块和底盘驱动模块就是分离的。
1.2 方案选型与硬件搭配
选Jetson Nano而不是树莓派,核心原因是CUDA生态。Jetson Nano带128个Maxwell架构的CUDA核心,可以跑TensorRT加速的深度学习模型,推理速度比树莓派的CPU硬扛快一个数量级。2024年之后官方主推的是Jetson Orin Nano系列,性能更强,但如果你手头是老的Jetson Nano 4GB版本,做这个项目也完全够用。
STM32我选的是STM32F103C8T6,也就是大家常说的“蓝丸”核心板。选它不是因为性能强,而是因为资料多、便宜、舵机控制这种任务它对时序的要求完全不虚。F103的主频72MHz,定时器分辨率足够输出平滑的PWM波形,而且HAL库和标准库的代码示例网上应有尽有。如果你对实时性有更高要求,换STM32F405或者G431也行,但项目逻辑不用改。
舵机这块,普通的SG90舵机(9g舵机)扭矩小,适合做云台这种轻负载;如果计划后面挂机械臂,建议上MG996R或者MG995,扭矩大不少,但要注意电流需求更大,必须独立供电。我后来在项目中就是先用SG90做验证,稳定后才换成MG996R,这个顺序能省掉很多排查故障的时间。
2. 数据集准备与深度学习模型训练
这个项目的数据集准备是很多人容易忽略的环节。很多新手上来就直接下载一个公开数据集,然后训个模型就开始部署,结果一到自己的实际场景里准确率暴跌。核心原因很简单:你的摄像头角度、光照、目标物体外观和训练集差异太大。所以我采用的是“自采数据+公开数据混合”的策略,目标放在识别几个固定颜色的物体上,这样数据集既好做,模型也好收敛。
2.1 数据采集与标注实操
数据采集我建议直接用Jetson Nano的CSI摄像头接口配合OpenCV来完成,不要用USB摄像头,因为CSI接口的延迟和CPU占用都更低。采集时不要只正对着拍,要模拟实际部署时可能出现的角度变化:俯视、侧视、远近、顺光、逆光,各种组合都要覆盖。我当时采集了大约800张图片,涵盖了红、绿、蓝三种颜色的方块在不同位置、不同光照下的情况,这些图片分到训练集、验证集、测试集的比例大约是7:2:1。
标注工具我推荐用LabelImg,它虽然界面老一点,但是生成的是Pascal VOC格式的XML文件,后续转成YOLO格式非常方便。如果标注的目标数量多,可以试试LabelStudio或者X-AnyLabeling这类半自动辅助工具。不过我个人的经验是,几百张图片的量级下手工标注反而更快,因为你不需要花时间去配置模型辅助标注的环境。标注的时候有一条经验:边界框紧贴目标边缘就行,不要把背景框进去太多,否则模型会学到不必要的背景特征。
2.2 模型选型与训练参数
模型这块我对比过两个方向:一个是YOLOv5n/YOLOv8n这类目标检测模型,另一个是MobileNetV3+SSD这种轻量级检测模型。实际测试下来,YOLOv8n在Jetson Nano上的TensorRT FP16精度下,推理延迟在30ms左右,检测精度比MobileNet SSD高不少,而且训练和导出的工具链更成熟。所以最终选型就锁定了YOLOv8n。
训练参数直接抄作业的话,可以这样设:输入分辨率640x640,batch size 16,epochs 100,优化器SGD,初始学习率0.01,权重衰减0.0005。这里有个容易踩的坑:如果数据量只有几百张,epochs不要开太大,50到100就能收敛,开300个epochs很容易过拟合,模型在训练集上表现很好但一到真实场景就乱识别。判断过拟合的一个直观方法就是观察训练日志里验证集mAP的曲线,如果验证集mAP在某个epoch后不再上升甚至下降,而训练集loss还在降,基本就是过拟合了。
数据增强是提升模型泛化能力的关键一步,我强烈建议在训练时打开YOLO自带的增强项,包括HSV色域扰动、随机翻转、平移缩放。尤其是HSV色域扰动,它对光照变化非常有效,因为实际部署时不可能保证每次光照都一样。我试过关闭增强和打开增强两组对比实验,实测在逆光场景下,打开增强的模型误检率下降了约40%。
2.3 训练结果与精度验证
整个训练过程我用了大概40分钟(用一张消费级显卡),最终在验证集上的mAP50能达到0.97以上。不过mAP只是一个参考,真正要看的还是部署到Jetson Nano之后在实际场景的检测效果。测试的时候我特意选了模型没怎么见过的角度和光照条件,发现确实会有一些漏检,这也是为什么我建议大家尽量在部署环境里做数据采集,而不是在电脑上随便找图片。
训练完成之后会得到最好的权重文件best.pt,这个文件不能直接部署到Jetson Nano上跑,除非你想忍受每帧几百毫秒的延迟。接下来就必须走模型转换和TensorRT优化这一步。
3. Jetson Nano端模型部署与推理优化
模型部署是整个项目里最容易卡壳的环节,因为环境配置牵扯到JetPack版本、PyTorch版本、CUDA版本、TensorRT版本之间的兼容性。Jetson Nano的JetPack 4.6.1自带TensorRT 8.2和CUDA 10.2,但如果你按照普通电脑上的pip命令去装PyTorch,百分之百装不上,因为Jetson Nano是ARM架构。正确的方式是去NVIDIA官网的论坛下载对应JetPack版本的PyTorch轮子文件(.whl),然后本地pip安装。
3.1 环境搭建与依赖安装
基础环境这样装:首先确认JetPack版本,直接在终端里运行cat /etc/nv_tegra_release,看到版本号之后去NVIDIA的官方索引下载对应的PyTorch。我用的是PyTorch 1.8.0版本,对应的torchvision是0.9.0,这两个版本需要严格匹配。然后装ultralytics库用于加载YOLOv8模型,再装onnx和onnxruntime-gpu用于模型导出和验证。这里提醒一句:如果不是必要,不要在Jetson Nano上直接跑训练,它的4GB内存根本不够折腾。
模型导出我推荐走YOLOv8n.pt -> ONNX -> TensorRT Engine这条链路。先用ultralytics库把pt导出为ONNX格式,命令是yolo export model=best.pt format=onnx opset=12。导出的时候注意输入尺寸固定为640x640,后面转TensorRT也要保持一致。然后使用trtexec工具把ONNX转为TensorRT引擎文件(.engine),命令是:
/usr/src/tensorrt/bin/trtexec --onnx=best.onnx --saveEngine=best.engine --fp16加上--fp16参数能启用半精度推理,速度几乎翻倍,而精度损失在目标检测任务里基本可以忽略。实测下来,FP16的TensorRT引擎推理速度大约是FP32的1.8倍,对项目来说非常关键。
3.2 推理程序设计与坐标解算
有了TensorRT引擎之后,需要自己写推理代码。推荐用Python的pycuda配合TensorRT的Python API来加载引擎并进行推理,网上有很多现成的YOLOv5/v8的TensorRT推理脚本可以改。核心逻辑分三步:读取摄像头帧、预处理到640x640、执行推理并解析输出。
预处理时注意要做letterbox处理,也就是把图像等比缩放到640x640并在四周填充灰边,而不是直接拉伸,否则目标形状会变形导致检测精度下降。推理输出是一个多维数组,需要通过后处理解析出检测框坐标、置信度和类别ID。后处理代码里要重点关注的三个参数是置信度阈值、NMS的IoU阈值和锚框尺寸,我实际用下来置信度阈值设为0.45、NMS IoU阈值设为0.45效果最好。
坐标解算是把检测框从640x640的缩放坐标系映射回原始画面坐标系。因为后面要控制舵机云台追踪物体,所以还需要把检测框的中心点转换成云台的偏航和俯仰角度。这一步我建议在代码里增加一个平滑滤波,比如用一个简单的低通滤波器或者移动平均值,避免检测框抖动导致舵机来回摆动。实际测试中,不加滤波时舵机明显在高频抖动,加了滤波之后转动就非常平滑了,这是提升体验感的一个关键点。
3.3 模型部署踩坑记录
部署过程中的坑主要集中在三点:一是ONNX导出时如果用的是高版本ultralytics库,可能会遇到算子不兼容的问题,解决办法是降低opset版本或者手写部分算子替换;二是TensorRT引擎构建时显存不足,4GB版本的Jetson Nano确实比较容易爆显存,解决办法是关闭桌面环境释放显存,或者在trtexec命令里加上--maxWorkspaceSize=1GB限制工作空间;三是推理时GPU利用率很低,帧率却不高,这通常是因为预处理和后处理都在CPU上执行,形成了瓶颈,可以试试用cv2.dnn或torchvision的GPU张量操作来加速。
这些都是实战中非常容易卡住的地方,我整理成一张速查表放在文末,方便大家直接对照排查。
4. Jetson Nano与STM32通信协议设计
到了通信这一步,就要把“大脑”和“小脑”打通了。Jetson Nano的GPIO里有一组UART(默认是ttyTHS1),STM32也有USART外设,两者连接直接用TX接RX、RX接TX、GND共地就行。这里最容易犯的错误就是TX接TX、RX接RX,结果完全通不了,这个我在第一次接线时也犯过。因为两边都默认数据引脚是输出,信号就对顶了。
4.1 串口通信参数与硬件连接
Jetson Nano侧的UART默认波特率是115200,但要注意Jetson Nano的GPIO UART默认被控制台占用了,需要先禁用控制台功能才能使用。步骤是:sudo nano /etc/nv-l4t-usb-device-mode-runtime.service,把相关控制台配置注释掉,或者用systemctl停掉nvgetty服务。STM32侧我用的USART1,配置为115200、8N1(8位数据、无校验、1位停止位),这个配置要和Jetson Nano完全一致,不然收发的数据全是乱码。
硬件连接上,Jetson Nano的UART电平是3.3V,STM32的USART也是3.3V电平,所以可以直接连接,不需要电平转换。但如果是用Jetson Nano的USB转TTL模块(比如CP2102)连接STM32,那就要注意USB转TTL模块的输出电平是否匹配,部分模块是5V电平,需要确认支持3.3V跳线设置。此外,还有一个非常容易被忽视的点:两边必须共地,否则串口通信会因为参考电平不一致而出现随机乱码。很多人通信不稳定,排查半天最后发现就是地线没接。
4.2 通信协议帧格式定义
串口通信的难点不在于收发,而在于数据解析的稳定性。因为串口是字节流,没有严格的帧边界,如果接收方不知道一帧数据从哪里开始、在哪里结束,解析就会错乱。所以必须定义一套协议帧格式。
我设计的帧格式如下:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 |
| 1 | 0x55 | 帧头确认 |
| 2 | 0x01 | 数据长度(从第3字节到倒数第2字节的字节数) |
| 3 | 0x01 | 命令字(0x01表示舵机角度控制) |
| 4 | 高字节 | 舵机通道号 |
| 5 | 低字节 | 目标角度值(0-180) |
| 6 | 0x0D | 校验和(前面所有字节的累加和取低8位) |
| 7 | 0x0A | 帧尾 |
这个格式看起来简单,但设计上包含了几个关键点:帧头用两个固定字节(0xAA 0x55)来减少误判概率;长度字段让接收方知道要读多少个数据字节;校验和用于检测数据是否在传输过程中出错;帧尾用来确认一帧数据的结束。实际调试中我发现,校验和非常重要,因为Jetson Nano的USB转串口在某些高负载情况下偶尔会丢字节,如果没有校验,舵机就可能接收到错误的角度值而乱转。
4.3 双端代码实现思路
Jetson Nano侧用Python的pyserial库发送指令,代码逻辑很简单:先根据检测到的目标中心坐标计算出舵机应该转到的角度,然后按帧格式打包发送。一个需要注意的细节是:发送频率不要太高,10-20Hz就足够了,舵机物理上也不可能响应更快的指令,反而会增加总线负担和抖动概率。
STM32侧用HAL库的串口接收中断来实现。我强烈建议不要用简单的阻塞式接收,而是使用HAL_UART_Receive_IT()配合空闲中断(IDLE line interrupt)来接收不定长数据。这样STM32在接收到一帧完整的数据后才会触发处理逻辑,避免了逐字节解析的状态机复杂度。如果用的是标准库,也可以开启USART_IT_RXNE和USART_IT_IDLE来实现同样的效果。
接收数据的处理逻辑就两步:第一步循环查找帧头,找到0xAA 0x55之后才认为有效帧开始;第二步校验长度和校验和,校验通过则解析出角度值并更新PWM占空比。如果校验失败,直接把当前缓冲区清空重新找帧头就行。这种状态机处理方式在实际项目中非常可靠,我在连续运行24小时的测试中没有出现过一次解析错误。
5. STM32端PWM舵机控制实现
舵机控制的本质就是产生特定占空比的PWM波形。市面上绝大多数舵机(SG90、MG996R等)都是模拟舵机,控制信号是50Hz的PWM,也就是周期20ms,其中高电平时间在0.5ms到2.5ms之间变化,对应舵机角度范围约0度到180度。也就是说,高电平1.5ms时舵机在中间位置90度,0.5ms时在0度,2.5ms时在180度。
5.1 定时器PWM配置详解
STM32F103的定时器能够直接输出PWM波形,不需要CPU持续干预。我用的是TIM2的通道1(PA0引脚),配置为PWM模式1,输出频率50Hz。具体配置时要注意几个参数的计算:定时器时钟是72MHz,分频器(PSC)设为71,那么定时器计数频率就是1MHz,也就是每个计数周期1微秒;自动重装载值(ARR)设为19999,那么PWM周期就是20000微秒,即20ms,正好是50Hz。
这里重点说一下角度和占空比的换算。因为一个PWM周期是20000微秒,而能控制的占空比部分只有2000微秒(0.5ms到2.5ms),对应0到180度,所以每度大约对应11.1微秒。换算公式是:
CCR = 500 + (angle / 180.0) * 2000
其中500对应0.5ms(0度),2000对应2.5ms(180度)。这个公式我建议直接写在代码注释里,方便后续调整。如果用的是180度舵机,角度范围就是0到180;如果用的是360度连续旋转舵机,则角度控制变成速度控制,那就是另一种玩法了。
5.2 舵机供电与电源设计
舵机供电是整个项目里最容易出问题的地方,没有之一。Jetson Nano和STM32还可以勉强用同一个5V电源,但舵机绝对不能直接挂在开发板的5V引脚上。普通SG90堵转时电流可能到500mA到800mA,MG996R堵转电流甚至能到2A以上。开发板的稳压器根本扛不住这么大的瞬态电流,一拉电流电压就掉,轻则舵机无力抖动,重则开发板直接重启。
我的做法是给舵机单独供电:用一个5V 5A的直流电源适配器,正极接舵机的红线,负极接舵机的棕线,同时把电源负极和STM32的GND连接在一起。信号线直接从STM32的PA0引出。这样电源回路和控制信号回路就完全隔离了,舵机的瞬态大电流不会拉垮控制板的电压。如果你没有5V开关电源,用3节18650锂电池串联加降压模块也行,但一定要保证电流输出能力大于2A。
另外还有一个小细节:舵机电源的两端要加一个470uF以上的电解电容和一个0.1uF的陶瓷电容,用于吸收舵机瞬间启停产生的尖峰电压。我在做这个项目之前,第一次直接裸奔跑舵机,结果发现STM32偶尔会复位,加上电容之后这个问题再也没有出现过。选型电容的时候注意耐压值要大于5V,一般选10V或16V的电解电容就行。
5.3 平滑转动与限位保护
舵机在接收到角度指令后,如果直接从当前位置跳到目标位置,速度过快会产生很大的冲击力,尤其是当舵机带动机械臂或云台时,这种冲击会严重影响机械结构的寿命。所以我加了一个增量步进算法:每次控制周期(比如每10ms)让舵机角度只移动一小步(比如1到2度),直到到达目标角度。这个效果非常明显,舵机转动变得非常顺滑,机械结构的晃动也大大减小。
限位保护同样重要。如果模型误检或者通信错误,导致STM32收到一个超出范围的90度以外角度,直接去执行的话可能会让舵机损坏或者机械臂撞到限位块。所以我在解析出角度后加了一个钳位检查,只允许0到180度范围内的值生效,超出范围就丢弃并保持当前角度。另外我还在代码里保存了一个“上次有效角度”变量,如果一帧数据校验失败,就直接沿用上次的角度,避免舵机因未知错误乱动。
6. 常见问题与排查技巧实录
做这个项目的过程中,我前前后后踩了不少坑,这里挑几个最典型的整理出来。这些问题如果不提前预防,排查起来非常耗时,我希望能帮大家节省一些调试时间。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| Jetson Nano和STM32串口通信全是乱码 | 波特率不匹配;RX/TX接反;未共地 | 统一波特率;TX接RX、RX接TX;检查GND是否连通 |
| 串口一直接收不到数据 | Jetson Nano的UART被控制台占用 | 禁用nvgetty服务;重启后再次确认端口映射 |
| 舵机上电后抖动但不动 | 供电电流不足;PWM频率不对 | 舵机独立供电;确认PWM周期为20ms(50Hz) |
| 舵机能动但角度不准确 | 角度换算公式错误;PWM精度不够 | 检查CCR计算公式;确认定时器分频系数 |
| 模型推理速度很慢只有5帧 | 没有用TensorRT;没有启用FP16;CPU后处理瓶颈 | 转TensorRT引擎并加--fp16;优化预处理和后处理 |
| 检测框乱跳导致舵机抖动 | 没有加平滑滤波;检测置信度阈值太低 | 对目标中心坐标做低通滤波;提高置信度阈值到0.5以上 |
6.1 通信乱码与数据丢失的排查思路
通信乱码的排查要按顺序来:第一步用示波器或者逻辑分析仪看USART引脚的波形,确认电平是否正常;第二步用串口调试助手分别测试Jetson Nano和STM32各自的收发是否正常;第三步再把两边的地线用万用表测一下连通性。这样做的好处是把问题域一步步缩小,避免上来就改代码。我遇到过一种很奇怪的情况:Jetson Nano和STM32单独连电脑都正常,但互相连的时候偶发丢包,最后发现是USB转TTL模块质量差,影响了信号完整性,换了一根好线就好了。
6.2 舵机抖动与响应迟钝的调优方向
舵机抖动一般从两个方向排查:供电和PWM信号。先用万用表测舵机供电电压,如果电机转动时电压跌落超过0.3V,基本就是电源没跟上。排除电源问题之后再看PWM波形,用示波器看PA0引脚上的波形是否稳定、占空比是否准确。如果波形有点毛刺,可以在信号线上串联一个100欧姆电阻来抑制反射。响应迟钝的现象通常是PWM更新频率过低导致的,检查一下是不是定时器中断里做了太多其他事情,导致PWM占空比更新不及时。
6.3 整体联调中的注意事项
整个系统联调的时候,我的建议是分三步走,每一步都验证清楚了再进入下一步。第一步:先用串口调试助手给STM32手动发固定的角度指令,验证舵机控制是否正常;第二步:在Jetson Nano上写一个简单的测试脚本,循环发送一组角度序列,验证串口通信链路是通的;第三步:把摄像头检测、坐标解算、角度映射、舵机控制全部串起来,跑真实的追踪场景。这样每步出了问题都能快速定位,不会出现“不知道是检测错、通信错还是执行错”的情况。
如果最终发现整体响应延迟还是偏高,可以排查一下每个环节的耗时:Jetson Nano的模型推理通常需要20到30ms,串口发送几乎可以忽略,STM32的PWM更新是毫秒级,舵机本身的机械响应时间在100到200ms左右。所以整个系统的主要延迟瓶颈在舵机本身,而不是在计算和通信环节。如果要做追踪类应用,这个延迟是可以接受的;如果要做抓取类应用,建议在PID闭环或者更高速的舵机伺服方案上做文章。
我个人做完这整套项目后最大的体会是:Jetson Nano和STM32的配合,其实是一个很好的IoT边缘计算架构样板——高算力设备和实时控制设备各司其职,既不用让单片机去跑复杂的AI推理,也不用让Linux系统去干硬实时的活。这种“AI算力+MCU控制”的组合可以复用到非常多场景里,后续你还能往这个项目里加ROS通信、加视觉伺服闭环、加多舵机联动,扩展空间非常大。但无论往哪个方向扩展,这套数据到部署、通信到控制的链路都是地基,把它打扎实了,后面做什么都会顺手很多。
本文还有配套的精品资源,点击获取