FPGA 三速自适应udp协议栈,支持8192字节的巨型帧分片重组和发送,使用IP为RAM与fifo。 支持ARP与ICMP。
先看架构里最骚的操作——分片重组模块。这货用Block RAM做了个环形缓冲区,每个分片进来先按offset排排坐。注意这个offset_counter数组,每个槽位存着分片位置和有效标志:
reg [15:0] offset_counter[0:7]; always @(posedge clk) begin if (fragment_valid) begin offset_counter[frag_id] <= {1'b1, frag_offset}; // 检测到连续分片就自动拼接 if (last_frag_offset + 1 == current_offset) reassembly_buffer <= {reassembly_buffer, current_payload}; end end这里用移位寄存器玩了个花活,当检测到分片连续时就自动拼接。不过要注意时序约束,特别是跨时钟域处理的时候得做好握手机制。
速度自适应这块更带劲,用MMCM动态调整时钟。看这个三速切换的状态机:
case(net_speed) 2'b00: mmcm_clkout <= 25; // 10M 2'b01: mmcm_clkout <= 125; // 100M 2'b10: mmcm_clkout <= 625; // 1G endcase // 切换时暂停3个时钟周期防毛刺 if (speed_changed) begin tx_enable <= 0; #3 tx_enable <= 1; end注意这里的延时不是随便写的,实测发现PHY芯片在速率切换时需要至少2.5个周期的稳定时间。调试时用ILA抓过波形,少一个周期都会丢包。
ARP缓存用CAM结构实现,比传统哈希表更合适FPGA。关键代码在这:
always @(posedge clk) begin // 并行比较所有表项 for (int i=0; i<16; i++) begin if (cam_table[i].ip == query_ip && cam_table[i].valid) hit_index <= i; end // 老化计数器每秒递减 if (timer_1s) cam_table[hit_index].age <= cam_table[hit_index].age - 1; end这种全并行查找比软件实现暴力多了,但要注意别把LUT给撑爆了。实际测试发现16个表项时综合出来刚好占满一个SLICE。
FPGA 三速自适应udp协议栈,支持8192字节的巨型帧分片重组和发送,使用IP为RAM与fifo。 支持ARP与ICMP。
最后说说巨型帧发送的坑。当payload超过8K时,得拆成多片并且要动态计算分片参数:
def calc_fragments(payload): frags = [] while len(payload) > 0: chunk = payload[:1480] # 留出IP头空间 frags.append(chunk) payload = payload[1480:] # 最后一个分片标记MF=0 flags = 0x2000 if len(payload) else 0x0000 return frags这个分片算法在Python测试环境里跑得挺欢,但移植到Verilog时发现个坑——分片偏移量必须以8字节为单位对齐。后来改成了右移3位的操作才符合RFC规范。
调试时最魔幻的是ICMP响应。本以为就是个echo小功能,结果发现不按规范填充payload会被某些路由器直接丢弃。现在回包时会原样带回发送数据:
assign icmp_reply = {icmp_header, orig_ip_header, timestamp, 8'h00, rx_payload}; // 必须补零到最小长度 if (total_len < 46) icmp_reply = {icmp_reply, {(46-total_len){8'h00}}};这手补零操作是从Wireshark抓包学来的,实测少了这个确实某些旧设备不认。
整个项目最费头发的还是时序收敛。特别是当1G速率下处理巨型帧时,组合逻辑路径必须控制在3ns以内。最后用register retiming大法把关键路径拆成了三级流水,这才勉强跑上625MHz。