STM32驱动多圈绝对值编码器:SSI协议解析与位置解算实战
简介本资源为多圈绝对值编码器STM32测试程序面向嵌入式开发工程师、工业自动化与机器人控制方向的学习者解决基于SSI协议读取多圈绝对值编码器位置信息并完成断电位置保持的实践问题。压缩包共163个文件约3.2MB以46个h头文件与46个c源文件为核心涵盖STM32外设驱动与SSI通信逻辑另含16个d、16个o、15个crf等编译中间文件以及uvprojx工程文件、hex可执行文件、map映射文件和bat编译脚本构成可直接导入Keil运行的完整工程。目前已有3106人学习下载。工程包含初始化配置、SSI收发函数、编码器协议解析、串口日志输出与错误处理等模块读者可据此理解时钟线与数据线的同步时序、数据位宽与极性相位设置并在此基础上修改适配不同型号编码器快速搭建位置检测与调试环境。1. 多圈绝对值编码器STM32测试程序从SSI协议到位置解算的完整落地拿到一个多圈绝对值编码器第一件事不是写代码而是确认它到底怎么把圈数和单圈角度传出来。这个项目就是干这个的用STM32驱动多圈绝对值编码器通过SSI协议读取位置数据解析出当前圈数和圈内角度最终拼出完整的多圈绝对位置。适合正在做伺服电机、机器人关节、升降平台或者任何需要断电记忆位置的嵌入式开发者。你手里如果有一块STM32板子和一个SSI接口的绝对值编码器这份代码能让你在半小时内看到串口打印出真实位置值而不是对着示波器猜时序。2. 硬件接口与SSI协议编码器到底在说什么2.1 多圈绝对值编码器的数据帧长什么样多圈绝对值编码器输出的不是脉冲而是一帧固定长度的二进制数据。常见的有25位、26位、29位等格式具体取决于单圈分辨率和圈数位数。比如一个25位编码器可能是13位单圈12位多圈也可能是12位单圈13位多圈这个顺序必须查手册确认搞反了位置会跳变。SSI协议本质上是同步串行通信编码器作为从机STM32作为主机提供时钟。一帧数据通常包含起始位、数据位、错误位和停止位。以25位数据为例帧结构可能是1个起始位固定为0或1 25个数据位 1个错误位 1个停止位总共28个时钟周期。有些编码器还会在数据位中间插入奇偶校验位这个必须在手册里看清楚。数据位的排列顺序也有讲究。大多数编码器是MSB先出但个别型号是LSB先出。更坑的是多圈数据和单圈数据的拼接顺序不同厂家不一样。我见过一个编码器前12位是圈数后13位是单圈角度但另一个型号正好反过来。所以第一步永远是拿手册确认帧格式别凭经验猜。2.2 STM32端SSI接口的硬件连接方式SSI是单向通信STM32只需要输出时钟和接收数据两根线。时钟线接编码器的CLK数据线接编码器的DATA。如果是差分输出RS422还需要接对应的负端STM32端用差分接收器转成单端再进GPIO或SPI。常见做法是用STM32的SPI外设来模拟SSI时序。SPI的SCK输出时钟MISO接收数据NSS可以不用或者用来控制帧间隔。但SPI有个问题它的时钟极性和相位要跟SSI匹配。SSI通常在时钟上升沿输出数据下降沿采样对应SPI的CPOL0、CPHA1或者CPOL1、CPHA0具体要看编码器手册。如果不想用SPI也可以用GPIO加定时器手动翻转时钟。这种方式更灵活能精确控制时钟频率和帧间隔但代码量大一些。我一般先用SPI跑通遇到时序问题再换GPIO方案。// SSI接口初始化使用SPI1主机模式时钟空闲高第二个边沿采样 void SSI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_HandleTypeDef hspi1 {0}; // 使能GPIO和SPI时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5-SCK, PA6-MISO GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // SPI参数主机8位数据CPOL1CPHA1预分频256 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; hspi1.Init.CLKPhase SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_256; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这段代码把SPI1配置成SSI主机模式。关键参数是CLKPolarity和CLKPhase它们决定了时钟空闲电平和采样边沿。BaudRatePrescaler设成256是为了把时钟降到编码器能接受的频率一般SSI编码器最高支持1MHz左右STM32主频72MHz时256分频约281kHz比较安全。FirstBit设成MSB因为绝大多数编码器都是高位先出。2.3 时钟频率与帧间隔的取舍SSI编码器的时钟频率不能太高否则内部处理跟不上。手册里一般会写最大时钟频率比如500kHz或1MHz。但实际用的时候我建议先降到100kHz到200kHz跑通再逐步提高。因为时钟太快会导致数据位还没稳定就被采样读出来全是0或者全是1。帧间隔也很重要。连续读的时候两帧之间要留足够时间让编码器准备下一帧数据。有些编码器要求帧间隔至少20微秒有些要求100微秒以上。如果读得太快第二帧数据可能是上一帧的重复或者直接出错。我一般会在两帧之间加一个延时用HAL_Delay或者定时器都行具体延时长度查手册。还有一个坑SSI是单向通信STM32只发时钟不收时钟所以不需要考虑时钟同步问题。但编码器的数据输出有建立时间和保持时间要求如果STM32的采样点太靠前或太靠后都会读错。用SPI的话采样点由CPHA决定用GPIO的话自己在时钟边沿后加几个NOP再采样。3. 位置解算与代码实现从原始位到物理角度3.1 原始数据读取与位拼接SSI一帧读回来的是几个字节的原始数据需要先拼成一个完整的整数再按位段拆出圈数和单圈角度。假设编码器是25位数据STM32用SPI读4个字节32位其中高25位有效低7位是填充或错误位。// 读取SSI原始数据并解析为多圈位置 uint32_t SSI_ReadRaw(void) { uint8_t rx_buf[4] {0}; uint32_t raw 0; // 拉低片选启动一帧传输 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_Receive(hspi1, rx_buf, 4, 100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拼接4字节为32位整数 raw ((uint32_t)rx_buf[0] 24) | ((uint32_t)rx_buf[1] 16) | ((uint32_t)rx_buf[2] 8) | ((uint32_t)rx_buf[3]); return raw; } // 从原始数据中提取圈数和单圈角度 void SSI_ParsePosition(uint32_t raw, uint16_t *turns, uint16_t *angle) { // 假设帧格式高12位圈数低13位单圈角度共25位 // 原始数据右移7位对齐到低25位 uint32_t data raw 7; *turns (data 13) 0x0FFF; // 取高12位 *angle data 0x1FFF; // 取低13位 }SSI_ReadRaw函数通过SPI接收4个字节拼成32位整数。注意这里用的是HAL_SPI_Receive而不是HAL_SPI_TransmitReceive因为SSI只需要接收数据时钟由SPI外设自动产生。片选信号用GPIO手动控制拉低开始传输拉高结束。SSI_ParsePosition函数做位段提取。先右移7位把有效数据对齐到最低位然后分别取高12位和低13位。这里的12和13是假设值实际要根据编码器手册修改。如果编码器是29位可能是13位圈数16位单圈角度移位和掩码都要相应调整。3.2 多圈位置合成与溢出处理圈数和单圈角度拼在一起才是完整的多圈绝对位置。但这里有个细节圈数是循环计数的比如12位圈数范围是0到4095超过4095会回绕到0。如果设备需要连续运行很多圈就要在软件里做溢出处理维护一个虚拟的高位圈数。// 多圈位置合成处理圈数溢出 int64_t SSI_GetMultiTurnPosition(uint16_t turns, uint16_t angle, uint16_t max_turns) { static int64_t virtual_turns 0; static uint16_t last_turns 0; int32_t delta 0; // 计算圈数变化量处理回绕 delta (int32_t)turns - (int32_t)last_turns; if (delta (int32_t)(max_turns / 2)) { delta - max_turns; // 正向回绕 } else if (delta -(int32_t)(max_turns / 2)) { delta max_turns; // 反向回绕 } virtual_turns delta; last_turns turns; // 合成完整位置虚拟圈数 * 单圈分辨率 单圈角度 return virtual_turns * (int64_t)(angle 1) angle; }这个函数用static变量保存上一次的圈数和虚拟圈数。每次读取时计算圈数变化量delta如果delta超过max_turns的一半就认为是回绕加上或减去max_turns修正。然后把delta累加到virtual_turns上得到实际圈数。最后用virtual_turns乘以单圈分辨率加上单圈角度得到完整位置。注意单圈分辨率是angle的最大值加1比如13位角度是0到8191分辨率就是8192。这里用angle1是简化写法实际应该用常量表示。3.3 串口输出与位置验证代码跑通后最直观的验证方式是把位置值通过串口打印出来。用printf重定向到USART然后在主循环里定时读取并打印。// 主循环每100ms读取一次位置并打印 int main(void) { HAL_Init(); SystemClock_Config(); SSI_Init(); UART_Init(115200); uint16_t turns 0, angle 0; int64_t position 0; while (1) { uint32_t raw SSI_ReadRaw(); SSI_ParsePosition(raw, turns, angle); position SSI_GetMultiTurnPosition(turns, angle, 4096); printf(Turns: %4d, Angle: %5d, Position: %lld\r\n, turns, angle, position); HAL_Delay(100); } }主循环每100毫秒读一次编码器解析后打印圈数、单圈角度和合成位置。串口波特率115200用USB转TTL接到电脑上就能看数据。验证时手动转动编码器轴观察圈数和角度是否连续变化。如果角度在0到8191之间循环圈数在0到4095之间递增说明解析正确。如果数据跳变或者不变先检查SPI时钟频率是否太高再检查帧格式是否匹配。可以用逻辑分析仪抓SPI的SCK和MISO波形对照编码器手册看数据位是否正确。4. 避坑与常见问题SSI调试中容易翻车的地方4.1 读出来全是0或全是1现象串口打印的原始数据始终是0x00000000或0xFFFFFFFF转动编码器轴也没变化。原因SPI的时钟极性或相位跟编码器不匹配导致采样点落在数据跳变沿上。或者MISO线没接对STM32根本没收到数据。解决先用示波器或逻辑分析仪看SCK和MISO波形。如果SCK有输出但MISO一直是高或低检查接线和编码器供电。如果MISO有波形但数据不对调整SPI的CPOL和CPHA组合四种组合都试一遍。常见做法是CPOL1、CPHA1或者CPOL0、CPHA1。4.2 数据偶尔跳变或出现固定偏移现象大部分数据正常但偶尔跳出一个异常值或者所有数据都偏了一个固定值。原因帧间隔太短编码器还没准备好下一帧数据就被读了。或者起始位和停止位没对齐导致数据整体移位。解决增加帧间隔延时从100微秒开始试逐步减小到稳定为止。检查原始数据的最高位和最低位是否符合手册的起始位和停止位定义。如果整体偏移可能是SPI读的字节数不对比如应该读3个字节却读了4个多出来的位把数据挤偏了。4.3 圈数回绕时位置突变现象编码器转过零点时合成位置突然跳变一个很大的值。原因圈数溢出处理逻辑有误delta计算时没有正确判断回绕方向。或者max_turns参数设错了跟实际圈数位数不匹配。解决确认圈数位数12位圈数max_turns是409613位是8192。检查delta的判断条件正向回绕和反向回绕的阈值要对称。可以在回绕点附近打印delta和virtual_turns观察变化过程。4.4 单圈角度分辨率不对现象角度值范围不是0到8191而是0到4095或者0到16383。原因单圈角度位数搞错了或者移位和掩码参数跟实际帧格式不匹配。解决查手册确认单圈分辨率和圈数位数重新计算移位量和掩码。如果手册写的是“单圈13位”那角度范围就是0到8191掩码是0x1FFF。如果写的是“单圈12位”掩码就是0x0FFF。4.5 多圈数据不更新现象单圈角度正常变化但圈数始终不变。原因多圈数据在帧中的位置搞错了或者编码器的多圈功能没使能。有些编码器需要外部电池或超级电容维持多圈计数断电后圈数会丢失。解决确认帧格式中多圈数据的位置用逻辑分析仪抓原始数据手动解析看圈数位是否变化。检查编码器是否需要外部电源维持多圈计数如果需要确保电池或电容已接好。5. 二次开发与改造把测试程序变成产品代码5.1 从轮询到中断或DMA测试程序用轮询方式读SSI简单但占CPU。产品代码里我一般会改成定时器触发DMA传输或者用SPI中断。定时器每1毫秒触发一次SPI接收DMA自动把数据搬到缓冲区主循环只负责解析。这样CPU占用率从100%降到不到5%还能腾出时间跑其他任务。改造步骤把SSI_ReadRaw里的HAL_SPI_Receive换成HAL_SPI_Receive_DMA在DMA完成回调里置一个标志位。主循环检查标志位有数据就解析。定时器配置成1kHz在更新中断里启动SPI传输。注意DMA缓冲区和解析缓冲区要分开避免数据竞争。5.2 位置数据的滤波与校准原始位置值会有抖动尤其是编码器安装在振动环境里。我通常会在解析后加一个滑动平均滤波窗口大小5到10。如果对实时性要求高用一阶低通滤波截止频率设成10Hz左右。校准方面编码器安装后需要找零点。常见做法是手动转到机械零点记录当时的单圈角度值存到Flash里。之后每次读位置都减去这个偏移量得到相对于机械零点的绝对位置。多圈零点也要校准把当前虚拟圈数设为0。// 一阶低通滤波 float LowPassFilter(float new_val, float old_val, float alpha) { return alpha * new_val (1.0f - alpha) * old_val; } // 零点校准 void CalibrateZero(int64_t current_pos) { zero_offset current_pos; // 存到Flash Flash_Write(ADDR_ZERO_OFFSET, zero_offset, sizeof(zero_offset)); }LowPassFilter的alpha决定滤波强度0.1到0.3之间比较合适。CalibrateZero把当前位置存为零点偏移之后读位置时减去这个偏移。Flash写入要注意擦除和写入时序别在中断里调用。5.3 多编码器同步读取有些设备需要同时读多个编码器比如双关节机器人。这时候SPI外设不够用可以用GPIO模拟SSI时序每个编码器分配一组GPIO。或者用SPI加多路复用器切换片选。GPIO模拟的代码量不大关键是时序要准。用定时器产生时钟在定时器中断里翻转SCK并采样MISO。时钟频率设成100kHz中断频率200kHzSTM32F103跑72MHz完全能应付。每个编码器用一个状态机轮流读取。我一般会先写一个编码器驱动封装成结构体包含GPIO端口、引脚、帧格式参数。然后实例化多个对象用统一的接口读取。这样增加编码器只需要改配置不用改代码逻辑。5.4 验证方法与调试技巧改造完成后验证分三步第一步用串口打印原始数据和解析结果确认跟测试程序一致。第二步用逻辑分析仪抓SPI波形确认时序没变。第三步跑长时间测试连续读24小时看有没有丢帧或跳变。调试时我习惯在关键位置加计数器比如SPI传输次数、解析成功次数、错误次数。通过串口定期打印这些计数器能快速定位问题。如果错误次数增长很快说明时序或帧格式有问题。如果错误次数为0但数据不对说明解析逻辑有bug。从那以后我每次改SSI驱动都强制走一遍“原始数据打印→波形抓取→长时间跑”的流程再急也不跳过。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

基于GitLab与Gerrit的开源代码审查流程搭建指南

基于GitLab与Gerrit的开源代码审查流程搭建指南

1. 整体设计思路:为什么我会选择自建一套代码审查流程代码审查这件事,团队里永远有人觉得它是形式主义,也有人觉得它是质量保障的最后一道防线。我个人的看法是:代码审查本身绝对有用,但很多团队之所以觉得它没用&…

2026/10/19 14:02:12 阅读更多 →
Simulink实时修改参数:ExportedGlobal与External Mode在线调参实战

Simulink实时修改参数:ExportedGlobal与External Mode在线调参实战

简介:这份资源面向需要在Simulink仿真过程中动态调整模块参数的开发者与学习者,核心是用S函数实现对模型内部参数的实时修改,并以正弦波形作为验证对象,在线改变其幅值与频率,直观观察参数变化对输出的影响。压缩包共2…

2026/10/19 14:02:12 阅读更多 →
从通信集成到数据建模:自研CRM的实战复盘

从通信集成到数据建模:自研CRM的实战复盘

如果你所在的公司曾经买过两套CRM都没能坚持用下来,那你应该能理解我们决定自研DeskcommCRM时,身边同事的表情。大多数CRM产品的出发点其实是管理者的报表需求,而不是一线销售的工作效率。销售不愿意录数据,主管天天催&#xff0c…

2026/10/19 14:01:08 阅读更多 →

最新新闻

多智能体协作模式深度解析:主从、对等与混合架构选型指南

多智能体协作模式深度解析:主从、对等与混合架构选型指南

多智能体协作这个题目,最近在项目里几乎是绕不开的。不管是机器人编队、物流调度,还是现在热门的多个AI智能体共同跑一个业务流程,本质上都要回答同一个问题:让一堆各自有感知、有决策的个体一起干活,到底怎么组织它们…

2026/10/19 14:39:53 阅读更多 →
Flume+HBase+Spark2.2构建秒级新闻热点实时分析链路

Flume+HBase+Spark2.2构建秒级新闻热点实时分析链路

简介:这是一份面向高校计算机专业本科生及大数据初学者的毕业设计级实战源码,聚焦基于Spark 2.2构建新闻网实时分析系统的完整实现,解决新闻用户行为日志的采集、存储、流式处理与热点挖掘等典型大数据场景问题。资源共43个文件,含…

2026/10/19 14:39:53 阅读更多 →
pymoo实战:车辆与无人机协同配送集中式模型复现

pymoo实战:车辆与无人机协同配送集中式模型复现

最近总有人拿着低空经济方向的新论文来问:车辆与无人机协同配送的优化模型,到底怎么用代码实现?尤其论文里常见的"集中式协同配送模式",听起来就是车辆带着无人机送货,真到复现的时候却要同时处理车辆路径、…

2026/10/19 14:39:53 阅读更多 →
OpenCode 美化增强指南:用 AI 一键装好 oh-my-opencode

OpenCode 美化增强指南:用 AI 一键装好 oh-my-opencode

最近很多朋友开始折腾 OpenCode,原因很直白:它能直接用 AI 帮你写代码、改配置、跑命令,确实比传统编辑器省事。但省事归省事,刚装完的 OpenCode 默认界面和默认快捷键都比较朴素,用起来总有那么点“不够顺手”。oh-my…

2026/10/19 14:39:53 阅读更多 →
VS Code数据科学提效插件DST:AST解析+内核探针+安全沙箱

VS Code数据科学提效插件DST:AST解析+内核探针+安全沙箱

1. 这不是又一个“语法高亮插件”:它重新定义了VS Code里的数据分析工作流你有没有过这样的时刻:在VS Code里打开一个Jupyter Notebook,写完一段pandas数据清洗代码,想立刻看df.head()结果,却得切到右侧的交互窗口——…

2026/10/19 14:39:53 阅读更多 →
离线环境二进制部署Kubernetes集群完整指南

离线环境二进制部署Kubernetes集群完整指南

做私有化交付这几年,“离线部署K8S集群,而且还得用二进制方式”基本上是隔离网络项目里躲不掉的一关。所谓离线,就是目标机房跟互联网物理隔离,或者网络策略收得很紧,你没法在目标环境直接拉依赖;所谓二进制…

2026/10/19 14:38:52 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

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

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/18 12:16:16 阅读更多 →