news 2026/9/27 1:08:45

I2C调试实战:从万用表测电平到示波器抓波形,定位ACK/NACK故障

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
I2C调试实战:从万用表测电平到示波器抓波形,定位ACK/NACK故障

说实话,I2C是那种看着简单、调起来容易上火的协议。两根线,SDA加SCL,焊点都没几个,可设备不干活的时候,你用万用表量电压,发现两根线都是3.3V,心里想“总线没问题啊”,结果软件那边照样报通信失败。等你把示波器接上去,波形确实有,可就是不知道问题出在哪一环——到底是主机没发对地址,还是从机根本没应答,又或者只是信号质量太差被干扰了。

我最早调I2C也踩过这种坑:拿万用表测SDA,看到高电平就以为总线正常,结果从机压根没理我。后来才明白,I2C的“正常”不是一个静态电平能说明的,它靠的是动态时序里的一次次应答握手。那次之后,我给自己定了一套固定流程——先用万用表把静态的物理层问题排干净,再用示波器抓动态波形定位协议层问题,最后死磕ACK位判断握手到底断在哪一步。

这篇文章就是把这套完整排查流程写出来。适合正在调I2C传感器、EEPROM、触摸屏IC或者RTC的硬件工程师和嵌入式软件工程师,也适合刚接触总线调试的学生。看完你会知道:万用表在I2C调试里到底能干什么、示波器怎么设置才能一次抓到关键帧、NACK背后有哪些常见原因,以及怎么用一套可复制的步骤从“有波形”走到“通信正常”。

1. 万用表先摸底:四类静态问题,两分钟排除大半

很多新手一上手就把示波器探头怼到信号线上,这其实走反了。I2C调试里相当大比例的问题根本不在时序上,而在物理层:芯片没供电、上拉电阻没焊、SDA对地短路、地址引脚悬空。这些问题全是静态的,用万用表两分钟就能查出来,根本不需要上示波器。

示波器的强项是看动态变化,但静态电平、通断、电阻值这些信息,反而是万用表最擅长的。而且万用表笔尖细,方便夹在IC引脚或者过孔上,在密密麻麻的板子上比示波器探头好用得多。我现在的习惯是:拿到一块I2C通信不正常的板子,先不急着写代码、抓波形,而是先花三分钟做一轮静态测量。

1.1 供电、空闲电平、上拉、短路:四项基础测量

第一项测供电。每个I2C从设备的VCC和GND之间电压必须正常,常见3.3V系统就要求3.3V正负5%左右。如果某个器件供电引脚虚焊,或者供电网络有断点,那它在总线上自然不会有任何反应,而且这种现象用示波器都不如万用表直测来得直接。

第二项测空闲电平。I2C总线空闲时,SDA和SCL都应该被上拉电阻拉到高电平,对3.3V系统来说接近3.3V。如果量出来是0V或者很低的电压,基本可以判定总线被某个设备拉住了,或者上拉电阻根本没起作用。

第三项查上拉电阻。断电状态下用万用表电阻档,一个表笔接SDA,另一个接VCC,看通不通。正常I2C上拉电阻一般在1kΩ到10kΩ之间,板子上可能还并联着IC内部的ESD二极管、滤波电容,所以读数不是纯电阻值,但只要通路存在,读数通常就是几kΩ的数量级。如果量出来是无穷大,那大概率是上拉电阻没焊好,或者从SDA到VCC的走线断了。

第四项查短路。分别量SDA对地、SCL对地、SDA和SCL之间有没有短路。如果阻值只有几十欧甚至更低,先别怀疑芯片,大概率是焊锡连锡、PCB毛刺或者排线压伤这类问题。

这几项测完,我还会顺手看一眼地址引脚。很多I2C芯片有AD0、A0、A1这类地址选择引脚,如果它们悬空或者接法不对,器件的实际地址会和程序里写的不一致,后面会引发非常隐蔽的NACK问题。

1.2 中间电平不代表正常,别被读数骗了

万用表测I2C有个特别容易误导人的地方:当你把表笔放在SDA上,如果总线正在高速通信,万用表电压档显示的其实是高低电平的平均值。100kHz速率下,SDA大概10微秒翻转一次,万用表根本来不及响应,就会显示一个接近1.5V到1.8V的中间值。

很多新手看到1.5V就慌了,以为是电平故障。其实那只是万用表的“迟钝”在骗你——总线可能正欢快地跑着数据。反过来,如果总线空闲,SDA稳定停在3.3V,那才是真正的静态高电平,值得放心。

但有一种中间电平确实要警惕:如果总线在主控没有主动驱动的时候、空闲状态本身就被某个从机拖到半电平,比如显示1.0V或者1.2V,那就说明有设备在“半挂”状态,可能驱动器内部损坏,也可能是多个设备同时抢占总线。这种问题万用表只负责发现问题,定位还要靠示波器。

所以我的判断标准是:万用表测的是“有没有明显断链”,不是“信号好不好”。测到正常的供电、空闲电平和通路,只能说明物理层没有硬伤,不代表协议层没问题。要想继续往下查,把示波器请出来。

1.3 静态项做完,什么情况下还找不到病根

现实里经常遇到这种情况:万用表把供电、上拉、短路全查了一遍,全部正常,SDA和SCL也有3.3V空闲电平,但设备就是不应答。这时候问题就进入动态范围了——要么主机发的地址不对,要么从机因为复位、使能、写保护等内部状态没有进入可寻址状态,要么总线上存在只在通信瞬间才暴露的时序问题。

这些靠万用表是搞不定的,必须靠示波器去抓真实波形。我把示波器在I2C调试里的角色理解成“拍现场”:它负责把总线上实际发生的事情还原出来,包括起始条件、地址位、读写位、ACK位,以及每一位的电压落在什么区间。

2. 示波器抓时序:从探头到ACK判读的一整套设置

示波器接上I2C总线,第一件事不是急着调触发,而是先把探头设置搞对。很多人上来就用1x挡、夹一根长地线,结果波形上全是毛刺和过冲,怎么看怎么不对劲,其实那根本不是总线的问题,是测量方式本身引入了噪声。

I2C信号本身不算快到离谱,100kHz到400kHz是主流,1MHz也不算罕见。但I2C是开漏结构加外部上拉,边沿通常比较缓,尤其是线长、设备多的时候,上升沿就是慢吞吞的一条斜线。测量这样的信号,探头选择、地线接法、带宽设置任何一个不对,都很容易让你误判时序。

2.1 探头和前端:10x衰减、短地线、带宽限制

我的固定做法是:SDA接CH1,SCL接CH2,两个通道都用10x探头,探头衰减比在示波器菜单里确认设置成10x。为什么不用1x?1x探头输入电容大,通常上百皮法,接上去会明显拖慢I2C的上升沿,尤其是总线上设备多、驱动弱的时候,探头一夹上去波形就变形了,甚至可能导致通信直接失败。10x探头输入电容小很多,对总线的影响也更小。

地线也要讲究。探头标配的鳄鱼夹地线通常又长又粗,夹上去之后会形成一个很大的地环路,示波器电源地、板子地、探头地之间产生的环路天线会把开关电源的噪声耦合进来,波形上出现一堆莫名其妙的毛刺。我一般用探头自带的弹簧地线,或者一根特别短的接地夹,直接夹在IC旁边的地过孔上,让环路面积最小化。

如果示波器有带宽限制功能,比如20MHz档,对I2C这种低速信号完全可以打开。带宽限制能滤掉高频开关噪声,让触发更稳定,读波形也更干净。但如果打开带宽限制后波形幅度明显降低、边沿变得特别圆,那可能不是带宽的问题,而是探头补偿没调好——用示波器自带的1kHz方波校准信号,把探头上的微调电容拧到波形平顶无过冲即可。

2.2 触发源放在SDA、时基按速率算:参数速查

触发设置是抓I2C波形最关键的一步。I2C通信的起始条件永远是SDA先拉低、SCL随后拉低,所以触发源选SDA、触发方式选下降沿,能稳定地抓到每一次通信的开始。如果触发源选SCL,抓到的波形往往从已经发生时钟脉冲的中间切入,反而看不到起始条件。

触发电平设在总线高电平的一半到三分之二之间比较稳妥,3.3V系统我习惯放在1.5V左右,太高可能触发不到沿,太低又容易被噪声误触发。

时基设置取决于总线速率。100kHz的I2C,SCL周期是10微秒,抓一个完整地址帧加ACK位大约需要9个SCL脉冲,加上起始和停止条件,用10微秒/格能看个大概;400kHz的I2C,SCL周期只有2.5微秒,要看清每一位,时基设在1微秒/格到2微秒/格比较合适。如果只关心ACK位,还可以把时基再调大点,用1微秒/格甚至500纳秒/格去数第九个脉冲。

以下是我常用的示波器参数参考表:

参数项推荐设置说明
CH1SDA,10x探头,DC耦合看数据线
CH2SCL,10x探头,DC耦合看时钟线
触发源CH1(SDA)起始条件稳定
触发方式下降沿匹配Start Condition
触发电平1.4V~1.6V(3.3V系统)取总线电平中位
时基2us/div(400k),10us/div(100k)按实际速率调整
电压量程1V/div完整显示0~3.3V
存储深度尽量开最大放大后仍能看清细节

还要记得把示波器触发模式从Auto切到Normal。Auto模式下长时间没触发信号会自动扫描,波形是滚动的,看不准;Normal模式只在满足触发条件时刷新,才能稳定观察I2C帧。如果要抓上电瞬间或者偶发失败,就用Single单次触发,按下按键后等事件发生。

2.3 数到第九个脉冲,读ACK位:关键帧怎么看

示波器抓到一帧I2C波形后,怎么判断通信是否成功?不要只看“有没有方波”,有方波只能说明主机在努力发数据,从机可能根本没理人。真正的关键在第九个时钟脉冲。

看波形时先找到起始条件:SDA从高变低,随后SCL从高变低,这就是Start位。接下来SDA上的每一个高低电平对应一个地址位或数据位,SCL的每一次上升沿代表这一位被写入。数到第九个SCL脉冲时,注意观察SDA在这个脉冲期间的电压。

第九个SCL脉冲高电平期间,如果SDA被拉低到接近0V,说明从机给出了ACK应答。如果SDA一直维持高电平,就是NACK,从机没有应答。只要看到NACK,通信就失败了,后面的数据位再多也没意义。

用示波器光标功能可以把第九个脉冲的高电平区域框出来,直接读取SDA电压值,避免肉眼数错。有些示波器支持测量SDA通道的最小值,如果最小值接近0V,再确认这个低电平出现在第九个脉冲期间,基本就能判断ACK存在。

我踩过的坑是:一开始只盯着地址位看,觉得地址符合手册就行了,忽略了ACK。结果软件那边一直报错,我还以为是干扰,后来才发现从机根本没应答——因为它的复位脚被下拉电阻拉住了,芯片压根就没工作。所以说,ACK位真的就是I2C调试的“红绿灯”,看到它,你才知道通信到底通没通。

3. ACK/NACK的真相:第九个脉冲里藏着握手状态

ACK是I2C协议里最容易读漏、也最值得深挖的信号。很多资料里写“从机应答时拉低SDA”,一句话就带过去了,但实际调试时,ACK和NACK背后的含义差别很大,排查方向也完全不同。

先把机制讲透。主机发送完8个数据位后,会释放SDA线的控制权,让SDA处于高阻态,由上拉电阻维持高电平。然后在第九个SCL脉冲到来时,从机如果正常工作且收到了前面的字节,就会主动把SDA拉低,表示“我收到了,继续发”。主机在这个脉冲期间采样SDA,读到低电平就是ACK。

用生活里的话说,主机发完一个字节,相当于往门口投了个快递,第九个时钟是“停下来听有没有人应门”。门开了是ACK,门没开就是NACK。I2C总线是线与结构,没人应答时SDA就被上拉拉成高电平,所以NACK在波形上表现为第九个脉冲处的高电平。

3.1 第九个脉冲谁在拉低SDA

这里有个容易误解的点:ACK不是主机产生的,是从机把SDA拉低。很多初学者以为第9个脉冲来了,主机自己把SDA拉低表示确认,这完全错了。主机在第9个脉冲要做的是“释放SDA并采样”,从机才负责拉低。

理解这一点对调试很重要。如果你在示波器上看到第9个脉冲时SDA是高电平,说明从机没有拉低——要么从机根本没收到前面的数据,要么收到了但不想理你,要么从机的SDA驱动根本没工作。这时候你就能确定:问题的“责任方”大概率在从机侧,而不是主机软件。

主机需要确认的是自己发的地址和数据是否正确,但从机侧需要排查的事情就多了:供电有没有到位、复位脚是否释放、使能脚有没有拉对、芯片是否处于写入保护状态、器件地址和实际硬件引脚配置是否一致等等。

3.2 地址NACK、数据NACK、偶发NACK:一张表定位原因

同样都是NACK,位置不同、现象不同,排查方向差得很远。我把常见情况整理成了一张速查表:

NACK现象常见原因排查动作
地址阶段NACK地址写错、器件不在总线上、器件未上电或未复位用示波器读地址位并核对;扫描总线确认从机地址
地址阶段NACK7位地址和8位地址混用核对软件API到底需要7bit还是8bit,检查地址左移一位
数据阶段NACK从机忙、写保护开启、不支持该命令查数据手册时序,检查WP引脚,适当降低写入频率
数据阶段NACK从机内部非易失写周期未完成EEPROM类芯片需等待写周期,通常几个毫秒
偶发NACK总线干扰、时序裕量不足、时钟拉伸不兼容降速、优化上拉、换短走线、改用软件I2C

地址阶段NACK最常见的原因,是7位地址和8位地址混用。看数据手册时,有些手册写0xA0,有些写0x50,其实这是同一个从机地址的两种写法:0x50是7位地址的十六进制形式,0xA0是左移一位补了读写位之后的8位形式。软件API里有的函数要求传7位地址,有的要求传8位地址,如果没搞清楚,I2C帧里地址位就对不上,从机永远NACK。

用示波器可以非常直观地验证这一点。把地址帧波形放大,一位一位读出SDA的电平,就能看到实际发出去的是7位地址0x50还是8位地址0xA0。对照手册确定从机应该响应的地址,这个坑很快就能排除。

数据阶段NACK则要多关注从机的内部状态。比如EEPROM芯片在写数据后需要时间把数据写入非易失存储,这段时间内如果主机继续发写命令,从机可能NACK。触摸屏IC上电初期固件还在初始化,主机太早发命令也会遇到NACK。这类问题最好的验证方法,是在示波器上同时观察SCL、SDA和一个GPIO信号,用GPIO标记软件尝试通信的时机,看看NACK是否集中出现在某个特定时间段。

3.3 时钟拉伸、总线冲突和手动ACK:会看波形更要会用协议分析工具

除了ACK,还有几个高难度的总线状态值得认识。第一个是时钟拉伸。有些从机处理速度慢,会在SCL上主动拉低一个或多个周期,让主机等一下。在示波器上看到的现象是:SCL低电平时间明显拉长,超过正常的半个周期甚至好几个周期。支持时钟拉伸的主机会等待,硬件I2C外设如果不支持,通信就会错乱。

第二个是总线冲突。两个主机同时驱动总线,SDA上会出现“两个驱动器打架”的情况,波形在高低电平之间出现台阶或者畸变,严重时SDA电压停在中间值。这种故障用万用表看到的是异常中间电平,用示波器能看到冲突发生的具体位置。

第三个进阶操作是手动ACK和自由数据模式。有些逻辑分析仪和协议分析工具允许你关闭自动应答,手动控制SDA在第9个脉冲的输出,用来验证从机在特定条件下的反应;自由数据模式则是完全不受标准帧格式约束,手动打时钟和数据位,逐bit喂给从机,特别适合调试从机固件。对于有时间深究的从业者,这两个工具能在常规排查无效时打开新的思路。

不过对大多数排查场景,我建议的顺序是:先用示波器确认ACK与否,再用带I2C解码的逻辑分析仪自动标记出地址和应答位置,把人工数脉冲的工作交给工具去做,效率会高很多。

4. 完整排查流程与三个实战案例

前面把万用表、示波器、ACK机制分别讲清楚了,但实际现场不会按教科书划分来出现故障。所以我总结了一套从“症状”到“根因”的完整排查流程,再用三个真实案例把它串起来。

这套流程的核心逻辑是:先静态后动态,先物理层后协议层,先确认地址再深究数据。无论从机是EEPROM、触摸屏还是传感器,盲目改软件代码之前,先让波形告诉你真实发生了什么。

4.1 可复制的排查清单:从示波器到逻辑分析仪

我的排查顺序一般这样:

  1. 万用表查供电、空闲电平、上拉、短路。这一步排除硬件物理问题,耗时三分钟。
  2. 示波器触发SDA下降沿,抓起始条件和地址帧。放大波形,逐位读地址电平,确认主机发的地址正确。
  3. 看第九个SCL脉冲期间SDA电平。低为ACK,高为NACK。
  4. 如果NACK,先查地址格式(7位/8位)、从机供电、复位、使能、写保护,再考虑是否器件本身损坏。
  5. 如果ACK正常但通信仍然失败,用逻辑分析仪抓完整帧,对照数据手册检查命令字、寄存器地址、数据格式。
  6. 如果波形上升沿缓慢、出现毛刺或偶发错乱,尝试降低I2C速率,检查上拉电阻阻值,缩短总线长度。

第2步和第3步可以合在一起做:示波器一次抓完地址帧加ACK位,是最有效率的方式。如果总线挂着多个设备,想确认哪些设备在线,可以写一个I2C地址扫描小程序,在MCU里循环向0x08到0x77的所有地址发送读命令,有ACK就打印出来。类似的逻辑也可以很快用脚本工具配合总线适配器实现。

for (addr = 0x08; addr < 0x78; addr++) { i2c_start(); if (i2c_write(addr << 1) == ACK) { printf("found: 0x%02X\n", addr); } i2c_stop(); }

跑扫描程序时,示波器会看到总线上一串连续的地址帧,用单次触发抓其中一帧,就能确认总线到底有哪些设备在应答。这个办法在排查“我以为器件在总线上但其实不在”的时候特别管用。

4.2 案例一:地址帧看着对,但GT911一直NACK

有块板子上的GT911触摸屏在Linux下报I2C通信失败,日志里全是timeout。万用表测供电3.3V正常,SDA和SCL空闲电平都接近3.3V,上拉电阻2.2kΩ也通路,静态检查挑不出毛病。示波器抓波形,主控确实在发地址帧,读到的是GT911常见的地址,看起来很合理。

但关键问题在第9个SCL脉冲:SDA一直是高电平,NACK。这就说明地址也许没错,但对面的芯片根本没有应答。顺着从机侧排查,发现复位脚被一个下拉电阻死死拉在低电平,GT911一直处于复位状态,压根没有起来干活。把这个下拉电阻去掉、复位脚改成正常的上拉配置后,ACK信号立刻出现,通信恢复正常。

这个案例的教训是:地址正确、总线静态正常,并不意味着从机在工作。很多芯片有复位脚、使能脚、中断脚,任何一个被拉在错误状态,都会让器件在总线上“装死”。示波器看到NACK后,第一时间就要往从机自身状态去查。

4.3 案例二:波形“漂亮”却偶发错乱,问题在容性超限

另一个项目里的板子平时跑400kHz的I2C没问题,但环境温度升高后就偶发通信失败,软件报CRC错。示波器抓波形,乍一看很标准,SDA和SCL都高低分明,可仔细测量上升沿,发现边沿非常缓,几乎是梯形波而不是陡峭的方波。更麻烦的是,在上升沿爬升到大概1.5V附近时,总有细小的毛刺叠在上面。

这个现象说明总线容性负载太重了:板子上挂了六个I2C设备,总线走线长,上拉电阻2kΩ虽然不算大,但在高电平时对寄生电容充电还是不够快,上升沿被拖慢。上升沿一旦太慢,数据位在SCL采样点附近就不稳定,外界稍微有点干扰就容易错位,于是出现偶发CRC错。

解决办法是把I2C速率从400kHz降到100kHz,相当于给总线留出更多时间让电平稳定。降速之后通信立刻稳定了。如果再激进一点,可以用I2C总线缓冲器把多个设备分成不同分支,降低单段总线上的容性负载。这个案例说明:波形“看着好看”和“时序裕量足够”是两码事,调I2C不能只满足于能看到波形,还要关注边沿和裕量。

4.4 案例三:探头夹上去的一瞬间,系统就卡死

还有一种很讨厌的情况:程序单独跑没问题,示波器探头一夹到SDA线上,系统立刻卡死或者通信失败。第一次遇到时我还以为是代码里有bug,后来发现是测量行为本身改变了电路。

原因有两类。一类是探头地线太长,地环路把噪声耦合进总线,导致SDA和SCL上出现毛刺,干扰了时序判断;另一类是探头输入电容过大,尤其是用了1x挡,探头电容叠加到总线电容上,把本来就不富余的上升沿拖得更慢,时序裕量直接被吃光。

解决方法是换10x探头、用短地线夹、尽量把探头直接接触IC引脚而不是长走线末端。如果还不行,就在探头尖上串联一个小电阻,比如几百欧到1kΩ,隔离探头电容对信号的影响。这个案例对排查“示波器一接就出问题”的诡异故障很有效。

我把示波器探头的影响也算进排查清单里之后,很多原本看着像软件bug的问题,最后证明都是测量方式不对导致的现象误判。

我后来调I2C,习惯是先给自己限时三分钟做万用表静态检查,再上示波器抓地址帧直接看ACK,最后用逻辑分析仪辅助看完整数据帧。这套流程帮我处理过不少从EEPROM、触摸屏到各类传感器的通信故障。如果你手里只有万用表,先把供电、上拉、短路测完再开始怀疑软件;如果你刚拿到示波器,先把触发放到SDA下降沿、时基从2微秒每格开始。I2C调试本质上不是玄学,它是一层层找握手在哪一步断开的过程,而找到断点的钥匙,就在那根SDA线的第九个脉冲上。

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

STM8开发环境搭建全攻略:从STVD到Cosmic配置与避坑指南

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

作者头像 李华
网站建设 2026/9/27 1:07:57

基于Python的景点数据分析系统:数据生成到GUI落地全流程

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

作者头像 李华
网站建设 2026/9/27 1:07:27

空天通信网络架构与关键技术:从低轨星座到链路预算验证

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

作者头像 李华
网站建设 2026/9/27 1:06:42

Smartbi报表开发实战:Excel模板、日期参数校验与发布避坑

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

作者头像 李华
网站建设 2026/9/27 1:06:26

RK3568 Buildroot重启需手配IP?根源与Debian固化方案

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

作者头像 李华
网站建设 2026/9/27 1:06:25

I2S协议详解:从时序原理到波形调试与代码实现

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

作者头像 李华