news 2026/9/29 1:12:02

OpenHarmony I2C调试指南:从协议到驱动,一步步排查总线故障

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenHarmony I2C调试指南:从协议到驱动,一步步排查总线故障

"哥几个,我又在OpenHarmony上栽跟头了。"上个月调试一块温湿度传感器,I2C总线死活读不到数据,示波器挂上去一看,SDA线跟心电图似的乱跳,折腾到后半夜才定位到是地址位多移了一位。当时就想,这玩意儿平时不起眼,真出问题的时候能把人磨到没脾气。所以这篇就把我在开源鸿蒙OpenHarmony系统里用I2C、排查I2C故障的完整思路写出来,从协议细节到驱动配置,从用户态读写到逻辑分析仪抓波形,一次性讲透。

1. 为什么在OpenHarmony开发中绕不开I2C

先说个现实:但凡你手上那块开发板接了屏幕、触摸、陀螺仪、温湿度、气压计、指纹模块、距离传感器,大概率走的就是I2C。这总线就两条线——SDA数据线加SCL时钟线,却能挂一堆设备,靠的就是每个器件有独立地址。OpenHarmony作为面向万物互联的系统,南向要适配五花八门的传感器,I2C自然成了HDF驱动框架里的常客。

在OpenHarmony里用I2C,跟你在Linux上用I2C非常像,但有它自己的皮。最直观的差异在于设备管理方式:OpenHarmony用HDF(Hardware Driver Foundation)统一管驱动,I2C控制器在系统启动时被注册进HDF框架,你在用户态通过/dev/i2c-X节点去操作。很多人一上来就照着Linux的i2c-tools去搞,发现命令不认,其实就是没搞懂HDF这层皮。

还有一个现实问题:OpenHarmony的发行版很多,各厂家的内核配置不一样。有的板子把I2C编译成了内核模块,有的直接编进去了,还有的需要你自己在device tree里打开某个节点。所以入手第一件事,不是写代码,是先确认你的系统里到底有没有暴露/dev/i2c-*节点。没有节点一切白搭。

2. 先把I2C协议嚼碎了再说排障

很多排障排到最后,发现是对协议本身的理解有偏差。所以这里先把I2C最核心的几个机制过一遍,我们后面所有的排查思路都建立在上面。

2.1 地址是7位还是8位:最容易翻车的细节

I2C设备地址标称7位,比如某传感器写的是0x38,这是7位地址。但在传输时,这7位要左移一位,最低位填读写标志:0表示写,1表示读。于是8位地址就成了0x70(写)、0x71(读)。

这个细节是新手翻车重灾区。你在驱动里写addr = 0x38,然后直接拿去做读操作,内核会把这当8位地址用,等于实际访问的7位地址变成了0x1C,人家设备根本不搭理你。所以确认器件手册上标的是7位还是8位是第一优先级,很多"明明地址对啊怎么就是不通"的案例,最后都是栽在这。

OpenHarmony的HDF框架里,I2cMsg结构体的addr字段也是按7位填的,写入时框架会帮你处理读写位。这倒是省心,但你对协议本身必须清楚,否则用户态测试脚本用的是8位地址,驱动里用的是7位地址,两边一对不上就懵了。

2.2 起始、停止和ACK:总线上每个bit都是有讲究的

I2C时序的关键动作就几个:

  • 起始条件:SCL高电平期间,SDA从高拉低,表示总线开始传输。
  • 停止条件:SCL高电平期间,SDA从低拉高,表示传输结束。
  • ACK:每传输完一个字节(8位),接收方在第9个时钟脉冲拉低SDA,表示"收到了"。
  • NACK:第9个时钟脉冲SDA保持高,表示"没收到"或者"读数据时我不想再要了"。

这里有个排障中的重要信号:读数据时,主机读到最后一个字节前要回NACK,告诉从机"别再发了",然后发停止条件。很多自己写软件I2C的朋友会在这里犯错——读最后一个字节也回ACK,从机就继续发,总线状态就乱了,表现出来就是数据错位、CRC不对、设备卡死。

从机地址发送后收到的ACK尤其关键。如果地址对了、线也通了,从机会在第9个脉冲拉低SDA——这是你判断"设备是否存在"的最快方法。排查I2C设备不响应时,第一件事就是在示波器或逻辑分析仪上看这个ACK位。

2.3 速率和上拉电阻:一对容易被忽略的搭档

I2C标准速率是100kbps(标准模式)、400kbps(快速模式)、1Mbps(快速模式+),OpenHarmony的HDF驱动里一般可以配置时钟频率。但很多人忽略了:速率提高后,对上拉电阻的要求更严格。

I2C的SDA和SCL都是开漏结构,必须靠上拉电阻拉到高电平。电阻太小,总线拉低时电流太大,设备可能扛不住;电阻太大,上升沿变缓,高速时波形根本达不到逻辑高电平阈值,设备就误判。

经验值:3.3V供电、400kbps、总线长度20cm以内,4.7kΩ上拉基本靠谱;如果线上设备多,换2.2kΩ;如果总线很长或者有软排线,1kΩ也常见。有次我调一个触摸屏,400k速率下就是偶发丢包,最后发现是板子上拉电阻用了10kΩ,换成2.2kΩ立刻稳定。这个属于典型的"硬件节奏不匹配"。

3. OpenHarmony平台侧:设备树和HDF驱动怎么配

在OpenHarmony里要让I2C跑起来,牵扯到设备树配置和HDF驱动两个层面。很多初学者把时间花在写应用层代码上,结果平台层没配好,所有努力白费。

3.1 设备树节点:先让你的控制器"现形"

以RK系列或Hi3516系列开发板为例,设备树里通常已经有I2C控制器节点,但默认可能是关闭的。你要做的是:

  1. 在设备树里找到对应的I2C控制器节点,比如i2c@fdf0d000(具体地址以实际芯片手册为准);
  2. 确认status = "okay",如果是"disabled"就改掉;
  3. 确认时钟、中断、引脚mux配置齐全。I2C引脚通常要复用,比如GPIO3_A0和GPIO3_A1要配成I2C3_SDA和I2C3_SCL功能,这步漏了,引脚就是普通GPIO,总线自然不通。

一个典型配置片段长这样:

&i2c3 { status = "okay"; clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&i2c3_pins>; /* 挂接具体设备 */ sht30@44 { compatible = "sht30"; reg = <0x44>; }; };

注意reg = <0x44>这里填的是7位设备地址(也有的平台直接填8位,看框架约定)。OpenHarmony的HDF设备树解析和Linux内核不完全一样,它有自己的匹配逻辑,所以你不能照搬Linux的设备树写法,要参照OpenHarmony对应芯片的dts样例来改。

3.2 HDF驱动:I2cAdapter和I2cMethod的注册逻辑

OpenHarmony的I2C驱动分两层:控制器驱动(管理硬件本身)和外设驱动(你写的具体传感器驱动)。控制器驱动在系统初始化时会把操作函数集合填充进一个叫I2cMethod的结构体:

struct I2cMethod { int32_t (*transfer)(struct I2cCntlr *cntlr, struct I2cMsg *msgs, int16_t count); };

外设驱动通过HDF的I2cOpen拿到控制器句柄,然后调I2cTransfer把消息数组发出去。整个过程是同步的:传入一个I2cMsg数组,每一条msg包含从设备地址、数据缓冲、长度、标志位(读还是写)。

我自己第一次在OpenHarmony里写I2C从设备驱动时,找I2cOpen这个接口找了半天,因为HDF的头文件路径和Linux的i2c-dev.h不一样。这里给你指个路:头文件一般在drivers/framework/include/utility/i2c_if.h(以你实际源码路径为准),函数是I2cOpen(uint32_t number),number对应第几号I2C控制器。操作完记得I2cClose,跟Linux的close(fd)一个道理。

4. 用户态怎么读写I2C:/dev/i2c-x节点的完整用法

OpenHarmony跑起来之后,正常情况下系统里会有/dev/i2c-0、/dev/i2c-1这些节点。用户态程序直接操作这些节点,是最快的验证方式——比写内核驱动和HDF驱动都简单,特别适合先验证硬件通路。

4.1 打开节点并配置从机地址

如果你只想对一个固定地址的设备做操作,用I2C_SLAVE这个ioctl最省事:

#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/i2c-dev.h> int main() { int fd = open("/dev/i2c-3", O_RDWR); if (fd < 0) { perror("open"); return -1; } // 设置7位从机地址,注意是0x44而不是0x88 if (ioctl(fd, I2C_SLAVE, 0x44) < 0) { perror("ioctl I2C_SLAVE"); return -1; } // 写一个字节给寄存器0x01 uint8_t buf[2] = {0x01, 0xAA}; if (write(fd, buf, 2) != 2) { perror("write"); return -1; } close(fd); return 0; }

很多开发板的OpenHarmony内核里CONFIG_I2C_CHARDEV是开着的,所以这套Linux用户态代码可以直接用。但要注意:如果内核没开这个配置,/dev/i2c-*节点就不会生成,你只能走HDF驱动或者用/sys/bus/i2c/devices下面的接口。

4.2 使用I2C_RDWR一次完成复合传输

真实场景里,读传感器数据一般分两步:先写寄存器地址,再读数据。用I2C_RDWR可以把这两步合到一个ioctl里,中间不释放总线,避免被其他进程插队:

#include <linux/i2c-dev.h> #include <sys/ioctl.h> static int read_register(int fd, uint8_t reg, uint8_t *value) { struct i2c_msg msgs[2]; struct i2c_rdwr_ioctl_data rdwr; msgs[0].addr = 0x44; // 7位地址 msgs[0].flags = 0; // 写 msgs[0].len = 1; msgs[0].buf = &reg; msgs[1].addr = 0x44; msgs[1].flags = I2C_M_RD; // 读 msgs[1].len = 1; msgs[1].buf = value; rdwr.msgs = msgs; rdwr.nmsgs = 2; if (ioctl(fd, I2C_RDWR, &rdwr) < 0) { perror("ioctl I2C_RDWR"); return -1; } return 0; }

这里有个细节:I2C_RDWR执行时,内核会把msgs[0]和msgs[1]当成一次原子操作处理,中间不允许其他进程插入。这在和多个进程共用同一条I2C总线时至关重要——不然你写寄存器地址后、读数据前,有个进程插进来跟设备说话,读回来的可能就是乱数据。

注意:OpenHarmony的HDF框架里也有类似的概念,叫I2cMsg数组,I2cTransfer会按顺序发送所有msg。所以你在HDF驱动里实现复合读写时,思路和上面完全一样,只是API换成了HDF封装。

5. 实测跑通:读写EEPROM和温湿度传感器

光说不练假把式。我这里用一块常见的AT24C02 EEPROM和一个SHT30温湿度传感器,演示完整的I2C读写流程。

5.1 AT24C02写读验证

AT24C02的7位地址一般是0x50(由A0/A1/A2引脚决定,默认全接地就是0x50)。写一个字节到某个地址,需要先发设备地址(写方向)、再发存储地址、再发数据:

步骤内容说明
1Start起始条件
20xA0设备地址+写位(0x50左移一位)
30x00存储地址
40x55要写入的数据
5Stop停止条件

很多EEPROM写完后需要一个内部写周期(大概5ms),这期间不响应任何命令。我在用户态测试时踩过这个坑:写完立刻去读,读回来的还是旧数据,百思不得其解。后来查手册才发现要等写周期结束,加个usleep(5000)就好了。这个问题在OpenHarmony里同样存在,时序要求由器件决定,跟操作系统没关系。

读的话有两种方式:当前地址读和随机读。随机读就是先发一个写方向的地址和存储地址,然后发一个重复起始条件,再发设备地址(读方向),连续读数据。用户态用I2C_RDWR就是上面那样的两个msg组合。

5.2 SHT30读取温湿度

SHT30的7位地址是0x44,它有个特性:测量命令是16位的,比如0x2C 0x06表示开启高重复性测量。完整流程:

uint8_t cmd[2] = {0x2C, 0x06}; struct i2c_msg write_msg = { .addr = 0x44, .flags = 0, .len = 2, .buf = cmd }; // 发送测量命令 if (ioctl(fd, I2C_RDWR, &rdwr) < 0) { perror("send command"); return -1; } // 等待测量完成,数据手册说最长约15ms usleep(50000); uint8_t data[6]; struct i2c_msg read_msg = { .addr = 0x44, .flags = I2C_M_RD, .len = 6, .buf = data }; // 读取6字节数据:温度高8位、低8位、CRC、湿度高8位、低8位、CRC

SHT30返回的6字节数据里有两个CRC校验字节,分别是温度数据和湿度数据的校验。很多人在这一步偷懒不校验,结果读出偶尔跳变的数据,还以为是总线干扰。用CRC校验能帮你区分"是传输错误"还是"传感器本身输出异常"——这是排查I2C数据可靠性问题的分水岭。

5.3 逻辑分析仪:排障的神器

写代码验证的同时,强烈建议挂一个逻辑分析仪。不贵的24MHz采样率的就够用,关键是能让你"看见"总线上到底发生了什么。

用逻辑分析仪抓I2C时,注意几点:

  • 采样率至少4倍于SCL频率。抓400kbps的I2C,建议用2MHz以上采样率,否则上升沿抓不准确。
  • 触发条件设为SCL下降沿,这样能保证抓到完整的起始条件。
  • 接好线再上电,SDA和SCL两根线别接反,GND必须共地。

抓到的波形能告诉你很多信息:起始条件是否正确、地址发的是多少、ACK有没有出现、数据字节和时序图对不对得上。有一次我排查一个I2C陀螺仪,逻辑分析仪一抓发现设备地址从机回了NACK,查器件手册才知道这颗芯片的地址是0x68而不是我以为的0x69——一个引脚的电平没拉对,地址就变了。这种事靠代码是查不出来的,必须回到硬件。

6. 排障实战:从现象到根因的完整排查链路

这部分是全文的重点。我按排查顺序写一套方法论,分成几个层次,从简单到复杂。很多I2C问题看起来千奇百怪,归根结底跑不出这几类。

6.1 第一板斧:确认物理层和基础配置

排查顺序很重要,乱序会让你在错误的方向上浪费大量时间。我会这样系统地排查:

排查项方法预期结果
供电万用表量传感器VDD和GND电压正常且稳定
共地确认传感器GND和主控GND连通电阻接近0Ω
上拉检查SDA和SCL对VDD的电阻1kΩ~10kΩ之间
引脚确认SCL和SDA没接反对调后正常
设备树status是否okay,引脚mux是否配了节点存在
设备节点ls /dev/i2c-*能看到对应节点
地址芯片手册确认7位地址与代码一致

我见过太多案例,最后发现是SDA和SCL接反了。I2C两个引脚接反后,通信100%失败,而且不会损坏设备,但就是啥都不工作。所以第一步先用万用表或者顺着电路图确认引脚再上电测试,不丢人。

6.2 第二板斧:看ACK,定位设备是否存在

如果用i2cdetect(如果系统里有)或者自己写个扫描程序,对0x03到0x77的所有地址发送一个空操作,看哪些地址有ACK返回。有ACK,说明有个设备在这个地址上存在。

i2cdetect在OpenHarmony里不一定有,你可以用一段简单的C代码实现扫描,核心就是给每个地址发一个0长度的写消息,看内核返回是否成功。但要注意:扫描I2C总线有风险。某些设备对未知命令会有奇怪反应,或者会干扰总线。更安全的做法是查器件手册确定地址,然后直接针对这个地址验证。

如果扫描发现在0x50、0x44这些预期地址上有ACK,说明物理层和地址都没问题,问题大概率在协议层——命令字写错了,寄存器地址不对,或者设备的模式没配对。

6.3 第三板斧:抓波形,区分"发了但错"和"根本没发"

当逻辑分析仪出场时,问题基本能定性:

  • SDA和SCL一点波形都没有:主控压根没在输出,检查驱动是否加载、设备节点是否打开成功、进程是否真的执行到I2C操作。
  • 有波形,但SCL只有起始条件后的几个脉冲就停了:大概率是从机没有ACK,主控检测到NACK后主动中止传输。这时重点查设备地址对不对、设备供电是否正常、设备是否处于异常状态。
  • 波形完整,ACK也正常,但数据不对:问题在协议逻辑,寄存器地址、数据字节顺序、命令格式有误。对照芯片手册逐字节核。
  • 波形乱跳,没有清晰的帧结构:可能是上拉电阻太小导致信号边沿过冲,或总线太长、干扰太大,甚至多个设备地址冲突打架。

有一次我调试一颗气压传感器,逻辑分析仪抓出来波形乱得没法看,SCL上频繁出现毛刺。查到最后发现是I2C总线上挂的两个设备,一个地址设成0x76,另一个设成0x77——本来不冲突,但其中一个设备支持SPI模式,它的SPI引脚浮空导致内部逻辑乱跳,影响了I2C引脚。把它的CS引脚拉高后,整个世界清净了。

6.4 总线挂死:clock stretching和复位手段

I2C有一个"总线挂死"的经典问题:中途通信异常,SDA被某个设备拉低,停在一个半不拉叽的状态。SCL还在翻转但SDA一直低,任何新通信都无法启动。

为什么挂死?I2C要求每个字节传输完必须有时钟脉冲用于ACK。如果通信中途被异常中断(比如主控复位了,但从机没复位),从机可能在等待后续时钟信号,这时它可能拉低SDA表示忙。主控要恢复通信,必须让SCL持续翻转9个周期以上,把从机的状态机清掉。

怎么复位?我常用的方法是GPIO模拟:

// 用GPIO分别控制SCL和SDA // 先把两根线都拉高 // 然后手动翻转SCL 9次以上,同时观察SDA是否释放 for (int i = 0; i < 9; i++) { gpio_set_value(scl, 0); usleep(10); gpio_set_value(scl, 1); usleep(10); } // 如果在某个时钟周期后SDA变高了,说明从机被释放

更彻底的办法是把I2C控制器的电源和从机电源都断一下再上电,让所有设备重新初始化。这在产品demo阶段非常管用,但在量产设备里不现实。所以如果你的设备有复位引脚,排查挂死时把它拉低再拉高一次,很多时候问题就解了。

OpenHarmony的HDF框架里没有现成的"总线复位"API,你得像上面那样用GPIO操作来模拟复位时序,或者干脆在驱动里加一个复位函数,对外暴露一个服务接口。

6.5 电平不匹配:3.3V和1.8V的混接陷阱

这个问题在带触摸屏、带高性能传感器的板子上特别常见。主控I2C是3.3V电平,传感器或者触摸屏控制器却是1.8V电平。直接对接,虽然短时间内可能不烧毁设备,但长期来看不靠谱,更重要的是:1.8V器件端的逻辑高电平阈值可能是0.7×1.8V约1.26V,3.3V主控输出的低电平倒是没问题,但3.3V的高电平灌进1.8V器件的引脚,超出其耐压会导致漏电、读不到正确的ACK。

正确做法是加电平转换芯片,比如TXS0102、PCA9306,或者用MOS管搭的双向电平转换电路。很多开发板已经板载了电平转换,但如果你是自己飞线接传感器,一定要查清楚两端的I2C电平。

我在OpenHarmony板子上接过一颗1.8V的接近传感器,一开始没注意电平,直接跟3.3V的I2C接上,现象是:写寄存器偶尔成功,读数据永远失败。后来用示波器对比发现1.8V器件拉低的SDA电平,在3.3V主控看来已经算低了,但器件回ACK时的低电平信号太弱,主控识别不到。加了电平转换之后,稳得一批。

6.6 地址冲突和总线竞争:多设备共存的坑

I2C总线上可以挂多个设备,每个设备必须有独一无二的地址。但有些芯片的地址引脚是硬连接的,不能软件改,这就导致两个同型号设备默认地址相同,直接冲突。

遇到地址冲突,有几个办法:

  • 硬件改地址:看芯片有没有A0/A1/A2引脚,通过接高接低改地址。比如AT24C02的A0/A1/A2全接地是0x50,如果把A0拉高,地址变成0x51。
  • 软件分时访问:如果两个设备地址冲突又没法改硬件,那只能把其中一个设备用GPIO控制电源,需要访问时单独上电,另一个断电。麻烦,但能work。
  • 换总线:把冲突设备挪到另一路I2C控制器上。很多主控有多路I2C,比如I2C0、I2C1、I2C3,合理分配可以避免冲突。

还有一种"软冲突":两个主控同时操作一条总线。有些设计里,多个主控(比如主控和协处理器)共享一条I2C总线。这时候总线仲裁就成了问题,如果你没有做多主控协调,同时发起通信就会导致数据错乱。OpenHarmony里一般不会遇到多主控,但如果你外接了MCU协处理器,要注意这个问题。

6.7 时序问题:从机慢,主机急

I2C是允许从机慢半拍的——通过clock stretching,从机在需要更多时间时会把SCL拉低,主机检测到后必须等待。但很多主控的I2C外设不自动处理clock stretching,或者处理有bug。

如果你遇到"偶发性通信失败"、"有时候读出全FF"这种问题,且波形上看ACK正常但数据对不上,可以怀疑是从机在时钟延展,而主机没等。对策:

  • 降低I2C速率。400k降到100k,很多时效问题会消失。代价是慢,但先跑通再说。
  • 检查主控I2C控制器的timing寄存器配置,看有没有开启clock stretching支持。
  • 如果主控完全不支持clock stretching,那要么换能支持的控制器,要么在软件里做重试机制——失败重发,重发还失败就报错。

在OpenHarmony里,I2C速率一般通过设备树clock-frequency属性配置,改起来方便。我之前调一颗需要较长启动时间的触摸芯片,100k下稳如老狗,400k下就偶发不响应,最后查下来就是这家芯片的I2C从机需要时钟延展,而主控没有正确处理。降到100k解决。

7. 最后几条实战心得

回到开头说的那个温湿度传感器,最后问题出在哪里?出在我自己身上——设备树里把clock-frequency写成了4000000,4MHz,超了I2C规格快三倍,总线直接罢工。改回400kHz,世界恢复平静。

我在这几年的开发里踩过的坑,汇成几句话:

第一,I2C排障的顺序永远是:先物理层,再协议层,最后才是软件逻辑。别一上来就盯着代码翻,拿万用表量一量、示波器看一眼,比啥都强。

第二,逻辑分析仪一定要常备。它能把总线上的每一帧都解码给你看,相当于把排障从盲人摸象变成了看监控视频。几十块钱的设备,回报远超成本。

第三,写OpenHarmony的I2C驱动时,先把用户态验证跑通再下沉到HDF驱动。用户态用/dev/i2c-x操作简单直接,适合验证硬件通路。硬件没问题了,再写正式的HDF驱动,能省掉大量调试时间。

第四,遇到诡异的偶发问题,先怀疑电源和地。I2C对电源纹波和地弹很敏感,一个劣质电源适配器能让一切"玄学"问题轮番上演。换电池供电试试,如果问题消失,恭喜你,找到了罪魁祸首。

I2C这总线吧,说简单也简单,两根线一个地址就能通信;说复杂也复杂,上拉电阻、电平匹配、时序、ACK、地址冲突,任何一个环节出错都能让你排查到怀疑人生。但掌握这套方法论之后,再遇到I2C问题,基本就是按部就班地"探测——定性——定位——解决"。希望你下次遇到I2C不工作的时候,能少走点弯路。

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

DC/DC恒压输出全链路设计:从反馈分压、环路补偿到布局布线

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

作者头像 李华
网站建设 2026/9/29 1:11:31

游戏网络同步:帧同步与状态同步的设计本质

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

作者头像 李华
网站建设 2026/9/29 1:11:31

大学生起重机创意大赛国一方案:工程化落地的三大核心技术

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

作者头像 李华
网站建设 2026/9/29 1:11:30

树莓派舵机控制:PWM原理、供电与树莓派5适配

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

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

TPshop电商测试实战:用例设计、订单状态与缺陷定位

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

作者头像 李华
网站建设 2026/9/29 1:09:55

13个开源Java小游戏项目:从入门到进阶的实战练手推荐

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

作者头像 李华