news 2026/9/30 1:14:12

OpenHarmony I2C开发实战:从协议原理到HDI排障全链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenHarmony I2C开发实战:从协议原理到HDI排障全链路

I2C 这东西,说简单也简单,两根线一挂,读写寄存器就完事了;说难也真难,一旦总线上某个器件抽风,或者时序差那么一点点,你盯着逻辑分析仪能看一整天。我在 OpenHarmony 上做外设适配的这几年,I2C 相关的调试占了相当大一块时间,从最早的 RK3568 到后来的多款开发板,踩过的坑能写满一个笔记本。这篇就把 I2C 在 OpenHarmony 下的完整使用链路和排障思路捋一遍,从协议本质讲到设备树配置,再到 HDI 接口调用和真实故障定位,尽量让刚上手的人少走弯路,也让已经能跑通但遇到偶发问题的人有个系统的排查框架。

1. 先把 I2C 的物理层和协议层吃透

很多人调 I2C 出问题,根子不在代码,而在对协议本身的理解有偏差。所以这一节先把基础打牢,后面排障才有依据。

1.1 两根线到底怎么传数据

I2C 全称是 Inter-Integrated Circuit,物理上就两根线:SDA(串行数据线)和SCL(串行时钟线)。两根线都是开漏输出,必须外接上拉电阻,这一点是理解所有 I2C 电气问题的起点。开漏意味着任何设备只能把线拉低,不能主动拉高,高电平全靠上拉电阻把线拽上去。这就解释了为什么总线上任何一个器件出问题,整条总线都可能被拉死——因为它只要一直把 SDA 或 SCL 拉低不放,别人就没法通信。

上拉电阻的取值是个经典问题。常见值在 2.2k 到 10k 之间,取值大小直接影响信号上升沿的陡峭程度。电阻越小,上升越快,但静态功耗越大;电阻越大,上升越慢,高速通信时波形可能还没到高电平就被下一个时钟沿打断了。经验上,100kHz 标准模式用 4.7k 到 10k 都行,400kHz 快速模式建议 2.2k 到 4.7k,再高速度就要具体看总线电容了。总线电容一般要求不超过 400pF,挂的设备越多、走线越长,电容越大,上升沿越缓。

提示:如果你用示波器看 SCL 或 SDA,发现上升沿是个明显的圆弧而不是陡峭的斜坡,基本就是上拉电阻偏大或者总线电容偏大,先从这里查。

1.2 起始、停止、应答:三个必须刻在脑子里的时序

I2C 通信的骨架就是几个关键时序信号,理解了它们,看逻辑分析仪的波形就一目了然。

起始条件(Start):SCL 保持高电平时,SDA 从高变低。这个"在时钟高电平期间拉低数据线"的动作是 I2C 独有的,用来告诉总线上所有设备"注意,要开始通信了"。

停止条件(Stop):SCL 保持高电平时,SDA 从低变高。通信结束的标志。

应答(ACK/NACK):每传输 8 位数据后,第 9 个时钟周期,接收方要把 SDA 拉低表示"收到了"(ACK),或者保持高电平表示"没收到或结束"(NACK)。主机读数据时,最后一个字节通常由主机发 NACK,告诉从机"我不再读了"。

数据位的传输规则是:SCL 低电平期间 SDA 可以变化,SCL 高电平期间 SDA 必须保持稳定。这条规则是判断波形是否正常的关键——如果你在 SCL 高电平期间看到 SDA 跳变,那要么是起始/停止条件,要么就是时序错乱。

1.3 7 位地址和读写位

标准 I2C 用 7 位地址,所以理论上最多 128 个地址,但有一部分是保留地址,实际可用的大概 112 个。地址后面跟一位读写标志:0 表示写,1 表示读。所以你在代码里看到的"设备地址"经常是 8 位的,比如某个器件写地址是 0x50,读地址就是 0x51,其实 7 位地址是 0x28。

这里有个特别容易踩的坑:不同厂商的数据手册给的地址格式不统一。有的直接给 7 位地址,有的给的是已经左移一位的 8 位地址。你在 OpenHarmony 里配置设备树或者调用 HDI 接口时,一定要确认用的是 7 位地址还是 8 位地址,搞错了就是死活读不到数据,而且逻辑分析仪上能看到地址字节发出去但没人应答。

地址类型示例值说明
7 位地址0x28协议层实际使用的地址
8 位写地址0x507 位地址左移一位,最低位为 0
8 位读地址0x517 位地址左移一位,最低位为 1

1.4 时钟拉伸:从机也会"踩刹车"

时钟拉伸(Clock Stretching)是 I2C 里一个容易被忽略但很关键的机制。从机如果处理不过来,可以在应答之后把 SCL 拉低不放,强制主机等待,直到从机准备好才释放 SCL。这是 I2C 相比 SPI 的一个优势——从机有反压能力。

但问题在于,不是所有主机控制器都支持时钟拉伸。有些硬件 I2C 控制器遇到从机拉低 SCL 会直接报超时错误。如果你在 OpenHarmony 上遇到某个器件偶尔读失败,而逻辑分析仪显示 SCL 被从机拉低了一段时间,那就要考虑是不是控制器不支持时钟拉伸,或者超时时间设得太短。

2. OpenHarmony 下 I2C 的软件栈长什么样

搞清楚协议之后,得知道 OpenHarmony 是怎么把 I2C 抽象出来的。这套软件栈从下到上分了好几层,每一层出问题的表现都不一样。

2.1 从内核驱动到 HDI 的分层结构

OpenHarmony 的 I2C 软件栈大致是这样的:

最底层是SoC 的 I2C 控制器驱动,这部分通常在内核态,负责操作具体的寄存器,产生时序波形。RK3568 这类芯片的 I2C 控制器驱动一般由芯片厂商提供,集成在内核里。

往上一层是I2C 核心层(i2c-core),提供统一的注册、匹配、传输接口。设备树里的 I2C 设备就是通过这一层和驱动匹配上的。

再往上是HDI(Hardware Device Interface)层,这是 OpenHarmony 特有的硬件抽象层。HDI 定义了一套标准的 I2C 访问接口,上层的系统服务和应用通过 HDI 来读写 I2C 设备,不用关心底层是哪个 SoC。

最上面是具体的设备驱动或系统服务,比如某个传感器服务、显示服务,它们调用 HDI 接口完成实际的数据交互。

这个分层的好处是上层代码和硬件解耦,但代价是一旦出问题,你得知道是哪一层的问题。我的经验是:先看波形,再看内核日志,最后查 HDI 调用,这个顺序能帮你快速定位问题在哪一层。

2.2 设备树里 I2C 节点怎么写

在 OpenHarmony 里,I2C 设备的描述主要靠设备树。一个典型的 I2C 控制器节点和挂载设备节点大概长这样:

i2c3: i2c@fe5c0000 { compatible = "rockchip,rk3568-i2c"; reg = <0x0 0xfe5c0000 0x0 0x1000>; interrupts = <GIC_SPI 79 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_I2C3>, <&cru PCLK_I2C3>; clock-names = "i2c", "pclk"; pinctrl-names = "default"; pinctrl-0 = <&i2c3m0_xfer>; #address-cells = <1>; #size-cells = <0>; status = "okay"; gt911: touchscreen@5d { compatible = "goodix,gt911"; reg = <0x5d>; interrupt-parent = <&gpio0>; interrupts = <RK_PB5 IRQ_TYPE_EDGE_FALLING>; reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>; status = "okay"; }; };

这里有几个关键点值得展开说。

reg 属性填的是设备的 7 位地址。上面例子里的 0x5d 就是 GT911 触摸屏的 I2C 地址。注意这里填的是 7 位地址,不是左移后的 8 位地址。

pinctrl配置的是引脚复用。I2C 的 SDA 和 SCL 引脚在很多 SoC 上是复用的,可能同时能当 GPIO 或别的功能用。pinctrl 没配对,引脚就不是 I2C 功能,自然通信不了。RK3568 的 I2C3 可能有 m0、m1 等多组引脚可选,选错了就是波形都出不来。

status必须是 "okay",默认可能是 "disabled"。这个坑很隐蔽,设备树写得好好的,就是没反应,一查 status 是 disabled。

中断和复位引脚对于触摸屏这类设备是必须的。GT911 上电后需要通过复位引脚和地址选择引脚来确定 I2C 地址,如果复位时序不对,它可能用了一个和你配置不一样的地址,结果就是地址发出去没人应答。

2.3 HDI 接口的调用逻辑

OpenHarmony 的 HDI 层为 I2C 提供了标准接口,核心的几个函数包括初始化、读写、以及带寄存器地址的读写。上层调用的一般流程是:

  1. 通过I2cOpen打开一个 I2C 控制器,拿到句柄。
  2. 用I2cTransfer或封装好的读写函数进行数据传输。
  3. 用完调用I2cClose关闭。

带寄存器地址的读写是最常见的场景,比如读一个传感器的某个寄存器。这种操作实际上是两次传输的组合:先写寄存器地址(写操作),再读数据(读操作),中间有一个重复起始条件(Repeated Start),而不是停止再起始。这个细节很重要,因为有些器件不支持在停止后重新起始,必须用重复起始。

// 伪代码示意,实际接口名以 OpenHarmony 版本为准 DevHandle handle = I2cOpen(3); // 打开 I2C3 uint8_t regAddr = 0x00; uint8_t data[2] = {0}; struct I2cMsg msgs[2]; msgs[0].addr = 0x5d; msgs[0].buf = &regAddr; msgs[0].len = 1; msgs[0].flags = 0; // 写 msgs[1].addr = 0x5d; msgs[1].buf = data; msgs[1].len = 2; msgs[1].flags = I2C_FLAG_READ; // 读 I2cTransfer(handle, msgs, 2); I2cClose(handle);

这种"写寄存器地址 + 读数据"的组合传输,是 I2C 里最典型的操作模式。理解它的关键在于:两次传输之间是重复起始,不是停止。如果你用两个独立的传输函数分别调用,中间会产生停止条件,某些器件就会出问题。

3. 从零跑通一个 I2C 设备的完整流程

理论讲完了,接下来是实操。我以一个典型的 I2C 传感器为例,把从硬件连接到软件验证的完整流程走一遍。

3.1 硬件连接和上拉电阻的确认

第一步永远是硬件。I2C 设备接线就四根:VCC、GND、SDA、SCL。但有几个细节必须确认。

供电电压。很多 I2C 器件是 3.3V 供电,但也有一些是 1.8V 或 5V。如果器件供电和 SoC 的 IO 电平不一致,要么通信不了,要么长期运行会损坏 IO。电平不匹配时需要电平转换芯片。

上拉电阻的位置。上拉电阻应该接在总线靠近主机的一侧,或者均匀分布。如果每个从机模块上都自带 10k 上拉,挂了五六个设备之后等效上拉就变成 2k 左右了,这时候上升沿会变得很陡,可能引起过冲和振铃。反过来,如果所有设备都不带上拉,总线就永远是低电平,完全没法通信。

地址冲突。挂多个同型号器件时,要确认它们的地址是否可以通过硬件引脚区分。比如有些传感器提供 ADDR 引脚,接高接低对应不同地址。如果两个器件地址一样,总线上就会打架。

注意:我曾经遇到过一个案例,开发板上已经焊了一个 I2C 设备,用户又外接了一个同地址的模块,结果两个设备同时应答,读回来的数据时对时错。这种问题逻辑分析仪上能看到应答位有异常,但不容易一眼看出是两个设备在抢答。

3.2 设备树配置的逐项检查

硬件确认没问题后,配置设备树。我习惯按这个顺序逐项检查:

  1. 控制器节点 status 是否为 okay。这是最基础的,但经常被忽略。
  2. pinctrl 是否正确。确认引脚复用配置和实际硬件走线一致。
  3. 时钟频率。设备树里通常可以配 clock-frequency,默认可能是 100kHz,如果器件支持 400kHz 可以改。但要注意,改频率之前先确认上拉电阻和总线电容能支持。
  4. 设备子节点的 reg 地址。确认是 7 位地址,且和器件实际地址一致。
  5. 中断和复位 GPIO。对于需要中断的设备,中断配置不对会导致数据读不到或者读到了但收不到中断通知。

设备树改完之后,重新编译内核或设备树,烧录,然后看内核启动日志里有没有 I2C 相关的报错。常见的报错有 "i2c i2c-3: bus not busy"、"timeout waiting for bus ready" 等,这些后面排障章节会详细讲。

3.3 用 i2c-tools 做第一轮验证

在正式写驱动之前,我强烈建议先用 i2c-tools 做一轮验证。OpenHarmony 的调试版本通常可以集成 i2c-tools,或者你在内核态用 i2c 的 sysfs 接口也能做类似的事。

最常用的命令是i2cdetect,它可以扫描总线上有哪些地址有设备应答:

i2cdetect -y 3

如果某个地址显示为数字,说明那个地址有设备应答;显示为--说明没设备;显示为UU说明该地址已被驱动占用。

这一步能快速确认三件事:总线本身是否工作、设备地址是否正确、设备是否正常上电。如果 i2cdetect 扫不到任何设备,那问题一定在硬件或控制器配置层面,不用往下查驱动了。

扫到设备之后,可以用i2cget和i2cset读写寄存器:

i2cget -y 3 0x5d 0x00 # 读 0x5d 设备的 0x00 寄存器 i2cset -y 3 0x5d 0x01 0x80 # 写 0x5d 设备的 0x01 寄存器为 0x80

如果这两个命令能正常工作,说明硬件和控制器都没问题,接下来就是驱动和 HDI 层的事了。

3.4 逻辑分析仪抓波形的正确姿势

当 i2c-tools 都不工作时,逻辑分析仪就是你的眼睛。抓 I2C 波形有几个要点:

采样率要够。I2C 400kHz 的话,采样率至少 4MHz 以上,建议 10MHz 或更高,否则波形细节看不清。

触发条件设对。可以设 SDA 下降沿触发(起始条件),或者设某个特定地址触发。如果总线一直没动静,用起始条件触发能抓到第一次通信。

看三个东西:起始条件是否正常、地址字节发出去后有没有 ACK、数据字节的时序是否符合规范。如果地址发出去没有 ACK,说明从机没响应,可能是地址错、供电问题、或者从机没准备好。如果有 ACK 但数据不对,可能是寄存器地址错或者器件工作模式不对。

我一般会把逻辑分析仪的协议解码功能打开,直接看解码后的字节流,比数波形快得多。但解码功能偶尔会误判,所以关键时候还是要人工核对波形。

4. I2C 排障:那些让你抓狂的典型故障

这一节是重点。我把这些年遇到的 I2C 故障归了几类,每一类都给出排查链路,你可以对照着复现排查思路。

4.1 总线被拉死:SDA 或 SCL 一直是低电平

这是最经典的 I2C 故障。表现是 i2cdetect 扫不到任何设备,示波器一看 SDA 或 SCL 被死死拉在低电平。

根本原因:某个从机在通信过程中异常复位或掉电,导致它的 I2C 状态机卡在某个中间状态,一直把数据线拉低不放。因为 I2C 是开漏的,只要有一个设备拉低,整条线就是低。

排查链路:

  1. 先断电,用万用表测 SDA 和 SCL 对地的电阻。如果某个线对地电阻很小,说明有设备短路或者被拉死。
  2. 逐个断开从机,看总线是否恢复。这是最笨但最有效的方法。
  3. 如果确认是某个设备拉死,检查它的供电和复位时序。很多设备在上电过程中如果复位不完整,会进入异常状态。

恢复方法:主机可以发送 9 个时钟脉冲,让从机把剩余的数据位发完,然后发一个停止条件,强制从机复位状态机。具体做法是把 SCL 配置成 GPIO,手动翻转 9 次,然后发停止条件。OpenHarmony 下如果控制器驱动支持总线恢复(bus recovery),可以在设备树里配置恢复引脚,内核会自动处理。

i2c3: i2c@fe5c0000 { ... pinctrl-names = "default", "gpio"; pinctrl-0 = <&i2c3m0_xfer>; pinctrl-1 = <&i2c3m0_gpio>; scl-gpios = <&gpio0 RK_PB1 GPIO_ACTIVE_HIGH>; sda-gpios = <&gpio0 RK_PB2 GPIO_ACTIVE_HIGH>; };

配置了 gpio 恢复之后,内核在检测到总线超时时会尝试用 GPIO 模拟时钟脉冲来恢复总线。

4.2 地址无应答:设备明明在,就是不应

i2cdetect 扫不到设备,但设备供电正常、接线也对。这种情况我遇到过好几次,原因各不相同。

地址格式错误是最常见的。前面说过,7 位地址和 8 位地址差一位。如果你在设备树里填了 8 位地址,实际发出去的就是错误的地址,自然没人应答。解决办法是查数据手册确认地址格式,或者用 i2cdetect 扫描整个地址空间,看设备实际在哪个地址应答。

复位时序问题。像 GT911 这类触摸屏,上电后需要主机通过复位引脚和地址选择引脚给它一个特定的时序,它才会用预期的地址启动。如果复位时序不对,它可能用了默认地址或者根本没启动。我遇到过一次 GT911 通信失败,最后发现是复位引脚的 GPIO 配置成了开漏但没有上拉,导致复位信号一直是低,芯片根本没起来。

供电时序问题。有些设备要求 IO 电压先于核心电压建立,或者反过来。如果时序不对,设备可能处于不确定状态。这种问题比较隐蔽,需要查数据手册的 power sequence 章节。

设备处于休眠状态。有些低功耗设备默认处于休眠,需要先发一个唤醒命令或者拉高某个引脚才能通信。ESP32 作为 I2C 从机时就有这个问题,休眠后 I2C 控制器复位,主机需要重新初始化。

4.3 数据时对时错:偶发性通信失败

这类问题最折磨人,因为它不是一直失败,而是偶尔失败。可能跑几个小时才出一次,也可能温度变化时才出。

时钟拉伸导致的超时。前面讲过,从机拉低 SCL 要求主机等待。如果主机控制器的超时时间设得太短,从机还没准备好就超时了,这次传输就失败了。解决办法是查控制器驱动里的超时配置,适当加大。但要注意,超时太大也会导致真正故障时恢复太慢。

总线电容过大导致上升沿变缓。挂的设备多、走线长,总线电容超过 400pF,上升沿变缓,高速通信时数据采样出错。用示波器看上升时间,如果超过 I2C 规范要求的值,就要减小上拉电阻或者降低通信速率。

电源噪声。I2C 器件的供电如果有噪声,可能导致内部状态机偶发错误。可以在电源引脚附近加去耦电容,一般 0.1uF 加 10uF 的组合比较常见。

中断竞争。如果 I2C 传输是在中断上下文里做的,而系统中断负载很重,可能导致传输被延迟,超过从机的超时时间。这种情况要考虑把 I2C 传输放到线程上下文,或者提高中断优先级。

排查这类问题,我的经验是:先降速。把 I2C 时钟从 400kHz 降到 100kHz,如果问题消失,基本可以确定是时序余量不够,再针对性优化。如果降速后还出问题,那可能是软件逻辑或者电源问题。

4.4 HDI 层调用返回错误但波形正常

这是一种让人困惑的情况:逻辑分析仪上看波形完全正常,地址有 ACK,数据也读回来了,但 HDI 接口返回错误。

参数配置错误。HDI 接口的 msg 结构体里,flags 字段没设对,比如读操作没设 I2C_FLAG_READ,或者地址填成了 8 位。这种问题波形上可能看不出来,因为地址字节可能碰巧对上了,但数据方向错了。

缓冲区长度不对。len 字段设得比实际需要的大或小,导致读回来的数据不完整或者越界。这种问题有时候波形正常但返回错误。

并发访问冲突。多个线程同时访问同一个 I2C 控制器,没有加锁,导致传输交错。表现是偶发的数据错乱。解决办法是在 HDI 层或者驱动层加互斥锁。

控制器状态未复位。上一次传输失败后,控制器状态机没有正确复位,下一次传输直接返回错误。这种情况需要检查驱动里的错误处理逻辑,确保每次失败后都正确复位控制器。

排查这类问题,关键是在内核日志里找线索。OpenHarmony 的内核日志通常会打印 I2C 传输失败的详细信息,包括错误码和失败原因。结合波形和日志,基本能定位到具体是哪一层的问题。

5. 几个容易被忽略的进阶话题

基础的东西讲完了,再聊几个进阶但很实用的点。

5.1 I2C 多路复用和地址扩展

当你要挂很多同地址的设备时,I2C 多路复用器(比如 TCA9548A)就派上用场了。它本身是一个 I2C 设备,通过写它的寄存器来选择哪一路通道导通,从而实现地址扩展。

在 OpenHarmony 下使用多路复用器,需要在设备树里把复用器作为 I2C 控制器的子节点,然后把实际设备挂在复用器的子节点下。内核的 i2c-mux 框架会自动处理通道切换。但要注意,每次访问不同通道的设备时,内核会先切换通道,这个切换本身也是一次 I2C 传输,会增加延迟。

5.2 软件 I2C 和硬件 I2C 的取舍

有些场景下硬件 I2C 控制器不够用,或者引脚被占用了,就需要用 GPIO 模拟 I2C(软件 I2C)。OpenHarmony 内核支持 i2c-gpio 驱动,在设备树里配置两个 GPIO 作为 SDA 和 SCL 就行。

软件 I2C 的优点是灵活,任何 GPIO 都能用;缺点是占用 CPU,速率上不去,而且时序精度依赖 GPIO 翻转速度。一般用在低速、非关键的场合。如果对速率有要求,还是优先用硬件 I2C。

5.3 I2C 和 SMBus 的区别

SMBus 是 I2C 的一个子集,主要用于电源管理和系统监控。它比 I2C 多了超时机制,时钟频率固定在 10kHz 到 100kHz。很多电源管理芯片用的是 SMBus 而不是标准 I2C。在 OpenHarmony 下,SMBus 设备通常也能用 I2C 控制器驱动,但要注意超时配置,因为 SMBus 要求从机在一定时间内响应,超时了会报错。

5.4 逻辑分析仪之外:用内核 trace 看 I2C 传输

除了逻辑分析仪,Linux 内核的 ftrace 也能用来跟踪 I2C 传输。打开 i2c 相关的 tracepoint,可以看到每次传输的地址、方向、长度和返回值。这对于分析偶发问题特别有用,因为 trace 是持续记录的,不像逻辑分析仪需要一直挂着。

echo 1 > /sys/kernel/debug/tracing/events/i2c/enable cat /sys/kernel/debug/tracing/trace_pipe

这个输出能告诉你每次 I2C 传输的详细信息,结合时间戳,可以分析出传输的间隔和频率,判断是否有异常。

6. 我在实际项目里总结的几条经验

最后分享几条踩坑踩出来的经验,都是文档里不会写的。

第一条:先怀疑硬件,再怀疑软件。I2C 问题里硬件原因占了一大半,尤其是接线、上拉、供电这些。我见过太多人一上来就改代码,结果查了半天发现是杜邦线接触不良。

第二条:i2cdetect 是你的第一道防线。不管什么问题,先跑 i2cdetect。扫不到设备就查硬件,扫到了但读写失败就查寄存器和协议,能省很多时间。

第三条:降速是万能的排查手段。遇到偶发问题,先把 I2C 速率降到 100kHz 甚至更低。如果问题消失,说明是时序余量问题;如果还在,说明是逻辑或电源问题。这个二分法很有效。

第四条:设备树的 pinctrl 一定要反复确认。引脚复用配错是新手最常犯的错误,而且症状是完全没有波形,容易误判为硬件坏了。

第五条:保留一份能工作的最小配置。每次调新设备时,先在一个已知能工作的配置基础上改,而不是从零写。这样出问题时可以对比差异,快速定位。

第六条:逻辑分析仪和内核日志要结合看。波形告诉你物理层发生了什么,日志告诉你软件层怎么想的。两者结合,基本没有定位不了的问题。

I2C 这个协议本身不复杂,但实际项目里的问题往往出在细节上。把协议吃透,把工具用熟,把排查链路理顺,大部分问题都能在半小时内定位。真正难的是那些偶发的、和电源、温度、时序余量相关的问题,这类问题需要耐心和系统的排查方法。希望这篇内容能帮你在 OpenHarmony 的 I2C 开发里少踩几个坑。

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

Visual C++与HGE引擎:超级玛丽源码解析与游戏改造指南

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

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

嵌入式Linux系统开发21天速成:从U-Boot到驱动的完整学习路线

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

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

Linux VFS 四大对象与 open/read 流程详解及内核模块拦截实验

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

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

汤小丹《计算机操作系统》课后题全解析:从进程调度到页面置换

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

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

PLC现场调试实战:从IO点表到PID调节的14条生存法则

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

作者头像 李华