【毕业设计】基于STM32F103C8T6最小系统板与OpenMV的二维云台PID控制追踪系统

一:系统概述

本系统使用OpenMV作为摄像头,追踪红色小球。系统利用OpenMV内置的MicroPython库中的find_blobs函数查找识别红色小球,接着调用find_max()函数进行滤波,将红色小球中心的X,Y坐标打包成数据包,通过串口发送给主控STM32F103C8T6最小系统板,当STM32F103C8T6通过串口中断接收到位置信息后通过PID算法输出合适的PWM波对X轴、Y轴的两个舵机控制旋转相应的角度,使OpenMV摄像头对准被测物体,以实现物体追踪功能。

二:硬件选型

(一):实验版本

1.云台部分

1.1:二维舵机云台带轴承

由于预算充足故选用了某宝的二维电动云台(带轴承)如图所示:

1.2:数字舵机

X轴采用S20系列270°舵机,工作电压5V,堵转电流1.8A,20KG负载马力十足。

Y轴采用S20系列180°舵机,工作电压5V,堵转电流1.8A,20KG负载马力十足。

Tips:

1:舵机必须与STM32F103C8T6共地!!否则无法控制舵机!!

2:舵机调试仪可以用来调试舵机中值,也可以检测舵机好坏。

2.主控部分

2.1:STM32F103C8T6最小系统板

本系统使用STM32F103C8T6最小系统板作为核心MCU,实物如所示。

本系统中STM32F103C8T6使用串口中断与OpenMV进行串口通信,进行数据接收,当其接收到数据后通过硬件IIC与OLED相连输出X轴与Y轴的坐标,并且输出两路PWM波分别控制X轴与Y轴的舵机。

3.传感器部分

3.1:0.96寸OLED显示屏

OLED模块供电可以是3.3V,也可以是5V。本系统使用5V供电。OLED屏具用于显示X轴与Y轴的点坐标。在实验的初期,主要利用OLED检验是否可以正常的通过串口中断进行数据传输,并且检验传输的数据是否正确。

Tips:1:GND与VCC千万不要接反,接反轻则烫手,重则烧屏。

3.2:OpenMV 4 H7 PLUS

本系统中OpenMV部分完成三个任务:

1:完成被测物体的识别(以红色小球为例)。

2:寻找最大色块区域。

3:通过串口将被测物体的位置信息发送给STM32F103C8T6。

Tips:

1:本实验中我们使用5V的电压脱机运行。

2:OpenMV与STM32F103C8T6进行数据传输必须共地!!!

4.稳压部分

4.1:稳压模块

MP1584固定输出5V的稳压模块。输出电压:5V;输出电流:3A(峰值),长时间工作1.5A左右。

5V的电压1.5A-3A的电流可以同时给Openmv与STM32F103C8T6,OLED,舵机供电。

5.电源部分

5.1:电源模块

本系统使用了两节18650锂电池搭配串联构成7.4V的电源。

(二):集成版本

杜邦线承载的电流有限,无法长时间负载大电流,并且信号传递不稳定。为了优化与简化电路,使得系统稳定工作,故绘制了主控PCB电路板,为了方便开源,使得各位嵌入式爱好者使用,故特意设计成排插式主控,只需要简单的焊接排针排母以及开关,就可以使用。主控板9cm*9cm配合嘉立创每个月免费打印两次PCB板子,经济又实惠。

1:PCB的3D立体示意图:

2:PCB焊接实际图:

三:整体功能分析

OpenMV:识别目标物体,并将其位置信息通过串口传递给STM32。

STM32的串口部分:串口中断接收OpenMV发来的位置信息。

STM32的定时器部分:利用PID算法输出PWM波,从而控制X、Y轴的两个舵机旋转。

四:软件实现

1:OpenMV端

1.1:外设整体逻辑分析

1.2:阈值调整及初始化

编程中首先需要对OpenMV的红色的阈值进行调整,路径为:工具->视觉终端->阈值编辑器。打开阈值编辑器,对LAB的阈值进行调整,使二进制图像中只有红色区域的映像(白色像素是被跟踪的像素)。

注:不同的光照会对颜色识别造成很大的影,所以请务必在稳定的光照环境下调整阈值以及识别。

import sensor, image, time, pyb
from pyb import UART

#【第一步:对Openmv进行初始化】
sensor.reset()                          # 初始化摄像头传感器.
sensor.set_pixformat(sensor.RGB565)     # 使用RGB565.
#sensor.set_framesize(sensor.QQVGA)     # 分辨率 160*120
sensor.set_framesize(sensor.QVGA)       # 分辨率 320*240
sensor.set_auto_gain(False)             # 关闭自动增益
sensor.set_auto_whitebal(False)         # 关闭自动白平衡.
#sensor.set_auto_exposure(False, 1400)
sensor.skip_frames(time = 2000)         # 让新设置生效.

clock = time.clock()                    # Tracks FPS.

uart = UART(3, 115200)                   #串口初始设定
red_threshold = (41, 72, 47, 82, -8, 70) #红色阈值设定

经过实验测试,在QQVGA,160*120像素排布下,脱机运行速度快但是精度较低。 故本系统使用的分辨率标准为QVGA,320*240像素排布,在牺牲系统脱机运行速度(帧率)的条件下,提高数据的精度。两种像素排布均可满足要求,读者自行选择。

1.3:寻找最大色块区域

在OpenMV追踪识别的过程中,可能出现背景或是其他区域出现小面积的红色区域,这会对识别造成干扰,所以需要用程序过滤掉那些小的红色区域。通过对识别到的红色区域进行比较,找出所有红色区域中最大的区域,即可避免背景中的小面积红色区域对识别的影响。

这里封装一个名为find_max()的子函数,该函数主要实现的功能就是寻找最大的红色区域,目的是滤波,可以消除噪音(误差) 干扰。在主函数中调用该函数以实现该功能,避免背景中的小面积红色区域对识别的影响。

#【第二步:滤波】
def find_max(blobs):
    max_size = 0
    for blob in blobs:
        if blob.pixels() > max_size:
            max_blob = blob
            max_size = blob.pixels()
    return max_blob

1.4:寻找色块数据处理

在main函数中调用find_blobs()函数识别红色区域,接着调用find_max()函数进行滤波,并且返回红色区域的XY轴坐标,这就是我们后续要串口发送给STM32F103C8T6的目标数据(CX,CY)。

注释:draw_rectangle()函数与draw_cross()函数方便在电脑端调试,并无实际作用。

#【第四步:寻找色块并发送数据】
while(True):
    img = sensor.snapshot()                     # 拍照并返回图像
    blobs = img.find_blobs([red_threshold])

    if blobs:
        max_blob=find_max(blobs)

        img.draw_rectangle(max_blob.rect())          #画外围正框
        img.draw_cross(max_blob.cx(), max_blob.cy()) #画中心十字

        CX = max_blob.cx()
        CY = max_blob.cy()

        sending_data(CX,CY)

        print("YES")

1.5:发送数据包

由于我使用的分辨率标准为QVGA,320*240像素排布。但是接收端STM32F103C8T6只能接收一个8位2^8也就是0-255大小的数,故需要对数据进行处理。

这里将Cx除以100取整得到百位,Cx除以100取余得到个位与十位。

同理将Cy除以100取整得到百位,Cy除以100取余得到个位与十位。

特别注意:对于//,是向下取整,即不会进行四舍五入。所以15//4 =  3的结果是3而不是4。

#【第三步:发送数据包】
def sending_data(Cx,Cy):
    Data = bytearray([0x01,0x03,Cx//100,Cx%100,Cy//100,Cy%100,0x05])
    #                 帧头1 帧头2  X百位   X剩余   Y百位   Y剩余  帧尾
    uart.write(Data);
    print(Cx,Cy)

这里的0x01,0x03是帧头。帧头是数据帧的起始标识,用于标识一个数据帧的开始。它通常是一个特定的字符或字符序列,用于通知接收端数据帧即将开始传输。通过识别帧头,接收端可以准确地定位数据帧的起始位置,从而正确地解析整个数据帧。

这里的0x05是帧尾。帧尾是数据帧的结束标识,用于标识一个数据帧的结束。它通常也是一个特定的字符或字符序列。在发送端,帧尾用于标识数据帧的结束,并计算校验,以确保数据帧在传输过程中没有发生错误。在接收端,通过校验和和帧尾的识别,可以验证数据帧的完整性,并确保数据帧正确无误地接收。

帧头和帧尾的作用是提供一种可靠的通信机制,以确保数据帧在传输过程中的完整性和准确性。通过识别帧头和帧尾,接收端可以准确地定位数据帧的起始和结束位置,并验证数据帧的完整性。这有助于避免数据传输过程中的误码和丢包问题,提高串口通信的可靠性和稳定性。

Tips:帧头和帧尾应该具有唯一性,即它们在数据中只出现一次,以便正确地标识数据帧的起始和结束位置,故应该选一些与发送数据不相等的值作为帧头和帧尾的选择。

2:STM32F103C8T6端

2.1:主控整体逻辑分析

2.2:基本外设的初始化

由于该部分过于基础且并无难度故不详细进行说明,如有不清楚的地方可以在评论区提问,或者去B站观看STM32的基础视频。

这里给出本项目所使用的GPIO口如图所示:

2.3:串口中断接收数据

这里的0x01,0x03是帧头。帧头是数据帧的起始标识,用于标识一个数据帧的开始。它通常是一个特定的字符或字符序列,用于通知接收端数据帧即将开始传输。通过识别帧头,接收端可以准确地定位数据帧的起始位置,从而正确地解析整个数据帧。

这里的0x05是帧尾。帧尾是数据帧的结束标识,用于标识一个数据帧的结束。它通常也是一个特定的字符或字符序列。在发送端,帧尾用于标识数据帧的结束,并计算校验,以确保数据帧在传输过程中没有发生错误。在接收端,通过校验和和帧尾的识别,可以验证数据帧的完整性,并确保数据帧正确无误地接收。

帧头和帧尾的作用是提供一种可靠的通信机制,以确保数据帧在传输过程中的完整性和准确性。通过识别帧头和帧尾,接收端可以准确地定位数据帧的起始和结束位置,并验证数据帧的完整性。这有助于避免数据传输过程中的误码和丢包问题,提高串口通信的可靠性和稳定性。

这里的i是接收数组的标注位,若是帧头帧尾不匹配,则标志位复位。

    if(huart->Instance == USART1)
    {
        if(HAL_UART_GetState(&huart1) != RESET)  //接收中断
        {
            HAL_UART_Receive(&huart1,(uint8_t*)g_rx_buffer,7,1000);//(USART1->DR);	//读取接收到的数据
            i = 0;
            if(g_rx_buffer[0] != 0x01)
            {
                OLED_ShowString(0,0,"Err1",16 ,1);
                OLED_Refresh();
                i = 0;//清空标志位
            }
            else i++;
            if( (i!=1) && (g_rx_buffer[1] != 0x03)   )
            {
                OLED_ShowString(0,0,"Err2",16 ,1);
                OLED_Refresh();
                i = 0;//清空标志位
            }
            else i++;
            if(i == 2)
            {
                if(g_rx_buffer[6] == 0x05)
                {
/*----------------------------代码的核心部分写在这里----------------------------*/
                }
            }
            i = 0;//清空标志位
        }
        else OLED_ShowString(0,0,"Err",16 ,1);
    }

2.4:数据整合以及测试

这里将g_rx_buffer[2]乘以100取整得到百位, g_rx_buffer[3]为十位与个位,相加得到x坐标。

这里将g_rx_buffer[4]乘以100取整得到百位, g_rx_buffer[5]为十位与个位,相加得到y坐标。

/*---------------------------------------------------------------------------------------*/
/*-------------------------------数据整合部分---------------------------------------------*/
                    Cx = g_rx_buffer[2]*100 + g_rx_buffer[3];   //真实x坐标
                    Cy = g_rx_buffer[4]*100 + g_rx_buffer[5];   //真实y坐标
/*-------------------------------数据整合完成---------------------------------------------*/
/*---------------------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------------------*/
/*-------------------------------数据测试部分---------------------------------------------*/
                    OLED_ShowString(0,0,"YES!",16 ,1);
                    OLED_ShowString(0,16,"Cx =",16 ,1);
                    OLED_ShowString(0,32,"Cy =",16 ,1);
                    OLED_ShowNum(40,16,Cx,3,16,1);
                    OLED_ShowNum(40,32,Cy,3,16,1);
                    OLED_Refresh();
/*-------------------------------数据测试完成---------------------------------------------*/
/*---------------------------------------------------------------------------------------*/

2.5:PID控制以及限幅

2.5.1:整体控制流程

首先通过位置式PID控制器算出PWM输出值,接着进行速度限幅与角度限幅,最后输出PWM。

Velocity_X_middle=Position_PID_2(Cx,Target_Cx);        //X轴
Velocity_Y_middle=Position_PID_1(Cy,Target_Cy);        //Y轴
Xianfu_Velocity(Velocity_X_middle,Velocity_Y_middle);  //增量限幅
Set_Pwm(Velocity_X_middle,Velocity_Y_middle);          //PWM输出
2.5.2:PID控制
float Position_PID_1(float Position,float Target)
{                                                            //增量输出
    Bias_y=Target-Position;                                  //计算当前偏差
    Integral_bias_y+=Bias_y;                                 //求出偏差的积分
    Pwm_y=Position_KP*Bias_y+Position_KI*Integral_bias_y+Position_KD*(Bias_y-Last_Bias_y);       //位置式PID控制器
    Last_Bias_y=Bias_y;                                      //保存上一次偏差 
    return Pwm_y;  
}
2.5.3:速度限幅
void Xianfu_Velocity(float Velocity1,float Velocity2)
{
    //Amplitude 英文为幅度
    int Amplitude_H =  Speed;
    int Amplitude_L = -Speed;  
    if(Velocity1<Amplitude_L)   Velocity1=Amplitude_L;	
    if(Velocity1>Amplitude_H)   Velocity1=Amplitude_H;	
    if(Velocity2<Amplitude_L)   Velocity2=Amplitude_L;	
    if(Velocity2>Amplitude_H)   Velocity2=Amplitude_H;	
}
2.5.4:角度限幅

每一次PID算出来的值,不是直接作为最终的PWM装载值,而是作为增量,累加到之前的PWM装载值上,然后再把累加后的PWM装载值作为我们最终的PWM装载值。

void Set_Pwm(float velocity_X_real,float velocity_Y_real)
{
    Position_X=Position_X+velocity_X_real;           //速度的积分,得到舵机的位置
    Position_Y=Position_Y+velocity_Y_real;           //速度的积分,得到舵机的位置

    if(Position_Y< 675)   Position_Y= 675;	
    if(Position_Y>1350)   Position_Y=1350;	
    if(Position_X<1000)   Position_X=1000;	
    if(Position_X>2000)   Position_X=2000;	

    __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,Position_Y);
    __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,Position_X);
}

2.6:测试代码

2.6.1:OLED显示测试代码

 这里的Test_Oled()代码进行OLED显示测试,显示需要的数据,检测数据传输与显示是否正常。

void Test_Oled(void)
{
    OLED_ColorTurn(0);
    OLED_DisplayTurn(0);
    OLED_ShowString(0, 0,"I Love Puppy!!!!",16 ,1);
    OLED_ShowString(0,16,"       From_Amai",16 ,1);
    OLED_ShowString(0,32,"         To_Amai",16 ,1);
    OLED_ShowString(0,48,"       For Puppy",16 ,1);
    OLED_Refresh();
}
2.6.2:X,Y轴舵机测试代码

 这里的Test_Servo()代码进行舵机角度测试,寻找舵机中值以及舵机起始位置。

void Test_Servo(void)
{
		__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_1,675);
		__HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_2,1600);
}

五:舵机调参

关于舵机云台PID调参:
首先说明使用的是位置式、单环(一个舵机一个环)、组合方式P+D。
1. 调参步骤: 先调P到过冲(在目标位置摇摆几下),然后加D抑制过冲(增加阻尼)。
2. 思考:目标静止时,想提高摄像头找到目标(使目标位于摄像头视野中央)速度就尽可能加大P;目标运动时,想提高摄像头追踪目标的速度就尽可能加大D。
3. 常见问题: 出现高频抖动,可能是D太大、或是摄像头的误识别、或是舵机精度太低(舵机太烂);响应频率慢可能是摄像头识别率太低、或是OLED上显示了太多字符串、或是画质设置太高、或是PID计算时间间隔太长。

六:视频展示

二维云台PID控制追踪系统

七:项目总结

本次项目主要为了锻炼自己解决工程问题的能力,所有代码都是自己独立完成,所有的硬件电路都是自己一点一点搭建起来的。在解决工程时,首先要列出要实现的目标功能,画出要搭建的电路结构,然后在脑子里搭建工程的机械结构。接着将项目拆解出独立的模块,分模块逐个完成,最后将其一一汇总起来。最后优化工程,绘制PCB板,优化代码工程。作为嵌入式小白,不能好高骛远,想着一口气就做出最完美的结构与系统,眉毛胡子一把抓,只会适得其反。

八:项目提升

(一)硬件:

1:Openmv补光板,减弱外界环境干扰

2:PCB电路板优化,提升结构稳定性

(二)软件:

1:Openmv端

1.1:拓展人脸识别,车牌识别,使识别物体更多样

1.2:卡尔曼滤波,预测目标物体的运动轨迹

2:STM32端

2.1:PID参数优化

2.2:OLED多级菜单

  • 28
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 28
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值