2023届电子设计大赛E题(控制类)

本文围绕电子设计大赛云台题目展开。基础题用一个云台,发挥题用两个云台。最初采用区域法,将openmv分5个区域处理,但存在误差。后改用PID算法,通过求出红点和绿点坐标差值,经公式调参,在摄像头用Python编程求坐标,数据传至32单片机处理,最终达题目要求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先上视频!

1.首先我们来看看题目

 

         题目还是比较简单明白的,基础题只需要用到一个云台去做这个题目,发挥题需要用到两个云台去解决,首先前几题除了写死没有更好的办法了,我们直接来说一下发挥题怎么做吧,怎么去解决这个问题吧。

        做发挥题的时候我是遇到了很多问题的,开始我用的方法是区域法,就是让我识别到的绿点在我的像素中心,刚好我的红点也在我的像素中心。于是我用了这样的方法:将openmv分成5个区域,这样的的话,识别到的点在不同区域就可对应的处理了,将我们寻找到的发送给单片机去处理,不同区域不同的方法解决问题。

IDE(openMV摄像头代码):基于python

# Single Color RGB565 Blob Tracking Example
#
# This example shows off single color RGB565 tracking using the OpenMV Cam.
from pyb import UART#开启串口
import sensor, image, time, math

threshold_index = 0 # 0 for red, 1 for green, 2 for blue

# Color Tracking Thresholds (L Min, L Max, A Min, A Max, B Min, B Max)
# The below thresholds track in general red/green/blue things. You may wish to tune them...
thresholds = [(63, 100, 6, 69, -41, 42)] # generic_blue_thresholds


uart = UART(3, 9600)
x_max = 320
x_min = 0
x_1 = 155 #中心区域左边界
x_2 = 163 #中心区域右边界

y_max = 240
y_min = 0
y_1 = 115 #中心区域上边界
y_2 = 123 #中心区域下边界
flag = 0#位置信息标志


sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False) # must be turned off for color tracking
sensor.set_auto_whitebal(False) # must be turned off for color tracking
clock = time.clock()

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
# Only blobs that with more pixels than "pixel_threshold" and more area than "area_threshold" are
# returned by "find_blobs" below. Change "pixels_threshold" and "area_threshold" if you change the
# camera resolution. "merge=True" merges all overlapping blobs in the image.

while(True):
    clock.tick()
    img = sensor.snapshot()
    for blob in img.find_blobs([thresholds[threshold_index]], area_threshold=50, pixels_threshold=300, area_threshold=200, merge=True):
        # These values depend on the blob not being circular - otherwise they will be shaky.
        if blob.elongation() > 0.5:
            img.draw_edges(blob.min_corners(), color=(255,0,0))
            img.draw_line(blob.major_axis_line(), color=(0,255,0))
            img.draw_line(blob.minor_axis_line(), color=(0,0,255))
        # These values are stable all the time.
        img.draw_rectangle(blob.rect())
        img.draw_cross(blob.cx(), blob.cy())#坐标数据
        # Note - the blob rotation is unique to 0-180 only.

        img.draw_keypoints([(blob.cx(), blob.cy(), int(math.degrees(blob.rotation())))], size=20)


        if blob.cx()>= x_min  and blob.cx() <= 160 and\
            blob.cy() >= 120 and blob.cy() <= y_max :
                flag = 1
        if blob.cx()>=160 and blob.cx() <= x_max and\
            blob.cy() >=120 and blob.cy() <= y_max :
                flag = 2
        if blob.cx()>= x_min and blob.cx() <= 160 and \
            blob.cy() >= y_min and blob.cy() <= 120 :
                flag = 3
        if blob.cx()>= 160 and blob.cx() <= x_max and \
            blob.cy() >= y_min and blob.cy() <= 120 :
                flag = 4
        if blob.cx()>= x_1 and blob.cx() <= x_2 and\
            blob.cy() >= y_1 and blob.cy() <=y_2 :
                flag = 5
        output_str="%d" %flag #方式1
        print('you send:',output_str)
        #time.sleep(0.02)
        uart.write('@'+output_str+'\r\n')

这个方法的32端代码,我用的是C8T6的最小系统去作为我们的主控制的,代码如下:

串口端接受数据包:(状态机)

//文本数据包处理格式
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;
	static uint8_t pRxPacket = 0;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);

		if (RxState == 0)
		{
			if (RxData == '@' && Serial_RxFlag == 0)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1)
		{
			if (RxData == '\r')
			{
				RxState = 2;
			}
			else
			{
//				strncpy(&Serial_RxPacket[pRxPacket],RxData,1);
				Serial_RxPacket = RxData;
//				pRxPacket ++;
			}
		}
		else if (RxState == 2)
		{
			if (RxData == '\n')
			{
				RxState = 0;
//				Serial_RxPacket[pRxPacket] = '\0';
				Serial_RxFlag = 1;
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

对串口数据进行处理,根据识别到的点不同位置,让舵机去运动:

Where_Addr=Serial_RxPacket-48;
			OLED_ShowNum(4,10,Where_Addr,1);
			switch(Where_Addr)
			{
				case  1:
					{
						AngleY--;
						AngleX++;
					}
					break;
				case  2:
					{
						AngleY--;
						AngleX--;
					}
					break;
				case  3:
					{
						AngleY++;
						AngleX++;
					}
					break;
				case  4:
					{
						AngleY++;
						AngleX--;
					}
					break;
			    case  5:
					{
						been_on();
						LED1_Turn();
						Delay_ms(50);
					}break;

			}

   方法修改:               

        但是这样做有很多弊端,题目要求我们在距离60cm,但是60cm,虽然在了像素中间,但是是斜的,误差很大,无法达到题目要求,这时候我们只能用PID算法来做这个题目了,让他们两个点在像素上无限接近,最后达到题目要求:但是去和用pid算法呢???

        这里我们求出红点和绿色点的坐标位置,然后最差值通过pid算法的公式带进去,分别进行pid进行调参,最后到达稳定状态;

        在摄像头方面的编程,我们用python进行将两个点的坐标求出来,然后通过串口将数据发送到32单片机进行处理:

import sensor, image, time,lcd,machine
from machine import UART
from fpioa_manager import fm

red_threshold   = (60, 98, 5, 127, 113, -128)
blue_threshold   = (55, 99, -18, -1, 4, 11)#(83, 58, -95, -1, 92, -51)#(65, 100, -113, 102, 3, 71)#(100, 60, -25, 106, 2, 3)

lcd.init()
sensor.reset() # Initialize the camera sensor.
sensor.set_pixformat(sensor.RGB565) # use RGB565.
sensor.set_framesize(sensor.QVGA) # use QQVGA for speed.
sensor.skip_frames(10) # Let new settings take affect.
sensor.set_auto_whitebal(False) # turn this off.
clock = time.clock() # Tracks FPS.
sensor.set_auto_gain(False)
lcd.rotation(2)

#映射UART2的两个引脚

fm.register(0, fm.fpioa.UART1_RX, force=True)  #GPIO0
fm.register(1, fm.fpioa.UART1_TX, force=True)  #GPIO1

#初始化串口,返回调用句柄
uart_A = UART(UART.UART1, 115200, 8, 0, 1)

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

while(True):
    img = sensor.snapshot() # 截取并返回一张图片

    blobs = img.find_blobs([red_threshold])
    blobs2 = img.find_blobs([blue_threshold])
    if blobs:
        if blobs2:
            max_blob=find_max(blobs)#红色色块
            max_blob2=find_max(blobs2)#绿色色块
            img.draw_rectangle(max_blob.cx()-10,max_blob.cy()-10,20,20)  #传入识别到的最大图块的x,y,w,h
            img.draw_rectangle(max_blob2.cx()-10,max_blob2.cy()-10,20,20)  #传入识别到的最大图块的x,y,w,h
            img.draw_cross(max_blob.cx(), max_blob.cy()) #在中心点画十字
            img.draw_cross(max_blob2.cx(), max_blob2.cy()) #在中心点画十字

            output_XY="@%d,%d$%d&%d#" % (max_blob.cx(),max_blob.cy(),max_blob2.cx(),max_blob2.cy())   #以@为开头,逗号分割,#号结尾,发送字符串,由单片机处理得到目标位置中心坐标

            uart_A.write(output_XY)   #传出数据
            print(output_XY)

    lcd.display(img)

32端的代码:

        串口中断对数据包处理:


//获取数据包
void USART1_IRQHandler(void)    //发送可变包长字符
{
	static uint8_t RxState = 0;   //定义静态变量表示状态位,根据不同状态位进行不同操作
	static uint8_t pRxPacket = 0;  //表示发送的数据到第几个
	Rx_Config=0;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0)
		{
			if (RxData == '@')  //判断是否为起始位@同时判断
			{
				RxState = 1;   // 起始位判断成功后将给RxState置1
				pRxPacket = 0;
			}
		}
		else if (RxState == 1)   
		{
			if (RxData == '#')  //优先判断是否为第一位结束位
			{
				
				Serial_RxPacket[pRxPacket] = '\0';
				RxState = 0;
				pRxPacket = 0;
				//设计结束标志位
				Rx_Config=1;
				
			}
			else
			{
				Serial_RxPacket[pRxPacket] = RxData;  //将接收到的字符存到数组
				pRxPacket ++;
			}
		}
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}


        首先我们设计了定时器中断,没隔一段时间发生中断去处理pid进行舵机的控制:

#include "stm32f10x.h"                  // Device header

//内部时钟

void Timer_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);   //开启APB1上的tim3时钟控制
	
	//TIM_InternalClockConfig(TIM3);     //设置内部时钟TIM3
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;    //定义开启TIM时钟接口体,并配置参数
	TIM_TimeBaseStructInit(&TIM_TimeBaseInitStructure);   //给未定义的结构体初始值
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;    //选择时钟分频方式(滤波器)
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;   //计数方式:向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 2000 - 1;    //ARR自动重装器值设置
	TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;   //PSC预分频值设置
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;   //高级定时器中的
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);   //初始化定时器
	
	TIM_ClearFlag(TIM3, TIM_FLAG_Update);   //清除TIM2复位后生成更新事件以
											//重新加载预分频器
											//和
											//重复计数器立即值
	
	TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);   //给TIM3中断控制使能
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //选择优先级分组方式
	
	NVIC_InitTypeDef NVIC_InitStructure;    //定义NVIC结构体并赋值
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;    //选择TIM2通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    //给所选的通道使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;    //赋值抢占优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;    //赋值响应优先级
	NVIC_Init(&NVIC_InitStructure);    //定义NVIC
	
//	TIM_Cmd(TIM3, ENABLE);   //使能TIM3外设
}

        在pid方面我们独一数据包将进行处理,处理数据包,获取每个点的坐标,然后求出差值,用pid算法进行调参数,控制pwm占空比 

float PID_KP_x =0.45;  //0.6
float PID_KI_x =0.065;	//0.07
float PID_KD_x =0.0;


float PID_KP_y =0.45;  //0.45
float PID_KI_y =0.034; //0.05
float PID_KD_y =0.0;

extern char Serial_RxPacket[40];

char Data_x[10];
char Data_y[10];

char DataBlue_x[10];
char DataBlue_y[10];

int16_t Data_xx;
int16_t Data_yy;

int16_t DataBlue_xx;
int16_t DataBlue_yy;

extern int Trace_Flag;//追踪标志位
/******************位置式****************/
/*******************速度环控制 - 位置式****************/
int16_t Servo_PID_x(int16_t Error)  //传入误差
{
	static int16_t Error_last, Error_difference , Error_All;   
	//静态变量储存,上次误差,上次误差与本次误差差值,误差累积
	
	int16_t Differential;      //返回的最终速度
	
	Error_difference = Error - Error_last;        //本次误差 - 上次误差
	
	Error_All += Error;     //误差累计
	
	if(Error_All >= 6000)        //积分限幅
	{
		Error_All =  6000;
	}
	if(Error_All <= -6000)
	{
		Error_All = -6000;
	}
	//位置式PID,速度闭环
	Differential = PID_KP_x*Error + PID_KI_x * Error_All + PID_KD_x * Error_difference;    
	Error_last = Error;     //储存上次误差
	
	return Differential;
}

int16_t Servo_PID_y(int16_t Error)  //传入误差
{
	static int16_t Error_last, Error_difference , Error_All;   
	//静态变量储存,上次误差,上次误差与本次误差差值,误差累积
	
	int16_t Differential;      //返回的最终速度
	
	Error_difference = Error - Error_last;        //本次误差 - 上次误差
	
	Error_All += Error;     //误差累计
	
	if(Error_All >= 6000)        //积分限幅
	{
		Error_All =  6000;
	}
	if(Error_All <= -6000)
	{
		Error_All = -6000;
	}
	//位置式PID,速度闭环
	Differential = PID_KP_y*Error + PID_KI_y * Error_All + PID_KD_y * Error_difference;    
	Error_last = Error;     //储存上次误差
	
	return Differential;
}

/****************** 数据包解析函数 *******************/
//数据包的处理
//截取数据
void analysis(void)
{
	uint8_t n = 0;
	uint8_t s = 0;
	
	uint8_t n2 = 0;
	uint8_t s2 = 0;
	 uint8_t i=0;
	
	while(Serial_RxPacket[i] != ',')
	{
		++i;
		
	}//得到数据长度
	
	for(n=0;n<i;n++)//存入数据
	{
		Data_x[n] = Serial_RxPacket[n];
	}
	Data_x[i] = '\0';//数据结束符号
	Data_xx = atoi(Data_x);
	

	
	n = i;
	do
	{
		++i;
	}
	while(Serial_RxPacket[i] != '$');
	
	for(n+=1;n<i;n++)
	{
		Data_y[s] = Serial_RxPacket[n];
		s+=1;
	}
	Data_y[s] = '\0';
	Data_yy = atoi(Data_y); //将接收到的字符数字转换成10进制数
	
	n = i;
	do
	{
		++i;
	}
	while(Serial_RxPacket[i] != '&');//
	for(n+=1;n<i;n++)
	{
		DataBlue_x[n2] = Serial_RxPacket[n];
		n2+=1;
	}
	DataBlue_x[n2] = '\0';
	DataBlue_xx = atoi(DataBlue_x); //将接收到的字符数字转换成10进制数


	n = i;
	do
	{
		++i;
	}
	while(Serial_RxPacket[i] != '\0');///
	
	for(n+=1;n<i;n++)
	{
		DataBlue_y[s2] = Serial_RxPacket[n];
		s2+=1;
	}
	DataBlue_y[s2] = '\0';
	DataBlue_yy = atoi(DataBlue_y); //将接收到的字符数字转换成10进制数
	

	i = 0;
}

void TIM3_IRQHandler(void)
{

	int16_t Error_x;  //定义误差
	int16_t Error_y;
	int16_t PWM_x;   //定义经过PID后的PWM
	int16_t PWM_y;
		
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
	{

		analysis();
		
		
		Error_x =DataBlue_xx - Data_xx;  //计算误差
		Error_y =DataBlue_yy - Data_yy;

		if(Error_x>0&&Error_x<8)
		{
			Error_x = 0;
		}
		else if(Error_x<0 && Error_x>-8)
		{
			Error_x = 0;
		}
		else if(Error_y>0&&Error_y<8)
		{
			Error_y = 0;
		}
		else if(Error_y<0 && Error_y>-8)
		{
			Error_y = 0;
		}
		if(Error_y ==0&&Error_x == 0)//表示到得了识别点,距离达到要求了
		{

			been_on();//蜂鸣器响

			LED2_ON();//led亮
			//关闭定时器
			//设计结束标志位
			Trace_Flag=0;

		}
		
		PWM_x = Servo_PID_x(Error_x);  //计算PID控制后的PWM
		PWM_y = Servo_PID_y(Error_y);
		
		PWM_SetCompare1(1500 + PWM_x); //舵机角度控制  下面的舵机   x轴
		PWM_SetCompare2(1500 + PWM_y); //舵机角度控制  上面的舵机   y轴
				
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update);	
	}
}

        最终我们达到了题目的要求,四天三夜电子设计大赛真的学到了很多!

. 使用jetson nano进行目标检测, 使用舵机进行控制, 使用串口进行通信 本项目为 矩形框识别 外围边线查找 部分.zip 1 目标检测的定义 目标检测(Object Detection)的任务是找出图像中所有感兴趣的目标(物体),确定它们的别和位置,是计算机视觉领域的核心问之一。由于各物体有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具有挑战性的问。 目标检测任务可分为两个关键的子任务,目标定位和目标分。首先检测图像中目标的位置(目标定位),然后给出每个目标的具体别(目标分)。输出结果是一个边界框(称为Bounding-box,一般形式为(x1,y1,x2,y2),表示框的左上角坐标和右下角坐标),一个置信度分数(Confidence Score),表示边界框中是否包含检测对象的概率和各个别的概率(首先得到别概率,经过Softmax可得到别标签)。 1.1 Two stage方法 目前主流的基于深度学习的目标检测算法主要分为两:Two stage和One stage。Two stage方法将目标检测过程分为两个阶段。第一个阶段是 Region Proposal 生成阶段,主要用于生成潜在的目标候选框(Bounding-box proposals)。这个阶段通常使用卷积神经网络(CNN)从输入图像中提取特征,然后通过一些技巧(如选择性搜索)来生成候选框。第二个阶段是分和位置精修阶段,将第一个阶段生成的候选框输入到另一个 CNN 中进行分,并根据分结果对候选框的位置进行微调。Two stage 方法的优点是准确度较高,缺点是速度相对较慢。 常见Tow stage目标检测算法有:R-CNN系列、SPPNet等。 1.2 One stage方法 One stage方法直接利用模型提取特征值,并利用这些特征值进行目标的分和定位,不需要生成Region Proposal。这种方法的优点是速度快,因为省略了Region Proposal生成的过程。One stage方法的缺点是准确度相对较低,因为它没有对潜在的目标进行预先筛选。 常见的One stage目标检测算法有:YOLO系列、SSD系列和RetinaNet等。 2 常见名词解释 2.1 NMS(Non-Maximum Suppression) 目标检测模型一般会给出目标的多个预测边界框,对成百上千的预测边界框都进行调整肯定是不可行的,需要对这些结果先进行一个大体的挑选。NMS称为非极大值抑制,作用是从众多预测边界框中挑选出最具代表性的结果,这样可以加快算法效率,其主要流程如下: 设定一个置信度分数阈值,将置信度分数小于阈值的直接过滤掉 将剩下框的置信度分数从大到小排序,选中值最大的框 遍历其余的框,如果和当前框的重叠面积(IOU)大于设定的阈值(一般为0.7),就将框删除(超过设定阈值,认为两个框的里面的物体属于同一个别) 从未处理的框中继续选一个置信度分数最大的,重复上述过程,直至所有框处理完毕 2.2 IoU(Intersection over Union) 定义了两个边界框的重叠度,当预测边界框和真实边界框差异很小时,或重叠度很大时,表示模型产生的预测边界框很准确。边界框A、B的IOU计算公式为: 2.3 mAP(mean Average Precision) mAP即均值平均精度,是评估目标检测模型效果的最重要指标,这个值介于0到1之间,且越大越好。mAP是AP(Average Precision)的平均值,那么首先需要了解AP的概念。想要了解AP的概念,还要首先了解目标检测中Precision和Recall的概念。 首先我们设置置信度阈值(Confidence Threshold)和IoU阈值(一般设置为0.5,也会衡量0.75以及0.9的mAP值): 当一个预测边界框被认为是True Positive(TP)时,需要同时满足下面三个条件: Confidence Score > Confidence Threshold 预测别匹配真实值(Ground truth)别 预测边界框的IoU大于设定的IoU阈值 不满足条件2或条件3,则认为是False Positive(FP)。当对应同一个真值有多个预测结果时,只有最高置信度分数的预测结果被认为是True Positive,其余被认为是False Positive。 Precision和Recall的概念如下图所示: Precision表示TP与预测边界框数量的比值
### 近三年电子设计中的控制题目 #### 2023控制题目2023年的全国大学生电子设计中,控制题目涉及到了舵机云台的直线运动调试。具体来说,通过调整两路PWM占空比的值来实现舵机云台沿指定路径的精确移动[^4]。这种控制方式不仅考验参者的硬件搭建能力,还要求具备一定的软件编程技巧。 ```python def adjust_pwm(duty_cycle_1, duty_cycle_2): """ 调整两个PWM通道的占空比以实现舵机云台的直线运动 :param duty_cycle_1: PWM通道1的占空比 :param duty_cycle_2: PWM通道2的占空比 """ pwm_channel_1.duty(duty_cycle_1) pwm_channel_2.duty(duty_cycle_2) # 假设我们有一个简单的线性关系用于计算所需的PWM占空比 slope = 1.0 # 斜率 intercept = 0.0 # 截距 target_position = 50 # 目标位置对应的数值 duty_cycle_value = slope * target_position + intercept adjust_pwm(duty_cycle_value, duty_cycle_value) ``` #### 2022年控制题目 针对2022年度的比,重点在于执行器的应用及其驱动路的设计。比中出现了多种型的机作为执行机构,包括但不限于有刷直流机、无刷直流机以及步进机等。每种机都有其独特的特性与应用场景,并且需要配套相应的驱动算法来进行精准操控[^2]。 #### 2021年控制题目 回顾2021年的事,“爬坡小车”的项目吸引了众多关注。此项目的难点之一就是如何选择合适的轮胎材料以增加抓地力,从而确保车辆能够在倾斜角度较大的斜面上稳定行驶并完成既定任务。此外,在动力供应方面也遇到了挑战——普通的12V池无法满足长时间高强度工作的需求;因此,优化源管理系统成为解决这一难的关键因素之一[^3]。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值