---
# System prepended metadata

title: 'CNN-FPGA '

---

# INTRO
這份筆記是從 [CNN-FPGA](https://github.com/omarelhedaby/CNN-FPGA) 這份開源資料中整理而得的。
整理了如何用Verilog實現CNN的幾個步驟，原版作者已經將內容發布成一篇論文，也將內容整理成了一本書。如果想更深入的探討可以再從作者的官方github中探討。

這份筆記的大多數圖片與內容都出自於這作者出的這本書:
![image](https://hackmd.io/_uploads/r1hAwAPtxx.png)

# Network


首先要先知道一個CNN是包含哪幾層，以下是整個網路的結構:
![image](https://hackmd.io/_uploads/SycB_0wFeg.png)

* 有三層卷積層(CONV)
* 兩層平均池化層(avgPool)
* 每層卷積層後面都有一個激活函數(Tanh)
* 再來還有兩層全連階層(FClayer)
* 最後再一個SoftMax函數輸出一個結果

所用的數據集是MNIST

# Convolution
以下先介紹卷積層，是以綠色標出來的模塊:
![image](https://hackmd.io/_uploads/B1qAtCvKxl.png)
實現出來的方案在並行度、速度以及資源占用中進行了一些權衡。

在vivado中打開後可以看到這個畫面
![image](https://hackmd.io/_uploads/B1vh6NuYel.png)
從這裡就看的出來他Network的flow是像我們前面說的

Conv2D->Tanh Activation->AvgPool->Conv2D->Tanh Activation->AvgPool->Conv2D->Tanh Activation->Fully Connected Layer->Relu->Fully Connected Layer->Softmax

而我們這邊要先整理**自頂而下**的卷積層邏輯，目的是了解設計者的思路，再**自底而上**的去詳細的進行功能分析與原理了解。
## Block diagrams
### (1) Multi Filter Layer
一個卷積層，有多個Filter，image輸入到不同的Filter得到不同特徵的feature map
![image](https://hackmd.io/_uploads/ryGR1SdFxx.png)
![image](https://hackmd.io/_uploads/H128-rdtgl.png =300x)


### (2) Single Filter Layer
單個filter，輸入image，輸出feature map。
![image](https://hackmd.io/_uploads/Syw9fBdKgx.png)
![image](https://hackmd.io/_uploads/Skk3MBOKlx.png =300x)

### (3) Convolutional Unit
每個卷積核與輸入圖像進行卷積，得到特徵圖。
![image](https://hackmd.io/_uploads/HyVFXBOtlg.png)
![image](https://hackmd.io/_uploads/BJ4s7HOKee.png =300x)

### (4) Processing Element
卷積的具體操作是內積(dot product)，本質是乘法與加法。這裡輸入的是float，硬體實現也就是定點數。
![image](https://hackmd.io/_uploads/SyTCmruYgl.png)
![image](https://hackmd.io/_uploads/ryvU4BdYxe.png =300x)

## 功能解析
### (4) Processing Element
![image](https://hackmd.io/_uploads/HylR8r_Kxe.png)

* floatMult16:兩個半精度浮點數的乘法
* floatADD16:兩個半精度浮點數的加法

半經度浮點數(16bits)，簡寫為FP16:
* 符號位1bit
* 指數位5bits
* 尾數位10bits
![image](https://hackmd.io/_uploads/Skl3vruYxg.png)
$$
\textbf{Number} = (-1)^s \times (1.M) \times 2^{(E - \text{Bias})}, \quad \text{Bias} = 15
$$
### FP16加法器
浮點數的加減法其實不難，就五個步驟:
1. 對階
2. 尾數運算
3. 規格化
4. 捨入
5. 溢出判斷。
以下是代碼以及我自己加的代碼註釋做參考:
```
module floatAdd16 (floatA,floatB,sum);
	
input [15:0] floatA, floatB; //兩個FP16
output reg [15:0] sum;

reg sign; // sign bit
reg signed [5:0] exponent; //sixth bit is sign 可以決定指數是否溢出
reg [9:0] mantissa; //10位小數部分 (1.mantissa)
reg [4:0] exponentA, exponentB; //指數位
reg [10:0] fractionA, fractionB, fraction;	//fraction = {1,mantissa} 比mantissa多一個隱藏1 變成11位
reg [7:0] shiftAmount; //在標準化裡需要移位
reg cout;

always @ (floatA or floatB) begin
	exponentA = floatA[14:10]; //取出floatA的指數位
	exponentB = floatB[14:10]; //取出floatB的指數位
	fractionA = {1'b1,floatA[9:0]}; //把floatA的小數取出來(mantissaA)
	fractionB = {1'b1,floatB[9:0]};//把floatA的小數取出來(mantissaB) 
	
	exponent = exponentA; //隨便給個初值

============特殊情況===========
	if (floatA == 0) begin						//special case (floatA = 0)
		sum = floatB;
	end else if (floatB == 0) begin					//special case (floatB = 0)
		sum = floatA;
	end else if (floatA[14:0] == floatB[14:0] && floatA[15]^floatB[15]==1'b1) // floatA 與 floatB的大小相等符號相反(1 XOR 0 = 1)
    begin
		sum=0;
	end 
 ============非特殊情況==============   
    else 
    begin
		if (exponentB > exponentA) begin //對階，使兩個數的階碼相等，小的對齊大的，若尾數向右移一格，指數位加一
			shiftAmount = exponentB - exponentA;
			fractionA = fractionA >> (shiftAmount);
			exponent = exponentB; //exponent等於大的
		end 
        else if (exponentA > exponentB) 
        begin 
			shiftAmount = exponentA - exponentB;
			fractionB = fractionB >> (shiftAmount);
			exponent = exponentA; //exponent等於大的
		end
		if (floatA[15] == floatB[15]) begin			//same sign
			{cout,fraction} = fractionA + fractionB;
			if (cout == 1'b1) begin //檢查是否溢出
				{cout,fraction} = {cout,fraction} >> 1; //若溢出則向右移一位
				exponent = exponent + 1; //指數位加一
			end
			sign = floatA[15];
		end else begin						//different signs
			if (floatA[15] == 1'b1) begin //A- B+
				{cout,fraction} = fractionB - fractionA; //fraction是無號數
			end
            else //符號位不同
            begin
				{cout,fraction} = fractionA - fractionB;
			end
			sign = cout;
			if (cout == 1'b1) begin
				fraction = -fraction;
			end else begin
			end
			if (fraction [10] == 0) begin //第十位那個隱藏位必須要是1，如果是0就要調整，就像一般的科學記號那樣。
            
				if (fraction[9] == 1'b1) begin
					fraction = fraction << 1; //左移一位，指數就要減一，以下同理
					exponent = exponent - 1;
				end else if (fraction[8] == 1'b1) begin
					fraction = fraction << 2;
					exponent = exponent - 2;
				end else if (fraction[7] == 1'b1) begin
					fraction = fraction << 3;
					exponent = exponent - 3;
				end else if (fraction[6] == 1'b1) begin
					fraction = fraction << 4;
					exponent = exponent - 4;
				end else if (fraction[5] == 1'b1) begin
					fraction = fraction << 5;
					exponent = exponent - 5;
				end else if (fraction[4] == 1'b1) begin
					fraction = fraction << 6;
					exponent = exponent - 6;
				end else if (fraction[3] == 1'b1) begin
					fraction = fraction << 7;
					exponent = exponent - 7;
				end else if (fraction[2] == 1'b1) begin
					fraction = fraction << 8;
					exponent = exponent - 8;
				end else if (fraction[1] == 1'b1) begin
					fraction = fraction << 9;
					exponent = exponent - 9;
				end else if (fraction[0] == 1'b1) begin
					fraction = fraction << 10;
					exponent = exponent - 10;
				end 
			end
		end
		mantissa = fraction[9:0];
		if(exponent[5]==1'b1) begin //exponent is negative 溢出
			sum = 16'b0000000000000000; //當0
		end
		else begin
			sum = {sign,exponent[4:0],mantissa};
		end		
	end		
end

endmodule
```

### FP16乘法器
乘法器跟前面的加法器差不多，也有三個步驟:
1. 階碼相加
2. 尾數相乘
3. 結果規格化
除法的話是:
1. 尾數調整
2. 階碼求差
3. 尾數相除
以下是代碼以及我自己加的代碼註釋做參考:
```
module floatMult16 (floatA,floatB,product);

input [15:0] floatA, floatB;
output reg [15:0] product;

reg sign;
reg signed [5:0] exponent; //6th bit is the sign 有符號的exponent
reg [9:0] mantissa;
reg [10:0] fractionA, fractionB;	//fraction = {1,mantissa}
reg [21:0] fraction; //因為有尾數相成，所以尾數會變兩倍

===============特殊情況============
always @ (floatA or floatB) begin
	if (floatA == 0 || floatB == 0) begin
		product = 0;
	end 
    else 
=============非特殊情況============
    begin
		sign = floatA[15] ^ floatB[15]; //進行XOR得到符號位
        
		exponent = floatA[14:10] + floatB[14:10] - 5'd15 + 5'd2; //後面減15(因為exp-bias,FP的bias=15),所以這裡的exp要-15才是真正的exp。加2應該是拓展位寬
	
		fractionA = {1'b1,floatA[9:0]};
		fractionB = {1'b1,floatB[9:0]};
		fraction = fractionA * fractionB;
============規格化=================	
		if (fraction[21] == 1'b1) begin //要找mantissa 所以不要最前面的1
			fraction = fraction << 1;
			exponent = exponent - 1; 
		end else if (fraction[20] == 1'b1) begin
			fraction = fraction << 2;
			exponent = exponent - 2;
		end else if (fraction[19] == 1'b1) begin
			fraction = fraction << 3;
			exponent = exponent - 3;
		end else if (fraction[18] == 1'b1) begin
			fraction = fraction << 4;
			exponent = exponent - 4;
		end else if (fraction[17] == 1'b1) begin
			fraction = fraction << 5;
			exponent = exponent - 5;
		end else if (fraction[16] == 1'b1) begin
			fraction = fraction << 6;
			exponent = exponent - 6;
		end else if (fraction[15] == 1'b1) begin
			fraction = fraction << 7;
			exponent = exponent - 7;
		end else if (fraction[14] == 1'b1) begin
			fraction = fraction << 8;
			exponent = exponent - 8;
		end else if (fraction[13] == 1'b1) begin
			fraction = fraction << 9;
			exponent = exponent - 9;
		end else if (fraction[12] == 1'b0) begin
			fraction = fraction << 10;
			exponent = exponent - 10;
		end 
	
		mantissa = fraction[21:12];
		if(exponent[5]==1'b1) begin //exponent is negative
			product=16'b0000000000000000;
		end
		else begin
			product = {sign,exponent[4:0],mantissa};
		end
	end
end

endmodule
```

### (3) Convolution Unit
一個窗口卷積出一個計算結果(FP16)，所以ConvUnit的作用就是**循環使用PE完成一個窗口的卷積運算，並輸出最後計算結果。**(用速度換面積)
![image](https://hackmd.io/_uploads/SyJsFSOFxe.png)

### (2) Single Filter Layer
由RFselector與CU所組成，執行一個Filter與image的卷積操作，輸出為一個feature map
![image](https://hackmd.io/_uploads/Syw9fBdKgx.png)

### (5) RFselector:
作用是對一個已經展開的一維image tensor進行數據重排，再對應分發給n個CU進行窗口卷積。
![image](https://hackmd.io/_uploads/HkTQk8_Yxl.png)
可以從下圖看出RFselector作為多個CU的輸入:
![image](https://hackmd.io/_uploads/rJjBkL_Fle.png)

## (1) Multi Filter Layer 
在Network固定了有兩個Layer，所以並行度為2。
這份資料有趣的地方除了把CNN拆解得很細以外，有另外一個點是他所有的模塊都進行層層分裝了，所以可以把一個模塊拆解成很多的子模塊，就可以讓我們去調整他的並行度。

那最基礎的單位就是PE，然後由PE構成CU，我們也可以去調整CU的數量，再去構建Single Filter Layer，所以在這個專案裡面每個模塊的並行度都可以自己做調整。
![image](https://hackmd.io/_uploads/ryGR1SdFxx.png)

# SoftMax Acitivation
![image](https://hackmd.io/_uploads/B1I1aADKxe.png)
SoftMax是這整個網路的最後一層，輸入為FClayer生成的10個值，然後在最終的分類給出結果。
## Block diagrams
![image](https://hackmd.io/_uploads/S1Rl4LOteg.png)
![image](https://hackmd.io/_uploads/r1ftN8_Yxg.png)
作用:SoftMax函數，將輸入標準化，求得各種類的概率。

電路邏輯:
1. 指數計算
2. 計算指數和
3. 求指數和倒數
4. 計算每個元素的SoftMax值(將指數值乘上指數和倒數)

如果你有一組數值V，Vi是V的第i個元素，則這個元素的SoftMax值為
$$
S_i = \frac{e^{z_i}}{\sum_{j} e^{z_j}}
$$


電路類型:時序邏輯電路

### SoftMax
所以SoftMax就是透過前面所述的邏輯分成幾個步驟來進行運算。

1.先將多個input分別輸入到各自的exponent來求指數。
![image](https://hackmd.io/_uploads/rkwgtIOYgx.png)

2.然後透過加法器來求指數和。再由floatReciprocal來計算指數和的倒數。
![image](https://hackmd.io/_uploads/HJrdFLutxx.png)

3.最後透過乘法器來計算各輸入的SoftMax值
![image](https://hackmd.io/_uploads/HyYkqLuYxl.png)

### (6) exponet
![image](https://hackmd.io/_uploads/S1m0qUdFll.png)
![image](https://hackmd.io/_uploads/S1qnqLutll.png)

作用:指數函數，求解e^x值。

邏輯:用泰勒展開近似，包含兩個乘法器以及一個加法器。

電路類型:時序邏輯電路。
$$
e^x = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!} + \frac{x^5}{5!} + \frac{x^6}{6!}
$$

# Average Pooling
![image](https://hackmd.io/_uploads/By8SpCwYxg.png)
在這個網路中有兩個平均池化層，作用是採集卷積層輸出，在減小數據量的時候盡可能保持數據的特徵。

## Block diagram
### (1) Average Pool Multi Layer
![image](https://hackmd.io/_uploads/BksA4vOKxl.png)
![image](https://hackmd.io/_uploads/HyKgHPutlg.png =300x)
功能:進行多通道的image的平均池化，但這裡的併行度為1，透過循環使用AvgPollSingle完成所有通道的平均池化。

電路類型:時序邏輯電路

### (2) Average Pool Single Layer
![image](https://hackmd.io/_uploads/rkp_tYdYel.png)
![image](https://hackmd.io/_uploads/r19KFF_tgx.png =300x)
功能:執行單個通道的平均池化，這裡為全並行度，每一個窗口都有一個AvgU

電路類型:組合邏輯電路

全並行度意謂著可以一次計算完一個窗口需要計算的數量。如下圖，若窗口為2x2，則全並行度就是可以一次計算四個，不用循環使用AvgU。所以有多少個窗口，就有多少個AvgU模塊。
![image](https://hackmd.io/_uploads/SkhJM9dKxx.png)

### Average Unit

![image](https://hackmd.io/_uploads/rJxsG5OKxe.png)
![image](https://hackmd.io/_uploads/r123GcdFgl.png)
![image](https://hackmd.io/_uploads/HkICfcdFlx.png)
功能:求輸入四個數的均值

邏輯:先求和，在將和乘以0.25(Const)即為四個數的均質。

電路類型:組合邏輯電路(然後我覺得這裡可以加pipeline)

# TanH Activation
即雙曲正切函數:
![image](https://hackmd.io/_uploads/BkxcDqCvKgx.png)


以下為雙曲正切函數的公式
$$
f(x)
= \frac{\sinh x}{\cosh x}
= \frac{1 - e^{-2x}}{1 + e^{-2x}}
= \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}
= \frac{e^{2x} - 1}{e^{2x} + 1}
= 2\,\operatorname{sigmoid}(2x) - 1
$$

![image](https://hackmd.io/_uploads/HJshj0PKge.png)

TahH如上圖藍線所示為其函數圖，其有以下幾個特點:
1. 函數輸出以(0，0)為中值
2. 其收斂速度比Sigmoid更快

### Block design
### (1) Using The Tanh
![image](https://hackmd.io/_uploads/HJMDMs_Kex.png =300x)
![image](https://hackmd.io/_uploads/SJvbNouFxg.png)
功能:把輸入壓縮到 [−1,1]，給神經網路做非線性映射。

邏輯:把大資料 bus 拆成單一輸入 → tanh → 合併回去，並用 MUX/RESET 控制流程。

電路類型:時序邏輯電路

### (2) HyperbolicTangent
![image](https://hackmd.io/_uploads/r1jWri_Ylx.png =300x)
![image](https://hackmd.io/_uploads/Sy87SodFgg.png)
![image](https://hackmd.io/_uploads/B1EIHjutgg.png)
![image](https://hackmd.io/_uploads/HJHDSjdKgg.png)
功能:算Tanh的值

邏輯:用前面給的那個公式算近似，包含三個乘法器和一個加法器。

電路類型:時序邏輯電路


# Integrating Network
![image](https://hackmd.io/_uploads/rku360PKxg.png)
將所有模塊連接在一起，調通控制通路與數據通路，是項目中最難工作量最大的一個部分

## Block Design
### (1) IntegrationFC
![image](https://hackmd.io/_uploads/SkxKkiOKll.png)

功能:積體的全連階層，包含兩個全連階層，TanH激活函數以及SoftMax函數層。

* Weight:儲存全連階層的權重
* Layer:進行線性運算
* TanH:激活非線性
* SoftMax:多分類

### (2) Layer
![image](https://hackmd.io/_uploads/SJVOejOYxl.png)
功能:進行線性計算

最主要的功能是在下面的這個always block，用來控制各個模塊的啟動，是透過Counter來決定。
```
always @(posedge clk or posedge reset) begin
  if (reset == 1'b1) begin
    FC1reset = 1'b1;
    FC2reset = 1'b1;
    TanhReset = 1'b1;
    SMaxEnable = 1'b0;
    counter = 0;
    address1 = -1;
    address2 = -1;
  end
  else begin
      counter = counter + 1; //工作週期記數
      
      //第一個if
    if (counter > 0 && counter < IntIn + 10) begin
       FC1reset = 1'b0; //啟動第一個FClayer
    end
    else if (counter > IntIn + 10 && counter < IntIn + 12 + FC_1_out*6) begin
       TanhReset = 1'b0; //啟動Tanh
       address2 = -3;
    end
    else if (counter > IntIn + 12 + FC_1_out*6 && counter < IntIn + 12 + FC_1_out*6 + FC_1_out + 10) begin
       FC2reset = 1'b0; //啟動FC2
    end
    else if (counter > IntIn + 12 + FC_1_out*6 + FC_1_out + 10) begin
       SMaxEnable = 1'b1; //啟動SoftMax
    end
    if (address1 != 8'hfe) begin
      address1 = address1 + 1;
    end
    else
      address1 = 8'hfe;
    address2 = address2 + 1;
  end
end

endmodule  
```
