news 2026/10/16 1:07:28

DS18B20温度计实战:数码管动态扫描与单总线时序详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DS18B20温度计实战:数码管动态扫描与单总线时序详解

刚拿到这个项目需求时,第一反应是:又一个把“测温”和“显示”两个经典模块拼一起的入门课题。但真正做完一圈才发现,DS18B20看似简单,单总线时序里埋的坑、数码管动态扫描里藏的门道,足够让新手折腾好几个通宵。这篇文章我按照完整做过的思路来写,把手头调试通过的方案、参数、代码直接摆出来,从选型到踩坑一次说透,希望能帮你少走几步弯路。

1. 方案选型与整体设计思路

1.1 为什么选DS18B20做温度采集

温度传感器这块其实可选的路子很多,热敏电阻、热电偶、模拟输出型的LM35、数字I2C接口的SHT30,各有各的适用场景。这个项目选择DS18B20,主要看中三点。

第一是数字输出,不需要ADC参与,MCU直接读回数字量就能换算成温度。这一条对刚接触单片机的朋友特别友好,因为不用处理模拟前端电路、不用纠结参考电压,省掉了一大堆容易出错的硬件环节。第二是单总线协议,一根IO口就能完成通信,接线极简,三根线(VCC、GND、DQ)往板子上一插就能跑起来。第三是测量范围和精度足够日常使用,-55℃到+125℃的测温范围,-10℃到+85℃这个区间内典型误差只有±0.5℃,放在室内环境监测、小家电控温、实验室温控这些场景里完全够用。

我个人的看法是,如果是做产品原型验证或者课程设计这类需要快速出结果的项目,DS18B20这类数字传感器是效率最高的选择。当然,如果你对精度有更高要求(比如±0.1℃级别),或者需要同时采集很多路温度,那可以考虑PT100配合高精度ADC,或者直接上多路数字传感器组网——后面我会专门聊扩展方案。

1.2 数码管显示方案的三种思路对比

温度采集到之后要显示出来,数码管是成本最低、最直观的方案。我自己用过三种驱动方式,各有取舍。

直接IO驱动最简单粗暴,把数码管的段选脚(a到dp)分别接到单片机的IO口,位选脚也各自接IO,程序里用动态扫描的方式刷新。优点是电路最简单、代码最直观,缺点是占用的IO口太多——四位数码管光是段选就要8个引脚,加上4个位选,一共12个IO,MCU的引脚一下子被吃掉大半。

74HC595移位寄存器驱动是折中方案,用一个595串转并芯片负责段选,单片机只需要3根线(SHCP时钟、DS数据、STCP锁存)就能把8位段码送出去,位选再用4根IO直接控制。这样总共7个IO就能搞定四位数码管,引脚压力小很多,唯一的要求是理解串行转并行的时序逻辑,但用起来并不难。

TM1640这类专用LED驱动芯片是省心方案,芯片内部自带扫描逻辑,你只要通过I2C(实际是两线接口)把段码数据发过去,剩下的刷新、消隐全部交给芯片处理。优点是占用的MCU资源最少、显示亮度均匀、还能省掉位选控制逻辑,缺点是多一颗芯片的硬件成本,以及需要读一下芯片手册。

从教学价值和通用性来说,我个人更推荐74HC595方案。它可以锻炼你对串行时序的理解,而且以后做点阵屏、LED条屏也还会用到类似思路。下面整个项目的实现我都按照STC89C52(51内核)加两颗74HC595来展开,如果你用的是STM32或者Arduino,底层函数替换一下,逻辑完全可以沿用。

1.3 系统整体工作流程

整个系统可以分成四个环节:温度采集、数据换算、BCD拆分、数码管显示。MCU上电后先做IO初始化和DS18B20复位握手,然后周期性发温度转换命令,等待转换完成后读取9字节暂存器数据,从中提取LSB和MSB拼出16位温度原始值,再经过补码还原和分辨率换算得到实际温度,接着把整数部分和小数部分拆成一个个数字,查段码表送进595,最后通过动态扫描循环点亮每一位数码管。主循环里不断重复“采集-显示”这个过程,就实现了温度的实时监测。

2. 核心原理与关键参数拆解

2.1 单总线时序协议(这一块必须吃透)

DS18B20的所有通信都建立在一根数据线DQ上,MCU通过控制这根线的拉高拉低时序来传输数据。整个协议里有三种最基础的时序:复位时序、写时序、读时序。很多人代码跑不通,基本都是在这三种时序的延时参数上出了问题。

复位时序是所有通信的开始。主机把总线拉低至少480μs(通常做500μs),然后释放总线,释放后总线由上拉电阻拉高。此时DS18B20检测到下降沿并等待,大概率在15μs到60μs后主动把总线拉低60μs到240μs,这个叫存在脉冲,相当于应答信号:“我在呢”。主机必须在这段时间内读到低电平,如果读不到,说明传感器没接好或者上拉电阻有问题。

写时序和读时序都要求主机在每一个通信周期内先主动拉低总线至少1μs作为起始信号。写“0”的时序是拉低总线并保持60μs到120μs,让传感器在总线低电平期间采样为0;写“1”的时序是拉低1μs到15μs后立刻释放总线,让上拉电阻把总线拉高,传感器在之后采样到高电平为1。读时序则略有不同,主机拉低总线1μs以上后释放,然后在15μs内去采样电平,因为传感器如果在就绪状态,会在主机释放后把总线拉低表示“0”,否则总线保持高表示“1”。关键点是读时序必须在释放后尽快采样,拖久了就读不准。

这里有个实战经验分享:51单片机的延时函数跟晶振频率强相关,我调试的时候发现网上很多代码是12MHz晶振写的,换成11.0592MHz晶振后部分时序会偏。最稳妥的办法是用逻辑分析仪抓一下波形,或者至少用示波器看复位脉冲和读时序的宽度。手头没有仪器的话,就多准备几个延时可调的版本对比测试。

2.2 温度数据处理与补码换算

DS18B20的温度值以16位有符号数形式存放在暂存器第0字节(LSB)和第1字节(MSB)中,格式是二进制补码。默认12位分辨率下,只取高12位有效数据,低4位是小数部分,每个LSB对应0.0625℃。

拿到16位数据后,先判断符号位。如果最高位是0,这是正温度,直接乘以0.0625就是实际温度;如果最高位是1,这是负温度,需要先把补码取反加1还原成绝对值,再乘以0.0625,最终结果加负号。举两个例子:读回0x0191,换算成十进制是401,乘以0.0625得到25.0625℃,这是正温;读回0xFF92,符号位为1,取反加1得到0x006E也就是110,乘以0.0625得到6.875,加个负号就是-6.875℃。

实际显示的时候,我们通常只保留一位或两位小数。我在代码里习惯的做法是直接乘以一个系数(比如乘以100得到不带小数的整数,再手动插入小数点),这样既能避开浮点运算在小单片机上的性能开销,也能方便地控制小数位。

2.3 数码管动态扫描与段码表

四位数码管如果同时点亮,每个段的工作电流会互相争抢,导致亮度不均匀,驱动电路也比较复杂。动态扫描的原理是:任何时候只有一位数码管通电点亮,轮流点亮四位,利用人眼的视觉暂留效应,只要刷新频率足够高(通常每轮40ms以内,个人建议做到5ms到10ms刷新一位),看起来就像四位同时点亮。

共阴和共阳数码管的段码是不一样的。共阴数码管的公共端接地,段选端给高电平点亮相应笔画;共阳数码管相反。以下表格是共阴数码管的常用段码(位序:dp g f e d c b a),注意别记混:

显示字符共阴段码显示字符共阴段码
00x3F50x6D
10x0660x7D
20x5B70x07
30x4F80x7F
40x6690x6F

需要显示小数点时,在段码上按位或0x80即可。例如显示“25.”,就是先查出数字5的段码0x6D(这里注意是数字5的小数点版本,实际上是给数字5的段码加0x80),即0xED。

3. 实操过程与代码实现

3.1 硬件清单与接线表

我这次搭建用的核心器件包括:STC89C52RC单片机最小系统板一块,DS18B20温度传感器一个(TO-92封装),四位数码管一个(共阴,带小数点),74HC595芯片两颗,4.7kΩ上拉电阻一个,10μF和104瓷片电容若干,以及5V直流电源。

接线方式列个表,清晰一些:

信号连接端口说明
DS18B20 DQP3.7同时接4.7kΩ上拉电阻至VCC
DS18B20 VCC5V电源外部供电模式
DS18B20 GNDGND必须共地
595的第一颗(段选)DSP1.0串行数据输入
595的第一颗(段选)SHCPP1.1移位寄存器时钟
595的第一颗(段选)STCPP1.2输出锁存时钟
595的第二颗(位选)DSP1.3位选数据输入
595的第二颗(位选)SHCPP1.1可与段选共用一个时钟脚
595的第二颗(位选)STCPP1.2可与段选共用一个锁存脚
位选输出Q0-Q3数码管公共端1-4通过三极管或直接连接(按电流选)

注意,DS18B20这里采用外部供电方式,比寄生供电更稳定。寄生供电模式下传感器会从数据线上“偷电”,动态功耗大时容易出现复位失败,初学者不建议用。

3.2 DS18B20底层驱动代码(完整可运行)

#include "reg52.h" #include "intrins.h" sbit DQ = P3^7; /**************** 微秒级延时(11.0592MHz) ****************/ void Delay_Us(unsigned int us) { while (us--) { _nop_(); _nop_(); } } /**************** 复位并检测存在脉冲 ****************/ // 返回1表示检测到传感器,0表示无应答 bit DS18B20_Reset(void) { bit presence; DQ = 1; Delay_Us(2); DQ = 0; Delay_Us(500); // 拉低至少480us DQ = 1; Delay_Us(70); // 等待应答窗口 presence = DQ; // 读取存在脉冲 Delay_Us(250); // 等待时序结束 return (~presence); // 读到0为正常 } /**************** 写入一个字节 ****************/ void DS18B20_WriteByte(unsigned char dat) { unsigned char mask; for (mask = 0x01; mask != 0; mask <<= 1) { DQ = 0; // 起始信号 _nop_(); if (dat & mask) { DQ = 1; // 写1:拉低后快速释放 Delay_Us(10); } else { Delay_Us(60); // 写0:维持低电平 } DQ = 1; Delay_Us(2); } } /**************** 读取一个字节 ****************/ unsigned char DS18B20_ReadByte(void) { unsigned char mask; unsigned char dat = 0; for (mask = 0x01; mask != 0; mask <<= 1) { DQ = 0; // 读起始信号 _nop_(); DQ = 1; Delay_Us(5); // 释放并等待 if (DQ) { dat |= mask; // 采样为高 } Delay_Us(50); } return dat; } /**************** 读取温度(返回原始值) ****************/ int DS18B20_GetTempRaw(void) { unsigned char low, high; int temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM,单设备场景 DS18B20_WriteByte(0x44); // 启动温度转换 // 等待转换完成,12位分辨率最长750ms,可根据需求调整 // 也可以在主循环里延时后一次性读取,见下文推荐方式 while (!DQ); // 传感器在转换期间拉低总线,完成后释放 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); temp = (high << 8) | low; return temp; }

上面的while (!DQ)等待转换完成的方式有一个问题:如果传感器断线,DQ一直为高,程序会卡在这里。实际用的时候建议改成固定延时,比如发完转换命令后延时750ms到800ms再读取,这样即便传感器异常也不会死等。另一种做法是发转换命令之后不等待,先把显示刷新几遍,等时间差不多了再去读数据,整体循环更流畅。

3.3 74HC595驱动与数码管显示代码

sbit SHCP = P1^1; // 移位时钟 sbit STCP = P1^2; // 锁存时钟 sbit DS_Seg = P1^0; // 段选数据 sbit DS_Pos = P1^3; // 位选数据 /**************** 向一颗595发送一个字节 ****************/ void HC595_SendByte(unsigned char dat, bit dataPin) { unsigned char mask; for (mask = 0x80; mask != 0; mask >>= 1) { if (dat & mask) { dataPin = 1; } else { dataPin = 0; } SHCP = 0; _nop_(); _nop_(); SHCP = 1; // 上升沿移入一位 } } /**************** 一次刷新全部四位 ****************/ void Display_Refresh(unsigned char *segCode, unsigned char *posCode) { unsigned char i; for (i = 0; i < 4; i++) { // 先送位选,再送段码,避免显示残影 HC595_SendByte(posCode[i], DS_Pos); // 位选,只有1位有效 HC595_SendByte(segCode[i], DS_Seg); // 段码 STCP = 0; _nop_(); STCP = 1; // 锁存输出,点亮当前位 Delay_Us(500); // 保持点亮时间,控制亮度 // 消隐处理,必要时送空段码 HC595_SendByte(0x00, DS_Seg); HC595_SendByte(0x00, DS_Pos); STCP = 0; STCP = 1; } }

这里有一个很重要的细节:动态扫描会产生“拖影”问题,是因为切换位的瞬间,前一位的段码还残留在输出端,下一位的位选已经打开了,就会出现不该亮的笔画亮一下。解决方法是先送位选、再送段码,或者在切换前先关闭所有位选(全部给无效电平),等段码稳定后再打开对应的位选。我上面的写法里每轮结束后送了空数据,就是用来消除残影的。

3.4 主程序与显示数据处理

显示时如果温度是正数,默认格式为“XX.X”;如果是负数,显示“-XX.X”,用第一位显负号。温度转换为显示数据的过程,我建议用整数运算避免浮点:

// 原始温度值 tempRaw,如0x0191表示25.0625℃ // 乘以100再处理,即 tempX100 = tempRaw * 625 / 100 // 因为1个LSB = 0.0625℃ = 625/10000,所以tempRaw * 625 / 100 = tempRaw * 6.25 // 直接用除法实现:乘以625再除以100 int tempX100; unsigned char segBuf[4]; unsigned char posBuf[4] = {0x01, 0x02, 0x04, 0x08}; // 共阴动态扫描 void Temp_To_Display(int tempRaw) { unsigned char digit[4]; unsigned char i; int tempInt; unsigned char negative = 0; if (tempRaw < 0) { negative = 1; tempRaw = ~tempRaw + 1; // 取绝对值 } tempX100 = (int)((long)tempRaw * 625 / (long)100); tempInt = tempX100 / 100; // 整数部分 // 小数一位四舍五入处理 if ((tempX100 % 100) >= 50) { // 这里注意整数部分可能进位,比如9.96 -> 10.0 // 简单处理:在tempX100加上50后整除10再取一位 } // 重组为:整数两位 + 小数一位 // 比如25.06℃ -> 显示25.0(直接截断) // 或者四舍五入后 25.1 digit[3] = Get_Digit_Code(tempInt % 10); // 个位 digit[2] = Get_Digit_Code((tempInt / 10) % 10); // 十位 if (negative) { segBuf[0] = 0x40; // 负号段码 } else { segBuf[0] = 0x00; } // 小数一位根据tempRaw的LSB低4位换算:低4位 * 0.0625 = 小数 // 小数 * 10后四舍五入即为显示值 unsigned char dec = ((tempRaw & 0x0F) * 625 + 500) / 1000; // 0~9 digit[1] = Get_Digit_Code(dec); segBuf[1] = digit[2]; // 十位(如果有) segBuf[2] = digit[1] | 0x80; // 个位带小数点 segBuf[3] = digit[3]; // 小数点后第一位 // 如果十位为0且非负温,关闭第一位显示 if ((tempInt / 10 == 0) && (!negative)) { segBuf[1] = 0x00; } } unsigned char Get_Digit_Code(unsigned char num) { unsigned char code[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; return code[num % 10]; }

这段代码里的小数处理和四舍五入是为了让显示更接近真实温度。实际调试中我发现很多人会忽略一位:当温度从25.9℃跳到26.0℃时,小数部分和整数部分同时变化,如果不做进位处理,显示会瞬间变成25.0然后又跳到26.0,看起来像跳变。处理方法是在判断小数时直接把四舍五入后的结果回加给整数部分。更简化的做法是干脆只显示到整数位,避免这些进位麻烦,应用场景不那么苛刻的话足够了。

4. 常见问题与排查技巧实录

4.1 DS18B20读取异常:读回0xFFFF、固定85℃、无应答

先说读回0xFFFF的情况。0xFFFF在DS18B20中是一个典型的错误标志值,代表传感器没有完成转换或者通信失败。我遇到过的原因集中在三个地方:一是上拉电阻缺失或阻值太大,DS18B20输出高电平拉不上去,读回来的数据就全是1;二是程序时序不对,读操作采样点偏晚,导致每一位都读成了1;三是总线过长,导线电容太大,信号波形变形。排查顺序我通常先量一下数据线电压,按下复位键时看DQ有没有短暂拉低,再用逻辑分析仪看时序,最后确认代码里的延时参数跟晶振匹配。

固定读到85℃的坑也很有代表性。85℃是DS18B20的默认上电值,意味着你读到的数据其实是芯片RAM里没有经过转换的初始值,也就是说可能没有发起温度转换指令(0x44),或者转换还没完成就读取了。解决办法就是在读暂存器之前确保已经发送过转换命令,并且等待750ms以上(12位分辨率)。如果你发完转换命令立刻去读,大概率就是这个85℃。

完全没有应答的情况(复位检测不到存在脉冲),优先检查接线有没有接反。DS18B20是TO-92封装,引脚朝自己、平面朝外时,从左到右分别是GND、DQ、VCC,很多人容易搞混GND和VCC,一接反芯片直接发烫甚至烧毁。这算是一条血泪经验,建议拿到芯片先对着型号查一下手册确认引脚,再通电。

4.2 数码管显示问题:亮度不均、闪烁、乱码

数码管动态扫描最常见的问题是亮度不均:某一位明显亮一点。原因在于各段点亮的时间不同,或者某一位公共端驱动能力不够。我试过最有效的解决办法是把扫描周期尽量做短,保证每一位点亮时间一致,同时使用三极管或者专门的位驱动管来放大位选电流。如果用的是两片595,位选侧595的输出其实就是逻辑电平,直接驱动共阴数码管可能电流不足,建议位选端加上NPN三极管(比如S8050)来提供足够电流,这样显示亮度会均匀很多。

整体乱码多半是段码表格错了。共阴共阳搞反是最常见的低级错误——共阳数码管用共阴段码的话,所有不亮的笔画反而会亮,看上去就是乱码加缺笔画。还有个细节是数码管的公共端数量,四位数码管如果是12脚的,通常是四个独立公共端,需要四个位选;如果是10脚的,位选可能需要通过二极管隔离或者特殊接法,买之前先看清楚引脚定义。

4.3 数据刷新卡顿与主循环设计

还有一个小问题:如果不做任何优化,主循环里发转换命令后死等750ms,再去读取和刷新显示,整个过程里数码管会明显闪一下。这是因为等待期间没有刷新显示。优化方法有两个,一是把温度转换间隔拉长到比如1秒一次,等待期间正常刷新显示;二是用状态机逻辑,发完转换命令后该干嘛干嘛,等下次循环再检查是否到达750ms,到了再去读取。这两个方案我在项目里都试过,状态机的方式对后续扩展多路传感器特别友好,建议代码结构上往那个方向靠。

5. 扩展方向与升级思路

5.1 负温度显示与超高精度模式

如果测温范围会低于0℃,除了在数据换算时做补码处理,显示部分也要处理负号和数字位的关系。我的习惯是确定好显示位数上限(比如“-55.0”到“125.0”),把负号显示固定在第一位,十位正常显示,同时关闭百位。显示负号时,最高位段码用0x40,其他段全部熄灭。

另外,DS18B20的分辨率可以在配置寄存器里设置9到12位,默认是12位。如果需要更快刷新速度(比如转换时间从750ms降到93.75ms),可以把分辨率调低到9位,代价是温度分辨率下降到0.5℃。是否值得,取决于应用场景——室内恒温控制里0.5℃就够了,做体温趋势监测就必须保留12位。

5.2 多路DS18B20组网采集

单总线的另一个特色是支持一条总线上挂多个DS18B20,每个芯片出厂时带有唯一的64位ROM序列号,可以通过ROM命令来寻址。实际做多路采集时,第一步是调用搜索ROM(0xF0)算法,把总线上所有传感器的序列号读出来并存储;第二步是每次都发匹配ROM(0x55)选中某个传感器再发温度转换命令;第三步是轮流读取各路温度。需要特别注意的是,总线上挂多个器件时上拉电阻阻值要适当调小一些(通常2.2kΩ到4.7kΩ之间尝试),线长的情况下更要注意总线电容,否则通信极不稳定。

5.3 低功耗和无线化改造参考

如果想把做成电池供电的设备,可以把DS18B20设置为寄生供电模式,减少一路电源线;同时降低采样频率,比如每分钟只采集一次,采集完成后让MCU进入休眠模式,用定时器唤醒。我一个朋友做仓库温湿度记录仪时就这样处理,两节AA电池撑了近一年。如果你想做无线温湿度监测,可以考虑把它接在带有RF模块的板子上,把温度数据打包通过无线协议发到网关或手机端,显示部分可以换成OLED屏幕或者干脆只在App端显示。数码管方案的核心价值在于教学和快速原型验证,升级到产品化时自然会面临显示和传输方案的重新选型。

6. 写在最后的实操心得

自己做这个项目时踩过几次坑,有个经验想特别分享给后来者:DS18B20的时序代码在网上能找到很多版本,但几乎每个版本在换了一个开发板或者换了一根杜邦线之后,稳定性都有微妙变化。最靠谱的做法不是迷信某一份代码,而是把时序延时常数抽出来集中管理,配合逻辑分析仪做一次标定,之后换板子时只调整那组参数就够了。

另外一个习惯是分模块调试,别一上来就写完整代码。先把DS18B20读回来的原始值通过串口打印验证,确认温度数据正确了,再写595驱动,让数码管先显示一个固定的“8888”,点亮验证通过后,最后才把两个模块组合起来。事实证明这种分步验证的方式,排查问题的时间能节省一半以上。希望这篇记录能让你少走点弯路,有问题也欢迎在评论里交流,尤其是时序调不通或者显示乱码这类经典难题,多聊聊总能找到解决办法。

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

从遍历序列构造二叉树:递归拆解与哈希优化实战

/* 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 1:05:20

双节锂电池主动均衡方案:MP2642GR-Z与PIC18F4610协同设计

/* 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 1:05:16

STM32内部Flash擦写与bin文件生成:HAL库实战与避坑指南

/* 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 1:04:46

哈夫曼树与哈夫曼编码详解:从手算建树到编码解码全流程

/* 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 1:03:26

FFmpeg编译实战:从音视频编码基础到依赖库集成

/* 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 1:02:55

24岁全款买车:从攒钱到养车的实操指南

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

作者头像 李华