国二,2023电赛 【G题】空地协同智能消防系
简介:小车上和无人机都使用STC32G12K128,其中四轴使用STC32做信息采集和下位机控制板,小车采用STC32G12K128作为主控,最终获得国二。
所有硬件信息大概会开源到嘉立创上(本人考研党,也可能在考完研后整理)。
---河南科技大学
先讲故事:
大学很精彩,大一上来到实验室,跟着学长们学习51单片机,大一下32单片机,然后是21年电赛,老师把无人机交到了我们手中,我们满怀热情的扑街了(省三),但是我们不甘心啊,于是开始沉淀。大二下的17界智能车竞赛中,每天高强度备赛,但是由于已经险些无法参赛,最终在一处山村里和河南另一所学校组成赛点(我们那届特殊,疫情原因)勉强完成比赛冲进国赛,但是在外租房成本真的太高了,我们也没有得到任何学校的支持,于是无奈告别线下决赛,遗憾国二。
正巧的是当时STC赞助的比赛,所以我们使用的芯片是STC16,从此和STC32结下不解之缘~
在完成智能车竞赛后,我们得到一段难得的清闲时光,在这段事件里,我开发了一个小游戏机(也用的是STC32,有机会也会开源,图片在最后),并准备22年河南赛的电赛,虽然我们在小车题中获得过国奖,但是我们组毅然选择飞控题,不为别的,主打一个弥补遗憾!
但是事与愿违,疫情又来了。河南省电赛本来说是延迟到3月份过完年返校后举办,然后我们就把飞机留到学校回家了,突然赛委会通知要在一周内完成所有评比工作(这都没东西咋比啊),遗憾止步省二(还是凭借ppt获得的)。
真的太难受了,当时这个飞控的板子就画出来了,不过族中还是在23年电赛用上了。
然后就是最后一次比赛--23年电赛,我们这时候其实已经没有大一大二那么多经历放在电赛上了,因为都要考研。但是飞控题真的给我留下太多遗憾了,所以我们还是打算搏一把,我几乎整个暑假每天都用半天时间在弄飞机(太痛了),这对考研党来说有点致命,但是没办法,真的很想拿国一。
唉,最后我们奋战4天3夜,但是由于测试的时候没识别到火源,可能摄像头算法的问题也可能是K210没读到SD卡,总之发挥不是很理想,回来的时候我们几个心情都非常沉重,国一肯定没戏了,有点遗憾。
结果出来还是国二。
没办法,竞赛总是伴随遗憾,这大概率是我大学生涯最后一个比赛了,虽然没能达到巅峰,但总归要记录一下,一路来也踩过不少坑,写下此篇,方便大家学习入门。
有些细节方面可能会在有时间以后详细展开,现在先大致介绍一下,也算给初学者点思路。
先上图吧
其中小车因为我们参加过17界智能车(也是国二),所以直接用了以前的车模。但是因为我们的车车都传给学弟了,所以搭车的时候也是慌手慌脚,最终没能达到很好的效果{:4_167:}。主板方面是抄的逐飞的板子,就不再过多展示,下面着重说明飞机STC32G12K128主控的设计和使用。
主板设计:
主板原理图并不是非常复杂,只是提供了很多接口方便连接传感器和数据采集,毕竟主要工作是做下位机嘛,所以画的并不是很复杂。
该主控板采用STC宏晶官方最新推出单片机STC32G12K128作为主控芯片,上有两个K210深度学习摄像头嵌槽,用于固定k210以及进行数据通讯,并且外引4路GPIO、3路AD、两路串口、两个可控LED灯、三个可编程按键以及一个IIC接口方便后续扩展模块等资源。其中IIC扩展部分可以直接排线相连接16路PWM舵机控制模块,对于有些需要依靠舵机转向或者抓取的任务也可以快速扩展。右侧K210直插的中间有四个孔位,可以与我们后续介绍的12v转5v稳压降压模块依靠铜柱直接相连接。
主板资源概述(来自于报告图片):
上面有两个K210的卡槽,可以插K210模块,因为近些年的无人机题都有识别任务,并且可能牵扯到前方和下方两个方向,所以设计两个卡槽来进行通讯。
电源设计:
与上述主控搭配使用,可对3C锂电池进行降压稳压处理,输出5V稳定电源,并且连接方式采用铜柱代替导线,无线化设计减少飞行扰动。
使用螺丝代替导线进行连接,这样可能会有一些问题(大佬轻喷),但是我们目前用了这么久是没遇到问题的,而且非常方便更换。
飞控方面
采用匿名飞控,懂得都懂,非常好用,方便二次开发,与飞控采用串口连接通讯,在本次设计中,重物的抛洒工作(舵机完成)、摄像头的信息反馈与控制、蜂鸣器的控制、LED的控制信号等都由飞控下达指令,然后由STC32进行传感器的控制,为了保持通讯的实时性,使用921600的波特率(之前都一直用的115200,没想到升高后数据依然稳定)。
还有就是为了方便图像的调试,自制了上位机方便观察图像,配合K210上面镶嵌的ESP32实现组网通讯(K210处理完图像直接通过ESP32发送至电脑)
通过图中信息可看的,灯点被识别后被涂满,在识别区密度满足要求后,返回数传信息给上位机,飞机通过调试信息完成对点位的矫正工作。假设目标值为 Setpoint,当前系统输出值为 ProcessVariable,定义以下变量:偏差: Error = Setpoint – ProcessVariable积分误差: Integral = Integral + Error * Δt,其中 Δt 为采样时间间隔导数: Derivative = (Error - PreviousError) / Δt 通过公式ControlOutput = Kp * Error + Ki * Integral + Kd * Derivative (1) 计算位置式PID偏差,对位置环进行校准。
“消防车”路径规划在系统中设计每个街区每个边都一一对应一条路径,首先无人机向小车发送火情位置,小车在接受到信息后计算火情所在街区,由于小车无法驶入街区,小车会将火情坐标位置转化成离街区“火情”最近的位置、需要到达“火情”街区的边。每个路径数据包括包括数个转向操作其中包括方向dir,进行拐弯位置的四元列表,其中x、y表示需要转向的区域矩形的左下角坐标,w、h表示需要转向的区域矩形的宽和高。这样利用车载“UWB”就可以实现小车在指定位置转向。
“消防车”对无人机轨迹刻画与里程计算本系统中使用UWB进行室内定位,无人机将UWB信息每秒通过蓝牙发送给“消防车”,“消防车”在接受到数据后将实际坐标转化为屏幕中的像素坐标,之后计算本次接收到的坐标值与上次接收到的坐标值之间的距离,累加进总里程中并显示。
我们的定位采用的是UWB的方案,UWB建系PID校准,然后状态机逐点搜索,大概就是这么个流程。
以下是STC32中关键代码的局部展示(4天3夜写的有点乱~)
/**********************************
**函数名:Isr_timerInit
**作用 :定时器ISR总初始化
************************************/
void Isr_timerInit()
{
// //最多定时20ms左右,多了会炸!!!!!
// pit_timer_ms(TIM_0, 5); //使用TIMER作为系统时钟,时间5ms一次,最高优先级
// PT0 = 1; //定时器0优先级中断低位
// PT0H =1; //定时器0优先级中断高位
//
// pit_timer_ms(TIM_1, 20); //使用TIMER作为系统时钟,时间20ms一次,次高优先级
// PT1 = 0; //定时器1优先级中断低位
// PT1H =1; //定时器1优先级中断高位
//
pit_timer_ms(TIM_4, 1); //使用TIMER作为系统时钟,时间1ms一次
}
/*1ms线程*/
void task_1ms()
{
}
/*5ms线程*/
void task_5ms()
{
LED_control();
}
/*20ms线程*/
void task_20ms()
{
Servo_actuator_control();
Uart1_Sendbuff(); //向两个K210传输数据
Uart4_Sendbuff(); //转发k210数据
//
// uart_putbuff(UART_1,"561",3);
// set_pwm(0,15);
// set_pwm(0,10);
// delay_ms(1000);
}
/*31ms线程*/
void task_31ms()
{
Uart3_Sendbuff(); //转发k210数据
}
/*32ms线程*/
void task_32ms()
{
Uart2_Sendbuff(); //转发k210数据
}
/*100ms线程*/
void task_100ms()
{
// UWB_Receive();//uwb接收函数
// UWB_X =UWB_Y =51231;
// UWB_NewDate =1;
// UWB_Send();
}
/*300ms线程*/
void task_300ms()
{
KeyAndLedRecall();
// BEEP==0?BEEP =1:BEEP=0;
}
/*****************任务管理**************************/
/*****************用户屏蔽**************************/
//创建任务配置
static schedule_task_t schedule_task[] =
{
//task_2ms(函数名) 2 (执行周期)2(初始化计时器) 0(任务执行标志)
{task_1ms, 1, 1, 0} ,
{task_5ms, 5, 5, 0},
{task_20ms, 20,20, 0},
{task_31ms,31 ,31 ,0},
{task_32ms,32,32,0},
{task_100ms, 100,100, 0},
{task_300ms, 300, 300, 0},
};
//根据数组长度,判断任务数量
#define TASK_NUM (sizeof(schedule_task)/sizeof(schedule_task_t))//记得改定时器值
void task_interrupt(void)
{
uint8 index = 0;
// 循环判断
for (index = 0; index < TASK_NUM; index++)
{
// 判断计时器是否到时间
if (schedule_task.run_timer) //不为0
{
// 计时器减1
schedule_task.run_timer--;
//判断计时器是否到时间
if (0 == schedule_task.run_timer)
{
// 恢复倒计时器的时间
schedule_task.run_timer = schedule_task.interval_time;
// 任务标志置1
schedule_task.run_sign = 1;
}
}
}
}
void task_process(void)
{
uint8 index = 0;
// 循环判断任务
for (index = 0; index < TASK_NUM; index++)
{
if (schedule_task.run_sign)
{
// 清除任务标志
schedule_task.run_sign = 0;
// 使用函数指针,执行任务,
schedule_task.task_func();
}
}
}stc32游戏机:
开心,STC32自由飞翔
再来两张飞机特写,方便大家参考结构 图传如下 测试K210代码块
def ToBytes(num):
num_H = (num >> 8) & 0xFF
num_L = num & 0xFF
return
def JoinBytes(Bytes):
low_byte = Bytes
high_byte = Bytes
return (high_byte << 8) | low_byte
def GetMinDistance(Radar):
Radar.ExpressGetData(10)
DataList = Radar.ReadRadarData()
if DataList:
min_radar_data = min(DataList, key=lambda radar_data: radar_data.distance)
return min_radar_data.ToBytes()
else:
return
def SetAngleScope(Scope,Radar):
MinAngle = JoinBytes(Scope)
MaxAngle = JoinBytes(Scope)
Maxdistance = JoinBytes(Scope)
Radar.SetScope(int(MinAngle),(MaxAngle))
def GetAngleScope(Radar):
minangle,maxangle,maxdistance = Radar.GetScope()
return ToBytes(minangle)+ToBytes(maxangle)+ToBytes(maxdistance)
def FindfrequentBarCode(List):
counter = {}
for s in List:
if s not in counter:
counter = 0
counter += 1
Frequentstr = max(counter, key=counter.get)
return int(Frequentstr)
def IdentifyBarCode(Batch,TimeOut):
timeout = TimeOut
DataList = []
while(len(DataList) < Batch and timeout > 0):
img = sensor.snapshot()
barcodes = img.find_barcodes()
if barcodes:
for barcode in barcodes:
barcodeData = barcode.payload()
(x, y, w, h) = barcode.rect()
#img.draw_rectangle(barcode.rect())
#img.draw_string(x, y, barcodeData, color=(0,255,0), scale=1)
#lcd.display(img)
DataList = DataList +
else:
timeout -= 1
if len(DataList) > Batch//2:
return FindfrequentBarCode(DataList)
return -1
def BarcodeTask(Uart,Radar):
Data =
#IdentifyResult = IdentifyBarCode(10,5)
#if IdentifyResult == -1:
Data = Data +
Result = GetMinDistance(Radar)
Data = Data + Result
Scope = GetAngleScope(Radar)
Data = Data + Scope
Uart.SendData(Data)
def CalculateCenter(rect):
x, y, width, height = rect
center_x = x + width // 2
center_y = y + height // 2
return
def AverageCoordinate(List):
x_List, y_List = zip(*List)
x_avg = sum(x_List) / len(x_List)
y_avg = sum(y_List) / len(y_List)
return
#Batch:识别Batch帧的数据平均成一帧 TimeOut:尝试TimeOut次未达到收集不齐Batch帧率,收集多少算多少
def SearchPark(YoloV2Task, Batch, TimeOut):
ResultList = []
timeout = TimeOut
while(len(ResultList) < Batch and timeout > 0):
img = sensor.snapshot()
SearchResults = kpu.run_yolo2(YoloV2Task,img)
if SearchResults:
timeout = TimeOut
for Result in SearchResults:
img.draw_rectangle(Result.rect())
Coordinate = CalculateCenter(Result.rect())
ResultList.append(Coordinate)
#lcd.display(img)
timeout -= 1
if ResultList:
return ResultList
else:
return
def SearchParkTask(MyUart):
Data = #发送时带模式号
ParkingList = SearchPark(ParkingTask, 1, 10)
if ParkingList != 0xff:
ParkImgCoordinate = AverageCoordinate(ParkingList)
else:
ParkImgCoordinate = ParkingList
#ParkCoordinate = [(int)(ParkImgCoordinate-112)//1,(int)(112-ParkImgCoordinate)//1] # 计算以最中心点为原点的坐标
ParkCoordinate = [(int)(ParkImgCoordinate),(int)(ParkImgCoordinate)]
Data = Data + ParkCoordinate
MyUart.SendData(Data)
# Batch:识别多少次 TimeOut:识别不到重试次数
def JudgeColorBlobs(Batch, TimeOut):
Color = (37, 95, -28, -6, -39, 29)
ColorRoi = (54,54,112,112)
timeout = TimeOut
SuccessCount = 0
while(SuccessCount < Batch and timeout > 0):
Picture = sensor.snapshot()
ColorBlobs = Picture.find_blobs(,roi=ColorRoi, x_stride = 5,y_stride = 5,pixels_threshold=50,merge=True)
if ColorBlobs:
SuccessCount += 1
else:
timeout -= 1
if SuccessCount == Batch:
return 1
return 0
def JudgeColorTask(MyUart):
Data = #发送时带模式号
Result = JudgeColorBlobs(5,10)
Data = Data +
MyUart.SendData(Data)
def GetCoordinateTask(MyUart):
Data = #发送时带模式号
Result = CD.FindXY()
Data = Data + [(int)(Result),(int)(Result)]
MyUart.SendData(Data)
OldRevDatas =
Count = 0
while True:
RevDatas = UartToStc32.ReadData()# 串口读到的模式标志为是持久性的 stc发送一次改变模式 直到下一次收到数据后
if RevDatas:
RevCmd = RevDatas
if RevCmd != 0x00 and RevCmd != 0x11 and RevCmd != 0x01 and RevCmd != 0x02 and RevCmd != 0x03 and RevCmd != 0x04 and RevCmd != 0x0F and RevCmd != 0xF0:
UartToStc32.RcvData = OldRevDatas
if RevCmd == 0x01:
BarcodeTask(UartToStc32,MyRadar)
elif RevCmd == 0x11:
if len(RevDatas)>=7:
SetAngleScope(RevDatas,MyRadar)
UartToStc32.RcvData =
elif RevCmd == 0x02:
MyRadar.PauseScan()
SearchParkTask(UartToStc32)
elif RevCmd == 0x03:
MyRadar.PauseScan()
JudgeColorTask(UartToStc32)
elif RevCmd == 0x04:
GetCoordinateTask(UartToStc32)
elif RevCmd == 0x0F:
MyData.TakePhoto()
UartToStc32.SendData(+ToBytes(MyData.datanum-MyData.nownum+1))
UartToStc32.RcvData = OldRevDatas
elif RevCmd == 0x0E:
MapImg = MyRadar.CreatMapping()
MyDataOrigin.TakePhoto(img = MapImg)
DataList = MyRadar.ReadRadarData()
MyDbscan.DataList.clear()
MyDbscan.DataList = DataList
ClusterList, NoiseList = MyDbscan.GetCluster()
MapImg = MyRadar.CreatDbscanMapping(ClusterList, NoiseList)
MyData.TakePhoto(img = MapImg)
MapImg = MyRadar.CreatAveragRadarDataMapping(ClusterList, MyDbscan)
MyDataAfter.TakePhoto(img = MapImg)
UartToStc32.SendData(+ToBytes(MyData.datanum-MyData.nownum+1))
UartToStc32.RcvData = OldRevDatas
elif RevCmd == 0x00:
Count += 1
MyRadar.PauseScan()
if Count >= 50:
Count = 0
UartToStc32.SendData()# 停机
OldRevDatas = RevDatas[:]
gc.collect()
激光雷达训练照片
详细照片1 详细照片2 雷达检测处理图像 详细照片1