news 2026/10/2 1:33:09

真值表到SOP/POS:数字逻辑设计核心化简与HDLBits实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
真值表到SOP/POS:数字逻辑设计核心化简与HDLBits实战指南

1. 内容整体设计与思路拆解

1.1 真值表、SOP与POS:数字电路设计的基本功为什么值得反复打磨

我先说一个很多人容易忽略的事实:在HDLBits刷题也好,在ECE241这类数字逻辑课程考试也好,真值表到逻辑表达式的转换能力,几乎是所有组合逻辑题目的底层地基。你后面学多路选择器、译码器、加法器、ALU,甚至有限状态机,往前翻到最后都会落到同一个问题——给你一个输入输出关系,你怎么用最少的逻辑门把它搭出来?

SOP和POS就是这个问题的两个标准答案形式。

SOP,全称Sum of Products,中文叫"与或式"或者"乘积之和"。它的结构是先用与门把输入变量组合成一个个乘积项(minterm),再用或门把这些项相加。POS相反,Product of Sums,中文叫"或与式"或者"和之乘积",先用或门组成和项(maxterm),再把这些和项用与门乘起来。两者描述的是同一个真值表,但在不同场景下会有完全不同的电路代价。

为什么SOP和POS能描述同一个真值表?因为它们本质上对应着两种看待表格的视角:SOP盯的是真值表中输出为1的那些行,POS盯的是输出为0的那些行。一个负责"凑出所有1",一个负责"排除所有0"。互补的视角,殊途同归的结果。

我在复习的时候发现,很多同学能背出SOP和POS的定义,但一到实际题目就懵。问题往往出在没有真正理解minterm和maxterm与真值表行号之间那种严格的对应关系。比如一个三变量函数,minterm m5对应的行是ABC=101,这个乘积项在SOP里写A·B'·C;而maxterm M5对应的也是ABC=101,但这个和项在POS里要写成A'+B+C'。变量取值的处理刚好是反的。这个细节就是考场上大量失分的重灾区。

1.2 ECE241为什么反复在真值表题型上做文章

本科数字逻辑课程里,ECE241算是比较有代表性的一门课。它为什么在作业、测验里一次又一次地考真值表到SOP/POS的转换?我个人的理解是,这门课的定位决定了它必须让学生从"看了真值表能写出表达式"过渡到"看着表达式能评估电路规模",再到"能在HDLBits上快速写出优雅的RTL代码"。

这种能力梯度是一层套一层的。你写不出正确的SOP,你的always块就会瞎写;你不会把POS化简成更紧凑的形式,你的综合面积和时序就吃亏。ECE241的考题往往用看似简单的三变量、四变量真值表做载体,实际上测试的是你对逻辑等价变换的熟练度。

再加上HDLBits本身就是一个在线的Verilog练习平台,它把EDAPlayground上能做的电路仿真练习搬到了浏览器里。你写完代码点Submit,立刻就知道对不对、综合出来什么规模。这种即时反馈特别适合刷真值表类型的题目——错了马上改,改了马上再测,比干坐在试卷前面手推K-map来得高效得多。

但这也就引出了一个关键点:HDLBits上你能靠"试错"过题,考试不行,岗位面试不行,真实的芯片设计流程更不行。所以在刷题之外,一定要把真值表、SOP、POS、卡诺图这条线彻底打通。

2. 核心细节解析与实操要点

2.1 SOP与POS的表达逻辑与符号系统

先说SOP。对于一个给定真值表,SOP的构造步骤是:找到所有输出为1的行,每一行写成一个乘积项,然后把所有乘积项用或门拼起来。

怎么把一行写成乘积项?规则是:该行某个输入变量为1,则这个变量用原变量(比如A)出现;该行为0,则用反变量(比如A')出现。比如一个三变量系统,真值表输出为1的一行是ABC=101,对应的乘积项就是A·B'·C。AB两位为0?不对,B=0所以用B',C=1用C。每一个乘积项都精确锁定了一行输入,所以叫minterm,最小项。

POS相反。找到所有输出为0的行,每一行写成一个和项,然后所有和项用与门拼起来。但这个和项的写法跟直觉是反的:该行某个变量为1,则这个变量在和中以反变量出现;该行为0,则以原变量出现。为什么?因为你要构造一个在那一行为0、其他行为1的逻辑值。比如三变量表里输出为0的一行是ABC=000,对应的maxterm是A+B+C。你在ABC=000时候代入,A+B+C=0,精确命中;在其他行代入,至少有一个变量是1,整个和为1。所有maxterm相与之后,只要有任意一个maxterm为0,整体就是0,完美复现了真值表。

这个"原变量反变量跟直觉相反"的规则,我建议你用一张速查表记死:

真值表行取值SOP minterm写原变量还是反变量POS maxterm写原变量还是反变量
变量=1原变量反变量
变量=0反变量原变量

很多新手做题的时候先写SOP写顺了,再转POS就容易延续SOP的习惯,写着写着就反了。我自己的办法是,每次写POS的时候强制自己在草稿纸上先标出每个变量的取反状态,再落笔。宁慢三分,不图快。

2.2 从真值表到HDL:HDLBits题型中的两种常见考法

在HDLBits的题库里,真值表类型的题目有好几种变体,但核心考法我归纳下来无非两大类。

第一类是纯组合逻辑题,直接给你一个真值表,让你用assign或者always块实现。比如给你一个三输入、一输出的真值表,要你写出对应的Verilog。这类题你直接把SOP或者POS抄进代码就行。但HDLBits考察的不只是结果的正确性,它还会看你的代码能不能综合成合理的电路。如果你写了一大串括号套括号的表达式,哪怕功能正确,也会让综合器头疼,更会让看代码的人头疼。

第二类是给一个带don't care项的真值表和一段partial代码,让你补全逻辑。这个难度会高一些。don't care项的意思是:这个输入组合在实际系统中永远不会出现,或者出现了也无所谓输出是什么。在真值表里通常标成x或者d。处理don't care项的核心价值在于:你可以在化简时把它们当成1或者0来用,从而减小电路的规模。

比如一个四变量函数,输出为1的行有0011、0111、1011,don't care项是0000。如果你把0000当成1来合并,卡诺图上的圈可以画得更大,化简出更短的乘积项。HDLBits的评测只看功能,don't care你选0还是选1只要最终输出不冲突就行,所以宁可把它们积极地纳入化简,做成更小的电路。

顺带提醒一个高频失分点:HDLBits和很多评测平台默认你在always块里没写到的分支会生成锁存器,也就是latch。真值表考的就是清晰的分支结构,如果你用always@(*)写case或者if,却漏掉了某些输入组合,综合时就会推断出意想不到的锁存器,功能直接炸掉。后面第四节我会专门讲这个坑和排查方法。

3. 实操过程与核心环节实现

3.1 一个完整案例:从真值表到SOP再到Verilog

我们直接上菜,用一道类似ECE241风格的题目走一遍完整流程。

假设真值表如下(三输入A、B、C,单输出Y):

ABCY
0000
0011
0101
0110
1001
1011
1101
1110

第一步,写出SOP。挑出输出为1的行,分别是001、010、100、101、110,对应的minterm为m1、m2、m4、m5、m6。展开写:

Y = A'B'C + A'BC' + AB'C' + AB'C + ABC'

这是一坨挺长的表达式。画个卡诺图化简一下。

三变量卡诺图按格雷码排列,行(变量A)从上到下是0、1,列(变量B C)从左到右是00、01、11、10。把上面五行的1填进对应格子。规整下来你会发现可以合并出一个圈:C'那列(BC=00和10)在A=0和A=1上都有1,合并出B'C';还要关注A'B那行?不对,A'B对应BC=00和01里A=0的行,其中01是0、00是0,所以没有。仔细圈圈:

  • 第一组:BC=00这列在A=0和A=1都有1,所以消去A,得到B'C'。
  • 第二组:C=1且A=1这两格(101和111?不,是101对应C=1,111是0,所以不是列组)。实际能圈的是AB'C'(100)和AB'C(101)合并,消去C,得到AB'。
  • 第三组:A=0、C=0这两格(000是0、010是1、100是1、110是1……这里要小心),100已经被用了,010可以用A'B C'单独做?还有101和100合并项AB',以及001和101合并项B'C,010和110合并项BC'。等等,110输出是1,列一下:010和110这两格可以合并,消去B?不对,010的意思是A=0 B=1 C=0,110是A=1 B=1 C=0,它们横跨A=0和A=1两行且同一列BC=10,所以消去A,得到B C'?BC=10意味着B=1 C=0,乘积项B·C'。

那最简SOP就是Y = B'C' + AB' + BC'。别急着写代码,我们验证一下和原SOP等价:B'C'覆盖了100和001?不对,B'C'对应BC=00,行覆盖000和100,但000输出是0,所以B'C'作为逻辑项在A=0、BC=00时会给Y=1,这跟真值表冲突。猜错了,回头查:真值表中000输出确实是0,但B'C'这个乘积项在A=0B=0C=0这个输入下确实是1,说明B'C'不能单独出现,除非有其他项约束它。但逻辑或里没有约束,B'C'=1就会让Y=1,所以这个化简错了。

问题出在哪儿?我在合并卡诺图时把000的0格也圈进去了。对,三变量卡诺图里,B'C'列对应BC=00,包含AB'C'(100行,Y=1)和A'B'C'(000行,Y=0)。0和1不能圈在一起。所以B'C'这个圈是非法的。正确圈法应该是:

  • AB'C'(100)和AB'C(101)纵向合并,消去C,得AB'。
  • A'B'C(001)和AB'C(101)横跨两行同列BC=01,合并消去A,得B'C。
  • A'BC'(010)和ABC'(110)横跨两行同列BC=10,合并消去A,得BC'。

Y = AB' + B'C + BC'。

这个例子特别典型,它说明的是我在HDLBits做题时反复踩过的一个坑:卡诺图化简必须严格按照相邻1格来圈,绝对不能为了图省事把0格混进去。很多人眼睛看着像就圈了,结果化简出来的表达式看似短,实际功能根本对不上。HDLBits的Submit按钮会立刻告诉你错,但在考场上没人提醒你。

有了最简SOP,Verilog就是抄作业:

module top_module( input A, input B, input C, output Y ); assign Y = (A & ~B) | (~B & C) | (B & ~C); // AB' + B'C + BC' endmodule

这里我用assign连续赋值,组合逻辑清晰直观。注意原变量和反变量的写法:在Verilog里反变量用~表示,乘积对应与运算&,和对应或运算|。

3.2 同一张表,POS形式怎么写

再用POS走一遍同一张真值表,验证一致性。

POS要找输出为0的行:000、011、111。三行对应三个maxterm。

  • 000行:三个变量都是0,写成和项时每个变量用原变量,即A+B+C。
  • 011行:A=0、B=1、C=1,和项里A用原变量A,B、C用反变量B'、C',即A+B'+C'。
  • 111行:三个变量都是1,和项里都用反变量,即A'+B'+C'。

POS形式为:

Y = (A+B+C) · (A+B'+C') · (A'+B'+C')

我们可以用代数方法或者用卡诺图圈0验证这个POS是否等价于前面的SOP。圈0法在卡诺图上找0格,直接圈出反函数的SOP,然后取反就得到POS。这里不展开所有代数细节,但可以拿其中一个组合验证。比如输入ABC=001,SOP三项:AB'=0、B'C=1、BC'=0,Y=1。再看POS三项:A+B+C=1、A+B'+C'=1?B'=1所以这一项为1、A'+B'+C'=1,三项相与为1。两者一致。这么验几个点,功能就对上了。

那问题来了:既然SOP和POS都能实现同一个真值表,做题时到底选哪个?

判断标准取决于哪边的门代价小。最直观的方法是把表达式里的字母数量数一遍:SOP是AB'+B'C+BC',总共6个字母,对应的物理电路是3个两输入与门加1个三输入或门,中间有3根连线;POS是(A+B+C)(A+B'+C')(A'+B'+C'),总共9个字母,电路是3个三输入或门加1个三输入与门。这题明显SOP更省。但如果真值表里0格很少、1格很多,POS反而更占优。考试和项目里我一直建议先把两种形式都写出来,快速比较一下规模,再决定用哪个。

3.3 HDLBits实践:从真值表到case语句的完整代码

HDLBits有时候题目给的模板不是让你写assign,而是要补全一个always块。这时case语句就要用得规范。

同样用上面的真值表,写成always加case:

module top_module( input A, input B, input C, output reg Y ); always @(*) begin case ({A, B, C}) 3'b000: Y = 1'b0; 3'b001: Y = 1'b1; 3'b010: Y = 1'b1; 3'b011: Y = 1'b0; 3'b100: Y = 1'b1; 3'b101: Y = 1'b1; 3'b110: Y = 1'b1; 3'b111: Y = 1'b0; default: Y = 1'b0; // 可选,但推荐写上 endcase end endmodule

这种写法本质上就是把真值表逐行翻译成代码,逻辑最直白,最不容易出错。它的优点是:跟手头的真值表一一对应,review代码的时候一眼就能看出有没有漏行。缺点是:如果表格很大(比如四变量16行、五变量32行),代码会非常冗长,而且综合器不一定能化简到最优。所以考试时如果只是要求功能正确,case直接硬写就行;如果题目明确要求最小电路或者要求你用SOP/POS形式输出,那就得用布尔表达式。

我特别要强调default这一项。很多教程说组合逻辑的case可以不写default,因为所有4个分支都有明确赋值,不会有latch。这话在理论上没错,但在工程实践里,我见过太多人因为删掉default、之后又增删分支导致漏分支、综合出latch的惨案。写default只多一行代码,却能直接杜绝这类隐患。在HDLBits上交作业,不会因为default报错;在真实项目代码评审里,default也是被推荐的好习惯。

3.4 卡诺图化简的完整手推过程

回到卡诺图,我把化简方法再系统地讲一遍,因为它是最容易出细节错误的地方。

卡诺图的核心原则是相邻性:逻辑相邻的格子可以合并,所谓逻辑相邻就是两个minterm只在一位变量上有差异。格雷码这个排列方式保证了横向和纵向的相邻格子天然只差一个变量,从而可以直接消去该变量。

三变量卡诺图有8格,四变量有16格。圈法规则有四个层次:

  1. 单个1格,无法合并,直接保留全部三个/四个变量。
  2. 两个相邻1格合并,消去1个变量。
  3. 四个1格形成一个2×2的矩形,可以消去2个变量。
  4. 八个1格形成一个2×4或类似的矩形,可以消去3个变量。

关键禁忌就一条:圈的格子数必须是2的幂(1、2、4、8...),而且必须是矩形,不能圈成L形、T形或者斜对角。

我画卡诺图时习惯用铅笔在草稿纸上把每个1格里对应的minterm编号标出来,然后扫行扫列。三变量表简单,四变量表就很容易乱。我的技巧是:先找孤立的1,处理不了就圈2格,圈完2格再尝试4格,从大到小来。自底向上虽然严谨,但效率低;自顶向下更能发现大的合并机会。实际做题时我喜欢先在脑子里按行、按列扫,把能形成4格的先圈出来,再看剩下的孤格怎么处理。

刚才我们圈出来的结果是:

  • 100和101纵向相邻,消去C,得到AB'
  • 001和101横向相邻,消去A,得到B'C
  • 010和110横向相邻,消去A,得到BC'

所以Y = AB' + B'C + BC'。圈过的格子可以被重复使用,这是卡诺图的另一个关键规则。上面的101就同时出现在前两个圈里。很多人卡在这一步——觉得一个1格只能圈一次,白白错过了化简机会。记住,minterm可以被多个圈共享,因为布尔代数里有X+X=X,重复项不影响结果。

4. 常见问题与排查技巧实录

4.1 锁存器真值表场景:为什么不完整赋值会生成latch

网络热词里出现了"锁存器真值表",这个点跟HDLBits里很多always块题目强相关。当你用always@(*)写组合逻辑时,如果某个输入组合下没有给输出赋值,综合工具默认保持前一个值,这就是锁存器行为。锁存器本身不是不能用,但组合逻辑里出现意外的latch通常是设计错误,会带来时序问题和毛刺。

举个例子:

always @(*) begin if (sel) Y = A; end

这个代码里sel为0时,Y没有被赋值。综合器推断出一个在sel=0时保持原值的锁存器。如果你只是想要一个二选一多路器,这显然是错的,必须补上else分支:

always @(*) begin if (sel) Y = A; else Y = B; end

排查方法很简单:看综合报告或者看生成的原理图里有没有DLATCH元件。写代码的时候养成习惯,case语句一律写全分支加default,if语句把else补满。真值表题目本来就给了你所有输入组合,每一个输出值都清清楚楚,完全没理由漏分支。

4.2 化简表达式与真值表对不上?用逐项验证法找bug

遇到化简后的表达式跟真值表对不上,先别急着怀疑卡诺图画错。我推荐一个系统化的排查思路:

第一,逐行代入验证。把真值表的每一行输入代入化简后的表达式,看看输出跟表格是否一致。这个过程看着笨,但定位特别准。比如下面这个反例:

Y = A'B'C + A'BC' + AB'C' + AB'C + ABC'

如果我们错把第三项写成AB'C' + AB'C合并成AB'(这步没错),又把第二项和第五项合并成BC'(这步也没错),但第一项A'B'C和第四项AB'C合并成B'C(也对)。合并本身没问题,但如果你把B'C这一项漏掉了,在ABC=001和101两行就会输出错误。逐行代入很快就能锁定是哪一行错了,然后反推是哪一项被漏掉。

第二,检查卡诺图的圈有没有混入0格。这一点前面讲过了,是化简环节中最常见的错误源。你可以在卡诺图上给每个圈用一种颜色的笔描边,画完之后对照原真值表检查每个圈覆盖的所有格子是否都是1。

第三,检查表达式在电路实现时有没有丢反相器。HDLBits的代码里,~写错、括号括错、优先级搞错,都会导致实际逻辑跟你算的SOP不一致。我建议先用最原始、最长的那版SOP先提交一次,确认真值表关系正确了,再逐步化简提交,用平台的反馈来验证化简等价性。这个方法在HDLBits这种即时评测环境里尤其好用。

4.3 HDLBits刷题中的三个时间黑洞

第一个时间黑洞是纠结某一道题的最优解。HDLBits的题目名称里有时候会暗示你"使用最少门电路",但实际上它只验证功能正确性。如果你已经跑通了功能,就没必要非得在卡诺图上和编译器、综合器较劲。真实工程里,让综合器去优化比人肉硬化简更高效。当然,考试题除外,考试明确要求化简你就要化简。

第二个时间黑洞是忽略前置知识,直接从难题开始刷。真值表专题是HDLBits里的基础模块,但不少人跳过了前面的门电路、多路器练习,直接挑战卡诺图题,结果连assign写标准了没有都不确定。我的建议是循序渐进,基础题10分钟能过的直接过,别恋战。

第三个时间黑洞是写代码不仿真直接提交。虽然HDLBits会告诉你结果对不对,但如果你能在本地用iverilog快速跑一遍testbench,你会发现定位bug的速度提升很多倍。尤其是当你的表达式变量一多、括号一深,肉眼很难看出优先级错误。

4.4 考试与面试中的真值表陷阱

可能是我被问过太多次,我把考试和面试里关于真值表最常见的陷阱整理成一张速查表,适合考前30分钟扫一遍:

陷阱类型典型错误应对方法
minterm/maxterm下标错位认为m5是ABC=101,M5也是ABC=101但写作形式混淆永远记住M5对应同一行,但变量取值要取反后写进和项
卡诺图行列顺序行列变量用错,导致相邻关系错误固定行变量和列变量的排列,写代码/画图前先标格子编号
don't care处理不当当0或当1没有统一;随意改变导致输出不满足约束把don't care当成化简的"万能充",但最终逐行验证don't care不影响已定输出
括号与优先级表达式里&和混用没加括号,逻辑错误
多输出真值表只化简了其中一个输出,其他输出照抄原表每个输出独立做卡诺图,输出之间不要互相借用项除非有公共子式

还有一个考点我很想单独提醒:竞争冒险。即使你的逻辑表达式完全等价于真值表,实际电路中因为门延迟的存在,某些输入变化瞬间可能出现毛刺。判断方法是看相邻minterm之间是否只有一个变量变化,而表达式里没有覆盖到那个"过渡项"。比如Y = AB' + AC,在B=1、C=1、A从0变到1时,第一项从0变1、第二项从?代入B=1 C=1,AB'=0、AC从0变1,理论输出跟着A走,但两个与门因为延迟不同,会在切换瞬间同时为0,出现一个短暂的毛刺。解决方法是加冗余项BC。这个点不会直接影响HDLBits提交,但ECE241考试和面试常考,能说出"加冗余项消除竞争冒险"非常加分。

实操总结

HDLBits上真值表到SOP/POS的练习,我刷了两遍。第一遍只是照着题目写assign,写完看结果对就过了。第二遍我强迫自己每一道题都用最原始的方法从真值表写出SOP、POS,再手动卡诺图化简,最后才写代码。刷第二遍的收获远大于第一遍。

我个人在实际操作中最深的体会是:不要急着化简。先把真值表原封不动翻译成冗长的SOP或者case语句,让电路功能先正确,然后再做逻辑优化。很多初学者一边写代码一边试图化简,结果化简错了也不知道错在哪。分阶段推进看起来慢,实际上是最快的路。

另外一个小技巧:HDLbits上所有涉及真值表的题目,我都习惯先列出minterm编号表,比如把输出为1的行号写成一串m1、m2、m4、m5、m6,再做卡诺图。这样即使卡诺图画歪了,回头看编号表也能很快重建表达式。这个方法对四变量以上的表格尤其管用,手动圈完图后再拿编号表逐项检查,基本不会漏项。

真值表、SOP、POS这条知识链,说到底就是数字设计里的"阅读理解"能力。你读得懂输入输出关系,写得对表达式,画得准卡诺图,后面学时序逻辑、有限状态机都会顺很多。反过来,如果这个环节靠蒙靠猜混过去了,后面的坑会接二连三。把基础打牢,HDLBits刷题也好,ECE241考试也好,都不会是难事。

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

工程师之路:编程技能、项目实战与面试求职的成长指南

说句实在话,刚工作那几年我特别不想谈“工程师之路”这种题目。总觉得它太宏大,自己那点经历根本不配写。后来陆陆续续带过十几个新人,也回答了不少想转行朋友的私信,才慢慢发现:太多人缺的并不是聪明和努力&#xff0…

作者头像 李华
网站建设 2026/10/2 1:32:51

柔软线防断传感器选型指南:光纤对射与电容式方案实战

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

作者头像 李华
网站建设 2026/10/2 1:32:17

SpringBoot体质测试系统:从Excel沉睡数据到运动处方

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

作者头像 李华
网站建设 2026/10/2 1:32:11

梯形图:工业控制中人机协同效率最高的编程语言

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

作者头像 李华
网站建设 2026/10/2 1:29:21

Java中使用iText生成图表PDF:矢量绘图实战指南

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

作者头像 李华
网站建设 2026/10/2 1:29:02

STM32CubeMX安装配置与工程实战指南

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

作者头像 李华