STM32智能小车实战:OpenMV+OpenCV双引擎图像识别与麦克纳姆轮运动控制算法详解(附源码)
1. 项目缘起:为什么需要“双引擎”?
大家好,我是老张,一个在嵌入式视觉和机器人领域折腾了十多年的工程师。今天想和大家聊聊一个特别有意思的实战项目:用STM32做大脑,结合OpenMV和OpenCV搞一个能“眼观六路、全向移动”的智能小车。这可不是简单的玩具车,而是一个能真正理解环境、自主决策的移动机器人平台。
你可能要问,市面上做视觉小车的方案很多,为啥非要搞“双引擎”这么复杂?这得从我踩过的一个坑说起。几年前,我尝试用OpenMV做小车的“眼睛”,它小巧、集成度高,在单片机上跑图像处理确实方便。但很快我就发现,当我想识别一些更复杂的图案,或者想用上一些高级的机器学习模型时,OpenMV的算力就有点捉襟见肘了。反过来,如果全用电脑上的OpenCV来处理,虽然算法强大,但实时性、功耗和体积又成了新问题。
于是,“双引擎”的思路就诞生了。简单说,就是让OpenMV和OpenCV各司其职,优势互补。OpenMV作为前端的“侦察兵”,负责在车上实时采集图像、做一些轻量级的预处理(比如颜色追踪、简单形状识别),然后把关键信息或者原始图像数据传给后台。后台可以是另一块性能更强的板子(比如树莓派),甚至是一台电脑,运行着OpenCV这个“参谋部”,进行更复杂的图像分析、深度学习推理,最后再把决策指令(比如“目标在左前方30厘米处”)发回给STM32这个“指挥官”,控制小车运动。
这种架构的好处是显而易见的。它既保证了小车反应的实时性(OpenMV的快速响应),又拥有了强大的图像分析能力(OpenCV的丰富算法库)。特别适合用在需要复杂视觉判断,但又对延迟有要求的场景,比如物料分拣、跟随特定目标、或者是在复杂地形里自主导航。
2. 硬件搭建:从零开始组装你的“战车”
工欲善其事,必先利其器。硬件是项目的地基,选对部件能让后续开发事半功倍。下面这张清单是我多次项目后总结出的“黄金配置”,兼顾了性能、成本和可玩性。
| 部件名称 | 推荐型号/规格 | 核心作用 | 选购要点 |
|---|---|---|---|
| 主控制器 | STM32F407VET6 | 系统大脑,负责逻辑控制、通信调度、运动解算 | 主频168MHz,内存192KB,外设丰富,性价比极高 |
| 视觉传感器1 | OpenMV Cam H7 Plus | 前端视觉“侦察兵”,实时图像采集与轻处理 | H7内核性能更强,支持更高分辨率,带TF卡槽 |
| 视觉传感器2 | 普通USB摄像头 | 可选,用于连接后台OpenCV进行复杂处理 | 分辨率720P以上即可,注意驱动兼容性 |
| 电机与轮子 | 直流减速电机 + 麦克纳姆轮 x4 | 实现全向移动的执行机构 | 电机需带编码器反馈,麦克纳姆轮尺寸与车架匹配 |
| 电机驱动器 | TB6612FNG或DRV8833模块 x2 | 驱动电机,接收PWM信号控制转速和方向 | 建议双路驱动模块,一个模块驱动两个电机 |
| 电源系统 | 18650锂电池两节串联(7.4V) + 降压模块 | 为全系统供电 | 电池需有保护板,降压模块输出5V和3.3V |
| 车体结构 | 亚克力或铝合金多层小车底盘 | 承载所有电子部件 | 结构稳固,有足够的空间安装摄像头和扩展板 |
| 通信模块 | USB转TTL串口模块 / ESP8266 WiFi模块 | 连接OpenMV与STM32,或连接后台电脑 | 串口通信最稳定简单,WiFi用于无线调试和数据传输 |
组装与接线实战经验:
- 车架与电机安装:先把四个麦克纳姆轮安装到电机上,注意轮子的辊子方向!这是全向移动的关键。四个轮的辊子方向需要呈“X”型对称分布。你可以记住一个口诀:左前轮和右后轮的辊子朝右下倾斜,右前轮和左后轮的辊子朝左下倾斜。装反了车就跑不起来了。把电机固定到车架底板上。
- 主控与驱动连接:STM32核心板或最小系统板固定在车架二层。将四个电机的线接到两个电机驱动模块上(每个模块驱动两个电机)。然后,将驱动模块的控制引脚(PWMA, AIN1, AIN2, PWMB, BIN1, BIN2)连接到STM32的6个GPIO口(需要支持PWM输出)。同时,把电机的编码器A、B相线也接到STM32的定时器编码器接口引脚,用于后续测速。
- 视觉系统安装:将OpenMV摄像头用支架安装在小车前方,确保视野开阔。它的串口TX、RX引脚直接与STM32的串口RX、TX交叉相连。如果你还用了一个USB摄像头给后台电脑,想办法把它也固定好。
- 电源布线:这是保证稳定的关键。7.4V电池正负极接入降压模块的输入,降压模块输出5V给电机驱动模块、OpenMV和USB摄像头(如果需要),再通过一个LDO(如AMS1117-3.3)从5V降压到3.3V给STM32供电。务必确保STM32和所有模块共地!
硬件组装完成后,先别急着写代码。用万用表仔细检查所有电源线路有没有短路,电压是否正确。然后单独测试每个模块:给STM32烧录一个简单的LED闪烁程序;手动给电机驱动模块高低电平信号看电机是否正反转;连接OpenMV IDE看能否正常预览图像。这些基础测试能帮你排除80%的硬件故障。
3. 软件环境配置:打通任督二脉
硬件是躯体,软件是灵魂。配置好开发环境,就像给机器人注入了灵魂。我们的软件栈涉及三个部分:STM32嵌入式开发、OpenMV图像编程、以及PC端的OpenCV环境。
3.1 STM32开发环境搭建
我强烈推荐使用 STM32CubeIDE,它是ST官方推出的免费集成开发环境,集成了CubeMX图形化配置工具和调试器,对新手非常友好。
- 安装与项目创建:去ST官网下载安装。打开后,点击“Start new STM32 project”,选择你的芯片型号(如STM32F407VE)。在接下来的图形化界面(CubeMX)里,你需要进行关键配置:
- 系统核心(SYS):将
Debug改为Serial Wire,这样才能用ST-Link调试。 - 时钟(RCC):将
HSE(高速外部时钟)设置为Crystal/Ceramic Resonator,确保主频能跑到168MHz。 - GPIO:找到你连接电机驱动和编码器的引脚,设置为对应的功能。例如,驱动PWM引脚设置为
TIMx_CHy(定时器通道),方向引脚设置为GPIO_Output。编码器引脚设置为TIMx_CH1和TIMx_CH2。 - 定时器(TIM):配置两个定时器用于生成4路PWM(控制4个电机),配置四个定时器用于编码器模式(读取4个电机转速)。
- 串口(USART):配置一个串口(如USART2)与OpenMV通信,波特率设为115200。
- 系统核心(SYS):将
- 生成代码与基础驱动:配置完成后,点击“Generate Code”。CubeIDE会自动生成包含HAL库的初始化代码。你只需要在
main.c的/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间添加你的应用逻辑。首先,可以写个测试函数,让四个轮子依次正转、反转,看看硬件连接是否正确。
3.2 OpenMV开发环境搭建
OpenMV的编程在专用的OpenMV IDE中进行,它基于MicroPython,上手极快。
- 连接与固件:用USB线连接OpenMV Cam到电脑,打开OpenMV IDE。如果第一次使用,IDE可能会提示更新固件,建议更新到最新版本。在IDE左下角可以看到连接的设备型号和串口号。
- 第一个视觉脚本:新建一个文件,输入以下最简代码测试摄像头和颜色追踪:
import sensor, image, time
# 初始化摄像头
sensor.reset()
sensor.set_pixformat(sensor.RGB565) # 彩色图像
sensor.set_framesize(sensor.QVGA) # 320x240分辨率
sensor.skip_frames(time = 2000) # 等待摄像头稳定
sensor.set_auto_gain(False) # 关闭自动增益,颜色追踪更稳定
sensor.set_auto_whitebal(False) # 关闭白平衡
# 定义要追踪的颜色的阈值 (这里以红色为例,需要根据实际环境调整)
red_threshold = (30, 100, 15, 127, 15, 127) # (L_min, L_max, A_min, A_max, B_min, B_max) in LAB color space
clock = time.clock()
while(True):
clock.tick()
img = sensor.snapshot() # 抓取一帧图像
# 在图像中寻找色块
blobs = img.find_blobs([red_threshold], pixels_threshold=100, area_threshold=100)
if blobs:
# 找到最大的色块
largest_blob = max(blobs, key=lambda b: b.pixels())
# 在图像上画框
img.draw_rectangle(largest_blob.rect())
# 计算并打印色块中心坐标
img.draw_cross(largest_blob.cx(), largest_blob.cy())
print("中心坐标: (%d, %d)" % (largest_blob.cx(), largest_blob.cy()))
# 在IDE中显示帧率
print(clock.fps())
运行这个脚本,你应该能在IDE右侧看到实时画面,并用一个红色物体测试追踪效果。同时,串行终端会打印出目标中心的坐标和帧率。这就是OpenMV作为“侦察兵”的核心任务:快速定位目标位置。
3.3 PC端OpenCV环境搭建
对于后台处理,我们使用Python版的OpenCV。安装非常简单,在命令行(Windows的CMD或PowerShell,Mac/Linux的终端)里输入:
pip install opencv-python
pip install opencv-contrib-python # 包含更多扩展模块
pip install numpy # OpenCV的依赖
pip install pyserial # 用于串口通信,接收OpenMV数据
安装完成后,可以写个简单的脚本测试一下,比如打开USB摄像头并显示:
import cv2
cap = cv2.VideoCapture(0) # 0代表第一个摄像头
while True:
ret, frame = cap.read()
if not ret:
break
cv2.imshow('USB Camera', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
至此,三大软件环境全部就绪。接下来,我们要让它们“对话”。
4. 双引擎通信与数据流设计
“双引擎”协同工作的核心在于稳定、高效的数据通信。这里我设计了一个简单可靠的串口通信协议,让OpenMV和STM32,以及STM32和后台PC(运行OpenCV)之间能够准确传递信息。
4.1 OpenMV与STM32的通信
OpenMV将处理后的结果(比如目标坐标、颜色标签)通过串口发送给STM32。为了便于解析,我们约定一个简单的数据包格式。例如,发送目标中心坐标(x, y):
$X123,Y156#
$是帧头,X和Y是数据标识,123和156是具体数值,#是帧尾。在OpenMV的脚本中,找到打印坐标的那行print,修改为按照协议格式发送:
# 在OpenMV的循环中
if blobs:
largest_blob = max(blobs, key=lambda b: b.pixels())
cx, cy = largest_blob.cx(), largest_blob.cy()
# 格式化字符串并发送
data_str = "$X{:03d},Y{:03d}#".format(cx, cy)
uart.write(data_str) # 假设uart是之前初始化好的串口对象
在STM32端,我们需要编写一个串口中断服务程序来接收和解析这个数据包。思路是:在串口接收中断中,将字符存入缓冲区,并检测帧头$和帧尾#。当收到完整的帧后,在主循环中解析出X和Y后面的数字,就得到了目标在图像中的坐标。
4.2 STM32与后台PC的通信(可选高级模式)
当需要进行更复杂的识别(如人脸、特定Logo、深度学习模型推理)时,可以让OpenMV直接将原始图像通过串口(速度较慢)或WiFi模块发送给PC,PC上的OpenCV程序处理完后,再将结果指令(如“向左转”、“前进”)通过串口/WiFi发回STM32。
对于图像传输,由于数据量大,通常需要压缩(如JPEG)和分包。OpenMV提供了image.compress()函数。PC端用pyserial接收,再用OpenCV的cv2.imdecode()解码。这是一个进阶玩法,对实时性有影响,但极大地扩展了识别能力。
4.3 数据流整合
整个系统的数据流可以这样理解:
- 场景A(轻量任务):红色小球追踪。OpenMV实时识别小球坐标 -> 通过串口发送给STM32 -> STM32根据坐标计算小车运动指令 -> 控制电机。
- 场景B(重量任务):识别特定包装盒。OpenMV抓取一帧图像 -> 压缩后通过WiFi发送给PC -> PC运行OpenCV+深度学习模型进行识别分类 -> PC将分类结果和位置通过WiFi/串口发回STM32 -> STM32控制小车和机械臂进行抓取。
在实际项目中,你可以根据任务复杂度动态选择使用单引擎(仅OpenMV)还是双引擎模式。
5. 麦克纳姆轮运动学与核心控制算法
这是项目的精髓所在,也是很多朋友觉得最难的部分。麦克纳姆轮凭什么能横着走、斜着走、原地转圈?理解了它的运动学,控制代码写起来就清晰了。
5.1 运动学原理(尽量说人话)
你可以把麦克纳姆轮想象成普通轮子,但在轮缘上斜着安装了很多小辊子。正是这些辊子,让轮子除了提供前进的力,还能提供一个侧向的力。四个轮子特定角度安装,侧向力相互耦合或抵消,就产生了全向运动的效果。
我们定义小车的运动为三个自由度:前后移动(Vx)、左右平移(Vy)、原地旋转(ω)。每个轮子对这三个运动都有贡献。通过力学分解(这里省略复杂的矩阵推导),我们可以得到四个轮子速度(W1, W2, W3, W4 对应左前、右前、左后、右后)与小车整体运动指令(Vx, Vy, ω)之间的关系,这就是逆运动学公式:
W1 = Vx + Vy + C * ω
W2 = Vx - Vy - C * ω
W3 = Vx - Vy + C * ω
W4 = Vx + Vy - C * ω
其中,C是一个与小车轮距、轴距相关的常数。Vx, Vy, ω是你期望的小车运动速度(单位自定义,如cm/s和rad/s)。公式非常优美对称,计算出的W1~W4就是四个电机应该达到的速度值。
5.2 STM32上的算法实现
在STM32的代码中,我们需要实现一个函数,输入期望的Vx, Vy, ω,输出四个电机的PWM占空比。这里假设我们已经通过编码器测速实现了电机速度的闭环控制(即给定一个速度值,电机就能稳定达到那个速度)。
// 定义小车机械参数
#define WHEEL_RADIUS 3.0f // 轮子半径,单位厘米
#define WHEEL_BASE 12.0f // 左右轮距的一半,单位厘米
#define TRACK_WIDTH 12.0f // 前后轴距的一半,单位厘米
// 常数C,由几何尺寸决定
#define C (WHEEL_BASE + TRACK_WIDTH)
// 麦克纳姆轮逆运动学解算
// vx: 前后速度 (cm/s), vy: 左右速度 (cm/s), omega: 旋转角速度 (rad/s)
void Mecanum_Kinematics(float vx, float vy, float omega, float* wheel_speeds) {
wheel_speeds[0] = vx + vy + C * omega; // 左前轮 W1
wheel_speeds[1] = vx - vy - C * omega; // 右前轮 W2
wheel_speeds[2] = vx - vy + C * omega; // 左后轮 W3
wheel_speeds[3] = vx + vy - C * omega; // 右后轮 W4
// 可选:将线速度转换为电机转速 (RPM) 或 PWM值
// 例如:电机转速 (RPM) = (轮子线速度 / (2 * π * 轮子半径)) * 60
for(int i=0; i<4; i++) {
wheel_speeds[i] = (wheel_speeds[i] / (2 * 3.1415926f * WHEEL_RADIUS)) * 60.0f;
}
}
// 控制小车运动的接口函数
void Chassis_Move(float vx, float vy, float omega) {
float target_speeds[4];
Mecanum_Kinematics(vx, vy, omega, target_speeds);
// 将目标速度设置为四个电机的速度环设定值
Motor_Set_Target_Speed(MOTOR_FL, target_speeds[0]); // 左前
Motor_Set_Target_Speed(MOTOR_FR, target_speeds[1]); // 右前
Motor_Set_Target_Speed(MOTOR_BL, target_speeds[2]); // 左后
Motor_Set_Target_Speed(MOTOR_BR, target_speeds[3]); // 右后
}
Motor_Set_Target_Speed函数内部会调用你写的PID控制器,根据编码器反馈的实际转速,调整PWM输出,让电机精确达到目标速度。这就是速度闭环控制,它是小车运动平稳、不漂移的关键。
5.3 结合视觉的反馈控制
现在,我们把视觉和运动控制连起来。假设OpenMV识别到目标在图像中心坐标为(cx, cy),图像分辨率是320x240,那么图像中心点是(160, 120)。我们可以计算一个偏差:
error_x = cx - 160; // 水平方向偏差
error_y = cy - 120; // 垂直方向偏差(对于固定仰角的摄像头,可映射为距离)
然后,用一个简单的P控制器(比例控制)来生成小车的运动指令:
// 在STM32的主循环中
if (new_vision_data_received) { // 收到新的视觉数据
float kp = 0.05f; // 比例系数,需要根据实际调试
float target_vx = 0.0f; // 默认不前后移动
float target_vy = -kp * error_x; // 横向偏差驱动左右平移
float target_omega = kp * error_y; // 纵向偏差(或面积)可能驱动旋转或前后移动
Chassis_Move(target_vx, target_vy, target_omega);
}
这样,当目标在图像左侧(error_x为负),小车就会向右平移(target_vy为正),使目标回到图像中心,实现视觉伺服跟踪。这就是一个完整的“感知-决策-控制”闭环。
6. 实战进阶:双引擎融合案例与源码解析
理论讲完了,我们来点实际的。我设计一个综合案例:让小车自动寻找并移动到场地内的一个红色方块,然后通过OpenCV后台精确识别方块上的数字,并根据数字做出不同动作。
6.1 OpenMV端代码(侦察兵)
OpenMV负责快速定位红色方块区域,并将该区域的图像切片通过串口发送给PC。为了减少数据量,我们只发送感兴趣区域(ROI)。
# OpenMV Script: find_red_square_and_send_roi.py
import sensor, image, time, struct, uio, uos
# 初始化
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time=2000)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)
# 红色阈值 (需校准)
red_threshold = (30, 80, 40, 80, 10, 70)
uart = uio.UART(3, 115200) # 假设使用P4/P5作为TX/RX
def send_roi_via_uart(img, roi):
# roi: (x, y, w, h)
# 1. 发送帧头 'ROIS'
uart.write(b'ROIS')
# 2. 发送ROI坐标信息 (4个unsigned short)
uart.write(struct.pack('<HHHH', roi[0], roi[1], roi[2], roi[3]))
# 3. 发送ROI图像数据 (RGB565格式)
roi_img = img.copy(roi=roi).compress(quality=70) # 压缩一下
uart.write(struct.pack('<I', len(roi_img))) # 发送图像数据长度
uart.write(roi_img) # 发送图像数据
clock = time.clock()
while(True):
clock.tick()
img = sensor.snapshot()
# 寻找红色色块
blobs = img.find_blobs([red_threshold], pixels_threshold=500, area_threshold=500, merge=True)
if blobs:
largest_blob = max(blobs, key=lambda b: b.area())
# 在图像上画框
img.draw_rectangle(largest_blob.rect())
# 发送ROI信息给PC
send_roi_via_uart(img, largest_blob.rect())
# 同时,将方块中心坐标发给STM32控制移动 (简单协议)
cx, cy = largest_blob.cx(), largest_blob.cy()
uart.write("$X{:03d},Y{:03d}#".format(cx, cy).encode())
else:
# 没找到,发个特殊指令让小车慢速旋转寻找
uart.write(b"$X999,Y999#")
print(clock.fps())
6.2 PC端OpenCV代码(参谋部)
PC端用Python接收ROI数据,进行数字识别(这里用简单的模板匹配或轮廓分析举例,实际可用OCR或训练好的CNN模型)。
# PC Script: receive_and_recognize_number.py
import cv2
import numpy as np
import serial
import struct
import threading
# 初始化串口 (连接STM32或OpenMV,取决于接线)
ser = serial.Serial('COM5', 115200, timeout=1) # 请修改为你的串口号
def parse_roi_data(serial_port):
"""解析来自OpenMV的ROI数据包"""
header = serial_port.read(4)
if header != b'ROIS':
return None
# 读取ROI坐标
roi_data = serial_port.read(8) # 4个unsigned short, 8字节
if len(roi_data) < 8:
return None
x, y, w, h = struct.unpack('<HHHH', roi_data)
# 读取图像长度
img_len_data = serial_port.read(4)
if len(img_len_data) < 4:
return None
img_len = struct.unpack('<I', img_len_data)[0]
# 读取图像数据
img_data = serial_port.read(img_len)
if len(img_data) < img_len:
return None
# 解码图像
nparr = np.frombuffer(img_data, np.uint8)
roi_img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
return (x, y, w, h, roi_img)
def recognize_number(roi_image):
"""在ROI图像中识别数字(示例:简单轮廓法)"""
gray = cv2.cvtColor(roi_image, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
# 这里简化处理,假设找到的轮廓数量就是数字(仅作示例,实际识别需更复杂算法)
num = len(contours)
if 0 <= num <= 9:
return num
return -1 # 识别失败
def send_command_to_stm32(command):
"""发送指令给STM32"""
# 简单协议,例如 'CMD1' 代表数字1对应的动作
ser.write(command.encode())
print(f"Sent command: {command}")
# 启动接收线程或循环
try:
while True:
result = parse_roi_data(ser)
if result:
x, y, w, h, roi_img = result
cv2.imshow('Received ROI', roi_img)
number = recognize_number(roi_img)
if number != -1:
print(f"Recognized Number: {number}")
# 根据识别结果发送不同指令
if number == 1:
send_command_to_stm32("CMD1") # 例如:原地转一圈
elif number == 2:
send_command_to_stm32("CMD2") # 例如:后退一段距离
# ... 其他数字对应动作
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except KeyboardInterrupt:
pass
finally:
ser.close()
cv2.destroyAllWindows()
6.3 STM32端代码(指挥官)
STM32同时监听来自OpenMV的坐标数据和来自PC的指令数据,综合决策。
// STM32 Code Snippet (主循环部分)
typedef enum {
MODE_VISION_TRACKING,
MODE_EXECUTE_PC_CMD,
MODE_IDLE
} SystemMode_t;
SystemMode_t current_mode = MODE_VISION_TRACKING;
char pc_command_buffer[10];
bool new_pc_cmd = false;
void USART2_IRQHandler(void) {
// 串口2中断,假设连接OpenMV
// ... 解析坐标数据,更新 target_cx, target_cy ...
}
void USART3_IRQHandler(void) {
// 串口3中断,假设连接PC
// ... 接收PC指令,存入 pc_command_buffer,设置 new_pc_cmd = true ...
}
int main(void) {
// 初始化...
while (1) {
switch(current_mode) {
case MODE_VISION_TRACKING:
if (new_vision_data) {
// 根据OpenMV坐标进行P控制移动
float vy = -KP_VISION * (target_cx - IMG_CENTER_X);
float vx = KP_VISION * (target_cy - IMG_CENTER_Y); // 假设y坐标映射前后距离
Chassis_Move(vx, vy, 0);
new_vision_data = false;
}
// 如果收到PC的强制指令,切换模式
if (new_pc_cmd) {
if (strncmp(pc_command_buffer, "CMD1", 4) == 0) {
current_mode = MODE_EXECUTE_PC_CMD;
// 执行CMD1对应的动作序列
}
new_pc_cmd = false;
}
break;
case MODE_EXECUTE_PC_CMD:
// 执行来自PC的复杂动作序列
// 例如:Chassis_Move(0, 0, 3.14); // 原地转180度
// 动作执行完毕后,切换回视觉跟踪模式
current_mode = MODE_VISION_TRACKING;
break;
case MODE_IDLE:
Chassis_Move(0, 0, 0); // 停止
break;
}
}
}
这个案例展示了“双引擎”的典型工作流:OpenMV快速锁定目标,STM32进行粗略跟踪;同时,OpenMV将关键图像区域发给PC,PC进行精细识别并下达高级指令,STM32融合两者信息做出最终决策。代码虽然简化,但清晰地展示了数据流和控制逻辑。在实际调试中,你需要仔细调整PID参数、通信协议和模式切换的逻辑,确保小车行为平滑、稳定。
更多推荐

所有评论(0)