一:系统概述
本系统使用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多级菜单