news 2026/9/6 3:49:52

STM32进阶避坑指南:调试口、时钟树与SysTick的致命陷阱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32进阶避坑指南:调试口、时钟树与SysTick的致命陷阱

玩STM32也有十来年了,带过不少新人,也见过很多从入门到进阶的开发者。说实话,真正把板子干到“救不回来”的,往往不是刚上手的小白,反而是那些学了几个月、觉得自己什么都懂了的人。为什么?因为新手谨小慎微,每一步都照着教程来,遇到问题第一反应是“我哪里抄错了”;而学得越久,胆子越大,开始动那些“默认就该这样”的底层配置,动完还经常忘记留后路。调试口复用、时钟树改写、SysTick和中断优先级的调整,这三块只要动错一处,轻则程序跑飞,重则调试器直接连不上芯片,看起来跟变砖一模一样。

这篇文章不聊入门教程,专门把这几个“越学越久越容易踩”的坑拆开讲透,附上我自己的翻车记录和救回办法。不管你是跟江科大的视频入门,还是用野火的指南者板子起步,学到最后都会面对同一个现实:教程里的代码能跑,不代表你随手改完还能跑。希望这篇文章能帮你少走几步弯路。

1. 先说说为什么“学得越久反而越容易踩坑”

1.1 新手阶段靠的是“照做”

刚接触STM32的时候,绝大多数人都是跟着开发板配套例程走:打开一个模板工程,改改GPIO、点个灯、串口打印,烧录就完事。这个阶段有个特点,就是“凡是不理解的地方都不敢动”。时钟树配置是CubeMX自动生成的,启动文件是官方写好的,链接脚本是从例程里复制来的,SWD调试脚默认连接着ST-Link,一切维持原样。

不夸张地讲,这个阶段反而不容易出严重问题。因为你不懂,所以你不敢瞎改,能跑就烧,烧坏了就重新下载一个例程,板子很难真正“变砖”。

1.2 进阶阶段开始挑战底层配置

学了几个月之后,事情就变了。你不满足于跑官方例程了,开始想用更少的引脚实现更多的功能,想把主频拉到芯片标称的最高值,想省掉那颗外部晶振省几毛钱成本,想自己把delay写得“更高端”,甚至想把启动文件、链接脚本、编译器优化等级都改成自己看得顺眼的样子。

这个阶段最危险。因为你对芯片已经有了一定的理解,但这些理解往往是碎片化的。你知道PA13、PA14是SWD调试脚,但你更在乎的是“IO不够用了”;你知道RCC可以配置PLL,但你不一定记得Flash等待周期和电压域之间的约束关系;你知道SysTick是系统滴答定时器,但你没意识到HAL_Delay在中断里调用会有死锁问题。知识量上去了,敬畏心下来了。

1.3 关键不是知识量,而是“想当然”

我见过太多翻车现场,有一个共同点:并不是操作太难,而是当事人在改配置之前,潜意识里已经认定“这样改肯定没问题”。改完出问题之后,第一反应往往是怀疑硬件、怀疑调试器、怀疑编译器优化,唯独不怀疑自己刚改的那几行代码。

学得越久,越容易掉进“想当然”的陷阱。所以这篇文章专门挑出三个我反复踩、也反复帮别人排查过的典型坑,一个一个说清楚:为什么会踩、踩了之后是什么现象、怎么救回来、下次怎么避免。

2. 坑一:SWD/JTAG调试口被复用后,板子“变砖”

2.1 为什么越学越久,越容易去动调试口

先问一个问题:新手敢不敢把PA13、PA14这两个引脚改成普通GPIO?大概率不敢,因为教程里反复强调这是SWD下载口,动了就没法烧程序了。但学久了之后呢?当你的项目需要多接一个按键、多驱动一个LED,而IO正好差两个的时候,很多人会想:反正程序这一版跑得好好的,暂时不需要调试,把SWD脚腾出来用一下应该没事。

这个想法本身没错,错的是“暂时”这两个字。你永远不会记得自己改过这个配置,直到第二天想烧录新功能时,调试器死活连不上。

STM32默认的调试引脚分布大概是这样:Cortex-M3/M4内核的型号,PA13是JTMS/SWDIO,PA14是JTCK/SWCLK,PA15是JTDI,PB3是JTDO/TRACESWO,PB4是JNTRST。对于F1系列的芯片,还有专门的SWJ_CFG配置位,可以单独关闭JTAG、保留SWD,也可以把JTAG和SWD全部关掉。很多进阶玩家为了省几个IO,会把JTAG引脚全部复用成GPIO,甚至把SWD也一并关掉。

2.2 现象:各种“连不上芯片”的报错长什么样

这个坑最直观的表现就是:烧录软件认不出芯片了。常见的报错包括:

  • ST-Link或STM32CubeProgrammer提示Error: no stm32 target found!
  • Keil MDK下载时提示Error: Flash Download failed - Target DLL has been cancelled
  • J-Link连接时提示Could not connect to target
  • ST-Link Utility或CubeProgrammer点连接时提示Target no device

如果你的芯片是比较新的型号,带debug authentication的话,ST-Link甚至会提示一句if your product embeds debug authentication, please...,大致意思是说芯片可能开启了调试认证保护。但更多时候,在F1、F4这些老型号上,碰到这类“No target found”的报错,八成不是硬件坏了,而是你自己把调试口“关上”或者“复用掉”了。

2.3 我的一次翻车实录

去年冬天我画了一块STM32F407的板子,功能不复杂,但IO规划得特别紧张。为了多引两个引脚出来控制LED和读取按键,我想都没想就把PA13、PA14在初始化代码里配置成了GPIO输出。当时整个项目是能正常跑的,点灯、按键、串口全都通,我心里还挺得意。

第二天想给这块板子加一段新的逻辑,顺手把ST-Link插上,打开Keil一点下载,直接报错。我一开始以为是ST-Link的固件挂了,换了条线,换了台电脑,问题依旧。折腾了半小时之后才猛地回想起来:昨天我把SWD的两个脚复用成GPIO了,下载口已经被我自己封死了。

那个瞬间真是一言难尽。板子本身没有任何硬件故障,但就是没办法通过SWD连接,看起来比变砖还尴尬——程序能跑,我却没法改它。

2.4 救回方法:从复位窗口到系统Bootloader

如果你的程序只是占用了PA13、PA14,但芯片本身还有别的外设,也就是程序还没跑死,那救回来的手段还是有不少的。我按成功率从高到低给你排一排。

方法一:复位窗口连接法。按住板子上的复位键不松手,在IDE或烧录软件里点连接、点下载,然后在点击之后的一瞬间松开复位键。芯片在复位期间,所有引脚都会恢复成默认功能,包括SWD调试脚。调试器只要在这个窗口期抓到了内核,就能抢在用户代码重新配置引脚之前建立连接。这个方法对“SWD脚被复用成GPIO”这种状况非常有效,成功率很高。Keil里可以勾选Settings里的Connect under Reset,STM32CubeProgrammer里也有类似选项。

方法二:换用JTAG连接。如果你只是关掉了SWD但保留了JTAG,或者反过来了,那换个调试接口就能绕过。比如F1系列的代码通过AFIO重映射把JTAG关了、但SWD还开着,那用SWD就能连;如果SWD被关掉但JTAG还活着,用J-Link的JTAG模式就能连。我自己有一次就是这样,SWD被复用了,换了个JTAG转接板之后直接连上,把所有引脚配置恢复原样,再切回SWD就一切正常。

方法三:进入系统Bootloader,擦除整片Flash。如果SWD和JTAG全都被你关了,或者复位窗口法也抢不到,那就只能动用最后的手段。以F1和F4为例,把BOOT0引脚拉高(F1还需要把BOOT1拉低),重新上电,芯片就会从内置的系统存储区启动,运行出厂固化的Bootloader。然后用串口线连接芯片的USART1(F1是PA9/PA10,F4也是USART1),打开STM32CubeProgrammer,选择UART模式,波特率一般选115200,连接成功之后直接执行整片擦除。Flash清空之后,用户代码没了,调试口引脚的复用配置也就跟着没了,重新把BOOT0拉低、上电,芯片恢复正常,SWD能连了,程序从头开始烧就行。

提示:F1系列的老芯片,Bootloader的USART1引脚固定是PA9(TX)、PA10(RX),不要接反。F4系列同样以USART1为主,但部分型号还支持USB DFU或者别的串口,具体以STM32官方应用笔记AN2606为准。

2.5 预防做法

这个坑我踩过一次之后,往后做板子都养成几个习惯:第一,SWDIO、SWCLK这两个脚不到万不得已绝不复用,如果非要用,也至少保留一组“恢复手段”——比如板子上留一个可选的0欧电阻,调试时接上、产品出货时拿走;第二,所有动调试口的代码都用宏包起来,调试版和发布版严格分开,不要混在一个工程里;第三,在动调试口配置之前,先复制一份当前能烧录的工程,万一改完真连不上了,还有退路。

说句实在话,绝大多数“板子变砖”都不是物理损坏,而是软件把自己锁死了。先别急着拆芯片,按上面的思路排查,大概率能救回来。

3. 坑二:时钟树“玩太花”,默认配置回不去了

3.1 学得越久的人,为什么越爱动时钟

时钟树是STM32里最基础、也最容易被“玩坏”的部分。新手阶段大家基本都是用CubeMX生成默认配置,外部晶振是多少就用多少,主频是多少就填多少,生成完就烧,反正是能跑的。学久了之后就不一样了,有人想省掉外部晶振改用内部HSI,有人想把主频拉到芯片极限,有人换了一块板子、晶振从8MHz换成了12MHz,还有人为了省电开始配置复杂的PLL和分频器。

动时钟本身没有问题,问题在于很多人只改了入口,没管出口。时钟树是一张严格的网,PLL输入频率有范围,VCO输出频率有范围,各总线分频有上限,Flash读取有等待周期要求,USB有48MHz的固定需求。任何一环没对上,表现出来的症状都极其诡异。

3.2 四类翻车现场

我整理了一下帮人排查过的案例,高发的基本是这四类:

翻车场景典型现象根因
板子换了晶振,程序没动串口乱码、通信偶尔正常偶尔异常代码里的HSE_VALUE还是旧的晶振频率,波特率全算错
主频拉到芯片最高,没配等待周期程序跑一会儿就HardFault,或者完全随机跑飞Flash读取速度跟不上内核频率,读到错误指令
改了APB分频让外设跑快点定时器时间比预期快了或慢了一倍,PWM频率不对忘了APB1/APB2定时器时钟在分频系数不为1时会自动翻倍
省了外部晶振,直接用HSIUSB枚举失败、CAN通信异常、串口漂移HSI精度不够,且不是所有外设都接受HSI作为时钟源

这里面最典型的例子是ST官方例程里,STM32F407在168MHz主频下需要把Flash延迟设为5个等待周期(具体数值要看参考手册的频率-电压-等待周期表)。如果忘了设置,芯片在低负载下可能还能跑,但一旦执行到密集指令或者开启优化,就开始随机HardFault。这种问题调试起来特别折磨人,你以为是指针写飞了,其实是Flash没跟上。

另一个很常见的场景是定时器时钟算错。很多人只记得APB1总线时钟是42MHz,却忘了如果APB1预分频系数不为1,那么挂在APB1上的定时器时钟会自动乘以2,也就是84MHz。你把预分频器和自动重载值按42MHz来算,实际跑出来就比预期快了一倍。坑二这个名字没起错,确实是“时钟树玩太花,默认配置回不去了”。

3.3 排查思路:先信工具,再信手册,最后信自己改过的代码

踩了这个坑之后,我的排查顺序基本固定了。第一步,打开CubeMX,重新加载工程的.ioc文件,看一眼时钟树页面。CubeMX会根据你填的晶振频率、PLL参数、分频系数,自动计算所有总线频率,还会用颜色标出超范围的配置。这一步能拦住八成的问题。

第二步,看芯片参考手册里对应的时钟树章节,确认PLL输入范围、VCO输出范围、Flash等待周期、各类外设的时钟上限。有些参数CubeMX老版本不会查得特别严,尤其是你自己手改代码、绕过了CubeMX的情况下,手册就是最终解释。

第三步,也是最重要的一步:在代码里把实际频率读出来验证。调用HAL_RCC_GetSysClockFreq()看主频,调用HAL_RCC_GetPCLK1Freq()HAL_RCC_GetPCLK2Freq()看APB1/APB2频率,再和CubeMX计算值对比。如果软件读出来的值和你的预期不一致,问题一定在RCC配置,不用怀疑别的。

3.4 一条可复用的自查流程

如果你现在正好被诡异的串口乱码或者定时器时间不准困扰,可以按这条路径走一遍:

  1. 先查HSE_VALUE宏。这个宏在HAL库的配置文件里,比如STM32F4系列是stm32f4xx_hal_conf.h,F1系列是stm32f4xx_hal_conf.hstm32f1xx_hal_conf.h,确认它和你板子上实际焊接的晶振频率完全一致。换过晶振没改宏,是串口乱码的头号原因。
  2. 再确认PLL配置。用CubeMX打开工程看一遍,或者对照寄存器代码手推一遍,确认主频没超芯片最高限制。
  3. 然后查Flash等待周期。F4系列在168MHz下至少要5个等待周期,F1系列虽然逻辑不一样,但高主频下同样有关联配置。
  4. 最后核对各外设分频。重点看ADC时钟有没有超过36MHz上线(F4),USB有没有拿到精确的48MHz,定时器时钟有没有按分频规则自动翻倍。

我个人的习惯是:每改一个和时钟相关的参数,就编译烧录一次,先用MCO引脚把主频引出来拿到示波器上看一眼,确认主频对了,再继续下一步。一次只改一个变量,别把换晶振、改PLL、调Flash等待周期、换分频器一次做完,否则出了事你都分不清是哪步改坏的。

4. 坑三:SysTick和中断优先级,被“自作聪明”改出了死锁

4.1 看似简单的HAL_Delay,背后依赖一条链

第三个坑几乎每个进阶者都踩过,而且越熟练的人越容易踩:程序跑到某个地方突然卡死,最经典的表现就是HAL_Delay()再也不返回。

很多人以为HAL_Delay()就是一个简单的循环延时,像老式单片机里的for(i=0;i<100000;i++);一样。但在HAL库的工程里,这个函数依赖一条完整的链条:

HAL_Delay()内部不断读取全局变量uwTick,判断是否到了目标时间;uwTick是在SysTick_Handler()中断服务函数里,每1毫秒加1;SysTick_Handler()由SysTick定时器触发,而SysTick定时器由内核时钟驱动,优先级由NVIC决定。

这条链上任何一个环节断了,HAL_Delay()就会在for(;;)里永远空转。

4.2 三个经典的delay卡死事故

我帮人排查过无数个“卡死在延时函数里”的现场,常见的有三种:

事故一:在中断回调里调用HAL_Delay。这是HAL库用户翻车频率最高的一种。比如你在某个外设的中断回调里写了一句HAL_Delay(10);,而这个中断的优先级比SysTick更高,那么问题就来了:中断正在执行,SysTick中断无法抢占,uwTick就不会继续增加,HAL_Delay永远等不到目标时间,直接死锁。越是学得久的人,越喜欢在中断回调里加逻辑,越容易掉进这个坑。

事故二:自己“优化”了SysTick。很多人嫌SysTick的优先级不够高、或者想让延时更准,就自己把SysTick的配置改了,甚至直接关掉了SysTick定时器。改完之后,HAL库里的HAL_DelayHAL_GetTickHAL_UART_Receive这些依赖uwTick的函数全部罢工,表现出来就是程序跑着跑着卡在某个超时等待里。

事故三:关中断后调用阻塞延时。有临界区保护意识的同学会在关键操作前后关全局中断,但如果关中断之后调用了任何依赖中断的延时或超时函数,那基本就是原地死亡。比如你在__disable_irq()之后调了HAL_Delay(1),SysTick中断永远进不去,自然永远到不了1毫秒。这种写法我在项目里见过不止一次,查了三天最后发现是临界区嵌套不当。

4.3 中断优先级的反直觉:数值越小越“高”

这里还要补一个非常容易混淆的知识点:在STM32的NVIC里,优先级数值越小,实际优先级越高。所以SysTick如果被配成优先级5,某个外设中断被配成优先级1,那么在两个中断同时发生的情况下,外设中断会抢占SysTick。

如果这个外设中断又恰好调用了HAL_Delay,那就会出现上面说的死锁:外设中断占着CPU不放,SysTick没法进入,延时永远到不了。很多人排查到这里就开始怀疑“是不是我中断优先级配错了”,结果一看代码:哦,外设中断优先级数值确实比SysTick小,配置没问题,那问题就出在“在中断里调HAL_Delay”这个行为本身,而不是优先级。

另外,NVIC优先级分组也是个容易埋雷的地方。HAL库默认的优先级分组是4位抢占优先级、0位子优先级,也就是NVIC_PriorityGroup_4。FreeRTOS的移植手册也严格要求使用这个分组。如果你在项目里改成了分组2或者分组3,原来的中断优先级数值含义就全变了,可能出现几个中断互相抢占、嵌套混乱的诡异现象。学得越久越容易犯这个错,因为你会觉得“优先级分组这个东西很简单,改一下无所谓”,结果一改,全世界都乱了。

4.4 安全延时的替代方案

如果你确实需要在中断里做短延时,或者需要做一个不受SysTick影响的延时函数,我的建议是:别去折腾SysTick了,用DWT内核周期计数器,也就是Cortex-M内核自带的DWT->CYCCNT

这个计数器每到一个内核时钟周期就加1,不依赖任何外设定时器,也不依赖中断,在中断里用完全安全。初始化代码很短,直接抄过去就能用:

// 使用Cortex-M内核DWT周期计数器实现微秒级短延时,不依赖SysTick static volatile uint32_t *DWT_CYCCNT = (uint32_t *)0xE0001004; static volatile uint32_t *DWT_CTRL = (uint32_t *)0xE0001000; static volatile uint32_t *DEMCR = (uint32_t *)0xE000EDFC; void DWT_Delay_Init(void) { *DEMCR |= 1 << 24; // TRCENA: 使能DWT访问 *DWT_CTRL |= 1; // CYCCNTENA: 使能周期计数器 *DWT_CYCCNT = 0; } void DWT_Delay_us(uint32_t us) { uint32_t start = *DWT_CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((*DWT_CYCCNT - start) < ticks); }

要注意一点:SystemCoreClock必须是你当前实际的主频。如果你改了主频但没更新这个变量,延时时间就会不准。另外,这个延时不进中断、不开调试口、不依赖任何外设,所以在中断服务函数里调用是很安全的。

但话又说回来,中断服务函数里最好还是别做长时间的阻塞延时。如果一定要做,优先考虑非阻塞的思路:进中断时记一个时间戳,设置一个状态标志,退出中断后再靠主循环或状态机去处理后续动作。这样既不会死锁,代码结构也更清晰。

5. 常见问题速查与长期避坑清单

5.1 三坑相关问题速查表

我平时在群里帮人看问题,发现很多问题问到最后都能归到上面三个坑。整理成一张表放在这里,方便以后排查时直接对照:

现象可能原因处理方式
下载时报No STM32 Target FoundSWD/JTAG脚被复用成GPIO,或连接松脱、供电不足按住复位键连接、换JTAG、BOOT0拉高进Bootloader擦除Flash
串口输出乱码,且最近换过晶振HSE_VALUE宏和实际晶振频率不一致修改HAL配置文件里的HSE_VALUE,重新编译下载
程序跑着跑着随机HardFault主频升高但Flash等待周期没配够查参考手册里频率-等待周期表,补全配置
定时器时间比预期快一倍或慢一倍APB预分频为1以外的值,定时器时钟自动翻倍确认定时器所挂总线的实际时钟频率,再算预分频器
USB枚举失败、CAN异常时钟源用了内部HSI,或48MHz时钟没配好优先用外部晶振,检查USB/CAN时钟源配置
程序卡死在HAL_Delay在中断里调延时,或SysTick被禁用/优先级被破坏中断里改用DWT延时或状态机,恢复SysTick配置
中断互相抢占、调用关系混乱NVIC优先级分组被改动统一用NVIC_PriorityGroup_4,重新规划优先级数值

5.2 我踩过多次之后总结的几条原则

这几个坑踩多了之后,我现在做项目基本都会遵守几条原则,算不上什么高深理论,但确实帮我省了很多事。

第一,任何底层配置改动之前,先在本地备份一个能烧录的版本。哪怕只是复制一个文件夹也行,不需要上Git,但一定要有退路。很多“变砖”其实不吓人,吓人的是你手头连一个能恢复的固件都没有。

第二,一次只动一个变量。不管是改时钟、改优先级还是改调试口,一次只改一处,改完就烧录验证。不要同时改三四处配置,出了问题你根本不知道该查哪里。

第三,遇到玄学问题,先怀疑自己最近改过的代码,再怀疑芯片型号、编译器优化、调试器固件。排错的方向如果一开始就错了,后面全都会白费。

第四,也是最重要的一条:不要神化默认配置,但也不要随便推翻默认配置。CubeMX生成的时钟树、HAL库的SysTick配置、官方例程里的Flash等待周期,这些默认值都是经过验证的。你可以改,但前提是你清楚地知道改完会发生什么,而不是抱着“我先改成我认为对的样子”的心态去瞎试。

写在后面

玩了这么多年STM32,我最大的心得是:这芯片真的不容易坏,但真的很容易被自己的代码“锁死”。尤其是学得越久、动手能力越强,越容易忽略那些基本功一样的底层约束。我到现在还保留着一个习惯:只要动到底层配置,先在本地存一份上一个能烧录的版本,再开始改;每改一步,先验证一步,绝不等到写了一大坨代码再下载。这个习惯帮我少走了太多弯路,也希望你能用上。

最后再分享一个小技巧:遇到“连不上芯片”这种问题,先别急着买新开发板,多半不是硬件坏了,是你自己把调试口、时钟或者延时机制改出了问题。把上面的排查表打印出来贴在工位上,能省不少事儿。

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

构建AI工程师演讲索引:1124场视频到秒级定位的实战

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

作者头像 李华
网站建设 2026/9/6 3:47:32

工业组态报表开发实战:从数据采集到展示的完整链路

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

作者头像 李华
网站建设 2026/9/6 3:44:44

Python 字符串:处理文本的这几十种方法,其实常用的就那些

Python 字符串&#xff1a;处理文本的这几十种方法&#xff0c;其实常用的就那些 写 Python 代码&#xff0c;字符串操作避不开。解析日志、处理用户输入、拼 SQL、格式化输出——天天都在跟文本打交道。 Python 的字符串方法多&#xff0c;但常用的就那么十几个。字符串是什么…

作者头像 李华
网站建设 2026/9/6 3:43:12

从JDG横扫到天禄翻盘:电竞复盘的决策链路与黑八奇迹真相

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

作者头像 李华
网站建设 2026/9/6 3:43:09

从Faith_bian学编程看新手入门:方向选择、避坑指南与AI工具实践

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

作者头像 李华
网站建设 2026/9/6 3:40:38

新版Copilot Studio:Agent与Workflow的分工协作

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

作者头像 李华