本文是本系列第三篇,我们将通过分析一个 SM3 的开源硬件实现,来进一步了解算法的实现流程和硬件实现思路
首发知乎
作者:李凡
相比在 GitHub 上搜索出的软件实现方案,硬件 SM3 实现就少得多,好像就几个的样子,其中有一个是笔者的 。
链接:https://github.com/ljgibbslf/sm3\_highP
咳咳,目前还没有装修好,之后会继续更新的。今天我们先来看一下其他的硬件实现,Gitbub 上来自 raymondrc 的:
https://github.com/raymondrc/FPGA-SM3-HASH
项目主页给出了顶层的信号的波形和描述
信号包括时钟与复位信号,SM3 消息输入与结果输出信号。其中值得注意的是 is\_last\_word 信号,表示当前输入字为消息的最后一个字。last\_word\_byte\_in 信号表示最后一个字中的有效字节,这样可以支持输入字不对称的消息,这样就和我们上一篇中了解到的软件实现一样,支持输入消息字节对齐。
使用自定义的总线能够提高开发的速度,笔者的方案中使用了一个类-AXI Stream 协议,能够兼容标准协议,提高 IP 的可移植性。
项目的 rtl 目录如下
模块结构
在模块顶层下,例化了三个模块,模块的使能控制信号由运算控制状态机给出。
运算状态控制状态机以一个消息块为单位,控制状态的跳转,并给出消息填充使能,迭代使能以及结果输出使能。状态根据运算完成信号跳转
在顶层计算迭代次数,数据扩展和压缩模块共享迭代计数,以保持一致。
消息填充模块
消息填充模块分为直接输出和填充两种工作模式。
直接输出模式中将消息直接输出并对有效输入字节进行计数,直到最后一个消息块将结束时进入填充模式。
填充模式的核心在于处理多种因为最后一个字中有效字节数和当前总字节数不同而产生的一些特殊情况。实际上模块中的大量代码都在归类不同的情况进行处理。
可以分为三种情况
- 当前数据块中可以完成填 1 ,填 0 以及填消息块长度
- 当前数据块中可以完成填 1,但无法填消息块长度,只能再增加一个数据块填消息块长度,长度之前填 0
- 当前数据块中无法填1,填消息块长度,之能再增加一个数据块填1,消息块长度,长度之前填 0
再展开 1. 的情况,虽然在当前消息块中可以完成填充,但因为输入的最后一个 32 位字中的有效字节数不同,也需要根据有效字节数进行填充,有效字节数决定了填 1 的位置。
比如有效字节为 3 个字节,那么此时在最后一个字节,无效的字节,填 80 (最高位填 1 ,其他位填 0): {msg\_in[31:8],8'h80};
不同的填充情况,在消息最后一字到来时,根据最后一字有效字节数和当前消息块字节数判断。
简单分析下上图,case1 表示不需要填充的情况,此时消息块不是最后一个数据块。case2 表示需要填充,可以在当前块完成填充。case3 表示需要填充,但不能在当前块完成填充。
该实现的还有一个特色是使用了移位寄存器寄存了整个数据块 512 字节。在完成填充后一起输出,输入到后续的消息拓展模块也是 512 bit 位宽。这样做有好处,比如填充操作逻辑更简单;初始化后全部寄存器清 0 ,0 值不需要再移入等等。
但这样做的劣势在于,需要缓存整个块后一次输出,输出位宽 512 bit,过宽的数据位宽会影响时序;同时,在消息块填充期间,后续的消息拓展和迭代压缩模块处于空闲状态,会导致消息从输入到输出的延迟增大。
如果想要降低数据输入-结果输出延迟,需要在待填充数据输入的下一个时钟就输出数据,并更新填充状态,即如果数据在这里结束,实时的填充图样是什么样。这样做的优势是降低了延迟,减少填充模块的存储,缺点是放大了系统的动态功耗。
填充模块
流水输出的消息填充机制如下图:
消息拓展模块
消息拓展模块的输入是 512bit 的消息拓展输出,得益于前级模块的消息块缓存机制,后级的消息拓展可以得到简化。
消息拓展模块的核心是利用输入的填充后消息,通过置乱公式,每轮生成两个 32 位字。
红线代表消息拓展输入,绿色表示消息拓展输出,用于进行下一轮迭代
由于前级缓存了整个消息块,消息以块为单位输入,所以消息拓展模块可以在迭代期间每个时钟输出。如果消息以字为单位到达,那么在扩展时,需要考虑不同输入阶段,有不同的输出策略。比如在 w4 到达之前 , w0' = w0 ^ w4 , w0' 就无法输出。
此外模块中有状态机,来控制消息拓展模块的控制信号输入输出,包括消息拓展开始和结束信号。
消息压缩模块
压缩模块的核心在于对 8 个 32 位寄存器根据压缩函数进行迭代运算。
在 always 块中更新寄存器。
对应于下方的公式
使用组合逻辑生成迭代需要的输入变量
对应于下方的公式
消息压缩模块的状态控制的关键在于确认当前块的状态。
如果当前是消息的第一个块,那么使用协议规定的初始值更新寄存器。
如果当前是消息最后一个块,那么准备输出,将当前寄存器值与上一轮输出异或。
构建仿真
由于项目中没有使用特定厂商的 IP,具有良好的移植性,使用作者提供的 testbench,可以在任何你熟悉的集成开发环境和厂商软件中开始仿真。通过仿真也可以进一步地熟悉时序。
在 Zynq 7020 器件上综合
综合资源情况
综合时序可以综合到 126Mhz
结语
来自 Rui 的项目,代码结构很清晰,编码风格也很好,1500 行左右的代码很适合初学者学习。通过硬件实现,也能够进一步熟悉 SM3 的实现流程以及硬件实现思路。建议配合前一篇软件实现文章一起食用,效果更佳。
硬件项目其实很难用语言来描述,笔者感觉自己描述地也不是太 OK。如果你有兴趣,还是建议自己去看一篇代码和运行一次仿真。通过这篇文章我们也有了查看一个开源硬件项目的体验,硬件项目别人的轮子很难用起来,但不妨碍我们学习他人的实现思路,编码风格。