OpenMV与STM32标准库实现舵机颜色追踪控制
一、硬件连接
1.OpenMV端:由图知UART_RX—P5 —— UART_TX—P42.STM32端:USART_TX—PA9 —–USART_RX—PA10
3.四针OLED IIC连接:SDA—PA2—–SCL—PA1 由于使用的是模拟IIC而不是硬件IIC,可以根据个人需要修改IO口来控制SDA线和SCL线,只需要简单修改一下代码即可。
4.STM32的TX–openmvRX,openmv的RX–stm32-TX
5.OLED的连接
这里直接调用江科大的就不再多提了
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOA, GPIO_Pin_2, (BitAction)(x))
6.pwm(也就是舵机的连接) 注意:舵机连接所需的电压
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_3;
注意事项:
7.openmv与stm32设置的波特率必须一致
图片多了两个模块其实是给舵机提供电压的,这个根据自己所需即可
二、openmv
理论部分
视觉的逻辑采用了番茄哥的思维(参考原文http://t.csdnimg.cn/yCY7s)
将图像分为5部分区域,在不同的区域发送不同的数据信号。
from pyb import UART
import sensor
import time
import image
def init_sensor():
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)#自动白平衡功能的作用是自动调整图像的颜色平衡,以消除不同光源颜色对图像颜色的影响
def init_uart():#初始化串口用于与其他设备进行通信
return UART(3, 115200)
# 查找最大的色块
def find_max(blobs):
max_blob = None#用于存储当前找到的最大色块对应的 blob 对象,。将 max_blob 初始化为 None,代表当前不存在最大的色块。None 是 Python 里的一个特殊值,用于表示变量没有值或者对象不存在。
max_size = 0#用于记录当前找到的最大色块所包含的像素数量。初始化为 0 能确保在首次比较时,任何非空的 blob 对象的像素数量都会大于 max_size,从而使比较过程得以正常开展
for blob in blobs:
if blob.pixels() > max_size:#若当前 blob 对象的像素数量大于已记录的最大像素数量 max_size,就更新 max_blob 为当前 blob 对象
max_size = blob.pixels()
max_blob = blob
return max_blob
# 标记最大色块
def mark_max_blob(img, max_blob):
blob_cx = max_blob.cx()#用于获取该色块的中心 x 坐标
blob_cy = max_blob.cy()
img.draw_cross(blob_cx, blob_cy)#画十字架
img.draw_rectangle(max_blob.rect())#画矩形
return blob_cx, blob_cy
#发送命令到串口
def send_control_commands(uart, x_offset, y_offset):
# 开启异常处理,捕获并处理可能出现的异常
try:
# 定义较大的偏移量阈值,用于判断是否需要较大幅度的调整
LARGE_OFFSET = 20
# 定义较小的偏移量阈值,用于判断是否需要较小幅度的调整
SMALL_OFFSET = 10
# 根据水平偏移量 x_offset 的大小,确定水平方向的控制指令
if x_offset < -LARGE_OFFSET:
# 当水平偏移量小于负的较大阈值时,设定控制指令为 'a'
command = 'a'
elif x_offset > LARGE_OFFSET:
# 当水平偏移量大于较大阈值时,设定控制指令为 'c'
command = 'c'
elif -LARGE_OFFSET <= x_offset < -SMALL_OFFSET:
# 当水平偏移量在负的较大阈值和负的较小阈值之间时,设定控制指令为 'b'
command = 'b'
elif SMALL_OFFSET < x_offset <= LARGE_OFFSET:
# 当水平偏移量在较小阈值和较大阈值之间时,设定控制指令为 'd'
command = 'd'
else:
# 当水平偏移量在较小阈值范围内时,不设置水平方向的控制指令
command = None
# 根据垂直偏移量 y_offset 的大小,确定垂直方向的控制指令
if y_offset < -LARGE_OFFSET:
# 当垂直偏移量小于负的较大阈值时,设定控制指令为 'w'
vertical_command = 'w'
elif y_offset > LARGE_OFFSET:
# 当垂直偏移量大于较大阈值时,设定控制指令为 'x'
vertical_command = 'x'
elif -LARGE_OFFSET <= y_offset < -SMALL_OFFSET:
# 当垂直偏移量在负的较大阈值和负的较小阈值之间时,设定控制指令为 'y'
vertical_command = 'y'
elif SMALL_OFFSET < y_offset <= LARGE_OFFSET:
# 当垂直偏移量在较小阈值和较大阈值之间时,设定控制指令为 'z'
vertical_command = 'z'
else:
# 当垂直偏移量在较小阈值范围内时,设定控制指令为 'o'
vertical_command = 'o'
# 确定最终要发送的控制指令
# 如果水平方向有有效的控制指令,则优先使用水平方向的指令
# 否则,使用垂直方向的指令
final_command = command if command is not None else vertical_command
# 通过串口 uart 发送最终确定的控制指令
uart.write(final_command)
# 捕获并处理可能出现的异常
except Exception as e:
# 打印错误信息,方便调试
print(f"Error sending commands: {e}")
def main():
init_sensor()
uart = init_uart()
red_threshold = [
(75, 43, 56, 30, -2, 28),
(70, 40, 64, 38, -19, 14),
]#设置颜色的阈值
clock = time.clock()#可以记录每次循环开始和结束的时间,通过时间差来计算帧率
while True:
clock.tick()
img = sensor.snapshot()
blobs = img.find_blobs(red_threshold)
fps = clock.fps()
print(f"FPS: {fps}")
if blobs:
max_blob = find_max(blobs)
blob_cx, blob_cy = mark_max_blob(img, max_blob)
img_center_x = img.width() // 2
img_center_y = img.height() // 2
#分别计算当前图像在水平方向和垂直方向上的中心坐标。
#值为 320 // 2 = 160;通过 img_height // 2 计算出图像垂直方向的中心坐标 img_center_y,其值为 240 // 2 = 120。
#240//2=120
x_offset = blob_cx - img_center_x
y_offset = blob_cy - img_center_y
#接着用最大色块的中心横坐标 blob_cx 减去图像水平中心坐标 img_center_x,
#得到水平偏移量 x_offset = 180 - 160 = 20;用最大色块的中心纵坐标 blob_cy 减去图像垂直中心坐标 img_center_y,
#得到垂直偏移量 y_offset = 150 - 120 = 30。
send_control_commands(uart, x_offset, y_offset)
else:
send_control_commands(uart, 0, 0)
time.sleep(50 / 1000)
if __name__ == "__main__":
main()
这是运行后的效果
这个时候就有人要问了,主播主播,怎么调节我想要识别的颜色的阈值啊?
我们可以选择左上角的工具栏找到-工具-机器视觉-阈值编辑器
然后把想要的颜色调节成白色就是我们想要识别的颜色了,然后复制对应的阈值放到代码里面。
接下来,由我们通过上面代码或者自己写的代码通过串口发送给stm32‘a’'b''c''d''e'等
最重要的一部就是openmv不要忘记把代码下载进去,主播就因为这个浪费了很多时间
以下是保存脚本到openmv的stm32方法
三、STM32
STM32与openmv可以通信参考江科大的那一章
PWM.c
#include "stm32f10x.h" // Device header
// PWM 初始化函数
void PWM_Init(void)
{
// 使能 TIM2 和 GPIOA 的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 定义 GPIO 初始化结构体
GPIO_InitTypeDef GPIO_InitStructure;
// 配置 GPIO 为复用推挽输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
// 选择 PA3 和 PA4 引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_3;
// 设置 GPIO 速度为 50MHz
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// 初始化 GPIOA
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 选择 TIM2 为内部时钟源
TIM_InternalClockConfig(TIM2);
// 定义定时器时基初始化结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
// 时钟分频选择不分频
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
// 计数器模式选择向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 设置计数周期,即 ARR 的值
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;记满2000溢出
// 设置预分频器,即 PSC 的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
// 重复计数器,高级定时器才会用到,这里设为 0
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//20ms
// 初始化 TIM2 的时基单元
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
// 定义定时器输出比较初始化结构体
TIM_OCInitTypeDef TIM_OCInitStructure;
// 初始化结构体成员为默认值
TIM_OCStructInit(&TIM_OCInitStructure);
// 输出比较模式选择 PWM 模式 1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
// 输出极性选择高电平有效
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 输出使能
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
// 初始的 CCR 值设为 0
TIM_OCInitStructure.TIM_Pulse = 0;
// 初始化 TIM2 的通道 3,对应 PA3 引脚
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 初始化 TIM2 的通道 4,对应 PA4 引脚
TIM_OC4Init(TIM2, &TIM_OCInitStructure);
// 使能 TIM2
TIM_Cmd(TIM2, ENABLE);
}
用于设置定时器指定通道的比较寄存器的值。在 PWM 模式下,比较寄存器的值决定了 PWM 信号的占空比。
// 设置 TIM2 通道 3 的比较值(CCR3)
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
// 设置 TIM2 通道 4 的比较值(CCR4)
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare4(TIM2, Compare);
}
Serial.c
#include "stm32f10x.h" // 包含 STM32F10x 系列微控制器的设备头文件,提供对寄存器、结构体等的定义
#include <stdio.h> // 标准输入输出头文件,用于 printf 等函数的声明
#include <stdarg.h> // 提供处理可变参数列表的宏和类型定义
// 定义全局变量,用于存储接收到的串口数据字节
uint8_t Serial_RxData;
// 定义全局变量,作为接收数据的标志位,接收到新数据时置为 1,读取数据后可清零
uint8_t Serial_RxFlag;
// 串口初始化函数
void Serial_Init(void)
{
// 使能 USART1 的时钟,USART1 是 STM32 的一个串口外设,使用前需使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 使能 GPIOA 的时钟,因为 USART1 的 TX(PA9)和 RX(PA10)引脚在 GPIOA 上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 定义 GPIO 初始化结构体,用于配置 GPIO 引脚参数
GPIO_InitTypeDef GPIO_InitStructure;
// 配置 USART1 的 TX 引脚(PA9)为复用推挽输出模式,用于发送数据
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置 USART1 的 RX 引脚(PA10)为上拉输入模式,用于接收数据
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定义 USART 初始化结构体,用于配置 USART1 的参数
USART_InitTypeDef USART_InitStructure;
// 设置 USART1 的波特率为 115200,波特率决定了数据传输的速度
USART_InitStructure.USART_BaudRate = 115200;
// 配置 USART1 无硬件流控制,即不使用硬件信号来控制数据的传输
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
// 使能 USART1 的发送和接收模式,允许数据的发送和接收
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
// 配置 USART1 无奇偶校验位,不进行奇偶校验
USART_InitStructure.USART_Parity = USART_Parity_No;
// 配置 USART1 有 1 个停止位,用于表示数据帧的结束
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置 USART1 数据位为 8 位
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 根据配置的参数初始化 USART1
USART_Init(USART1, &USART_InitStructure);
// 使能 USART1 的接收缓冲区非空中断(USART_IT_RXNE),当接收缓冲区有数据时触发中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 设置 NVIC(嵌套向量中断控制器)的中断优先级分组为 2
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 定义 NVIC 初始化结构体,用于配置 USART1 中断的优先级等参数
NVIC_InitTypeDef NVIC_InitStructure;
// 设置 NVIC 中断通道为 USART1 中断通道
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
// 使能 USART1 中断通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 设置 USART1 中断的抢占优先级为 1
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// 设置 USART1 中断的子优先级为 1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
// 根据配置的参数初始化 NVIC
NVIC_Init(&NVIC_InitStructure);
// 使能 USART1 外设,使其开始工作
USART_Cmd(USART1, ENABLE);
}
// 向 USART1 发送一个字节数据的函数
void Serial_SendByte(uint8_t Byte)
{
// 向 USART1 发送指定的字节数据
USART_SendData(USART1, Byte);
// 等待 USART1 的发送缓冲区为空标志(USART_FLAG_TXE)置位,确保数据发送完成
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 向 USART1 发送一个字节数组的函数
void Serial_SendArray(uint8_t *Array, uint8_t Length)
{
int i;
// 遍历字节数组,逐个发送数组中的字节
for (i = 0; i < Length; i++)
{
Serial_SendByte(Array[i]);
}
}
// 向 USART1 发送一个字符串的函数
void Serial_SendString(char *String)
{
// 遍历字符串,逐个发送字符串中的字符,直到遇到字符串结束符 '\0'
for (int i = 0; String[i] != 0; i++)
{
Serial_SendByte(String[i]);
}
}
// 计算 X 的 Y 次方的函数,用于数字发送函数中
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t result = 1;
// 通过循环计算 X 的 Y 次方
while (Y--)
{
result *= X;
}
return result;
}
// 向 USART1 发送一个无符号 32 位整数的函数,按照指定长度以 ASCII 码形式发送
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
uint8_t Byte;
// 遍历指定长度,分离出整数的每一位数字,并转换为对应的 ASCII 码字符发送
for (i = 0; i < Length; i++)
{
Byte = (Number / Serial_Pow(10, Length - i - 1)) % 10;
//
Serial_SendByte(Byte + 0x30);
}
}
// 重映射 fputc 函数,使其通过串口发送字符,从而实现 printf 函数通过串口发送数据的功能
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
// 实现类似 printf 的功能,支持可变参数,通过串口发送格式化后的字符串
void Serial_Printf(char *format, ...)
{
char String[100];
// 定义可变参数列表
va_list arg;
// 初始化可变参数列表
va_start(arg, format);
// 将格式化后的字符串存储到 String 数组中
vsprintf(String, format, arg);
// 结束可变参数列表的使用
va_end(arg);
// 通过串口发送格式化后的字符串
Serial_SendString(String);
}
// 获取接收数据标志位的函数,并在读取后清零标志位
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
// 返回接收到的数据字节的函数
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
// USART1 中断处理函数,当 USART1 接收到数据触发接收缓冲区非空中断时执行
void USART1_IRQHandler(void)
{
// 检查 USART1 的接收缓冲区非空中断标志位是否置位
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
// 读取 USART1 接收到的数据,并存储到 Serial_RxData 中
Serial_RxData = USART_ReceiveData(USART1);
// 置位接收标志位,表示接收到了新数据
Serial_RxFlag = 1;
// 清除 USART1 的接收缓冲区非空中断标志位,准备接收下一次数据
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
Servo.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
PWM_Init();
}
void Servo1_SetAngle(float Angle)
{
//PWM_SetCompare2(Angle / 180 *2000 +500);
PWM_SetCompare1(Angle / 180 *2000 +500);
}
void Servo2_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 *2000 +500);
//PWM_SetCompare1(Angle / 180 *2000 +500);
}
main.c
#include "stm32f10x.h"// Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "Serial.h"
#include "Servo.h"
uint8_t RxData;
float Angle1 = 165.0, Angle2 = 35;
int main(void)
{
Serial_Init();
OLED_Init();
Servo_Init();
Servo1_SetAngle(Angle1);
Servo2_SetAngle(Angle2);
OLED_ShowString(1, 1, "RxData");
while (1)
{
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
// 当接收到新数据时,在OLED上显示
OLED_ShowChar(1, 8, RxData);
switch (RxData)
{
case 'a'://右
Angle1 += 2;
break;
case 'd':
Angle1 += 1;
break;
case 'c'://左
Angle1 -= 2;
break;
case 'b':
Angle1 -= 1;
break;
case 'w'://下
Angle2 += 1.5;
break;
case 'z':
Angle2 += 1;
break;
case 'y':
Angle2 -= 1;
break;
case 'x'://上
Angle2 -= 1.5;
break;
case 'o':
// 可以在这里添加针对 'o' 的其他处理逻辑
break;
default:
break;
}
// 重置 RxData
RxData = 0;
}
// 限制角度范围
if (Angle1 < 0) Angle1 = 0;
if (Angle1 > 180) Angle1 = 179;
if (Angle2 < 0) Angle2 = 0;
if (Angle2 > 180) Angle2 = 180;
// 设置舵机角度
Servo1_SetAngle(Angle1);
Servo2_SetAngle(Angle2);
}
}
main.c的舵机控制角度需根据自己需求更改
最后是星瞳科技的学习参考资料网址
序言 · OpenMV中文入门教程https://book.openmv.cc/
MicroPython库 — MicroPython 1.22 文档https://docs.singtown.com/micropython/zh/latest/openmvcam/library/index.html
作者:ldf819455946