简介:这是一份面向数字电路与密码学学习者的 SHA256 算法 Verilog 实现源码,适合希望从硬件层面理解哈希函数内部逻辑的工程师、学生与研究者。资源包共 18 个文件,约 13KB,以 10 个 Verilog 源文件为核心,辅以 Rust 工程文件、Makefile、Python 脚本与说明文档,覆盖数据填充、初始化变量、消息块处理、压缩函数及最终哈希值计算等关键模块,便于对照理解算法在硬件中的模块划分与并行优化思路。目前已有 146 人学习下载。通过阅读整套源码,读者可掌握 SHA256 各步骤如何转化为 Verilog 模块,理解消息调度、主循环与资源消耗之间的权衡,并借助测试脚本与工程配置快速验证实现结果,是兼顾算法原理与硬件编程实践的完整学习素材。
1. 从一次时序违例说起:这套 SHA256 的 Verilog 源码到底能干什么
很多人第一次在 FPGA 上做哈希加速,都会卡在同一个地方:算法逻辑写对了,仿真波形也漂亮,可一旦上板跑高频,时序就崩。我当年做某个安全模块时,SHA256 的压缩函数迭代路径太长,关键路径直接顶到 8ns 以上,时钟一拉到 150MHz 就出玄学错误。后来翻了不少开源实现,才慢慢摸清 SHA256 硬件化的门道——它难的不是算法本身,而是怎么把 64 轮迭代、消息扩展和加法链拆成能收敛的流水线。
这套 Verilog 实现 SHA256 的整套源码,正好覆盖了从消息填充、消息调度到压缩函数、最终摘要输出的完整链路,适合两类人:一类是想学硬件描述语言怎么落地密码算法的学生和初学者,另一类是需要在 FPGA 上做哈希加速、又不想从零造轮子的工程师。它把 SHA256 的 64 轮常量、初始向量、消息扩展逻辑都固化在 RTL 里,读一遍就能看清一个密码学算法是怎么变成时序电路的。
2. 架构拆解:SHA256 在 RTL 里被切成了哪几块
2.1 顶层模块的接口与数据流
先看顶层模块长什么样。一个合格的 SHA256 硬件实现,接口通常不会太花哨,核心就是时钟、复位、数据输入、启动信号、数据输出和完成标志。下面这段是我从源码里提炼出的典型顶层端口定义,参数化程度决定了它能不能复用到不同项目里。
module sha256_top #( parameter DATA_WIDTH = 32, // 每个消息字的位宽 parameter BLOCK_SIZE = 512 // SHA256 以 512bit 为一个消息块 )( input wire clk, input wire rst_n, // 低电平复位 input wire start, // 启动一次哈希计算 input wire [BLOCK_SIZE-1:0] msg_block, // 当前消息块 output reg [255:0] digest, // 最终 256bit 摘要 output reg done // 计算完成脉冲 );这里有几个参数值得注意。DATA_WIDTH固定为 32,因为 SHA256 的所有运算都建立在 32 位字上;BLOCK_SIZE为 512,对应标准里一个消息分组的长度。start信号一般要求单周期脉冲,用来触发内部状态机从空闲态进入计算态。done在最后一轮压缩完成后拉高一个周期,外部模块靠它来锁存digest。
数据流是这样的:外部先把 512 位消息块送进来,拉高start,内部状态机开始跑 64 轮压缩。每一轮会用到消息扩展生成的W[t]和K[t]常量,经过多级加法、移位和逻辑运算后更新 8 个工作变量a到h。64 轮跑完,这 8 个变量和初始向量相加,得到这一块的中间哈希,如果是最后一块就输出为最终摘要。
2.2 消息扩展模块:把 16 个字扩成 64 个
SHA256 的消息扩展是整个算法里最容易被低估的部分。输入是 16 个 32 位字,输出是 64 个 32 位字,扩展公式涉及右旋、右移和异或。硬件实现时,常见做法是用一个 64 深度的寄存器组,前 16 个直接来自消息块,后面 48 个按公式递推生成。
// 消息扩展:W[0..15] 来自消息块,W[16..63] 递推生成 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < 64; i = i + 1) w_reg[i] <= 32'd0; end else if (load_msg) begin for (i = 0; i < 16; i = i + 1) w_reg[i] <= msg_block[511 - 32*i -: 32]; // 大端切分 end else if (ext_en) begin for (i = 16; i < 64; i = i + 1) w_reg[i] <= w_reg[i-16] + sigma0(w_reg[i-15]) + w_reg[i-7] + sigma1(w_reg[i-2]); end end这段代码里有两个关键点。第一,消息块的切分必须按大端顺序,msg_block[511 - 32*i -: 32]这种写法保证第一个字落在最高 32 位,这是 SHA256 标准规定的字节序,搞反了结果全错。第二,sigma0和sigma1是两个组合逻辑函数,分别对应右旋 7、18、3 和右旋 17、19、10 的异或组合。如果把它们写成函数或宏,综合工具通常能自动优化成移位加异或的结构。
扩展的时序安排也有讲究。如果每个周期只算一个W[i],那 48 个扩展字要花 48 个周期,加上 64 轮压缩,一个消息块就要 112 个周期。想快的话可以并行展开,但面积会上去。源码里用的是折中方案:扩展和压缩流水起来,压缩第 t 轮时扩展第 t+16 个消息字,这样总周期数能压到 64 左右。
2.3 压缩函数:64 轮迭代的关键路径在哪
压缩函数是时序收敛的主战场。每一轮要做的事情包括:计算T1 = h + Sigma1(e) + Ch(e,f,g) + K[t] + W[t],计算T2 = Sigma0(a) + Maj(a,b,c),然后更新h=g, g=f, f=e, e=d+T1, d=c, c=b, b=a, a=T1+T2。把这些全塞进一个时钟周期,关键路径会非常长。
// 单轮压缩的核心运算 assign Sigma1_e = {e[5:0], e[31:6]} ^ {e[17:0], e[31:18]} ^ {e[21:0], e[31:22]}; assign Ch_efg = (e & f) ^ (~e & g); assign T1 = h + Sigma1_e + Ch_efg + K[t] + W[t]; assign Sigma0_a = {a[1:0], a[31:2]} ^ {a[12:0], a[31:13]} ^ {a[21:0], a[31:22]}; assign Maj_abc = (a & b) ^ (a & c) ^ (b & c); assign T2 = Sigma0_a + Maj_abc;上面这段是纯组合逻辑,如果直接在一个 always 块里更新a到h,综合出来的关键路径大概包括 5 到 6 级加法器加上若干异或门。在低端 FPGA 上,这通常意味着最高时钟只能跑到 50 到 80MHz。想提速的话,常见做法是在加法链中间插寄存器,把一轮拆成两拍或三拍,代价是控制逻辑变复杂,需要额外的轮计数和状态标记。
源码里保留了单周期一轮的结构,好处是逻辑清晰、方便对照标准文档验证。如果你要上高频,可以自己加流水寄存器,但记得同步调整轮计数器的比较逻辑,否则会出现少算一轮或多算一轮的翻车情况。
3. 跑起来:仿真、综合与上板验证的完整步骤
3.1 用测试向量验证功能正确性
拿到源码后第一件事不是急着综合,而是跑仿真。SHA256 标准里有现成的测试向量,比如空字符串的摘要、"abc"的摘要,这些都可以拿来当基准。测试平台一般这样写:
initial begin // 复位 rst_n = 0; #20 rst_n = 1; // 送入 "abc" 填充后的消息块 msg_block = 512'h6162638000000000000000000000000000000000000000000000000000000000 | 512'h0000000000000000000000000000000000000000000000000000000000000018; start = 1; #10 start = 0; // 等待 done wait (done == 1); $display("digest = %h", digest); // 期望值:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad end这里要注意消息填充的细节。"abc"只有 3 个字节,填充规则是先补一个0x80,然后补零到 448 位,最后 64 位放原始消息长度(24 位,即0x18)。上面这段把填充后的 512 位块直接拼出来,省去了在测试平台里写填充逻辑的麻烦。跑仿真时重点看digest是否和期望值一致,如果不一致,先检查消息切分的字节序,再检查K常量表有没有抄错。
3.2 综合约束与时序收敛
仿真过了之后,下一步是综合。不管你是用哪家的工具,约束文件里至少要有时钟定义和输入输出延迟。下面是一个典型的 SDC 约束片段:
create_clock -name clk -period 10.0 [get_ports clk] set_input_delay -clock clk 2.0 [get_ports msg_block] set_input_delay -clock clk 2.0 [get_ports start] set_output_delay -clock clk 2.0 [get_ports digest] set_output_delay -clock clk 2.0 [get_ports done]period 10.0对应 100MHz,如果综合报告里关键路径超过 10ns,就要考虑降频或者加流水。常见做法是先跑一遍综合看时序报告,找到最差路径,如果落在压缩函数的加法链上,就在T1和T2的计算中间插一级寄存器。插的时候注意保持a到h的更新顺序,别把T1和T2的依赖关系搞乱。
3.3 上板调试:怎么确认硬件跑出来的摘要是对的
上板之后,最直接的验证方法是通过串口或其它接口把digest读出来,和软件算的对比。如果板子上有 LED,也可以先把digest的低 8 位打到 LED 上,至少确认电路在动。更稳妥的做法是在 FPGA 里加一个简单的 UART 发送模块,把 256 位摘要分 8 次发出去,上位机用 Python 的hashlib算一遍对比。
import hashlib data = b"abc" print(hashlib.sha256(data).hexdigest()) # ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad如果硬件读回来的值和 Python 算的不一样,先别怀疑算法,优先检查三件事:消息填充是不是在硬件里也做对了、字节序有没有反、done信号的采样时机是不是在稳定之后。我见过好几次因为done和digest同时变化,外部用同一个时钟沿采样导致读到旧值的情况,加一级寄存器打拍就好了。
4. 避坑记录:调试 SHA256 硬件实现时最容易翻车的几个点
4.1 现象:仿真结果对,上板结果随机错
原因通常是跨时钟域或复位释放时机不对。start信号如果来自另一个时钟域,没有做同步处理,可能在亚稳态窗口被采样到,导致状态机进入错误分支。解决办法是在start进入 SHA256 模块前加两级触发器同步,或者干脆用同一个时钟域驱动。
4.2 现象:摘要前 128 位对,后 128 位错
这种一半对一半错的情况,多半是消息扩展的后半段出了问题。W[16]到W[63]的递推依赖前面的值,如果寄存器组初始化不完整,或者扩展使能信号的时序和压缩轮次没对齐,就会出现部分轮次用了错误的消息字。检查ext_en和轮计数器的关系,确保第 t 轮压缩时W[t]已经稳定。
4.3 现象:综合报大量锁存器
组合逻辑里用了不完整的if-else或case,综合工具推断出锁存器。SHA256 的K常量表如果用case语句实现,记得加default分支;消息扩展的循环里如果条件不完整,也会出锁存器。改成时序逻辑或者补全所有分支就能解决。
4.4 现象:时序报告显示关键路径在K常量选择上
如果K[t]是用大的case语句写的,综合工具可能综合成多级选择器,反而成了关键路径。常见做法是把K常量存到一个小 ROM 里,用轮计数器当地址去查,这样路径更短也更省面积。
4.5 现象:多消息块连续计算时第二块结果错
SHA256 支持对长消息分块计算,第二块的初始向量应该是第一块的输出。如果模块内部把初始向量写死成标准常量,没有在块与块之间更新,就会出错。检查h0到h7的加载逻辑,确保非首块时从上一轮的digest加载。
5. 二次开发:把这套源码改成你要的样子
5.1 加流水线提频率的具体改法
如果你需要更高的吞吐率,最直接的办法是把 64 轮压缩拆成两级流水:第一级算T1和T2的组合逻辑,第二级更新a到h。这样关键路径大概能砍掉一半,时钟频率有机会翻倍。改的时候注意轮计数器要跟着流水节拍走,W[t]和K[t]的索引也要相应延迟一拍。
// 第一级:计算 T1/T2 always @(posedge clk) begin T1_reg <= h + Sigma1(e) + Ch(e,f,g) + K[t] + W[t]; T2_reg <= Sigma0(a) + Maj(a,b,c); end // 第二级:更新工作变量 always @(posedge clk) begin if (round_en) begin h <= g; g <= f; f <= e; e <= d + T1_reg; d <= c; c <= b; b <= a; a <= T1_reg + T2_reg; end end这样改完,每个消息块的周期数从 64 变成 65 左右(多一拍流水延迟),但时钟频率可能从 80MHz 提到 150MHz,整体吞吐率反而是提升的。代价是面积增加,因为多了一组寄存器存中间结果。
5.2 支持多消息块连续哈希的改造
标准 SHA256 对长消息是分块迭代的,每块 512 位。如果要做成一个能连续处理任意长度消息的模块,需要在顶层加一个消息缓冲和填充逻辑。常见做法是外部先把消息按 512 位对齐送进来,最后一块由硬件自动补0x80和长度字段。改造时重点处理两个地方:一是块计数器,用来判断是不是最后一块;二是初始向量的加载,首块用标准常量,后续块用上一块的输出。
5.3 验证改造结果的一个笨办法但有效
每次改完,别急着上板,先在仿真里跑三组测试向量:空字符串、"abc"、以及一个刚好跨块边界的消息(比如 56 字节,填充后会变成两个块)。三组都过了,再综合上板。我一般还会在测试平台里加一个自动对比,把硬件输出和hashlib的结果逐位比较,不一致就打印出错位置。这个习惯帮我省了很多来回烧板子的时间。
从那以后我每次改密码学硬件模块,都强制走一遍「仿真三向量 → 综合看时序 → 上板读摘要」的流程,不再凭感觉跳步。希望这套 SHA256 的 Verilog 源码和上面的拆解,能帮到你顺利跑通自己的哈希加速模块。
本文还有配套的精品资源,点击获取