news 2026/9/24 13:29:19

示波器实测RS485:从波形看懂差分信号与串口调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
示波器实测RS485:从波形看懂差分信号与串口调试实战

做嵌入式、工控或者上位机开发的朋友,应该都背过 RS485 那一套电平参数:A 线几伏、B 线几伏、逻辑 1 是几伏、逻辑 0 是几伏……我说句实话,这些数字背下来用处真不大,因为你真到现场拿示波器一看,波形和教材里画的那个方块图往往差得挺远。

这篇文章我想换个思路:不背电压,直接看波形。用示波器实测 RS485 通信过程中 A、B 两根线的实际电压变化,把差分信号到底是什么、波形长什么样、怎么从波形上判断通信是否正常一次讲清楚。文章后面还会给一份我平时调试用的 C# 串口收发源码,用来配合硬件侧做联调,读完可以直接抄作业。

适合谁看?刚接触 485 通信的嵌入式工程师、自动化现场调试人员,以及做上位机但一直没搞懂“差分信号”到底差在哪的 C# 开发。就算你手头还没有示波器,看完也能在脑子里建立起一个“波形图像”,以后再遇到 485 通信异常,至少知道该往哪个方向查。

1. 为什么 RS485 的电压值靠背不如靠测

1.1 别被 A/B 极性绕晕,先搞清楚标准的判据

RS485 的标准定义里,用 A、B 两根线之间的差分电压来表示逻辑状态。按 TI 等主流芯片手册的说法,接收端的判断阈值是:A-B 大于 +200mV 判为逻辑 1,A-B 小于 -200mV 判为逻辑 0,中间的 -200mV 到 +200mV 属于不确定区。

这里有个特别容易踩的坑:很多国产设备、老设备的丝印把 A、B 标反了。你拿万用表量 A 对地是 3.5V,B 对地是 1.5V,按芯片手册理解 A-B 应该是 +2V,是逻辑 1,但如果这个设备把“发送正端”定义成了 B,那实际含义就反了。

所以我在现场从来不死记“A 高 B 低”这种口诀,而是直接看示波器上 A 相对 GND 和 B 相对 GND 的波形关系,再把结果和设备的协议文档对照。波形不会骗人,丝印才会。

1.2 差分信号的“差”到底指什么

差分信号的核心思想是:不拿某一根线对 GND 的绝对电压来表示数据,而是拿两根线之间的电压差来表示。这样做的好处是共模抑制。

举个生活里的例子:你在嘈杂的火车上和朋友面对面聊天,周围人声很大,这是共模噪声,但你们两个人之间的“音量差”是不受周围影响的,因为噪声同时作用在你们两个人身上。RS485 的 A、B 两根线就是这两个人,干扰信号会同时叠加在两根线上,相减之后干扰就被消掉了。

所以 RS485 能在线缆几十米甚至上千米的场景下稳定传输,而 RS232 只能跑十几米,本质原因就在这里。RS232 是单端信号,一根信号线对地表示电平,地电位稍微偏一点,数据就乱了。

1.3 发送端和接收端的电压标准不一样,别混用

很多新手问:RS485 逻辑 1 到底是多少伏?答案是:取决于你看的是发送端还是接收端。

  • 发送端在带负载的情况下,差分输出电压一般要大于 1.5V,很多芯片实际能输出 2V 以上。
  • 接收端只要检测到 ±200mV 的差分电压就能正确判断逻辑,这个阈值低得多。

也就是说,发送端给的是“强驱动”,接收端用的是“弱判别”,中间留了充足的余量,这也是 485 能做到长距离传输的原因之一。你在示波器上看到的 A-B 差分波形,幅度通常在 1V 到 5V 之间,具体大小取决于驱动芯片型号、线缆长度和终端电阻。

2. 示波器测 RS485 波形的正确姿势

2.1 测量前的准备:探头、接地和差分通道

测 RS485 波形,普通双通道示波器就够用,不需要一开始就上差分探头。我常用的做法是:

  • 通道 1 接 A 线,探头夹子接设备侧的 GND
  • 通道 2 接 B 线,探头夹子接同一个 GND
  • 然后用示波器的数学通道做 CH1-CH2,得到的就是 A-B 差分波形

很多人问能不能直接用两个探头跨接在 A、B 之间测差分?原则上不行,因为普通探头的地夹是接到示波器外壳地的,直接跨接等于把 A 或 B 对地短路了,轻则测不准,重则损伤设备。老老实实用数学通道相减最稳妥。

如果条件允许,用一根差分探头当然更准,尤其是共模电压很高或者地电位不稳的场合。但日常调试,双通道 + 数学通道完全够用。

2.2 示波器参数设置:时基、触发电平、带宽限制

示波器参数设得不对,抓出来的波形要么乱成一团,要么根本触发不了。我调试 485 时的典型设置如下,以 9600bps、8N1 为例:

参数推荐值说明
时基1ms/div 或 2ms/div9600 波特率下 1 字节约 1.04ms,一帧 8 字节约 8.3ms
电压档位通道单端看 2V/div,数学通道看 1V/div单端波形 0~5V 左右,差分会小一些
触发方式下降沿触发,或根据起始位极性选择触发源选 CH1 或 CH2 单端波形
触发电平1.5V 或 2.5V设在单端波形高电平和低电平之间
带宽限制打开 20MHz 低通滤掉高频噪声,波形更干净
采样深度尽量大,至少能存几秒方便抓完整帧后放大逐字节分析

9600 波特率下,1 个 bit 的时间是 1/9600 ≈ 104μs。一帧数据如果是 8 个字节,加上起始位和停止位,总共 80 个 bit,大约 8.3ms。所以时基设在 1ms/div 到 2ms/div 比较合适,能完整看到一整帧,又不至于太密看不清细节。

2.3 波形上那些“坑”:空闲态、起始位和数据位的识别

把探头接好、参数设好、让设备循环发数据,示波器上就能看到类似这样的规律性波形。我来给你拆解一下每个部分对应什么含义。

首先是空闲态。485 总线在没有任何设备发送时,处于一个确定或半确定的状态。如果电路里有上拉电阻接到 A、下拉电阻接到 B,那么 A-B 差分电压会稳定在正电压(通常是 1V 以上),对应逻辑 1。如果电路没有加偏置,总线处于高阻状态,波形可能不稳定,甚至出现随机跳变。

然后是起始位。UART 协议规定空闲态是逻辑 1,所以发送端开始发送时,会先把差分电压拉到逻辑 0 方向,也就是 A-B 变成负电压。这个从正跳到负的边沿,就是示波器触发的关键点,也是接收端开始同步数据的信号。

接着是 8 个数据位。每个 bit 的时间长度固定,等于 1/波特率。数据位的内容决定了波形是保持高还是低。这里要注意:UART 发送一个字节时,是先发最低位 LSB,再发最高位 MSB。如果示波器抓到一个字节的波形是 1010 1010,换算成数值要反过来读,是 0x55 而不是 0xAA。

最后是停止位。发送完数据位后,总线回到逻辑 1 状态,持续至少 1 个 bit 的时间。如果后面还有下一个字节,起始位会再次拉低,形成连续的帧序列。

3. 实测场景:一帧真实 485 报文从头看到尾

3.1 主站发送“01 03 00 00 00 01”的波形实录

我用一个常见的 Modbus RTU 请求帧举例:设备地址 01,功能码 03,起始地址 00 00,读取长度 00 01,再加上 CRC16 校验,完整帧是01 03 00 00 00 01 0A 0B这样的 8 个字节(CRC 具体值取决于计算方式,此处仅示意)。

在实际示波器抓到的波形里,你会看到一串由方波组成的序列。以 9600 波特率为例,每个 bit 104μs。第一个字节 0x01 的二进制是 0000 0001,发送顺序是 LSB 在前,实际线上依次是 1、0、0、0、0、0、0、0,配合起始位和停止位,波形就是:先是起始位的低电平,然后 1 个高电平 bit(LSB=1),接着 7 个低电平 bit,最后停止位回到高电平。

这种直接把波形和字节内容对应起来看的练习,做上几次,你对“数据在线上是怎么流动的”就会有肌肉记忆。以后看到一串波形,即使没有协议解析工具,也能大概判断出帧的起始和结束位置。

3.2 同一帧波形在无终端电阻和有终端电阻下的区别

这个对比我建议每个人都在实验室里试一次。用示波器抓同一帧数据,先不加 120Ω 终端电阻,再加 120Ω 终端电阻,观察波形变化。

不加终端电阻时,你会看到方波的上升沿和下降沿有明显过冲,甚至会振铃——波形在跳变之后上下抖动几次才稳定。这是因为信号在线的末端发生了反射。如果线很短(比如 20cm 以内的调试线),反射不明显;一旦线长超过几米,振铃会非常明显,严重时会导致接收端误判数据。

加上 120Ω 终端电阻之后,过冲和振铃基本消失,但边沿会变得稍微圆滑一些。这个圆滑是正常的,只要边沿斜率没有差到让接收端无法判断,就没问题。终端电阻的本质是让线的特性阻抗和负载阻抗匹配,减少反射。485 标准建议的双绞线特性阻抗约 120Ω,所以终端电阻也取 120Ω。

3.3 上下拉电阻对空闲态的影响,一测便知

还有一个值得实测的项目:总线上的上拉和下拉电阻。很多 485 电路在 A 线上拉、B 线下拉,目的是让总线在空闲时保持确定的高电平状态,避免接收端因为悬空而收到乱码。

示波器上你可能会看到两种现象:

  • 有偏置电阻时,空闲态 A-B 稳定在正电压,波形是一条平直的线。
  • 没有偏置电阻时,空闲态可能漂移,甚至偶尔出现一个毛刺,这个毛刺被接收端当成起始位,就会产生一个乱码字节。

上下拉电阻的阻值选择也有讲究。阻值太大,驱动能力弱,抗干扰差;阻值太小,会增加发送端的负载电流,影响输出幅度。我常用的范围是 1kΩ 到 10kΩ,具体取多少要结合终端电阻和线上挂的设备数量来算。每个节点的等效电阻并联后,要保证所有设备的接收端都能在空闲时看到大于 200mV 的差分电压。

4. 常用 485 电路与自收发电路的工程细节

4.1 最小系统长什么样:收发器、上下拉、终端电阻一个都不能少

一个典型的 485 节点电路通常包含:485 收发器芯片(比如 MAX485、SP3485、ISO3082 等)、两个偏置电阻(A 上拉、B 下拉)、一个 120Ω 终端电阻(只在总线两端加)、以及必要的防护器件。

收发器芯片的关键引脚有:

  • RO:接收输出,接 MCU 的 RX
  • RE:接收使能,低电平有效
  • DE:发送使能,高电平有效
  • DI:发送输入,接 MCU 的 TX
  • A、B:总线差分端口

MCU 发送数据时,把 DE 拉高,同时把数据从 DI 送入;接收数据时,把 RE 拉低,从 RO 读取。如果使用半双工模式,DE 和 RE 经常连在一起,由同一个 GPIO 控制。

很多初学者直接拿 USB 转 485 的适配器做实验,只关心 A、B 怎么接,不关心内部的偏置电路,这样也能通。但一旦自己画板子、组网,偏置电阻和终端电阻的布局就会直接影响通信稳定性。

4.2 MOS 管自收发电路到底靠不靠谱?230400 波特率能不能跑

网上有一种用 MOS 管或三极管做的 485 自收发电路:不占用 MCU 的 GPIO 来控制 DE,而是利用 TX 信号本身的高低电平自动切换收发状态。典型原理是:TX 为低电平时 MOS 管导通,把 DE(或 RE+DE 组合)置为发送使能;TX 为高电平时 MOS 管截止,回到接收状态。

这种电路的优点是省一根控制线,接线简单。但波特率高了之后,问题就来了。MOS 管的栅极电容、上下拉电阻的充放电时间会在 TX 由高变低、或由低变高时产生延迟,可能导致发送起始位变形、停止位被截断。230400 波特率下,1 bit 的时间只有约 4.3μs,MOS 管切换慢一点,第一个字节就废了。

我自己实测过:在 9600/115200 波特率下,这种电路还能勉强工作,但到 230400 就得看具体选型和布线,翻车概率很大。如果项目必须跑高速,建议换成带自动方向控制功能的收发器芯片,比如 MAX13487、ISL3170E 这类,或者老老实实用一个 GPIO 控制 DE。

4.3 接口防护设计:为什么你会烧毁 RS485 芯片

现场调试中,485 芯片烧毁的案例并不少见。常见原因有三个:

  • 共模电压超过芯片耐压范围。长距离传输时,设备之间的地电位差可能达到几十伏甚至上百伏,超过收发器的共模输入范围就会损坏。
  • 热插拔引起浪涌。带电插拔连接器时,瞬间的电流冲击可能打坏芯片。
  • 雷击或感性负载切换产生的瞬态过压。

对策主要有:加 TVS 管把 A、B 对地和 A、B 之间的电压钳位在安全范围;加 PTC 自恢复保险丝限制过大电流;在总线入口处加共模电感抑制共模干扰。条件允许的话,直接用带隔离的收发器模块,比如 ADM2483 或各种隔离 485 模块,现场故障率会低很多。

5. C# 上位机调试源码讲解

5.1 先用 SerialPort 把最基础的串口收发跑通

C# 操作串口,核心就是 System.IO.Ports.SerialPort 这个类。我写了一个简单的 Rs485Client 类,包含打开串口、发送字节、接收完整帧三个基本功能。先看打开串口的部分:

using System.IO.Ports; public class Rs485Client { private SerialPort _port; private List<byte> _buffer = new List<byte>(); private System.Timers.Timer _frameTimer; public Rs485Client(string portName, int baudRate = 9600) { _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _port.DataReceived += OnDataReceived; _frameTimer = new System.Timers.Timer(50); _frameTimer.AutoReset = false; _frameTimer.Elapsed += OnFrameTimeout; } public void Open() { if (_port.IsOpen) return; _port.Open(); Console.WriteLine($"串口 {_port.PortName} 已打开,波特率 {_port.BaudRate}"); } public void Close() { if (_port.IsOpen) _port.Close(); } }

这里有三点要提醒:

  • 串口参数必须和设备的配置完全一致,包括波特率、数据位、停止位、校验位。485 通信最常见的乱码原因就是两边参数不匹配。
  • 发送数据时要操作 byte 数组,不要用 string。串口按字节传输,字符串编码容易引入额外字节。
  • Open 和 Close 方法要做重复调用的判断,否则会抛异常。端口被其他程序占用时,Open 也会报错,可以加一个 try-catch 给用户明确提示。

5.2 发送一帧数据并等待设备应答

485 是半双工通信,上位机发一帧命令给设备后,设备会应答一帧数据。上位机要做的就是:发送前清空接收缓冲,发完开启一个超时计时器,在超时时间内收齐一帧数据。

public void SendAndReceive(byte[] data) { _buffer.Clear(); _port.DiscardInBuffer(); _port.Write(data, 0, data.Length); Console.WriteLine("SEND: " + BitConverter.ToString(data).Replace('-', ' ')); _frameTimer.Stop(); _frameTimer.Start(); } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { byte[] temp = new byte[_port.BytesToRead]; _port.Read(temp, 0, temp.Length); _buffer.AddRange(temp); _frameTimer.Stop(); _frameTimer.Start(); } private void OnFrameTimeout(object? sender, System.Timers.ElapsedEventArgs e) { if (_buffer.Count > 0) { ProcessFrame(_buffer.ToArray()); _buffer.Clear(); } else { Console.WriteLine("等待应答超时"); } }

这个“定时器重置法”是判断帧结束的一种务实方案:每次收到数据就重置定时器,如果 50ms 内没有新数据进来,就认为一帧结束了。这个时间可以根据设备实际响应速度调整,设备处理慢的话可以放宽到 100ms。

5.3 结果回调:把收到的一帧数据交给业务逻辑

ProcessFrame 方法只是简单打印,实际项目中这里会做 CRC 校验、解析寄存器值、更新界面等操作。示例代码如下:

private void ProcessFrame(byte[] frame) { Console.WriteLine("RECV: " + BitConverter.ToString(frame).Replace('-', ' ')); // 这里只做最基本的长度校验,实际项目要加 CRC 校验 if (frame.Length < 3) { Console.WriteLine("帧长度异常"); return; } // 假设帧头是 0xAA 0x55,帧尾是 0x0D 0x0A if (frame[0] == 0xAA && frame[1] == 0x55) { // 把有效载荷取出来,具体业务自行扩展 byte[] payload = frame.Skip(2).Take(frame.Length - 4).ToArray(); Console.WriteLine("有效数据长度: " + payload.Length); } }

注意 DataReceived 事件是在后台线程里触发的,如果你想在方法里直接操作 WinForms 或 WPF 的界面控件,必须使用 Invoke/BeginInvoke 或者用并发队列把数据传给 UI 线程处理,否则会报跨线程操作异常。

5.4 一个更贴近实际需求的命令构造方法

如果你做的是 Modbus RTU 协议,发送报文需要计算 CRC16。这里给一个命令构造的骨架,CRC 具体实现可以根据标准算法补全:

public byte[] BuildModbusFrame(byte slaveId, byte functionCode, ushort startAddr, ushort regCount) { byte[] frame = new byte[8]; frame[0] = slaveId; frame[1] = functionCode; frame[2] = (byte)(startAddr >> 8); frame[3] = (byte)(startAddr & 0xFF); frame[4] = (byte)(regCount >> 8); frame[5] = (byte)(regCount & 0xFF); ushort crc = Crc16(frame, 6); frame[6] = (byte)(crc & 0xFF); frame[7] = (byte)(crc >> 8); return frame; }

用的时候,把设备地址、功能码、寄存器地址传入,就能得到一帧完整报文,再调用 SendAndReceive 发出去即可。配合示波器观察总线上的实际波形,你可以看到上位机发出去的每一帧,和示波器抓到的是完全一致的数据。

6. 现场常见波形与问题排查速查表

6.1 一看波形就能判断的常见故障

日常调试 485 通信,很多问题不用查协议、不用翻代码,示波器一看就能锁定方向。我把这几年遇到比较典型的场景整理成一张表:

波形现象可能原因排查动作
A、B 完全无波形方向控制没有生效,DE 未拉高检查发送使能引脚控制逻辑
A、B 波形正常但差分通道无输出数学通道设置错误,或探头未正确接 GND重新配置 CH1-CH2
波形幅度只有几百 mV总线上设备过多、线缆过长负载过重检查终端电阻和偏置电阻
波形边沿有严重振铃缺少终端电阻或阻抗不匹配在总线两端加 120Ω 终端电阻
总线空闲态漂移,偶发乱码缺少偏置电阻在 A 线上拉、B 线下拉
数据完全反向A/B 接反调换 A、B 接线再测
通信偶尔超时,波形幅度接近阈值线路过长,共模干扰过大考虑加隔离、改善布线
某一台设备接入后整条总线瘫痪该设备 A/B 短路或方向控制异常单独测试该设备

这张表看着简单,但每一条背后都是真实踩坑积累出来的。比如总线瘫痪那条,我遇到过一台设备上电之后,它的发送使能被强拉到高电平,导致它一直在占用总线,其他设备全部发不出数据。用示波器一看,总线就一直被拉在低电平状态,立刻就能定位到问题设备。

6.2 组网调试的几条铁律

  • 终端电阻只加在总线两端,中间节点不要加。很多人图省事每个节点都加 120Ω,结果总线负载电阻变成几十欧,驱动电路扛不住,波形幅度被拉得很低。
  • 总线布线用双绞线,尽量远离动力线、变频器输出线等干扰源。走线做不到的话,至少保证 A、B 两根线是缠绕在一起的,不要让两根线分开走很远的距离。
  • 所有设备的 GND 必须共地。RS485 虽然是差分信号,但收发器芯片的共模电压范围是有限的,设备之间地电位差太大会导致通信异常甚至烧毁芯片。
  • 节点数量不要超过收发器规格。常见的 MAX485 支持最多 32 个标准负载,如果设备多了,要用高输入阻抗的收发器或者增加中继器。

6.3 烧过几个芯片之后总结的防护建议

早期我做项目不重视 485 接口防护,现场烧过好几次收发器。后来总结出几条经验:

  • 每一台设备的 A、B 端口对地都要加 TVS 管,推荐选用钳位电压在 6V 左右的型号,这样正常通信的差分信号(1V~5V)不会被钳位,但雷击浪涌和电源异常带来的高压会被吸收掉。
  • 隔离不是可选项,是长距离传输的必选项。用带隔离的收发器模块,或者用数字隔离器加收发器的方案,能彻底断开设备之间的地环路。
  • 连接器选型尽量选带锁扣的航空插头或端子台,避免现场有人带电插拔导致瞬时浪涌。如果无法避免热插拔,接口处要加浪涌防护器件。

写在最后的一点体会

从最早背 RS485 电压参数、到后来拿示波器一帧一帧看波形,这个过程我最大的感受是:抽象的协议概念落到示波器屏幕上,会突然变得特别具体。差分信号不是一句“两根线取差值”就能理解透的,只有亲眼看到 A、B 两根线在干扰环境下同时上下浮动,差分通道却稳如泰山,你才真正体会到这种设计有多巧妙。

如果你手头有示波器,我强烈建议你找个下午,把 485 收发器、一块 MCU 最小系统板、一台电脑串口助手全部接起来,发一帧数据,调一调时基、触发电平,看看波形在加不加终端电阻时分别是什么样子。这套实验做下来,比死记十倍参数都有用。

C# 那边的源码,我给的版本是精简过的基础框架,实际项目里你可以在 ProcessFrame 里加 CRC 校验、超时重试、日志记录,也可以把 Rs485Client 封装成服务注入到你的业务系统里。调通之后,你再用示波器去对比真实波形和上位机发出去的数据,会发现原来设备和上位机之间就是靠这些方波在交流。搞懂这一层,再复杂的总线协议也都不难理解了。

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

离线语音AI芯片选型指南:蜂鸟系列在IoT家居的落地实践

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

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

SD NAND 深度解析:嵌入式存储选型、SPI/SDIO 驱动与 ECC 机制

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

作者头像 李华
网站建设 2026/9/24 13:26:30

Polar SI9000阻抗仿真原理与PCB高速设计实战

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

作者头像 李华
网站建设 2026/9/24 13:25:43

aardio 设置窗口图标

//增加图标核心代码&#xff08;修改默认的图标&#xff0c;即左上角图标重绘&#xff09; import win.image var iconPath "./资源文件/icon/favicon.ico" hIcon win.image.loadIconFromFile(iconPath,true) ::SendMessage(mainForm.hwnd, 0x0080, , hIcon);

作者头像 李华
网站建设 2026/9/24 13:24:32

EMC测试全流程解析:从项目分类到整改避坑的硬件工程师指南

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

作者头像 李华
网站建设 2026/9/24 13:24:19

DCDC降压芯片选型核心:Buck原理、参数本质与工程避坑指南

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

作者头像 李华