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

    作者:ldf819455946

    物联沃分享整理
    物联沃-IOTWORD物联网 » OpenMV与STM32标准库实现舵机颜色追踪控制

    发表回复