news 2026/9/20 7:55:14

基于STM32F103C8T6与ST7540的电力线载波远程抄表系统设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于STM32F103C8T6与ST7540的电力线载波远程抄表系统设计

简介:这份资源是一份面向电子信息、自动化及电气工程专业学生与嵌入式开发初学者的完整毕业设计/课设文档,围绕单片机智能电表抄表系统的软硬件实现展开,适合需要参考课题方案、撰写论文或搭建电力载波通信实验的读者。压缩包内为1个docx文件,约1.57MB,涵盖摘要、绪论、总体方案设计、硬件电路设计、软件代码编写与调试等章节,涉及STM32F103C8T6主控、ST7540电力线载波modem、MAX232电平转换等核心器件的选型与外围电路分析。文档结合FSK调制解调通信技术,给出电路原理图绘制、各功能模块解析及软硬件联调思路,可帮助读者理解抄表电路板从方案论证到调试落地的完整流程。该系统具备可靠性高、可扩展性强、成本低、计量准确等特点,主要应用于居民住宅电量的自动检测、收费与管理。目前已有84人学习下载,适合作为嵌入式电力抄表方向课题设计、论文写作与实验参考的材料。

1. 从一户一表到一台 PC:抄表链路上的断点在哪

一个 600 户的老小区,抄表员每月要爬十几栋楼,抄完还得回去手工录入。问题不在于爬楼累,而在于数据链路是断的——电表读数和收费系统之间,中间靠人当"传输介质",错抄、漏抄、抄多抄少都从这里来。

这套设计给的答案很直接:STM32F103C8T6 当控制核心,ST7540 当电力线载波调制解调器,把电量数据调制成载波信号直接耦合到既有的 220V 电力线上,不额外架设通信线。终端侧电表把读数存进 EEPROM,掉电不丢;PC 端串口助手当管理平台,按一对多主从轮询的方式挨个点名取数。

它的适用边界要说清楚:电力载波信号被配电变压器阻隔,一套系统只能覆盖同一个台区;三相电力线之间存在 10dB~30dB 的信号损失,工程上一般只走单相。适合做单片机课程设计、毕业设计,或者想低成本验证远传抄表原型的人。

2. STM32F103C8T6 与 ST7540 的载波前端电路设计

2.1 主控与载波芯片的分工与选型理由

STM32F103C8T6 是 LQFP48 封装、Cortex-M3 内核的 32 位 RISC 处理器,最高 72MHz,片内 64K Flash 加 20K SRAM。放到这套系统里,真正被看中的是外设数量:3 个 USART、2 个 SPI、2 个 I2C、3 个 16 位定时器加 1 个高级定时器。串口刚好够分——一路给 PC 侧的电平转换芯片,一路给 ST7540,I2C 一路挂 EEPROM,不用做软件模拟。

ST7540 的定位是电力线载波 modem。单电源 7.5~12.5V 供电,内部集成了线路驱动器和 5V、3.3V 两个线性调整器,最大输出电流 50mA,静态电流约 5mA。它自带 8 个可编程发送频率、最高 4800bps 的可编程波特率、500μV(rms) 的接收灵敏度,向下兼容 ST7538 和 ST7537,做升级替换时改动小。内部功放已经集成,外围不需要再搭大功率驱动级,这是选它而不是"MCU + 独立调制电路 + 功放"三段式方案的主要原因。

引脚资源的实际分配如下,这张表在画原理图前最好先定死,后面改线很麻烦。

功能STM32 引脚外设/模式关键参数
PC 通信PA9 / PA10USART1_TX / RX接 MAX232,最高 4.5Mbit/s
ST7540 通信PA2 / PA3USART2_TX / RX接载波模块,本设计跑 4800bps
EEPROMPB6 / PB7I2C1_SCL / SDA开漏输出,外部上拉
程序烧录调试PA13 / PA14SWDIO / SWCLK优先 SWD,不用 JTAG
复位NRST异步复位低电平有效
启动选择BOOT0 / PB2BOOT1决定启动映射

2.2 最小系统:8M 晶振、复位、启动模式与 SWD 烧录

外部 8M 晶振接在 OSC_IN / OSC_OUT 上,经 PLL 九倍频得到 72MHz。如果为了省料去掉外部晶振改用内部 RC,48 脚封装下有两个处理方式:把 OSC_IN、OSC_OUT 各串 10K 电阻接地来提高 EMC 性能,或者把这两个脚重映射成 PD0、PD1 并配置为推挽输出低电平,后者能省掉两个电阻。代价是内部 RC 的温漂和精度都差一档,跑 4800bps 串口还凑合,再往上就容易出误码。

复位电路的作用是在上电时产生足够宽的低电平,NRST 被拉低的这段时间里,内部寄存器和片内 SRAM 被重设;电平回升后 PC 指针从 0 地址取指。看门狗溢出时也是往这里灌高电平强制复位,所以复位脚不要挂大电容,否则看门狗复位会被拖慢。

BOOT0 的接法是最容易踩坑的一处。常见做法是经 10K 电阻接地,从片内 Flash 启动,程序烧完直接跑;要烧录时才把 BOOT0 拉到 3.3V 进系统存储器,走串口 ISP 下载。如果 BOOT0 长期拉高又没接对 BOOT1,上电后不会进你的应用程序,现象是"程序明明烧进去了却不动"。

时钟树配置建议直接操作寄存器,逻辑比库函数清楚:

/* 8MHz 外部晶振 -> PLL 9 倍频 -> 72MHz 系统时钟 */ RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)); /* 等 HSE 稳定 */ FLASH->ACR |= FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2; /* 72MHz 需 2 个等待周期 */ RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL); RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9; /* HSE 不分频,9 倍频 */ RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; /* APB1 最高 36MHz */ RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; /* APB2 保持 72MHz */ RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)); RCC->CFGR |= RCC_CFGR_SW_PLL; /* 切系统时钟到 PLL */ while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);

参数上要盯住 APB1 和 APB2 的分频。USART1 挂在 APB2 上跑 72MHz,USART2 挂在 APB1 上跑 36MHz,两者的 BRR 计算基准不同。以 USART2 跑 4800bps 为例,BRR = 36 000 000 / 4800 = 7500,写成 USART2->BRR = 0x1D4C。如果分频配错成 72MHz,同一个 BRR 值算出来就是 9600bps,对端收到的全是乱码。

烧录口用 PA13(SWDIO)、PA14(SWCLK) 的 SWD 模式。JTAG 在大数据量下载时失败率明显更高,SWD 占用引脚少、高速下更稳,调试器接四根线(3.3V、GND、SWDIO、SWCLK)就能用。

2.3 ST7540 外围:滤波网络与电流限制

ST7540 到电力线之间不是直连,中间有三级滤波,顺序和作用不能随意调换。

滤波级位置作用调整时的观察点
发送有源滤波PA_OUT 之后抑制功放输出的谐波,防止污染其他频段输出波形是否削顶
发送无源滤波耦合变压器前阻抗匹配 + 进一步滤除带外分量耦合端电压幅度
接收无源滤波RX_IN 之前窄带选频,压低带外噪声弱信号下的接收误码率

发送通路最终通过耦合变压器并到电力线上。接收通路经滤波后直接进 RX_IN,芯片内部再做三次混频滤波,在低信噪比条件下保证动态范围。24 脚的 CL 与 SVSS 之间接一个限流设定电阻,这个电阻决定功放的最大输出电流——阻值取得太小,轻载时波形失真;取得太大,重载时驱动不足。常用做法是先按典型值装上去,用示波器看耦合端的波形,边调边看,不要一上来就按计算值赌。

还有两处必须接地:TEST1 直接接数字地,TEST2 接模拟地(SVSS)。这两个脚悬空时芯片行为不确定,表现是收发时好时坏,很难查。

2.4 TTL/RS232 电平转换与 I2C EEPROM 电路

STM32 的 USART1 输出是 CMOS/TTL 电平,PC 的串口是 RS232 电平,两者之间用 MAX232 转换。MAX232 靠内部电荷泵把 5V 泵成 ±10V 左右,需要外接 4 个 1μF 电容,容值不足会直接导致通信距离变短或者完全不通。接线记住是交叉:单片机 TX 接 MAX232 的 T1IN、对应输出 T1OUT 接 DB9 的 RXD;单片机 RX 接 R1OUT,R1IN 接 DB9 的 TXD。

终端电表侧的数据存放在 EEPROM 里,STM32 通过 I2C1(PB6/PB7) 读写。SDA 和 SCL 都是开漏输出,必须外接上拉电阻,4.7K 是常用值,线长超过 20cm 可以降到 2.2K 提高上升沿速度。器件地址的低三位由 A0~A2 引脚决定,同一块板上挂多片时靠这三根线区分。

/* I2C1 初始化:100kHz 标准模式,PB6=SCL,PB7=SDA */ RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; GPIOB->CRL &= ~(0xFF << 24); GPIOB->CRL |= (0xEE << 24); /* PB6/PB7 复用开漏输出,50MHz */ I2C1->CR2 = 36; /* APB1 36MHz,单位 MHz */ I2C1->CCR = 180; /* 标准模式:36MHz/(2*100kHz) */ I2C1->TRISE = 37; /* 36MHz * 1us + 1 */ I2C1->CR1 |= I2C_CR1_PE; /* 使能 I2C */

这里有个隐蔽的坑:APB1 一旦从 36MHz 改成 72MHz,CCR 和 TRISE 必须同步重算,否则 I2C 时序错乱,表现为能写进去但读出来是 0xFF,或者干脆卡在等待 ACK 的循环里。

3. ST7540 寄存器配置与载波数据收发驱动

3.1 REG_DATA 与 RxTx 的四种组合

ST7540 靠两根控制线切换工作状态,这两根线的组合决定了芯片此刻是收数据、发数据,还是在读写内部寄存器。程序里第一件事就是把这张真值表封成函数,不然后面到处直接操作 IO 口,出错很难定位。

REG_DATARxTx工作状态CLR/T 角色
00数据传送发送时钟输入
01数据接收恢复时钟输出
11控制寄存器读出寄存器读时钟
10控制寄存器写入寄存器写时钟

状态切换后有一段 TCC 时间的空闲期,数据线上不做任何事,等芯片内部 PLL 稳定。很多"发送时机不对导致丢首字节"的问题,就是把 TCC 等待省掉了。

#define MODE_TX_DATA 0x00 /* REG_DATA=0, RxTx=0 */ #define MODE_RX_DATA 0x02 /* REG_DATA=0, RxTx=1 */ #define MODE_REG_READ 0x03 /* REG_DATA=1, RxTx=1 */ #define MODE_REG_WRITE 0x01 /* REG_DATA=1, RxTx=0 */ static void st7540_set_mode(uint8_t mode) { /* REG_DATA 接 PB0,RxTx 接 PB1 */ GPIOB->BSRR = (mode & 0x01) ? GPIO_BSRR_BS0 : GPIO_BSRR_BR0; GPIOB->BSRR = (mode & 0x02) ? GPIO_BSRR_BS1 : GPIO_BSRR_BR1; if (mode & 0x01) { /* 进入寄存器模式才需要时钟配合 */ delay_us(50); } }

3.2 控制寄存器初始化:频率、波特率、看门狗

芯片上电后必须先写控制寄存器把发送频率、波特率、看门狗等参数配好,再切到数据模式。寄存器写入是同步串行:CLR/T 提供时钟,TXD 上先送寄存器地址再送数据,上升沿采样。初始化序列的顺序建议固定为:先复位、再配频率、再配波特率、最后开看门狗。

static void st7540_init(void) { st7540_set_mode(MODE_REG_WRITE); clr_t_low(); /* 拉低进入空闲,等待 TCC */ delay_us(TCC_US); st7540_wr_reg(REG_CTRL_1, 0x00); /* 先清零控制位,避免残留配置 */ st7540_wr_reg(REG_FREQ_SEL, FREQ_CHx); /* 选择载波通道,8 档可选 */ st7540_wr_reg(REG_BAUD_SEL, BAUD_4800);/* 波特率必须与从机一致 */ st7540_wr_reg(REG_WD_CTRL, WD_EN); /* 使能看门狗,掉线时自动复位输出 */ clr_t_high(); st7540_set_mode(MODE_RX_DATA); /* 初始化完先进接收态待命 */ }

参数上有两个硬约束:发送频率必须落在允许的载波频段内,且同一台区内所有节点用同一档;波特率两端必须完全一致,ST7540 最高支持 4800bps,超了直接不通信。看门狗建议开,载波信道噪声重,芯片偶尔会卡在某个状态,看门狗能把发送通路复位回来。

3.3 发送与接收:UART 模式下的数据流

UART/SPI 引脚拉高时,RxD、TxD 走 UART 类型,ST7540 作为通信主体提供 CLR/T 参考时钟;拉低则走 SPI 类型。本设计用 UART 模式,STM32 侧的 USART2 只需按普通串口收发,不用自己造时钟。

发送流程是:切到 MODE_TX_DATA,拉低 CLR/T 进空闲,等 TCC,然后往 USART2->DR 里写字节;接收流程是:切到 MODE_RX_DATA,CLR/T 由芯片输出恢复时钟,RxD 上的数据在上升沿稳定,STM32 用中断接收。接收态下芯片会关掉内部发送模块,发送态下关掉接收模块,这是它静态电流能压到 5mA 的原因,不用手动去管。

void st7540_send(const uint8_t *buf, uint16_t len) { st7540_set_mode(MODE_TX_DATA); clr_t_low(); delay_us(TCC_US); for (uint16_t i = 0; i < len; i++) { while (!(USART2->SR & USART_SR_TXE)); /* 等发送寄存器空 */ USART2->DR = buf[i]; } while (!(USART2->SR & USART_SR_TC)); /* 等最后一字节发完 */ st7540_set_mode(MODE_RX_DATA); /* 立刻切回接收,准备应答 */ }

发送完立刻切回接收态很关键。主从轮询下从机应答通常只有几毫秒的窗口,如果发送后还停在发送态,应答帧会直接丢掉,表现为"能发不能收"。

3.4 数据帧解析:前导码、地址、命令与校验

载波信道上电噪声大,帧头必须有足够长的前导码让接收侧的窄带滤波器锁定。帧结构上建议固定字段顺序,长度用显式字段而不是靠结束符判断。

uint8_t frame_parse(const uint8_t *buf, uint16_t n, meter_frame_t *out) { uint16_t i = 0; while (i + 1 < n && !(buf[i] == 0xAA && buf[i+1] == 0x55)) i++; /* 找帧头 */ if (i + 6 > n) return 0; /* 长度不够,直接丢 */ out->addr = buf[i+2]; /* 电表地址 */ out->cmd = buf[i+3]; /* 命令码 */ out->len = buf[i+4]; /* 数据长度 */ if (out->len > sizeof(out->data) || i + 5 + out->len + 2 > n) return 0; /* 越界保护,别省这一步 */ for (uint8_t k = 0; k < out->len; k++) out->data[k] = buf[i+5+k]; uint16_t crc_rx = buf[i+5+out->len] | (buf[i+6+out->len] << 8); if (crc_rx != crc16_modbus(&buf[i+2], out->len + 3)) return 0; /* 校验不过,交给上层重发 */ return 1; }

解析函数里最不能省的是长度越界保护。载波信道误码是常态,收到半截帧时如果直接按 len 拷贝,会踩到缓冲区外面的内存,现场表现是"跑几个小时就死机",比通信失败难查得多。

4. PC 主节点协议与电表数据读写实现

4.1 一对多轮询:为什么不用侦听冲突

系统拓扑是 PC 管理平台作主节点,所有终端电表作从节点,控制命令一律由主节点发出,从节点只应答不主动发。这样一来就不存在多节点同时抢信道的问题,也就不需要载波侦听多路访问那套机制——在电力线这种高噪声、高衰减的信道上做冲突检测本来就不可靠,退回到主从轮询反而稳。

代价是轮询周期。4800bps 下一帧 20 字节大约 40ms,加上 TCC 等待和从机响应时间,单节点一轮按 100ms 估,128 户一个小区的完整轮询约 13 秒。对抄表这种一天一次的业务完全够用,但别指望它做实时负荷监控。

4.2 串口数据帧格式与 CRC16

PC 与主板卡之间、主板卡与从板卡之间用同一套帧格式,字段定义要一次定死,后面扩功能只加命令码不动结构。

字段长度说明
前导码2 字节固定 0xAA 0x55
地址1 字节0x00 为广播,0x01~0xFE 为电表编号
命令码1 字节0x01 读电量,0x02 写参数,0x03 校时
长度1 字节后续数据域字节数
数据域N 字节电量为 4 字节 BCD 或小端整数
CRC162 字节Modbus 多项式 0xA001,低字节在前
uint16_t crc16_modbus(const uint8_t *p, uint16_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *p++; for (uint8_t i = 0; i < 8; i++) crc = (crc & 0x01) ? (crc >> 1) ^ 0xA001 : (crc >> 1); } return crc; } /* 组帧:返回整帧长度,buf 由调用者保证足够大 */ uint16_t frame_build(uint8_t *buf, uint8_t addr, uint8_t cmd, const uint8_t *data, uint8_t len) { uint16_t i = 0; buf[i++] = 0xAA; buf[i++] = 0x55; buf[i++] = addr; buf[i++] = cmd; buf[i++] = len; for (uint8_t k = 0; k < len; k++) buf[i++] = data[k]; uint16_t crc = crc16_modbus(&buf[2], len + 3); /* 从地址域开始算 */ buf[i++] = crc & 0xFF; /* 低字节在前 */ buf[i++] = crc >> 8; return i; }

CRC 的起算位置必须和解析端保持一致,都从地址域算起,把前导码排除在外。这一处对不上,整个系统一帧都收不下来,但单独测 CRC 函数又是对的,很容易在两个模块之间来回甩锅。

4.3 PC 侧串口收发与超时重发

STM32 接 MAX232 的那路串口优先用中断接收,配合一个环形缓冲和空闲超时状态机,把不定长帧切出来。

void USART1_IRQHandler(void) { if (USART1->SR & USART_SR_RXNE) { uint8_t b = USART1->DR; /* 读 DR 自动清 RXNE */ rx_ring_push(b); /* 入环形缓冲 */ last_rx_tick = tick_ms; /* 刷新帧间超时计时 */ } if (USART1->SR & USART_SR_ORE) { /* 溢出错误必须显式清 */ (void)USART1->DR; } }

主循环里判断tick_ms - last_rx_tick > 5就认为一帧结束。PC 侧发送命令后启动 200ms 超时,超时未收到合法应答就重发,连续三次失败把该节点标记为离线。这个重发次数不要设太大,载波信道一次轮询本来就不长,重发太多次会拖垮整个轮询周期。

4.4 读电表数据:从 EEPROM 取电量并回帧

终端侧电表把电量存在 EEPROM 里,主板卡收到读命令后按地址取出再回帧。读操作走标准 I2C 时序:起始、器件地址+写、内存地址、重启、器件地址+读、连续读、非应答、停止。

uint8_t ee_read(uint8_t dev, uint16_t off, uint8_t *buf, uint8_t len) { i2c_start(); if (!i2c_write((dev << 1) | 0)) return 0; /* 器件地址 + 写 */ if (!i2c_write(off >> 8)) return 0; if (!i2c_write(off & 0xFF)) return 0; i2c_start(); /* 重复起始,不释放总线 */ if (!i2c_write((dev << 1) | 1)) return 0; /* 器件地址 + 读 */ for (uint8_t i = 0; i < len; i++) buf[i] = i2c_read(i == len - 1); /* 最后一字节回 NACK */ i2c_stop(); return 1; }

注意最后一字节必须回非应答再发停止位,回应答会让 EEPROM 继续输出下一字节,把总线拉死。EEPROM 写周期约 5ms,写完立刻读会拿到旧数据,两次写之间要留够等待时间,或者用应答轮询确认写完。

4.5 写电表数据与掉电保护

写电表参数(比如费率、表号)时,单次写不超过 EEPROM 的页大小,跨页要拆成多次。更重要的是顺序:先把新数据写到一个备份区,校验通过后再更新主区标志位。抄表系统最怕的是写一半掉电,主区数据变成半新半旧,电量对不上账。

5. 联调顺序与串口、载波的故障定位

联调不要一上来就把 PC、载波、电表全接上,按链路分段验证,出问题时才能定位到具体一段。

第一步,只接 STM32 和 PC,用串口助手 4800-8-N-1 发固定字符串,确认收发正常;第二步,示波器看 ST7540 的 CLR/T 有没有时钟输出,判断芯片是否正常上电工作;第三步,两台板子之间用短线直接连 TX_OUT 和 RX_IN,跳过电力线验证调制解调本身;第四步,接耦合变压器和电力线,短距离通信;第五步,接电表终端,跑完整轮询。

常见故障的定位路径如下。

现象高概率原因定位手段
串口收到乱码波特率基准算错、晶振未起振量 OSC_OUT 有无 8MHz,核对 APB 分频
完全收不到数据TX/RX 未交叉、MAX232 电容不足短接 MAX232 两端做回环测试
能发不能收发送后未及时切回接收态抓 RxTx 引脚电平变化时序
通信时好时坏限流电阻取值不当、TEST 脚悬空示波器看耦合端波形有无削顶
跨楼层就断跨配电变压器、三相耦合损失确认两端在同一台区同一相
跑数小时死机帧解析越界、串口溢出未清加看门狗,检查 ORE 标志处理

有两个技巧值得单独说。一是做一条"回环线",把 ST7540 的 TX_OUT 直接短接到 RX_IN,写一段自发自收的测试程序,能把调制解调部分和电力线耦合部分彻底隔离开,省掉大量来回换板子的时间。二是所有串口和 I2C 的等待循环都加超时退出,载波信道噪声重,硬件偶尔不响应是正常的,卡死在 while 里等标志位,比通信失败更难查——现场表现就是"设备死机,断电重启又好"。

最后调限流电阻时,别按计算值一次到位。先在耦合端接示波器,从小阻值慢慢往上调,看波形从削顶变成完整正弦那个点,再往上留一点余量。这个值跟实际线路阻抗相关,换个台区可能要重调。

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

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

Claudian 插件安装配置教程:把 Claude Code 装进 Obsidian 知识库

Claudian 插件安装配置教程&#xff1a;把 Claude Code 装进 Obsidian 知识库 【免费下载链接】claudian An Obsidian plugin that embeds Claude Code/Codex as an AI collaborator in your vault 项目地址: https://gitcode.com/GitHub_Trending/cl/claudian 收藏夹里…

作者头像 李华
网站建设 2026/9/20 7:48:24

2026年AI工具选型指南:性价比与合规性实战解析

1. 项目概述&#xff1a;AI工具选型的时代需求最近两年AI工具呈现爆发式增长&#xff0c;但真正能在实际工作中稳定发挥价值的却不多。作为长期关注生产力工具的技术博主&#xff0c;我测试过上百款AI产品后发现&#xff1a;2026年的AI工具市场已经进入"精耕细作"阶段…

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

用BrewUI可视化Homebrew:包管理与依赖关系一目了然

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

作者头像 李华
网站建设 2026/9/20 7:46:39

Jetson Orin NX 16G:边缘AI部署的工程黄金标准

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

作者头像 李华