前言
一、环境准备
软件
- Quartus Prime 18.1 Standard Edition
- Visual Studio2010 Ultimate X86
- Win32 Disk Imager
- SoCEDS18.1
- MobaXterm 22.0
- CP210x Universal Windows Driver
硬件
- Cyclone V C5MB_PCBA
二、测试文件及ip制作
2.1 测试文件
- 目录结构,手写识别(一)课程中获取的头文件
- 增加mm_slave.cpp文件,代码如下:
#include "HLS/hls.h"
#include "stdio.h"
#include "input_1.h" //图片 1 的像素值
#include "input_5.h"
#include "input_8.h"
#include "layer1_weight.h" //权重参数、偏置参数
#include "layer1_bias.h"
#include "layer2_weight.h"
#include "layer2_bias.h"
#define IMG_ROW 1
#define IMG_COL 784
#define WEIGHT0_ROW 784
#define WEIGHT0_COL 64
#define WEIGHT1_ROW 64
#define WEIGHT1_COL 10
hls_avalon_slave_component
component int full_connect(
hls_avalon_slave_memory_argument(784*sizeof(float)) float* img_pixel, //输入像素 1*784
hls_avalon_slave_memory_argument(50176*sizeof(float)) float* weight1, //输入第一层权重参数 784*64
hls_avalon_slave_memory_argument(64*sizeof(float)) float* bias1, //输入第一层偏置参数 1*64
hls_avalon_slave_memory_argument(640*sizeof(float)) float* weight2, //输出第二层权重参数 64*10
hls_avalon_slave_memory_argument(10*sizeof(float)) float* bias2 //输出第二层偏置参数 1*10
)
{
static float a[WEIGHT0_COL];
static float b[WEIGHT1_COL];
static float result[WEIGHT1_COL];
float c=0.0f;
int num=0;
for(int i=0;i<WEIGHT0_COL;i++)
{
a[i] = 0.0f;
for(int j=0;j<IMG_COL;j++)
{
a[i] = a[i] + img_pixel[j]*weight1[i+j*WEIGHT0_COL]; //第一层全连接
}
a[i] = (a[i] + bias1[i])>0?(a[i] + bias1[i]):0.0; //第一层偏置 、 激活
}
for(int i=0;i<WEIGHT1_COL;i++)
{
b[i] = 0.0f;
for(int j=0;j<WEIGHT1_ROW;j++)
{
b[i] = b[i] + a[j]*weight2[i+j*WEIGHT1_COL]; //第二层全连接
}
b[i] = b[i] + bias2[i]; //第二层偏置 1*10
if(b[i] > c)
{
c = b[i]; //选出10个数中的最大值 和对应的索引号 ,然后返回索引号
num = i;
}
}
return num;
}
int main()
{
int res = 0;
res = full_connect(input_5,layer1_weight,layer1_bias,layer2_weight,layer2_bias);
printf("\nthe number is %d\n",res);
return 0;
}
- win + r cmd打开dos窗口
- 初始化hls环境,在安装的IntelFPGA\18.1\hls下执行init_hls.bat
注意路径,初始化过后不能关闭dos窗口,关闭过后环境重置。
- 切换至工程目录
- 运行命令:i++ -march=x86-64 mm_slave.cpp
tips:march是machine architecture机器架构的意思。
- 执行过后会生成一个exe可执行文件,运行测试一下
2.2 ip制作
- 在FPGA平台上测试,执行命令:i++ -march=CycloneV mm_slave.cpp -v -ghdl
三、硬件制作
- 将ip制作步骤中生成的ip,复制到黄金工程的ip文件夹下(platform designer会自动搜索ip)
- 打开黄金工程的qpf(quartus project file)文件
- 点击platform designer
- 选择qsys文件
- 选择full_connect
- 点击finish
- 生成结果
- 点击空心圈进行连线
- 分配基地址
- Generate生成HDL
- Generate
- finish过后弹出以下窗口
- 编译工程(编译后生成sof文件,大约10多分钟)
- 搜索SoC EDS
- 切换至黄金工程目录,操作和linux一致
- make dtb生成设备树
- 设备树文件
- 进入output_files目录,双击sof_to_rbf.bat生成rbf文件
- 点击运行结果
四、烧写硬件和系统
- Win32 Disk Imager刻录镜像文件至SD卡
- 替换rbf文件
- 替换dtb文件
五、搭建软件工程
- 生成头文件
Soc EDS Command Shell 命令窗口切换到硬件工程目录,输入./generate_hps_qsys_header.sh
- 执行生成的头文件
- 黄金工程新建app目录
- 打开eclipse
- 工作空间
- 新建c工程
- 工程名字
- 新建c源文件
- 将手写识别产生的权重、偏置、测试图片的头文件,以及hps_0.h文件复制到工程
- 添加库文件路径,因人而异,看你的quartus安装在哪里
F:\intelFPGA\18.1\embedded\ip\altera\hps\altera_hps\hwlib\include
F:\intelFPGA\18.1\embedded\ip\altera\hps\altera_hps\hwlib\include\soc_cv_av
- 编写main.c
//1.导入头文件
//2.接口定义(结构体的方式)
//3.定义初始化函数
// 例如:int led_init(void *virtual_base)
//4.主函数功能
//gcc标准头文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
//HPS厂家提供的底层定义头文件
#define soc_cv_av //开发平台Cyclone V 系列
#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"
//与用户具体的HPS 应用系统相关的硬件描述头文件
#include "hps_0.h"
#include "input_0.h" //10张图片的像素值矩阵 还可以通过网口、串口输入像素值,存储再数组 中,全部接收完成后再发送给FPGA进行计算
#include "input_1.h"
#include "input_2.h"
#include "input_3.h"
#include "input_4.h"
#include "input_5.h"
#include "input_6.h"
#include "input_7.h"
#include "input_8.h"
#include "input_9.h"
#include "layer1_bias.h" //权重参数、偏置参数矩阵
#include "layer1_weight.h"
#include "layer2_bias.h"
#include "layer2_weight.h"
#define HW_REGS_BASE (ALT_STM_OFST) //HPS外设地址段基地址
#define HW_REGS_SPAN (0x04000000) //HPS外设地址段地址空间 64MB大小
#define HW_REGS_MASK (HW_REGS_SPAN - 1) //HPS外设地址段地址掩码
//接口定义(结构体的方式)
typedef struct{
volatile float *img;
volatile float *w1;
volatile float *b1;
volatile float *w2;
volatile float *b2;
}fc_port_def;
fc_port_def fc_port;
typedef struct{
volatile long long busy;
volatile long long start;
volatile long long ire_en;
volatile long long done;
volatile long long result;
}fc_ctrl_def;
fc_ctrl_def *fc_ctrl;
int fc_init(void *virtual_base)
{
void *fc_ctrl_addr;
fc_ctrl_addr = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_CRA_BASE) & (unsigned long)(HW_REGS_MASK));
fc_ctrl = (fc_ctrl_def*)fc_ctrl_addr; //接口映射
fc_ctrl->start = 0;
fc_port.img = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_IMG_PIXEL_BASE) & (unsigned long)(HW_REGS_MASK));
fc_port.w1 = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_WEIGHT1_BASE) & (unsigned long)(HW_REGS_MASK));
fc_port.b1 = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_BIAS1_BASE) & (unsigned long)(HW_REGS_MASK));
fc_port.w2 = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_WEIGHT2_BASE) & (unsigned long)(HW_REGS_MASK));
fc_port.b2 = virtual_base + ((unsigned long)(ALT_LWFPGASLVS_OFST + CONNECT_0_FULL_CONNECT_INTERNAL_INST_AVS_BIAS2_BASE) & (unsigned long)(HW_REGS_MASK));
//加载权重参数、偏置参数
memcpy(fc_port.w1,layer1_weight,784*64*sizeof(float));
memcpy(fc_port.b1,layer1_bias,64*sizeof(float));
memcpy(fc_port.w2,layer2_weight,64*10*sizeof(float));
memcpy(fc_port.b2,layer2_bias,10*sizeof(float));
return 0;
}
const float *imgx[10]={input_0,input_1,input_2,input_3,input_4,input_5,input_6,input_7,input_8,input_9};
int main()
{
int fd,i;
void *virtual_base;
float time_s,time_ns,time_ms;
struct timespec ts1,ts2;
//1.打开MMU open()
fd = open("/dev/mem",(O_RDWR | O_SYNC));
if(fd == (-1))
{
printf("ERROR:could not open\"/dev/mem\"...\n");
return 1;
}
//2.将外设地址空间映射到用户空间 mmap()
virtual_base = mmap(NULL,HW_REGS_SPAN,( PROT_READ | PROT_WRITE ),MAP_SHARED,fd,HW_REGS_BASE);
//3.初始化(一般是自己写的函数 )
fc_init(virtual_base);
//4.对外设进行相应的操作
while(1)
{
for(i=0;i<10;i++)
{
memcpy(fc_port.img,imgx[i],784*sizeof(float));
clock_gettime(CLOCK_MONOTONIC,&ts1); //记录函数开始时间
fc_ctrl->start = 1;//打开推理
while((fc_ctrl->done & 0x02) == 0);//当done不为2的时候(推理未完成),就阻塞(等待)
printf("%d",fc_ctrl->done);
fc_ctrl->start = 0; //推理完成,关闭使能
clock_gettime(CLOCK_MONOTONIC,&ts2); //记录函数结束时间
//由于总的时间=time_s+time_ns
//为了显示方便,将总的时间统一转化为毫秒
time_s = ts2.tv_sec - ts1.tv_sec;
time_ns = ts2.tv_nsec - ts1.tv_nsec;
time_ms = time_s*1000 + time_ns/1000000;
printf("predict time:%.6f ms\n",time_ms);
printf("input:%d,predict result:%d\n",i,fc_ctrl->result);
}
break;
}
//5.取消映射 munmap()
if(munmap(virtual_base,HW_REGS_SPAN)!=0)
{
printf("ERROR:munmap()failed...\n");
close(fd);
return 1;
}
//6.关闭设备描述符 close()
close(fd);
return 0;
}
- ctrl + s保存然后编译
- 生成binary
六、调试
- 插入sd卡,启动开发板
- 打开MobaXterm
- 输入用户:root
- 输入密码:test
- 设置静态ip
- 配置开发板ip地址,使电脑和开发板处于同一网段,使用vi编辑器:vi /etc/network/interfaces
- 配置结果
- 测试电脑和开发板是否连通
- 配置ssh
-
reboot重启
-
eclipse配置ssh
- 输入开发板ip地址
- 切换视图
- 建立连接
- 输入用户名和密码
- 复制生成的binary文件full_connect
- 粘贴至开发板Linux的/opt文件夹下
- 打开terminal
- 修改full_connect文件的权限,执行chmod 777 full_connect
运行结果显示,推理一张的图片所花的时间是1ms多一点,与手写识别(一)中推理一张图片要使用0.8s时间相比,低了一个量级的推理时间,可见FPGA的优势是显而易见的。
总结
手写识别项目到此就画上句号了,过程很艰辛,但结果让人满意。通过实验对比,我们也看到了FPGA推理神经网络模型的优势。后期将推出使用FPGA实现口罩识别项目,敬请期待!