背景:卷积的c程序用于FPGA生成IPcore,其中参数的传输用的stream格式。
目的:读懂这个程序
相应的单片机程序参考。文档UG902相关内容。
目录
读懂相关程序
//top.h
#ifndef __TOP_H
#define __TOP_H
template<int D,int U,int TI,int TD>
struct ap_axif{
float data;
ap_uint<(D+7)/8> keep;
ap_uint<(D+7)/8> strb;
ap_uint<U> user;
ap_uint<1> last;
ap_uint<TI> id;
ap_uint<TD> dest;
};
typedef ap_axiu<32,1,1,1> AXI_VALUE;
typedef hls::stream<AXI_VALUE> AXI_STREAM;
void axi_stream_top(AXI_STREAM &inStream, AXI_STREAM &outStream);
#endif
1. template 模板
模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计。C++ 的标准库提供许多有用的函数大多结合了模板的观念,如STL以及IO Stream。
c++模板总结https://blog.csdn.net/qq_35637562/article/details/55194097
-
1.1函数模板
Template <class或者也可以用typename T>
返回类型 函数名(形参表)
{//函数定义体 }
template是一个声明模板的关键字,在模板定义语法中关键字class与typename的作用完全一样。声明一个模板关键字class不能省略,如果类型形参多余一个 ,每个形参前都要加class <类型 形参表>可以包含基本数据类型也可以包含类类型.
//exmaple
template <class T>
T min(T x,T y)
{ return(x<y)?x:y;}
void main( )
{
int n1=2,n2=10;
double d1=1.5,d2=5.6;
cout<< "较小整数:"<<min(n1,n2)<<endl;
cout<< "较小实数:"<<min(d1,d2)<<endl;
system("PAUSE");
}
程序分析:main()函数中定义了两个整型变量n1 , n2 两个双精度类型变量d1 , d2然后调用min( n1, n2); 即实例化函数模板T min(T x, T y)其中T为int型,求出n1,n2中的最小值.同理调用min(d1,d2)时,,T为double型,求出d1,d2中的最小值.
-
1.2类模板
类模板的写法
Template < class或者也可以用typename T >
class类名{
//类定义......
};
关于类模板的使用:类模板的使用实际上是将类模板实例化成一个具体的类,它的格式为:类名<实际的类型>
-
1.3参数模板
模板可以有类型参数,也可以有常规的类型参数int,也可以有默认模板参数,
-
1.4模板专门化
当我们要定义模板的不同实现,我们可以使用模板的专门化。例如我们定义的stack类模板,如果是char*类型的栈,我们希望可以复制char的所有数据到stack类中,因为只是保存char指针,char指针指向的内存有可能会失效,stack弹出的堆栈元素char指针,指向的内存可能已经无效了。还有我们定义的swap函数模板,在vector或者list等容器类型时,如果容器保存的对象很大,会占用大量内存,性能下降,因为要产生一个临时的大对象保存a,这些都需要模板的专门化才能解决。
2.#ifndef,防止重复编译
2.1 #ifndef x //先测试x是否被宏定义过
2.2 #define x ... //如果没有宏定义下面就宏定义x并编译下面的语句
2.3 #endif //如果已经定义了则编译#endif后面的语句
条件指示符#ifndef检查预编译常量在前面是否已经被宏定义。如果在前面没有被宏定义,则条件指示符的值为真,于是从#ifndef到#endif之间的所有语句都被包含进来进行编译处理。相反,如果#ifndef指示符的值为假,则它与#endif指示符之间的行将被忽略。条件指示符#ifndef 的最主要目的是防止头文件的重复包含和编译。
3. iostream
iostream 库的基础是两种命名为 istream 和 ostream 的类型,分别表示输入流和输出流。流是指要从某种 IO 设备上读出或写入的字符序列。术语“流”试图说明字符是随着时间顺序生成或消耗的。
标准库定义了 4 个 IO 对象。处理输入时使用命名为 cin(读作 see-in)的 istream 类型对象。这个对象也称为标准输入。处理输出时使用命名为 cout(读作 see-out)的 ostream 类型对象,这个对象也称为标准输出。标准库还定义了另外两个 ostream 对象,分别命名为 cerr 和 clog(分别读作“see-err”和“see-log”)。cerr 对象又叫作标准错误,通常用来输出警告和错误信息给程序的使用者。而 clog 对象用于产生程序执行的一般信息。
因此,当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
>>运算符,用来从一个istream对象读取输入数据;
<<运算符,用来向一个ostream对象写入输出数据;
//iostream example
int test_iostream_cin()
{
char name[50];
std::cout << "Please enter your name: ";
std::cin >> name;
std::cout << "Your name is: " << name << std::endl;
return 0;
}
4. C++中的&
&符号可以用于位与,取址,引用。
4.1 取址&
变量前面加上&即可。
int i = 0 , *pi = &i; //指针pi获得了i的地址
int ** p_pi = π //指针p_pi获得了pi的地址
cout << &i << endl
<< &pi << endl << &p_pi; //可以输出地址
4.2 引用&
引用就是被引变量的另一个名字 , 当改变引用值时 , 被引值也会改变 , 因为它们两就是同一个值 , 另外 , 引用不能脱离对象单独存在 , 引用是依附与对象存在的 , 因为引用在定义后可能被使用 , 所以引用不能是无对象的 , 所以引用在定义时必须初始化。
int i = 0 , &ri = i; //定义并初始化一个引用
ri++; //使i的值加1
//int &ri2; 这里是非法的 , 因为定义了引用ri2却没有初始化它 , 在这行代码和下一行之间可能使用无对象的引用
//ri2 = i;
//int &ri3 = 10;
//错误 , 引用必须有对象 , 常量没有对象int i = 0 , j = 0;
int & ri = i; //合法 , ri是i的引用
ri = j; //合法 , ri是j的引用int i = 0;
int &ri = i , *pi = &i; //前一个&是引用 , 与类型名绑定在一起 , 后一个是取地址
cout << ri << endl; //输出i的值
cout << &i << endl; //输出i的地址
cout << &ri << endl; //输出ri的地址
cout << &pi << endl; //输出pi的地址
为什么要有引用 , 在C++这样一种重视效率的语言里 , 必然需要这样一种类型 , 但我们调用一个函数时 , 就必须把参数全部复制一次 , 这样既浪费时间又浪费空间 , 但当我们把函数列表中的形参改为该类型的引用时 , 编译器就不会把整个对象复制进去 , 而只是把对象的地址传过去 , 这样在函数里使用的就是实参本身了 , 也能够修改它的值。
void reference_test(int &i){ //传入的是引用,不是地址!
cout << i <<endl;
i = 0;
cout << i << endl; //离开函数后被传入的i的值为0
}
主程序解析
//cnn.cpp
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "parameters.h"
#include "necstream.hpp"
#define REAL float
#define BIT_WIDTH 32
/* DMA constants */
float weights[16*27] = {}
void cnn(FloatStream streamIn, FloatStream streamOut)
{
FloatAxis tmp;
int i, j, k;
int sof = 1;
//DMA buffer and pointers
REAL buf_in[27*600];
REAL buf_out[16*600];
//REAL weights[16*27];
REAL temp, result;
//Load input image data
for (i = 0; i < 27 * 600; i++) {
#pragma HLS PIPELINE II=1
buf_in[i] = streamPop < float, FloatAxis, FloatStream > (streamIn);
}
for (i = 0; i < 16; i++) {
for (j = 0; j < 600; j++) {
result = 0;
for (k = 0; k < 27; k++) {
temp = weights[i*27+k] * buf_in[k*600+j];
result += temp;
}
buf_out[i*600+j] = result;
}
}
for (i = 0; i < 16 * 600; i++) {
//#pragma HLS PIPELINE II=1
FloatAxis d;
d.data = buf_out[i];
if (sof) {
d.user = 1;
sof = 0;
} else {
d.user = 0;
}
if (i == 16*600 - 1)
d.last = 1;
else
d.last = 0;
d.keep = -1;
streamOut << d;
}
}
5.全局变量与局部变量
//example
int a,b; /*外部变量*/
void f1() /*函数f1*/
{
……
}
float x,y; /*外部变量*/
int f2() /*函数f2*/
{
……
}
main() /*主函数*/
{
int maomi();
……
}/*全局变量x,y作用域 全局变量a,b作用域*/
- 从上例可以看出a、b、x、y 都是在函数外部定义的外部变量,都是全局变量。但x,y 定义在函数f1之后,而在f1内又无对x,y的说明,所以它们在f1内无效。 a,b定义在源程序最前面,因此在f1,f2及main内不加说明也可使用。
- 全局变量是使用相同的内存块在整个类中存储一个值.
- 全局变量extern与static:extern在其他源程序中也可以使用;static只能在本程序中使用。
- 所以程序中的weights算是全局变量,(存储在DMA constants中?)主函数中可以调用。
6. IPcore主函数的核心:输入输出
输入:buf_in[i] = streamPop < float, FloatAxis, FloatStream> (streamIn);
streamPop是个类模板。其中有三个参数,float,FloatAxis,FloatStream
6.1 FloatStream和UIntStream分别表示float和unsigned int类型的数据流。
在necstream.hpp程序中的定义:
typedef my_ap_axis<float,32,1,1,1> FloatAxis;
typedef my_ap_axis<unsigned int,32,1,1,1> UIntAxis;
typedef hls::stream<FloatAxis> FloatStream;
typedef hls::stream<UIntAxis> UIntStream;
6.2 streamPop 是一个函数,它的定义也在necstream.hpp中
template <class RET, class DATA, class STREAM>
RET streamPop(STREAM &stream){
RET value;
DATA axisData;
stream >> axisData;
value = axisData.data;
return value;
}
这个函数的的意思就是把单片机送来的stream送给axisData,然后把axisData.data给value,从而实现从单片机中送来的数据读入IPcore。这样看来单片机中的数据不止是数据,有其他的内容,而IPcore中程序需要用的value只是单片机送来的stream中的一部分。
所以相应的template <class RET, class DATA, class STREAM>
分别对应streamPop < float, FloatAxis, FloatStream> (streamIn);
主函数后面的参数(streamIn)对应于模板里面RET streamPop(STREAM &stream)中的(STREAM &stream)
6.3 输入和输出中,IPcore需要用于运算的数据仅仅是数据data,而与单片机通信的是stream,stream中包括data,但比data多一些其他的内容。
输出时,除了相应的d.data是需要的数据外,多一个d.user和d.last,这个在数据流之中,意思是当d.user为1时意思是数据流的开始,d.last为1时表示数据流的结束。
6.4 具体的输入输出流操作
输入输出参见上面3
>>运算符,用来从一个istream对象读取输入数据;
<<运算符,用来向一个ostream对象写入输出数据;
6.4.1 数据传入中,用streamPop函数来向IPcore传数据,stream>>axisData
template <class RET, class DATA, class STREAM>
RET streamPop(STREAM &stream){
RET value;
DATA axisData;
stream >> axisData;
value = axisData.data;
return value;
}
6.4.2 数据传出中,加一个d.user和d.last之后,直接<<,把数据传给输出流streamOut
FloatAxis d;
d.data = buf_out[i];//定义d.data
//定义d.user和d.last和d.keep
streamOut << d;