03-FPGA設計套路(1)——移位暫存器
移位暫存器
在點亮LED中,有如下程式碼:
always @ (posedge sys_clk or negedge hard_rst_n) begin if (~hard_rst_n) begin r_sys_rst_8ff <= 8'hFF; r_sys_rst <= 1'b1; end else begin r_sys_rst_8ff <= {r_sys_rst_8ff[6:0],~s_pll_lock}; r_sys_rst <= |r_sys_rst_8ff; end end
此程式碼中主要用了移位暫存器。
向左或向右的移位暫存器,在FPGA設計中有3種常用用途:
- 連續打拍
- 標識節拍
- 拉長訊號
連續打拍
假設我們需要把一個8bit訊號i_a打4拍生成o_a輸出,在FPGA中有以下幾種設計方法:
always @ (posedge clk) begin r_a_d1 <= i_a; r_a_d2 <= r_a_d1; r_a_d3 <= r_a_d2; r_a_d4 <= r_a_d3; end assign o_a = r_a_d4; always @ (posedge clk) begin r_a_4ff <= {r_a_4ff[23:0],i_a}; end assign o_a = r_a_4ff[31:24]; always @ (posedge clk) begin r_a_4ff <= {i_a,r_a_4ff[31:8]}; end assign o_a = r_a_4ff[7:0];
在需要打拍的級數較大時,使用移位暫存器可以縮減程式碼量。比如將一個2bit訊號打20拍,顯然使用移位暫存器會更簡便。
標識節拍
假設我們有一個序列輸入的8bit訊號i_a,將提供給3個通道的資料順序放在i_a上。即i_a輸入的資料為:ch0的資料->ch1的資料->ch2的資料->ch0的資料。
例如i_a輸入1,2,3,4,5,那麼1是ch0通道的資料,2是ch1通道的資料,3是ch2通道的資料,4是ch0通道的資料,以此類推。
需要將資料分離出來輸出給3個通道,程式碼如下:
always @ (posedge clk) begin if (rst) begin r_ch_flag <= 3'b001; r_a <= 8'd0; r_ch0_valid <= 1'b0; r_ch1_valid <= 1'b0; r_ch2_valid <= 1'b0; end else begin if (i_a_valid) begin r_ch_flag <= {r_ch_flag[1:0],r_ch_flag[2]}; r_a <= i_a; end r_ch0_valid <= i_a_valid & r_ch_flag[0]; r_ch1_valid <= i_a_valid & r_ch_flag[1]; r_ch2_valid <= i_a_valid & r_ch_flag[2]; end end assign o_ch0_data = r_a; assign o_ch0_valid = r_ch0_valid; assign o_ch1_data = r_a; assign o_ch1_valid = r_ch1_valid; assign o_ch2_data = r_a; assign o_ch2_valid = r_ch2_valid;
在上述程式碼中,復位時,r_ch_flag[0]為1,表示當前輸入的資料是ch0的資料。當接收到有效的a後(i_a_valid有效),通過移位切換成r_ch_flag[1]為1,表示當前輸入的資料是ch1的資料。於是,通過對flag迴圈移位,實現了對3個通道資料的標識。
在FPGA中,資料通常會和有效標誌成對出現,即data伴隨valid。
在上述程式碼中,i_a伴隨i_a_valid。當i_a打一拍生成r_a時,r_a的有效訊號應該為r_a_valid。但在這裡為了直接將r_a分配給3個通道,所以ch0通道的i_a的有效訊號是i_a_valid & r_ch_flag[0],那麼ch0通道的r_a的有效訊號就是上述程式碼中的r_ch0_valid。
再例如,我們需要將i_a延遲4拍輸出,那麼程式碼如下:
always @ (posedge clk) begin
//CHN: i_a打4拍
r_a_d1 <= i_a;
r_a_d2 <= r_a_d1;
r_a_d3 <= r_a_d2;
r_a_d4 <= r_a_d3;
//CHN: 生成伴隨資料的valid訊號
r_a_valid_4ff <= {r_a_valid_4ff[2:0],i_a_valid};
end
assign o_a = r_a_d4;
assign o_valid = r_a_valid_4ff[3];
在上述程式碼中,r_a_valid_4ff[0]標識r_a_d1是否有效,r_a_valid_4ff[1]標識r_a_d2是否有效,以此類推。
另一種標識節拍的常見程式碼是將序列協議解碼。比如我們有一個序列協議:{包頭0,包頭1,包頭2,資料}。序列輸入的資料包含3個包頭,然後是資料。3個包頭的內容分別如下:
- 包頭0:資料型別,比如資料是引數,還是資料。
- 包頭1:資料地址,比如資料需要寫入的地址。
- 包頭2:資料長度,需要注意協議是使用size(由1起始)還是length(由0起始)。
那麼解包的參考程式碼如下:
//input FF
always @ (posedge clk) begin
if (rst) begin
r_a_d1 <= 'd0; //CHN: 使用'd或'h,將位寬預設,工具會自動補全
r_a_valid <= 'd0;
end else begin
r_a_d1 <= i_a; //CHN: 輸入資料通常都需要先打1拍,可以優化時序
r_a_valid <= i_a_valid;
end
end
//CHN: 序列資料解包
always @ (posedge clk) begin
if (rst) begin
r_hd_flag <= 3'd1; //CHN: 移位暫存器構成的包頭標識,復位後當前輸入為包頭0
r_data_type <= 'd0;
r_data_addr <= 'd0;
r_data_len <= 'd0; //CHN: 假設協議中使用的是length
r_data <= 'd0;
r_data_valid <= 'd0;
end else begin
if (s_data_ilast) begin
r_hd_flag <= 3'd1; //CHN: 輸入最後1個數據後,接下來當前輸入為包頭0
end else if (i_a_valid) begin
r_hd_flag <= (r_hd_flag << 1); //CHN: 每次輸入有效,都將包頭標誌左移
end
if (i_a_valid & r_hd_flag[0]) begin //CHN: 獲取資料型別
r_data_type <= i_a;
end
if (i_a_valid & r_hd_flag[1]) begin //CHN: 獲取資料地址
r_data_addr <= i_a;
end
if (i_a_valid & r_hd_flag[2]) begin //CHN: 獲取資料長度
r_data_len <= i_a;
end else if (i_a_valid & ~|r_hd_flag) begin
r_data_len <= r_data_len - 1; //CHN: 每收到1個數據,資料長度減1
end
if (i_a_valid & ~|r_hd_flag) begin //CHN: 3個包頭標識都是0,說明是資料
r_data <= i_a;
end
r_data_valid <= i_a_valid & ~|r_hd_flag;
end
end
//CHN: len是由0開始計數,當輸入資料有效並且len為0,說明這是最後一個數據
assign s_data_ilast = i_a_valid & ~|r_data_len;
思考上述程式碼的設計思路。
拉長訊號
拉長訊號就是使用移位暫存器把想要拉長的單拍訊號打拍,然後將移位暫存器按位或。
比如點亮led程式碼中的把~s_pll_lock打8拍,然後|r_sys_rst_8ff就是一個8拍長度的未鎖定標誌。