news 2026/10/7 1:36:56

I2C设备不上线?从物理层到驱动的完整排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
I2C设备不上线?从物理层到驱动的完整排查指南

1. 从一次"设备死活不上线"说起:I2C调试到底难在哪

搞嵌入式的人,几乎都经历过这样的场景:硬件同事说板子焊好了,驱动同事说代码写完了,结果上电一跑,i2cdetect扫出来一片空白,或者地址是出来了,但读回来的数据全是0xFF。然后两边开始互相甩锅——硬件说软件时序不对,软件说硬件上拉没焊。这种扯皮我见得太多了,而 I2C 恰恰是最容易引发这类扯皮的 bus 之一。

为什么是 I2C?因为它是一个多主多从、开漏输出、靠上拉电阻拉高、没有硬件流控、没有标准错误反馈机制的总线。SPI 你接错了顶多读出来是乱的,UART 你波特率不对顶多是乱码,但 I2C 一旦出问题,表现往往是"完全没反应"或者"卡死在等待 ACK",你连它到底走到哪一步了都不知道。更麻烦的是,I2C 的电气特性决定了它对上拉阻值、总线电容、走线长度、电源时序都极其敏感,而这些因素在原理图上往往看不出来,只有上电用示波器才能暴露。

这篇内容我想聊的不是"I2C 协议是什么"——那个网上讲烂了。我想聊的是当你面对一个不工作的 I2C 设备时,脑子里应该有一套什么样的排查顺序。这套思路我在 Linux 平台(设备树 + i2c-tools + 内核驱动)和裸机 MCU 平台(STM32、CH32 这类)上都反复用过,核心逻辑是通用的:先确认物理层,再确认协议层,最后才怀疑驱动和代码。顺序错了,你会在软件里绕一整天,结果发现是上拉电阻焊成了 10k 而总线电容又大。

关键词里出现了i2c-tools、设备树、i2c读写eeprom、ssd1306 i2c控制命令、i2c时序这些,说明关注这个主题的人覆盖了从裸机到 Linux 驱动的完整谱系。所以我会把两条线的调试方法都讲清楚,并且重点放在**"怎么一步步缩小问题范围"**这个方法论上,而不是罗列寄存器。

2. 物理层先过关:上拉、电平和总线电容这三件事

2.1 上拉电阻不是"随便焊一个4.7k"

I2C 的 SDA 和 SCL 都是开漏结构,也就是说器件只能把线拉低,拉高完全靠上拉电阻。这就带来一个经典的两难:

  • 上拉电阻太大(比如 10k、20k),上升沿会变缓。因为总线有电容(走线电容 + 器件引脚电容 + 上拉本身的寄生),RC 时间常数变大,波形上升沿变成"圆角"。在 100kHz 标准模式下可能还能凑合,一旦上到 400kHz 快速模式,上升时间超过规格(标准模式 1000ns,快速模式 300ns),从机就可能采样错误。
  • 上拉电阻太小(比如 1k),上升是快了,但器件拉低时的灌电流变大。I2C 规定单根线灌电流一般不超过 3mA,3.3V 系统下 1k 上拉就是 3.3mA,已经到边缘了,多几个器件并联就更危险,长期可能损伤 IO。

所以 4.7k 之所以成为"默认值",是因为它在 3.3V、100kHz、总线电容不大的场景下刚好平衡。但它不是万能值。我一般的经验判断是:

场景建议上拉理由
3.3V / 100kHz / 短线(<10cm)4.7k通用默认,稳妥
3.3V / 400kHz / 短线2.2k ~ 3.3k加快上升沿
3.3V / 长线或多器件(电容大)1.5k ~ 2.2k对抗大电容,但要算灌电流
5V 系统4.7k ~ 10k5V 下同样阻值灌电流更大,可适当加大
混合电压(3.3V 主 + 5V 从)需电平转换不能直接上拉,会倒灌

提示:如果你不确定总线电容,最土但最有效的办法是拿示波器看上升沿。把探头挂在 SCL 上,触发一个下降沿,看它从 0.3VDD 升到 0.7VDD 用了多久。超过规格就减小上拉,波形"方"了但灌电流没超标就对了。

2.2 用万用表做的三个"上电前检查"

在写任何代码之前,我一定会做这三件事,能省掉后面 80% 的扯皮:

  1. 断电测通断:SDA、SCL 到主控引脚是否导通,有没有虚焊。特别是 QFN、BGA 封装的器件,肉眼根本看不出来。
  2. 上电测静态电平:不通信时,SDA 和 SCL 都应该是高电平(被上拉拉高)。如果测出来是低电平,说明要么有器件把线拉死了(器件损坏或地址冲突),要么上拉没焊,要么主控引脚配置成了推挽输出低。
  3. 测上拉是否真的存在:断电,万用表电阻档测 SDA 对 VCC 的阻值,应该接近上拉阻值。如果测出来是无穷大,恭喜你,上拉漏焊了——这是新手最常见的坑。

我见过一个案例:板子上 SDA 和 SCL 的上拉电阻位置画反了,焊的时候也跟着反了,结果 SCL 上拉到了 SDA 的网络,两条线互相干扰,i2cdetect扫出来一堆莫名其妙的地址。这种问题你盯着代码看一辈子也看不出来。

2.3 总线电容:被忽视的"隐形杀手"

I2C 规范规定总线电容上限是400pF。这个数字听起来很大,但实际很容易超:

  • 每米普通排线大约 50~100pF
  • 每个器件的 IO 引脚约 5~10pF
  • PCB 走线每厘米约 1~2pF

如果你挂了 8 个器件,光引脚电容就 40~80pF,再加 30cm 排线 15~30pF,再加上走线,很容易逼近 200pF。这时候如果还用 4.7k 上拉,上升沿就会明显变缓。解决办法要么减小上拉,要么用I2C 缓冲器/中继器(比如 PCA9515 这类)把总线分段,每段电容独立计算。

3. 协议层排查:i2c-tools 是 Linux 下最好用的"听诊器"

3.1 i2cdetect 扫不到设备,先别急着改驱动

在 Linux 平台上,i2c-tools是我排查 I2C 的第一工具。基本流程:

# 列出所有 I2C 总线 i2cdetect -l # 扫描某条总线上的设备(-y 跳过交互确认,-r 用读方式探测) i2cdetect -y -r 1

i2cdetect -l会列出系统里注册的所有 I2C adapter,比如i2c-0、i2c-1。这里有个关键点:你要先确认你的设备挂在哪条总线上。在设备树平台(比如瑞芯微 RK3568、各种 ARM SoC)上,总线编号和硬件控制器是对应的,但编号顺序不一定和原理图上的 I2C1、I2C2 一致。我一般会去/sys/bus/i2c/devices/下面看,或者直接dmesg | grep i2c看内核启动时注册了哪些控制器。

如果i2cdetect扫出来全是--,说明总线上没有任何设备响应。这时候按这个顺序排查:

  1. 确认总线选对了:换几条总线都扫一遍,别死磕一条。
  2. 确认设备供电了:很多传感器有独立的 VDD,主控上电不代表传感器上电。用万用表量一下传感器的 VCC 引脚。
  3. 确认设备没有处于复位状态:有些器件有 RESET 引脚,低电平复位,如果这个脚悬空或者被拉低,器件根本不工作。
  4. 确认地址:有些器件的 I2C 地址由地址引脚决定(比如 AT24C02 的 A0/A1/A2),地址引脚接错,扫描的地址范围就不对。i2cdetect默认扫 0x03~0x77,如果你的设备地址在这个范围外(比如某些器件的 0x78),需要手动指定范围。

3.2 扫到了地址但读写失败:区分"假地址"和"真设备"

有时候i2cdetect会扫出一个地址,但那个地址根本不是你的设备。这种情况通常是:

  • 地址冲突:两个器件地址一样,或者某个器件的地址引脚配置成了和另一个器件相同的值。
  • 总线被拉低产生的假象:如果 SDA 被某个器件持续拉低,i2cdetect可能误判。
  • 上拉问题导致的误读:上升沿太缓,采样点落在不确定区域。

验证方法很简单:用i2cget读一个已知寄存器,看返回值是否合理。

# 读地址 0x50 的寄存器 0x00(以 AT24C02 EEPROM 为例) i2cget -y 1 0x50 0x00 # 写一个字节再读回来验证 i2cset -y 1 0x50 0x00 0xAB i2cget -y 1 0x50 0x00

如果写进去读出来不一样,或者读出来永远是0xFF(总线空闲时的高电平)或0x00,那基本可以确定通信没真正建立。EEPROM 是特别好的测试对象,因为它读写逻辑简单、地址固定、不怕写坏(只要别写太频繁),非常适合用来验证一条 I2C 总线是否健康。

3.3 用 i2c-tools 的-f和-a参数处理特殊情况

有些场景下i2cdetect会报 "Resource temporarily unavailable",这是因为内核里已经有驱动占用了这个地址。这时候可以用-f强制扫描,或者用-a指定地址范围。但要注意,强制扫描可能会和正在工作的驱动冲突,生产环境慎用。

另外,i2ctransfer是比i2cget/i2cset更灵活的工具,可以一次传输多个字节,适合调试那些需要"先写寄存器地址再读数据"的器件:

# 先写寄存器地址 0x00,再读 4 个字节 i2ctransfer -y 1 w1@0x50 0x00 r4

这个命令的语义是:向地址 0x50 写 1 个字节(0x00),然后读 4 个字节。对于很多传感器(比如带寄存器的加速度计、温湿度传感器),这种"写地址+读数据"的组合是标准操作,i2ctransfer一条命令就能搞定,比i2cget方便得多。

4. 设备树里的 I2C:那些让你设备"注册不上"的细节

4.1 设备树节点写对了,驱动才可能 probe

在 Linux 嵌入式平台(RK3568、i.MX、全志、瑞芯微这些),I2C 设备是通过设备树描述的。一个典型的 I2C 设备节点长这样:

&i2c1 { status = "okay"; clock-frequency = <100000>; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; pagesize = <8>; }; };

这里有几个极其容易出错的点:

  • status必须是"okay":很多 SoC 的 I2C 控制器在默认 dtsi 里是"disabled",你不在板级 dts 里改成"okay",总线根本不会注册,i2cdetect -l里都看不到这条总线。
  • reg是 7 位地址:写的是0x50,不是0xA0。有些老驱动或者文档里用 8 位地址(左移一位),设备树里必须用 7 位。这个坑我踩过,写成了0xA0,驱动死活 probe 不上。
  • compatible要和驱动匹配:这个字符串必须和驱动里的of_device_id表完全一致,大小写、逗号位置都不能错。写错了内核不会报"找不到驱动",而是静默地不 probe,非常隐蔽。
  • clock-frequency:不写的话用控制器默认值,但有些控制器默认是 100kHz,你的器件支持 400kHz 却跑在 100kHz,性能上不去。反过来,如果器件只支持 100kHz 而你写了 400kHz,通信会不稳定。

4.2 用/sys和dmesg确认设备树是否生效

设备树改完、重新编译 dtb、重启之后,怎么确认生效了?

# 看这条总线上注册了哪些设备 ls /sys/bus/i2c/devices/ # 看某个设备的详细信息 cat /sys/bus/i2c/devices/1-0050/name # 看内核日志里 I2C 相关的信息 dmesg | grep -i i2c dmesg | grep -i "1-0050"

如果/sys/bus/i2c/devices/下面有1-0050这样的目录,说明设备树节点被正确解析了(1是总线号,0050是地址)。如果没有,说明设备树没生效,或者节点写错了位置。

我遇到过一个很典型的问题:设备树节点写在了&i2c1下面,但硬件实际接的是i2c3。结果就是设备树里有个"幽灵设备",实际总线上什么都没有。这种问题只能靠对照原理图确认总线编号来避免。原理图上标的 I2C1,不一定对应软件里的i2c1,中间可能经过 pinmux 重映射。

4.3 pinmux 冲突:I2C 引脚被别的功能占了

这是设备树调试里最隐蔽的坑之一。SoC 的引脚往往是复用的,一个物理引脚可以当 GPIO、UART、I2C、PWM 用。如果你的设备树里,某个引脚被配置成了 GPIO,而它实际要当 I2C 的 SCL 用,那 I2C 控制器虽然注册了,但引脚没接到控制器上,波形根本出不来。

排查方法:查 SoC 的 pinmux 配置,确认 I2C 对应的引脚被正确设置为 I2C 功能。在设备树里通常体现为pinctrl节点:

&i2c1 { pinctrl-names = "default"; pinctrl-0 = <&i2c1_xfer>; status = "okay"; };

如果pinctrl-0引用的节点不对,或者这个节点被别的外设也引用了(资源冲突),I2C 就可能工作不正常。这种问题在dmesg里有时会有 "pin already requested" 之类的报错,有时则完全静默,只能靠仔细核对。

5. 裸机 MCU 侧:时序、ACK 和那些"卡死"的瞬间

5.1 硬件 I2C 还是软件模拟 I2C

在 STM32、CH32 这类 MCU 上,第一个决策就是:用硬件 I2C 外设,还是 GPIO 模拟?

  • 硬件 I2C:省 CPU、时序精确、支持中断和 DMA。但很多 MCU 的硬件 I2C 有历史遗留 bug(尤其是早期 STM32 的 I2C 外设,卡死、总线锁死是出了名的),而且调试起来不透明,出问题只能看寄存器。
  • 软件模拟 I2C:用两个 GPIO 手动翻转,时序完全可控,出问题可以用示波器逐位看。缺点是占 CPU,高速下(400kHz 以上)可能跟不上。

我的经验是:调试阶段优先用软件模拟,因为可控性高,能快速定位是时序问题还是器件问题。等通信稳定了,如果对性能有要求,再换成硬件 I2C。很多量产项目最后用的其实是软件模拟,因为稳定、可移植、不挑芯片。

5.2 软件模拟 I2C 的四个关键时序点

软件模拟 I2C 看起来简单,但有几个时序细节必须严格遵守,否则就会出现"偶尔能通、偶尔不通"的玄学问题:

  1. 起始条件:SCL 高时,SDA 从高变低。这个顺序不能反,反了就是从机眼里的停止条件。
  2. 数据建立时间:SDA 变化必须在 SCL 低电平期间完成,SCL 拉高后 SDA 要保持稳定。很多新手在 SCL 高的时候改 SDA,导致从机采样到错误数据。
  3. ACK 采样:主机发完 8 位数据后释放 SDA,在第 9 个时钟周期采样 SDA 是否为低。如果读到高,说明从机没应答(NACK),要么地址错,要么器件没准备好。
  4. 停止条件:SCL 高时,SDA 从低变高。

一个常见的坑是延时不够。软件模拟时,SCL 高低电平的持续时间要满足器件要求。100kHz 下,一个周期 10us,高低各 5us。如果你用delay_us(1),实际可能因为函数调用开销变成 2~3us,导致频率偏高。稳妥的做法是用示波器实测 SCL 频率,调整延时。

5.3 总线锁死:SDA 被从机拉低怎么办

I2C 最恶心的问题之一是总线锁死:某个从机在传输过程中复位或异常,把 SDA 持续拉低,导致主机无法产生起始条件,整条总线瘫痪。

标准恢复方法是手动发送 9 个时钟脉冲:把 SCL 配置成 GPIO,手动翻转 9 次,让从机把剩余的位发完,然后发一个停止条件。代码逻辑大致是:

// 伪代码:I2C 总线恢复 void i2c_bus_recover(void) { // 1. 把 SCL 和 SDA 都配置为 GPIO 开漏输出 // 2. 确保 SDA 释放(输出高) // 3. 翻转 SCL 9 次 for (int i = 0; i < 9; i++) { scl_low(); delay_us(5); scl_high(); delay_us(5); } // 4. 发送停止条件:SCL 高时 SDA 从低变高 sda_low(); delay_us(5); scl_high(); delay_us(5); sda_high(); delay_us(5); // 5. 重新初始化 I2C 外设 }

这个恢复流程我在多个项目里都用过,成功率很高。但要注意,恢复之后要重新初始化 I2C 控制器,否则硬件状态机可能还停在错误状态。

6. 典型器件实战:EEPROM 和 OLED 的调试差异

6.1 AT24C02 EEPROM:验证总线健康的最佳试金石

EEPROM 是 I2C 调试的"Hello World",因为它的协议最简单:写地址、写数据、等 5ms(写周期)、读回来。用i2c-tools验证:

# 写 0xAB 到地址 0x00 i2cset -y 1 0x50 0x00 0xAB # 等待写周期完成(EEPROM 写入需要时间) sleep 0.01 # 读回来 i2cget -y 1 0x50 0x00 # 应该返回 0xab

如果写进去读出来不对,常见原因:

  • 写周期没等够:AT24C02 的写周期典型 5ms,最大可能到 10ms。如果你写完立刻读,器件还在内部写入,不会应答,读出来就是错的。稳妥做法是写完sleep 10ms再读。
  • 页边界问题:AT24C02 每页 8 字节,跨页写会回卷到页首。如果你一次写超过 8 字节且跨越页边界,数据会写错位置。这是 EEPROM 的经典坑。
  • 地址引脚:A0/A1/A2 决定设备地址的低 3 位,接错地址就变了。

6.2 SSD1306 OLED:命令和数据要分清

0.9 寸、1.3 寸的 OLED 模块很多用 SSD1306 驱动,I2C 接口。它的坑和 EEPROM 完全不同:

  • 控制字节:SSD1306 的 I2C 传输第一个字节是控制字节,0x00表示后面是命令,0x40表示后面是数据。如果你把命令当数据发,屏幕不会有任何反应,但总线是通的(有 ACK),所以i2cdetect能扫到,i2cget也能读到东西,但屏幕就是不亮。这种"通信正常但功能不对"的问题,只能靠看数据手册解决。
  • 初始化序列:SSD1306 上电后必须发送一长串初始化命令(设置时钟、复用率、对比度、扫描方向、电荷泵等),少一条都可能不显示。而且电荷泵命令(0x8D, 0x14)特别关键,不发的话屏幕供电不足,全黑。
  • 兼容性问题:关键词里提到"0.9寸oled对i2c兼容问题",这个确实存在。不同批次的 0.9 寸模块,有的用 SSD1306,有的用 SH1106。SH1106 的显存是 132x64,比 SSD1306 的 128x64 宽,如果按 SSD1306 的寻址方式写 SH1106,图像会偏移 2 列。判断方法:看模块背面丝印,或者试着发 SH1106 的初始化序列看是否正常。

调试 OLED 时,我一般先用i2ctransfer手动发几条命令,确认屏幕有反应(比如发显示开命令0xAF),再上完整驱动。这样能把"总线问题"和"初始化序列问题"分开。

7. 一套可复用的 I2C 排查决策树

把上面所有内容浓缩成一套排查顺序,遇到 I2C 不工作时按这个走:

步骤检查项工具通过标准
1供电是否正常万用表器件 VCC 达到规格
2上拉是否存在万用表(断电测阻)SDA/SCL 对 VCC 有阻值
3静态电平是否为高万用表/示波器空闲时 SDA/SCL 都是高
4波形是否干净示波器上升沿满足规格,无毛刺
5总线是否注册i2cdetect -l能看到对应 adapter
6设备是否响应i2cdetect -y -r N扫出预期地址
7读写是否正常i2cget/i2cset/i2ctransfer写入能读回
8设备树是否生效ls /sys/bus/i2c/devices/有对应设备目录
9驱动是否 probedmesg无报错,有 probe 成功日志
10功能是否正常实际应用数据正确、功能符合预期

这个顺序的核心逻辑是从物理到协议再到软件,每一步都建立在前一步通过的基础上。很多人一上来就改驱动,结果绕了一大圈发现是上拉没焊。按这个顺序走,能最快定位问题层级。

8. 几个我踩过的坑和私藏技巧

坑一:示波器探头电容影响波形。用普通无源探头(约 10pF)测 I2C 时,探头本身的电容会加重总线负载,本来勉强能通的波形,一挂探头就不通了。这时候要么用低电容探头,要么把上拉临时减小再测。我遇到过测的时候正常、拔了探头就不正常的情况,就是探头电容在"帮忙"。

坑二:多个主控共享总线时的仲裁问题。如果一条 I2C 总线上有两个主控(比如 SoC 和 MCU 都能控制),一定要确认它们不会同时发起传输。I2C 有仲裁机制,但前提是双方都正确实现了。如果一方是软件模拟且没做仲裁检测,就可能出现两个主控同时拉低、数据错乱的情况。这种场景建议加一个硬件互斥信号,或者干脆分成两条总线。

坑三:热插拔导致的总线锁死。有些设备支持热插拔(比如某些传感器模块),拔插瞬间如果正好在传输中,从机可能把 SDA 拉死。所以支持热插拔的 I2C 设备,驱动里最好加上总线恢复逻辑,检测到连续 NACK 就触发 9 时钟恢复。

私藏技巧:用逻辑分析仪抓完整传输。示波器只能看波形,逻辑分析仪能直接解码出地址、数据、ACK/NACK,一眼就能看出是地址错了还是数据错了。几百块的入门逻辑分析仪配合开源软件,调试 I2C 的效率比示波器高一个数量级。我现在的习惯是:示波器看电气特性(上升沿、电平),逻辑分析仪看协议内容(地址、数据、ACK),两者配合,基本没有定位不了的问题。

私藏技巧:给 I2C 总线留测试点。画 PCB 时,在 SDA、SCL、GND 上各留一个测试点(哪怕是 1mm 的焊盘),调试时能直接夹探头或焊飞线。这个成本几乎为零,但能省下大量"找不到地方下探头"的时间。很多新手板子画得很紧凑,结果调试时连个下探头的地方都没有,只能飞线,飞线又引入干扰,恶性循环。

私藏技巧:软件模拟 I2C 加超时保护。软件模拟时,等待 ACK 的循环一定要加超时,否则从机没响应时程序会死循环卡死。我一般设一个计数器,超过一定次数就返回错误,并触发总线恢复。这个习惯能避免"程序跑飞"这类难查的问题。

I2C 这个总线,协议本身半小时就能学明白,但真正把它调稳,靠的是对电气特性、时序细节和排查方法的理解。上面这些内容,每一条背后都是实际项目里踩出来的。下次再遇到 I2C 设备不上线,别急着改代码,先按第 7 节那张表走一遍,大概率能少走很多弯路。

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

运放噪声来源与斩波技术降噪原理实战解析

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

作者头像 李华
网站建设 2026/10/7 1:35:40

SAP FICO业务范围配置与实战指南:从概念到常见坑

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

作者头像 李华
网站建设 2026/10/7 1:35:03

AD 封装导入 Allegro:从原理到实操的完整指南

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

作者头像 李华
网站建设 2026/10/7 1:34:59

Cadence Virtuoso中CMOS半加器版图设计实战

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

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

平面变压器仿真验证全流程:从PEmag模型提取到Twin Builder电路联调

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

作者头像 李华
网站建设 2026/10/7 1:33:20

C#与MySQL房屋租赁管理系统:数据库课程设计完整源码解析

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

作者头像 李华