聊嵌入式通信协议I2C、I2S、SPI、UART这四位是绕不开的常客。不管是做单片机、FPGA还是驱动开发整天跟它们打交道但真要细说各自的门道、时序细节和适用场景很多人其实是一知半解的。这篇就把这四个协议放到一起拆开揉碎讲清楚每个都从物理层、时序、典型应用讲到实操时的注意事项和常见坑最后给出选择建议。内容偏向实战适合刚入门的同学建立整体认知也适合干了几年想系统梳理细节的工程师查漏补缺。1. 整体认识四条总线四种性格先给这四位定个位。如果把一个嵌入式系统比作一个公司UART像是两个员工点对点打电话一次只能两个人聊简单直接I2C像是公司内部的会议总线大家都挂在一根线上靠约定好的“工号”地址来点名发言人多了会抢线所以要有一套仲裁机制SPI像是一个领导主机给多个下属从机单独下指令用片选线点名谁被拉低谁听话速度快、效率高I2S则专门用来传音频数据流像是广播电台一个台不停放所有收音机都在收讲究的是同步和节奏。这四种协议都是串行通信也就是一根线上一位一位地传数据但它们的诞生背景和设计目标截然不同。UART诞生最早就早期的电脑和终端设备就是靠它连的全双工、异步、不用时钟线两根线搞定缺点是速度上不去、也没有多设备寻址能力。I2C是飞利浦现在的NXP为了电视内部芯片之间通信搞出来的两根线、多设备、带寻址适合板级低速控制。SPI是摩托罗拉推出的设计思路就是快四根线全双工速率能跑到几十甚至上百兆适合高速数据搬移。I2S是飞利浦为数字音频专门设计的把时钟、数据、声道选择分开保证音频流稳定连续不卡顿。一句话概括UART用来聊天I2C用来点名开会SPI用来快速发文件I2S用来播放音乐。这四位性格完全不同也就决定了它们出现在不同场合。2. I2C拆解开漏、上拉、时序与仲裁2.1 物理层的开漏设计和上拉电阻计算I2C的物理层很有特色两根线SCL和SDA都是开漏结构也就是说设备的引脚只能把线拉低不能主动拉高。线的电平全靠外部的上拉电阻提上去。这个设计的好处是多个设备直接“线与”任何一个设备拉低总线所有设备都能感知到这就为多主机仲裁和时钟同步打好了基础。上拉电阻的取值很有讲究取小了灌电流太大会把引脚灌坏取大了上升沿太慢高速模式根本跑不动。标准模式下总线电容一般是几十到上百pF选4.7kΩ通常稳快速模式400kHz就要降到2.2kΩ甚至1kΩ不然上升沿超过容许时间时序就乱了。有个简单的估算办法上升时间约等于0.847乘以电阻和总线电容的乘积标准模式要求上升时间不超过1微秒快速模式不超过300纳秒高速模式更严。我实际用过最顺手的组合是总线上设备不多时用4.7kΩ线长了或者设备多了直接换2.2kΩ100k速率下4.7k和2.2k都能跑400k还是2.2k稳妥。2.2 时序核心起始、停止、应答和数据有效性I2C的时序复杂度全在边沿的先后关系上。起始条件是SCL高电平时SDA从高拉低停止条件是SCL高电平时SDA从低拉高这两个特殊状态优先于一切普通数据位。数据位的规则是SDA只能在SCL低电平期间变化SCL高电平期间SDA必须保持稳定接收方在SCL高电平时采样。应答位是I2C最实用的设计。主机每发送完一个字节8位第9个时钟周期释放SDA从机如果想继续通信就拉低SDA表示ACK不想理你或者没收到就保持高电平表示NACK。看波形别急着数数据先找ACK/NACK这个细节基本就能判断通信成没成。比如写EEPROM的时候器件在内部写周期内不响应ACK主机收到NACK就知道正在忙可以重试或等待。2.3 时钟同步、仲裁机制和多主机复杂度I2C支持多主机但很多人没用过这个功能原因在于仲裁机制其实很巧妙。多个主机同时在SCL上发时钟由于线与特性谁想拉低时钟谁就能拉低这导致SCL的低电平时间取决于最长的那个高电平时间取决于最短的那个结果所有主机被同步到同一个时钟上。数据仲裁就更巧妙了。两个主机同时在SDA上发数据一个发高一个发低低电平会覆盖高电平发高那个主机读回总线发现电平和自己发的不一致就知道仲裁失败了立刻退出让赢家继续。这个机制保证了不丢数据但硬件实现复杂软件模拟I2C基本没法支持多主机。所以我的建议是能用一个主机就别搞多主机I2C的设计初衷是简单多主机是锦上添花不是雪中送炭。2.4 热词解析I2C扩展、自由数据模式和从机主动更新热词里的“I2C自由数据模式”其实是指I2C的SMBus/管理总线扩展特性比如PEC校验、分组传输这些在PMBus电源管理场景里很常见。PMBus和I2C的区别就在这PMBus基于I2C物理层但加了一套标准命令集和时序要求读电压读温度都有固定命令不是裸的I2C读写。“I2C从机主动更新主机寄存器”这个问题本质上是从机不能主动发起传输因为总线时钟掌握在主机手里。但从机可以在主机读取时返回最新数据或者用“通知位”机制请求主机来读。更优雅的做法是设计一个事件标志寄存器主机轮询时读到有新数据就去读完整内容。“I2C扩展”一般指用PCA9548这类多路复用器把一条总线扩成8条或者用TCA6408做GPIO扩展。选扩展方案时注意总线电平要一致带缓冲的扩展器能隔离总线电容但也会增加传输延迟。3. SPI拆解四种模式、片选与高速传输的代价3.1 四线制结构和主从模型SPI用四根线SCLK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、CS片选。主机控制SCLK和CS通信是真正的全双工主机发一个字节的同时也能收一个字节这是SPI吞吐量高的一个重要原因。SPI没有地址概念选中谁全看CS。一个主机挂多个从机有两种接法独立片选每个从机一根CS线和级联/菊花链从机之间串起来。独立片选用得最多接线稍微多点但每个从机独立工作互不干扰。菊花链适合传感器阵列这类需要依次扫描的场景少占用主机IO但延迟会叠加。3.2 四种模式详解CPOL和CPHA的排列组合SPI的四种模式由时钟极性CPOL和时钟相位CPHA决定。CPOL决定空闲时SCLK是高还是低CPHA决定数据采样是在时钟上升沿还是下降沿。四种组合对应四种模式主机和从机必须配成一样的才能通信不然读出来的全是乱码。选择模式没有绝对标准但有个经验默认先用模式0也就是CPOL0、CPHA0空闲时钟为低、第一个边沿上升沿采样数据。绝大多数从机芯片默认支持模式0特别是传感器类。一些特殊器件比如某些ADC和Flash喜欢模式3也就是CPOL1、CPHA1空闲为高、下降沿采样。FPGA驱动SPI设备时模式配置不匹配是最常见的“读回全FF/全00”原因。3.3 硬件片选与软件片选的较量片选信号这么简单的东西反而坑最多。硬件片选由SPI外设自动控制发数据前自动拉低CS发完自动拉高省心。但它有个缺点如果连续传输几百个字节中间不想断开CS比如读Flash状态寄存器硬件片选会在每笔传输之间自动拉高又拉低破坏了“连续读”的时序要求。软件片选就是自己用GPIO控制CS传输前手动拉低整批数据发完再拉高。需要连续读写传感器或存储器的场景用软件片选几乎是必须的。代价是多占一个GPIO、多写几行代码但对时序的把控自由度大得多。比如RK3588这类主控上SPI的硬件CS可能映射到特定功能脚软件片选反而更灵活。3.4 从Flash到ADCSPI的典型场景和半双工模式SPI最常见的两个场景读写Flash存储器和连接ADC/DAC。比如FPGA做高速数据采集SPI接ADC芯片采样率几M到几十M都可以FPGA端用状态机控制SCLK和片选读回来的数据直接进乒乓缓存这一套流程很成熟。热词里的“MT6701 SPI”就是磁编码器芯片读出角度值也是走SPI。SPI支持半双工模式单线双向吗标准SPI是全双工四线制但有些控制器支持半双工配置比如STM32的SPI可以配置为单线模式。这种情况一般用在一根线既发又收的场景减少引脚占用代价是通信效率和软件复杂度。我的经验除非引脚紧张到没办法否则老老实实用全双工标准SPI别拿半双工折腾自己。4. UART与I2S异步通信和音频总线的门道4.1 UART帧格式、波特率误差与电平标准UART是异步串行没有时钟线全靠收发双方事先约定好波特率每秒多少位。标准帧格式是起始位1位低电平、数据位5到8位常用8位、可选的校验位、停止位1位或2位高电平。空闲状态是高电平一旦检测到下降沿就认为是起始位开始。波特率误差是UART最要命的细节。收发双方的时钟频率不可能完全一致误差积累到一定程度就会采错位。实际工程中误差控制在2%以内基本稳1%以内更保险。比如用11.0592MHz晶振配9600波特率算下来误差几乎为0用12MHz晶振配9600就有一定误差短帧没事长帧容易出问题。STM32标准库配置UART DMA接收时波特率校准这一步千万别省。电平标准方面UART的电平定义五花八门TTL/CMOS电平3.3V/5V用在板内RS232电平±12V用在老式设备RS485/RS422是差分信号用在长距离工业总线。FT232R这类USB转UART芯片就是把USB转成TTL电平的UART信号。热词里“FT231x USB UART驱动安装失败”这类问题多半是驱动版本或者系统签名问题最直接的办法是换官方最新驱动、禁用Windows强制签名安装。4.2 阻塞、非阻塞、DMA三种接收方式的取舍UART接收方式的选择直接决定程序架构。阻塞接收就是死等串口调试最常用简单可靠但CPU全程占用不能干别的。非阻塞接收用中断每收到一个字节触发一次中断把数据放到环形缓冲区里主循环再来取这样CPU在等待串口时可以做别的事。DMA接收则是外设直接把数据搬到内存搬运完成才通知CPU适合一次接收大量数据比如用STM32标准库做UART DMA中断接收发送几百字节一帧的数据毫无压力。这三者不是谁替代谁的关系。断点调试、交互命令处理用阻塞没问题数据量中等、响应要求高用中断批量数据传输用DMA。我见过不少人在DMA上踩坑DMA接收需要定时器辅助判断帧结束因为UART本身没有帧结束标志如果数据断断续续地来DMA不知道什么时候算一帧结束。解决办法是开一个空闲中断或者用定时器超时来判断一帧结束再处理环形缓冲。4.3 I2S协议解析BCLK、WS与声道数据I2S专门为音频设计三根核心线BCLK位时钟、WS声道选择/左右时钟、SD串行数据。非标准I2S可能还需要MCLK主时钟。BCLK频率等于采样率乘以位深乘以声道数比如44.1kHz采样率、16位、双声道BCLK就是44.1k×16×21.4112MHz。WS决定声道WS低电平是左声道高电平是右声道WS的切换边沿要领先数据一位这是标准I2S的特性数据从WS切换后的第二个BCLK上升沿开始传。主从模式是配置I2S的一个关键点。主机产生BCLK和WS从机只是接收这些时钟然后收发数据。如果两个音频设备都是主机模式时钟肯定会冲突。实际项目里基本是主控当主机DAC/ADC当从机不然就要做外部时钟同步电路麻烦得很。另一个容易忽视的是MCLK很多音频芯片要求MCLK是BCLK的整数倍通常是256×采样率用来驱动内部滤波器和时钟整形有些控制器的I2S外设需要额外配置一个MCLK输出引脚不配上芯片就不工作。4.4 I2S在FPGA和主控平台的应用思路FPGA做I2S接口很常见比如做音频采集、音频处理FPGA开发板。FPGA内部生成BCLK和WS数据从SD线进来后按声道分别存进FIFO再交给后续的滤波或FFT模块。逻辑分析仪看I2S波形时先看WS对齐的位置再数BCLK个数确认位宽最后看SD数据是否在BCLK的边沿稳定基本就能锁定时序问题。RK3588这类Linux主控平台本身就集成了I2S控制器设备树里配置好时钟和引脚mux就行但要注意时钟源的选择音频对抖动敏感用片内PLL产生的时钟一般比外部晶振差一些需要实测信噪比。5. 横向对比与选型思路把四个协议摆一起看差异会更清楚对比项UARTI2CSPII2S信号线数2TX/RX2SCL/SDA4SCLK/MOSI/MISO/CS3~4BCLK/WS/SD/MCLK拓扑结构点对点多设备总线带地址一主多从片选一主多从WS广播速度范围常见≤1Mbps标准100k/快速400k/高速3.4M最高几十~上百Mbps取决于音频采样率CRUCIAL时钟线无异步有同步有同步有同步全双工是否半双工是是数据独立多设备支持不支持支持寻址7位/10位支持片选扩展支持多从机WS共享抗干扰差需电平标准转换中开漏弱驱动中推挽但易受干扰中音频专用软件复杂度低中需处理时序/仲裁中需处理模式/片选中需管理时钟/左右声道典型场景调试、GPS、蓝牙模块传感器、EEPROM、PMBusFlash、ADC/DAC、LCD屏音频Codec、I2S麦克风选型的时候其实不用纠结太久。低速传感器、小容量存储、板级通信I2C是默认选项两根线、多设备挂靠、软件模拟也容易。要读大容量Flash、高速ADC、LCD显存这类数据吞吐要求高的SPI是唯一合理选择。点对点连接、速度要求不高、两边都是独立模块直接UART最省事逻辑分析仪抓一个起始位就能看数据调试起来不要太方便。音频相关只要涉及数字音频流I2S几乎是唯一标准选项。一句话控制用I2C数据搬运用SPI点对点用UART听歌用I2S。6. 逻辑分析仪实测四种协议的波形解读与Python验证6.1 抓波形看时序肉眼判断问题逻辑分析仪在调试通信协议时是刚需。先说I2C波形怎么读正常通信时SCL和SDA都是高电平先看到SCL高时SDA拉低起始然后SCL开始周期性出现方波SDA在SCL低电平区间切换数据。停止条件是SCL高时SDA从低拉高。如果看到SDA一直被拉低不放多半是从机没回应比如地址不对、从机没上电、或者从机在忙。还有一种常见情况SDA波形有明显毛刺或上升沿很斜这就是上拉电阻太大或者总线电容过大的表现。SPI的波形解读先看CS。每次传输CS先拉低然后SCLK开始振荡。一个字节8个SCLK边沿。看MOSI和MISO的电平变化是否发生在SCLK的采样边沿之前。如果读数全零或全FF先检查CPOL/CPHA是否匹配再看CS时序很多传感器在CS拉低后需要一段延时tSUCS才能开始传输太短了第一个字节就读不对。UART波形最简单空闲高电平然后一个明显的下降沿起始位接着8个数据位最后停止位拉高。UART没有时钟线所以逻辑分析仪依靠波特率来采样。把波特率配错波形就会看起来“乱跳”根本没法解析。逻辑分析仪软件一般会自动识别波特率但偶发识别错误的手动指定最稳。6.2 Python模拟SPI主机读取Sensor/ADC示例热词里提到“Python调用USB模拟SPI接口”这个实际项目里真能用上。比如你手头有个USB转SPI的适配器或者直接用FT232H这类芯片通过pyftdi库在Python里就能模拟SPI主机去读传感器。下面的代码展示怎么读一个ADC芯片的通道数据from pyftdi.spi import SpiController # 初始化USB转SPI控制器 spi SpiController() spi.configure(ftdi://ftdi:232h/1) # 挂载SPI从设备CS接D4引脚速率1MHz slave spi.get_port(cs0, freq1000000, mode0) # 构造读命令读通道0增益为1具体命令格式见ADC芯片手册 cmd bytes([0x01, 0x80, 0x00]) response slave.exchange(cmd, 3) # 发3字节收3字节 # 解析24位数据这里是举例实际按芯片手册来 raw (response[0] 16) | (response[1] 8) | response[2] voltage raw * 4.096 / 0x7FFFFF print(ADC通道0电压: {:.3f}V.format(voltage))这段代码里exchange函数干了SPI全双工读写的事发3个字节的同时也收3个字节。用Python调USB模拟SPI接口最大的价值就是快速验证硬件时序和数据格式不用反复烧固件。在实际项目里我经常用这个方式在PC端先调通传感器协议再把验证过的时序逻辑翻译成C语言固件能省不少调试时间。6.3 用Python解析UART波形和I2C波形处理完SPI再看看怎么用Python抓录的波形数据还原出协议帧。逻辑分析仪导出的CSV文件通常包含每根线的电平变化时间戳解析思路就是找边沿、判断电平状态、按协议规则组帧。比如UART找到下降沿起始位置按波特率换算每位时间采样每个位间隔的电平拼出8位数据。I2C解析也同理先找SDA在SCL高电平时的变化来识别起始/停止再在SCL高电平期间采样SDA遇到第9个时钟处理ACK。这个思路在调试协议栈时特别好用因为你能完全按手册逻辑重新实现一遍比对芯片到底发了什么。Python写这些解析脚本其实不复杂核心就是三个函数找边沿、按时间采样、按协议组装成帧。关键是弄明白协议的状态机转换I2C的起始/停止/数据位用状态机描述特别直观UART的起始位/数据位/停止位也是一个状态序列。做完这套解析你对协议的理解会比看十遍手册都深。7. 实战避坑总结那些让人半夜抓狂的问题7.1 I2C类问题GT911触控芯片I2C通信失败是很多做屏幕项目的人都会遇到的。其实排查方向很固定地址对不对GT911有个地址引脚接法不同地址不同0x5D/0x14都常见复位时序够不够触控IC上电后需要至少几十毫秒稳定内部固件没跑完就发I2C命令当然失败中断脚有没有被占用GT911的INT脚在某种配置下影响I2C通信地址。“I2C HID该设备找不到足够资源可以使用。代码12”这种Windows报错一般是I2C控制器驱动冲突或者BIOS里的I2C设备没正确枚举多见于触控板和传感器设备。解决办法是去设备管理器删掉隐藏设备重装芯片组驱动或者在BIOS里关闭冲突的I2C控制器。I2C上拉电阻的问题也特别普遍。上拉电阻不是随便选的要根据总线设备数量和线长综合决定。有一次项目里测I2C偶尔通信失败用示波器一看上升沿斜得像山坡算了下总线电容有300pF4.7kΩ电阻根本带不动换成1kΩ立刻好了。这个案例后来被我写进团队的设计规范里新项目I2C一律用2.2kΩ起步。7.2 SPI类问题SPI读Flash全FFSPI读传感器全FF这两个是论坛日经问题。全FF意味着MISO线上一直是高电平可能是从机根本没响应没上电、CS没拉对、模式不对也可能是在连续读的时候CS被中途拉高了硬件片选导致。排查顺序示波器看CS是否正常拉低且保持、量从机供电、确认模式、最后怀疑时序配置。STM32 CubMX配置SPI DMA时容易忽略一件事DMA的中断优先级和外设DMA请求的触发条件。SPI DMA发送一般没问题但DMA接收容易丢数据。原因是RXNE接收缓冲区非空事件和DMA请求之间有极短的竞争窗口如果DMA响应不够快数据就丢了。解决办法是保证DMA优先级足够高同时开启SPI的CRC/ERR中断兜底。这个问题在STM32F4/F7上都存在网上吐槽很多改优先级是最快的解法。7.3 UART类问题UART阻塞和非阻塞的争议其实没有绝对对错核心看场景。我之前做一个串口屏项目界面刷新和数据解析都在同一个线程用阻塞接收导致屏幕卡顿改成中断接收环形缓冲后解决。但反过来如果只是板子间传个简单的控制命令阻塞接收反而是最简单的代码短不容易出并发问题。DMA接收帧判断问题上面提过再补一个细节做好DMA超时处理防止半包卡死缓冲区。我用的一个方案是DMA接收每收到一个字节就重置定时器定时器溢出表示一帧接收完成然后把缓冲区的数据交给上层处理。这个方案在STM32F103上配合标准库实现很稳定也容易移植。UART波形抓来解析时还有一个常见误区逻辑分析仪采样率要设成波特率的10倍以上不然起始位的边沿定位不准位采样会偏。MT6701 SPI这类磁编码器还有自己的特点数据手册上的时序要求比较严格CS拉低到SCLK首个上升沿之间有个最小建立时间tSUCS通常是100ns左右SPI主机频率低的时候没问题频率上到10M以上就容易被忽略导致高位数据错误。遇到SPI高速时数据偏差的情况先查手册上tSUCS、tHOLD这些参数用软件延时补一下比改驱动快得多。7.4 选型与后续扩展建议在这个项目基础上可以继续扩展的方向不少。如果对协议底层感兴趣可以用FPGA自己实现一个I2C主机或SPI从机把时序图变成RTL代码理解会完全不一样。如果想深入音频I2S这块可以做音频Loopback测试用逻辑分析仪抓BCLK和WS验证采样率是否精确顺便学习DPLL锁相环在音频时钟恢复中的角色。如果想搞系统级开发Linux下i2c-dev接口、spidev接口的设备树配置也是一大块内容RK3588这类平台默认支持的SPI/I2C控制器配置方式都值得一试。我个人在实际项目里的体会是不要指望靠死记手册来选择协议多动手抓波形、多看时序关系遇到问题先用逻辑分析仪定性再用示波器定量最后回到数据手册核对参数。协议本身不复杂复杂的是无数细节堆积起来后的状态判断。希望这篇对比能帮你少踩几个坑把这四条总线用得明明白白。
企业数字化 ERP 产品动态
相关推荐
SpringBoot宠物领养系统毕业设计实战:从环境搭建到核心功能实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:43:11
展讯SPRD平台AT指令完全指南:生态、命令解析与实战排障 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 4:43:11
天猫复购预测实战:从特征工程到模型调优的完整指南 简介:本资源为阿里天池大赛学习赛「天猫复购预测」的完整案例包,面向计算机、人工智能、通信工程等专业的在校学生、教师及企业员工,尤其适合作为毕业设计、课程设计、作业或项目初期立项演示的参考素材。包内共7个文件,以3个csv数… · 2026/9/25 4:43:11
HTTP协议头URL完全拆解:从编码规则到502故障排查实战 干这行久了,你会发现一件挺反直觉的事:越基础的东西,越容易在关键时刻坑人。就拿URL来说,浏览器地址栏里那串以http开头的字符,我们每天敲、每天看、每天传,可真到排查问题的时候,有多少人能一口… · 2026/9/25 5:56:28
自部署CRM系统实战:从服务器环境搭建到客户数据安全迁移 1. 为什么我会在2024年把公司客户管理切换到DeskcommCRM做客户管理这件事,我前前后后换了不下五套方案。最早用Excel表,客户一多就乱,业务员各填各的,连“最近跟进时间”都能填出三种格式。后来用过在线表格协作,虽然实… · 2026/9/25 5:56:28
2KB限制下,域名停放页的压缩与DNS配置实战 说到域名停放,很多人的第一反应是“随便买个域名、挂个页面,等别人点击就行”。但等你真去操作,就会发现事情没那么简单:平台对页面体积有要求、域名解析要等生效、页面写轻了没信息量、写重了又超限。这些年我在闲置域名上折腾过… · 2026/9/25 5:56:22
Oracle EBS R12表结构实战:数据字典、常用表关联与导出脚本 简介:Oracle EBS R12表结构资料是为ERP实施顾问、开发人员和数据库管理员准备的一套系统性参考文档,旨在帮助读者快速定位各业务模块的核心数据表,理清表与表之间的关联逻辑,可直接用于日常维护、问题排查、二次开发及数据迁移规划… · 2026/9/25 5:56:22
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37