STM32F407/405标准库串口DMA收发实战:从IDLE中断到双缓冲避坑指南
简介这份资源面向使用STM32F405/407进行嵌入式开发的工程师与学习者聚焦标准库下UART串口与DMA协同的数据收发实现可解决高吞吐量通信中CPU占用过高、实时性不足的问题适用于工业控制、步进电机控制等对响应速度有要求的场景。压缩包共2个文件包含1个.c源文件与1个.h头文件整体约2KB源码文件承载GPIO与UART初始化、DMA流与通道配置、收发及中断处理等核心逻辑头文件则提供函数原型、结构体与常量声明便于直接移植调用。目前已有2878人学习下载说明该实现方案在同类需求中具备一定参考价值。读者可据此掌握DMA与UART关联配置、传输方向与优先级设置、完成与错误中断处理等关键环节理解如何让CPU从数据搬运中解放出来专注执行其他任务从而提升系统整体性能与通信稳定性。1. STM32F407、405标准库串口数据收发-DMA为什么中断方式一忙就丢包串口收发是嵌入式开发里最基础也最容易翻车的一环。很多人在STM32F407、405上用标准库把串口跑通之后第一反应是「能收能发就行了」结果一旦波特率上到115200以上、数据帧变长、主循环里再塞点别的活就开始丢字节、粘包、接收缓冲区被覆盖。这时候DMA串口数据收发就成了绕不开的方案。它解决的核心问题只有一个让CPU不再逐字节搬运串口数据把搬运工作交给DMA控制器CPU只在「一帧收完」或「半缓冲满」的时候被叫醒一次。适合谁适合已经能用标准库点灯、能配NVIC、能看懂USART寄存器的新手也适合被中断收发折磨过、想找一套稳定收发框架的熟手。下面这套方案我在F407和F405上都跑过核心逻辑一致差异只在时钟树和DMA控制器编号上。2. 先把DMA串口的收发模型立住从寄存器到缓冲区的分工2.1 为什么标准库下DMA比中断更适合串口批量收发中断方式收发的本质是「每来一个字节进一次中断」。以115200波特率算一个字节大约87微秒如果主循环里有耗时操作中断响应被推迟USART的接收寄存器就会溢出。DMA方式下USART每收到一个字节硬件自动触发DMA请求DMA把数据从USART数据寄存器搬到内存缓冲区全程不经过CPU。CPU的负担从「每字节一次中断」降到「每半缓冲或整缓冲一次中断」。标准库配置DMA的关键结构体是DMA_InitTypeDef核心参数有四个DMA_PeripheralBaseAddr指向USART数据寄存器地址DMA_MemoryBaseAddr指向你的接收缓冲区DMA_BufferSize是搬运次数DMA_DIR决定方向。发送方向是内存到外设接收方向是外设到内存。这里有个容易忽略的点DMA的传输方向和外设地址、内存地址的对应关系必须严格匹配写反了不会报错但数据就是不动。2.2 接收用DMA加空闲中断发送用DMA加传输完成中断纯DMA接收有个问题你不知道一帧数据什么时候结束。常见做法是配合USART的空闲中断IDLE。总线空闲一个字节时间后触发IDLE中断此时DMA已经把所有字节搬进缓冲区你在IDLE中断里计算收到了多少字节、置一个标志位给主循环处理。发送方向用DMA加传输完成中断TC。你只需要把要发的数据放进发送缓冲区配置DMA传输长度启动DMA然后在TC中断里置发送完成标志。注意发送缓冲区的生命周期必须覆盖整个DMA传输过程如果发送缓冲区是局部变量函数返回后栈被覆盖DMA搬出去的就是垃圾数据。2.3 最小可跑的初始化顺序标准库下初始化的顺序不能乱乱了就是各种玄学问题。正确顺序是先开时钟USART时钟、DMA时钟、GPIO时钟再配GPIO复用再配USART参数再配DMA通道最后配NVIC和使能。下面这段是接收方向的DMA配置骨架// 以USART1接收为例DMA2通道5标准库写法 DMA_InitTypeDef DMA_InitStructure; USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 开时钟USART1和DMA2都在APB2/AHB1上 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // 2. GPIO复用配置PA9 TX, PA10 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // 3. USART参数115200-8-N-1 USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); // 4. DMA接收配置外设到内存循环模式 DMA_InitStructure.DMA_Channel DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)rx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize RX_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream5, DMA_InitStructure); // 5. 使能DMA和USART的DMA请求 DMA_Cmd(DMA2_Stream5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); USART_Cmd(USART1, ENABLE);这段代码里DMA_Mode_Circular是接收方向的关键。循环模式下DMA搬完一轮自动回到缓冲区头部继续搬不会停。配合IDLE中断你每次在IDLE里读DMA_GetCurrDataCounter就能知道这一帧收了多少字节。DMA_PeripheralInc_Disable必须关掉因为外设地址始终是USART数据寄存器不能自增。3. 收发双通道落地IDLE中断收帧与TC中断发帧的完整代码3.1 接收侧IDLE中断里怎么算长度、怎么清标志IDLE中断的处理逻辑是进中断后先读一次USART_ReceiveData清掉IDLE标志然后计算本次收到的字节数。计算方式是RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA2_Stream5)。这个值是从缓冲区头到当前写指针的偏移。但要注意如果DMA已经绕了一圈这个算法会出错。所以更稳妥的做法是每次处理完把DMA计数器重置或者用双缓冲。volatile uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_len 0; volatile uint8_t rx_flag 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 读DR清IDLE标志这一步不能省 volatile uint16_t temp USART1-DR; (void)temp; // 计算本次接收长度 rx_len RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA2_Stream5); rx_flag 1; // 重新配置DMA计数器准备下一帧 DMA_Cmd(DMA2_Stream5, DISABLE); DMA_SetCurrDataCounter(DMA2_Stream5, RX_BUFFER_SIZE); DMA_Cmd(DMA2_Stream5, ENABLE); } }USART_GetITStatus和直接读SR寄存器效果一样但标准库封装更清晰。清IDLE标志必须「先读SR再读DR」标准库的USART_GetITStatus内部已经做了读SR的动作所以你只需要再读一次DR。DMA_SetCurrDataCounter在DMA关闭状态下调用才有效所以要先DMA_Cmd关掉再设再开。3.2 发送侧DMA传输完成中断与发送缓冲区的生命周期发送比接收简单但缓冲区生命周期是最大的坑。下面是一个可靠的发送函数volatile uint8_t tx_done 1; uint8_t tx_buffer[TX_BUFFER_SIZE]; void uart_send_dma(uint8_t *data, uint16_t len) { if(len TX_BUFFER_SIZE) return; while(!tx_done); // 等上一次发完 memcpy(tx_buffer, data, len); tx_done 0; DMA_Cmd(DMA2_Stream7, DISABLE); DMA_SetCurrDataCounter(DMA2_Stream7, len); DMA_Cmd(DMA2_Stream7, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); } void DMA2_Stream7_IRQHandler(void) { if(DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) ! RESET) { DMA_ClearITPendingBit(DMA2_Stream7, DMA_IT_TCIF7); USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE); tx_done 1; } }tx_buffer必须是全局或静态的不能是局部数组。while(!tx_done)这个忙等在实际项目里要改成状态机或者超时机制否则一个DMA异常就会死循环。发送DMA的通道和接收不同USART1_TX对应DMA2_Stream7通道4接收对应DMA2_Stream5通道4这两个别搞混。3.3 主循环里怎么安全取走数据主循环里检测rx_flag取数据前先关IDLE中断或者做临界区保护取完再开。因为IDLE中断随时可能来如果主循环正在读rx_buffer而中断里重置了DMA数据就会错位。int main(void) { // ... 初始化 ... while(1) { if(rx_flag) { __disable_irq(); uint16_t len rx_len; uint8_t local_buf[RX_BUFFER_SIZE]; memcpy(local_buf, (const void*)rx_buffer, len); rx_flag 0; __enable_irq(); // 处理local_buf process_frame(local_buf, len); } } }__disable_irq和__enable_irq是CMSIS内建函数关中断时间要尽可能短只包住memcpy。如果帧长超过几百字节建议用双缓冲或者环形缓冲替代。4. 避坑与排查DMA串口调不通时先看这五条4.1 现象DMA配置完一个字节都收不到原因通常是时钟没开全。F407的DMA2在AHB1上USART1在APB2上GPIOA在AHB1上三个时钟缺一不可。另一个常见原因是DMA通道号选错USART1_RX固定是DMA2_Stream5通道4USART1_TX固定是DMA2_Stream7通道4这个映射关系在参考手册的DMA请求映射表里选错了DMA请求根本不会触发。解决对照手册逐个核对时钟和通道号。4.2 现象能收到数据但长度不对偶尔多几个字节这是IDLE中断里长度计算的问题。如果DMA配置成循环模式且没有在每次IDLE后重置计数器第二次进IDLE时DMA_GetCurrDataCounter返回的是剩余空间算出来的长度就是错的。解决每次IDLE处理完关闭DMA、重置计数器、再开启。或者改用双缓冲模式用DMA_IT_HTIF和DMA_IT_TCIF两个中断分别处理前后半缓冲。4.3 现象发送正常但接收偶尔丢第一字节这是IDLE中断使能时机的问题。如果在USART使能之后才开IDLE中断第一帧数据可能在中断使能前就到了IDLE标志被置位但中断没开等中断开了标志已经在了但DMA可能还没启动。解决先配DMA并使能再使能USART的DMA请求最后开IDLE中断顺序不能反。4.4 现象高波特率下数据错位字节顺序乱了检查DMA_PeripheralDataSize和DMA_MemoryDataSize是否都设成了Byte。如果设成HalfWordDMA会按16位搬运USART数据寄存器是8位的就会把两个字节拼成一个半字顺序全乱。另外检查DMA_MemoryInc是否使能不使能的话所有数据都写到缓冲区第一个字节。4.5 现象调试器暂停再运行后DMA就不工作了这是调试时的常见现象。调试器暂停CPU时DMA可能还在跑恢复后DMA状态和USART状态不同步。解决调试串口DMA时尽量用硬件断点或者在暂停后手动复位DMA流。更彻底的办法是在初始化里加DMA复位流程DMA_DeInit再重新DMA_Init。5. 进阶技巧用双缓冲把接收抖动压到最低单缓冲加IDLE中断在大多数场景够用但如果你的协议帧长短不一、帧间隔又很短单缓冲会有处理窗口期。双缓冲的思路是让DMA在两个缓冲区之间交替搬运一个在收的时候另一个在处理CPU永远不用等DMA。标准库下实现双缓冲有两种方式。第一种是用DMA的双缓冲模式配置DMA_MemoryBurst和DMA_DoubleBufferMode但标准库对双缓冲的封装比较薄需要直接操作寄存器。第二种更通用用两个独立的DMA流或者一个流配合半传输中断。我一般用半传输中断方案配置如下// 缓冲区大小设为2倍帧长半传输中断和传输完成中断各代表半缓冲满 #define BUF_SIZE 256 uint8_t dma_buf[BUF_SIZE]; // 在DMA初始化后使能两个中断 DMA_ITConfig(DMA2_Stream5, DMA_IT_HT | DMA_IT_TC, ENABLE); void DMA2_Stream5_IRQHandler(void) { if(DMA_GetITStatus(DMA2_Stream5, DMA_IT_HTIF5) ! RESET) { DMA_ClearITPendingBit(DMA2_Stream5, DMA_IT_HTIF5); // 前半缓冲满处理dma_buf[0..127] process_half(dma_buf[0], BUF_SIZE/2); } if(DMA_GetITStatus(DMA2_Stream5, DMA_IT_TCIF5) ! RESET) { DMA_ClearITPendingBit(DMA2_Stream5, DMA_IT_TCIF5); // 后半缓冲满处理dma_buf[128..255] process_half(dma_buf[BUF_SIZE/2], BUF_SIZE/2); } }这个方案的关键参数是BUF_SIZE要大于等于两倍的最大帧长。如果帧长可能超过半缓冲就需要在process_half里做帧重组。另外注意DMA_IT_HT和DMA_IT_TC两个中断的清除顺序先清哪个不影响但两个都要清漏清一个会反复进中断。验证双缓冲是否正常最直接的方法是在两个中断里各翻转一个GPIO用示波器看两个GPIO的翻转频率是否一致。如果其中一个明显偏慢说明对应的半缓冲处理超时了DMA已经绕回来覆盖了还没处理的数据。这时候要么加大BUF_SIZE要么优化process_half的执行时间。我自己的习惯是任何用DMA收发的串口第一件事不是写协议解析而是先用双缓冲加GPIO翻转把底层收发的稳定性验证一遍。底层不稳上层协议写得再漂亮都是白搭。这个习惯帮我省掉了至少三次在协议层找丢包原因的弯路。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

TensorFlow 2.0实战Wide  Deep模型:原理、代码与调优全解析

TensorFlow 2.0实战Wide Deep模型:原理、代码与调优全解析

TensorFlow 2.0入门到进阶系列写到现在,前面已经跑通了DNN、CNN、RNN这些基础结构。这一篇来聊一个在推荐系统、广告CTR预估场景里非常经典,而且至今还在大量使用的模型——Wide & Deep。我最早接触这个模型,还是在某次技术交流会上&…

2026/10/16 15:53:15 阅读更多 →
STM32F10x标准库V3.5.0工程搭建与避坑指南

STM32F10x标准库V3.5.0工程搭建与避坑指南

简介:STM32F10x-StdPeriph-Lib-V3.5.0标准库文件面向使用意法半导体STM32F10x系列微控制器的嵌入式开发者,尤其适合刚接触底层寄存器操作、希望借助成熟外设驱动快速搭建工程的学生与工程师。它提供GPIO、ADC、DAC、TIM、SPI、I2C、USART等内置外设的完整…

2026/10/16 15:53:15 阅读更多 →
STM32避障小车源码解析:超声波+PWM+状态机实现自动导航

STM32避障小车源码解析:超声波+PWM+状态机实现自动导航

简介:面向嵌入式与智能小车爱好者,这是一份基于STM32F103VC的避障智能小车完整工程源码,以超声波传感器HC-SR04探测障碍物,通过舵机CDS5516控制转向,实现自主避障。工程包含初始化、传感器驱动、PWM舵机控制、避障逻辑…

2026/10/16 15:53:15 阅读更多 →

最新新闻

InternVL2-Pretrain-Models的Pixel Shuffle与mlp1投影器:图像特征如何压缩成LLM的词汇

InternVL2-Pretrain-Models的Pixel Shuffle与mlp1投影器:图像特征如何压缩成LLM的词汇

【免费下载链接】InternVL2-Pretrain-Models 项目地址: https://ai.gitcode.com/OpenGVLab/InternVL2-Pretrain-Models 点击查看 免费下载 InternVL2-Pretrain-Models 是 InternVL 2.0 多模态大语言模型 Stage-1 阶段的多模态预训练模型仓库,提供从 Int…

2026/10/16 16:24:17 阅读更多 →
EN 300 132-1接口A1标准解读:电压暂降、冲击电流与保护器件选型实战

EN 300 132-1接口A1标准解读:电压暂降、冲击电流与保护器件选型实战

简介:《ETSI EN 300 132-1 V2.1.1(2019-03)》是欧洲电信标准协会发布的环境工程与信息通信技术(ICT)设备电源接口标准,其第1部分规定了交流电输入接口的电压范围、电流等级、插头插座类型、过载及接地保护等…

2026/10/16 16:23:15 阅读更多 →
NASA CEA程序全解析:从化学平衡到火箭比冲计算实战

NASA CEA程序全解析:从化学平衡到火箭比冲计算实战

简介:CEA(Chemical Equilibrium with Applications)是NASA开发的热化学平衡计算程序,能模拟火箭发动机燃烧室内的高温高压化学反应,求解平衡组分、温度分布及焓熵等热力学参数,常被用于不同燃料/氧化剂组合…

2026/10/16 16:23:15 阅读更多 →
云原生入门:用Minikube跑通自愈型Hello World

云原生入门:用Minikube跑通自愈型Hello World

简介:本资源是一份系统化、分层级的云原生技术学习路线图,面向开发者、运维工程师及希望转型云原生领域的IT从业者,解决技术栈庞杂、学习路径不清晰、各组件关系难把握等核心痛点。路线图PDF文件共1个,大小1.29MB,内容…

2026/10/16 16:23:15 阅读更多 →
职业心理测评系统源码怎么选?二次开发避坑指南

职业心理测评系统源码怎么选?二次开发避坑指南

简介:这是一套面向职业心理测评场景的完整管理系统源码,适合需要搭建或二次开发测评平台的开发者、企业HR与就业指导机构。系统围绕职业倾向、性格特征、工作适应力等维度,覆盖用户认证、测评数据采集、结果生成、反馈展示,以及用…

2026/10/16 16:23:15 阅读更多 →
免停电免布线的开关柜局放在线监测:工程落地与选型要点

免停电免布线的开关柜局放在线监测:工程落地与选型要点

前阵子到一个工厂配电房看现场,配电柜的母线室局部放电量已经连续三次巡检超过告警线,运维负责人跟我说得很直白:柜子不敢停,负荷没法切;柜内二次小室塞满了,电缆沟和桥架也走不了线,厂里一年只…

2026/10/16 16:23:15 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* 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 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

2026/10/15 16:18:54 阅读更多 →
影刀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/15 0:14:36 阅读更多 →
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 阅读更多 →