news 2026/10/23 3:54:04

嵌入式开发Bug排查实战:从在线调试到硬件辅助的七种方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式开发Bug排查实战:从在线调试到硬件辅助的七种方法

嵌入式开发这行干久了,你会发现一个很有意思的现象:写代码的时间可能只占三成,剩下七成都在跟Bug斗智斗勇。尤其是那种“偶发、难复现、一上电就抽风”的问题,能让人从下午三点一直查到凌晨三点。我见过太多同行,遇到问题第一反应就是加打印,结果串口输出刷得比心跳还快,问题没定位到,反而把实时性给拖垮了。这篇文章就围绕嵌入式软件开发中排查Bug的几种实战方法展开,把我在实际项目中反复验证过的思路、工具和踩坑经验一次性讲透。不管你是刚入行的新手,还是已经能独立带项目的熟手,这些方法都能直接拿去用,帮你少熬几个通宵。

1. 排查思路的底层逻辑与方案选型

1.1 先搞清楚Bug的几种“性格”

嵌入式系统里的Bug跟纯软件Bug有个本质区别:它跑在真实的物理世界里,受电压、温度、时序、电磁干扰的影响。所以排查之前,先给Bug分个类,能省掉大量无用功。

我习惯把嵌入式Bug分成四类。第一类是必现逻辑错误,比如数组越界、指针未初始化、状态机跳转条件写反,这类问题最好查,因为稳定复现,单步调试就能抓到。第二类是时序相关错误,典型表现是“跑十分钟死一次”或者“中断频繁时必挂”,根源往往是临界区保护不当、中断优先级配置冲突、DMA与CPU争抢总线。第三类是环境相关错误,比如低温启动失败、高温下通信误码率飙升,这类问题跟硬件参数强相关,软件层面只能配合排查。第四类是资源耗尽型错误,堆栈溢出、内存泄漏、文件句柄耗尽,特点是运行越久越容易出问题。

分类的意义在于:不同类型的Bug,排查手段完全不同。你拿查逻辑错误的方法去查时序问题,就像拿放大镜找风,方向就错了。

1.2 为什么不能只靠打印调试

打印调试是嵌入式开发中最原始也最常用的手段,但它有三个致命缺陷。第一,打印本身会改变系统时序。你在一个高优先级中断里加一句串口输出,如果串口是阻塞发送,那这个中断的响应时间可能从几微秒变成几百微秒,原本能跑通的逻辑反而跑不通了。第二,打印信息量有限。当系统有几十个任务、上百个变量需要观察时,串口带宽根本不够用,你只能选择性打印,而选择性打印就意味着你可能恰好漏掉了关键信息。第三,打印无法回溯。问题发生的那一刻,你打印出来的只是冰山一角,真正导致问题的前因后果,打印信息里往往看不到。

所以我的原则是:打印可以用,但只能作为辅助手段,不能作为主要排查手段。真正高效的排查,需要组合使用多种方法。

1.3 方法选型的决策树

面对一个具体Bug,怎么快速决定用哪种排查方法?我总结了一个简单的决策流程。

先问自己三个问题:这个Bug能稳定复现吗?能复现的话,复现条件是什么?不能复现的话,有没有规律性的触发场景?

如果能稳定复现,优先用在线调试器单步跟踪,配合断点和变量监视,直接定位到出问题的代码行。如果不能稳定复现但有规律,比如“每次通信量大的时候就出问题”,那大概率是时序或资源竞争问题,优先用逻辑分析仪抓时序,或者用RTOS的运行时统计功能看任务栈使用情况。如果完全找不到规律,那就要上“重武器”了:硬件异常捕获、看门狗复位记录、内存保护单元配置,先把现场保留下来再说。

这个决策树不是死的,实际排查中经常需要交叉验证。但有了这个框架,至少不会像无头苍蝇一样乱撞。

2. 在线调试与断点技术的实战细节

2.1 调试器选型与连接方式

在线调试的核心工具是调试探针,常见的有ST-Link、J-Link、CMSIS-DAP等。选型时重点看三个指标:下载速度、断点数量、是否支持实时变量监视。

J-Link在这三项上表现最均衡,支持无限Flash断点和实时变量监视,但价格偏高。ST-Link性价比好,适合STM32生态,但断点数量有限,通常只有6个硬件断点。CMSIS-DAP是开源方案,DAPLink固件刷进去就能用,适合预算有限或者需要批量生产的场景。

连接方式上,SWD比JTAG更常用,因为引脚少、速度快。接线时注意三点:SWDIO和SWCLK要尽量短,最好加地线隔离;复位线建议接上,方便调试器控制芯片复位;如果目标板供电和调试器供电不一致,一定要确认电平匹配,否则可能烧毁调试器。

注意:调试器连接不稳定时,先检查接线长度和地线回路,再检查目标芯片的调试引脚是否被复用为普通GPIO。很多低功耗项目会把SWD引脚配置成普通IO来省电,结果调试器连不上,白白浪费时间。

2.2 硬件断点与软件断点的区别

硬件断点依靠芯片内部的断点单元实现,数量有限但功能强大,可以在Flash代码中任意位置打断,而且不修改代码内容。软件断点则是把目标地址的指令替换成断点指令,数量不受限,但只能用在RAM中运行的代码,而且会修改代码内容。

实际使用中,我通常这样搭配:在Flash中运行的代码用硬件断点,在RAM中运行的代码或者需要大量断点的场景用软件断点。如果芯片支持Flash Patch and Breakpoint单元,那就可以在Flash中设置几乎无限个断点,非常方便。

有个坑要注意:有些芯片在低功耗模式下会关闭调试单元,导致断点失效。这时候需要在调试配置里勾选“调试期间保持调试单元供电”之类的选项,具体名称看芯片手册。

2.3 变量监视与实时数据刷新

变量监视是调试器最实用的功能之一,但很多人只用到了皮毛。除了看单个变量的值,还可以做这几件事:

第一,设置数据断点。当某个变量被写入时触发断点,这对于排查“谁改了我的变量”这类问题极其有效。比如你发现一个全局状态变量莫名其妙变成了非法值,直接给它设个写入断点,一触发就能看到调用栈。

第二,使用实时变量监视。J-Link配合IDE的Live Watch功能,可以在不暂停CPU的情况下周期性刷新变量值,适合观察缓慢变化的信号。但要注意刷新频率不能太高,否则会占用调试接口带宽,影响程序运行。

第三,监视表达式。不要只看单个变量,把相关的变量组合成表达式一起看。比如监视(uart_rx_index - uart_tx_index) & 0xFF,能直接看出环形缓冲区的剩余空间,比分别看两个变量直观得多。

2.4 调用栈分析与现场还原

当程序跑飞或者进入硬件异常时,调用栈是还原现场的关键。但嵌入式环境的调用栈分析比PC端复杂得多,因为编译器优化、中断嵌套、栈溢出都会让调用栈变得不可靠。

我的做法是:在硬件异常处理函数里,先把关键寄存器压栈保存,然后通过栈帧指针回溯调用链。如果编译器开了优化,栈帧指针可能被省略,这时候就需要借助调试器的调用栈分析功能,或者手动分析汇编代码。

更稳妥的办法是提前埋点。在关键函数入口和出口处记录一个全局的函数调用计数器或者环形缓冲区,记录最近几次函数调用。一旦发生异常,先把这个缓冲区的内容读出来,就能大致还原异常发生前的执行路径。这个方法虽然土,但在没有调试器或者调试器连不上的场景下,非常管用。

3. 日志系统与打印调试的高级用法

3.1 分级日志系统的设计

打印调试被诟病,很大程度上是因为很多人只会用printf。一个设计良好的分级日志系统,能在不显著影响实时性的前提下,提供足够的信息量。

我通常把日志分成五个级别:ERROR、WARN、INFO、DEBUG、TRACE。ERROR级别只在系统出现严重错误时输出,比如硬件初始化失败、关键任务创建失败。WARN级别用于记录可恢复的异常,比如通信超时后重试成功。INFO级别记录系统状态变化,比如任务启动、模式切换。DEBUG级别用于开发阶段的详细跟踪。TRACE级别最细,通常只在排查特定问题时临时开启。

关键设计点是:日志级别要能在运行时动态调整。通过一个全局变量控制当前输出级别,调试时把级别调低,量产时把级别调高。这样既保证了量产版本的性能,又保留了现场排查的能力。

3.2 环形缓冲区与异步输出

直接在中断里调用printf是大忌,因为printf内部可能使用锁、可能阻塞、可能重入。正确的做法是:中断里只把日志内容写入环形缓冲区,然后由一个低优先级的日志任务负责从缓冲区读取并输出到串口。

环形缓冲区的大小需要权衡。太小了容易溢出丢日志,太大了浪费RAM。我的经验值是:如果日志任务每10毫秒调度一次,串口波特率115200,那缓冲区至少要有256字节。如果日志量很大,可以适当加大到1KB甚至2KB。

异步输出的另一个好处是不阻塞业务逻辑。日志任务优先级设到最低,有日志就输出,没日志就挂起,对系统实时性的影响可以忽略不计。

3.3 日志内容的格式化技巧

日志内容的质量直接决定了排查效率。我见过很多日志,输出一堆“Error occurred”就没了,这种日志有等于没有。

好的日志应该包含这几个要素:时间戳、模块名、日志级别、关键变量值、错误码。时间戳可以用系统tick计数,精度到毫秒就够了。模块名用于快速定位问题来源。关键变量值要具体,比如“UART接收超时,已接收字节数=3,期望字节数=8”,而不是“UART错误”。

另外,错误码要统一管理。每个模块定义自己的错误码范围,比如UART模块用0x1000开头,SPI模块用0x2000开头。这样一看错误码就知道是哪个模块出的问题。

3.4 日志输出通道的选择

串口是最常用的日志输出通道,但并不是唯一选择。根据场景不同,可以灵活选择:

输出通道适用场景优点缺点
串口开发调试、现场排查通用、简单占用引脚、速度有限
SWOCortex-M系列调试不占引脚、速度快需要调试器支持
RTT实时性要求高极快、不阻塞需要调试器连接
内存日志无法连接外部设备不依赖外设容量有限、需要事后读取
Flash日志长期记录掉电不丢失写入速度慢、寿命有限

SWO和RTT是我最推荐的两种方式。SWO利用调试接口的Trace功能,不需要额外引脚,速度比串口快得多。RTT则是SEGGER公司推出的技术,通过调试器直接读写目标芯片内存,速度极快,几乎不影响CPU运行。两者都需要调试器连接,适合开发阶段使用。量产阶段如果需要在现场记录日志,内存日志或者Flash日志更合适。

4. 硬件辅助调试手段的深度应用

4.1 逻辑分析仪抓时序

逻辑分析仪是排查通信协议问题和时序问题的利器。它能把SPI、I2C、UART、CAN等总线的电平变化完整记录下来,并且自动解码成协议数据。

使用逻辑分析仪时,采样率是关键参数。根据奈奎斯特定理,采样率至少要是信号最高频率的两倍,但实际使用中建议至少五倍以上。比如抓10MHz的SPI时钟,采样率最好设到50MHz以上。采样深度也要注意,深度不够的话,抓不到问题发生的那一刻。

抓取触发条件设置也很重要。不要一上来就抓一大段,先设置一个合理的触发条件,比如“CS拉低后的第一个时钟上升沿”,这样能精准抓到通信开始的那一刻。如果问题出现在通信中间,可以设置协议触发,比如“地址字节等于0x2A时触发”。

4.2 示波器看电源和信号完整性

有些Bug表面上是软件问题,实际上是硬件问题。比如系统偶尔复位,你查了半天软件逻辑,最后发现是电源纹波太大导致欠压复位。这时候示波器就派上用场了。

用示波器排查时,重点看这几个地方:电源纹波,特别是CPU核心电压和IO电压,纹波超过芯片手册规定的范围就可能出问题;复位引脚波形,看是否有毛刺或者缓慢下降;晶振起振波形,看起振时间和幅度是否正常;通信信号质量,看上升沿下降沿是否陡峭,是否有过冲和振铃。

我遇到过一个典型案例:系统在电机启动时偶尔死机。用示波器一看,电机启动瞬间电源电压跌落超过200mV,导致CPU欠压复位。这种问题,你在软件里怎么查都查不出来。

4.3 电流探头与功耗分析

低功耗设备排查Bug时,电流波形能提供很多线索。比如设备应该每10秒唤醒一次,但实际电流波形显示它每2秒就唤醒一次,那说明唤醒源配置有问题。又比如设备在休眠时电流应该是微安级,但实测是毫安级,那说明有外设没有正确关闭。

用电流探头配合示波器,可以把电流波形和GPIO波形同步抓取,直观看到“哪个GPIO拉高之后电流变大”,快速定位到耗电大户。

4.4 硬件异常捕获与看门狗策略

Cortex-M系列芯片提供了硬件异常机制,包括HardFault、MemManage、BusFault、UsageFault等。当程序跑飞、访问非法地址、除零、未对齐访问时,会触发对应的异常。

我的做法是:在HardFault处理函数里,把关键寄存器(R0-R3、R12、LR、PC、xPSR)压栈保存到一个全局结构体里,然后通过日志或者调试器读出来。通过分析PC值,能直接定位到出问题的指令地址。再结合反汇编文件,就能找到对应的C代码行。

看门狗策略也很关键。不要只是简单地“喂狗”,而是要在喂狗的同时记录一些状态信息。比如用一个全局变量记录最后一次喂狗时系统处于哪个任务,一旦看门狗复位,先把这个变量读出来,就能知道是哪个任务卡死了。

5. 常见问题与排查技巧实录

5.1 程序跑飞与硬件异常定位

程序跑飞是最让人头疼的问题之一。我的排查步骤通常是这样的:

第一步,确认是否触发了硬件异常。如果调试器显示停在HardFault_Handler,那就直接去看保存的寄存器现场。重点看PC值,它指向出问题的指令地址。

第二步,根据PC值查找反汇编文件,定位到对应的C代码行。如果PC值指向的是一个不可能执行到的地址,那说明栈被破坏了,返回地址被篡改。

第三步,检查栈使用情况。在启动文件里把栈大小改大一些,看看问题是否消失。如果消失了,说明是栈溢出。如果没消失,继续查其他原因。

第四步,检查数组越界和指针误用。这类问题往往不会立即触发异常,而是悄悄破坏内存,等到某个时机才爆发。可以用内存保护单元把关键区域保护起来,一旦越界访问立即触发异常。

5.2 内存泄漏与堆栈溢出排查

嵌入式系统的内存资源极其有限,内存泄漏和堆栈溢出是常见问题。

排查内存泄漏,最直接的方法是记录每次内存分配和释放。在malloc和free的封装函数里,记录分配大小、调用位置、分配序号,然后定期打印当前未释放的内存块列表。如果发现某个位置分配的内存一直没释放,那大概率就是泄漏点。

排查堆栈溢出,可以用栈填充法。在任务创建时,把整个栈空间填充成特定模式(比如0xAA),运行一段时间后检查栈底附近还有多少字节保持原样,就能估算出栈的最大使用深度。FreeRTOS提供了uxTaskGetStackHighWaterMark函数,直接返回栈的高水位线,非常方便。

5.3 通信超时与数据错误排查

通信问题在嵌入式开发中占比很高。排查时按这个顺序来:

先确认物理层是否正常。用示波器或者逻辑分析仪看波形,确认电平幅度、上升沿下降沿、时钟频率都符合协议要求。

再确认配置层是否正确。波特率、数据位、停止位、校验位是否匹配?I2C的从机地址是否正确?SPI的时钟极性和相位是否匹配?

然后确认协议层是否有问题。数据帧格式是否正确?校验和计算是否正确?超时时间设置是否合理?

最后确认软件层是否有Bug。接收缓冲区是否足够大?中断优先级是否合理?DMA配置是否正确?

我遇到过一个典型案例:SPI通信偶尔出错,查了半天软件,最后发现是SPI时钟线走线太长,高速时信号完整性变差。把时钟频率降低一半,问题就消失了。

5.4 低功耗模式下的调试陷阱

低功耗调试有几个特有的坑。第一,进入低功耗模式后,调试器可能断开连接,导致无法单步调试。解决办法是在调试配置里禁止进入深度睡眠,或者使用支持低功耗调试的调试器。

第二,唤醒源配置错误。比如配置了多个唤醒源,但实际只需要一个,结果设备频繁唤醒,功耗降不下来。排查时逐个屏蔽唤醒源,看电流波形变化。

第三,外设时钟未关闭。低功耗模式下,所有不用的外设时钟都应该关闭。但有些外设的时钟控制位在寄存器里比较隐蔽,容易遗漏。建议对照芯片手册的外设列表,逐个确认。

5.5 常见问题速查表

现象可能原因排查手段
程序跑飞栈溢出、数组越界、指针误用硬件异常捕获、栈填充法、MPU
偶发复位电源纹波、看门狗误触发、复位引脚干扰示波器看电源和复位波形
通信超时波特率不匹配、信号完整性差、中断优先级冲突逻辑分析仪抓波形、检查配置
内存泄漏分配未释放、重复分配记录分配释放日志、定期检查
功耗偏高外设时钟未关、唤醒源过多、GPIO状态错误电流探头、逐个排查外设
任务卡死死锁、优先级反转、中断未清除RTOS运行时统计、看门狗记录

6. 工具链与调试环境的搭建建议

6.1 调试工具链的选型对比

嵌入式调试工具链的选择,直接影响排查效率。我把常用工具分成三类:调试探针、协议分析工具、系统监控工具。

调试探针方面,J-Link功能最全,支持RTT、SWO、无限断点,但价格高。ST-Link性价比好,适合STM32项目。DAPLink开源免费,适合批量生产时给产线用。

协议分析工具方面,逻辑分析仪推荐Saleae系列,软件好用,协议解码丰富。示波器推荐带宽100MHz以上的数字示波器,能同时看模拟波形和数字信号。

系统监控工具方面,FreeRTOS的运行时统计和任务状态查询功能非常实用,建议在项目初期就配置好。SEGGER的SystemView可以图形化显示任务调度和中断时序,排查时序问题很直观。

6.2 编译优化对调试的影响

编译器优化是调试的“隐形杀手”。开了-O2或-O3之后,变量可能被优化到寄存器里,断点可能被重排,函数可能被内联,导致单步调试时看到的执行流程和源码对不上。

我的建议是:开发阶段用-O0或-Og,发布阶段再开高优化。如果必须在高优化下调试,可以采取这几个措施:把关键变量声明为volatile,防止被优化掉;使用__attribute__((optimize("O0")))对特定函数关闭优化;查看反汇编代码,以汇编为准来理解实际执行流程。

6.3 版本管理与问题追溯

嵌入式项目也要用版本管理工具,Git是目前的主流选择。每次提交时,在提交信息里写清楚改了什么、为什么改、影响范围是什么。这样当某个Bug出现时,可以通过git bisect快速定位到引入问题的提交。

另外,保留每个版本的固件和对应的符号文件。当现场设备出问题时,如果能拿到对应的符号文件,就可以用调试器分析core dump或者现场保存的寄存器数据,快速定位问题。

6.4 持续集成与自动化测试

嵌入式项目的自动化测试比纯软件项目难,但并非不可做。基本思路是:用脚本控制调试器,自动下载固件、运行测试用例、读取结果、判断通过与否。

比如用Python配合J-Link的脚本接口,可以实现自动烧录和寄存器读取。用逻辑分析仪的命令行接口,可以自动抓取波形并解码。把这些脚本集成到CI流程里,每次提交代码后自动跑一遍基础测试,能提前发现很多低级错误。

7. 排查经验与避坑心得

7.1 先怀疑自己,再怀疑硬件

我见过太多人,一遇到问题就说“硬件有问题”。诚然,硬件确实会出问题,但根据我的经验,八成以上的Bug还是软件问题。所以排查顺序应该是:先确认软件逻辑是否正确,再确认配置参数是否匹配,最后才怀疑硬件。

当然,这不是说硬件不用查,而是说不要一上来就把锅甩给硬件。正确的做法是:用软件手段先排除软件问题,如果软件层面实在找不到原因,再用示波器、逻辑分析仪等工具去查硬件。

7.2 保留现场比急于恢复更重要

很多新手遇到系统死机,第一反应是赶紧复位重启,看看能不能恢复。但这样做往往把现场破坏了,等重启之后问题不复现,你就失去了唯一的线索。

正确的做法是:先保留现场。如果调试器连着,先暂停CPU,把关键寄存器和内存dump出来。如果调试器没连,先看日志输出到了哪里,把日志保存下来。如果系统支持,触发一个软件异常,把现场信息保存到Flash里。总之,在恢复之前,尽可能多地收集信息。

7.3 二分法与增量排查

当问题范围很大时,二分法是最有效的缩小范围的手段。比如你怀疑是某个模块的问题,那就先把这个模块的代码注释掉,看问题是否消失。如果消失了,说明问题在这个模块里,然后再把这个模块一分为二,继续排查。

增量排查则是另一个思路:从已知正常的版本开始,逐步添加改动,每添加一步就测试一次,直到问题复现。这样能精确定位到是哪次改动引入了问题。

7.4 记录排查过程与知识沉淀

每次排查完一个Bug,我都会写一份简短的排查记录,内容包括:问题现象、排查过程、根本原因、解决方案、经验教训。这份记录不仅方便自己以后查阅,也能帮助团队里的其他人避免踩同样的坑。

时间长了,这些记录就形成了一套内部的“排查手册”。新同事遇到类似问题时,先查手册,往往能快速找到方向,大大缩短排查时间。

7.5 保持耐心与系统性思维

最后说一点心态上的体会。嵌入式Bug排查,最忌讳的就是急躁。越急越容易乱改代码,越改问题越多。正确的做法是:冷静下来,按照系统性的方法一步步排查,每做一次改动都要有明确的假设和验证。

我个人的习惯是:遇到难缠的Bug,先泡杯茶,把问题现象、已知条件、可能原因都写在纸上,然后逐个排除。写下来的过程本身就是理清思路的过程,很多时候写着写着就找到方向了。

这个内容后续还可以这样扩展:针对特定芯片平台(比如Cortex-M系列、RISC-V系列)的调试技巧,或者针对特定RTOS(比如FreeRTOS、RT-Thread)的排查方法,都可以单独展开成更详细的专题。如果大家在实际项目中遇到什么奇葩Bug,也欢迎一起交流排查思路。

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

无刷电机相序适配原理与快速校准方法

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

作者头像 李华
网站建设 2026/10/23 3:52:37

Linux 6.6 IOMMU DMA重映射:IOVA、IOTLB与映射开销排查

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

作者头像 李华
网站建设 2026/10/23 3:52:12

Windows 10性能治理:四层可验证系统级调优方法论

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

作者头像 李华
网站建设 2026/10/23 3:51:57

Grad-CAM热力图实战:PyTorch实现、目标层选择与排查

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

作者头像 李华
网站建设 2026/10/23 3:51:23

国产碳化硅MOSFET横向评测:12维度量化选型方法论

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

作者头像 李华
网站建设 2026/10/23 3:50:14

Ubuntu 24.04 安装搜狗输入法:fcitx4 框架与 X11 会话完整指南

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

作者头像 李华