news 2026/9/3 7:56:16

STM32 ADC采集MQ-2烟雾传感器数据:从硬件连接到软件滤波全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC采集MQ-2烟雾传感器数据:从硬件连接到软件滤波全解析

简介:本资源是一份面向嵌入式初学者与STM32实践者的完整硬件接口项目案例,聚焦STM32微控制器与MQ2烟雾浓度传感器的软硬协同开发,解决气体浓度采集、ADC数据转换及串口实时打印等典型嵌入式传感应用问题。压缩包共182个文件,含31个C源文件(如stm32f10x_adc.c、stm32f10x_usart.c)、32个头文件(h)、32个目标文件(o)及配套链接脚本、映射文件(map)、可执行镜像(axf/hex)等,完整覆盖Keil MDK工程结构,包体大小为4.58MB。已有62人学习下载,适用于课程设计、毕业实践或IoT传感模块快速验证。用户可直接编译运行,获得从硬件连接说明、ADC初始化配置、模拟电压—浓度换算逻辑到串口数值输出的全流程代码支撑,尤其包含针对STM32F10x系列的底层驱动适配与调试关键点注释,显著降低传感器接入门槛。

1. 项目概述:从传感器到数据

最近在整理工作室的杂物,翻出来几个MQ-2烟雾传感器模块,这玩意儿在智能家居、安防报警里算是常客了。正好手边有块STM32F103C8T6的核心板,就想着干脆搭个简单的系统,把传感器读到的模拟浓度值,通过串口实时打印到电脑上看看。这个项目听起来简单,不就是“读ADC -> 串口打印”吗?但真做起来,从硬件连接到软件滤波,再到数据标定,每一步都有不少细节值得琢磨。它非常适合刚接触STM32和传感器的新手,作为第一个完整的“感知-处理-输出”项目来练手,也能让有经验的开发者回顾一下ADC配置、串口通信这些基础但至关重要的环节。

整个过程,我们将围绕STM32的ADC(模数转换器)功能展开,学习如何采集MQ-2输出的模拟电压,并将这个电压值转换为我们能理解的浓度相关数值,最后通过串口助手清晰地展示出来。你会接触到硬件电路的连接、STM32CubeMX的图形化配置、HAL库函数的使用,以及一些简单的数据处理技巧。无论你是想做一个烟雾报警器的原型,还是单纯想理解传感器如何与微控制器“对话”,这个项目都能给你一个直观的答案。

2. 核心硬件解析与连接方案

2.1 MQ-2传感器模块探秘

MQ-2是一个广谱型半导体气敏传感器,对液化气、丙烷、氢气、烟雾等可燃气体和烟雾都比较敏感。我们通常买到的是已经集成好的模块,这大大降低了使用难度。模块的核心是MQ-2气敏元件和一个比较器电路(比如LM393)。

模块通常有4个引脚:

  • VCC: 供电引脚,接5V或3.3V。注意:虽然模块逻辑电平是3.3V兼容,但传感器加热丝部分通常需要5V供电才能达到最佳工作状态。务必查看你的模块说明书,很多模块的VCC接5V,DO和AO输出仍是3.3V电平。
  • GND: 接地。
  • DO: 数字量输出。模块上通常有一个电位器,可以调节检测阈值。当传感器感知的浓度超过这个阈值时,DO引脚输出低电平(反之高电平),同时板载指示灯亮起。它适合做简单的开关报警。
  • AO: 模拟量输出。这是我们本次项目的重点。它会输出一个与气体浓度相关的模拟电压值(通常是0-VCC之间)。浓度越高,输出电压一般也越高。

模块上电后需要一段预热时间(通常1-2分钟),因为内部的陶瓷传感元件需要被加热丝加热到一定的工作温度后,阻值才会稳定并对气体敏感。刚上电时的读数剧烈变化是正常现象。

2.2 STM32的ADC接口与电路连接

我们使用STM32的ADC来读取AO脚的模拟电压。以STM32F103C8T6为例,它拥有2个12位的ADC,精度为4096(2^12)个等级。假设我们使用3.3V作为ADC的参考电压(Vref+),那么ADC能分辨的最小电压变化是3.3V / 4096 ≈ 0.0008V (0.8mV)

连接电路图非常简单:

  1. MQ-2模块 VCC-> 开发板5V引脚(或外部5V电源)。
  2. MQ-2模块 GND-> 开发板GND
  3. MQ-2模块 AO-> 开发板某个ADC输入引脚,例如PA0(对应ADC1的通道0)。
  4. 开发板的VDD(MCU供电)接3.3V

这里有一个关键细节:MQ-2的AO输出范围是0到它的VCC(假设5V)。如果我们用3.3V参考电压的ADC去测量一个可能接近5V的电压,会存在风险!超过3.3V的输入可能会损坏STM32的ADC引脚。因此,强烈建议在AO引脚和STM32的ADC引脚之间,加入一个简单的电阻分压电路,例如用两个电阻(10kΩ和20kΩ)将电压衰减到原来的1/3(5V -> 1.67V,在安全范围内)。或者,确认你的模块AO输出上限不超过3.3V。稳妥起见,加个分压电路是万全之策。

注意:如果你使用的是3.3V给整个模块供电,那么AO输出最大就是3.3V,可以直接连接。但传感器灵敏度可能会因供电电压不同而略有差异。

2.3 工具选型:为什么是HAL库与CubeMX

对于这个项目,我选择使用STM32CubeMX进行初始化配置,并基于HAL库(硬件抽象层)编写代码。原因如下:

  • 快速原型开发:CubeMX通过图形界面配置时钟、引脚、ADC参数、串口参数,能自动生成初始化代码,避免了繁琐的寄存器操作,让我们能快速聚焦在应用逻辑上。
  • 可移植性:HAL库的API在不同系列STM32芯片间有较好的一致性。今天在F103上写的ADC读取代码,稍作修改就能移植到F407或G0系列上。
  • 维护与可读性:对于初学者和中小型项目,HAL库代码结构清晰,更易于理解和维护。

当然,标准外设库(SPL)或直接寄存器操作效率更高,但开发速度稍慢。本项目以功能实现和清晰理解为目标,HAL库是更合适的选择。

3. 软件环境搭建与CubeMX工程配置

3.1 创建工程与核心外设配置

首先,打开STM32CubeMX,新建一个工程,选择你的具体芯片型号(如STM32F103C8Tx)。

  1. 系统核心(SYS): 在“SYS”选项卡下,将“Debug”设置为“Serial Wire”。这样可以使用ST-LINK进行下载和调试。
  2. 时钟配置(RCC): 在“RCC”选项卡下,将“High Speed Clock (HSE)”设置为“Crystal/Ceramic Resonator”。我们的开发板通常外部接有8MHz晶振。
  3. 时钟树配置: 进入“Clock Configuration”标签页。这是关键一步。我们需要为ADC和USART提供正确的时钟。
    • 将HSE输入频率设为8MHz。
    • 将PLL源选择为HSE。
    • 调整PLL倍频因子,使系统时钟(SYSCLK)达到72MHz(对于F103系列是常见最高频率)。
    • 查看APB2总线时钟(PCLK2),ADC通常挂载在此总线下。确保其频率不超过72MHz(通常设为72MHz或36MHz)。ADC的时钟需要进一步分频,后面会设置。
  4. ADC1配置
    • 在“Pinout & Configuration”界面左侧,找到“Analog”下的“ADC1”。
    • 启用一个通道,例如“IN0”(对应PA0引脚)。PA0引脚在右侧的芯片图上会自动被标记为“ADC1_IN0”。
    • 在右侧的“Parameter Settings”选项卡中,进行关键配置:
      • Resolution: 设置为“12-bit”。这是我们需要的精度。
      • Scan Conversion Mode: 禁用(Disable)。我们只扫描一个通道。
      • Continuous Conversion Mode: 启用(Enable)。这样ADC会连续自动转换,我们只需读取数据即可。
      • Discontinuous Conversion Mode: 禁用。
      • DMA Continuous Requests: 禁用。我们先用轮询方式,简单直接。
      • End Of Conversion Selection: 保持默认。
      • Clock Prescaler: 选择分频系数,确保ADC时钟(ADCCLK)不超过14MHz(F103的极限)。例如,如果PCLK2是72MHz,分频系数选“PCLK2 divided by 6”,得到12MHz,是安全的。
      • Sampling Time: 采样时间。对于MQ-2这样的慢速传感器,不需要很高的采样速率。可以设置为“239.5 Cycles”以获得更稳定的采样。采样时间越长,ADC内部电容充电越充分,抗噪声能力越强,但转换速度会变慢。这里我们追求稳定性。
  5. USART1配置(用于串口打印)
    • 找到“Connectivity”下的“USART1”。
    • 将模式设置为“Asynchronous”(异步通信)。
    • 引脚会自动分配为PA9(TX)和PA10(RX)。
    • 在参数设置中,配置波特率为“115200”,字长“8 Bits”,停止位“1”,无奇偶校验,无硬件流控。
  6. 生成工程代码
    • 转到“Project Manager”选项卡,设置项目名称、存储路径,选择IDE(如MDK-ARM V5)。
    • 在“Code Generator”部分,选择“Copy only necessary library files”,并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”,这会让代码结构更清晰。
    • 点击“GENERATE CODE”,生成Keil工程文件。

3.2 核心代码编写:ADC读取与串口输出

打开生成的Keil工程,我们主要修改main.c文件。

首先,在/* USER CODE BEGIN PV */部分定义变量:

/* Private variables ---------------------------------------------------------*/ ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart1; uint32_t adc_value = 0; float voltage = 0.0; float concentration_ratio = 0.0; // 浓度比值 char msg[50]; // 用于存储格式化后的字符串

main()函数中,系统初始化后,进入主循环while (1)

第一步:启动ADC并读取值。HAL库提供了轮询、中断和DMA三种方式。我们先使用简单的轮询。

/* USER CODE BEGIN WHILE */ while (1) { // 启动ADC转换(轮询模式) if (HAL_ADC_Start(&hadc1) != HAL_OK) { Error_Handler(); // 启动失败,进入错误处理 } // 等待转换完成,并获取值。HAL_MAX_DELAY表示一直等待直到完成。 if (HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY) == HAL_OK) { adc_value = HAL_ADC_GetValue(&hadc1); // 获取12位的ADC原始值 } HAL_ADC_Stop(&hadc1); // 停止转换(对于连续模式,其实可以不用每次都停,这里演示流程) /* USER CODE END WHILE */

第二步:将ADC原始值转换为电压值。

// 计算实际电压值。假设Vref+ = 3.3V, ADC是12位(0-4095) // 如果接了分压电路,这里计算的是分压后的电压,需要根据分压比反推传感器AO脚的实际电压。 voltage = (float)adc_value * 3.3f / 4095.0f; // 示例:如果分压电路是10k+20k,分压比为 R2/(R1+R2) = 20k/(10k+20k)=2/3 // 那么传感器AO脚的实际电压应为: voltage_actual = voltage / (2.0f/3.0f); // 本例假设AO直接输出在0-3.3V范围内,或已通过分压匹配。

第三步:将电压值转换为浓度相关值并格式化输出。MQ-2的灵敏度特性曲线并非线性,且需要在实际洁净空气和已知浓度气体中校准才能得到精确的ppm值。对于一般演示,我们常用“传感器电阻与空气中电阻的比值(Rs/R0)”来表征浓度变化。简化处理中,我们可以直接用电压值或ADC值作为一个相对的“浓度指示”。

更严谨的做法是计算Rs/R0。R0是传感器在洁净空气中的电阻,Rs是在当前气体中的电阻。模块内部电路可以等效为传感器电阻Rs与一个负载电阻RL的分压。AO输出电压与Rs有关。公式推导略复杂,但一个常见的近似公式是:浓度比值 = (传感器电压 - 零点电压) / 灵敏度系数。这里我们做一个极度简化的演示,假设浓度与电压成正比(仅用于演示!):

// 简化处理:将电压映射到一个0-100的“相对浓度”值(仅演示用!) // 假设3.3V对应100,0V对应0。实际应用必须校准! concentration_ratio = (voltage / 3.3f) * 100.0f; // 格式化字符串,准备通过串口发送 // 使用snprintf安全格式化,避免缓冲区溢出 snprintf(msg, sizeof(msg), "ADC: %lu, Voltage: %.3f V, Ratio: %.2f%%\r\n", adc_value, voltage, concentration_ratio); // 通过串口发送字符串 HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); // 延时一段时间,避免串口数据刷屏太快 HAL_Delay(1000); // 每秒打印一次 /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */

3.3 串口打印浮点数的关键点

很多初学者会发现,按照上面的代码写了,但串口只打印出“ADC: 1234, Voltage:”,后面的浮点数不见了。这是因为默认的Keil MDK-ARM编译器微库(MicroLIB)对浮点数格式化的支持不完整

解决方法有两种:

  1. 启用完整的标准C库(推荐): 在Keil的“Options for Target” -> “Target”选项卡中,取消勾选“Use MicroLIB”。然后勾选“Use Standard Library”。重新编译即可。这会增加代码体积,但对于F103C8T6(64KB Flash)来说,这个项目完全装得下。
  2. 如果必须使用MicroLIB: 需要实现_sys_write等系统调用,或者将浮点数转换为整数部分和小数部分分别打印,例如:
    int int_part = (int)voltage; int dec_part = (int)((voltage - int_part) * 1000); // 取三位小数 snprintf(msg, sizeof(msg), "Voltage: %d.%03d V\r\n", int_part, dec_part);
    方法1是最简单直接的。

4. 数据优化与滤波处理

4.1 ADC采样稳定性提升技巧

直接读取一次的ADC值往往噪声较大,尤其是对于模拟传感器。常见的软件滤波方法有:

  1. 多次采样取平均: 这是最简单有效的方法。在主循环读取ADC值的地方,改为连续采样N次后取平均值。

    #define SAMPLE_TIMES 10 uint32_t adc_sum = 0; for(int i=0; i<SAMPLE_TIMES; i++) { HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY) == HAL_OK) { adc_sum += HAL_ADC_GetValue(&hadc1); } HAL_ADC_Stop(&hadc1); // 可以加一个很小的延时,如 HAL_Delay(1),避免ADC连续转换过热或干扰 } adc_value = adc_sum / SAMPLE_TIMES;
  2. 滑动平均滤波: 维护一个固定长度的数组,每次新采样值存入并覆盖最旧的值,然后计算数组中所有值的平均值。这种方法能平滑数据,且响应速度比简单平均快。

    #define FILTER_LEN 5 uint32_t adc_buf[FILTER_LEN] = {0}; uint8_t buf_index = 0; uint32_t adc_sum = 0; // 在循环中 // ... 获取到单次adc_raw ... adc_sum -= adc_buf[buf_index]; // 减去即将被覆盖的旧值 adc_buf[buf_index] = adc_raw; // 存入新值 adc_sum += adc_buf[buf_index]; // 加上新值 buf_index = (buf_index + 1) % FILTER_LEN; // 索引循环 adc_value = adc_sum / FILTER_LEN; // 得到滤波后的值
  3. 中位值平均滤波: 采样N次,去掉一个最大值和一个最小值,然后对剩下的N-2个值取平均。这种方法能有效抑制脉冲干扰。

4.2 传感器校准与数据标定思路

要让显示的值有实际意义(如ppm),必须进行校准。这需要标准气体或一个相对稳定的环境。

  1. 确定R0(洁净空气中电阻): 将传感器置于洁净空气中(例如室外通风处),预热足够时间(20-30分钟)使其稳定。读取此时的ADC值adc_air,并计算对应的电压V_air。根据模块原理图(通常RL已知,比如10kΩ),可以计算出R0。公式为:Rs = RL * (Vcc - V_out) / V_out,其中V_out是AO脚电压。在洁净空气中,Rs = R0。记录下这个R0值,可以固化在代码中。

  2. 计算Rs/R0比值: 在检测时,实时计算当前的Rs(方法同上),然后计算比值ratio = Rs / R0

  3. 查表或使用经验公式: MQ-2的数据手册会提供不同气体浓度下Rs/R0的比值曲线图。我们可以将曲线分段线性化,或者使用近似的对数公式来估算浓度。例如,对于某些气体,浓度ppm与比值的关系近似为:ppm = A * (ratio)^B,其中A和B是常数,需要根据数据手册的曲线拟合得到。

一个简化的、用于演示的代码片段可能如下:

// 假设已通过校准得到以下值 #define VCC 5.0f // 模块供电电压 #define RL 10.0f // 模块上的负载电阻,单位千欧 #define R0_CLEAN_AIR 10.0f // 校准得到的洁净空气中传感器电阻,单位千欧 float calculate_rs_from_adc(uint32_t adc_val) { float v_out = (float)adc_val * 3.3f / 4095.0f; // ADC测得的电压 // 如果ADC前有分压,需先反推回AO脚电压v_out_ao // float v_out_ao = v_out * (R1+R2)/R2; float v_out_ao = v_out; // 假设直接连接 float rs = RL * (VCC - v_out_ao) / v_out_ao; return rs; } float get_concentration_ratio(void) { uint32_t adc_filtered = get_filtered_adc_value(); // 获取滤波后的ADC值 float rs = calculate_rs_from_adc(adc_filtered); float ratio = rs / R0_CLEAN_AIR; return ratio; // 这个比值越小,通常表示浓度越高(对于MQ-2) }

在实际项目中,你可以根据ratio值,设定几个阈值来触发不同级别的报警(比如ratio < 0.6为轻度报警,ratio < 0.4为重度报警)。

5. 进阶优化与问题排查

5.1 使用DMA实现后台连续采样与传输

轮询方式效率低,且HAL_Delay会阻塞整个程序。更高级的做法是使用DMA(直接存储器访问)让ADC连续自动转换,并将结果源源不断地存入数组,完全不需要CPU干预。主程序只需要定期去处理这个数组里的数据即可。

在CubeMX中的配置:

  1. 在ADC1配置中,启用“DMA Continuous Requests”。
  2. 在“DMA Settings”标签页点击“Add”,选择ADC1,模式设为“Circular”(循环模式),数据宽度都为“Word”(32位,对应uint32_t)。
  3. 生成代码后,在main.c中启动带DMA的ADC转换:
    uint32_t adc_dma_buffer[BUFFER_SIZE]; // 定义一个缓冲区 HAL_ADC_Start_DMA(&hadc1, adc_dma_buffer, BUFFER_SIZE);
    之后,adc_dma_buffer数组会被自动填充。你可以开启一个定时器中断,每隔一段时间(如1秒)去计算缓冲区中数据的平均值,然后通过串口发送。这样主循环就完全自由了。

5.2 常见问题与排查技巧

  1. 串口无输出或乱码

    • 检查接线: TX接RX,RX接TX,GND接GND。
    • 检查波特率: 确保代码中设置的波特率(如115200)与串口助手软件(如XCOM、Putty)的波特率完全一致。
    • 检查时钟配置: 如果系统时钟配置错误,会导致所有外设(包括USART)的时钟频率不对,从而产生乱码。回头仔细检查CubeMX中的时钟树配置,确保HSE、PLL、系统时钟、APB总线时钟设置正确。
  2. ADC读数不稳定,跳动很大

    • 硬件滤波: 在ADC输入引脚(PA0)到地之间,并联一个0.1uF的瓷片电容,可以滤除高频噪声。
    • 软件滤波: 务必实施前面提到的多次采样平均或滑动平均滤波。
    • 检查电源: 模拟部分对电源噪声敏感。确保给STM32和传感器模块的电源是干净的。可以在VCC和GND之间并联一个10uF电解电容和一个0.1uF瓷片电容。
    • 采样时间: 适当增加ADC配置中的“Sampling Time”(采样时间),给ADC内部的采样保持电容更长的充电时间,对高阻抗信号源更有利。
  3. ADC读数始终为0或4095

    • 0: 可能ADC引脚没有正确接收到电压,检查连线,确认AO脚有输出。也可能是ADC通道配置错误。
    • 4095(满量程): 输入电压超过了ADC的参考电压(3.3V)。检查MQ-2模块AO输出是否过高,务必确认是否需要分压电路。
  4. 代码下载后不运行

    • 启动模式: 确认BOOT0和BOOT1引脚设置正确(通常都接地,从主Flash启动)。
    • 复位电路: 检查复位引脚是否正常,尝试手动复位。
    • 调试器连接: 如果使用ST-LINK,检查驱动是否安装,连接是否牢固。

5.3 项目扩展思路

这个基础项目可以很容易地扩展:

  • 添加阈值报警: 除了串口打印,可以连接一个LED或蜂鸣器,当浓度比值超过设定阈值时,触发声光报警。
  • 接入OLED显示屏: 使用I2C或SPI接口的OLED屏,实时显示浓度值和曲线图,脱离电脑。
  • 实现物联网上报: 搭配ESP8266 Wi-Fi模块,将浓度数据上传到云平台(如阿里云、OneNET),实现远程监控。
  • 多传感器融合: 同时接入温湿度传感器(如DHT11)、其他气体传感器(如MQ-135用于空气质量),构建一个环境监测站。
  • 使用RTOS: 引入FreeRTOS等实时操作系统,将ADC采样、数据处理、串口发送、显示刷新等任务模块化,提高代码的健壮性和可维护性。

这个“STM32连接MQ-2”的项目,就像一把钥匙,打开了嵌入式系统与真实物理世界交互的大门。从电压的读取、数据的处理,到最终信息的呈现,每一个环节都蕴含着从模拟到数字、从硬件到软件的基本原理。动手做一遍,遇到的问题和解决的思路,远比只看文档要深刻得多。当你看到串口助手上那一行行随着烟雾靠近而变化的数字时,那种把不可见的物理量变成可控数据的感觉,正是嵌入式开发的乐趣所在。

本文还有配套的精品资源,点击获取

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

Xilinx FPGA DDR4独立测试工程:从MIG配置到Verilog压力测试实战

简介&#xff1a;本资源是一套基于Xilinx FPGA平台的DDR4内存控制器Verilog实现工程&#xff0c;面向FPGA开发工程师、数字电路进阶学习者及高速接口设计实践者&#xff0c;解决DDR4在Xilinx器件上的底层驱动与读写验证难题。工程已在Vivado 18.3环境下完成综合、实现与板级测试…

作者头像 李华
网站建设 2026/9/3 7:52:54

AI歌声合成实战:基于RVC的杨博文音色《Can‘t Get You Out Of My Head》翻唱制作

在数字音乐制作和 AI 技术快速发展的今天&#xff0c;利用人工智能进行人声克隆和歌曲翻唱已经不再是专业录音棚的专利。许多开发者、音乐爱好者和技术研究者开始尝试使用开源工具和模型&#xff0c;基于特定歌手的音色&#xff0c;生成全新的演唱作品。这类项目通常被称为“AI…

作者头像 李华
网站建设 2026/9/3 7:50:49

基于CC2530与ZigBee的智能照明系统:从传感器到Mesh网络的完整实现

简介&#xff1a;本资源是一套基于CC2530 ZigBee无线传感网络的嵌入式综合实践项目&#xff0c;面向物联网专业学生、ZigBee初学者及智能硬件开发者&#xff0c;解决多节点协同控制与手机远程交互的实际工程问题。项目以自动照明系统为载体&#xff0c;实现光感自控、APP手动开…

作者头像 李华
网站建设 2026/9/3 7:50:17

零基础自学C语言:翁恺MOOC课程实战指南与核心概念解析

简介&#xff1a;本资源是一份系统化的C语言自学实践记录包&#xff0c;面向零基础编程学习者与计算机相关专业初学者&#xff0c;旨在通过浙江大学翁恺教授的经典MOOC课程&#xff0c;夯实C语言核心能力与工程化编程思维。压缩包共558个文件&#xff0c;涵盖255个C源码&#x…

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

结构化输出实战:用JSON Schema约束LLM解析简历文本

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

作者头像 李华
网站建设 2026/9/3 7:49:22

农村自建房三层装修全流程实战:从规划到验收的避坑指南

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

作者头像 李华