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;

注意事项:

  • 电平匹配: 确保两者的工作电压一致,通常都是3.3V。
  • 引脚对应: OpenMV的TX连接到STM32的RX,反之亦然。
  • 共地: 两个设备必须连接共同的地线。

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值