尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
打造STM32舵机库:STS3215串口帧协议与半双工总线控制
简介针对飞特舵机STS3215的控制库文件面向机器人、无人机及其他自动化设备开发者意在简化舵机驱动开发与底层通信管理。压缩包共4个文件包含2个C源码和2个头文件源码负责协议解析、指令发送等功能实现头文件对外提供函数接口与数据结构定义整体约6KB轻量易集成。该库封装了初始化配置、角度/速度控制、PWM脉宽调节、运行状态反馈及异常处理等常用接口并支持多舵机同步控制开发者可直接调用相关函数配合示例逻辑快速完成驱动移植与调试。已有1579人学习适合正在学习嵌入式运动控制、或需要快速驱动STS3215的工程师参考使用。1. 为什么 STS3215 需要一套自己的库第一次用飞特舵机 STS3215 的人多半是被它那根三线数据口吸引的主控只要一根线就能把七八个舵机串成一条总线位置、速度、电压、温度全都能读回来比 PWM 舵机一根信号线带一个舵机干净太多。但真正开始写代码时才会意识到这颗舵机不走占空比那套它跑的是串口帧协议——帧头、ID、指令号、校验和少一个字节舵机都不理你。有人直接拿串口助手手工拼十六进制有人把别人的整包代码搬过来改到头来都绕不开一件事针对 STS3215 写一个自己的库文件把协议封装成 readPosition、writePosition 这种一眼能看懂的接口。这颗舵机的库文件和其他舵机库最大的不同在于它必须自己管半双工方向切换、管一帧数据的完整性、管超时重试还要把 0~4095 的位置编码换算成角度。写库之前如果没把协议摸清后面每加一个功能都会返工。这篇就把这个过程讲透先立协议再搭库最后落到 STM32 上跑通再把参数设置和排错一并收掉。适合正在做机械臂、云台或多自由度机器人手里已经拿到几颗 STS3215 的嵌入式工程师。2. STS3215 的帧协议与寄存器地址写库前要摸清的底层2.1 STS3215 硬件接口与 TTL 半双工总线形态STS3215 的物理层和普通 UART 有一个明显区别它不提供独立的 RX、TX 两根线而是把发送和接收合并到一根数据线上TTL 电平半双工。总线空闲时主控侧处于接收态数据线被上拉到空闲高电平主控要发指令时先把方向脚拉成发送模式发完再切回接收。这个切换动作必须由驱动代码来做所以库的物理层函数里一定会出现“方向脚”这个参数这是写文件时最容易漏掉的一环。多颗舵机级联时数据线是手拉手串在一起的。舵机内部由固件自行切换接收和发送状态主控不需要管每颗舵机的方向只需要保证自己在发送时总线上没有第二个主设备在发数据。常见接线错误是舵机电源和主控逻辑电源共用一路输出STS3215 堵转时电流能到 2A 以上瞬间拉低电压UART 电平跟着塌表现出来就是舵机偶尔动一下、偶尔完全没响应。我一般会单独用 2S 锂电池或大电流 BEC 给舵机供电信号线只需要和主控共地。项目参数供电电压4.8V~7.4V推荐 6.0V~7.4V总线电平TTL半双工默认波特率10000001Mbps串口格式8 数据位、1 停止位、无校验物理接口红 VCC、黄 DATA、黑 GND默认波特率 1Mbps 意味着每字节在总线上只占 10µs一次读位置的完整交互发 8 字节、收 9 字节不到 200µs。这个速度对多舵机轮询很友好但对时序要求也更严格方向切换时机不对最后一位就会被截掉。2.2 帧格式与校验和从 ID 累加到最后一个参数STS3215 的帧由 6 段构成两个 0xFF 帧头、1 字节 ID、1 字节 LEN、1 字节指令号、N 字节参数、1 字节校验和。LEN 不是整帧长度而是“参数个数 2”加的这两个字节分别是指令号和校验和本身整帧字节数等于 4 LEN。也就是说LEN 同时告诉接收方后面还要收多少字节这是中断接收状态机的主要依据。段长度说明帧头2固定 0xFF 0xFFID11~2530xFE 为广播地址LEN1参数个数 2CMD1指令号如 0x03 写、0x02 读PARAMSN参数CHK1校验和校验和的计算不覆盖帧头只从 ID 累加到最后一个参数然后取反、截取低 8 位。这个规则在库的封包函数里实现一次所有指令复用static uint8_t sts_checksum(const uint8_t *pkt, uint8_t total_len) { uint8_t sum 0; for (uint8_t i 2; i total_len - 1; i) { sum pkt[i]; // 从 ID 累加到 CHK 前一字节 } return (uint8_t)(~sum); }total_len是整帧字节数。循环从下标 2 开始是因为下标 0、1 是帧头不参与校验停在total_len - 1即累加到校验和本身的前一个字节。返回值取反后就是 CHK。实际封包时还需要一个通用函数把帧头、ID、LEN、CMD、参数和 CHK 拼起来static uint8_t sts_build_frame(uint8_t id, uint8_t cmd, const uint8_t *params, uint8_t n, uint8_t *out) { out[0] 0xFF; out[1] 0xFF; out[2] id; out[3] n 2; // LEN 参数个数 2 out[4] cmd; for (uint8_t i 0; i n; i) { out[5 i] params[i]; } out[5 n] sts_checksum(out, 5 n 1); return 5 n 1; // 返回整帧长度 }n是参数个数out是调用方提供的缓冲区。返回的整帧长度 5 n 1正好对应 5 个固定字节帧头 2 ID LEN CMD加 n 个参数加 1 个校验和。写库时所有指令都走这两个函数可以保证每一帧的长度和校验不出错。2.3 常用指令和寄存器角度、速度、扭矩在哪读库文件真正要用的指令不多核心是下面这四条。PING 用于探测舵机在线状态READ 和 WRITE 用于读写内部寄存器SYNC_WRITE 用于一条总线上多个舵机同时动作。指令值用途PING0x01探测舵机是否在线READ0x02读取寄存器数据WRITE0x03写入寄存器数据SYNC_WRITE0x06一帧同时写多颗舵机寄存器地址是承接协议和业务的关键。STS3215 的控制表里扭矩开关在 0x0D加速度在 0x0E目标速度在 0x10目标位置在 0x12当前位置在 0x16。注意 0x0E 和 0x10 之间有一个保留字节跨地址连续写时会踩到它所以运动参数我一般分开写不贪图一次写 5 个字节。地址名称读写属性说明0x0D扭矩开关RAM 读写0 关 1 开0x0E加速度RAM 读写0~2540x10目标速度RAM 读写0~40950x12目标位置RAM 读写0~40950x16当前位置RAM 只读0~40950x18电压RAM 只读单位 0.1V0x19温度RAM 只读单位 ℃位置编码是 0~4095 对应 0°~360°2048 是中位 0°。也就是说每 1 个 LSB 约等于 0.088°。角度换算公式为deg (pos - 2048) * 360.0f / 4096.0f反之pos deg * 4096.0f / 360.0f 2048。多字节数据一律低字节在前写 16 位位置时先放低 8 位再放高 8 位。3. 库文件的三层结构物理层、协议层、API 层怎么拆3.1 为什么不用预编译库源码才是这个库的正确交付形态很多刚接触库文件的人第一反应是找一个现成的.a或.lib文件拿过来链接进工程。实际上 STS3215 这类舵机库预编译的交付形态反而麻烦。IAR 里生成库文件确实方便新建一个 library 工程、把协议层源码编译进去就行但生成时选择的优化等级、浮点计算选项必须和最终应用工程保持一致否则移植到另一个项目时又要重编一次。更关键的是物理层强依赖具体 MCU 的串口外设这部分没法打包进预编译库。所以我的做法是把库拆成三层最底下是物理层只有bus_send、bus_recv、方向脚切换三个函数需要用户针对自己的板子实现中间是协议层负责封包、解包、校验和计算与硬件无关最上层是 API 层提供sts_write_pos、sts_read_pos这类舵机级接口。协议层和 API 层用源码形式维护物理层留成回调或者弱函数换板子只改最底下那一小段。3.2 PING 扫描初始化时先看看总线上有几颗舵机上电后第一件事不是发位置指令而是把总线上实际存在的舵机 ID 扫一遍。这个功能在调试阶段能省下大量排查时间也能防止代码里写死 ID 之后舵机地址被改过导致静默失败。PING 指令本身不带参数只要收到任意一帧响应就认为该 ID 在线。uint8_t sts_scan(uint8_t *found, uint8_t max_found) { uint8_t n 0; for (uint8_t id 1; id 253; id) { uint8_t cmd[6]; sts_build_frame(id, 0x01, NULL, 0, cmd); // PING bus_send(cmd, 6); if (bus_wait_frame(2) 0) { // 2ms 超时 found[n] id; if (n max_found) break; } } return n; }扫描循环里有两个细节。第一ID 只扫到 253不要用 0xFE 广播地址做 PING否则总线上所有舵机同时应答数据冲突一根线根本收不到有效帧。第二超时时间给 2ms 而不是更长因为 1Mbps 下应答帧只有几十微秒2ms 足够覆盖舵机内部处理时间同时把整轮扫描控制在半秒左右。bus_wait_frame在物理层实现它负责把收到的帧暂存到缓冲区并返回帧长度收不到就返回 0。3.3 writePosition 的实现位置寄存器 0x12 与两字节小端写入写位置是调用最频繁的接口。它只需要一个 WRITE 指令从地址 0x12 开始写两个字节位置低字节、位置高字节。速度、加速度单独提供接口避免跨过保留地址做连续写。uint8_t sts_write_pos(uint8_t id, uint16_t pos) { uint8_t params[3]; params[0] 0x12; // 目标位置寄存器地址 params[1] pos 0xFF; // 低字节在前 params[2] (pos 8) 0xFF; uint8_t frame[9]; uint8_t len sts_build_frame(id, 0x03, params, 3, frame); bus_send(frame, len); return len; }写指令发出后不等待应答直接释放总线。飞特舵机对 WRITE 指令默认会回一帧状态但工程实践里这个应答利用率很低反而会占用总线时间需要确认时用 PING 或者回读位置更可靠。lib 文件里的这段逻辑是整个库的地基所有上层动作最终都落到这个函数上。3.4 readPosition 与接收状态机超时重试怎么设计读位置用 READ 指令从地址 0x16 读 2 字节。关键在于响应帧的数据位置帧头 2 字节、ID 1 字节、LEN 1 字节、CMD 1 字节、地址 1 字节之后才是数据。所以数据低字节在响应缓冲区下标 6高字节在下标 7。int16_t sts_read_pos(uint8_t id, uint8_t retry) { for (uint8_t r 0; r retry; r) { uint8_t params[2] {0x16, 0x02}; // 地址 长度 uint8_t frame[8]; sts_build_frame(id, 0x02, params, 2, frame); bus_send(frame, 8); uint8_t *rsp bus_wait_frame(2); // 阻塞收帧 if (rsp ! NULL) { return (int16_t)(rsp[6] | (rsp[7] 8)); } } return -1; // 超时失败 }连续读失败时需要在重试之间留出总线空闲时间。我的做法是在bus_wait_frame返回 NULL 后加一个 1ms 延时让舵机内部状态机复位避免上一次半截帧干扰下一次通信。retry一般给 3三次都失败就认为舵机离线或硬件异常调用方可以根据返回值决定要不要报警。4. 飞特舵机 STS3215 的 STM32 控制UART 与方向脚集成4.1 CubeMX 里怎么配1Mbps、8N1、一个方向脚在 STM32 工程里集成这个库CubeMX 的配置其实很少。UART1 开异步模式波特率填 1000000字长 8停止位 1无校验使能 UART1 全局中断。方向脚随便选一个 GPIO 配成推挽输出比如 PB1初始电平拉低让总线默认处于接收态。开启中断接收时注意一点1Mbps 下每字节间隔约 10µs逐字节中断对 Cortex-M 内核来说压力不大但中断回调函数里绝对不能做阻塞操作。常见做法是中断里只做状态机和字节搬运把帧解析放到主循环。时钟树那边确保给 UART1 的时钟能整除 1000000比如 84MHz PCLK 经过分频后正好整除时波特率误差为零。4.2 bus_send 里等 TC 标志最后一个字节别被腰斩方向切换的时机是 STM32 集成里最隐蔽的坑。HAL_UART_Transmit返回时UART 外设已经把数据写进发送移位寄存器但最后一个字节可能还在移位过程中。如果函数一返回就立刻把方向脚拉低最后几个位会被硬生生截断舵机收到的帧不完整表现为指令偶发无效。void bus_send(const uint8_t *buf, uint16_t len) { DIR_GPIO_Port-BSRR DIR_Pin; // 方向脚拉高进入发送态 HAL_UART_Transmit(huart1, (uint8_t *)buf, len, 2); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET) { // 等待最后一字节完全移出移位寄存器 } DIR_GPIO_Port-BRR DIR_Pin; // 回接收态 }UART_FLAG_TC是发送完成标志它置位表示移位寄存器已经清空这时再切换方向才安全。如果用的是 DMA 发送需要在 DMA 传输完成中断里等 TC不能只等 DMA 的完成标志。这个细节新手第一次调舵机总线时十有八九会踩到。4.3 中断接收状态机按 LEN 收整帧接收侧的状态机围绕 LEN 字段展开。收到帧头 0xFF 0xFF 后下一个字节就是 LEN它直接告诉状态机后面还有多少字节要收。这样不管响应帧长短状态机都能精确地在一帧结束时复位。uint8_t rx_state 0; // 0 空闲 1 半帧头 2 已收LEN 3 收body uint8_t rx_len, rx_idx, rx_buf[16]; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart huart1) { uint8_t b rx_byte; if (rx_state 0) { if (b 0xFF) rx_state 1; } else if (rx_state 1) { if (b 0xFF) rx_state 2; else rx_state 0; // 帧头错误复位 } else if (rx_state 2) { rx_len b; // LEN rx_idx 0; rx_state 3; } else { if (rx_idx 16) rx_buf[rx_idx] b; if (rx_idx rx_len) { // body 收完一帧完整 frame_ready 1; rx_state 0; } } HAL_UART_Receive_IT(huart1, rx_byte, 1); } }这段代码里rx_buf存的是从 CMD 开始的 body 数据不包含帧头。frame_ready标志由主循环轮询一旦置位就把rx_buf里的数据拷走再清零。状态机必须处理帧头错误的情况比如收到 0xFF 后第二字节不是 0xFF要立刻回到空闲态重新同步否则之后所有帧都会错位。4.4 SYNC_WRITE 同步写一颗总线上多舵机同时动作单发模式下逐个写多颗舵机存在时间差机械臂在启动瞬间会体现为“波浪式”动作。SYNC_WRITE 指令能在一帧里携带多个舵机的 ID 和各自的目标值所有舵机在同一时刻执行。这里 ID 固定为 0xFE 广播参数区从低地址开始每颗舵机占1 data_len字节ID 数据。uint8_t sts_sync_write_pos(const uint8_t *ids, const uint16_t *pos, uint8_t num) { uint8_t frame[2 1 1 2 (1 2) * num 1]; uint8_t p[2 (1 2) * num]; p[0] 0x12; // 起始地址目标位置 p[1] 0x02; // 每颗舵机数据长度 2 字节 for (uint8_t i 0; i num; i) { p[2 i * 3] ids[i]; p[2 i * 3 1] pos[i] 0xFF; p[2 i * 3 2] (pos[i] 8) 0xFF; } uint8_t len sts_build_frame(0xFE, 0x06, p, 2 3 * num, frame); bus_send(frame, len); return len; }LEN的计算是这套帧里最容易错的地方参数个数等于2 3 * num其中 2 是地址和长度字段每颗舵机占 3 字节。同步写适合关节数量少、动作同步性要求高的场景如果只是做云台俯仰这种单舵机控制用普通 WRITE 就足够了。5. STS3215 库参数怎么设加速度、扭矩、电压保护与改 ID 的坑5.1 位置、速度、加速度的取值与典型值库文件里这几个参数的取值范围直接决定了舵机的运动表现。位置范围 0~4095但实际机械限位通常远小于这个范围软件层应该再做一层限幅防止舵机撞到机械结构。参数寄存器范围典型值说明目标位置0x120~40952048 居中0.088°/LSB目标速度0x100~40951000~2000数值越大转动越快加速度0x0E0~25430~80值越大启停越柔扭矩开关0x0D0/110 时舵机可自由转动加速度这个参数的直观理解设为 0 时舵机会用最大加速度起步机构会猛地一抖设成 50 左右启动和刹车的过渡明显变缓。速度不要直接拉满到 4095尤其是带负载的情况下启停冲击对减速齿轮不友好。电压保护阈值写在 EEPROM 里低于阈值舵机直接不响应库初始化时可以读一次 0x18把这个值打印出来确认供电链路是否健康。5.2 超时、重试与总线占用read 失败别死等总线通信里最忌讳的就是无限等待。sts_read_pos的阻塞等待如果没超时保护舵机掉线时整个控制循环会卡死。库的设计上所有读操作都走同一个bus_wait_frame(timeout)timeout 单位毫秒内部用HAL_GetTick实现。每次读失败后还要确保方向脚已经回到接收态否则下一次发送时总线还停留在上一轮的发送状态。重试机制放在 API 层而不是物理层。物理层只负责“发一帧、收一帧”超时或校验错误都返回失败API 层决定要不要重试、重试几次。这样分层的好处是以后要在总线上挂多个舵机控制循环可以自己决定哪个舵机优先重试而不是被物理层阻塞住。5.3 改 ID 和波特率EEPROM 写错后怎么救ID 和波特率存在 EEPROM 区域改错之后舵机不会像 RAM 寄存器那样重启即恢复。库文件里提供写 ID 的功能并不难难的是写错之后的救回流程。我一般只在调试工具里改 ID不在业务代码里频繁操作 EEPROM因为 EEPROM 有写入寿命限制每次上电都写一次 ID 等于慢性自杀。改完 ID 或波特率后需要重新上电或者按协议发特定指令让舵机重新加载配置。最危险的场景是改波特率时把数值写错舵机从此用未知速率通信。这时候不要慌把舵机单独从总线上拆下来用飞特官方的调试软件配合 USB 转 TTL 直连单台舵机重新写回正确参数。这也是为什么每颗舵机在入库前就应该设置好独立 ID而不是等装到机械结构上再改——后者一旦失联拆装的代价远大于设置本身。5.4 掉电重上电后防猛转先把当前位置读回来STS3215 的位置反馈是 RAM 寄存器掉电后清零上电默认停在 2048 这个中位值。如果上电后直接下发一个目标位置而舵机当前实际角度在 90°它会立刻以最大速度冲向中位轻则撞限位重则扫齿。所以库的初始化序列里扭矩开关上电后是关闭的这时候应该先读一次当前位置把读到的值作为目标位置回写一次再打开扭矩。int16_t cur sts_read_pos(id, 3); if (cur 0) { sts_write_pos(id, cur); // 先让目标等于当前角度 } sts_write_reg(id, 0x0D, 1); // 打开扭矩这段代码的顺序不能反。先写位置再开扭矩舵机才会在执行目标位置之前建立位置闭环如果先开扭矩再写位置中间那一小段时间舵机处于失控自由状态反而容易抖一下。库的提供者应该在初始化函数里就做好这个保护而不是把责任留给使用者。6. 验证与排错库写完了舵机不动按这三步查6.1 用逻辑分析仪看总线上的一帧代码写完先别急着调动作把逻辑分析仪夹在 DATA 线和 GND 之间采样率至少 4Mbps触发条件设成下降沿。正常的一帧发送应该能看到连续两个低电平脉冲0xFF 帧头然后是 ID、LEN、CMD 和参数。如果看到帧头发完就断了十有八九是bus_send里方向脚切得太早也就是 4.2 节说的没等 TC 标志如果整个波形都在但舵机没反应下一步检查方向脚极性是否接反。6.2 无响应、回读抖动、上电猛转的分诊表现象主要嫌疑排查方法完全无响应供电、共地、方向脚极性万用表量 VCC示波器看数据线空闲电平偶尔动一下舵机电源被拉垮示波器探头夹舵机供电端观察启动瞬间电压跌落首尾字节丢失方向切换过早外观特征最后一个响应字节总是不完整回读位置抖动线太长达不到 1Mbps 信号质量降低波特率到 500000或缩短总线距离上电猛转位置寄存器清零按 5.4 节先回写当前角度总线不是越长越好1Mbps 的 TTL 信号在几十厘米飞线上就开始劣化。如果机械结构决定了线必须很长把波特率降到 500000 往往比加各种调理电路更省事代价只是控制周期变长一点点。6.3 用串口助手手工发 PING 帧验证协议层最后给一个最直接的验证技巧先绕过自己的库用带方向控制的 USB 转 TTL 模块直连一颗舵机在串口助手里手工发送十六进制帧。ID 为 1 时PING 帧应该发FF FF 01 02 01 FB。其中01是 ID02是 LEN0 个参数 201是 PING 指令FB是校验和取 0x010x020x01 0x04取反得到 0xFB。如果舵机回了一帧说明舵机本身、供电、数据线全部正常问题只可能出在你自己的库代码里。如果连这帧都不回那就回到表格第一行查供电和共地别在代码层面浪费时间。本文还有配套的精品资源点击获取
RELATED

相关推荐

34mm SiC功率模块:新能源系统高效化与小型化的物理基准

34mm SiC功率模块:新能源系统高效化与小型化的物理基准

1. 项目概述:为什么34mm SiC模块正在成为新能源系统里的“心脏级”器件最近在几个光伏逆变器客户现场做技术复盘时,反复被问到一个问题:“你们说的‘基本半导体全系34mm SiC模块’,到底和我们原来用的62mm IGBT模块、甚至市面上常…

📅 2026/9/12 3:02:09
分岔系统特征提取与MPC自适应控制实战

分岔系统特征提取与MPC自适应控制实战

简介:本资源是一套面向数学、电子信息工程及计算机专业本科生的分岔动态系统预测控制参数特征分析MATLAB仿真代码集,聚焦非线性系统建模与关键参数敏感性识别问题,适用于课程设计、期末大作业及毕业设计等实践环节。压缩包共394个文件&#x…

📅 2026/9/12 3:02:09
机动目标跟踪中运动模型失配的本质与IMM解决方案

机动目标跟踪中运动模型失配的本质与IMM解决方案

简介:本资源是一份面向雷达/导航系统开发与目标跟踪算法学习者的MATLAB仿真程序,聚焦于机动目标(含匀速、转弯、加速等多阶段运动)的建模与滤波跟踪问题。适用于自动控制、信号处理、无人系统感知等方向的本科生高年级课程设计、研…

📅 2026/9/12 2:57:08
MORE NEWS

更多资讯

📰

Material for MkDocs 自定义社交卡片:基于 default/variant 布局打造“新版本发布“公告卡片

Material for MkDocs 自定义社交卡片:基于 default/variant 布局打造"新版本发布"公告卡片 【免费下载链接】mkdocs-material Documentation that simply works 项目地址: https://gitcode.com/GitHub_Trending/mk/mkdocs-material Material for M…

📰

路面附着系数估计实战:基于EKF与UKF的Simulink实现与对比

做车辆动力学控制方向的朋友,几乎迟早都会撞上同一个问题:当前路面到底能提供多大的附着系数。ABS要靠它判断车轮会不会抱死,ESP要靠它决定要不要介入,AEB在低附着路面上能不能在目标距离内刹停,也跟它直接挂钩。这个参…

📰

阿兹海默症MRI辅助诊断系统:从CNN到Grad-CAM的深度学习实践

简介:面向高校计算机相关专业学生的深度学习应用型毕业设计项目。基于Python与Spring Boot技术栈,实现阿兹海默症早期诊断辅助系统,覆盖医学影像数据处理、模型训练与诊断结果可视化等环节,适合用于毕业设计、课程设计或作为AI医疗…

📰

VisualSVN Server+TortoiseSVN从零搭建SVN版本控制系统完整指南

从零搭建一套SVN版本控制系统:VisualSVN Server TortoiseSVN 完整实操记录我自己动手给团队搭过好几套SVN版本控制系统,也帮客户现场部署过,每次用到的组合基本都是服务端VisualSVN Server加客户端TortoiseSVN。这套方案最大的优势就是省心&…

📰

LPC1114例程详解:从GPIO到外设驱动开发实战

简介:LPC1114是NXP基于ARM Cortex-M0内核的32位微控制器,最高主频60MHz,集成ADC、UART、I2C、SPI、GPIO等丰富外设,以低功耗、高性价比被广泛用于小型控制系统、物联网终端和消费电子产品。资源以《LPC1114芯片基础教程与应用实践…

📰

AI视频生成工具横评:Higgsfield替代方案实测对比

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬