简介:本资源是一个面向嵌入式AI初学者与课程设计者的端侧智能控制实战项目,聚焦Jetson Nano与STM32协同完成图像识别→指令下发→舵机精准响应的完整闭环。适用于毕业设计、大创竞赛、嵌入式实训及深度学习部署练手,尤其适合缺乏PCB设计经验但希望快速验证算法与硬件联动效果的学习者。压缩包共111个文件(142.82MB),涵盖STM32标准外设库C/H源码(如usart.c、tim.c、rcc.c等,支撑串口通信与PWM舵机驱动)、Jetson Nano端PyTorch模型导出的Paddle Lite可部署文件(model、params、pdiparams等)、Keil工程配置(uvprojx/uvoptx)、自动化脚本(bat/py)及说明文档(md)和演示视频(mp4)。已有1339人学习下载,提供可直接烧录运行的完整工程、清晰引脚连线指导、模型转换与部署关键步骤注释,以及作者全程技术答疑支持,显著降低嵌入式深度学习落地门槛。 我从一个有点“贪心”的想法开始讲这个项目:想做一个能识别物体、并根据识别结果自动控制机械臂/舵机转动的小系统。最开始只打算在电脑上跑跑检测,后来发现真正有意思的是把模型放到边缘设备上推理,再通过单片机去驱动舵机——这几乎把“AI算法”和“嵌入式控制”这两条线串了个遍。于是就有了这个标题里的完整链路:准备数据集 → 训练模型 → Jetson Nano部署 → 和STM32通信 → 控制舵机转动。
这个过程中踩的坑比想象中多得多,尤其是Jetson Nano的推理环境和STM32串口通信之间的配合。如果你也在做类似的“视觉+控制”入门项目,或者正想搞清楚Jetson Nano和STM32之间到底怎么协作,这篇文章应该能帮你省下两三个星期的折腾时间。
1. 项目整体设计与链路拆解
1.1 核心需求与系统架构思路
整个项目的需求看起来很简单:让摄像头识别出目标类别,然后把这个类别映射成舵机角度,驱动舵机转到指定位置。
但是细拆下来,它其实分成了三个相对独立的子系统:边缘AI推理子系统(Jetson Nano + 摄像头)、通信链路(Jetson Nano与STM32之间的数据交换)、舵机执行子系统(STM32 + 舵机 + PWM驱动)。
系统架构上,我选择的是“主从结构”:Jetson Nano作为上位机,负责图像采集、模型推理,并将推理结果按照自定义协议打包,通过UART串口发送给STM32;STM32作为下位机,负责解析串口数据、校验、执行PWM输出,从而控制舵机角度。
这个架构的好处是边界清晰。AI推理部分和实时控制部分彻底解耦,各自开发各自的,最后联调时只要对好通信协议就行。如果你把AI推理和舵机控制都塞到一个板子上,调试时会非常痛苦:图像处理延迟会直接影响舵机控制周期,舵机负载波动又可能反过来拖慢推理流程。
1.2 为什么选Jetson Nano和STM32,而不是其他组合
一开始我也考虑过树莓派,或者直接用STM32跑轻量级模型。最后选了Jetson Nano + STM32的组合,有几个非常现实的原因:
第一,Jetson Nano的优势是完整支持CUDA和TensorRT生态。树莓派虽然也能跑模型,但基本依赖CPU推理或者外围NPU,部署效率和推理速度都和Nano不在一个水平线。Jetson Nano原生的JetPack SDK自带TensorRT,可以把训练好的模型转换成高度优化的推理引擎,跑YOLOv5这类轻量目标检测网络能轻松到20~30 FPS。
第二,STM32的优势是实时性和PWM控制的可靠性。舵机控制需要精确的50Hz周期PWM信号,而STM32的硬件定时器输出PWM非常稳定,不占用CPU资源。相比之下,Jetson Nano直接输出PWM不仅不稳定,Linux非实时系统的调度延迟还会导致舵机抖动。所以“AI负责看,MCU负责动”是我认为合理且稳健的分工。
第三,从学习价值的角度看,这个组合覆盖了Linux部署、深度学习模型转换、串口协议设计、嵌入式外设驱动四大块,做完之后你对“AI到底怎么落地到真实设备”会有非常直观的理解。
1.3 软件链路总体流程
整个软件链路可以概括为一条流水线:
- 数据准备阶段:用摄像头采集目标物体的图片,标注或分类整理成数据集。
- 模型训练阶段:在PC或云端训练目标检测/分类模型,导出为ONNX格式。
- 模型转换阶段:在Jetson Nano上把ONNX转为TensorRT推理引擎(.engine文件)。
- 推理部署阶段:Jetson Nano循环读取摄像头帧,执行推理,得到识别结果。
- 命令生成阶段:根据识别结果查表映射成舵机角度,封装成通信帧。
- 串口发送阶段:通过UART向STM32发送数据帧。
- STM32解析阶段:校验数据帧、解析类别/角度指令。
- 舵机控制阶段:STM32产生PWM信号,控制舵机转到指定角度。
这个流水线里,每一步都有各自的坑,后面慢慢展开。
2. 数据集准备与模型训练要点
2.1 自己拍数据集:看似无聊却最关键的一步
我最初想偷懒,直接用网上公开的数据集训练,直接和想要的场景对不上。因为我的目标是让摄像头识别桌面上的几种物品(比如红球、蓝方块、手机、水杯),而公开数据集里的物体类别和视角跟我真实场景差距很大。模型在公开数据集上表现再好,换到我的摄像头视角下也会“翻车”。
所以最终我选择自己采集数据。把USB摄像头固定在一个三脚架上,模拟实际运行时的视角,然后对每个物体采集不同光照、不同角度、不同背景下的图片。这里有几个关键经验:
- 数量不需要太多,但分布要均匀。我每个类别拍了150~200张左右,一共5类,总计约900张。比动不动上万张的数据集少得多,但针对单一场景完全够用。
- 尽量模拟推理时的真实环境。如果你的摄像头是俯视的,训练图片就不要用平视视角;如果你的桌面是木纹的,训练图片里就要出现木纹背景。否则模型会学到错误特征。
- 加入一些负样本。我额外拍了一些“空桌面”和“手部入镜”的图片,减少误检和乱动的情况。这一点在实机测试中特别有用,不然模型很容易把背景或者其他杂物识别成目标。
采集完图片后,用LabelImg做目标检测标注,或者直接按文件夹整理成分类数据集,两种方案都可以。我的项目是检测后接控制,用了YOLOv5s模型做目标检测,所以做的是边界框标注。
提示:数据集的图片分辨率不需要太高,统一缩放到640x640再训练,否则训练时间成倍增加,部署时的预处理也会更麻烦。
2.2 模型选择与训练细节
如果只是控制一个舵机,检测和目标分类都能完成功能。分类模型更加轻量,检测模型更加鲁棒。我之所以选目标检测,是因为它可以同时输出物体位置和类别,以后想升级成“追踪物体并让舵机跟随”也比较容易。
模型方面,我选了YOLOv5s。虽然YOLOv5已经被更新的YOLOv8、YOLO11超越,但它仍然胜在生态成熟、资料多、转TensorRT的教程遍地都是,非常适合作为入门和落地项目。真正跑起来之后,YOLOv5s在Jetson Nano上转成TensorRT后大约能跑25~30 FPS,完全够用。
训练过程里几个值得注意的点:
- 我用了预训练权重YOLOv5s.pt做迁移学习,迭代100个epoch左右,batch size设为16,输入尺寸640x640。
- 训练平台直接用PC上的NVIDIA显卡。如果没有显卡,可以用云GPU,但数据上传下载会比较繁琐。
- 训练集、验证集按8:2划分,确认训练时没有数据泄漏。
- 最终训练出来的模型在验证集上mAP能达到0.95左右,这个精度对一个玩具级项目来说已经足够了。
2.3 模型转换为ONNX和TensorRT引擎
这是Jetson Nano部署里最容易卡壳的一步。YOLOv5官方仓库里的export.py脚本可以直接导出ONNX,但导出的ONNX不能直接用于TensorRT部署,还需要进一步转换。
我的转换流程是:
- 在PC上用
python export.py --weights best.pt --include onnx --opset 11导出ONNX文件。 - 把ONNX文件拷贝到Jetson Nano上。
- 使用TensorRT自带的
trtexec命令,或者写Python脚本调用TensorRT Python API,将ONNX转换为.engine文件。
关键的转换命令(Jetson Nano上执行):
/usr/src/tensorrt/bin/trtexec \ --onnx=best.onnx \ --saveEngine=best.engine \ --fp16 \ --workspace=1024这里用--fp16开启半精度推理,速度能翻倍。注意Jetson Nano的GPU是Maxwell架构,对FP16的支持虽然存在,但效率不如后来的Volta/Turing架构,不过仍然比FP32快不少。
注意事项:TensorRT版本和PyTorch导出的ONNX算子兼容性是个大坑。如果你的PyTorch版本太新,导出的ONNX里可能出现TensorRT不支持的算子。我最终固定了PyTorch 1.8 + TensorRT 8.2的组合,才稳定转换成功。换版本后记得先跑一个小模型验证环境是否正常。
3. Jetson Nano端:环境配置与推理部署实操
3.1 Jetson Nano刷机与环境配置
Jetson Nano的刷机需要另外一台Ubuntu主机(或者虚拟机),使用NVIDIA官方提供的SDK Manager进行刷机。这里有一个不算坑但容易让人困惑的点:Jetson Nano的SD卡版本和桌面版刷机方式不一样。
我用的是SD卡烧录方式,直接下载JetPack 4.6.1的镜像,用balenaEtcher写入128GB的SD卡,然后插到Jetson Nano上开机。在开机引导里设置好用户名密码后,系统就带好了CUDA、cuDNN、TensorRT、OpenCV等开发环境,不需要单独安装CUDA,这一点比PC上装深度学习环境省心不少。
环境配置的检查命令:
# 查看JetPack版本 cat /etc/nv_tegra_release # 查看TensorRT版本 dpkg -l | grep nvinfer # 确认CUDA可用 nvcc --version如果你用的是Jetson Orin Nano,刷机流程和JetPack版本会不一样,但整体思路是一样的。Orin Nano的算力比经典Nano强很多,尤其是支持更高效的FP16和INT8推理,如果预算允许,可以直接选Orin Nano起步。
3.2 TensorRT推理代码的完整骨架
部署阶段的代码核心就是把图片预处理、推理、后处理这三个环节串起来。这里给出一个简化但可用的TensorRT推理骨架(Python):
import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np import cv2 class TRTInference: def __init__(self, engine_path): self.logger = trt.Logger(trt.Logger.WARNING) with open(engine_path, "rb") as f: runtime = trt.Runtime(self.logger) self.engine = runtime.deserialize_cuda_engine(f.read()) self.context = self.engine.create_execution_context() self._allocate_buffers() def _allocate_buffers(self): self.inputs = [] self.outputs = [] for binding in self.engine: size = trt.volume(self.engine.get_binding_shape(binding)) * 4 dtype = trt.nptype(self.engine.get_binding_dtype(binding)) if self.engine.binding_is_input(binding): self.inputs.append(cuda.mem_alloc(size)) self.input_shape = self.engine.get_binding_shape(binding) else: self.outputs.append(cuda.mem_alloc(size)) self.output_shape = self.engine.get_binding_shape(binding) def infer(self, img): # 图像预处理:resize到640x640,归一化到0-1,并转换为CHW img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[None] # 拷贝输入并执行推理 cuda.memcpy_htod(self.inputs[0], np.ascontiguousarray(img)) self.context.execute_v2(bindings=self.inputs + self.outputs) # 取回输出 output = np.empty(self.output_shape, dtype=np.float32) cuda.memcpy_dtoh(output, self.outputs[0]) return output后处理部分需要根据YOLOv5的输出格式做解析。主要是三个部分:解析边界框坐标、做NMS(非极大值抑制)、把检测到的类别与置信度映射出来。
这里踩过最大的坑是输出张量的shape与Python广播不匹配,因为YOLOv5的ONNX输出是1x25200x85(1个批次、640x640下共有25200个候选框、85维向量是4个坐标+1个置信度+80个类别概率)。在解析时一定注意先用reshape改变维度再操作。
3.3 从推理结果到控制命令的映射
推理结果只是一组坐标和类别ID,要变成舵机控制指令,还需要加一层逻辑。我的做法比较简单直接:检测到不同类别时,舵机转向预设角度。
映射表大致长这样:
| 物体类别 | 对应角度 | 用途 |
|---|---|---|
| 红球 | 0° | 舵机左极限位 |
| 蓝方块 | 45° | 中偏左 |
| 手机 | 90° | 中间位置 |
| 水杯 | 135° | 中偏右 |
| 无目标 | 恢复90° | 回到中间 |
这些角度不是随便定的,而是根据实际机械结构校准出来的。比如舵机安装角度是水平0°,但机械臂的物理限位可能只能转0°~180°,具体映射值需要在联调时手动调整。
确定角度后,把它封装成一帧通信数据,通过串口发送给STM32。这里就引出了下一个关键环节:Jetson Nano和STM32之间的通信协议设计。
4. STM32端:通信协议与舵机控制实现
4.1 通信方案选型:为什么选UART
Jetson Nano和STM32之间的通信方式有几种选择:UART串口、I2C、SPI、USB虚拟串口,甚至网络TCP/UDP。
我最终选了UART串口,理由是:
- 实现简单,硬件连接只需要TX、RX、GND三根线。
- 能满足带宽需求。一帧指令大约8~10个字节,即使115200波特率下也只需要不到1ms的传输时间。
- 调试方便,串口数据可以直接用逻辑分析仪或USB-TTL模块抓取分析。
- 后续如果想加无线通信,串口转蓝牙/串口转WiFi模块都是现成的。
SPI虽然通信速率更高,但需要更多引脚,且从机端解析要更复杂;I2C速率低,且地址冲突等问题会让人头疼。USB虚拟串口则需要装驱动,不能即插即用。UART是性价比最高的选择。
4.2 自定义通信帧格式设计
通信协议最重要的是“可靠”和“易解析”。我设计了一帧8字节的简单协议:
| 字节序号 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 |
| 1 | 0x55 | 帧头校验 |
| 2 | data_len | 数据长度(本帧固定为4) |
| 3 | cmd | 命令号(0x01表示角度控制指令) |
| 4 | target_id | 舵机编号(多舵机时用) |
| 5 | angle_high | 角度高字节 |
| 6 | angle_low | 角度低字节 |
| 7 | checksum | 前面7个字节的累加和取低8位 |
这里两次帧头是为了降低误判概率。data_len字段用于支持不定长数据扩展,checksum用于简单的错误检测。
角度用两个字节(16位)表示,范围为0~1800,对应0.0°~180.0°(精度0.1°)。这样设计的好处是直接发送整数,避免串口传输浮点数带来的解析烦恼。
4.3 STM32串口接收与解析实现
STM32端的核心任务是把这8个字节可靠地接收下来并解析。
我用的是STM32F103C8T6(Blue Pill板),HAL库开发。串口接收采用了“空闲中断 + DMA”组合。这是一个值得多讲几句的配置,因为很多初学者只会用阻塞式接收HAL_UART_Receive,效果非常差。
空闲中断(IDLE)是指串口在一段时间内没有新数据进来时触发的中断。利用它,可以一次性把一帧完整数据接收完。搭配DMA的话,CPU不需要逐字节处理,接收过程由DMA自动搬运到内存缓冲区,完成帧接收后触发空闲中断。
关键代码如下:
#define RX_BUF_SIZE 128 uint8_t rx_buffer[RX_BUF_SIZE]; uint8_t rx_frame[8]; // 解析后的数据帧 void MX_USART1_UART_Init(void) { // 默认115200 8N1 huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; HAL_UART_Init(&huart1); // 开启DMA接收和空闲中断 HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); }中断服务函数里处理帧接收逻辑:
void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 计算本次接收长度 uint32_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 如果长度足够,复制到rx_frame并解析 if (len >= 8) { memcpy(rx_frame, rx_buffer, 8); parse_frame(rx_frame); } // 重新开启DMA接收 HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE); } }这里的关键点是:DMA接收是循环模式,所以每次空闲中断产生时,要立刻取出当前数据长度,然后复位DMA接收。否则下一帧数据会覆盖上一帧内容。
4.4 舵机PWM控制的核心参数与实现
舵机控制的原理并不复杂,核心是产生一个周期为20ms(50Hz)的PWM信号,通过改变高电平脉宽(通常0.5ms~2.5ms)来控制舵机角度。
对于市面上最常见的SG90舵机,脉宽和角度的关系大致是:
- 0.5ms脉宽 → 0°
- 1.5ms脉宽 → 90°
- 2.5ms脉宽 → 180°
STM32上使用定时器的PWM输出模式。比如用TIM2的CH1产生PWM,配置为:
// 假设TIM2时钟72MHz,预分频72-1 -> 1MHz计数频率,自动重载20000-1 -> 50Hz TIM_HandleTypeDef htim2; htim2.Init.Prescaler = 72 - 1; htim2.Init.Period = 20000 - 1; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;角度转PWM比较值的公式:
// pulse_width单位是us,PWM周期20000对应20ms // 角度angle范围0~180 uint32_t pulse_width = 500 + angle * 2000 / 180; // 0.5ms + (angle/180)*2ms uint32_t compare = pulse_width * 20; // 因为计数频率是1MHz,周期20000对应20ms,所以1us=20次计数 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, compare);这个公式里最容易错的是单位换算。计数器频率是1MHz,所以1us对应1000个计数,上面公式里写“1us=20次计数”是错误的,实际应该是:如果预分频后计数频率是1MHz,周期20000计数对应20ms;脉宽0.5ms对应500us,也就是500个计数。我当时在这里卡了很久,把预分频配成了72-1后,计数频率是1MHz,所以正确的比较值是pulse_width乘以1,即500~2500之间。
修正后的代码:
// 计数频率1MHz,周期20000对应20ms uint32_t pulse_us = 500 + angle * 2000 / 180; __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse_us);也就是比较值直接等于脉宽微秒数。如果你的预分频不同,需要重新换算。
注意:SG90这类舵机对供电很敏感。用STM32的3.3V引脚直接给舵机供电是不可行的,舵机一转动就会把电压拉低,导致STM32复位。正确做法是舵机电源单独用5V电源(比如USB的5V或DC-DC降压模块),且舵机电源地和STM32的地必须共地,否则PWM信号无参考电平,舵机会乱抖。
5. 联调实录与问题排查
5.1 第一个坑:推理延迟导致舵机跟不上
第一次联调时,我让Jetson Nano每帧都推理并发送指令,结果发现舵机动起来像“抽风”,角度跳来跳去,根本无法稳定对准。
排查后发现原因有两个:
- 模型推理+预处理+后处理的总耗时大约是50~80ms(FP16引擎),也就是每秒大约12~20帧。但舵机机械响应本身需要约300ms才能完成转动,如果每帧都发指令,舵机永远在执行“上一个指令”的中途。
- 检测结果的置信度波动会导致类别误判。某一帧检测到手机,下一帧漏检,再下一帧又检测到,角度就变成90°→恢复中间→90°这样抖动。
解决方案是加入“检测结果平滑”和“指令频率控制”:
# 只在置信度高于阈值且连续N帧为同一目标时才发送指令 if score > 0.5: if cls == last_cls and frame_count > 3: send_servo_command(cls_to_angle[cls]) else: frame_count += 1 else: detect_count = 0简单来说,连续5帧都识别出同一个类别才会发指令,这样指令频率降到大约2~3Hz,舵机动起来就平稳多了。
5.2 第二个坑:串口数据丢帧与粘包
Jetson Nano用Python的pyserial发送数据时,如果发送频率太快,STM32端偶尔会出现解析错误。后来我用逻辑分析仪抓了波形,发现UART数据本身没丢,但如果Jetson Nano把两帧数据连续发出,STM32的DMA缓冲会被一次性填满,空闲中断只在最后一帧结束后触发一次,结果一帧数据被当成了包含两帧内容的“粘包”。
解决办法是在STM32解析时增加状态机。每收到一个字节,就尝试匹配帧头0xAA 0x55,只有匹配成功后才开始填充数据,填满8字节后校验checksum,校验通过才执行控制动作。
关键状态机伪代码:
void parse_rx_byte(uint8_t byte) { switch (state) { case STATE_WAIT_HEADER1: if (byte == 0xAA) state = STATE_WAIT_HEADER2; break; case STATE_WAIT_HEADER2: if (byte == 0x55) state = STATE_RECEIVE_DATA; else state = STATE_WAIT_HEADER1; break; case STATE_RECEIVE_DATA: rx_frame[rx_index++] = byte; if (rx_index >= 6) { // data_len + cmd + target_id + angle + checksum if (check_checksum(rx_frame) == 0) { execute_servo_command(); } state = STATE_WAIT_HEADER1; rx_index = 0; } break; } }加入状态机之后,无论是粘包还是半包都能正确处理。
5.3 第三个坑:舵机抖动和供电不足
实机测试中常见的现象是:舵机在不该动的时候轻微抖动,或者转动到目标角度后一直发出嗡嗡声。
这个问题的根源基本在供电。SG90的工作电流在空载时大约是100mA,堵转时能到500mA以上。USB供电口或者STM32板上稳压芯片根本无法稳定提供这么大电流,电压跌落导致舵机控制信号抖动。
解决办法:用一节18650电池(3.7V)升压到5V,或者直接用一个5V 2A的独立电源模块给舵机供电。同时把舵机电源地、STM32电源地、Jetson Nano的USB转TTL模块地全部接在一起,确保参考电平一致。
5.4 常见问题速查表
| 问题 | 可能原因 | 排查方法 |
|---|---|---|
| Jetson Nano推理速度很慢 | 没有用TensorRT,或未启用FP16 | 确认加载的是.engine文件,并检查是否使用--fp16转换 |
| 舵机完全不转 | PWM频率不对或供电不足 | 用逻辑分析仪查看PWM波形,确认周期20ms、脉宽0.5~2.5ms |
| 串口收不到数据 | 波特率不一致 | 确认两端都是115200,8N1 |
| 舵机乱抖 | 供电电压跌落 | 换独立5V电源,共地 |
| STM32偶尔复位 | 舵机启动电流过大 | 加大电源电容(100uF以上),或单独给舵机供电 |
| 模型识别不稳定 | 训练数据场景与实际不一致 | 增加真实场景数据,降低置信度阈值,多次连续检测再判定 |
6. 常见进阶扩展方向与经验总结
6.1 后续可以往哪些方向扩展
这个项目的框架一旦跑通,扩展空间其实是很大的。我自己做完之后,发现至少有三个方向很值得继续做下去:
第一,把单舵机扩展成多舵机机械臂。Jetson Nano通过扩展协议里的target_id字段,可以控制多个舵机,甚至实现简单的“视觉引导抓取”。只要协议设计时预留了舵机编号,代码改动并不大。
第二,把目标检测升级为视觉伺服控制。现在只是“识别类别→映射固定角度”,属于开环控制。如果想做闭环,可以检测目标在图像中的坐标,然后根据坐标偏差实时调整舵机角度,让摄像头“跟随”目标移动。这个玩法对实时性和控制算法要求更高,但效果非常惊艳。
第三,换用Jetson Orin Nano或Orin Nano Super开发板。经典Jetson Nano的算力现在确实有些紧张,Orin Nano能跑更大的模型、更高的帧率。如果你的项目需要同时处理多路摄像头或者更复杂的模型,性能升级带来的体验差异是巨大的。
6.2 个人实操过程中的几个体会
回到这个项目本身,我想说几点实际的体会。
硬件联调一定给“电源”留够预算。这个项目里80%的诡异问题都出在供电上:舵机抖动是供电问题、STM32复位是供电问题、Jetson Nano偶发死机也可能是供电问题。Jetson Nano官方推荐5V 4A电源,但很多人(包括我)刚开始用充电宝或者普通手机充电器顶替,最后发现推理时电压跌落会造成各种莫名问题。这里建议直接买一个5V 4A的电源适配器,能省去很多烦恼。
通信协议一定要设计成“可扩展”的。我最早设计的协议只有帧头+角度+校验,后来想加舵机编号时发现不得不推倒重来。如果一开始就预留了cmd和target_id字段,后面扩展就轻松很多。
调试时尽量发挥逻辑分析仪的作用。STM32的串口数据、PWM波形都可以通过逻辑分析仪抓出来看,比肉眼看代码推断快得多。我那时候用一根便宜的8通道逻辑分析仪,很多丢帧问题都是靠它定位的。
如果你也在做类似的项目,可以先从“最小可用系统”入手:先让STM32单独控制舵机转动,再让Jetson Nano单独跑通推理,然后把两者用串口连起来。每一步都确认稳妥后再继续下一步,整个项目就不会陷入“到处都是问题、不知道从哪修起”的泥潭。
本文还有配套的精品资源,点击获取