文章目录
前言
一、环境准备
软件
- 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 -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;
//定义一个指针数组指向10张图片的数组,方便后面的操作
const float *img_test[10] = {input_0,input_1,input_2,input_3,input_4,input_5,input_6,input_7,input_8,input_9};
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实现口罩识别项目,敬请期待!










