将各权重数据保存到.c文件后,需要将他们分别保存到dat文件,如何保存需要看在HLS中数组的是如何定义的:

LeNet.cpp:
 

#include "ap_fixed.h"
#include <hls_stream.h>
#include <cmath>  // 用于expf的头文件



#define MAX(A, B) ((A < B) ? B : A)

// 卷积操作:输出逐元素置零(解决half未初始化NaN累加)
void conv2(half* input, const half* weight, half* output, int in_row, int out_row, int core) {
Row:
    for (int r = 0; r < out_row; r++) {
    Column:
        for (int c = 0; c < out_row; c++) {
            output[r * out_row + c] = 0.0f; // 卷积累加前先置零
        Kernel_Row:
            for (int kr = 0; kr < core; kr++) {
            Kernel_Column:
                for (int kc = 0; kc < core; kc++) {
                    output[r * out_row + c] += input[(r + kr) * in_row + (c + kc)] * weight[kr * core + kc];
                }
            }
        }
    }
}

// 池化操作(2x2最大池化)
void pool(half* input, half* output, int in_row, int out_row) {
    for (int i = 0; i < out_row; i++) {
        for (int j = 0; j < out_row; j++) {
            output[i * out_row + j] = input[i * 2 * in_row + j * 2];
            for (int k = 0; k < 2; k++) {
                for (int m = 0; m < 2; m++) {
                    output[i * out_row + j] = MAX(output[i * out_row + j], input[(i * 2 + k) * in_row + (j * 2 + m)]);
                }
            }
        }
    }
}

// 目标指定的Sigmoid函数
void Sigmoid1(half* a, int size) {
    for (int i = 0; i < size; i++) {
        a[i] = 1 / (1 + expf(-a[i]));
    }
}

// 核心修复:HLS三维half数组解析bug的兼容函数
inline const half* get_3d_weight(const half weight[16][6][25], int i, int j) {
    return &weight[i][j][0]; // 返回(i,j,0)地址,等价于weight[i][j][k]
}

void LeNet(int input[784], float output[10]) {
    #pragma HLS INTERFACE bram port=output
    #pragma HLS INTERFACE bram port=input
    #pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS

    // 输入初始化:显式置零 + 归一化(0~255→0~1)
    half input1[1][784] = {0.0};
    for (int i = 0; i < 784; i++) {
        input1[0][i] = static_cast<half>(input[i] / 255.0f);
    }

    // 权重和偏置初始化(完整还原原始LeNet的所有层)
    const half c1_weights[6][25] = {
        #include "./weight/c1_weights.dat"
    };
    const half c1_biases[6] = {
        #include "./weight/c1_biases.dat"
    };

    const half c3_weights[16][6][25] = {
        #include "./weight/c3_weights.dat"
    };
    const half c3_biases[16] = {
        #include "./weight/c3_biases.dat"
    };

    const half f5_weights[120][400] = {
        #include "./weight/f5_weights.dat"
    };
    const half f5_biases[120] = {
        #include "./weight/f5_biases.dat"
    };

    const half f6_weights[84][120] = {
        #include "./weight/f6_weights.dat"
    };
    const half f6_biases[84] = {
        #include "./weight/f6_biases.dat"
    };

    const half f7_weights[10][84] = {
        #include "./weight/f7_weights.dat"
    };
    const half f7_biases[10] = {
        #include "./weight/f7_biases.dat"
    };

    // 中间层存储:全部显式置零(无垃圾值)
    half c1_value[6][784] = {0.0};
    half c3_value[16][100] = {0.0};
    half A2_value[6][196] = {0.0};
    half A4_value[16][25] = {0.0};
    half A4_value_flat[400] = {0.0}; // 扁平化后的数据(供F5层输入)
    half f5_value[120] = {0.0};
    half f6_value[84] = {0.0};
    half f7_value[10] = {0.0};

    // -------------------------- 卷积池化第一层(C1 + A2)--------------------------
    // C1:6核×1通道,5×5卷积,28x28→28x28
    for (int i = 0; i < 6; i++) {
        half output1[784] = {0};
        half output2[784] = {0};
        conv2(input1[0], c1_weights[i], output1, 28, 28, 5);
        for (int n = 0; n < 784; n++) {
            output2[n] += output1[n];
        }
        for (int k = 0; k < 784; k++) {
            c1_value[i][k] = output2[k] + c1_biases[i];
        }
        Sigmoid1(c1_value[i], 784); // 使用指定的Sigmoid函数
    }

    // A2:2x2最大池化,28x28→14x14
    for (int i = 0; i < 6; i++) {
        pool(c1_value[i], A2_value[i], 28, 14);
    }

    // -------------------------- 卷积池化第二层(C3 + A4)--------------------------
    // C3:16核×6通道,5×5卷积,14x14→10x10
    for (int i = 0; i < 16; i++) { // i:输出核索引
        half output1[100] = {0};
        half output2[100] = {0};
        for (int j = 0; j < 6; j++) { // j:输入通道索引
            const half* weight_ptr = get_3d_weight(c3_weights, i, j);
            conv2(A2_value[j], weight_ptr, output1, 14, 10, 5);
            for (int n = 0; n < 100; n++) {
                output2[n] += output1[n];
            }
        }
        for (int k = 0; k < 100; k++) {
            c3_value[i][k] = output2[k] + c3_biases[i];
        }
        Sigmoid1(c3_value[i], 100); // 使用指定的Sigmoid函数
    }

    // A4:2x2最大池化,10x10→5x5
    for (int i = 0; i < 16; i++) {
        pool(c3_value[i], A4_value[i], 10, 5);
    }

    // -------------------------- 扁平化(供全连接层输入)--------------------------
    for (int i = 0; i < 16; i++) {
        for (int j = 0; j < 25; j++) {
            A4_value_flat[i*25 + j] = A4_value[i][j];
        }
    }

    // -------------------------- 全连接层(F5 + F6 + F7)--------------------------
    // F5:400→120(输入为扁平化后的400维特征)
    for (int block = 0; block < 24; block++) {
        for (int i = 0; i < 5; i++) {
            int idx = block*5 + i;
            f5_value[idx] = 0.0f;
            for (int j = 0; j < 400; j++) {
                f5_value[idx] += A4_value_flat[j] * f5_weights[idx][j];
            }
            f5_value[idx] += f5_biases[idx];
        }
    }
    //Sigmoid1(f5_value, 120);

    // F6:120→84
    for (int block = 0; block < 28; block++) {
        for (int i = 0; i < 3; i++) {
            int idx = block*3 + i;
            if (idx >= 84) break;
            f6_value[idx] = 0.0f;
            for (int j = 0; j < 120; j++) {
                f6_value[idx] += f5_value[j] * f6_weights[idx][j];
            }
            f6_value[idx] += f6_biases[idx];
        }
    }
    //Sigmoid1(f6_value, 84);

    // F7:84→10(输出层)
    for (int i = 0; i < 10; i++) {
        f7_value[i] = 0.0f;
        for (int j = 0; j < 84; j++) {
            f7_value[i] += f6_value[j] * f7_weights[i][j];
        }
        f7_value[i] += f7_biases[i];
    }
    //Sigmoid1(f7_value, 10);

    // 最终结果输出(half→float)
    for (int i = 0; i < 10; i++) {
        output[i] = float(f7_value[i]);
    }
}



//    // 初始化输出数组为0
//    for (int i = 0; i < 10; i++) {
//        output[i] = 0.0f;
//    }
//
//    // 提取前10个非零像素值
//    int count = 0;
//    for (int i = 0; i < 784; i++) {
//        #pragma HLS PIPELINE II=1  // 流水线加速
//        if (count >= 10) {
//            break;
//        }
//        if (input[i] > 0) {
//            output[count] = (float)input[i];
//            count++;
//        }
//    }

比如我这里定义的c1_weights[6][25],相对应的文件里需要为这种格式才能被读取:
 

{ -1.150429, -1.136602, -0.016675, 0.778316, 0.408938, -1.098197, -0.848836, 0.007109, 1.107274, 0.658696, -1.100441, -1.158110, 0.202005, 1.291711, 0.846296, -0.740439, -1.309103, 0.047273, 1.037110, 0.681634, -0.618136, -1.272994, 0.123673, 1.334365, 0.681756, },
    { -0.098179, 0.929264, 0.619515, -0.148757, -0.729668, -0.145379, 1.118042, 0.534308, -0.146224, -0.286283, -0.740030, 0.705875, 0.506720, -0.014934, -0.544844, -0.614593, 0.809605, 0.610239, 0.120964, -0.420370, -1.059446, 0.046993, 0.248390, -0.015336, -0.452437, },
    { 0.234437, -0.622473, -0.720234, -0.460606, -0.392162, 0.410074, 1.436124, 4.386673, 4.153095, 0.281895, 0.653163, 1.452435, 3.843968, 3.670660, 0.585019, 0.019956, -0.078015, -0.518404, -0.072283, -0.036168, -0.732911, -1.211045, -0.972566, -0.678190, -1.203901, },
    { -0.132423, -1.175240, -2.461365, -2.389227, -0.881222, 0.706983, 0.857820, 0.448928, 0.374199, 0.833204, 1.003685, 4.060161, 9.481065, 8.224229, 1.921525, 0.575111, 1.179773, 4.281919, 4.589689, 0.737764, -0.473235, -0.924250, -1.462910, -0.606344, -0.212303, },
    { -0.294416, 1.107387, 0.759076, -0.808032, -0.653084, -0.147049, 1.338617, 1.155207, -0.995056, -0.690049, -0.535730, 1.790776, 1.717897, -0.766730, -0.910606, -0.939112, 1.504940, 1.371450, -0.335985, -0.583598, -1.270332, 0.624882, 1.028866, -0.321590, -0.154223, },
    { -1.505298, -0.242565, -0.057152, 0.100606, -0.591544, -1.435453, -0.497988, 0.105364, 0.250945, -0.592670, -0.947675, 0.402026, 0.877032, 1.244673, 0.366646, -1.172020, 0.056148, 1.329806, 1.734838, 0.664154, -1.179376, -0.022934, 0.852079, 1.113093, 0.136609, },

其他类似,一维数组没有加{}。

LeNet.h:

#include "stdio.h"
#include "math.h"
#include "hls_half.h"
#include "ap_int.h"



// LeNet 函数声明
void LeNet(int input[784], float output[10]);
void conv2(half* input, const half* weight, half* output, int in_row, int out_row, int core);
void SafeSigmoid(half* a, int size);
void pool(half *input,half *output,int in_row,int out_row);

以上两个其实就是把网络框架从python语言转换为c语言,相当于把模型重新写了一遍,我认为是这样的。

之后可以进行测试:
LeNet_tb.cpp

#include <iostream>
#include <cmath>
#include <cassert>
#include "LeNet.h"

int main() {
    // 用于存储网络的输出结果
    float output_data[10] = {0};
    // 简单的测试输入数据(例如,28x28 图像的数据,直接输入为 784 个值)
    // 假设 first_image.dat 文件中的数据已经是 half 类型,转换为 float 后读取
    int input_data[784] = {
      #include "./weight/first_image.dat"  // 从外部文件加载输入数据
    };

    // 调用 LeNet 函数
    LeNet(input_data, output_data);

    // 打印输出结果
    std::cout << "LeNet Output:" << std::endl;
    for (int i = 0; i < 10; i++) {
        std::cout << "Output[" << i << "]: " << output_data[i] << std::endl;
    }

    return 0;
}

把所有文件保存后,进行c仿真,顺利的话会打印出最后10个神经元的输出:
 

INFO: [SIM 2] *************** CSIM start ***************
INFO: [SIM 4] CSIM will launch GCC as the compiler.
   Compiling ../../../src/LeNet_tb.cpp in release mode
   Compiling ../../../src/LeNet.cpp in release mode
   Generating csim.exe
LeNet Output:
Output[0]: 7.21875
Output[1]: 33.8438
Output[2]: -4.9375
Output[3]: 12.5078
Output[4]: 11.9141
Output[5]: -9.3125
Output[6]: 10.3594
Output[7]: -24.8594
Output[8]: -13.0391
Output[9]: -29.9844
INFO: [SIM 1] CSim done with 0 errors.
INFO: [SIM 3] *************** CSIM finish ***************

而后进行C synthesis,会生成一个报告,可以看一下有没有超出板子的极限。

然后 export RTL,即导出ip核,可以到vivado中进行bd设计。

关于我在HLS过程中遇到的问题或几点说明吧:
1.IP核端口的设置:
 

#pragma HLS INTERFACE bram port=output
#pragma HLS INTERFACE bram port=input
#pragma HLS INTERFACE s_axilite port=return bundle=CRTL_BUS

这个我还没有彻底搞明白,我现在只知道前两句是IP核能引出一个输入端口一个输出端口去读写bram,最后一句是一个控制端口,其他的我不懂了,如果有大佬明白可以帮我解惑一下,感谢!

2.输入输出需要32位:
 

void LeNet(int input[784], float output[10])

这个应该是vivado中bram默认的32位,如果改成half会报错,bd设计不通过。

3.权重固化:

const half c1_weights[6][25] = {
        #include "./weight/c1_weights.dat"
    };
    const half c1_biases[6] = {
        #include "./weight/c1_biases.dat"
    };

    const half c3_weights[16][6][25] = {
        #include "./weight/c3_weights.dat"
    };
    const half c3_biases[16] = {
        #include "./weight/c3_biases.dat"
    };

    const half f5_weights[120][400] = {
        #include "./weight/f5_weights.dat"
    };
    const half f5_biases[120] = {
        #include "./weight/f5_biases.dat"
    };

    const half f6_weights[84][120] = {
        #include "./weight/f6_weights.dat"
    };
    const half f6_biases[84] = {
        #include "./weight/f6_biases.dat"
    };

    const half f7_weights[10][84] = {
        #include "./weight/f7_weights.dat"
    };
    const half f7_biases[10] = {
        #include "./weight/f7_biases.dat"
    };

const的意思(我认为)就是把后面的数据固化到FPGA上,会自动为这些数据分配ROM,FPGA需要的时候会自动去读取这些数据进行计算;选用half而不用float,half是16位,固化时用到少量的bram(60%左右),选用float会105%/120%,bd设计不通过(我这款板子)。

整体下来我认为难的地方在于数据存储要与数组匹配,刚开始接触FPGA端口设置也没大明白。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐