news 2026/9/7 13:11:02

Verilog核心解析:assign、always组合逻辑与时序逻辑对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Verilog核心解析:assign、always组合逻辑与时序逻辑对比

这次我们直接切入 Verilog 里最容易被绕晕的三个知识点:连续赋值assign、过程块always @(*)组合逻辑,以及always @(posedge clk)时序逻辑。很多初学者能看懂单个例子,但一旦把三种写法混在一个模块里,就会遇到“阻塞赋值和非阻塞赋值到底怎么选”“为什么组合逻辑综合出来一个 latch”“为什么同一个信号在多个 always 里赋值会报错”这类问题。这篇文章不会只讲语法,会把三种赋值方式放到同一个对比框架里,明确告诉你在什么场景用哪一种、仿真行为和综合行为分别是什么、出了问题从哪几个方向排查。

先说结论:assign适合描述“数据流”级别的组合逻辑,always @(*)适合描述带if/case分支的“过程”组合逻辑,always @(posedge clk)是描述寄存器、计数器和状态机时绕不开的时序逻辑。三种写法都能综合成真实电路,但它们对信号类型的要求、赋值符号的选择、敏感列表的写法和综合出的硬件结构完全不同。如果你能把这三个写法的边界分清楚,Verilog 编码里很大一部分警告和仿真错误都可以提前避免。

下面会从核心概念速览开始,然后分别拆解assignalways @(*)always @(posedge clk)的语法规则和代码实例,最后给出 Testbench 仿真、常见问题排摸和工程规范建议。整个过程会用到可以在本地运行的小例子,你可以直接复制到 Icarus Verilog 或 Verilator 中跑一遍,对照波形检查自己的理解。

1. 核心概念速览

特性assign连续赋值always @(*)组合逻辑always @(posedge clk)时序逻辑
关键字assignalways @(*)always @(posedge clk)
左侧信号类型wire/ 其他net类型必须为reg类型变量必须为reg类型变量
常用赋值符号==(阻塞赋值)<=(非阻塞赋值)
触发条件右侧表达式任意信号变化敏感列表中任意信号变化时钟上升沿(或复位边沿)
典型硬件结果组合逻辑门、数据流连线组合逻辑门、优先编码器、译码器触发器、寄存器、计数器、状态机
常见错误左侧用了reg或存在多驱动条件分支不完整导致 latch使用阻塞赋值导致仿真竞争
可综合性可综合可综合可综合

这张表是全文的最小记忆单位。后续所有代码和排错思路,都是围绕表中这几个差异展开的。如果你已经能理解“always块里赋值给reg,但reg不一定是寄存器”这句话,那么组合逻辑和时序逻辑的区别你已经掌握了一大半。

2. 适用场景与使用边界

assign连续赋值的核心是“一旦右侧变化,左侧立刻更新”。它适合描述一组输入到一组输出的纯组合映射,例如数据选择器、译码器、加法器的进位输出、总线信号的拼接和截取。这类逻辑用表达式就能写清楚,不涉及顺序判断,也没有中间变量。

always @(*)过程块适合描述需要分支判断的组合逻辑。比如多路选择器用case、优先编码器用if...else if、仲裁器用多级优先级判断。由于always块内部可以声明局部变量、使用for循环、临时变量,它比assign更容易表达“先比较,再选择”的过程化逻辑。

always @(posedge clk)时序逻辑则用于描述需要记忆状态的硬件:计数器、状态机、移位寄存器、同步 FIFO 的读写指针、流水线寄存器等。它的本质是“在时钟边沿采样输入,输出保持到下一个时钟边沿”。使用时必须注意复位方式、非阻塞赋值和敏感列表完整性。

边界也很明确:同一个信号不能同时被assignalways驱动,也不能被两个always块同时驱动。组合逻辑块中不要使用非阻塞赋值,时序逻辑块中尽量不要使用阻塞赋值。只描述纯组合逻辑时,assignalways @(*)在综合结果上可能等价,但在代码可读性和维护性上各有侧重。

3. 环境准备与前置条件

本文所有代码都可以在本地仿真环境里运行,不依赖特定 FPGA 板卡。如果你还没有 Verilog 仿真环境,推荐安装 Icarus Verilog(iverilog)配合 GTKWave 查看波形,这是一套轻量、免费、适合学习的最小组合。

在 Ubuntu/Debian 上,可以执行:

sudo apt update sudo apt install iverilog gtkwave

Windows 环境可以通过 Icarus Verilog 官方安装包安装,也可以在 WSL 中执行同样的命令。macOS 下可以通过 Homebrew 安装:

brew install icarus-verilog brew install --cask gtkwave

安装完成后,在终端运行:

iverilog -V

如果能看到iverilog版本信息,说明环境可用。Vivado、Quartus、ModelSim/Questa 也可以运行后面的代码,但建议先在小仿真环境里把 RTL 行为验证清楚,再放到工程里综合实现,这样排错成本最低。

目录结构建议按下面的方式组织:

verilog_assign_always/ ├── rtl/ │ ├── half_adder.v │ ├── mux4_1.v │ └── counter.v └── sim/ ├── tb_half_adder.v ├── tb_mux4_1.v └── tb_counter.v

这样做的目的是把设计文件和测试文件分开,编译时使用明确路径,避免大量文件堆在一起后分不清驱动关系。后面所有仿真命令都会基于这种目录结构写。

4. 连续赋值assign组合逻辑

4.1 基本语法与规则

assign的基本语法如下:

assign 左侧信号 = 右侧表达式;

左侧信号必须是wireoutput wire类型的网络变量,右侧可以是wirereg、常量、拼接或函数调用结果。assign是并行语句,不放在initialalways里,写在模块体内任意位置都能生效。

它的行为可以理解为“持续连线”:右侧任意变量发生变化,左侧立即重新计算。例如:

assign y = (a & b) | c;

只要abc任一变化,y就会被更新。这种特性非常适合表达组合逻辑的数据流关系。

4.2 用assign实现半加器

下面是一个最经典的半加器例子:

module half_adder( input wire a, input wire b, output wire sum, output wire carry ); assign sum = a ^ b; assign carry = a & b; endmodule

这里sumcarry都是wire类型,使用assign直接实现异或和与操作。综合后得到的就是一个异或门和一个与门,没有任何存储元件。

也可以用一条assign同时输出多个信号,例如使用拼接:

assign {carry, sum} = a + b;

这种写法在算术逻辑中更紧凑,但要保证左侧位宽和右侧位宽一致,否则会产生符号扩展或截断问题。

4.3assign的常见错误

第一个常见错误是左侧使用了regassign只能驱动网络类型,不能直接对reg做连续赋值。如果确实需要在一个模块里既做组合逻辑又做输出,可以让output端口是wire,然后通过assign驱动。

第二个常见错误是多个驱动源。同一个wire在模块内被两个assign赋值,或者一个assign加一个always同时赋值,都会造成多驱动。综合工具会报错或产生不确定的连接关系。正确的做法是让每个信号只被一条语句驱动。

第三个常见错误是忽略位宽。assign右侧表达式宽度和左侧不一致时,Verilog 会按左侧宽度自动截断或扩展,这种隐式行为容易掩盖 bug。建议在写高位宽总线时显式使用[N-1:0]定义,并通过仿真断言检查数值范围。

4.4 什么时候优先用assign

如果一个逻辑可以用一条表达式描述清楚,并且没有分支判断,优先用assign。典型场景包括:地址译码、低位宽算术运算、总线拼接、常量偏移、状态转移条件中的组合标志位。assign的优点是结构清晰、并行语义直观、不容易产生 latch。

但如果逻辑里有大量ifcase,强行用条件表达式嵌套会变得很难读,这时就应该改用always @(*)

5. 过程块always @(*)组合逻辑

5.1 基本语法与敏感列表

always过程块可以描述组合逻辑,也可以描述时序逻辑,差别主要体现在敏感列表和赋值符号上。组合逻辑的标准写法是:

always @(*) begin // 组合逻辑处理 end

@(*)在 Verilog-2001 中表示自动敏感列表,编译器会自动把块内读取的所有信号加入敏感列表。它比手写always @(a or b or sel)更安全,因为手写容易漏信号,漏掉后仿真行为会与实际硬件不一致。

块内被赋值的信号必须声明为reg类型,这里的reg只是过程赋值的载体,并不代表综合后是寄存器。always @(*)内必须使用阻塞赋值=,这样变量更新才是“立即生效”,符合组合逻辑的数据流语义。

5.2 用always @(*)实现 4 选 1 多路选择器

下面是一个完整例子:

module mux4_1( input wire [1:0] sel, input wire [3:0] in, output reg y ); always @(*) begin case (sel) 2'd0: y = in[0]; 2'd1: y = in[1]; 2'd2: y = in[2]; 2'd3: y = in[3]; default: y = 1'bx; endcase end endmodule

这里y声明为output reg,因为在always块内赋值。case语句完整覆盖了sel的四种状态,所以不会产生 latch。如果不写default,在sel2'bxx2'bzz时,综合工具会认为某些输入组合下y不需要保持旧值,从而可能推断出锁存器。

5.3 条件不完整导致 latch

always @(*)里最典型的坑是:某个分支下没有给输出赋值。例如:

always @(*) begin if (enable) begin y = a; end end

enable = 0时,y没有任何语句赋值,硬件为了保持原值,只能引入锁存器。大多数设计里 latch 不是想要的结果,综合报告会出现Latch inferred警告。

解决方法是给输出赋默认值:

always @(*) begin y = 1'b0; if (enable) begin y = a; end end

也可以写完整的else分支。对于case,则补default。这条规则不仅适用于多路选择器,也适用于状态机的组合输出、读写控制信号生成等所有组合逻辑。

5.4assignalways @(*)的区别

从综合结果看,同样一段逻辑用assignalways @(*)写,可能综合出相同或接近的电路。区别在于表达方式:assign是显式表达式,适合简单映射;always @(*)是过程描述,适合分支判断和需要临时变量的逻辑。

有一点要特别注意:两个方式不能同时驱动同一个信号。比如一个模块里既写assign y = a & b;,又在always @(*)里给y赋值,那么y会被多个驱动源驱动,仿真和综合都会出问题。

6. 时序逻辑always @(posedge clk)

6.1 基本语法与复位方式

时序逻辑的标准模板是:

always @(posedge clk) begin // 同步时序逻辑 end

这个写法表示:只有时钟clk的上升沿到达时,块内语句才执行。输出在时钟边沿采样输入,并在下一个时钟边沿前保持不变,综合后对应的是触发器。

如果使用异步复位,通常写作:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin q <= 1'b0; end else begin q <= d; end end

敏感列表里同时包含时钟上升沿和复位下降沿。复位信号rst_n低电平有效时,无论时钟是否到来,复位都能立即将q清零。同步复位则写作:

always @(posedge clk) begin if (!rst_n) begin q <= 1'b0; end else begin q <= d; end end

同步复位的复位动作只在时钟沿发生,不容易受复位毛刺影响。实际工程里建议全模块统一使用同一种复位方式,不要在一个模块里混用同步复位和异步复位,否则时序约束和复位验证都会变复杂。

6.2 为什么时序逻辑必须用非阻塞赋值

时序逻辑块内部推荐使用非阻塞赋值<=。非阻塞赋值的语义是:右侧表达式在进入 always 块时先采样旧值,左侧更新在时间步结束时统一完成。用这个模板写出的行为符合真实寄存器“边沿采样,随后更新”的特性。

反过来,如果在时序逻辑里用阻塞赋值=,同一时刻多个 always 块之间的信号依赖会变得非常敏感,仿真时很容易出现竞态。例如一个模块里有两个 always 块,一个把a赋给b,另一个把b赋给c,使用=时,模拟器对块执行顺序的差异会导致c的值不确定。

所以工程规范是:组合逻辑用=,时序逻辑用<=,同一个 always 块内不要混用两种赋值方式。

6.3 8 位计数器示例

下面用一个带异步复位的 8 位计数器演示时序逻辑:

module counter( input wire clk, input wire rst_n, output reg [7:0] cnt ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 8'd0; end else begin cnt <= cnt + 1'b1; end end endmodule

这个计数器每来一个时钟上升沿就加一。复位信号rst_n为低电平时,cnt异步清零。使用<=的好处是,所有寄存器在当前时钟沿先读取旧值,再统一更新,因此cnt <= cnt + 1'b1不会出现“一步加两次”的问题。

6.4 移位寄存器与状态机思维

时序逻辑还可以描述移位寄存器:

module shift_reg( input wire clk, input wire rst_n, input wire din, output reg [3:0] q ); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin q <= 4'b0; end else begin q <= {q[2:0], din}; end end endmodule

这个例子展示了“并行赋值”的时序行为:q[3]拿到q[2]的旧值,q[2]拿到q[1]的旧值,q[0]拿到din。因为所有右侧都在时钟沿前采样旧值,所以不会出现竞争。

写状态机时也是一样的逻辑:状态寄存器和输出寄存器都用<=,状态转移条件用组合逻辑生成,最终在时钟沿统一更新。只要记住“时序块只描述存储,组合块只描述判断”,状态机的代码结构就不会乱。

7. 三种赋值方式对比与选择

7.1 选择指南

需求推荐写法示例
简单逻辑表达式assignassign y = a & b;
带条件判断的组合逻辑always @(*)+=caseif...else
寄存器、计数器、状态机always @(posedge clk)+<=计数器、移位寄存器

如果是纯组合逻辑,可以用assign,也可以用always @(*)。选择依据是代码可读性。能一行写完的用assign;需要多个分支、优先级判断或临时变量的,用always @(*)

如果是时序逻辑,不要用assign,也不要用always @(*)描述存储行为。时序逻辑必须出现在以时钟边沿为敏感事件的always块里,并且使用非阻塞赋值。

7.2 阻塞赋值与非阻塞赋值的核心区别

阻塞赋值=在赋值语句执行时立即更新左侧变量,后面的语句会立刻看到新值。非阻塞赋值<=则是先把右侧表达式求值保存,等当前时间步结束时统一更新左侧变量,后面的语句在执行时仍然看到旧值。

这个区别在建模时非常关键。组合逻辑要求信号在某个时间点立即反映输入变化,所以用=;寄存器逻辑要求在时钟沿统一采样,所以用<=。如果你发现仿真波形中的寄存器输出总是比预期晚一拍,或者出现不确定的 X 态,优先检查是否在时序块里误用了=

7.3always块不要混用赋值类型

一个常见但不规范的做法是在同一个always块里先做组合逻辑又做时序逻辑,同时混用=<=。这样的代码仿真也许能跑通,但综合后的结果很难预测,debug 成本很高。

推荐的做法是:把组合逻辑和时序逻辑拆成两个块。组合部分负责生成中间信号,时序部分只负责采样和寄存。这样每个always块的语义单一,后续加约束、查波形、写断言都会轻松很多。

8. Testbench 仿真与效果验证

8.1 仿真环境基本流程

使用 Icarus Verilog 的运行流程是:先编译所有设计文件和 testbench 文件,生成仿真可执行文件,然后运行仿真,最后用 GTKWave 查看 VCD 波形。

以计数器counter.v为例,先写一个 testbench:

`timescale 1ns/1ps module tb_counter; reg clk; reg rst_n; wire [7:0] cnt; counter u_counter( .clk (clk), .rst_n (rst_n), .cnt (cnt) ); initial begin clk = 1'b0; rst_n = 1'b0; #20 rst_n = 1'b1; #500 $finish; end always #10 clk = ~clk; initial begin $dumpfile("tb_counter.vcd"); $dumpvars(0, tb_counter); end initial begin $monitor("time=%0t rst_n=%b cnt=%0d", $time, rst_n, cnt); end endmodule

编译和仿真命令:

iverilog -o sim_counter rtl/counter.v sim/tb_counter.v vvp sim_counter

执行后会在终端看到每隔 10ns 打印一次的计数器值,如果cnt从 0 开始按 1 递增,说明时序逻辑行为正确。GTKWave 打开 VCD 文件:

gtkwave tb_counter.vcd

在波形窗口里可以检查复位释放后cnt是否在每个时钟上升沿加一,这是判断时序逻辑是否正确的直接方式。

8.2 组合逻辑 testbench 示例

对于半加器,可以写一个自动对比的 testbench:

`timescale 1ns/1ps module tb_half_adder; reg a, b; wire sum, carry; half_adder u_half_adder( .a(a), .b(b), .sum(sum), .carry(carry) ); initial begin $monitor("a=%b b=%b sum=%b carry=%b", a, b, sum, carry); a = 0; b = 0; #10; a = 0; b = 1; #10; a = 1; b = 0; #10; a = 1; b = 1; #10; $finish; end endmodule

如果仿真结果符合真值表,说明assign写出的组合逻辑没有问题。同样的方法也适用于always @(*)的多路选择器,只需要把selin的值周期性切换,并通过$displayassert检查输出。

8.3 仿真中容易忽略的点

仿真通过只代表功能行为正确,不代表综合后时序一定满足要求。组合逻辑在仿真中看起来是瞬时更新的,但实际电路存在门延迟;时序逻辑在仿真中忽略时钟偏移和建立保持时间,但真实 FPGA 上必须做时序约束。

所以在功能仿真之外,还需要在 FPGA 工程里跑综合实现和时序收敛。仿真用于验证逻辑,综合用于验证可制造性和性能,两者不能互相替代。

9. 常见问题与排查方法

问题现象可能原因排查方式解决方案
仿真波形出现X信号未初始化、多驱动、复位未拉低检查initial是否给reg赋初值,检查是否多个语句驱动同一信号统一初始化,确保复位信号先有效,消除多驱动
综合报告出现Latch inferredalways @(*)中 if/case 分支不完整查看 warning 中给出的信号名elsedefault,或提前给输出赋默认值
时序块中寄存器输出晚一拍=<=混用,或对组合信号路径有误解检查 testbench 中采样时刻时序块统一使用<=,组合块统一使用=
同一个信号多处赋值,编译报错assignalways同时驱动一个信号搜索该信号在模块内所有出现位置确认只保留一个驱动源
assign左侧使用reg导致语法错误assign只能驱动wire查看报错行号reg改为wire,或改用always
敏感列表手写漏了信号,仿真不更新使用了always @(a or b)但遗漏sel对比实际逻辑与敏感列表改用always @(*)自动生成敏感列表
仿真时间一直没有前进testbench 缺少循环/延时控制检查是否有always#延时在 testbench 中加入固定时钟或使用$finish
计数器综合后 bit 扩展异常位宽不匹配或加法溢出检查cnt + 1'b1的位宽显式定义localparam WIDTH并统一位宽

这些都是在学习assignalways和过程块时最常踩的坑。看到警告不要急着忽略,Latch inferredmulti-driverincomplete sensitivity这三类信息是保证代码质量的关键信号。

10. 最佳实践与代码规范

第一,每个always块只描述一种功能。组合逻辑用always @(*)配合阻塞赋值,时序逻辑用always @(posedge clk)配合非阻塞赋值,不要让一个块同时承担两种语义。

第二,组合逻辑中尽量补全条件。无论是if还是case,都要覆盖所有分支,否则综合工具大概率推断出 latch。最简单的做法是在always块最前面给所有输出赋一个默认值,再写具体分支逻辑。

第三,敏感列表尽量使用@(*)。手写敏感列表容易漏信号,漏一个信号就会导致仿真和综合的行为不一致。使用@(*)后,编译器会自动维护敏感列表,降低人为错误。

第四,命名要有区分度。建议对组合逻辑信号和时序逻辑信号做不同后缀,例如组合信号用_comb,寄存器输出用_q_r。这样在长代码里一眼就能看出信号性质,综合报告里的 warning 也更容易定位。

第五,复位方式要统一。同步复位和异步复位各有适用场景,但一个模块内不要混用。异步复位必须写进敏感列表,复位释放还要考虑恢复时间;同步复位逻辑上更简单,适合大多数内部寄存器复位场景。

第六,仿真环境保留一组最小可运行文件。把counter.vmux4_1.v和对应 testbench 放在固定目录,这样每次学习新知识点时,都能用同一套环境快速验证语法修改是否影响功能。

第七,做综合前先跑 lint 或语法检查。Vivado、Quartus、Verilator 都能给出大量 warning,优先看latch inferredwidth mismatchmulti-driver这几类,先把它们清零再进入布局布线,后面遇到时序问题时排除范围会小很多。

第八,代码中不要写不可综合的仿真语法用于 RTL,比如#10$display$monitorinitial。这些只允许出现在 testbench 里。RTL 中一旦出现延时,综合工具通常会忽略或产生不一致结果。

第九,涉及总线时统一位宽。assign {carry, sum} = a + b;这种写法要确认两侧位宽一致。高位宽不要隐式依赖 Verilog 自动扩展,建议通过localparam定义位宽,并在仿真 testbench 中检查边界值。

第十,状态机和计数器这类时序逻辑,尽量使用参数化的位宽和状态编码。用localparam定义状态名,用parameter定义计数器宽度,后续修改时只改一处,代码维护成本会明显降低。

11. 总结与下一步

assignalways @(*)always @(posedge clk)三者的边界其实很清晰:assign描述连续组合逻辑,always @(*)描述过程组合逻辑,always @(posedge clk)描述时序逻辑。assign左侧用wirealways块内左侧用reg;组合逻辑用阻塞赋值=,时序逻辑用非阻塞赋值<=;同一信号只能有一个驱动源。

建议你先不用急着写复杂状态机,而是把半加器、4 选 1 多路选择器和 8 位计数器这三个例子全部在iverilog里跑一遍,打开 VCD 波形观察赋值时刻。重点看三件事:assign是否在输入变化后立即更新输出、always @(*)在条件分支补全后是否不再产生 latch、计数器在异步复位释放后是否每个时钟沿加一。

下一步可以在这个基础上继续做三件事:第一,给计数器增加使能信号和清零信号,体会条件分支在时序逻辑中的写法;第二,把多路选择器升级为参数化优先编码器,对比assignalways @(*)的代码可读性;第三,尝试写一个简单的状态机,用always @(posedge clk)保存状态,用always @(*)生成输出,这样你会对组合逻辑和时序逻辑的配合有更完整的理解。

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

嵌入式Linux Modbus RTU开发实战:串口配置、协议解析与调试技巧

嵌入式Linux下做Modbus RTU开发&#xff0c;听起来像是个基础活儿&#xff0c;但真要一次把串口调通、把传感器数据读准&#xff0c;坑还是不少。这个项目说白了就三件事&#xff1a;把嵌入式Linux的串口配置对&#xff0c;按Modbus RTU协议把请求帧发出去&#xff0c;再把从站…

作者头像 李华
网站建设 2026/9/7 13:07:22

单相交流调压电路Simulink仿真全流程:从建模、触发到波形验证

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

作者头像 李华
网站建设 2026/9/7 13:06:15

中配GPU集群的正确出路:本地LLM推理与视频转码实战

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

作者头像 李华
网站建设 2026/9/7 13:01:56

Coze智能体开发实战:从工作流编排到Agent落地的完整教程

最近有不少读者在后台问我&#xff1a;Coze&#xff08;扣子&#xff09;到底是什么&#xff0c;它和AI大模型、Agent、工作流这些词到底是什么关系&#xff1f;为什么大家突然都在说“用Coze搭建智能体”“Coze工作流免费下载”这类话题&#xff1f;带着这些疑问&#xff0c;我…

作者头像 李华
网站建设 2026/9/7 13:01:33

用Python实现全自动拼豆:图像像素化与自动放置实战

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

作者头像 李华
网站建设 2026/9/7 13:01:05

Claude Code 接入 DeepSeek:环境变量配置与省钱实战

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

作者头像 李华