STM32F103 UART中断编程实战:从轮询到事件驱动的效率提升
1. 项目概述从轮询到中断的通信效率革命在嵌入式开发尤其是基于STM32这类MCU的项目里串口通信UART几乎是工程师的“必修课”。无论是打印调试信息、与上位机交互还是连接蓝牙、GPS等外设模块UART都扮演着关键角色。然而很多初学者甚至一些有经验的开发者在实现UART通信时往往停留在最基础的“轮询”方式——也就是在主循环里不停地去查询串口接收寄存器有没有新数据。这种方式简单直接但效率极低CPU大部分时间都在做无意义的等待严重浪费了宝贵的计算资源系统也无法及时响应其他事件。这就像你派一个人去门口等快递他必须一直站在门口什么其他事都干不了直到快递员出现。而“中断”机制则是给门铃装了个响铃。快递来了门铃一响中断触发这个人CPU才放下手头的事去开门执行中断服务函数处理完快递处理数据后立刻回去继续之前的工作。整个系统的响应速度和并发处理能力得到了质的飞跃。今天我们就以STM32F103这款经典的“蓝桥杯”神器、学生入门首选MCU为例深入探讨如何为其UART配置和使用中断服务函数。这不仅仅是配置几个寄存器那么简单更是理解STM32中断系统NVIC、编写高效可靠通信代码的核心技能。掌握了它你的程序将从“单线程阻塞”升级为“事件驱动”为更复杂的多任务系统打下坚实基础。2. 核心思路与硬件框架解析2.1 为什么选择STM32F103的UART中断STM32F103系列MCU内部通常集成了多个USART通用同步异步收发器和UART通用异步收发器外设例如USART1、USART2、USART3等。这些外设功能强大支持全双工异步通信、单线半双工通信、红外调制解调等多种模式。其核心优势在于丰富的中断源我们可以让UART在多种事件发生时触发中断例如接收数据寄存器非空RXNE收到一个字节的数据这是最常用的接收中断。发送数据寄存器空TXE发送寄存器已空可以写入下一个待发送字节用于实现非阻塞发送。传输完成TC最后一个数据帧的停止位发送完毕用于精确判断一次发送过程的结束。空闲线路检测IDLE串口总线在收到数据后持续一段时间一个字符帧的时间没有新数据用于接收不定长数据帧。通过中断CPU无需主动查询这些状态从而被解放出来执行其他任务如传感器数据采集、算法运算、状态机维护等。对于STM32F103实现UART中断通信本质上是完成以下三层的配置与协作外设层UART本身配置UART的波特率、数据位、停止位等基本参数并开启所需的中断源如RXNE中断。中断控制器层NVIC配置嵌套向量中断控制器NVIC为指定的UART中断通道设置优先级并全局使能该中断。应用层用户代码编写对应的中断服务函数ISR在其中安全、高效地处理接收或发送数据。2.2 NVIC中断系统的“交通警察”NVIC是Cortex-M内核的一部分是管理所有中断的“大脑”。对于STM32F103你需要理解几个关键概念中断通道IRQn每个外设的中断都有一个唯一的编号例如USART1的全局中断通道是USART1_IRQn。抢占优先级Preemption Priority和子优先级SubprioritySTM32F103的NVIC支持优先级分组。你可以通过HAL_NVIC_SetPriorityGrouping函数设置分组方式例如分组4即4位抢占优先级0位子优先级。抢占优先级高的中断可以打断正在执行的抢占优先级低的中断而相同抢占优先级的中断之间子优先级高的先执行但不能互相打断。使能与失能需要分别使能NVIC中的特定中断通道以及外设本身的特定中断源中断才能真正生效。一个常见的策略是将UART接收中断的抢占优先级设置为较高但不要高于系统关键中断如SysTick以确保通信数据不会因为被其他低优先级任务长时间阻塞而丢失。3. 开发环境搭建与工程配置3.1 工具链选择与工程创建对于STM32F103开发目前主流且高效的方式是使用ST官方推出的STM32CubeIDE。它集成了STM32CubeMX图形化配置工具和基于Eclipse的IDE可以无缝完成从芯片选型、外设配置、中间件使能到代码生成、编译调试的全过程。首先打开STM32CubeIDE创建一个新的STM32项目。在芯片选择器中输入“STM32F103C8”这里以常见的64引脚中等容量型号为例然后开始配置。3.2 使用STM32CubeMX进行图形化配置在项目初始化后的图形化界面我们可以直观地进行配置引脚分配在芯片图上找到你想要使用的UART引脚。例如使用USART1通常PA9是TXPA10是RX。点击这两个引脚将其功能模式设置为“USART1_TX”和“USART1_RX”。外设参数配置在左侧边栏找到“USART1”进入其配置页面。Basic Parameters:Baud Rate: 设置为常用波特率如115200。Word Length: 8 Bits最常用。Parity: None无校验。Stop Bits: 1。Over Sampling: 16倍过采样默认抗干扰性较好。NVIC Settings这是关键的一步勾选“USART1 global interrupt”使能NVIC中断。然后点击其后的齿轮图标可以设置抢占优先级和子优先级。例如可以设置为抢占优先级1子优先级0。生成代码配置完成后点击“Project - Generate Code”。CubeMX会生成完整的初始化代码包括HAL_UART_MspInit函数其中完成了GPIO和NVIC的初始化以及MX_USART1_UART_Init函数配置UART参数。注意CubeMX生成的代码默认使用HAL库。HAL库封装程度高易于移植但执行效率相对标准外设库SPL稍低。对于极致性能要求的场景你可能需要直接操作寄存器或使用SPL但HAL库对于快速开发和理解流程非常有帮助。4. UART中断服务函数深度解析与实现4.1 中断服务函数的框架与编写要点STM32CubeIDE生成的代码中中断服务函数ISR的“壳子”已经由启动文件定义好了。例如USART1的中断服务函数名为USART1_IRQHandler。这个函数在stm32f1xx_it.c文件中。通常我们不会直接在这个函数里写大量业务逻辑而是调用HAL库的中断处理函数HAL_UART_IRQHandler然后在对应的回调函数中实现我们的功能。核心流程如下当USART1发生中断如收到数据时CPU跳转到USART1_IRQHandler。USART1_IRQHandler内部调用HAL_UART_IRQHandler(huart1)。HAL_UART_IRQHandler这个库函数会判断具体是哪种中断源RXNETXETC然后清除中断标志位并调用相应的回调函数Callback。我们需要重写Weak Override这些回调函数以加入自己的数据处理逻辑。4.2 接收中断的完整实现示例假设我们需要通过中断方式接收不定长的字符串并以回车符\r或\n作为一帧数据的结束标志。步骤一启动接收中断在main函数初始化完外设后我们需要启动UART的接收中断。HAL库提供了HAL_UART_Receive_IT函数。// 定义一个接收缓冲区 uint8_t rx_buffer[256]; uint16_t rx_index 0; // 在main初始化部分启动接收中断 // 参数UART句柄接收缓冲区地址每次中断期望接收的字节数 HAL_UART_Receive_IT(huart1, rx_buffer[rx_index], 1);这行代码的意思是使能USART1的RXNE中断并且告诉HAL库每次收到1个字节就把它存放到rx_buffer[rx_index]这个地址然后调用接收完成回调函数。步骤二重写接收完成回调函数HAL库中接收完成回调函数是一个弱定义__weak的函数HAL_UART_RxCpltCallback。我们需要在main.c或自己的用户文件中重新实现它。// 重写接收完成回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 判断是哪个UART触发的中断 if(huart-Instance USART1) { // 处理刚刚收到的1个字节rx_buffer[rx_index] if(rx_buffer[rx_index] \r || rx_buffer[rx_index] \n) { // 收到结束符一帧数据接收完成 rx_buffer[rx_index] \0; // 添加字符串结束符 printf(Received: %s\r\n, rx_buffer); // 回显需实现printf重定向 // 处理接收到的完整数据...例如设置一个标志位通知主循环 data_received_flag 1; // 重置索引准备接收下一帧 rx_index 0; } else { // 不是结束符索引加一准备接收下一个字节 rx_index; // 防止缓冲区溢出 if(rx_index sizeof(rx_buffer)) { rx_index 0; // 可以在此处发送错误信息 } } // 至关重要重新启动接收中断以等待下一个字节 HAL_UART_Receive_IT(huart1, rx_buffer[rx_index], 1); } }关键点解析回调函数参数huart指向触发中断的UART句柄通过判断huart-Instance可以区分多个UART。数据处理在回调函数中对收到的单字节进行判断。这里实现了简单的帧尾判断。重新启动中断这是最容易被遗忘的一步HAL_UART_Receive_IT是一次性的。处理完一个字节后必须再次调用它否则UART将不再触发接收中断。这是HAL库中断模式与标准库或直接寄存器操作的一个重要区别。缓冲区管理在中断服务函数中应避免进行复杂、耗时的操作如浮点运算、长时间循环。我们的原则是“快进快出”。通常只做最简单的数据搬运和标志位设置将复杂的解析工作留给主循环。4.3 发送中断与空闲中断的进阶应用发送中断TXE常用于实现非阻塞发送。当你需要发送一大段数据时可以调用HAL_UART_Transmit_IT它会启动发送中断在HAL_UART_TxCpltCallback回调中处理发送完成后的工作如释放缓冲区、通知主任务等。这避免了HAL_UART_Transmit函数的阻塞等待。空闲中断IDLE是接收不定长数据的“神器”。它的触发时机是在接收到一字节数据后总线保持空闲状态超过一帧的时间。结合DMA或普通接收中断可以非常优雅地获取一包完整数据。使能空闲中断__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);在USART1_IRQHandler中或HAL_UART_IRQHandler之前判断空闲中断标志。void USART1_IRQHandler(void) { // 检测是否是空闲中断 if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除空闲中断标志重要 // 在这里可以认为一帧数据已经接收完毕 // 计算DMA已传输的数据量或者根据接收索引得到数据长度 idle_received_flag 1; } HAL_UART_IRQHandler(huart1); // 继续处理其他UART中断 }在主循环中检测idle_received_flag然后处理接收缓冲区里的完整数据包。实操心得对于高速、大数据量的连续接收“空闲中断 DMA”是黄金组合。DMA负责将数据自动搬运到内存完全不占用CPU空闲中断在数据流暂停时通知CPU。CPU只在整包数据就绪后才介入处理效率极高。在CubeMX中配置UART的DMA接收并勾选“USART1 global interrupt”以使用空闲中断即可轻松实现此模式。5. 中断服务函数编写的高级技巧与避坑指南5.1 中断服务函数的设计原则保持简短ShortISR执行时间应尽可能短。长时间占用中断会导致其他低优先级中断无法响应甚至可能使系统看门狗超时复位。复杂的算法、浮点运算、延时函数如HAL_Delay绝对禁止出现在ISR中。避免阻塞Non-Blocking不要在ISR中等待某个外部事件。ISR应该是“触发-处理-返回”的快速通道。共享数据保护Reentrancy VolatileISR和主循环之间共享的变量如缓冲区索引rx_index、数据就绪标志data_received_flag必须使用volatile关键字声明防止编译器优化导致数据不一致。对于更复杂的共享数据结构如果主循环和ISR都可能修改需要考虑使用简单的关中断/开中断__disable_irq()/__enable_irq()进行保护但需谨慎使用因为会增大中断延迟。volatile uint8_t data_received_flag 0; // 必须加volatile及时清除标志位进入ISR后应尽快清除触发本次中断的外设标志位如UART的RXNE、IDLE等。HAL库的HAL_UART_IRQHandler函数内部已经帮我们做了这件事但如果你直接操作寄存器或使用空闲中断务必手动清除UART_FLAG_IDLE等标志否则会连续触发中断。5.2 常见问题排查实录在实际开发中UART中断不工作是最常见的问题。下面是一个排查清单现象可能原因排查步骤与解决方案完全收不到数据1. 引脚配置错误2. 波特率不匹配3. 中断未使能1. 用万用表或逻辑分析仪检查TX/RX引脚是否与对方交叉连接。2. 确认双方波特率、数据位、停止位、校验位完全一致。用示波器测量一个字节的时长反算波特率。3. 检查CubeMX中是否勾选NVIC中断并检查生成的HAL_UART_MspInit中NVIC配置代码是否存在。在main中是否调用了HAL_UART_Receive_IT。只能收到第一个字节未在回调函数中重新启动接收中断在HAL_UART_RxCpltCallback函数末尾务必再次调用HAL_UART_Receive_IT。数据接收混乱、错位1. 缓冲区溢出2. 中断处理太慢数据丢失3. 共享变量未加volatile1. 增加缓冲区大小并在索引增加时检查边界。2. 优化ISR代码确保其执行时间远小于字节间隔时间1/波特率 * 10。3. 给所有在ISR和主循环间共享的全局变量加上volatile关键字。发送中断无法触发1. 发送函数调用错误2. 发送缓冲区为空1. 确保调用的是HAL_UART_Transmit_IT而非HAL_UART_Transmit。2. 确保传递给HAL_UART_Transmit_IT的数据长度大于0且缓冲区地址有效。程序运行异常偶尔死机1. 中断服务函数栈溢出2. 在ISR中调用了不可重入函数1. 在启动文件或链接脚本中适当增大堆栈Stack大小。2. 绝对避免在ISR中使用printf、malloc、HAL_Delay等函数。如需调试可以设置一个标志在主循环中打印。一个典型的调试技巧当你怀疑中断是否触发时可以在USART1_IRQHandler函数的最开头设置一个GPIO引脚的电平翻转。void USART1_IRQHandler(void) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 用LED或示波器探头观察 HAL_UART_IRQHandler(huart1); }然后用示波器观察这个GPIO引脚。如果每次收到数据都能看到方波说明中断成功触发问题可能出在HAL库处理或回调函数中如果没有方波则说明中断配置有问题。5.3 多串口中断管理与优先级规划在一个复杂的项目中可能会使用多个UART同时与不同设备通信。这时合理规划NVIC中断优先级至关重要。策略建议根据实时性要求分配对响应速度要求最高的通信接口例如紧急命令接收应分配最高的抢占优先级。避免优先级反转如果高优先级中断的服务函数执行时间很长它会阻塞所有低优先级中断。因此即使优先级高其ISR也应保持简短。使用不同的回调函数在HAL_UART_RxCpltCallback中通过huart-Instance区分是USART1、USART2还是USART3然后分别处理。void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1){ // 处理USART1数据 } else if(huart-Instance USART2){ // 处理USART2数据 } // ... }考虑使用RTOS当通信逻辑非常复杂时更高级的做法是结合实时操作系统RTOS如FreeRTOS。你可以将每个UART的接收任务设计成一个独立的RTOS任务或队列。在UART接收中断回调函数中仅仅将数据放入队列xQueueSendFromISR然后由专门的任务从队列中取出并处理。这样彻底将耗时操作与中断解耦系统结构更清晰、健壮。从轮询到中断是嵌入式程序员思维的一次重要升级。它要求我们从“顺序执行”的思维转向“事件响应”的思维。STM32F103的UART中断实现虽然底层有HAL库为我们简化了大量工作但理解其背后的机制——从NVIC优先级管理到中断服务函数的编写规范从数据缓冲区的设计到多外设的协同——这些才是写出稳定、高效嵌入式代码的核心。我个人的体会是初期多踩几次“忘了重启接收中断”、“缓冲区溢出”的坑并不是坏事每一次排查和解决这些问题的过程都会让你对中断机制的理解加深一层。当你能够熟练运用中断、DMA甚至结合RTOS来构建你的应用时你会发现手中的STM32不再是一个简单的单片机而是一个真正能够并发处理多任务的微型计算机系统。

相关新闻

Java强制类型转换全解析:从基础规则到实战避坑指南

Java强制类型转换全解析:从基础规则到实战避坑指南

1. 项目概述:为什么我们需要深入理解强制类型转换?如果你写过Java代码,大概率见过类似(int) someDouble这样的写法。这行简单的代码背后,就是Java强制类型转换(Explicit Type Casting)的典型应用。它看起来…

2026/10/16 9:49:47 阅读更多 →
从 IDC 迁移到云原生数据库的 TCO 怎么算?去 IDC 成本分析

从 IDC 迁移到云原生数据库的 TCO 怎么算?去 IDC 成本分析

阿里云 PolarDB(云原生数据库)是企业从 IDC 自建数据库上云的推荐目标:100% 兼容 MySQL 协议让应用几乎零改造迁移,存储计算分离把硬件预留成本转为按量付费,配合 DTS 数据迁移工具实现平滑割接,是降低去 I…

2026/10/16 15:32:33 阅读更多 →
ModbusRTU功能码实战指南:从通信原理到嵌入式实现

ModbusRTU功能码实战指南:从通信原理到嵌入式实现

1. 从一次通信故障说起:为什么必须搞懂ModbusRTU功能码前段时间,我接手了一个工业现场的数据采集项目,设备是一台老旧的温控仪表,通信协议写着ModbusRTU。我信心满满地接上线,用调试助手发了条读取温度的指令&#xff…

2026/10/16 8:52:37 阅读更多 →

最新新闻

PAT 1046最短距离题的I/O性能陷阱与前缀和优化

PAT 1046最短距离题的I/O性能陷阱与前缀和优化

1. 这道题不是考算法,是考“读题陷阱”的临场反应看到标题里那个醒目的“21行代码AC”,再配上“1046 Shortest Distance (20 分)”这个编号,第一反应往往是:又一道水题?不就是环形数组求两点间最短路径嘛,前…

2026/10/17 0:18:50 阅读更多 →
H3C GB0-670认证考点解析:MSA存储、SAN组网与RAID知识点梳理

H3C GB0-670认证考点解析:MSA存储、SAN组网与RAID知识点梳理

简介:面向H3C认证工程师-MSA存储(GB0-670)考试的题库文档,适合正在备考H3CNE-MSA认证的存储工程师、网络管理员及售后服务人员。文档以问答形式整理了MSA 2042与MSA 2040差异、主机访问协议(iSCSI/SAS/FC/InfiniBand&a…

2026/10/17 0:18:50 阅读更多 →
工业蒸汽量预测:物理引导的时序建模实战

工业蒸汽量预测:物理引导的时序建模实战

1. 项目概述:这不是一场“刷分游戏”,而是一次工业场景下的真实建模实战“工业蒸汽量预测”——光看这六个字,很多人第一反应是“又一个回归赛题”,点开天池学习赛页面,看到特征表里密密麻麻的“入口温度”“炉膛压力”…

2026/10/17 0:18:50 阅读更多 →
5G KPI体系分层架构与指标计算口径:从接入到体验的排查实战

5G KPI体系分层架构与指标计算口径:从接入到体验的排查实战

简介:面向运营商网络优化工程师与无线网规人员,这份演示文稿系统梳理了爱立信5G-KPI体系,用于帮助读者理解5G网络性能评估的量化指标框架。内容涵盖从单站健康检查、簇优化到全网优化的层级划分,并覆盖接入性、保持性、移动性、完…

2026/10/17 0:18:50 阅读更多 →
ChatGPT生成原理六讲:从Token到人话的工程链路

ChatGPT生成原理六讲:从Token到人话的工程链路

简介:本资源是一份面向AI初学者与NLP从业者的ChatGPT原理深度解析文档,聚焦语言模型底层机制与工程实现逻辑,帮助读者突破“黑箱”认知,系统理解对话生成技术的演进路径与关键技术栈。全文约四千字,以清晰的技术脉络展…

2026/10/17 0:18:50 阅读更多 →
HEIF/HEIC深度解析:从Box结构到属性链的完整指南

HEIF/HEIC深度解析:从Box结构到属性链的完整指南

简介:ISO/IEC 23008-12:2017 是国际标准《信息技术 异构环境中的高效编码和媒体交付 第12部分:图像文件格式》的官方 PDF 原文,对应 HEIF/HEIC 格式,面向图像编解码、流媒体传输、跨平台格式兼容等方向的工程师、研究人员及标准合…

2026/10/17 0:17:49 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →