前文根据奇哥的方法使用FPGA实现了SM3加密算法,算法实现方式正确,但在并行度为2的情况下,在zynq7030ffg676-2也只能跑到50MHz,并行度为1也跑不到100MHz。
因此在了解原理的过程中,发现消息扩展和迭代过程其实可以全部放入流水线中,并行度固定为1,最终设计能够跑到200MHz,下面就讲解代码实现过程。
1、代码讲解
首先是端口信号,根据参数DATA_W设置输入数据位宽,取值范围是[1,447],支持对这个范围内所有数据加密。
module sm3_encrypt #(
parameter DATA_W = 9'd256 //待加密数据位宽,取值范围[1,447]。
)(
input clk ,//系统时钟信号;
input rst_n ,//系统复位信号,低电平有效;
input [DATA_W - 1 : 0] din ,//输入待加密数据;
input din_vld ,//输入待加密数据有效指示信号,高电平有效;
output reg [255 : 0] dout ,//输出加密结果数据信号;
output reg dout_vld //输出加密结果数据有效指示信号,高电平有效;
);
然后是整个加密过程中会使用到的函数,如下所示,与前文代码一致。
//生成置换函数P1
function [31 : 0] P1;
input [31 : 0] X;
begin//X异或X循环左移15位,在异或X循环左移23位;
P1 = X ^ {X[16:0],X[31:17]} ^ {X[8:0],X[31:9]};
end
endfunction
//计算置换函数P0
function [31 : 0] P0;
input [31 : 0] X;
begin//计算公式:P0=X ^ (X <<< 9) ^ (X <<< 17)
P0 = X ^ {X[22:0],X[31:23]} ^ {X[14:0],X[31:15]};
end
endfunction
//计算布尔函数FFj(x,y,z);
function [31 : 0] FFj;
input [31 : 0] X;
input [31 : 0] Y;
input [31 : 0] Z;
input [31 : 0] j;
begin
FFj = (j < 16) ? (X ^ Y ^ Z) : ((X & Y) | (X & Z) | (Y & Z));
end
endfunction
//计算布尔函数GGj(x,y,z);
function [31:0] GGj;
input [31 :0] X;
input [31 :0] Y;
input [31 :0] Z;
input [31 :0] j;
begin
GGj = (j < 16) ? (X ^ Y ^ Z) : ((X & Y) | (~X & Z));
end
endfunction
//常量Tj;
function [31:0] Tj;
input [31:0] j;
begin
Tj = (j < 16) ? 32'h79cc4519 : 32'h7a879d8a;
end
endfunction
使用移位寄存器将输入数据有效指示信号暂存,与后续填充数据和大端转换数据对齐。
//使用移位寄存器将输入数据暂存;
always@(posedge clk)begin
din_vld_r[2:0] <= {din_vld_r[1:0],din_vld};
end
把输入数据填充到512比特,对应代码如下所示。注意工程在运行时必须保证待加密数据位宽与设置的输入数据位宽一致,否则以设置的输入数据位宽为准。
always@(posedge clk)begin
if(~rst_n)begin
padding_data <= 'd0;
end
else begin//进行填充,在数据后面加1'b1,然后将数据补足448位,最后64位存储数据长度;
padding_data <= {din[DATA_W - 1 : 0],1'b1,{{447-DATA_W}{1'b0}},{55'd0,DATA_W[8:0]}};
end
end
数据在扩展和迭代过程中都是以大端模式进行存储,因此需要先将输入小端数据转换为大端数据,对应代码如下所示。
genvar g_i;
generate
for(g_i = 0 ; g_i < 16 ; g_i = g_i + 1)begin : encode
always@(posedge clk)begin
if(din_vld_r[0])//将输入的小端数据转换为大端数据;
padding_data_big[g_i*32+31 : g_i*32] = padding_data[511-g_i*32 : 480-g_i*32];
end
end
endgenerate
前面这部分与前文基本一致,修改部分主要在消息扩展,迭代必须每个时钟迭代一轮,因此也没太多修改的地方。
如下图所示,上面是消息扩展部分,下面是迭代过程,前文是通过一个时钟计算出wi(1667)和wi’(063)所有的结果。经过下面不难看出,每次迭代只使用了消息扩展wi和wi’中的一个数据,那么是不是每个时钟也只需要计算一个wi和wi’的数据即可呢?
而且消息扩展的大部分延时都在计算wi这部分,那是不是可以在这段计算过程中多插入几级触发器呢?首先wi(016)是消息填充后的数据,表示在后面的64轮迭代过程中,其实只有53个时钟需要计算wi(1667)的值,完全可以插入几级触发器。
计算wi’只需要wi和wi+4即可,只需要在迭代开始时,将wi‘0提前计算出即可,后续数据可以在迭代的同时计算出。
整体的修改思路就是这样,在消息扩展中插入触发器,将一个时钟的并行计算分为64个时钟串行计算,并且不会影响迭代效率。
对应代码如下所示,首先把大端转换完成的数据存入wi[0~15]中,作为后续运算的数据。
genvar g_exi;
generate//将输入的低16个字存入扩展结果中;
for(g_exi = 0 ; g_exi < 16 ; g_exi = g_exi + 1)begin : Extend_Word1
always@(posedge clk)begin
if(~rst_n)
extend_data_m[g_exi] <= 'd0;
else if(din_vld_r[1])
extend_data_m[g_exi] <= padding_data_big[(g_exi * 32) + 31 : g_exi * 32];
end
end
endgenerate
然后计算出wi’[0]便于后续第一轮迭代使用。
//计算第一个w'(0)
always@(posedge clk)begin
if(~rst_n)
extend_data_m0[0] <= 'd0;
else if(din_vld_r[2])//将输入的低16个字存入扩展结果中;
extend_data_m0[0] <= extend_data_m[0] ^ extend_data_m[4];
end
将迭代运算的初始值存入ai[0]~hi[0],作为迭代运算的初始值。
//迭代的初始值;
always@(posedge clk)begin
if(~rst_n)begin
{ai[0],bi[0],ci[0],di[0],ei[0],fi[0],gi[0],hi[0]} <= 256'h7380166f4914b2b9172442d7da8a0600_a96f30bc163138aae38dee4db0fb0e4e;
end
end
下面的cmp_vld用于指示当前进行的迭代轮数,对应位为高电平表示正在进行对应轮的迭代。
//表示正在进行计算;
always@(posedge clk)begin
cmp_vld[64:0] <= {cmp_vld[63:0],din_vld_r[2]};
end
下面的循环内部包含每个时钟需要完成的消息扩展和迭代运算。
genvar i;
generate
for(i = 0 ; i < 64 ; i = i + 1)begin : I
//每个时钟完成一次扩展运算;
if(i < 52)begin
always@(posedge clk)begin
if(~rst_n)
p1_x[i] <= 'd0;
else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
p1_x[i] <= extend_data_m[i] ^ extend_data_m[i+7] ^ {extend_data_m[i+13][16:0],extend_data_m[i+13][31:17]};
end
end
//计算P1的结果;
if((i > 0) && (i < 53))begin
always@(posedge clk)begin
if(~rst_n)
p1[i - 1] <= 'd0;
else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
p1[i - 1] <= P1(p1_x[i - 1]);
end
end
//每个时钟完成一次扩展运算;
if((i > 1) && (i < 54))begin
always@(posedge clk)begin
if(~rst_n)
extend_data_m[14 + i] <= 'd0;
else if(cmp_vld[i])//用于计算w(16)~w(67)的数据;
extend_data_m[14 + i] <= (p1[i - 2]) ^ {extend_data_m[i+1][24:0],extend_data_m[i+1][31:25]} ^ extend_data_m[i+8];
end
end
//每个时钟完成一次扩展运算;
if(i < 63)begin
always@(posedge clk)begin
if(~rst_n)
extend_data_m0[i + 1] <= 'd0;
else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
extend_data_m0[i + 1] <= extend_data_m[i + 1] ^ extend_data_m[i + 5];
end
end
/**************************** 消息扩展结束 ****************************/
/**************************** 迭代运算部分开始 ****************************/
assign w_Tj[i] = Tj(i);//计算Ti
assign w_Tj_y[i] = (i[4:0]) ? ({w_Tj[i][31 - i[4:0] : 0],w_Tj[i][31:32 - i[4:0]]}) : w_Tj[i];//计算(Ti<<<(jmod32))
assign w_ss1_mid0[i] = {ai[i][19:0],ai[i][31:20]} + ei[i] + w_Tj_y[i];//计算(A<<<12)+E+(Ti<<<(jmod32));
assign w_ss1[i] = {w_ss1_mid0[i][24:0] , w_ss1_mid0[i][31:25]};//计算SS1=((A<<<12)+E+(Ti<<<(jmod32)))<<<7;
assign w_ss2[i] = w_ss1[i] ^ {ai[i][19:0],ai[i][31:20]};//计算SS2=SS1^(A<<<12);
assign w_tt1[i] = FFj(ai[i],bi[i],ci[i],i) + di[i] + w_ss2[i] + extend_data_m0[i];//计算TT1=FFi(A,B,C)+D+SS2+W';
assign w_tt2[i] = GGj(ei[i],fi[i],gi[i],i) + hi[i] + w_ss1[i] + extend_data_m[i];//计算TT2=GGi(E,F,G)+H+SS1+Wi;
always@(posedge clk)begin
if(~rst_n)
{ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= 'd0;
else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
{ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= {w_tt1[i],ai[i],{bi[i][22:0],bi[i][31:23]},ci[i],P0(w_tt2[i]),ei[i],{fi[i][12:0],fi[i][31:13]},gi[i]};
end
/**************************** 迭代运算部分结束 ****************************/
end
endgenerate
使用三级流水线来计算消息扩展结果wi[16~67],首先计算P1函数的系数,对应代码如下。
//每个时钟完成一次扩展运算;
if(i < 52)begin
always@(posedge clk)begin
if(~rst_n)
p1_x[i] <= 'd0;
else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
p1_x[i] <= extend_data_m[i] ^ extend_data_m[i+7] ^ {extend_data_m[i+13][16:0],extend_data_m[i+13][31:17]};
end
end
下个时钟将上个时钟得到的p1系数带入函数,计算得到p1函数的结果。
//计算P1的结果;
if((i > 0) && (i < 53))begin
always@(posedge clk)begin
if(~rst_n)
p1[i - 1] <= 'd0;
else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
p1[i - 1] <= P1(p1_x[i - 1]);
end
end
然后再完成表达式P1 (Wi-16异或Wi-9异或(Wi-3<<<15))异或(Wi-13<<<7)异或Wi-6的计算,对应代码如下。
//每个时钟完成一次扩展运算;
if((i > 1) && (i < 54))begin
always@(posedge clk)begin
if(~rst_n)
extend_data_m[14 + i] <= 'd0;
else if(cmp_vld[i])//用于计算w(16)~w(67)的数据;
extend_data_m[14 + i] <= (p1[i - 2]) ^ {extend_data_m[i+1][24:0],extend_data_m[i+1][31:25]} ^ extend_data_m[i+8];
end
end
下面是通过wi计算wi’的代码,依旧是采用流水线设计,每个时钟计算一个数据。
//每个时钟完成一次扩展运算;
if(i < 63)begin
always@(posedge clk)begin
if(~rst_n)
extend_data_m0[i + 1] <= 'd0;
else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
extend_data_m0[i + 1] <= extend_data_m[i + 1] ^ extend_data_m[i + 5];
end
end
上述完成了消息扩展,下面就是迭代的内容,由于迭代必须在一个时钟完成,而且里面的函数需用到上次迭代的结果,导致中间无法插入触发器(插入触发器会导致几个时钟才能完成一次迭代,会减小数据吞吐量),因此这部分依旧使用组合逻辑实现,与前文并没有什么改变。
下面是计算ss1、ss2、tt1、tt2的代码,直接与图1的迭代过程对比即可。
assign w_Tj[i] = Tj(i);//计算Ti
assign w_Tj_y[i] = (i[4:0]) ? ({w_Tj[i][31 - i[4:0] : 0],w_Tj[i][31:32 - i[4:0]]}) : w_Tj[i];//计算(Ti<<<(jmod32))
assign w_ss1_mid0[i] = {ai[i][19:0],ai[i][31:20]} + ei[i] + w_Tj_y[i];//计算(A<<<12)+E+(Ti<<<(jmod32));
assign w_ss1[i] = {w_ss1_mid0[i][24:0] , w_ss1_mid0[i][31:25]};//计算SS1=((A<<<12)+E+(Ti<<<(jmod32)))<<<7;
assign w_ss2[i] = w_ss1[i] ^ {ai[i][19:0],ai[i][31:20]};//计算SS2=SS1^(A<<<12);
assign w_tt1[i] = FFj(ai[i],bi[i],ci[i],i) + di[i] + w_ss2[i] + extend_data_m0[i];//计算TT1=FFi(A,B,C)+D+SS2+W';
assign w_tt2[i] = GGj(ei[i],fi[i],gi[i],i) + hi[i] + w_ss1[i] + extend_data_m[i];//计算TT2=GGi(E,F,G)+H+SS1+Wi;
后面是将迭代的计算结果存入ai[i+1]~hi[i+1]中,作为下轮迭代的初始值。
always@(posedge clk)begin
if(~rst_n)
{ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= 'd0;
else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
{ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= {w_tt1[i],ai[i],{bi[i][22:0],bi[i][31:23]},ci[i],P0(w_tt2[i]),ei[i],{fi[i][12:0],fi[i][31:13]},gi[i]};
end
将64轮迭代的结果ai[64]~hi[64]与迭代初始值ai[0]~hi[0]异或得到加密结果,对应代码如下,同时生成加密结果有效指示信号。
always@(posedge clk)begin
if(~rst_n)begin
dout <= 'd0;
end
else begin//将迭代计算的最终结果与初始值异或,得到加密结果;
dout <= {ai[64],bi[64],ci[64],di[64],ei[64],fi[64],gi[64],hi[64]} ^ {ai[0],bi[0],ci[0],di[0],ei[0],fi[0],gi[0],hi[0]};
end
end
always@(posedge clk)begin
dout_vld <= cmp_vld[64];
end
2、代码仿真
顶层模块和仿真激励文件生成模块依旧使用前文的代码,只需要修改模块的参数即可。
顶层模块代码如下所示:
//--###############################################################################################
//--#
//--# File Name : top
//--# Designer : 数字站
//--# Tool : Vivado 2021.1
//--# Design Date : 2024.6.2
//--# Description : sm3加密顶层模块
//--# Version : 0.0
//--# Coding scheme : GBK(If the Chinese comment of the file is garbled, please do not save it and check whether the file is opened in GBK encoding mode)
//--#
//--###############################################################################################
module top (
input clk ,//系统时钟信号,默认100MHz;
input rst_n ,//系统复位信号,低电平有效;
output reg led
);
(* MARK_DEBUG = "TRUE" *)reg [255 : 0] Original_Data ;
(* MARK_DEBUG = "TRUE" *)reg Original_Valid ;
reg [3 : 0] cnt ;
(* MARK_DEBUG = "TRUE" *)wire [255 : 0] Encrypt_Data ;
(* MARK_DEBUG = "TRUE" *)wire Encrypt_Valid ;
sm3_encrypt #(
.DATA_W ( 256 ) //待加密数据位宽,取值范围[1,447]。
)
u_sm3_encrypt (
.clk ( clk ),//系统时钟信号;
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.din ( Original_Data ),//24'h616263 ),//输入待加密数据;
.din_vld ( Original_Valid),//输入待加密数据有效指示信号,高电平有效;
.dout ( Encrypt_Data ),//输出加密结果数据信号;
.dout_vld ( Encrypt_Valid ) //输出加密结果数据有效指示信号,高电平有效;
);
//循环产生测试数据的时钟信号;
always@(posedge clk)begin
if(~rst_n)
cnt <= 'd0;
else
cnt <= cnt + 'd1;
end
//每间隔128个时钟生成一个测试数据;
always@(posedge clk)begin
if(~rst_n)begin
Original_Data <= 256'h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0b0c0d0e0f;
Original_Valid <= 'd0;
end
else if(&cnt)begin
Original_Data <= Original_Data + 3;
Original_Valid <= 'd1;
end
else begin
Original_Valid <= 'd0;
end
end
//防止Encrypt_Data被优化掉;
always@(posedge clk)begin
if(~rst_n)begin//初始值为0;
led <= 'd0;
end
else if(Encrypt_Valid)begin
led <= (Encrypt_Data > 10000);
end
end
endmodule
运行仿真结果如下所示,加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0b0c0d0e12,最后加密结果为256’h3C6F866ABC77AF25_F3EE32C4864BDAE506_F4A946197D774D45_ACD127797360A6。
使用网页加密的结果如下所示,与上图仿真保持一致,证明上述设计的逻辑运算应该没有问题。
3、上板测试
首先综合工程,然后分配管脚,添加ILA,加入时钟约束,当前系统时钟约束为200MHz,如下图所示,然后对工程进行布局布线,最后生成比特流文件。
查看时序报告,如下所示,建立时间余量和保持时间余量均大于零,表示该设计工作在200MHz时钟下没有问题。
将比特流文件下载到开发板,使用ILA抓取一帧数据,如下所示。待加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020 304050607_08090a0b0d0856a3,抓取加密结果为256’h 2FE2A06A9075CC30_30BA74879761D179_60AD30C6ABA57F37_ECA4410AA A1E0864。
使用加密工具验证结果如下所示,与上图加密结果一致,表示加密正常。
使用ILA再抓取一帧数据,如下所示。待加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0 b341f75ca,抓取加密结果为256’h D9E6BCD11C87679B_D07B230BC9368229_6537E97CFD774AAD_A54ABDAEBA935C1B。
使用加密工具验证结果如下所示,与上图加密结果一致,表示加密正常。
在讲解SM3原理时,对24’h616263进行了加密,本文对该加密结果进行验证,首先把顶层模块的输入数据位宽修改为24位,输入数据改为固定的24’h616263,如下图所示。
然后重新综合工程,将生成的比特流下载到FPGA中,使用ILA抓取加密结果如下所示,加密结果为256’h 66C7F0F462EEEDD9_D1F2 D46BDC10E4E2_4167C4875CF2F7A2_297DA02B8F4BA8E0。
最后使用网页工具验证24’h616263的加密结果,如下所示,与上图ILA抓取的结果一致,表示加密正确,与前文原理讲解时的加密结果也是一致的。
虽然该工程在zynq7030ffg676-2上可以正常工作在200MHz时钟下,但是输入有效数据间隔时间必须大于等于16个时钟,因为消息扩展的低16个数据必须被用于迭代计算后才能输入新的数据,否则这些数据会被覆盖掉,导致计算错误。
通过把消息扩展的低16个数据使用触发器保存一段时间,不会被后输入的数据覆盖,来达到支持连续数据输入的目的。但是会消耗更多的资源,奇哥的代码由于消息扩展在一个时钟就被计算出来,然后保存了,因此下一个数据输入不会影响上次输入的数据计算。
本文的工程可以在公众号后台回复“SM3算法的流水线优化”(不包括引号)获取。
如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!
如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!