news 2026/9/8 10:57:32

FPGA实战:8b10b编解码原理与Verilog实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实战:8b10b编解码原理与Verilog实现

简介:一套基于FPGA的8B/10B编解码Verilog实现工程,定位面向FPGA学习者与高速串行接口设计人员,旨在帮助理解直流平衡编码原理,以及编解码器在真实硬件中的落地过程。压缩包共包含137个文件,整体约3.88MB,其中以Verilog源码、Quartus II工程文件、sof下载文件、仿真脚本及各类报告为主,从工程配置到验证结果一应俱全,解压后直接打开qpf工程即可运行整个设计流程。目前已有3745人学习下载。设计内部按默认编码、差异度计算、编码校正、并串转换和显示五个模块划分,完整覆盖编码、解码与差异度校验环节;配套ModelSim功能仿真和Quartus II逻辑综合,最终在Cyclone IV E系列的EP4CE6F17C8芯片上完成适配与下载测试。对希望快速掌握8B/10B编解码实现、参照完整FPGA工程结构,或从事高速串行传输验证的读者,这套资料提供了可直接复用和二次开发的起点。

1. 为什么搞8b10b:串行链路上的“平衡艺术”

做FPGA的同学迟早会碰到8b10b,不管你是调PCIE、USB3.0、千兆以太网,还是SATA、JESD204B,底层物理编码子层里都跑着8b10b或者它的小改款(比如64b/66b、128b/130b)。说是“迟早”,是因为只要链路是串行的、速度一上来,DC平衡问题就躲不掉。

先说一个最原始的问题:为什么不能直接把8bit数据怼到串行线上?因为接收端需要从数据流里恢复时钟,而恢复时钟依赖信号边沿的密度。如果你的数据是连续的0或者连续的1,接收端过一段时间就“失去参考”了,CDR(时钟数据恢复)直接罢工。再一个问题是直流漂移,长时间发送同一电平会让线路的直流分量偏移,长期运行误码率就上来了。

8b10b的核心思想就两个字:平衡。8bit数据映射成10bit码字,这10个bit里0和1的数量差异被严格控制,运行差异度(Running Disparity,简称RD)被限制在正负1之间。同时保证码字里连续的0或1不超过5个,这样接收端就有足够多的跳变沿来恢复时钟。

再往深一层看,8b10b的设计还有一个很多新手容易忽略的点:它不是一个简单的查表映射,而是分成了3b/4b和5b/6b两级子映射。为什么这么设计?因为如果直接做8bit到10bit的完整映射表,表规模是2^10=1024项,虽然Verilog里写case也写得下,但逻辑综合和时序优化会麻烦不少。分两级之后,每一级的逻辑都很小,组合逻辑路径短,时序更好收敛,这也是当年IBM那帮工程师的高明之处。

说实话,我个人做FPGA项目多年,真正对8b10b“开窍”是在自己用Verilog写过一遍编解码,并且调通了高速收发器(比如Xilinx的GTP/GTX)之后。光看协议文档,你永远没法真正理解RD状态为什么会对、那几根控制信号为什么要同步、K码为什么是干这个用的。这篇文章就把我手写8b10b编解码模块的完整思路、代码结构、仿真排错过程捋一遍,给要做相关项目的朋友一个可以“抄作业”的参考。

2. 8b10b编解码核心机制拆解

2.1 编码表与RD状态机的关系

8b10b编码的输入是一个8bit数据(D码)加上一个控制位(K码使能),输出是10bit码字。编码表本身不是随意的映射,它做了两个关键约束:

  • 每个码字中1的个数和0的个数的差值只能是0、+2或-2(极少数特殊码字除外)
  • 根据当前RD状态(RD+或RD-)来选择发送码字的极性,保证码流平均DC电平为0

RD状态机是怎么工作的?简单说:当前RD为正表示最近发出去的码字里1比0多,下一步倾向于发负极性的码字来拉平衡;RD为负则相反。具体计算规则如下:

  • 如果码字里1比0多2个,且当前RD=+,则下个码字的RD变为-
  • 如果码字里1比0多2个,且当前RD=-,则下个码字的RD变为+
  • 如果码字里0和1一样多,RD保持不变

这个规则用Verilog实现就是几行always块的事,但前提是你得把编码表的极性分类搞清楚。

这里我补充一张常用于快速查找的编码表结构,方便大家对照代码理解。表格不是完整128项数据,只是把几个典型字符列出来,关键看规律。

输入数据数据类型RD=-时输出RD=+时输出
D0.0 (0x00)D码100111 0100011000 1011
D1.0 (0x01)D码011101 0100100010 1011
D21.5 (0xB5)D码101010 1010101010 1010
K28.5 (0xBC)K码001111 1010110000 0101

看到D21.5这行的规律了吗?它本身就是一个平衡码(0和1各5个),所以不管RD是正是负,输出都一样。这也是10bit码字里少数能保持完全平衡的字符,K28.5则因为自带连续的1和0,成为接收端做码组对齐的黄金选择。

2.2 控制字符(K码)到底有什么用

K码在8b10b体系里不是用来传数据的,它承载的是“命令”或“标记”。协议里一共有12个K码,但真正在物理层用得最多的是K28.1、K28.5和K28.7。

  • K28.5:常用作comma(逗号)字符,用于接收端做字节对齐。它的10bit码字里有一个唯一的“0011111”或“1100000”模式,连续的1或0不会在任何D码组合中出现
  • K28.1和K28.7:常用于一些协议特定的控制序列,比如PCIE里的SKP(skip)字符会用K28.0/K28.3等

做FPGA实现的时候,K码使能信号(通常叫K或KCHAR)必须在发送端和接收端有统一约定。比如你的链路层数据是32bit还是64bit的,K码字节在整个通道里怎么分布,这些属于协议适配层的工作,物理编码子层只管按输入编码就行。

经验之谈:新手写编解码时,经常忽略K码使能与数据对齐的问题。如果你的数据位宽是8bit,那还好办,一个字节对应一个使能。但如果用了32bit的链路层接口(一个时钟周期4个字节),那么你既要判断4个字节各自的K使能,还要注意字节顺序对不对。曾经我调一个多通道的项目,发送端byte0放K码,接收端却按byte3的位置去取comma,结果对齐逻辑始终不触发,排查了整整一天才发现是字节通道映射的问题。

2.3 RD(Running Disparity)究竟怎么算

RD的计算是8b10b实现里最容易写错的地方。多数资料会告诉你“编码后统计1和0的个数差”,听起来很简单,但实际编码过程是分两步走的:

  • 先对低5bit做5b/6b编码,根据当前RD算出中间RD
  • 再对高3bit做3b/4b编码,根据中间RD算出最终RD

也就是说,RD在整个编码过程中会更新两次。这个细节非常关键,如果你的代码先编码完整10bit再统一更新一次RD,那么对于某些输入序列,结果可能碰巧对,但遇到边界情况就会出错。

具体的RD更新判断,其实用不着真的去数1的个数。更高效的做法是直接分类:6bit子码(输出为6bit)里有“000111”这种三位相同的结构,4bit子码(输出为4bit)里也有类似特征,这些特定的模式会直接决定RD翻转方向。分类判断加几个组合逻辑,比统计1个数少占用不少LUT。

我在实际工程里习惯把RD状态做成一个单独的模块,输出给编码器用,同时回传给上层状态机,好处是调试的时候可以单独看这个信号的变化轨迹,定位问题的时候效率极高。

3. 编码器Verilog实现:架构设计、代码结构与关键点

3.1 顶层模块如何划分

我建议把8b10b编码做成独立的模块,接口尽量标准化,方便后续复用到不同项目。顶层端口如下:

module enc_8b10b ( input wire clk, input wire rst_n, input wire [7:0] data_in, input wire k_in, // 1: K码使能, 0: D码 output reg [9:0] data_out, output reg rd_out // 当前码字发送后的RD状态 );

为什么要把RD输出也拉出来?因为接收端的解码也需要RD,有时候调试系统级回环测试时,你需要把发射端的RD和接收端的RD做交叉比对。如果你的模块把这个状态锁在内部不让外面看,仿真调试就没法做了。

除了顶层,我通常会拆三个子模块:5b/6b编码器、3b/4b编码器、RD更新逻辑。这样拆的优势是每个子模块的逻辑都很纯粹,综合时时序路径短,后期如果要改协议参数(比如做64b/66b或自定义变种)也只需要换其中一小块。

有一个设计习惯值得推荐:把编码表用case语句罗列清楚,但不要把这些case放在组合逻辑块里,而是用casez匹配“XX”这种不关心的位。因为8b10b里有几个码字的映射是等价的(同一个输入在RD+和RD-下会输出两个不同码字),写casez可以让综合更清楚哪些分支可以合并优化。

3.2 编码器源码核心片段解析

这里给出最核心的5b/6b编码逻辑。

// 5b/6b encoder reg [5:0] coded_6b; always @(*) begin casez ({k_in, data_in[4:0]}) // D码 6'b0_00000: coded_6b = (rd_in) ? 6'b100111 : 6'b011000; 6'b0_00001: coded_6b = (rd_in) ? 6'b011101 : 6'b100010; 6'b0_00010: coded_6b = (rd_in) ? 6'b101101 : 6'b010010; // ... 中间省略 6'b0_11111: coded_6b = (rd_in) ? 6'b101011 : 6'b010100; // K码的5b部分 6'b1_11100: coded_6b = (rd_in) ? 6'b001111 : 6'b110000; // K28 default: coded_6b = 6'b000000; endcase end

注意这里的rd_in是编码前的当前RD,而不是编码完成后的状态。在组合逻辑里用rd_in查表,然后在时序逻辑里根据coded_6b的极性更新rd_mid(中间RD),再把这个rd_mid送给3b/4b编码器。这一步顺序要是搞反了,整个码流就全乱了。

K码的5b部分一定要用独立分支,因为同一个输入D28(0x1C)在作为D码和K码时是完全不同的编码。比如5'b11100作为D码输出是01010这个序列的一部分,但作为K28时输出是001111或110000,两者差别巨大。

下面这段是3b/4b编码和RD更新:

// 3b/4b encoder reg [3:0] coded_4b; always @(*) begin casez ({k_in, data_in[7:5]}) 4'b0_000: coded_4b = (rd_mid) ? 6'b1011 : 6'b0100; // D.x.0 4'b0_001: coded_4b = (rd_mid) ? 6'b1001 : 6'b1001; // D.x.1 4'b0_010: coded_4b = (rd_mid) ? 6'b0101 : 6'b0101; // D.x.2 // ... 4'b1_010: coded_4b = (rd_mid) ? 6'b0101 : 6'b1010; // K.x.2 4'b1_111: coded_4b = (rd_mid) ? 6'b1110 : 6'b0001; // K.x.7 default: coded_4b = 4'b0000; endcase end // RD update always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rd_out <= 1'b0; // 复位后约定RD为负,也就是rd_in=0表示RD- end else begin case ({coded_6b, coded_4b}) 12'b000111_????: rd_out <= 1'b1; // 6b不平衡,取反极性 12'b111000_????: rd_out <= 1'b1; 12'b????_0011: rd_out <= 1'b1; // 4b部分导致的不平衡 12'b????_1100: rd_out <= 1'b1; default: rd_out <= rd_in; endcase end end

最后组合起来,data_out = {coded_4b, coded_6b},位序是高位在前,低6位放5b/6b的结果,高4位放3b/4b的结果。这个位序是PCIE、千兆以太网等常用协议的标准做法,写代码前务必确认你的协议文档里的bit顺序定义,否则收发两端对不上会非常痛苦。

3.3 编码器的时序与复位策略

编码器全是纯组合逻辑加一级寄存器输出,所以时序收敛很容易。真正要小心的是复位策略和RD初始状态。

  • 复位后RD必须明确约定为RD-(就是rd_in=0),两端一致
  • 发送端和接收端如果复位不完全同步,在系统刚上电的那段时间RD状态可能短暂不一致,但这不影响后续稳定,因为8b10b的RD自我纠错能力很强,只要一个平衡码过去,RD就重新对齐了

还有一个小技巧:如果你要在FPGA里做多通道并行编码(比如4通道PCIE),每个通道的RD状态必须是独立维护的,不能用同一个全局RD。因为通道间初始相位可能不同,共用一个RD会让对端解码彻底错乱。这一点从协议文档里看不出来,但做过多通道集成的人都知道。

4. 解码器与时钟恢复对齐:工程真正复杂的部分

4.1 解码器输入与对齐逻辑

解码器的输入是连续的10bit码字流,但接收端如何知道哪里是一个码字的边界?答案是在数据流里搜索comma模式。物理层通常是先把串行数据一个bit一个bit地收进来,然后在bit流里找K28.5(comma)的特征序列“0011111”或“1100000”,找到后就把这个位置当作字节边界,后续数据都按10bit为粒度切分。

这个逻辑用Verilog实现时一般有两种方案:

  1. 滑动移位寄存器:把收到的bit不断移入一个10bit寄存器,每bit都检查是否匹配comma模式,匹配后输出一个对齐脉冲,同时产生load信号给后续的10bit打拍逻辑
  2. 状态机方案:维护一个bit计数器,如果当前窗内不匹配comma,则滑动1bit继续找,直到找到为止

方案1比较直观,代码就是移位寄存器加一个比较器,做高速率高带宽时可能资源稍多;方案2节省逻辑,但状态机控制要小心。我个人建议在GTP/GTX这类高速收发器里直接用IP核内部的comma检测,但在教学或者在低速自定义链路上,手写对齐逻辑能让你彻底理解底层原理。

对齐完成之后,数据就以10bit为单位进入解码主体。解码主体做的事刚好和编码器相反:根据码字查表找到对应的8bit数据和K码使能,同时用上一个码字的RD状态来验证当前码字的极性是否符合预期,并更新RD。

4.2 解码器源码核心片段解析

解码器的查表和编码器几乎对称,唯一的区别是解码器的输入是10bit码字,需要同时匹配RD+和RD-两种情况。

always @(*) begin casez (code_in) 10'b011000_0100: begin data_out = 8'h00; k_out = 1'b0; end 10'b100111_1011: begin data_out = 8'h00; k_out = 1'b0; end 10'b100010_0100: begin data_out = 8'h01; k_out = 1'b0; end 10'b011101_1011: begin data_out = 8'h01; k_out = 1'b0; end // ... 10'b110000_0101: begin data_out = 8'hBC; k_out = 1'b1; end 10'b001111_1010: begin data_out = 8'hBC; k_out = 1'b1; end default: begin data_out = 8'h00; k_out = 1'b0; err_out = 1'b1; // 无效码字 end endcase end

注意default分支里的err_out信号,这是解码器里非常有价值的附属产出。通过统计err_out的脉冲个数,你能直接判断链路质量。如果误码率偏高,err_out会持续有脉冲;如果只是偶尔一个,可能只是噪声干扰。在调试系统时,用这个信号配合逻辑分析仪(比如ILA)观察,远比盯着波形数bit高效。

解码器的RD校验逻辑和编码器类似,也必须两级更新。这里有一个常见误区:只校验码字是否在合法编码表内,而忽略了RD极性。有些非法场景下,码字本身在表里存在,但极性顺序不对(比如连续出现两个RD+极性的码字),这属于非法码字序列,单纯查表是发现不了的。工业级实现通常会同时检查“码字合法”和“RD跳变合法”两个条件。

4.3 对齐后数据的字节序与位宽处理

真实工程里很少做8bit的编解码接口,因为串行链路的吞吐量太大了。比如千兆以太网是1.25Gbps的线速率,如果数据通路只有8bit宽,那需要250MHz的主频才能吞吐,FPGA上时序压力很大。常用的做法是内部数据通路做32bit甚至64bit宽,一个周期处理4个或8个字节。

位宽扩展后,对齐逻辑也要跟着变。如果你一次收到32bit的数据,comma可能出现在其中任意一个字节位置,所以搜索逻辑要同时检查4个位置,并且根据找到的位置决定输出数据怎么“挪”到对齐后的格式上。

看起来复杂,但原理就是一个桶形移位器配合mux选通。Xilinx和Altera的收发器IP核在RX端都自带这个功能,用FPGA开发时大部分情况可以直接用IP,不用自己写,但你要明白原理,否则调IP参数时面对一堆选项会很懵。

5. 仿真验证与上板调试:从“看起来对”到“跑得稳”

5.1 Testbench怎么写才最有效

8b10b模块的testbench,最理想的方式是回环测试:发送端先把数据编码,再把编码结果直接送给解码器,然后比对解码后的数据和原始数据是否一致。这个回环测试能在仿真阶段把编解码器的大部分逻辑错误暴露出来。

回环测试里要特别注意几个测试向量的选定:

  • 遍历所有256个D码,每个D码分别在RD-和RD+两种状态下编码,确保查表全覆盖
  • 遍历常用K码加随机数据混合,验证K码使能通道
  • 构造连续的平衡码输入(比如D21.5),测试RD不翻转时模块是否稳定
  • 构造连续的不平衡码输入(比如D0.0),测试RD翻转是否按预期交替
// 遍历所有D码的测试片段 integer i; initial begin for (i = 0; i < 256; i = i + 1) begin data_in = i[7:0]; k_in = 1'b0; #10; // 检查解码输出是否等于data_in if (dec_data_out !== i[7:0]) begin $display("ERROR at data=%02x", i); end end // 测试结束 $finish; end

Vivado自带的XSim或者ModelSim/QuestaSim都支持跑这个脚本。重点是波形覆盖率测试,一定要确保遍历完所有RD状态,否则某些输入在特定RD极性下的映射是查不到错误路径的。

5.2 上板调试实战:常见问题清单

调试经验告诉我们,这类编解码模块上板后80%的问题不是出在逻辑功能上,而是出在接口时序和复位顺序上。下面这几个故障,我在项目里至少各踩过一次:

故障现象根本原因解决方案
接收端始终无法对齐发送端K码没发出来,或者comma被其他逻辑修改先用ILA观测发送端发出的原始bit流,确认K28.5序列正确
能对齐但数据全是错的解码器表和编码器表的bit顺序不一致检查MSB/LSB定义,输出10bit是否高低位搞反
偶发误码,跑一段时间才出错复位后两端RD初始状态不一致统一复位策略,确认收发的RD初始化完全相同
速率上去以后时序不过编码表用组合逻辑堆太大,路径延迟超标拆分两级编码,中间加流水寄存器

第二个故障特别有意思。之前做一个自定义高速链路,编码器输出的码字是按照协议文档的bit序(高4位在前,低6位在后),但解码器表里我是按MATLAB脚本自动生成的,脚本里没注意位序,把高4位和低6位写反了。仿真回环时因为回环路径同一个模块,编码端和解码端的查表都“一致地”错了,所以仿真全通过——上板后收发两端一对接,全部乱码。后来我在testbench里故意加入了一个“反序注入”的测试用例,才把这个坑堵上。

还有一个调试技巧:如果链路能长时间跑通但偶尔误码,建议在发送端周期性插入K28.5,这样接收端可以定期做对齐复核。一旦数据错位,K28.5能让对齐逻辑在几个字节内快速恢复。这也是PCIE里SKP字符存在的部分原因。

5.3 结合IGT收发器(GTP/GTX)的联调注意事项

如果你是用FPGA的GTP/GTX高速收发器来做物理层,8b10b编解码一般可以直接使用收发器内部的编码器(用配置选项开启)。这样做的性能当然比自己写的Verilog模块要好,毕竟是硬核实现。但有时候你还是需要自己写编解码,比如:使用非标准的编码变种、需要对编码过程做插桩观测级调试、或者收发器的编码器不支持你需要的K码组合。

自写编解码模块接入收发器时,最需要注意的是时钟域和位宽的匹配。收发器的内部接口(FPGA侧接口)通常已经是并行数据加字节时钟,你拿到的是已经对齐好的并行数据,编码模块工作在这个字节时钟域。所以你的编码器主时钟直接取字节时钟即可,但要对做CDC(跨时钟域)的地方保持警觉。收发器复位时序和编码器复位时序如果要联动,最好用同一次复位。

6. 项目复盘:我踩过的坑和给新手的建议

做这个8b10b项目前后历时不到两周,但收获比预想中大很多。复盘下来有几个点值得分享:

第一,文档协议一定要自己推导一遍。8b10b的编码表网上到处都是,但如果你直接抄表然后一头扎进代码,你会发现自己根本不理解为什么某些码字有等价替代项,为什么某些码字要强制跳过。给自己半天时间,把编码表的生成逻辑理一遍,比多改十次代码都强。我最后用的是从标准里抄的编码表,但每一步都有手写推导的备查笔记。

第二,仿真用例要“故意做错”。一个高效的testbench不仅要测正确路径,还要故意注入非法码字、反序码字、错误的RD极性,看看异常路径有没有被检测到。你可以在解码器里加一个错误注入端口,用testbench把某个码字强制翻转几bit,然后检查err_out响应。这个特性在联调时特别有用,能帮你快速给出“链路误码来自物理层还是协议层”的判断依据。

第三,做高速串行项目,第一版就该考虑FPGA收发器的IP使用方式。自写8b10b作为学习完全值得,但如果商用项目时间紧,直接用收发器内建的8b10b编码器,把精力放在上层的链路调试上,会更高效。不是说自写没用,而是要有意识地区分“学习目的”和“工程交付”两种模式。

第四,代码要有记录波形导出的习惯。如果你想分析一个跑了很久的偶发误码,不要只靠仿真。在FPGA里把收发器的数据、RD状态、err信号接到ILA上,触发条件设为err==1,等误码发生的那一瞬抓下波形,基本一眼就能看出问题。没有抓住现场波形之前,不要轻易猜原因——串行链路的问题经常和你最初怀疑的地方完全无关。

最后再分享一个小技巧:如果你在做多通道的编解码,把各个通道的RD状态做成可以软件读取的寄存器,上板调试时通过串口或以太网远程观测每个通道的RD跳变频率。如果某个通道的RD跳变明显异常,那这个通道的物理链路大概率有质量问题。这个方法看着土,但在我做8通道JESD204B的时候,帮我在十分钟内定位到了一个PCB布线引起的通道间串扰问题。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 10:56:20

从JSON到SQL:机器学习数据准备全流程解析

在机器学习项目中&#xff0c;拿到一份可以直接训练的干净数据集&#xff0c;往往比调模型参数更花时间。很多真实数据不会以 CSV 表格的形式摆在面前&#xff0c;而是分散在 API 返回的 JSON 字符串里&#xff0c;或者躺在 MySQL、PostgreSQL、SQLite 的某张业务表中。今天这篇…

作者头像 李华
网站建设 2026/9/8 10:54:56

Web化ETL开发与调度:基于WebSpoon和自研调度系统的实践

过去这几年&#xff0c;我一直负责企业数据集成平台的建设。从最初用命令行脚本、SQL定时跑批&#xff0c;到后来引入Kettle做ETL&#xff0c;再到现在推进Web化、平台化&#xff0c;踩了不少坑&#xff0c;也积累了一些心得。最近我们团队基于WebSpoon9.4和自研的XKG调度系统&…

作者头像 李华
网站建设 2026/9/8 10:54:30

SSM商铺租赁管理系统实战:从框架整合到毕设答辩全攻略

做毕设的同学如果看到这个标题&#xff0c;大概率已经开始头痛了。“SSM商铺租赁管理系统”这个题目&#xff0c;在毕业设计里属于非常典型的Java Web方向&#xff0c;看起来不难&#xff0c;但真要做得漂亮、答得顺、跑得稳&#xff0c;还是有不少门道。这篇文章我就把自己折腾…

作者头像 李华
网站建设 2026/9/8 10:53:51

RabbitMQ镜像文件全攻略:从部署到死信配置一次讲清

简介&#xff1a;这是一份RabbitMQ镜像及配套示例工程资源包&#xff0c;面向需要在本地或内网环境快速搭建消息队列服务的开发者、运维人员&#xff0c;特别适合刚接触RabbitMQ、希望绕过镜像拉取障碍的Spring Boot使用者。压缩包约102.71MB&#xff0c;共8个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/8 10:52:59

《小Pip的导盲犬之梦》动画制作技术解析与情感表达艺术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 10:51:11

Java实战:从零开发一个命令行背单词软件,覆盖面向对象与集合框架

去年这个时候&#xff0c;我还在为 Java 的语法细节焦虑——lambda 表达式到底怎么用、HashMap 和 Hashtable 有什么区别、异常处理为什么要分 checked 和 unchecked。这些东西单个拿出来都能写几百字笔记&#xff0c;可真要独立做一个完整项目&#xff0c;脑子里反而是空的。这…

作者头像 李华