# 2021年全国电子设计大赛F题送药小车教学
作者:阳海峰
## 1. 摄像头部分
```
import sensor, image, lcd, time
import KPU as kpu
import gc, sys
from machine import UART
from fpioa_manager import fm
```
`sensor`是摄像头模块
`image`是图像模块
`lcd`是显示屏模块
`time`是时间模块
`KPU`是图像处理加速模块
`gc` 是垃圾回收模块
`sys`是系统模块
`machine`是外设模块
---
```
lcd.init(freq=15000000)
```
*显示屏初始化*
---
```
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))
sensor.set_hmirror(False)
sensor.set_vflip(False)
sensor.run(1)
```
`sensor.reset()`是重置摄像头
`sensor.set_pixformat(sensor.RGB565)`是设置像素格式,为`RGB565`即16位彩图
`sensor.set_framesize(sensor.QVGA)`是设置分辨率为`QVGA`(320X240)
`sensor.set_windowing((224, 224))`是设置窗口大小,这个模型只支持(224, 224)的窗口尺寸
`sensor.set_hmirror(False)`为设置水平翻转,这里为False即不翻转
`sensor.set_vflip(False)`设置竖直翻转,这里设置为False即不翻转
`sensor.run(1)`是让摄像头正式跑起来
---
```
fm.register(9, fm.fpioa.UART1_TX, force=True)
fm.register(10, fm.fpioa.UART1_RX,force=True)
```
这里是从`fm`调用一个管理引脚映射的函数`register`来实现物理引脚,在*STM32*里面一般物理引脚都是写死的,即使存在映射也是在固定的映射表里面。而*K210*可以任意映射物理引脚
- 第一句是把物理引脚9映射到串口1的发送端
- 第二局是把物理引脚映射到串口1的接收端
---
```
uart1 = UART(UART.UART1,115200,8,1,0,timeout=1000, read_buf_len=4096)
```
初始化串口1,波特率设置为115200,8位数据位,1位起始位,无校验位,超时1000ms,接收缓冲长度设置为4096
---
```
if __name__ == "__main__":
try:
labels = ["9","1","4","2","3","8","5","6","7"]
anchors = [0.4192, 0.3702, 0.5744, 1.6689, 0.6932, 0.6054, 1.0054, 0.9615, 2.1672, 1.6683]
main(anchors = anchors, labels=labels, model_addr=0X300000)
except Exception as e:
sys.print_exception(e)
pass
finally:
gc.collect()
```
`if __name__ == "__main__":`此句表示当本文件以脚本运行时运行这部分,如果是以模块调用则不执行这一部分。
`try:`表示先尝试运行这一部分代码,如果抛出异常就执行`except Exception as e:`下面的代码,否则执行`finally:`后面的代码
`sys.print_exception(e)`为打印异常
`gc.collect()`为垃圾回收,如果出现异常则会进行垃圾回收并退出*boot.py*去执行*main.py*的代码(如果你没有写,系统会自动帮你生成)
`labels = ["9","1","4","2","3","8","5","6","7"]`是模型的标签,他表示物体是归属于那个类(标签就是类的名字)
`anchors = [0.4192, 0.3702, 0.5744, 1.6689, 0.6932, 0.6054, 1.0054, 0.9615, 2.1672, 1.6683]`表示锚点数,他说描述模型的一个特征矩阵。
`main(anchors = anchors, labels=labels, model_addr=0X300000)`是调用主函数(其实名字可任取),在主函数传进去的参数里面,anchors表示锚点数,labels表示标签列表,model_addr表示模型路径会内存地址。(如果存在文件系统中就用路径,如果烧录在内置flash则用内存地址)
```
def main(anchors = None,labels = None, model_addr=0X300000):
task = kpu.load(model_addr)
kpu.init_yolo2(task, 0.5, 0.3, 5, anchors)
try:
while True:
img = sensor.snapshot()
objects = None
objects = kpu.run_yolo2(task, img)
if objects:
for obj in objects:
pos = obj.rect()
img.draw_rectangle(pos)
img.draw_string(pos[0], pos[1], "%s : %.2f" %(labels[obj.classid()], obj.value()), scale=2, color=(255,255,255))
#发送信息
buf = bytearray([0X3A,int(labels[obj.classid()]),0X5C])
uart1.write(buf)
#print(buf)
#lcd.display(img)
except Exception as e:
raise e
finally:
kpu.deinit(task)
```
定义一个名字为`main`的函数,此函数传入的参数如下:
- `anchors`锚点列表
锚点和模型一一对应,一个模型对应一个描点列表
- `labels`标签列表
标签是把数字序列翻译成字符比如如果模型检测到苹果会返回3,那么列表里面第4(从0开始数)个就数"apple",这样方便使用(没人会觉得4比"apple"表达苹果更加生动吧)
- `model_addr`模型地址或路径
如果你是使用flash直接烧录模型的化就用直接地址比如你把模型烧录在0X300000就把0X300000传给他,如果你把他放到文件系统比如SD卡就把路径传给它,例如"sd/m.kmodel"或"/m.kmodel"
- `task = kpu.load(model_addr)`是加载模型,把数据从硬盘加载到数据存储器
- `kpu.init_yolo2(task, 0.5, 0.3, 5, anchors)`是创建任务把相关参数传给封装好的`init_yolo2`函数
- `while True:`这是一个大循环,类似于C语言里面的`while(1){...}`
- `img = sensor.snapshot()`是从摄像头获取图片
- `objects = None`先定义一个变量并使他为空
- `objects = kpu.run_yolo2(task, img)`把任务和图片传入`run_yolo2`从而获得检测到的目标
- `if objects:`如果检测到目标的话(之所以要用if是因为如果检测不到对象那么后面的操作会报错异常退出)
- `for obj in objects:`遍历检测的的1至多个目标
- `pos = obj.rect()`获取目标的位置
- `img.draw_rectangle(pos)`画一个框,把检测到的物体框出来
- `img.draw_string(pos[0], pos[1], "%s : %.2f" %(labels[obj.classid()], obj.value()), scale=2, color=(255,255,255))`把标签和概率标出来
- `buf = bytearray([0X3A,int(labels[obj.classid()]),0X5C])`发送信息,把信息格式化并存入`buf`
- `uart1.write(buf)`把数据通过串口发送出去
- 异常处理
```
except Exception as e:
raise e
finally:
kpu.deinit(task)
```
## 2. stm32部分
### 1. 主函数
```
#include "main.h"
char T_flag = 0;
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
OLED_Init();
openmv_config(9600);
k210_init(115200);
Encoder_Init_TIM2(0X0000,0XFFFF);
Encoder_Init_TIM3(0X0000,0XFFFF);
TIM1_PWM_Init(71,99);
Motor_Init();
//确认巡线摄像头初始化
while(recv.pix == 0);
//确定第一个T形路口怎么转
if(recv.rho > 5)
{
T_flag = 1;
}
else
{
T_flag = 2;
}
TIM4_Init();
while(1)
{
// OLED_Print(1,1,"first:%d",firstNum);
// OLED_Print(2,1,"get:%d",numGet);
//// OLED_Print(2,1,"T:%2d",T_flag);
// OLED_Print(3,1,"lenth:%6d",speed_sum);
//// OLED_Print(4,1,"L:%d R:%d",see[0],see[1]);
OLED_Print(1,1,"rho:%4d",recv.rho);
OLED_Print(2,1,"theta:%4d",recv.theta);
OLED_Print(3,1,"mag:%4d",recv.mag);
OLED_Print(4,1,"pix:%1.3f",recv.pix);
}
}
```
- `#include "main.h"`导入一些函数和变量的声明
- `int main(void)`主函数,*stm32*上电后执行完默认启动项会跳转到`main`
- `NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);`中断向量分组
- `OLED_Init();`初始化OLED
- `openmv_config(9600);`初始化连接openmv的串口
- `k210_init(115200);`初始化连接k210的串口
-
```
Encoder_Init_TIM2(0X0000,0XFFFF);
Encoder_Init_TIM3(0X0000,0XFFFF);
```
初始化定时器2和3用来读取编码器,不分频,自动重装载值为0XFFFF(65535)
- `TIM1_PWM_Init(71,99);`初始化定时器1用来产生PWM波,周期为100us,装载值为99
- `Motor_Init();`初始化控制电机正反转的电机
-
```
//确认巡线摄像头初始化
while(recv.pix == 0);
```
等待巡线摄像头初始化
-
```
//确定第一个T形路口怎么转
if(recv.rh