大学生电子设计竞赛

大学生电子设计竞赛
Nihil电子设计大赛参赛记录
你好,这是一份参赛记录,请查收~
它确实只是参赛记录,因为只是被拉去凑人数的我并没有什么拿得出手的奖😭😭😭。不过回忆起为了获奖所接触到的知识、四天三夜队友的陪伴以及遇到的有趣的事情,我想我还是有所收获的。(不是指这份参赛记录啊喂)
如果你还不了解全国电子设计竞赛,下方是官方链接👇
关于竞赛
全国大学生电子设计竞赛由教育部高等教育司及工业和信息化部人事教育司共同主办,教育部高等教育司及工业和信息化部人事教育司共同负责领导全国范围内的竞赛工作。各地竞赛事宜由地方教委(厅、局)统一领导。为保证竞赛顺利开展,组建全国及各赛区竞赛组织委员会和专家组。
全国大学生电子设计竞赛每逢单数年的8月份举办,赛期四天(具体日期届时通知)。在双数的非竞赛年份,根据实际需要由全国竞赛组委会和有关赛区组织开展全国的专题性竞赛,同时积极鼓励各赛区和学校根据自身条件适时组织开展赛区和学校一级的大学生电子设计竞赛。
竞赛采用全国统一命题、分赛区组织的方式,竞赛采用“半封闭、相对集中”的组织方式进行。竞赛期间学生可以查阅有关纸介或网络技术资料,队内学生可以集体商讨设计思想,确定设计方案,分工负责、团结协作,以队为基本单位独立完成竞赛任务;竞赛期间不允许任何教师或其他人员进行任何形式的指导或引导;竞赛期间参赛队员不得与队外任何人员讨论商量。参赛学校应将参赛学生相对集中在实验室内进行竞赛,便于组织人员巡查。为保证竞赛工作,竞赛所需设备、元器件等均由各参赛学校负责提供。
从介绍也可以看出,这个竞赛的含金量是非常高的,涉及计算机、微机原理、单片机、电子信息、机械制造、通信原理、汇编等非常多的知识点,所以如果有意愿参加此类竞赛一定是需要老师与研究组进行教学讨论与造轮子等赛前准备的。学习效率比较高的情况下,一年下来就可以拿奖。😴
小编学习内容
编程语言方面
C语言
Python
MicroPython
Vue(好像什么奇怪的东西混进来了)
汇编(一丢丢)
FPGA(一丢丢丢丢)
C++(做GUI很香!一个学长用它做硬件GUI赚了大几万)
单片机方面
STM32c8t6(实现一些小功能,亲眼见证了从¥10涨到¥30😩)
STM32zet6(可以参加比赛写算法,例程也是最多的!)
Py-AI K210(人工智能!机器学习!)
OpenMV(¥600+)
MSP430(真的太难用了😱)
SU-03T (这个是语音识别,以防万一,UI编程无敌!)
仪器方面
示波器(一定要会用好!)
电源(参加电源类比赛的话一定要用好!)
焊烙铁(Emm小心手。。)
导线(包括跳线、型号线径功率等不要大意)
热缩管、轧带等附件(干净整洁可以减少不必要的麻烦哦~😘)
配件方面
单片机
电源(别心疼、对它好点儿)
电机驱动(没它的话、就要看单片机的运气了😄)
电机 (选择符合要求的型号种类即可)
降压稳压模块(数控有风险!使用需谨慎!)
传感器(请务必阅读使用文档)
洞洞板、锡丝等消耗品(建议白嫖🙈)
算法方面
主逻辑
PID(参数整定是个头疼的事情)
DMA(对我来说有些难😣)
LCD(显示屏、字模转换等)
按键防抖(硬件软件)
ADC采集(比较重要)
串口通信(非常重要)
传感器常见算法(移步baidu.com🙄、不过很多都已经封装好了)
学习网址推荐
虽然是网址推荐,但由于历史原因,并没有当年那么全(好吧全丢了其实)
历次比赛记录
接下来是正文!从大一到大四一共参加了三次,大一入学时旁观了一届,算来算去也算守门人了👉👈。记录下来的是比较正式的比赛,从国赛到省赛再到校赛(为什么越参加级别越低)。校赛和院赛复现运气很好,均是最高分;省赛那一次是非常可惜的一次,全部题目的逻辑已经写完并排除了我认知范围内的所有bug,最后复现时因为PID的参数整定问题以极低的分数结束;国赛那一次是我第一次正式参加电赛,国赛并不限制芯片,我使用了封装程度较高的芯片,逻辑也写的大致正确,再有复现时的运气加成😝,拿到了一个在当时还算不错的成绩(湖北赛区二等奖)。
在电赛这方面我投入的不算少,但是由于当时的学校领导并不算非常重视这个比赛,也并没有很正式地组织过指导教师培训等活动,甚至到大二下时协会的负责老师进行了更换(协会主要对接电赛),可以看出学校想要重新抓起这一项赛事。如果哪位小朋友想要认认真真参赛的话,一定要努力准备!相信你的学校一定会给你提供必要的支持,包括资金、教学以及报名的相关事项,只要你有一颗坚强的内心。
那么,下面是正式记录🤫
2021大学生电子设计竞赛(国赛)
开赛前看了看各大论坛上的控制题目的预测,包括亚克力球里面放小车,用小车和无人机通信…为此特地去学了学串口、蓝牙通信,最后看到题目震惊:原来摄像头可带处理器模块原来是这个意思!🙃 因为组内没有人会用OpenMV,确定下这个题目还是不容易的。毕竟第二年想去智能车比赛,想不到电赛让我速成摄像头 😆
首先我仅代表我的组员和我感谢比赛过程中帮助过我们的同学、指导老师、以及星瞳科技,我们最后的成果包括相关设备的购买均基于星瞳科技。
关于题目
“制作智能送药小车,模拟完成在医院药房与病房间药品的送取作业。”嗯…这很现实😷。第一段就是说循迹红线、中远端病房号不固定。第二段增加一个识别药品和亮灯。说实话,这个数字识别确实劝退 😢
数字识别
- Netlet:刚开赛时候一些论坛和群就有人说nn库不能用了,我们当时还没有意识到事情的严重性。等到写算法时候发现,就算把nn库手动装到IDE和SD卡里,还是会有未知bug出现。
- Edge Impulse神经网络训练:拍照片到网站在线训练出tflite文件,然后进行在线识别。记得当初拍了20000多张照片,最后去重后有6000多张,训练了半个小时,然而,识别正确率并不高…💢💢💢
- 特征点匹配:虽然和题目要求及其相似,但是我们还是优先考虑了模板匹配,感兴趣的同学可以试一试。
- 模板匹配:拍模板,识别出来会有反馈,经测试效果还可以,不过对图像位置大小要求高一点,直接通过十字路口可能识别不到,so,经我们一致决定:让它到十字路口**”抽搐“**几下,识别率马上就高了起来。
细节实现
- 提取红色路线:识别红线可以通过摄像头设置阈值来提取红线,刚开始我们设置的是红线黑线全部展示,后来发现如果保留黑线的话如果偏离角度差一点儿,拟合效果就会大打折扣,这并不能通过调节PID参数或者拟合后直线斜率占比实现。(不要问我怎么知道的)
- PID调节:这部分星瞳科技快速上手部分有提供源码,这里不进行过多解释,我们测试时用的就是那个例程,参数都没改。
- 控制启动停止:熟悉的红外对管
- LED:我们就打算用OpenMV上的RGB灯(虽然被测试专业员吐槽了,但好像没啥影响 😏
- 检测药品:因为当时没有压力传感器(开赛就买了,测试才到),我们就用光敏传感器代替了,这个用压感效果应该会不错,不过光感调好灵敏度也没有什么影响,当然,超声波模块也可。
- 返回:本来打算继续识别数字,但仍要拍照,而且增大运算量,于是,我们可以使用列表存储路径,返回的时候逆向遍历即可。
反思总结
- 3天速成Openmv,又接触了micropython 😮,最后效果还是可以滴!
- 硬件搭得晚了点儿,导致调试,确定识别方法有些晚
- 传感器方面准备不足,需要在这方面下一些功夫咯
- 编程底层有些混乱,虽然程序可以跑,但连我自己都在吐槽
唯一遗憾的就是没有在宿舍看EDG捧起奖杯😭😭😭
2022大学生电子设计竞赛(省赛)
说实话这次算准备的不算很认真,赛前也没有针对相关模块及时进行补充学习。不过、从赛前的材料准备清单来看,基本和去年的题目很类似了。吸取去年的教训,这次准备了一些封装十分完善的MCU(有手就会那种🤫。不过因为这次是省赛,故主办方对芯片进行了限制——采用TI的MCU。(啊啊啊啊真的头疼😣
首先我仅代表我的组员和我感谢比赛过程中帮助过我们的同学、指导老师、以及论坛上的网友们
关于题目
小车跟随行驶系统,设计一套小车跟随行驶系统,采用TI的MCU,由一辆领头小车和一辆跟随小车组成,要求小车具有循迹功能,且速度在0.3 ~ 1m/s可调,能在指定路径上完成行驶操作,行驶场地的路径如图1 所示。其中,路径上的A点为领头小车每次行驶的起始点和终点。当小车完成一次行驶到达终点,领头小车和跟随小车要发出声音提示。领头小车和跟随小车既可以沿着ABFDE圆角矩形( 简称为内圈 )路径行驶,也可以沿着ABCDE的圆角矩形( 简称为外圈 )路径行驶。
其实今年的题目就是去年和前年控制题目的结合版,识别路口->判断转向->蓝牙通信->循迹->附加功能。这种类型的题目方案很多,因为是黑线,可以红外传感,也可以摄像头,小车跟随可以光电传感,也可以超声波测距,甚至直接写在蓝牙里面也可以。TI MCU我们不是很熟,故尽量将算法写在摄像头了。
循迹算法
**基本的巡线:**采用星瞳科技的官方例程
常见的PID封装:
from pyb import millis
from math import pi, isnan
class PID:
_kp = _ki = _kd = _integrator = _imax = 0
_last_error = _last_derivative = _last_t = 0
_RC = 1/(2 * pi * 20)
def __init__(self, p=0, i=0, d=0, imax=0):
self._kp = float(p)
self._ki = float(i)
self._kd = float(d)
self._imax = abs(imax)
self._last_derivative = float('nan')
def get_pid(self, error, scaler):
tnow = millis()
dt = tnow - self._last_t
output = 0
if self._last_t == 0 or dt > 1000:
dt = 0
self.reset_I()
self._last_t = tnow
delta_time = float(dt) / float(1000)
output += error * self._kp
if abs(self._kd) > 0 and dt > 0:
if isnan(self._last_derivative):
derivative = 0
self._last_derivative = 0
else:
derivative = (error - self._last_error) / delta_time
derivative = self._last_derivative + \
((delta_time / (self._RC + delta_time)) * \
(derivative - self._last_derivative))
self._last_error = error
self._last_derivative = derivative
output += self._kd * derivative
output *= scaler
if abs(self._ki) > 0 and dt > 0:
self._integrator += (error * self._ki) * scaler * delta_time
if self._integrator < -self._imax: self._integrator = -self._imax
elif self._integrator > self._imax: self._integrator = self._imax
output += self._integrator
return output
def reset_I(self):
self._integrator = 0
self._last_derivative = float('nan')
起点路线的判断:
# Basic_speeds -> 0.3m/s -> 0.5m/s -> 0.6m/s -> 0.7m/s -> 0.9m/s -> 1.0m/s
self.basic_speed_list = [25, 40, 48, 56, 64, 70]
# Question Speed and Route lists
self.ques_speed_list = [25, 40, 32, 70]
self.ques_route_list = [[2], [2,2], [2,1,2], [2]]
这次将所有的配置参数提取到setting.py中构造Setting类来提高封装度,也便于修改参数,ques_route_list即是小车所要行驶的路线通过遍历这个二维列表即可以知道小车的路线,那么,关键就在于转弯路口的判断
转弯路口的判断:
特征点匹配:虽然和题目要求及其相似,但是我们也还没有尝试过,故暂不做考虑。
模板匹配:拍模板,识别出来会有反馈,因为采用模板匹配这个功能需要sensor采用灰度测试,而基本的线路拟合又是RGB565,如果二者交叉测试的话,又担心MCU处理不过来,故也没有考虑模板匹配
直线检测:遇到0°线段宽度大于设定值时则判断起停线,或者90°线段有检测到时判断起停线。我们组采用的是这种方法,因为看起来逻辑可行,有算法支撑,也不涉及到example的存储。但是参数整定真的好难!😢我们一直调到比赛结束也没有调的很好。
细节实现
- 小车路线的确定:题目中要求跟随小车的行驶完全由领头小车指挥控制,领头小车上有启动按键和设置按键,而跟随小车只有一个上电开关,不得有其他启动和操作按键。每一次行驶发车时,领头小车和跟随小车按照题目要求摆放在行驶路径的指定位置,跟随小车上电,处于等待接收领头小车指令的状态。领头小车一键启动行驶,直到整个行驶过程结束。故采用MSP430上的按键GPIO输出到OpenMV的固定端口上,再和Setting中设定的任务路线相匹配即可知道路线。
- PID调节:这部分星瞳科技快速上手部分有提供源码
- 蜂鸣器:普通GPIO
- 蓝牙:串口通信,传输的数字对应相应的任务(0-3),4代表停车信号。
- 转弯方式:这里其实考虑了好多方法,包括定点sleep、开环转弯、设定ROI区域、调节PID…实际采用的是重新设定ROI区域,但效果不是很理想,包括后面并道的时候会有误差,而且实际的PID参数的整定还和车的长度、宽度等因素有关,不恰当的PID、ROI等参数会导致车轮打滑。
反思总结
- 这次硬件和赛道很快到位,主要卡在了MCU的选择以及蓝牙通信上
- PID参数整定啊啊啊啊啊、调参方式没有结合实际车的长宽等硬件信息(简直太难了。
- 最初启停线识别方案选择错误,最后没有时间修改
- 软件封装有些晚,应尽早确定方案后进行封装,将参数接口暴露。
2022大学生电子设计竞赛(校赛)
关于题目
设计制作一个声源定位跟踪系统,能够实时显示及指示声源的位置,当声源移动时能够用激光笔动态跟踪声源。 声源检测系统测量区域分布俯视如图 1 所示。
要求
- 设计并制作声音发生装置——“声源”,装置能独立工作,声音音量手动可调,装置最大边长或直径不超过 10cm,装置可用支架安装,并可在地面移动;声源中心点 B 用红色或其他醒目颜色标识,并在 B 点所在的平面以 B 点为圆心,直径为 5cm 画圆圈,用醒目线条标识,该平面面向检测指示装置(图中 A 点)。(4 分)
- 设计并制作一个声源定位检测装置,传感器安装在图 1 的 C 区范围内,高度不超过 1m,系统采用的拾音器 或麦克风传感器数量不超过 10 个;在装置上标记测试参考点 A,作为位置坐标的原点;装置上有显示电路,实时显 示 D 区域内声源的位置,显示 A、B 两点直线距离 γ 和以 A 点为原点,AB 在地面的投影与图 1 中心线的夹角 θ, 测量时间不超过 5s,距离 γ 和角度 θ 的测值误差越小越好。(36 分)
- 设计并制作一个声源指示控制装置,此装置和上述声源定位检测装置可以合为一体。也放置在图 1 的 C 区, 安装有激光笔和二维电动云台,能控制激光笔指向声源,定位计算过程中时,激光笔关闭,定位运算完成时激光笔 开启。定位指示声源时,动作反应时间不超过 10s,光点与 B 点偏差越小越好。(30 分)
- 声源移动动态追踪:当声源摆放在地面,用细绳牵引,以 0.2m/s 左右的速度在 D 区移动时,激光笔光点指 向 B 点,光点与 B 点偏差越小好,跟踪反应时间越短越好。(20 分)
解题过程
听说Sipeed的MIC阵列模块可以秒杀E题?MicArray 麦克风阵列 - Sipeed Wiki
图2所示的麦克风阵列模块是 Sipeed 基于 MSM261S4030H0 数字麦克风芯片设计的,模块声音识别灵敏度、信噪比高,由沿板的六个麦克风和一个中心的麦克风组成,阵列板上的 12 颗 LED 可以用来可视化识别声源方位,基于 GCC-PHAT
算法实现声源定位、语音识别、波束成形等需求场合。
对就是这个😧
相关参数
功能特点 | 参数 |
---|---|
声压级 | 140 dB SPL |
灵敏度 | -26(dB,dBFS @1kHz 1Pa) |
信噪比 | 57 dB (20kHz bandwidth,A-weighted) THD<1% (100dB SPL @1kHz S=Nom,Rload>2k ) |
时钟频率 | 1.0-4.0Mhz(正常模式) 150-800khz(低功耗模式) |
MEMS 麦克风 | 7 个 MSM261S4030H0 组成阵列 |
连接器 | 支持 2*5P 2.54mm 端子和 10P 0.5mm FPC 连接器 |
灯光 | 12 个 SK9822 LED 组成一个环形 LED 阵列 |
多个 LED 通过双信号线级联 / 8 Bit(256 级)可调颜色 / 5 Bit(32 级)亮度调节 | |
尺寸 | 78.1*88.8mm |
因为我买的是官方的产品,所以直接按照官方提供的代码逻辑套用即可。包括官方还给了一个2022年电赛E题声源定位跟踪系统做好的链接。。。我照着完善了一下代码框架,接上线后阵列模块非常容易受干扰(当然可以通过选择音源提高效果),需要进行一些处理。
下面是官方调用函数:
from Maix import MIC_ARRAY as mic
import lcd
lcd.init()
mic.init()#默认配置
#mic.init(i2s_d0=23, i2s_d1=22, i2s_d2=21, i2s_d3=20, i2s_ws=19, i2s_sclk=18, sk9822_dat=24, sk9822_clk=25)#可自定义配置 IO
while True:
imga = mic.get_map() # 获取声音源分布图像
b = mic.get_dir(imga) # 计算、获取声源方向
a = mic.set_led(b,(0,0,255))# 配置 RGB LED 颜色值
imgb = imga.resize(160,160)
imgc = imgb.to_rainbow(1) # 将图像转换为彩虹图像
a = lcd.display(imgc)
mic.deinit()
下面是最终的完整代码:
from Maix import MIC_ARRAY as mic
import lcd, image
import time
import math
from Maix import GPIO
from fpioa_manager import fm
from machine import Timer, PWM
# MIC-IO
mic.init(i2s_d0=32, i2s_d1=33, i2s_d2=34, i2s_d3=35, i2s_ws=9, i2s_sclk=10, sk9822_dat=11, sk9822_clk=15)
# Laser-IO
fm.register(7, fm.fpioa.GPIO0)
Laser = GPIO(GPIO.GPIO0, GPIO.OUT)
# KEY-IO
fm.register(26, fm.fpioa.GPIO1)
KG0 = GPIO(GPIO.GPIO1, GPIO.IN, GPIO.PULL_DOWN)
fm.register(27, fm.fpioa.GPIO2)
KG1 = GPIO(GPIO.GPIO2, GPIO.IN, GPIO.PULL_DOWN)
# SERVO-IO
tim = Timer(Timer.TIMER0, Timer.CHANNEL0, mode=Timer.MODE_PWM)
Servo_laser = PWM(tim, freq=50, duty=0, pin=17)
# LCD-INIT-320*240 and MAP-INIT
lcd.init()
lcd.fill_rectangle(46, 5, 230, 4, (255, 0, 0)) # 上边线
lcd.fill_rectangle(46, 5, 4, 230, (255, 0, 0)) # 左边线
lcd.fill_rectangle(46, 235, 230, 4, (255, 0, 0)) # 下边线
lcd.fill_rectangle(276, 5, 4, 234, (255, 0, 0)) # 右边线
lcd.fill_rectangle(237, 5, 4, 234, (255, 0, 0)) # 右边线2
lcd.fill_rectangle(0, 77, 47, 4, (255, 0, 0)) # 左区域上边线
lcd.fill_rectangle(0, 154, 47, 4, (255, 0, 0)) # 左区域下边线
lcd.fill_rectangle(0, 77, 4, 77, (255, 0, 0)) # 左区域左边线
class Argument:
"""Some Argus"""
def __init__(self):
super(Argument, self).__init__()
# Normal
self.num = 0
self.num2 = 0
self.Angle_LB = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
self.t = 0
self.t1 = 0
self.t2 = 0
self.maxnum = 0
self.minnum = 0
self.jiaodu = 0
self.Angle_last = 0
self.b = [3, 1, 2, 5, 6]
self.i = 100
self.a = []
self.times = 0
self.pid = 0
self.err = 0
self.JD = 0
self.output = 0
# Kalman Filter
self.KF_lastP = 0.1 # 上次的协方差
self.KF_nowP = 0 # 本次的协方差
self.KF_x_hat = 0 # 卡尔曼滤波的计算值,即为后验最优值
self.KF_Kg = 0 # 卡尔曼增益系数
self.KF_Q = 0 # 过程噪声
self.KF_R = 0.01 # 测量噪声
arguments = Argument()
def kalman_filter(argus, value):
argus.output = 0 # output为卡尔曼滤波计算值
x_t = argus.KF_x_hat # 当前先验预测值 = 上一次最优值
argus.KF_nowP = argus.KF_lastP + argus.KF_Q # 本次的协方差矩阵
argus.KF_Kg = argus.KF_nowP / (argus.KF_nowP + argus.KF_R) # 卡尔曼增益系数计算
argus.output = x_t + argus.KF_Kg * (value - x_t) # 当前最优值
argus.KF_x_hat = argus.output # 更新最优值
argus.KF_lastP = (1 - argus.KF_Kg) * argus.KF_nowP # 更新协方差矩阵
def servo(servo_inter, angle):
servo_inter.duty((angle + 90) / 180 * 10 + 2.5)
while True:
# Inner Params: [AngleX, AngleY, AngleR, Angle, AngleAddPi]
angle_params = [0, 0, 0, 0, 0]
# MIC-Detecting
img_a = mic.get_map()
img_b = mic.get_dir(img_a)
mic.set_led(img_b, (10, 10, 0))
# If Ques 3
if KG0.value() == 1 and KG1.value() == 0:
arguments.times += 1
if arguments.times == 1:
time.sleep(1)
Laser.value(0)
# Angle
for i in range(len(img_b)):
if img_b[i] >= 0:
angle_params[0] += img_b[i] * math.sin(i * math.pi / 6)
angle_params[1] += img_b[i] * math.cos(i * math.pi / 6)
angle_params[0] = round(angle_params[0], 6)
angle_params[1] = round(angle_params[1], 6)
if angle_params[1] < 0:
angle_params[4] = 180
if angle_params[0] < 0 and angle_params[1] > 0:
angle_params[4] = 360
if angle_params[0] != 0 or angle_params[1] != 0:
if angle_params[1] == 0:
angle_params[3] = 90 if angle_params[0] > 0 else 270
else:
angle_params[3] = angle_params[4] + round(math.degrees(math.atan(angle_params[0] / angle_params[1])), 4)
# Determine the scope
if 90 < angle_params[3] < 270:
angle_params[3] = arguments.Angle_last * 0.1 + angle_params[3] * 0.9
kalman_filter(arguments, angle_params[3])
arguments.Angle_last = angle_params[3]
show_angle = 180 - angle_params[3]
# Check Ques 3
if KG0.value() == 1 and KG1.value() == 0:
Laser.value(1)
servo(Servo_laser, show_angle)
lcd.draw_string(60, 200, "Angle: " + str(show_angle), lcd.BLUE, lcd.BLACK)
lcd.draw_string(60, 180, "Distance: " + str(275 / math.cos(-show_angle * math.pi / 180)), lcd.BLUE,
lcd.BLACK)
lcd.fill_rectangle(251, 10, 25, 225, (0, 0, 0))
location = int(108 + math.tan(-show_angle * math.pi / 180) * 48)
print(show_angle, location)
if 0 < location < 200:
lcd.fill_rectangle(251, location, 15, 15, (0, 255, 200))
# Check Ques 4
if KG0.value() == 1 and KG1.value() == 1:
arguments.JD = angle_params[3]
Laser.value(1)
error = arguments.JD - arguments.pid
arguments.pid += error * 0.1
arguments.output = -arguments.pid
if -270 < arguments.output < -90:
servo(Servo_laser, 180 + arguments.output)