news 2026/9/16 3:15:25

51单片机温度显示与报警系统设计:从DS18B20到Proteus仿真与实物调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51单片机温度显示与报警系统设计:从DS18B20到Proteus仿真与实物调试

简介:面向51单片机课程设计与毕业设计场景,这份资料提供了一套基于DS18B20的数字温度显示与超限报警系统参考实现。项目以汇编语言编写核心测温与显示逻辑,实时驱动1602液晶呈现温度,并通过定时器中断配合阈值判断完成报警,整体代码通过老师验收,具备直接移植到实验板的基础。压缩包共885个文件,整体约28.97MB,内容以244个hex固件、180个asm汇编源码为主体,同时包含81个c语言辅助程序、55个jpg与8个png图片素材、35个txt说明、28个lst列表文件以及Keil工程配置文件等,覆盖从源码阅读、编译烧录到硬件调试的关键环节。已有2022人学习下载,适合正在做单片机课设、毕设或希望掌握DS18B20时序与定时器报警用法的读者参考。资料中既保留了可直接烧录的固件,也提供了完整工程文件、图片及说明文档,拿到后可以对照源码梳理单总线通信与报警流程,快速搭建实验环境,减少从零调试的重复工作。

1. 为什么用 51 单片机做温度显示与报警系统

在嵌入式入门和课程设计里,“基于51单片机的温度显示并报警系统”几乎是一个绕不开的题目:一套硬件里至少包含传感器采集、数据解析、显示输出、阈值判断和人机交互,恰好覆盖了单片机开发的主要环节。很多人觉得 51 单片机性能弱、外设少,温度采集用 STM32 加 HAL 库不是更省事?但从实际落地看,51 单片机在成本、资料成熟度和逻辑可控性上依然有不可替代的位置。一颗 AT89C52 或 STC89C52 加上 DS18B20、LCD1602、蜂鸣器和几个按键,百元以内就能搭出完整的可演示系统,且所有时序都裸露在代码里,排查问题反而比动不动就上 RTOS 的方式直观得多。这套系统要解决的是两件事:实时把温度显示出来,以及当温度越限时可靠地触发声光报警,同时允许用户按自己的需求调整报警门限。对要做课程设计、参加电子竞赛或者想在硬件底层动手深挖的工程师,这套小系统是性价比很高的练手载体。

2. 传感器选型与硬件电路设计:DS18B20 为什么比 NTC 更适合教学板

2.1 温度传感器的三条路线对比

温度采样常见方案有热敏电阻 NTC 加 ADC、LM35 模拟输出、以及单总线数字传感器 DS18B20。对 51 单片机来说,NTC 需要额外占用 ADC 引脚,而 51 内部通常没有 ADC(STC 系列部分型号自带,但传统 AT89C52 没有),要么外扩 ADC0832,要么用 RC 充放电测电阻值,误差和时间开销都不小。LM35 仍然需要 ADC。DS18B20 直接输出数字温度值,测温范围 -55 到 +125 摄氏度,12 位分辨率时精度为 0.0625 摄氏度,单总线协议只需一根 I/O 口线就能完成双向通信,对引脚紧张、片上资源少的 51 单片机非常友好。

从学习价值上看,DS18B20 的时序要求严格,初始化、写时序、读时序都有明确的时隙长度,这恰好是训练 LOC 理解和延时精度的好题材。产品化角度呢,DS18B20 的供电可以做到外部 3 到 5.5 伏,数据线内置上拉时走线可以拉长到几十米,满足一般的环境监测需求。

2.2 最小系统电路连接表与设计要点

控制核心选择 AT89C52 或 STC89C52,晶振 11.0592MHz 或 12MHz 都行。如果后面要串口向电脑发送温度记录,建议直接用 11.0592MHz,这样波特率误差接近于零;如果只是本地显示与报警,12MHz 让延时时序更整齐。DS18B20 的数据引脚接单片机的 P1.0,LCD1602 的数据口接 P0 口,控制脚 RS、RW、E 分别接 P2.5、P2.6、P2.7,蜂鸣器用 NPN 三极管 S8050 驱动后接 P1.1,LED 报警指示接 P1.2,三个独立按键接 P3.0 到 P3.2,用来调整报警阈值和设置选择。

这里最需要留意的是 P0 口。传统 51 单片机的 P0 引脚在作为通用 I/O 输出时是开漏结构,内部没有上拉电阻,直接驱动 LCD 数据线时高电平能力很弱,必须外接 4.7k 到 10k 的上拉排阻,否则 LCD 显示会出现乱码或白屏。DS18B20 的数据线接一个 4.7k 上拉电阻到 VCC,这是单总线通信正常工作的前提。蜂鸣器不要直接接在单片机的引脚上,因为 I/O 口输出电流有限,驱动不了蜂鸣器,常见做法是用 S8050 三极管做开关,单片机引脚通过 1k 电阻接到基极,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接 5V。

器件引脚连接对象说明
DS18B20DQP1.0需接 4.7k 上拉电阻到 VCC
LCD1602D0-D7P0.0-P0.7P0 口必须外接上拉排阻
LCD1602RSP2.5寄存器选择
LCD1602RWP2.6读写选择,接地或由单片机控制
LCD1602EP2.7使能信号
蜂鸣器正极VCC通过 S8050 驱动,不直接接 I/O 口
按键K1-K3P3.0-P3.2按下接地,默认接高电平

2.3 看门狗可不可以在这种系统里用

STC89C52 内部集成看门狗,但 AT89C52 没有。如果用的是 STC 系列,可以在代码初始化阶段使能看门狗,喂狗周期一般设在主循环里,避免超温报警时程序卡在长延时中导致复位。注意 DS18B20 的读时序对延时精度敏感,如果看门狗触发中断或复位打断了时序,读出的数据就会是 85 摄氏度,也就是 DS18B20 的上电默认值。后面章节会专门讲这个坑的处理方式。

3. Proteus 仿真与最小系统验证方式

3.1 用 Proteus 搭建 51 单片机温度显示报警电路

Proteus 仿真 51 单片机是课程设计和硬件验证最常见的路径,可以避开反复焊接和芯片烧毁的问题。新建工程后依次放置 AT89C52、DS18B20、LCD1602、LED、蜂鸣器、电阻和按键。DS18B20 在 Proteus 元件库里可以直接搜到,其模型支持温度调整,右键点击元件选择 Edit Properties,把 Temp 参数从 25 改成 80,仿真时温度显示和报警状态会立刻发生变化。这个交互验证方式在生产前非常有用。

最小系统搭建时给单片机电源引脚接好 +5V 和 GND,晶振电路用 12MHz 晶振加两个 22pF 电容,复位电路用 10uF 电解电容和 10k 电阻。LCD1602 的对比度脚 V0 接一个 1k 电位器到 GND,调节到能看清字符的位置。Proteus 里电阻不需要精确选型,上拉用 10k 即可,蜂鸣器模型可以直接用 Speaker 或 Buzzer 表示。

3.2 晶振频率与延时函数的联动关系

仿真和实物最容易被忽略的是延时函数的频率匹配。DS18B20 时序要求写 0 时隙为 60 到 120 微秒,写 1 时隙也类似,读时隙要求主机拉低总线后 15 微秒内采样,这些延时如果用循环空转实现,不同的晶振频率会直接导致结果不同。12MHz 晶振下,普通for(i=0; i<N; i++);里的空循环一个 N 大约对应 10 微秒量级,但具体值要看编译器优化等级。Keil 的优化级别设到 8 以上可能会把空循环直接优化掉,导致 DS18B20 完全不响应,这是仿真和实物最常见的问题之一。

建议用可调延时的函数实现,并且将优化级别设置为默认的 Level 0 或 Level 1。严谨的做法是写一个基于空循环的延时函数,用示波器实测校准。如果手头没有示波器,可以在 Proteus 里虚拟示波器测量引脚波形,确认 DQ 线上的时序脉宽是否落在 DS18B20 的规定范围内。

3.3 仿真通过后转到实物要检查的四个点

仿真软件不会模拟所有电气特性。转到实物时先检查电源是否稳定,很多 DS18B20 读到的温度跳变,是因为 USB 供电纹波太大或导线压降导致 5V 跌到 4.5V 以下。第二个检查点是上拉电阻,P0 口上拉阻值过大,LCD 显示会变暗,过小则会增加功耗;DS18B20 上拉阻值建议在 4.7k 左右。第三个检查点是按键引脚的电平状态,未按下时需要被上拉电阻或内部上拉稳定在高电平,在 51 上 P3 口内部有上拉,所以按键另一端接地即可。第四个检查点是蜂鸣器驱动管放大倍数不够时声音会很小,S8050 的 hFE 通常够用,但基极电阻不要取太大,1k 以下一般没问题。

4. 系统代码实现:DS18B20 读取、LCD 显示、按键设定与报警联动

4.1 DS18B20 单总线驱动的完整可编译代码块

51 单片机的 C 语言代码习惯用 Keil C51 编译。DS18B20 驱动核心包括初始化函数、写一个字节函数、读一个字节函数以及读取温度函数。下面这段是比较标准的实现。

#include <reg52.h> sbit DQ = P1^0; sbit BEEP = P1^1; sbit LED_ALARM = P1^2; void delay_us(unsigned int n) { while (n--) { _nop_(); } } bit ds18b20_init() { bit presence = 0; DQ = 1; delay_us(2); DQ = 0; delay_us(500); // 拉低总线 480-960us,发出复位脉冲 DQ = 1; delay_us(60); presence = DQ; // 读取存在脉冲,低电平表示设备在线 delay_us(420); return presence; } void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); DQ = dat & 0x01; delay_us(60); // 保持时隙时间 DQ = 1; dat >>= 1; } } unsigned char ds18b20_read_byte() { unsigned char i, value = 0; for (i = 0; i < 8; i++) { value >>= 1; DQ = 0; _nop_(); DQ = 1; _nop_(); if (DQ) { value |= 0x80; } delay_us(60); } return value; } float ds18b20_get_temp() { unsigned char low, high; int raw; float temp; ds18b20_init(); ds18b20_write_byte(0xCC); // 跳过 ROM 匹配,适用于单总线只挂一个设备 ds18b20_write_byte(0x44); // 启动温度转换 while (!DQ); // 等待转换完成 ds18b20_init(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读取暂存器 low = ds18b20_read_byte(); high = ds18b20_read_byte(); raw = (high << 8) | low; temp = raw * 0.0625; // 12 位分辨率,每位代表 0.0625 摄氏度 return temp; }

代码逻辑说明:ds18b20_init发出复位脉冲后读取存在脉冲,返回 0 表示器件应答正常;write_byteread_byte都是低位在前。温度计算时要把高低字节拼成有符号整数,DS18B20 的原始数据用补码表示,负温度时高字节的高位为 1,乘以 0.0625 后得到负值。参数上最重要的一点是delay_us的精度,注释里写的时间是理想值,具体要按晶振频率和编译器优化级别调整。

注意复位后连续两帧通信之间要有足够的间隔,通常建议ds18b20_get_temp调用周期不低于 100 毫秒,因为 12 位分辨率下转换时间最长要 750 毫秒,这里用while(!DQ)等待转换完成。在实物上如果 DQ 线接触不良,这个循环会卡住,改进做法是加超时计数,超过 1 秒则返回上一次温度值。

4.2 LCD1602 显示温度和报警状态

LCD1602 是 16 字符两行的字符型液晶,显示温度时用第一行显示Temp: 25.5C,第二行显示当前阈值或报警状态。初始化流程为:延时 15 毫秒以上,写入功能设置命令 0x38(8 位数据、双行显示、5x7 点阵)、开关显示命令 0x0C、清屏命令 0x01、进入模式命令 0x06。

#include <reg52.h> #define LCD_DATA P0 sbit RS = P2^5; sbit RW = P2^6; sbit EN = P2^7; void lcd_write_cmd(unsigned char cmd) { RS = 0; RW = 0; LCD_DATA = cmd; EN = 1; _nop_(); EN = 0; delay_us(40); } void lcd_write_data(unsigned char dat) { RS = 1; RW = 0; LCD_DATA = dat; EN = 1; _nop_(); EN = 0; delay_us(40); } void lcd_init() { delay_us(15000); lcd_write_cmd(0x38); lcd_write_cmd(0x0C); lcd_write_cmd(0x01); lcd_write_cmd(0x06); } void lcd_show_temp(float temp) { int int_part, dec_part; int_part = (int)temp; dec_part = (int)((temp - int_part) * 10); lcd_write_cmd(0x80); // 第一行起始地址 lcd_write_data('T'); lcd_write_data('e'); lcd_write_data('m'); lcd_write_data('p'); lcd_write_data(':'); lcd_write_data(' '); lcd_write_data(int_part / 100 + '0'); lcd_write_data(int_part / 10 % 10 + '0'); lcd_write_data(int_part % 10 + '0'); lcd_write_data('.'); lcd_write_data(dec_part + '0'); lcd_write_data('C'); }

这里的逻辑说明:LCD 显示的是 ASCII 码,所以数字都要加'0'转成字符。温度值拆成整数部分和小数部分后分别显示,为了快速验证可以只显示一位小数。注意调用lcd_show_temp之前,程序里别在一个循环里刷屏太快,LCD 的写入周期在微秒级,但加上 DS18B20 每次转换的等待,整体刷新周期大概在 800 毫秒左右,这是正常现象,不是程序卡死。

4.3 按键设定报警阈值与报警逻辑

报警系统不能只有固定阈值,要给用户提供修改入口。三个按键定义清楚:K1 进入阈值设置模式,K2 增加阈值,K3 减少阈值。设置状态下 LCD 显示Set Alarm: 30C,正常状态下显示当前温度和当前阈值。

unsigned char alarm_threshold = 30; void key_scan() { if (K1 == 0) { delay_ms(20); // 消抖 if (K1 == 0) { setting_mode = 1; while (K1 == 0); // 等待松手 } } if (setting_mode == 1) { if (K2 == 0) { delay_ms(20); if (K2 == 0) { alarm_threshold++; if (alarm_threshold > 100) alarm_threshold = 100; while (K2 == 0); } } if (K3 == 0) { delay_ms(20); if (K3 == 0) { if (alarm_threshold > 0) alarm_threshold--; while (K3 == 0); } } } } void check_alarm(float temp) { if (temp >= alarm_threshold) { BEEP = 0; // 蜂鸣器驱动低电平有效 LED_ALARM = 0; } else { BEEP = 1; LED_ALARM = 1; } }

逻辑说明:按键消抖采用先判断电平,再延时 20 毫秒后再次确认的方式。while (K1 == 0);等待松手是为了防止长按时连续触发设置模式。阈值调整一次按键只加或减 1,按住不会自动连加,如果产品需要连加功能,可以根据按下时长做加速处理。报警逻辑里BEEP = 0有效,是因为蜂鸣器电路用的是 PNP 或 PNP 驱动方式,实际电平极性要根据原理图确认;如果直接驱动 LED,51 的 I/O 口灌电流能力比拉电流强,LED 正极接 VCC、负极接引脚会获得更亮的显示效果。

4.4 主程序流程拼接

主函数里把外设初始化、温度采集、显示刷新、按键扫描和报警判断放在一个 while 循环里。常见错误的写法是在ds18b20_get_temp()内部等待转换完成时直接阻塞,导致按键失灵,按下没有任何反应。改进做法是设置一个 500 毫秒定时中断作为时间基准,主循环里只做非阻塞式查询,这样按键扫描和报警判断的周期能做到 20 毫秒左右,不会因为温度转换而卡顿。

void main() { float current_temp; lcd_init(); ds18b20_init(); while (1) { key_scan(); if (setting_mode == 1) { lcd_show_set_mode(); } else { current_temp = ds18b20_get_temp(); lcd_show_temp(current_temp); lcd_show_threshold(alarm_threshold); check_alarm(current_temp); } } }

参数说明:这种简单轮询方式适用于控制逻辑少的场景。设置模式下不读取温度,避免按键扫描被温度转换阻塞。如果要在设置模式下继续检测温度,可以把温度转换放到定时器中断里完成,主循环只取结果。

5. 报警阈值、回差与抗干扰参数设计

5.1 临界抖动问题与回差控制

报警系统有一个在工程上很常见但教科书里很少提到的参数,叫回差。当实际温度稳定在报警阈值附近时,比如阈值设置成 30 摄氏度,温度在 29.9 到 30.1 摄氏度之间波动,报警就会反复触发和解除,蜂鸣器发出断续刺耳的声音。解决方式是在代码里增加回差逻辑:进入报警状态的判断条件为temp >= threshold + hysteresis,退出报警状态的条件为temp <= threshold - hysteresis,其中hysteresis是一个可配置的滞回区间。

参考参数按温控场景不同取值:环境温度报警用 0.5 摄氏度即可,如果用于电机外壳温度保护,建议回差取 2 到 3 摄氏度,避免设备在临界点反复启停。实现代码时需要记住当前报警状态,不能只根据单次采样结果做判断。

bit alarm_active = 0; #define HYSTERESIS 1.0 void update_alarm(float temp, unsigned char threshold) { if (alarm_active == 0) { if (temp >= threshold + HYSTERESIS) { alarm_active = 1; BEEP = 0; } } else { if (temp <= threshold - HYSTERESIS) { alarm_active = 0; BEEP = 1; } } }

回差参数的取值原则:报警用于保护设备时,回差应大于正常工况的温度波动幅度。用于实验室恒温监控时,回差设小一些,但至少大于传感器本身的量化误差,也就是 0.0625 摄氏度。

5.2 采样滤波与异常数据剔除

DS18B20 偶尔会读出完全错误的温度,典型值是 85 摄氏度。这不是器件坏了,而是通信时序被中断或者供电不足,器件返回了上电复位后的默认值。如果直接把这个值送到报警逻辑,系统会误报警。常见做法是连续采样 5 次,去掉最大值和最小值后取平均,或者采用一阶低通滤波:

当前值等于上一次的值加上当前采样值与上一次值的差值乘以滤波系数。滤波系数根据采样周期和期望的响应时间来选择,一般取 0.1 到 0.3。采样周期约 1 秒时,系数 0.2 可以让温度变化响应时间在 5 秒左右,对于缓慢变化的温度场足够,又不会把突变信号平滑得失去报警意义。

异常数据剔除可以再叠加一个超范围判断:当采样温度超出 -20 到 100 摄氏度范围时,直接丢弃本次采样,继续使用上一次数值。这个判断必须放在滤波之前,否则异常值会污染滤波状态。

5.3 报警方式拓展到多级阈值

很多应用场景要求分两级报警。一级报警作为预警,比如温度达到 40 摄氏度时 LED 闪烁、蜂鸣器间歇鸣叫;二级报警作为紧急停机信号,温度达到 50 摄氏度时蜂鸣器长鸣。两级报警之间同样需要回差,并且每一级的进入和退出条件要分开记状态。51 单片机的定时器可以用来实现 LED 闪烁的时基,不需要额外延时函数,在主循环中通过一个递减变量来控制闪烁频率。

蜂鸣器驱动方面,间歇鸣叫的实现方式是设定一个鸣叫周期,比如响 200 毫秒、停 200 毫秒。在主循环里记录时间基准,判断当前时刻落在响区间还是停区间。注意不能直接在报警函数里做delay_ms(200),那会让温度采集和按键全部停顿,显示也会出现明显卡顿。

6. 从仿真到实物的验收技巧与高频故障排除

6.1 用定时器中断做时间基准,重写延时逻辑

把空循环延时全部替换为定时器时间基准,是实物稳定性提升关键。使用定时器 0 的 1 毫秒中断,主循环中维护一个sys_tick变量,所有需要延时的操作都基于该变量做非阻塞等待。DS18B20 的时序仍然需要用微秒级延时,这部分用空循环实现;但消抖、蜂鸣器节奏等毫秒级操作,统一走时间基准,能避免按键按下时主循环卡在对 DS18B20 的阻塞等待上。

6.2 现场故障排查对照表

实物调试遇到温度显示 85 摄氏度,原因集中在电源供电不足或 DQ 上拉不良,用万用表量传感器 VCC 和 GND 之间电压,低于 4.5V 时更换供电线或者改用更粗的导线供电。LCD 显示黑块、不显示字符,检查 P0 口上拉排阻是否焊好,对比度电位器是否调到合适位置。按键失灵则先量按下时引脚电平是否确实拉低,再检查消抖延时是否被编译器优化。

故障现象可能原因排查手段
温度显示 85DQ 线接触不良、上拉电阻缺失、VCC 供电过低万用表测 DQ 对地电平与 VCC 电压
温度始终不变DS18B20 被设置为 9 位分辨率、读取间隔太短重发 0xCC 0x4E 命令设置 12 位
LCD 出现方块P0 口上拉电阻缺失、初始化时序不对检查排阻,确认 0x38 写入成功
蜂鸣器不响三极管极性接反、基极电阻过大直接给基极短接高电平验证
按键调节阈值跑到设置模式消抖不足或while(K1==0)被优化增加volatile修饰变量

6.3 用示波器或逻辑分析仪验证单总线时序

做过实物的工程师都有这种经历:程序逻辑看着完全正确,DS18B20 就是不工作,问题出在时序上而不是代码逻辑里。用逻辑分析仪抓 DQ 引脚的波形,对照数据手册检查初始化复位脉冲是否在 480 到 960 微秒,写时序中低电平宽度是否是 60 到 120 微秒,读时序中主机释放总线后是否在 15 微秒内完成采样。如果手头只有示波器,把时基调到 10 微秒每格,触发方式设为下降沿,也能看清大致脉宽。抓波形时注意探头要接在 DQ 引脚上,地线夹到系统 GND,接地线长度尽量短,否则探头本身的寄生电容会改变波形上升沿,导致误判。

最后一个有效的实调手法是直接从暂存器读原始字节并打印到串口,先确认低字节、高字节读出来的数值符合 DS18B20 文档中的计算示例。比如已知某温度下理论原始值为 0x0191,如果读出来两个字节分别是 0x91 和 0x01,说明字节顺序和拼接正确。这样一条链路验证过去,硬件和软件的问题边界就能尽早切分清楚。

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

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

Redis核心场景实战:从缓存穿透到分布式锁的15个案例

Redis这玩意儿&#xff0c;我前后用了快十年。从最早只是拿它做某个后台模块的本地缓存&#xff0c;到后来在微服务架构里当分布式锁、扛排行榜、处理延迟任务&#xff0c;一路踩过的坑确实不少。一开始我也觉得它无非就是个厉害点的HashMap&#xff0c;但用久了才意识到&#…

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

Fast DDS共享内存零拷贝完全指南:配置、验证与避坑

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

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

Java SE游戏开发:从Swing主循环到AABB碰撞的完整实践

简介&#xff1a;这是一份基于Java实现的经典超级马里奥风格小游戏源码&#xff0c;面向计算机、数学、电子信息等专业的本科生&#xff0c;适用于课程设计、期末大作业及毕业设计参考&#xff0c;帮助学习者通过完整可运行项目掌握Swing图形界面开发、游戏主循环、碰撞检测、音…

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

技术人如何通过写博客从极客成为行业意见领袖:路径、方法与避坑

先把话说在前面&#xff1a;这篇文章不是什么成功学&#xff0c;也不是那种“三个月从零做到十万粉”的速成教程。我见过太多技术人&#xff0c;代码写得漂亮、方案讲得清楚&#xff0c;但一提到写博客、做分享&#xff0c;就觉得那是另一个世界的事。老蒋博客从最开始一个没人…

作者头像 李华
网站建设 2026/9/16 3:11:53

ROS多无人机仿真:命名空间、TF前缀与气流耦合的系统级重构

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

作者头像 李华