文章目录
-
- 一、项目概述与系统架构
- 二、硬件清单与接线
-
- [2.1 所需硬件组件](#2.1 所需硬件组件)
- [2.2 电源系统接线](#2.2 电源系统接线)
- [2.3 信号线接线详解](#2.3 信号线接线详解)
- 三、OpenMV视觉识别程序开发
-
- [3.1 OpenMV开发环境搭建](#3.1 OpenMV开发环境搭建)
- [3.2 颜色阈值获取工具](#3.2 颜色阈值获取工具)
- [3.3 主程序代码编写](#3.3 主程序代码编写)
- [3.4 代码关键部分详解](#3.4 代码关键部分详解)
- 四、STM32控制程序开发
-
- [4.1 开发环境准备](#4.1 开发环境准备)
- [4.2 标准外设库文件配置](#4.2 标准外设库文件配置)
- [4.3 主控程序代码](#4.3 主控程序代码)
- [4.4 代码关键部分详解](#4.4 代码关键部分详解)
- 五、系统联调与测试
-
- [5.1 程序烧录](#5.1 程序烧录)
- [5.2 首次上电测试步骤](#5.2 首次上电测试步骤)
- [5.3 参数调试指南](#5.3 参数调试指南)
- [5.4 常见问题排查](#5.4 常见问题排查)
- 六、功能扩展建议
- 七、总结
一、项目概述与系统架构
本项目将手把手教你制作一辆能够自动识别并追踪特定颜色物体的智能小车。系统采用OpenMV摄像头进行颜色目标检测与定位,通过串口将位置数据发送给STM32主控,STM32根据数据控制舵机云台和底盘电机,实现目标追踪功能。
整个系统的工作流程如下:OpenMV摄像头持续采集图像,通过颜色阈值分割算法识别目标物体,计算目标在画面中的坐标位置,然后将坐标数据打包通过串口发送给STM32。STM32接收到数据后解析出目标的X轴和Y轴偏移量,分别控制云台舵机保持目标在画面中央,以及控制底盘电机使小车朝向目标移动。
控制执行层
数据传输层
视觉感知层
OpenMV摄像头
图像采集
颜色阈值分割
目标定位算法
坐标数据打包
UART串口发送
串口数据接收
STM32F103主控
数据解析
X轴偏移量
Y轴偏移量
舵机云台控制
底盘电机控制
SG90舵机
L298N电机驱动
直流减速电机
二、硬件清单与接线
2.1 所需硬件组件
在开始动手之前,请确保你准备好了以下所有硬件组件。每个组件都有其不可替代的作用,缺少任何一个都会导致项目无法完成。
| 组件名称 | 型号规格 | 数量 | 作用说明 |
|---|---|---|---|
| OpenMV摄像头 | OpenMV4 H7 Plus | 1个 | 图像采集与颜色识别处理 |
| STM32主控板 | STM32F103C8T6最小系统 | 1个 | 主控制器,处理串口数据并控制执行器 |
| 舵机 | SG90微型舵机 | 2个 | 组成二自由度云台,控制摄像头俯仰和偏航 |
| 直流减速电机 | 1:48减速比,带编码器 | 4个 | 驱动小车底盘运动 |
| 电机驱动模块 | L298N双H桥 | 2个 | 驱动四个直流电机正反转及调速 |
| 小车底盘 | 四轮底盘套件 | 1套 | 承载所有硬件的主体结构 |
| 锂电池 | 7.4V 2200mAh航模电池 | 1块 | 为整个系统供电 |
| 降压模块 | LM2596 DC-DC可调降压 | 2个 | 分别降压到5V和3.3V供各模块使用 |
| 杜邦线 | 公对母、母对母 | 若干 | 连接各模块之间的信号和电源线 |
| USB转TTL模块 | CH340G | 1个 | 用于STM32程序下载和调试 |
2.2 电源系统接线
电源系统的设计至关重要,不合理的电源分配会导致系统不稳定甚至烧毁元件。请严格按照以下方案进行接线。
电池7.4V输出端分为两路:一路直接连接到L298N电机驱动模块的12V输入端子,为电机提供动力;另一路连接到第一个LM2596降压模块,调整输出电压为5V,这个5V输出同时供给OpenMV摄像头和两个SG90舵机。5V输出再连接到第二个LM2596降压模块,进一步降压到3.3V供给STM32F103C8T6最小系统板。
在所有电源连接中,请务必将各模块的GND连接在一起形成共地,这是串口通信能够正常工作的前提条件。
2.3 信号线接线详解
OpenMV与STM32之间的串口连接:OpenMV的P4引脚作为UART3的TX,连接到STM32的PA10引脚,即USART1的RX。OpenMV的P5引脚作为UART3的RX,连接到STM32的PA9引脚,即USART1的TX。
舵机云台的接线:控制水平方向旋转的舵机信号线连接到STM32的PA0引脚,该引脚对应TIM2的通道1,可以输出PWM信号。控制俯仰方向的舵机信号线连接到STM32的PA1引脚,对应TIM2的通道2。
电机驱动的接线:L298N模块1控制左侧两个电机,其IN1、IN2、IN3、IN4分别接STM32的PB3、PB4、PB5、PB6,使能端ENA和ENB接PB0和PB1用于PWM调速。L298N模块2控制右侧两个电机,其IN1、IN2、IN3、IN4分别接PB12、PB13、PB14、PB15,使能端ENA和ENB接PA6和PA7。
信号连接
UART3
UART3
OpenMV P4 TX
STM32 PA10 RX
OpenMV P5 RX
STM32 PA9 TX
STM32 PA0 PWM
舵机1 水平
STM32 PA1 PWM
舵机2 俯仰
STM32 PB3-PB6
L298N模块1
STM32 PB12-PB15
L298N模块2
电源系统
5V输出
5V输出
3.3V输出
7.4V锂电池
LM2596降压模块1
L298N电机驱动
OpenMV摄像头
SG90舵机x2
LM2596降压模块2
STM32F103C8T6
三、OpenMV视觉识别程序开发
3.1 OpenMV开发环境搭建
首先需要在电脑上安装OpenMV IDE。访问OpenMV官方网站下载最新版本的OpenMV IDE,根据你的操作系统选择对应的安装包。安装完成后,使用Micro USB数据线将OpenMV摄像头连接到电脑,在IDE中选择对应的串口端口,点击连接按钮。连接成功后,IDE中会显示摄像头的实时画面。
3.2 颜色阈值获取工具
在编写颜色识别代码之前,我们需要确定目标物体的颜色阈值。OpenMV IDE提供了非常方便的工具来获取这个阈值。将需要追踪的目标物体放在摄像头前方,在IDE中点击"工具"菜单,选择"机器视觉",然后选择"阈值编辑器"。在弹出的窗口中,选择"帧缓冲区"作为源图像,选择LAB色彩空间,然后通过拖动滑块来调整各个通道的阈值范围,直到目标物体在右侧的二值图像中显示为白色,背景显示为黑色。记录下此时的LAB阈值,这就是后续代码中需要使用的参数。
3.3 主程序代码编写
在OpenMV IDE中新建一个文件,命名为main.py。这个文件是OpenMV上电后自动运行的主程序。下面是完整的代码实现。
文件名:main.py
python
import sensor
import image
import time
import math
from pyb import UART
# 初始化摄像头传感器
sensor.reset()
sensor.set_pixformat(sensor.RGB565) # 设置像素格式为RGB565
sensor.set_framesize(sensor.QVGA) # 设置分辨率为320x240
sensor.skip_frames(time=2000) # 跳过前2秒的帧,等待摄像头稳定
sensor.set_auto_gain(False) # 关闭自动增益
sensor.set_auto_whitebal(False) # 关闭自动白平衡
clock = time.clock() # 创建时钟对象用于计算帧率
# 初始化串口通信
# UART3: 波特率115200,数据位8,停止位1,无校验
uart = UART(3, 115200, timeout_char=1000)
# 定义目标颜色的LAB阈值
# 这里以红色物体为例,实际使用时请根据阈值编辑器获取的值进行修改
# LAB色彩空间的三个通道:L表示亮度,A表示红绿分量,B表示黄蓝分量
red_threshold = (30, 80, 20, 70, 10, 60)
# 定义画面中心点坐标
# QVGA分辨率320x240,中心点为(160, 120)
CENTER_X = 160
CENTER_Y = 120
# 定义舵机角度范围
SERVO_X_MIN = 0 # 水平舵机最小角度
SERVO_X_MAX = 180 # 水平舵机最大角度
SERVO_Y_MIN = 30 # 俯仰舵机最小角度
SERVO_Y_MAX = 150 # 俯仰舵机最大角度
# 定义死区范围,目标在死区内时不进行角度调整
DEAD_ZONE = 15
# 当前舵机角度变量,初始化为中间位置
current_servo_x = 90
current_servo_y = 90
# 比例系数,用于根据偏移量计算角度调整量
KP_X = 0.3
KP_Y = 0.3
# 数据发送间隔控制,避免串口数据发送过于频繁
last_send_time = 0
SEND_INTERVAL = 50 # 毫秒
def find_largest_blob(blobs):
"""
在找到的所有色块中找出面积最大的一个
参数:
blobs: find_blobs返回的色块列表
返回:
面积最大的色块对象,如果没有找到则返回None
"""
if not blobs:
return None
max_blob = blobs[0]
max_area = blobs[0].area()
for blob in blobs:
if blob.area() > max_area:
max_blob = blob
max_area = blob.area()
return max_blob
def calculate_servo_angle(offset, current_angle, kp, angle_min, angle_max):
"""
根据偏移量计算新的舵机角度
参数:
offset: 目标位置与画面中心的偏移量
current_angle: 当前舵机角度
kp: 比例系数
angle_min: 舵机角度下限
angle_max: 舵机角度上限
返回:
计算后的新舵机角度
"""
# 计算角度调整量
angle_change = offset * kp
# 计算新的角度
new_angle = current_angle - angle_change
# 限制角度范围
if new_angle < angle_min:
new_angle = angle_min
elif new_angle > angle_max:
new_angle = angle_max
return int(new_angle)
def send_servo_data(x_angle, y_angle):
"""
通过串口发送舵机角度数据到STM32
数据格式: 帧头0xFF + X角度高字节 + X角度低字节 + Y角度高字节 + Y角度低字节 + 校验和
参数:
x_angle: 水平舵机角度 (0-180)
y_angle: 俯仰舵机角度 (0-180)
"""
# 计算校验和,取所有数据字节之和的低8位
checksum = (0xFF + (x_angle >> 8) + (x_angle & 0xFF) +
(y_angle >> 8) + (y_angle & 0xFF)) & 0xFF
# 构建数据包
data_packet = bytearray([
0xFF, # 帧头
(x_angle >> 8) & 0xFF, # X角度高8位
x_angle & 0xFF, # X角度低8位
(y_angle >> 8) & 0xFF, # Y角度高8位
y_angle & 0xFF, # Y角度低8位
checksum # 校验和
])
# 发送数据包
uart.write(data_packet)
def send_motor_data(left_speed, right_speed):
"""
通过串口发送电机速度数据到STM32
数据格式: 帧头0xFE + 左轮速度 + 右轮速度 + 校验和
参数:
left_speed: 左轮速度 (-100到100,正值前进,负值后退)
right_speed: 右轮速度 (-100到100,正值前进,负值后退)
"""
# 将速度值转换为0-200范围,100为停止,0为最大后退,200为最大前进
left_val = left_speed + 100
right_val = right_speed + 100
# 限制范围
left_val = max(0, min(200, left_val))
right_val = max(0, min(200, right_val))
# 计算校验和
checksum = (0xFE + left_val + right_val) & 0xFF
# 构建数据包
data_packet = bytearray([
0xFE, # 帧头
left_val, # 左轮速度值
right_val, # 右轮速度值
checksum # 校验和
])
# 发送数据包
uart.write(data_packet)
def calculate_motor_speed(offset_x, offset_y):
"""
根据目标的偏移量计算电机速度
参数:
offset_x: X轴偏移量(正值表示目标在右侧)
offset_y: Y轴偏移量(正值表示目标在上方)
返回:
(left_speed, right_speed): 左右轮速度元组
"""
# 基础速度
base_speed = 40
# 根据X轴偏移量计算转向速度差
# 目标偏右时,右轮减慢,左轮加快,实现右转
turn_factor = offset_x * 0.3
# 根据Y轴偏移量调整基础速度
# 目标偏上(距离远)时加速,偏下(距离近)时减速
distance_factor = -offset_y * 0.15
# 计算最终速度
adjusted_speed = base_speed + distance_factor
left_speed = adjusted_speed - turn_factor
right_speed = adjusted_speed + turn_factor
# 限制速度范围
left_speed = max(-100, min(100, int(left_speed)))
right_speed = max(-100, min(100, int(right_speed)))
return left_speed, right_speed
# 主循环
print("智能视觉追踪小车启动中...")
print("请将目标物体放在摄像头前方")
while True:
clock.tick() # 开始计时当前帧
# 捕获一帧图像
img = sensor.snapshot()
# 在图像中查找符合颜色阈值的色块
# pixels_threshold: 色块的最小像素面积,过滤掉小面积的噪点
# area_threshold: 色块的最小面积
# merge: 是否合并相邻的色块
blobs = img.find_blobs([red_threshold],
pixels_threshold=100,
area_threshold=100,
merge=True)
# 找到面积最大的色块作为目标
target_blob = find_largest_blob(blobs)
# 如果找到了目标
if target_blob is not None:
# 获取色块的中心坐标
blob_cx = target_blob.cx()
blob_cy = target_blob.cy()
# 获取色块的外接矩形
blob_rect = target_blob.rect()
# 计算目标相对于画面中心的偏移量
offset_x = blob_cx - CENTER_X
offset_y = blob_cy - CENTER_Y
# 在图像上绘制色块的外接矩形(绿色)
img.draw_rectangle(blob_rect, color=(0, 255, 0), thickness=2)
# 在图像上绘制色块的中心十字标记
img.draw_cross(blob_cx, blob_cy, color=(0, 255, 0), size=10, thickness=2)
# 在图像上绘制画面中心十字标记(红色)
img.draw_cross(CENTER_X, CENTER_Y, color=(255, 0, 0), size=15, thickness=2)
# 绘制从目标中心到画面中心的连线
img.draw_line(blob_cx, blob_cy, CENTER_X, CENTER_Y, color=(255, 255, 0), thickness=1)
# 判断偏移量是否超出死区
if abs(offset_x) > DEAD_ZONE:
# 计算新的水平舵机角度
current_servo_x = calculate_servo_angle(
offset_x, current_servo_x, KP_X, SERVO_X_MIN, SERVO_X_MAX
)
if abs(offset_y) > DEAD_ZONE:
# 计算新的俯仰舵机角度
current_servo_y = calculate_servo_angle(
offset_y, current_servo_y, KP_Y, SERVO_Y_MIN, SERVO_Y_MAX
)
# 计算电机速度
left_speed, right_speed = calculate_motor_speed(offset_x, offset_y)
# 在图像上显示偏移量信息
img.draw_string(10, 10, "Offset X: %d" % offset_x, color=(255, 255, 255))
img.draw_string(10, 25, "Offset Y: %d" % offset_y, color=(255, 255, 255))
img.draw_string(10, 40, "Servo X: %d" % current_servo_x, color=(255, 255, 255))
img.draw_string(10, 55, "Servo Y: %d" % current_servo_y, color=(255, 255, 255))
img.draw_string(10, 70, "Left: %d Right: %d" % (left_speed, right_speed), color=(255, 255, 255))
# 控制数据发送频率
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) > SEND_INTERVAL:
# 发送舵机角度数据
send_servo_data(current_servo_x, current_servo_y)
# 发送电机速度数据
send_motor_data(left_speed, right_speed)
last_send_time = current_time
else:
# 没有找到目标时的处理
img.draw_string(10, 10, "Target Not Found", color=(255, 0, 0))
img.draw_string(10, 30, "Searching...", color=(255, 0, 0))
# 没有目标时停止电机
current_time = time.ticks_ms()
if time.ticks_diff(current_time, last_send_time) > SEND_INTERVAL:
send_motor_data(0, 0)
last_send_time = current_time
# 在图像上显示帧率
fps = clock.fps()
img.draw_string(10, img.height() - 20, "FPS: %.1f" % fps, color=(255, 255, 255))
# 打印调试信息到串口终端
if target_blob is not None:
print("Target at (%d, %d), Offset: (%d, %d), Servo: (%d, %d)" %
(blob_cx, blob_cy, offset_x, offset_y, current_servo_x, current_servo_y))
3.4 代码关键部分详解
颜色阈值定义部分是整个识别系统的核心参数。red_threshold = (30, 80, 20, 70, 10, 60) 这个元组包含了LAB色彩空间三个通道的最小值和最大值。前两个数值对应L通道,表示亮度的下限和上限;中间两个数值对应A通道,正值表示红色方向,负值表示绿色方向;最后两个数值对应B通道,正值表示黄色方向,负值表示蓝色方向。不同的目标颜色需要使用不同的阈值范围。
串口数据协议设计方面,舵机数据使用0xFF作为帧头,后面跟随两个字节的X角度值和两个字节的Y角度值,最后是一个字节的校验和。电机数据使用0xFE作为帧头,后面跟随左轮速度值和右轮速度值,最后也是校验和。使用不同的帧头可以让STM32端方便地区分数据类型。
PID控制思想体现在calculate_servo_angle函数中。这里使用了最简单的比例控制,即目标偏移量乘以一个比例系数得到角度调整量。比例系数KP_X和KP_Y需要根据实际情况进行调试,系数过大会导致舵机抖动,系数过小则响应缓慢。
四、STM32控制程序开发
4.1 开发环境准备
STM32的程序开发使用Keil MDK-ARM集成开发环境。首先需要安装Keil5软件,然后安装STM32F1系列的器件支持包。同时需要安装CH340G的USB转串口驱动,确保电脑能够识别下载器。
创建一个新的Keil工程,选择STM32F103C8作为目标芯片。在工程配置中,需要启用MicroLIB以支持标准库函数的简化版本。将系统时钟配置为72MHz,这是STM32F103的最高工作频率。
4.2 标准外设库文件配置
为了让代码更加清晰易读,我们需要将标准外设库中必要的文件添加到工程中。以下是需要添加的核心文件:
stm32f10x_gpio.c:GPIO外设驱动stm32f10x_usart.c:USART串口驱动stm32f10x_tim.c:定时器驱动,用于PWM输出stm32f10x_rcc.c:时钟系统驱动stm32f10x_misc.c:NVIC中断控制器驱动
在stm32f10x_conf.h文件中,取消以上模块对应的宏定义注释,使这些模块被编译到工程中。
4.3 主控程序代码
在工程中创建以下文件并编写代码。
文件名:main.c
c
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_tim.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_misc.h"
#include <string.h>
#include <stdio.h>
/* 全局宏定义 */
#define SERVO_X_TIM TIM2
#define SERVO_X_CHANNEL TIM_Channel_1
#define SERVO_X_GPIO_PORT GPIOA
#define SERVO_X_GPIO_PIN GPIO_Pin_0
#define SERVO_X_GPIO_CLK RCC_APB2Periph_GPIOA
#define SERVO_Y_TIM TIM2
#define SERVO_Y_CHANNEL TIM_Channel_2
#define SERVO_Y_GPIO_PORT GPIOA
#define SERVO_Y_GPIO_PIN GPIO_Pin_1
#define SERVO_Y_GPIO_CLK RCC_APB2Periph_GPIOA
/* 电机控制引脚定义 */
#define MOTOR1_IN1_PORT GPIOB
#define MOTOR1_IN1_PIN GPIO_Pin_3
#define MOTOR1_IN2_PORT GPIOB
#define MOTOR1_IN2_PIN GPIO_Pin_4
#define MOTOR1_IN3_PORT GPIOB
#define MOTOR1_IN3_PIN GPIO_Pin_5
#define MOTOR1_IN4_PORT GPIOB
#define MOTOR1_IN4_PIN GPIO_Pin_6
#define MOTOR1_ENA_PORT GPIOB
#define MOTOR1_ENA_PIN GPIO_Pin_0
#define MOTOR1_ENB_PORT GPIOB
#define MOTOR1_ENB_PIN GPIO_Pin_1
#define MOTOR2_IN1_PORT GPIOB
#define MOTOR2_IN1_PIN GPIO_Pin_12
#define MOTOR2_IN2_PORT GPIOB
#define MOTOR2_IN2_PIN GPIO_Pin_13
#define MOTOR2_IN3_PORT GPIOB
#define MOTOR2_IN3_PIN GPIO_Pin_14
#define MOTOR2_IN4_PORT GPIOB
#define MOTOR2_IN4_PIN GPIO_Pin_15
#define MOTOR2_ENA_PORT GPIOA
#define MOTOR2_ENA_PIN GPIO_Pin_6
#define MOTOR2_ENB_PORT GPIOA
#define MOTOR2_ENB_PIN GPIO_Pin_7
/* 串口接收缓冲区大小 */
#define RX_BUFFER_SIZE 64
/* 舵机角度范围 */
#define SERVO_X_MIN 0
#define SERVO_X_MAX 180
#define SERVO_Y_MIN 30
#define SERVO_Y_MAX 150
/* 当前舵机角度 */
volatile uint16_t current_servo_x = 90;
volatile uint16_t current_servo_y = 90;
/* 电机速度变量 */
volatile int16_t motor_left_speed = 0;
volatile int16_t motor_right_speed = 0;
/* 串口接收缓冲区 */
volatile uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint8_t rx_index = 0;
volatile uint8_t rx_data_ready = 0;
/* 数据帧解析状态 */
typedef enum {
WAIT_HEADER, /* 等待帧头 */
WAIT_DATA /* 等待数据 */
} FrameState;
/* 函数声明 */
void RCC_Configuration(void);
void GPIO_Configuration(void);
void USART_Configuration(void);
void TIM_PWM_Configuration(void);
void NVIC_Configuration(void);
void Servo_SetAngle(TIM_TypeDef* TIMx, uint8_t Channel, uint16_t Angle);
void Motor_Control(int16_t left_speed, int16_t right_speed);
void Motor_SetSpeed(uint8_t motor_id, int16_t speed);
void USART_SendString(char* str);
void Delay_ms(uint32_t ms);
/**
* 系统时钟配置
* 使用外部8MHz晶振,通过PLL倍频到72MHz
*/
void RCC_Configuration(void)
{
ErrorStatus HSEStartUpStatus;
/* 复位RCC寄存器为默认值 */
RCC_DeInit();
/* 使能外部高速晶振 */
RCC_HSEConfig(RCC_HSE_ON);
/* 等待外部晶振就绪 */
HSEStartUpStatus = RCC_WaitForHSEStartUp();
if(HSEStartUpStatus == SUCCESS)
{
/* 使能预取指缓存 */
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
/* 设置FLASH等待周期,72MHz需要2个等待周期 */
FLASH_SetLatency(FLASH_Latency_2);
/* 设置AHB预分频器,HCLK = SYSCLK */
RCC_HCLKConfig(RCC_SYSCLK_Div1);
/* 设置APB2预分频器,PCLK2 = HCLK */
RCC_PCLK2Config(RCC_HCLK_Div1);
/* 设置APB1预分频器,PCLK1 = HCLK/2,最高36MHz */
RCC_PCLK1Config(RCC_HCLK_Div2);
/* 设置PLL时钟源和倍频系数,8MHz * 9 = 72MHz */
RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);
/* 使能PLL */
RCC_PLLCmd(ENABLE);
/* 等待PLL就绪 */
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
/* 选择PLL作为系统时钟源 */
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
/* 等待系统时钟切换完成 */
while(RCC_GetSYSCLKSource() != 0x08);
}
/* 使能各外设时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
RCC_APB2Periph_GPIOB |
RCC_APB2Periph_USART1 |
RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 |
RCC_APB1Periph_TIM3, ENABLE);
}
/**
* GPIO引脚配置
* 配置舵机PWM输出引脚、电机控制引脚
*/
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 舵机PWM输出引脚配置为复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = SERVO_X_GPIO_PIN | SERVO_Y_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SERVO_X_GPIO_PORT, &GPIO_InitStructure);
/* 电机控制引脚配置为推挽输出 */
GPIO_InitStructure.GPIO_Pin = MOTOR1_IN1_PIN | MOTOR1_IN2_PIN |
MOTOR1_IN3_PIN | MOTOR1_IN4_PIN |
MOTOR1_ENA_PIN | MOTOR1_ENB_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOTOR1_IN1_PORT, &GPIO_InitStructure);
/* 第二个电机驱动模块引脚配置 */
GPIO_InitStructure.GPIO_Pin = MOTOR2_IN1_PIN | MOTOR2_IN2_PIN |
MOTOR2_IN3_PIN | MOTOR2_IN4_PIN;
GPIO_Init(MOTOR2_IN1_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = MOTOR2_ENA_PIN | MOTOR2_ENB_PIN;
GPIO_Init(MOTOR2_ENA_PORT, &GPIO_InitStructure);
/* USART1引脚配置 */
/* TX: PA9 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* RX: PA10 浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 初始化所有电机控制引脚为低电平 */
GPIO_ResetBits(MOTOR1_IN1_PORT, MOTOR1_IN1_PIN | MOTOR1_IN2_PIN |
MOTOR1_IN3_PIN | MOTOR1_IN4_PIN |
MOTOR1_ENA_PIN | MOTOR1_ENB_PIN);
GPIO_ResetBits(MOTOR2_IN1_PORT, MOTOR2_IN1_PIN | MOTOR2_IN2_PIN |
MOTOR2_IN3_PIN | MOTOR2_IN4_PIN);
GPIO_ResetBits(MOTOR2_ENA_PORT, MOTOR2_ENA_PIN | MOTOR2_ENB_PIN);
}
/**
* USART1串口配置
* 波特率: 115200
* 数据位: 8
* 停止位: 1
* 校验位: 无
* 接收中断使能
*/
void USART_Configuration(void)
{
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
/* 使能接收中断 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
/* 使能USART1 */
USART_Cmd(USART1, ENABLE);
}
/**
* 定时器PWM配置
* TIM2用于舵机PWM输出
* 舵机控制信号:周期20ms,高电平0.5ms-2.5ms对应0-180度
*
* 计算方法:
* 定时器时钟频率 = 72MHz / (预分频值 + 1) = 72MHz / 72 = 1MHz
* 每个计数周期 = 1us
* 20ms周期需要计数20000次,所以自动重装载值设为19999
*
* 0度:高电平0.5ms = 500us,比较值 = 500
* 90度:高电平1.5ms = 1500us,比较值 = 1500
* 180度:高电平2.5ms = 2500us,比较值 = 2500
*/
void TIM_PWM_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
/* 定时器时基配置 */
TIM_TimeBaseStructure.TIM_Period = 19999; /* 自动重装载值,20ms周期 */
TIM_TimeBaseStructure.TIM_Prescaler = 71; /* 预分频值,72分频得到1MHz */
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(SERVO_X_TIM, &TIM_TimeBaseStructure);
/* PWM输出模式配置 - 通道1(水平舵机) */
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1500; /* 初始占空比,对应90度 */
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(SERVO_X_TIM, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(SERVO_X_TIM, TIM_OCPreload_Enable);
/* PWM输出模式配置 - 通道2(俯仰舵机) */
TIM_OCInitStructure.TIM_Pulse = 1500; /* 初始占空比,对应90度 */
TIM_OC2Init(SERVO_X_TIM, &TIM_OCInitStructure);
TIM_OC2PreloadConfig(SERVO_X_TIM, TIM_OCPreload_Enable);
/* 使能定时器 */
TIM_Cmd(SERVO_X_TIM, ENABLE);
}
/**
* NVIC中断控制器配置
* 配置USART1接收中断优先级
*/
void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 设置中断优先级分组为组2:2位抢占优先级,2位响应优先级 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置USART1中断 */
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; /* 抢占优先级1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; /* 响应优先级1 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* 舵机角度设置函数
*
* 参数:
* TIMx: 定时器外设,如TIM2
* Channel: 定时器通道,如TIM_Channel_1
* Angle: 目标角度 (0-180度)
*/
void Servo_SetAngle(TIM_TypeDef* TIMx, uint8_t Channel, uint16_t Angle)
{
uint16_t pulse_width;
/* 限制角度范围 */
if(Angle < SERVO_X_MIN) Angle = SERVO_X_MIN;
if(Angle > SERVO_X_MAX) Angle = SERVO_X_MAX;
/*
* 将角度转换为脉冲宽度
* 0度 -> 500us (计数值500)
* 180度 -> 2500us (计数值2500)
* 线性映射公式: pulse = 500 + (Angle * 2000 / 180)
*/
pulse_width = 500 + ((uint32_t)Angle * 2000) / 180;
/* 根据通道设置比较寄存器的值 */
switch(Channel)
{
case 1:
TIM_SetCompare1(TIMx, pulse_width);
break;
case 2:
TIM_SetCompare2(TIMx, pulse_width);
break;
case 3:
TIM_SetCompare3(TIMx, pulse_width);
break;
case 4:
TIM_SetCompare4(TIMx, pulse_width);
break;
default:
break;
}
}
/**
* 单个电机速度设置函数
*
* 参数:
* motor_id: 电机编号 (1-4)
* speed: 速度值 (-100到100)
* 正值表示正转,负值表示反转,0表示停止
*/
void Motor_SetSpeed(uint8_t motor_id, int16_t speed)
{
/* 限制速度范围 */
if(speed > 100) speed = 100;
if(speed < -100) speed = -100;
switch(motor_id)
{
case 1: /* 左前轮 */
if(speed > 0)
{
/* 正转:IN1高电平,IN2低电平 */
GPIO_SetBits(MOTOR1_IN1_PORT, MOTOR1_IN1_PIN);
GPIO_ResetBits(MOTOR1_IN2_PORT, MOTOR1_IN2_PIN);
}
else if(speed < 0)
{
/* 反转:IN1低电平,IN2高电平 */
GPIO_ResetBits(MOTOR1_IN1_PORT, MOTOR1_IN1_PIN);
GPIO_SetBits(MOTOR1_IN2_PORT, MOTOR1_IN2_PIN);
}
else
{
/* 停止:IN1低电平,IN2低电平 */
GPIO_ResetBits(MOTOR1_IN1_PORT, MOTOR1_IN1_PIN);
GPIO_ResetBits(MOTOR1_IN2_PORT, MOTOR1_IN2_PIN);
}
break;
case 2: /* 左后轮 */
if(speed > 0)
{
GPIO_SetBits(MOTOR1_IN3_PORT, MOTOR1_IN3_PIN);
GPIO_ResetBits(MOTOR1_IN4_PORT, MOTOR1_IN4_PIN);
}
else if(speed < 0)
{
GPIO_ResetBits(MOTOR1_IN3_PORT, MOTOR1_IN3_PIN);
GPIO_SetBits(MOTOR1_IN4_PORT, MOTOR1_IN4_PIN);
}
else
{
GPIO_ResetBits(MOTOR1_IN3_PORT, MOTOR1_IN3_PIN);
GPIO_ResetBits(MOTOR1_IN4_PORT, MOTOR1_IN4_PIN);
}
break;
case 3: /* 右前轮 */
if(speed > 0)
{
GPIO_SetBits(MOTOR2_IN1_PORT, MOTOR2_IN1_PIN);
GPIO_ResetBits(MOTOR2_IN2_PORT, MOTOR2_IN2_PIN);
}
else if(speed < 0)
{
GPIO_ResetBits(MOTOR2_IN1_PORT, MOTOR2_IN1_PIN);
GPIO_SetBits(MOTOR2_IN2_PORT, MOTOR2_IN2_PIN);
}
else
{
GPIO_ResetBits(MOTOR2_IN1_PORT, MOTOR2_IN1_PIN);
GPIO_ResetBits(MOTOR2_IN2_PORT, MOTOR2_IN2_PIN);
}
break;
case 4: /* 右后轮 */
if(speed > 0)
{
GPIO_SetBits(MOTOR2_IN3_PORT, MOTOR2_IN3_PIN);
GPIO_ResetBits(MOTOR2_IN4_PORT, MOTOR2_IN4_PIN);
}
else if(speed < 0)
{
GPIO_ResetBits(MOTOR2_IN3_PORT, MOTOR2_IN3_PIN);
GPIO_SetBits(MOTOR2_IN4_PORT, MOTOR2_IN4_PIN);
}
else
{
GPIO_ResetBits(MOTOR2_IN3_PORT, MOTOR2_IN3_PIN);
GPIO_ResetBits(MOTOR2_IN4_PORT, MOTOR2_IN4_PIN);
}
break;
default:
break;
}
}
/**
* 整车电机控制函数
*
* 参数:
* left_speed: 左侧两个电机的速度 (-100到100)
* right_speed: 右侧两个电机的速度 (-100到100)
*/
void Motor_Control(int16_t left_speed, int16_t right_speed)
{
/* 控制左侧两个电机 */
Motor_SetSpeed(1, left_speed); /* 左前轮 */
Motor_SetSpeed(2, left_speed); /* 左后轮 */
/* 控制右侧两个电机 */
Motor_SetSpeed(3, right_speed); /* 右前轮 */
Motor_SetSpeed(4, right_speed); /* 右后轮 */
/* 更新全局速度变量 */
motor_left_speed = left_speed;
motor_right_speed = right_speed;
}
/**
* 串口发送字符串函数(用于调试)
*/
void USART_SendString(char* str)
{
while(*str)
{
/* 等待发送数据寄存器为空 */
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, *str);
str++;
}
}
/**
* 毫秒级延时函数(简易实现)
*/
void Delay_ms(uint32_t ms)
{
uint32_t i, j;
for(i = 0; i < ms; i++)
for(j = 0; j < 7200; j++);
}
/**
* 串口数据帧解析函数
*
* 数据帧格式(舵机数据):
* 帧头: 0xFF
* 数据: X角度高字节, X角度低字节, Y角度高字节, Y角度低字节
* 校验: 校验和(所有字节和的低8位)
*
* 数据帧格式(电机数据):
* 帧头: 0xFE
* 数据: 左轮速度值, 右轮速度值
* 校验: 校验和
*/
void Parse_Received_Data(void)
{
uint8_t i;
uint8_t checksum_calc = 0;
uint16_t x_angle, y_angle;
int16_t left_speed, right_speed;
/* 检查数据长度 */
if(rx_index < 2)
{
rx_index = 0;
return;
}
/* 判断帧头类型 */
if(rx_buffer[0] == 0xFF)
{
/* 舵机数据帧,需要6个字节 */
if(rx_index < 6)
{
return; /* 数据未接收完整,继续等待 */
}
/* 计算校验和 */
for(i = 0; i < 5; i++)
{
checksum_calc += rx_buffer[i];
}
checksum_calc &= 0xFF;
/* 校验和验证 */
if(checksum_calc == rx_buffer[5])
{
/* 解析角度数据 */
x_angle = ((uint16_t)rx_buffer[1] << 8) | rx_buffer[2];
y_angle = ((uint16_t)rx_buffer[3] << 8) | rx_buffer[4];
/* 更新舵机角度 */
current_servo_x = x_angle;
current_servo_y = y_angle;
/* 设置舵机PWM输出 */
Servo_SetAngle(SERVO_X_TIM, 1, x_angle);
Servo_SetAngle(SERVO_X_TIM, 2, y_angle);
}
/* 清除接收缓冲区 */
rx_index = 0;
memset((void*)rx_buffer, 0, RX_BUFFER_SIZE);
}
else if(rx_buffer[0] == 0xFE)
{
/* 电机数据帧,需要4个字节 */
if(rx_index < 4)
{
return; /* 数据未接收完整,继续等待 */
}
/* 计算校验和 */
for(i = 0; i < 3; i++)
{
checksum_calc += rx_buffer[i];
}
checksum_calc &= 0xFF;
/* 校验和验证 */
if(checksum_calc == rx_buffer[3])
{
/*
* 解析速度数据
* OpenMV发送的是0-200的值,100为停止
* 需要转换为-100到100的范围
*/
left_speed = (int16_t)rx_buffer[1] - 100;
right_speed = (int16_t)rx_buffer[2] - 100;
/* 控制电机 */
Motor_Control(left_speed, right_speed);
}
/* 清除接收缓冲区 */
rx_index = 0;
memset((void*)rx_buffer, 0, RX_BUFFER_SIZE);
}
else
{
/* 无效帧头,丢弃该字节,重新同步 */
for(i = 0; i < rx_index - 1; i++)
{
rx_buffer[i] = rx_buffer[i + 1];
}
rx_index--;
}
}
/**
* USART1中断服务函数
* 处理串口接收中断
*/
void USART1_IRQHandler(void)
{
uint8_t received_byte;
/* 检查是否是接收中断 */
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
/* 读取接收到的字节 */
received_byte = USART_ReceiveData(USART1);
/* 将字节存入接收缓冲区 */
if(rx_index < RX_BUFFER_SIZE)
{
rx_buffer[rx_index] = received_byte;
rx_index++;
/* 检测帧头,开始接收新的一帧 */
if(received_byte == 0xFF || received_byte == 0xFE)
{
if(rx_index == 1)
{
/* 这是帧头,正常开始接收 */
}
else
{
/*
* 在数据中间收到帧头,说明上一帧数据异常
* 丢弃之前的数据,重新开始接收
*/
rx_buffer[0] = received_byte;
rx_index = 1;
}
}
}
else
{
/* 缓冲区溢出,重置索引 */
rx_index = 0;
memset((void*)rx_buffer, 0, RX_BUFFER_SIZE);
}
/* 清除中断标志位 */
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
/* 检查溢出错误 */
if(USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET)
{
/* 读取DR寄存器清除ORE标志 */
USART_ReceiveData(USART1);
USART_ClearFlag(USART1, USART_FLAG_ORE);
}
}
/**
* 主函数
*/
int main(void)
{
/* 系统初始化 */
RCC_Configuration();
GPIO_Configuration();
USART_Configuration();
TIM_PWM_Configuration();
NVIC_Configuration();
/* 短暂延时等待外设稳定 */
Delay_ms(100);
/* 发送启动信息 */
USART_SendString("STM32 Smart Tracking Car Started!\r\n");
USART_SendString("Waiting for data from OpenMV...\r\n");
/* 初始时将舵机设置到中间位置 */
Servo_SetAngle(SERVO_X_TIM, 1, 90);
Servo_SetAngle(SERVO_X_TIM, 2, 90);
/* 初始停止所有电机 */
Motor_Control(0, 0);
while(1)
{
/* 检查是否有新数据需要解析 */
if(rx_index > 0)
{
/* 检查是否收到完整的数据帧 */
if((rx_buffer[0] == 0xFF && rx_index >= 6) ||
(rx_buffer[0] == 0xFE && rx_index >= 4))
{
Parse_Received_Data();
}
else if(rx_buffer[0] != 0xFF && rx_buffer[0] != 0xFE)
{
/* 无效数据,清除缓冲区重新同步 */
rx_index = 0;
memset((void*)rx_buffer, 0, RX_BUFFER_SIZE);
}
}
}
}
文件名:stm32f10x_it.c(中断服务函数文件)
这个文件是Keil工程自动生成的中断服务函数文件。我们需要确保USART1的中断服务函数被正确定义。如果中断服务函数已经在main.c中定义,则需要在此文件中注释掉或删除USART1的中断处理函数,避免重复定义导致编译错误。
c
#include "stm32f10x_it.h"
/*
* 注意:USART1_IRQHandler已经在main.c中定义
* 请确保此文件中没有重复定义该函数
* 如果此文件中已有USART1_IRQHandler的定义,请将其注释或删除
*/
/* SysTick中断处理函数(如果使用延时的话) */
void SysTick_Handler(void)
{
/* 系统滴答定时器中断处理 */
}
/* 其他中断处理函数保留默认的空实现即可 */
void NMI_Handler(void) {}
void HardFault_Handler(void) { while(1); }
void MemManage_Handler(void) { while(1); }
void BusFault_Handler(void) { while(1); }
void UsageFault_Handler(void) { while(1); }
void SVC_Handler(void) {}
void DebugMon_Handler(void) {}
void PendSV_Handler(void) {}
4.4 代码关键部分详解
串口数据接收和解析是整个STM32程序的核心。数据帧的设计采用了帧头区分法,0xFF表示舵机角度数据,0xFE表示电机速度数据。在中断服务函数中,每收到一个字节都会检查是否为帧头,如果是帧头则重新开始接收一帧数据。这种机制能够保证即使出现数据丢失或错位,系统也能快速恢复同步。
在Parse_Received_Data函数中,对接收到的数据进行校验和验证,只有校验正确的数据才会被用于控制舵机和电机。这是保证系统可靠性的重要措施。
舵机PWM的配置需要特别注意。SG90舵机的控制信号周期为20ms,通过改变高电平的持续时间来控制角度。0.5ms对应0度,1.5ms对应90度,2.5ms对应180度。定时器配置中,我们设置预分频器为71,使得定时器时钟为1MHz,每个计数周期为1us。自动重装载值设为19999,正好对应20ms的周期。通过设置比较寄存器的值,就可以精确控制高电平的持续时间。
五、系统联调与测试
5.1 程序烧录
将OpenMV通过USB线连接到电脑,在OpenMV IDE中打开main.py文件,点击工具栏上的"工具"按钮,选择"将打开的脚本保存到OpenMV Cam"。这样程序就会被保存到OpenMV的内部Flash中,下次上电时会自动运行。
将STM32通过ST-Link或USB转TTL模块连接到电脑。如果使用ST-Link,在Keil中点击"Download"按钮即可将程序烧录到STM32中。如果使用USB转TTL串口下载,需要先将STM32的BOOT0引脚接高电平,BOOT1接低电平,然后使用FlyMcu等烧录软件通过串口下载程序。
5.2 首次上电测试步骤
- 确保所有硬件连接正确,特别注意电源正负极不要接反。
- 先给STM32和电机驱动上电,此时舵机应该自动回到90度的中间位置。
- 然后给OpenMV上电,此时OpenMV上的蓝色LED会闪烁表示程序正在运行。
- 将目标物体(如红色小球)放在摄像头前方约30厘米处。
- 观察舵机云台是否跟随目标转动,小车是否朝向目标移动。
5.3 参数调试指南
如果小车运行不理想,需要逐步调整以下参数:
颜色阈值调整:如果目标不能被正确识别,首先需要重新获取颜色阈值。环境光线变化会显著影响颜色识别效果,建议在最终使用环境中进行阈值标定。
PID参数调整:OpenMV代码中的KP_X和KP_Y是比例控制系数。如果舵机反应太慢,增大这些值;如果舵机抖动,减小这些值。通常KP值在0.1到0.5之间比较合适。
速度参数调整 :在calculate_motor_speed函数中,base_speed控制基础速度,turn_factor控制转向灵敏度。根据实际场地的摩擦力情况调整这些参数。
5.4 常见问题排查
| 故障现象 | 可能原因 | 解决方法 |
|---|---|---|
| 舵机不转动 | PWM引脚配置错误 | 检查PA0和PA1的硬件连接和软件配置 |
| 电机不转动 | L298N使能端未拉高 | 确保ENA和ENB引脚输出高电平 |
| 串口无数据 | 波特率不匹配 | OpenMV和STM32都使用115200波特率 |
| 目标识别不稳定 | 环境光线变化 | 重新标定颜色阈值,或增加光照补偿 |
| 舵机剧烈抖动 | PID系数过大 | 减小KP_X和KP_Y的值 |
六、功能扩展建议
在完成基本功能后,你可以尝试以下扩展来提升小车的性能:
PID控制算法升级:将简单的比例控制升级为完整的PID控制,加入积分项和微分项,可以获得更平滑的追踪效果。积分项能够消除稳态误差,微分项能够减少超调量。
多目标追踪:修改OpenMV代码,同时识别多种颜色的目标,通过串口发送目标编号和坐标,使小车能够选择追踪特定颜色的目标。
避障功能集成:在小车前方安装超声波传感器,连接到STM32的ADC输入引脚。当检测到前方障碍物时,自动停止或绕行。
无线遥控切换:增加蓝牙或WiFi模块,实现手机APP远程控制和自动追踪模式的切换。
高级应用
进阶扩展
基础功能
颜色识别追踪
PID算法升级
多目标追踪
避障功能
无线遥控
路径规划
目标分类识别
自主导航
物联网接入
七、总结
通过本项目的完整实践,你掌握了OpenMV图像处理、串口通信协议设计、STM32外设驱动开发、舵机PWM控制以及电机驱动控制等嵌入式系统开发的核心技能。这些知识可以应用到更多复杂的机器人项目中。
整个系统的关键在于:OpenMV负责高层的视觉处理算法,STM32负责底层的实时控制,两者通过自定义的串口协议高效协作。这种主从架构在嵌入式系统中非常常见,理解并掌握这种设计思想对于后续更复杂项目的开发非常有帮助。