news 2026/10/1 1:05:04

Keil调试实战指南:从SWD连接、断点观察到FreeRTOS多任务排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Keil调试实战指南:从SWD连接、断点观察到FreeRTOS多任务排查

做嵌入式这行,Keil调试几乎是每天都要打交道的事。从最早用C51写单片机程序,到后来转到ARM Cortex-M系列,再到给GD32、瑞萨这些国产和日系芯片做开发,Keil MDK一直是我工作台上最常用的 IDE 之一。很多人觉得 Keil 就是个写代码、点编译的工具,但真正到了硬件联调、程序跑飞、数据不对的时候,调试功能用得好不好,直接决定你排查问题的速度。这篇内容我整理了自己这些年用 Keil 做调试的完整经验,从环境搭建、调试器连接,到 Debug 模式下的窗口操作、串口联动、代码静态检查,再到 FreeRTOS 这种复杂工程的调试思路,基本覆盖了日常开发会遇到的高频场景。不管你是刚接触 Keil 的新手,还是已经被奇怪 BUG 折磨好几天的老手,这篇文章应该都能帮你省下一些查资料的时间。

1. Keil 调试的第一关:环境、工程与调试器连接

1.1 开发包与编译器版本:先理清再动手

很多初学者拿到 Keil 的第一反应是去网上找安装包、注册机。先不说这类工具的安全风险,光是从长期开发的角度看,我强烈建议你走正规渠道。Keil MDK 官方提供了免费评估版(代码限制 32KB)以及社区版(非商业用途免费),对于学习和小规模项目已经完全够用。如果你做商业项目,购买正版授权不仅是合规问题,还能避免后续更新、技术支持上的各种麻烦。实际开发中因为版本不匹配而浪费一整天的情况,我见过太多了。

装好软件只是第一步,更关键的是安装对应芯片的 Device Pack。Keil 5 以后采用了 Pack 机制,不同厂商的芯片支持包是分开管理的。比如你做 STM32F103C8T6,要去 Pack Installer 里装 STM32F1 系列的支持包;如果换成 GD32,就要装 GigaDevice 的 Pack;瑞萨的 RASC 环境下也要单独配置对应的器件支持。我经常看到有人编译工程时报错Error: Device not found,十有八九就是 Pack 没装或者版本不对。

还有一个容易踩坑的是 ARM Compiler 版本。Keil 5 里默认可能用的是 AC5,到了 Keil MDK 5.37 之后开始主推 AC6(基于 Clang)。两者对 C 语言的语法检查严格程度不一样,AC6 对类型不匹配、隐式声明的处理更敏感。直接把旧工程从 AC5 切到 AC6,经常会出现一堆原来没有的 Warning 甚至 Error。我的建议是:除非你要用新编译器特性,否则沿用工程原来的编译器版本最稳妥。如果非要用 AC6,先把-Wno系列警告选项熟悉一遍,否则光是清理编译告警就够你喝一壶。

1.2 调试器连接不上?先从“no ULINK device found”说起

No ULINK Device Found这个报错,是老生常谈的问题了。很多人的第一反应是调试器坏了、芯片烧了,其实大部分时候问题出在连接层面。

先讲排查思路。第一步,确认 USB 驱动识别正常。把调试器插到电脑上,打开设备管理器,看是否有未识别的设备或者黄色感叹号。ST-Link 需要装 ST-Link 驱动,J-Link 自带驱动但也可能被系统拦截,DAP-Link 一般是免驱的。如果 USB 识别有问题,换一根数据线试试,不要用那种只有充电没有数据线的“工科男专用线”。

第二步,检查 Keil 的调试器配置。菜单栏Options for Target,切到Debug选项卡,右上角需要选择正确的调试器类型。很多人用 ST-Link,结果下拉框里还留着 ULINK2,那必然连不上。选好调试器后,点旁边的Settings,正常应该能看到调试器的 ID 码和目标芯片的 ID。如果这里显示No target connected,那就不是 Keil 配置的问题,而是硬件连接的问题。

第三步,检查 SWD 接线。SWD 模式下只需要四根线:SWDIO、SWCLK、GND,外加 VCC(用于电平参考)。SWDIO 和 SWCLK 接反是新手最常见的错误。还有个容易被忽略的点——目标板的复位电路。如果复位引脚被强下拉或者电容过大,调试器可能连不上。我遇到过一块板子 SWD 死活识别不到,查到最后是复位电容焊错了容值,导致时序不对。

1.3 编译通过但进不了调试的隐藏雷区

连接都正常了,编译也通过了,结果点 Debug 按钮进入调试模式后,程序跑起来完全不是预期表现,断点也打不上。这种问题往往和优化等级有关。

Keil 的Options for Target -> C/C++里有个 Optimization 选项。默认可能是-O0或者-O1,如果为了减小代码体积选了-O2甚至-O3,那调试体验会非常难受。高优化等级下,局部变量可能被优化到寄存器里,Watch 窗口里根本看不到;断点打在函数内部某一行,可能因为代码重排导致断点位置偏移甚至无效;单步执行时,代码执行的顺序和源码看起来对不上,很容易把人绕晕。

我的做法是:Debug 版本统一用-O0,Release 版本再看情况开优化。虽然生成的代码大一些、跑得慢一些,但调试体验好太多了。不要一边开最高优化,一边怪 Keil 的调试功能不好使,这锅真不该它背。

另一个常见的坑是 MicroLIB 和半主机模式。用 printf 做串口输出时,很多教程会教你勾选Use MicroLIB,因为标准 C 库的 printf 会占用大量 Flash,而 MicroLIB 精简了实现。但如果你自己重定向了fputc却没有勾选 MicroLIB,编译器可能会把 printf 的底层输出连接到半主机模式(Semihosting)上,这在硬件调试环境下会导致程序在调用 printf 时直接 HardFault。这个问题的排查思路,我们放到串口那一节再细讲,但调试之前先把这两项配置弄清楚,能省去很多莫名其妙的问题。

2. Debug 模式的核心操作:窗口、断点与变量观察

2.1 结构体变量怎么完整看?Watch 窗口的正确用法

有朋友在调试助手里问“Debug 模式如何显示结构体变量”,这确实是 Keil 调试时非常高频的需求。当你进入 Debug 模式后,找到View -> Watch Windows,可以打开 Watch 1 和 Watch 2 两个窗口。在 Watch 窗口里,点击<Enter expression>,输入结构体变量的名字,回车,就能看到它的完整内容了。

结构体变量在 Watch 窗口里是支持展开的。比如你定义了一个PID_TypeDef pid;,展开后可以看到Kp、Ki、Kd、integral、output这些成员变量的实时数值。对于嵌套结构体,也支持一层一层往下展开,直到最基础的整型、浮点型变量。这个功能在调 PID 参数时尤其好用——你可以在线修改结构体成员的值,然后观察控制量输出是否按照预期变化,全程不用重新编译烧录。

有一点要注意:如果变量是static修饰的局部变量,或者定义在某个函数内部,Watch 窗口不一定能直接找到。这时候需要保证程序已经运行到那个函数内部,且变量在当前作用域内可见,否则 Watch 窗口会显示not in current scope。还有一种情况是变量被优化掉了,这就要回到上面提到的编译优化等级问题。

2.2 寄存器、内存与外设窗口:硬件调试的显微镜

Watch 窗口适合看“软件逻辑里”的变量,但嵌入式调试还有一个重要维度——看硬件寄存器和内存。Keil 的Peripherals菜单下,可以根据芯片型号列出所有外设。比如你打开Peripherals -> USART1,可以看到 USART1 的所有寄存器,包括 SR、DR、BRR 等。这在排查串口收不到数据的问题时非常直观:检查 TXE 位是否置位、RXNE 位有没有拉高、波特率寄存器值是否符合预期,一目了然。

View -> Memory Windows可以打开内存窗口,查看任意地址的原始数据。调试 flash 烧写问题、DMA 搬运的数据对不对、协议栈的缓冲区内容,都可以用内存窗口来看。你只需要在 Address 栏输入十六进制地址,比如0x20000000,就能看到从该地址开始的内存内容。右侧会同时显示 ASCII 码表示,查字符串、查数据包很方便。内存窗口还支持实时刷新,程序跑起来后数据变化会直接更新在窗口里。

寄存器窗口(View -> Registers Window)则能实时看 CPU 寄存器的值,包括 R0~R15、PSP、MSP、LR、PC 等。程序死循环卡住时,看一眼 PC(程序计数器)在哪个地址、LR(链接寄存器)指向哪里,基本上能快速定位卡在哪个函数。调试 HardFault 时,读一下 MSP/PSP 和堆栈里的内容,就能还原出错的调用链,这是嵌入式工程师必须掌握的技能。

2.3 断点的高级玩法:条件断点、数据断点与 Trace

除了在代码行上打普通断点,Keil 还支持条件断点、数据断点(也叫硬件断点)和 Trace 功能。这些功能用得好,调试效率能翻倍。

条件断点的意思是,只有满足某个条件时断点才触发。比如你在一个 for 循环里调试,想等i == 50的时候停下来看看情况,不需要手动按 50 次 Continue。右键断点位置,选择Breakpoint Properties,在 Expression 里填入i == 50,运行后程序会在 i 等于 50 那一瞬间停下。这个功能在排查“数据量达到一定阈值后出现的异常”时尤其好用。

数据断点则是监控某个变量或内存地址。当你怀疑某个变量在某个瞬间被意外修改时,可以在 Watch 窗口里选中该变量,右键选择Set Breakpoint on Data Access,指定读写触发。这样只要程序对该变量的读或写操作发生,Keil 就会立刻中断,停在修改这个变量的代码行上。查那种“变量无缘无故变了值”的灵异事件,数据断点是最强工具。

Trace 功能则可以在View -> Trace窗口里查看代码执行的历史记录。由于 Cortex-M 内核的 ITM/SWO 引脚支持跟踪输出,Keil 可以记录最近的执行路径。当程序跑飞或跳转到异常地址时,通过 Trace 能回溯进入异常前的执行序列,帮助你找出是哪一步操作破坏了堆栈或触发了故障。

3. 串口调试与上位机联动:从 printf 到可视化调参

3.1 printf 重定向:串口输出的经典姿势与坑

串口调试是嵌入式开发中最常用的输出手段,而 printf 重定向则是实现串口打印最方便的方式。核心思路是重写标准库的fputc函数,让它将字符输出到串口发送寄存器。

以 STM32 标准外设库为例,重定向代码大概是这样的:

#include <stdio.h> int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }

如果是 HAL 库,则写成:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000); return ch; }

这段代码加到工程里后,记得勾选Use MicroLIB。原因是标准 C 库的 printf 完整版可能会引用半主机模式的底层实现,在硬件环境下频繁调用会导致 HardFault。而 MicroLIB 移除了对半主机模式的依赖,重定向 fputc 后即可正常工作。

如果你用的是 GCC 工具链或者 AC6,可能还需要额外处理_write或_sys_write函数,这里不展开,但原理类似。串口打印能出来的那一刻,调试效率会有一个质的提升——传感器数据、状态机的状态切换、报错信息,全部可以直接打到串口助手里看。

3.2 VOFA+ 与 PID 调试:把数据变成曲线

单纯的串口打印文本,在调 PID 参数时其实效率不高。你打印 100 行数据,眼睛看到的是满屏滚动的数字,很难直观判断有没有超调、震荡、响应够不够快。所以我现在调 PID 基本都用波形显示工具,其中最推荐 VOFA+。

VOFA+ 是一款跨平台的串口上位机,支持 JustFloat、FireWater 等协议。你只需要在单片机里把要观察的变量按固定格式打包发出来,上位机就能实时绘制曲线。最常用的 JustFloat 协议格式很简单:四个字节的 float 数据 + 一个帧尾字节0x00 0x00 0x80 0x7f。

发数据的代码可以这样写:

#include <string.h> float tx_data[4]; uint8_t frame_tail[4] = {0x00, 0x00, 0x80, 0x7f}; void send_float_data(float *data, uint8_t len) { HAL_UART_Transmit(&huart1, (uint8_t *)data, len * 4, 100); HAL_UART_Transmit(&huart1, frame_tail, 4, 100); } // 使用示例 // tx_data[0] = target_speed; // tx_data[1] = actual_speed; // tx_data[2] = pid_output; // send_float_data(tx_data, 3);

有一点要注意:发送 float 数据时,单片机和上位机的字节序必须一致。Cortex-M 默认是小端存储,VOFA+ 的 JustFloat 协议默认也是小端,所以直接用即可。如果你用的是其他上位机,先确认一下字节序,否则看到的数据会完全乱掉。

有了一路曲线,调 PID 就直观多了。观察目标值和实际值的跟随情况、响应速度、超调量大小,比看数字文本高效十倍。我用这个方法最多的时候,同时发了八个变量:目标位置、实际位置、速度、加速度、P 项输出、I 项输出、D 项输出、总输出,所有曲线在屏幕上叠一起,问题在哪里一眼就看出来了。

3.3 蓝牙与无线调试:摆脱线缆束缚

有些场景不方便插 USB 转串口线,特别是做姿态传感器、四轴飞行器这类有移动部件、或者装在设备内部的硬件调试时。这时候用蓝牙串口模块做无线调试就很合适。

市面上常见的 HC-05、HC-06 都是经典选择,还有一些更小型的 BLE 模块。使用方法和有线串口一模一样:单片机 TX 接蓝牙模块 RX,单片机 RX 接蓝牙模块 TX,共地,上电后用手机端的蓝牙调试助手(比如“小牛蓝牙调试助手”)连接模块,就能远程看到单片机的打印信息了。

我用蓝牙调试遇到过几个问题,给你避坑。第一,蓝牙串口的波特率一定要和单片机串口配置一致,默认一般 9600 或者 115200,但也有出厂默认不同速率的。第二,无线传输不稳定时会出现乱码,所以协议帧最好加上帧头和校验字,不要裸发裸收。第三,蓝牙模块的电流需求比普通 LED 大不少,用 3.3V 供电有时会出现电压跌落导致单片机复位,建议直接供 5V(模块上一般带稳压),并且共地要接好。

4. 代码质量与协作效率:静态检查、格式化与工程管理

4.1 Astyle 代码对齐:拯救强迫症的格式化工具

代码缩进混乱是团队协作里最让人崩溃的问题之一。不同人的编辑器 Tab 宽度不同、缩进风格不同,代码合到一起后格式完全没法看。在 Keil 里手动逐行对齐纯属浪费时间,要用工具解决。

Astyle(Artistic Style)是一个开源的代码格式化工具,支持 C、C++、Java 等多种语言。它可以通过命令行直接格式化文件,也可以在 IDE 里配置外部工具调用。以 Keil 为例,在Tools -> Customize Tools Menu里可以添加一个菜单项,Command 指向 astyle.exe,Arguments 填格式化参数,比如:

--style=allman --indent=spaces=4 --convert-tabs %E

--style=allman表示花括号单独占一行(Allman 风格),--indent=spaces=4表示用四个空格缩进,%E是 Keil 的宏,代表当前编辑的文件路径。配置好后,点一下菜单就能把当前文件格式化成统一风格。

对于团队项目,最重要的是把 Astyle 的配置参数写进项目文档里,让大家都用同一套参数格式化。我在之前的项目组里就吃过这个亏:有人用 Allman,有人用 K&R,还有人混合着来,最后 review 代码时一大半时间都在争论格式问题。后来统一在提交前跑一遍 Astyle,世界瞬间清净了。如果你用的是 VS Code 或者别的编辑器,也有对应的 Astyle 插件,核心参数是一样的。

4.2 Cppcheck 静态分析:编译不报的错它来查

Keil 的编译器主要做语法检查和类型检查,但代码里的逻辑问题、越界访问、空指针解引用、内存泄漏,编译器不一定能发现。Cppcheck 是一款免费开源的 C/C++ 静态分析工具,能在不运行程序的情况下找出大量潜在问题。

Cppcheck 可以在 Keil 里以外部工具的方式集成。配置方式和 Astyle 差不多,Command 指向 cppcheck.exe,Arguments 指向当前工程文件或源码目录。命令行大致是:

cppcheck --enable=all --platform=arm32 --std=c99 --force --inline-suppr ./Src

--enable=all表示启用所有检查项,--platform=arm32指定目标平台为 32 位 ARM,模拟和 Keil 环境一致的整型大小,--force表示检查所有组合。跑完后 Cppcheck 会输出带行号的问题列表,哪里越界了、哪里漏了 break、哪里重复定义了,一目了然。

我用 Cppcheck 查出来过几个印象深刻的 bug:一个是在解析协议帧时用了memcpy,长度字段没校验,攻击者可以构造超长数据包造成缓冲区溢出;还有一个是中断回调里调用了非中断安全函数,导致优先级反转和死锁。这些靠运行时调试很难复现,但静态分析几秒钟就能报出来。所以现在我的习惯是:每次提交代码前,先本地跑一遍 Cppcheck,把清零告警数量的日期记在项目周报里,客观上提升了代码的可靠度。

4.3 Keil 与 GDB 调试命令的对比思考

热词里有人提到 gdb 调试常用命令,这里顺便聊一聊。GDB 是 GNU 调试器的缩写,主要用在 Linux 环境和 GCC 工具链下。你可能好奇 Keil 的调试和 GDB 有什么关联?其实 Keil MDK 的底层也用了 GDB 的思路,比如断点、监视变量、查看寄存器,只是 Keil 把这些封装成了图形化的用户界面。而 GDB 本身是命令行交互方式。

如果你日后要接触 Linux 嵌入式开发、RISC-V 开发或者其他非 ARM 平台,GDB 的基本思想是通用的:break设置断点、continue继续运行、next单步跳过、step单步进入、print打印变量、x查看内存。这些命令在 Keil 里对应的就是断点按钮、全速运行按钮、单步按钮和 Watch、Memory 窗口。学会一种调试器,换到另一个环境时思路完全迁移得过去。

我个人体会到的一个习惯是:不要太依赖图形界面。有些问题用命令行方式反而更精准。比如在 GDB 里敲info registers看一下所有寄存器值,比在 Keil 的窗口里翻半天下拉菜单快得多。现在很多微控制器的开发也在往命令行的方向走,VP 的学习资料里都推荐用 OpenOCD+GDB 的方式代替真机调试器,所以趁早理解一下 GDB 的思路没有坏处。

5. FreeRTOS 与复杂工程的调试要点

5.1 FreeRTOS 在 STM32F103C8T6 上的移植与调试陷阱

FreeRTOS 是目前最主流的嵌入式实时操作系统之一,在 STM32F103C8T6 这样的低内存芯片上也能流畅运行。但引入了 RTOS 之后,调试复杂度会上升一个级别:不是简单的单线程程序可以单步跟踪了,而是多个任务由调度器抢占式切换,断点打在哪个任务里、切换瞬间发生了什么,都变得难以把握。

热词里提到“freertos学习篇一:stm32f103c8t6下的移植”,这个确实是入门 RTOS 的高频起点。F103C8T6 只有 20KB RAM,移植时要特别注意 FreeRTOS 的堆大小配置。在 FreeRTOSConfig.h 里有一个configTOTAL_HEAP_SIZE宏,这是 RTOS 内核管理的内存池大小。F103C8T6 上我一般设置为 8KB 到 12KB,再大就留给业务逻辑的全局变量、任务栈、中断栈了。

任务栈大小也需要仔细计算。默认情况下每个任务分配 128 字(word)的栈,但如果你在任务里调用了较深的函数嵌套或者用了 printf,这个栈很可能会爆。FreeRTOS 提供了栈溢出检测机制,把configCHECK_FOR_STACK_OVERFLOW设为 2,当栈溢出时会调用vApplicationStackOverflowHook钩子函数,你可以在那里设置断点。调试时栈溢出最常见的现象是:任务运行一段时间后突然 HardFault,或者另一个不相关的全局变量被异常修改。

建议新手在体会 FreeRTOS 调度原理时,先用一个 LED 闪烁任务和一个串口打印任务做实验,让两个任务交替运行,观察串口输出的时序,再逐步加入更复杂的业务逻辑。调试多任务程序时,善用 Keil 的 RTOS 插件(如果有)或者直接添加变量监视uxCurrentNumberOfTasks、pxCurrentTCB,能更直观地看到当前正在运行的任务。

5.2 多任务调试的经验总结

多任务调试和单线程调试最大的区别是:单线程下你总是知道“下一步会执行哪行代码”,多任务下处理器随时可能切到另一个任务的上下文。所以若你在某个任务里设置了普通断点,程序停下来时可能停在另一个任务里,而你根本没意识到,就会产生“我的断点怎么断到了别的地方”的错觉。

解决这种困惑,我总结了三个经验。第一,进入调试前先确定当前运行的任务号,观察 FreeRTOS 的pxCurrentTCB变量,它指向当前任务的 TCB(任务控制块),从中可以读出任务状态和优先级。第二,要在任务切换的时刻做分析时,可以在vTaskSwitchContext函数里设置断点,这样每次调度器切换上下文时程序都会停下,配合 Trace 窗口回溯路径。第三,条件断点非常适合 RTOS:比如你只关心某个特定任务运行到某行的情况,可以用pxCurrentTCB == &task1_TCB这样的条件,只在切换到 task1 时才停下来。

另一个不能忽视的问题是多任务下的优先级反转。低优先级任务持有资源,高优先级任务等待资源,导致中优先级任务抢占了低优先级任务的执行机会,最终高优先级任务迟迟等不到锁。这种情况下调试现象非常迷惑:高优先级任务没有执行,但看起来代码逻辑没有问题。排查方法是用 Trace 功能记录各任务的状态变化,或者使用 FreeRTOS 的vTaskList、vTaskGetRunTimeStats输出任务调度信息,分析各任务的实际运行时间和状态。

写在最后的个人体会

做了这么多年嵌入式,我最大的感受是:调试能力不是靠“多用几次 Debug 按钮”练出来的,而是靠对工具底层逻辑的理解和反复实操积累出来的。Keil 的调试功能远比我刚接触时想象的要强大,但前提是你得知道它有哪些功能、在什么场景下该用哪个功能。建议你花一个下午的时间,专门把 Keil 的每个窗口点开看一看、每个菜单翻一翻,对照本文提到的方法,在一个简单的工程上逐一验证。调试工具的意义不只是帮你找 bug,更重要的是让你能理解程序的真实行为,而一旦你理解了真实行为,bug 往往就不攻自破了。希望这篇 Keil 调试汇总能给你带来一点点启发,哪怕只是让你下次遇到问题时少翻一次搜索引擎,也算值了。

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

Android 10 Perfetto 命令行抓 trace 与 SQL 分析

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

作者头像 李华
网站建设 2026/10/1 1:04:45

PICORV32软核源码解析:从Verilog到RISC-V处理器设计入门

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

作者头像 李华
网站建设 2026/10/1 1:04:26

工业气体泄漏检测数据集:双模态实例分割+多级语义标注

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

作者头像 李华
网站建设 2026/10/1 1:04:02

井盖缺陷检测数据集:2890张实拍图+VOC/YOLO双格式+5类细粒度标注

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

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

Switch原生运行Wine兼容层:无需刷系统跑PC游戏实战

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

作者头像 李华