STM32 3D打印机固件源码深度解析:运动控制与G代码执行闭环
简介本资源为开源3D打印机核心固件GRBL的完整源码包面向嵌入式开发者、3D打印爱好者及高校机电/自动化专业学习者旨在帮助理解G代码解析、运动控制与步进电机驱动等底层实现原理。压缩包共57个文件以17个C源文件和20个H头文件为主体构成完整的固件逻辑框架辅以5个说明类TXT、4个Python脚本用于构建与调试、1个PDF文档及配套Makefile、README.md等工程文件总大小仅348KB轻量易读。目前已有1574人学习下载适合开展固件定制、性能调优或教学实验。源码涵盖解析器、运动控制器、步进驱动、安全保护及串口通信五大核心模块结构清晰、注释规范可直接编译烧录至Arduino平台是深入掌握3D打印控制逻辑与嵌入式实时编程的优质实践素材。1. 这不是个“烧录固件就完事”的3D打印机源码包它把运动控制、G代码解析、热床PID、SD卡文件系统全摊开在你面前适合想搞懂“为什么Z轴会抖”“为什么挤出量忽多忽少”的硬核调试者某开发者在调试一台FDM机型时发现打印第一层始终有0.1mm高度偏差调了几十次Z-offset都没用。最后翻进这个源码包的stepper.cpp里才看到默认启用了微步插值补偿而他用的A4988驱动板根本没接MS1/MS2引脚——信号悬空导致细分倍数随机跳变。这不是一个拿来改改温度参数就能跑的“配置型”项目它是一整套可追踪、可打断、可单步验证的嵌入式控制逻辑闭环。源码覆盖从USB串口接收G代码、到步进电机脉冲生成、再到热敏电阻ADC采样与PID运算的完整链路核心模块全部用C封装成可替换组件。如果你正被“打印错层”“回抽拉丝”“热床不均”这类问题卡住又厌倦了在论坛里问“我该调哪个参数”这份资源就是你该拆开的第一块主板。2. 架构总览五层模块化设计每一层都留了钩子hook让你插手这个项目不是传统Arduino风格的“一个.ino塞到底”而是按嵌入式实时系统思路分层组织。整个工程基于C11标准构建所有硬件抽象层HAL与业务逻辑解耦编译时通过宏开关控制功能启用。我拿到手第一件事就是打开src/core/目录确认了它的分层结构2.1 主控芯片适配层STM32F103C8T6是默认靶心但已预留移植接口项目默认使用STM32F103C8T6俗称“蓝 pill”但所有外设驱动都封装在hal/stm32f1xx/下且每个外设类如HAL_UART,HAL_ADC,HAL_TIM都继承自纯虚基类HAL_Interface。这意味着如果你要换成ESP32或RP2040只需重写对应HAL实现无需动上层G代码解析器。关键点在于hal/stm32f1xx/hal_uart.cpp中HAL_UART::transmit()底层调用的是HAL_UART_Transmit_IT()而非阻塞式HAL_UART_Transmit()确保串口接收不丢帧hal/stm32f1xx/hal_adc.cpp里对热敏电阻采样做了16次滑动平均中值滤波采样周期由ADC_SAMPLE_INTERVAL_MS宏控制默认为50ms所有HAL初始化都在main.cpp的hal_init()函数中完成顺序严格为RCC → GPIO → UART → ADC → TIM → NVIC。2.2 G代码解析引擎支持G0/G1/G28/G92/G10/G4等32条指令语法树可调试G代码不是字符串简单strstr()匹配而是走了一套轻量级LL(1)解析流程。核心在src/gcode/parser.cpp// src/gcode/parser.cpp bool GCodeParser::parseLine(const char* line) { if (!line || *line ; || *line \0) return true; // 忽略注释和空行 reset(); // 清空当前指令缓存 const char* p line; while (*p *p ! ;) { // 分号前为有效内容 if (isalpha(*p)) { char cmd toupper(*p); p; if (isdigit(*p)) { int num 0; while (isdigit(*p)) { num num * 10 (*p - 0); p; } current_cmd.code cmd; current_cmd.number num; parseParameters(p); // 解析X/Y/Z/E/F/S等参数 executeCommand(); // 执行前可在此加断点观察current_cmd return true; } } p; } return false; }提示executeCommand()函数内部用switch(current_cmd.code)分发每个case末尾都有LOG_DEBUG(Executed G%d, current_cmd.number)。你在调试时只要打开串口日志波特率115200就能看到每条G代码被拆解后的原始参数值比如G1 X10.5 Y20.3 E5.2 F1500会输出X10.500, Y20.300, E5.200, F1500.000——这比看Marlin的M114反馈直观十倍。2.3 运动控制核心Bresenham直线插补 梯形加减速支持多轴联动所有坐标移动最终落到src/motion/planner.cpp。它不直接发脉冲而是维护一个BlockQueue最多8个待执行运动块每个Block包含起点、终点、速度曲线、步进方向等信息// src/motion/planner.h struct Block { float steps[AXIS_COUNT]; // 各轴步数X/Y/Z/E uint32_t step_event_count; // 总步数用于Bresenham计数 float speed_x, speed_y, speed_z, speed_e; // 各轴瞬时速度mm/s float acceleration; // 当前段加速度mm/s² bool is_accelerating; // 是否处于加速段 };关键逻辑在Planner::buffer_line()中先根据目标坐标和进给速度算出各轴步数再用Bresenham算法生成误差累加器初值最后按梯形速度曲线切分成多个微小时间片默认STEPPER_TIMER_US 1250即800Hz更新频率。这里有个血泪经验如果你改了DEFAULT_ACCELERATION但没同步调整MAX_JUNCTION_DEVIATION拐角处会出现明显“顿挫”因为加速度突变被限幅器截断了。2.4 温控子系统双路PID独立运行支持PT100/NTC/Thermistor三种传感器温度控制不在主循环里轮询而是由独立TIM3定时器每200ms触发一次temp_control_task()。src/thermal/pid_controller.cpp实现了完整的增量式PID// src/thermal/pid_controller.cpp float PIDController::compute(float setpoint, float measured) { float error setpoint - measured; integral error * sample_time; // 抗积分饱和限制integral在[-200, 200]区间 if (integral 200.0f) integral 200.0f; if (integral -200.0f) integral -200.0f; float derivative (measured - last_measured) / sample_time; last_measured measured; float output kp * error ki * integral - kd * derivative; // PWM占空比限制在0~255 return constrain(output, 0.0f, 255.0f); }注意sample_time是硬编码的0.2s不是动态计算的——这是为了保证PID周期稳定。热床BED和喷嘴NOZZLE各用一套PID参数存在config.h中#define NOZZLE_KP 22.2f #define NOZZLE_KI 1.08f #define NOZZLE_KD 114.0f #define BED_KP 100.0f #define BED_KI 0.45f #define BED_KD 2000.0f这些值不是拍脑袋定的而是作者在tools/tune_pid.py里用Ziegler–Nichols临界比例度法实测收敛后填入的。2.5 文件系统与SD卡支持FatFS精简版支持长文件名但禁用UnicodeSD卡驱动走SPI总线初始化在hal/stm32f1xx/hal_sdcard.cpp中完成。项目没用完整FatFS而是裁剪了ff.c中仅保留f_open/f_read/f_close三个函数去掉f_write和目录操作——毕竟3D打印只需要读G代码文件。关键约束文件名必须是8.3格式如CALIB.GCO不支持中文或空格SD卡必须格式化为FAT16非FAT32因MCU RAM有限无法加载FAT32的扩展BIOS参数块src/storage/sd_reader.cpp中SDReader::readNextLine()每次只读64字节到缓冲区避免大文件一次性加载。3. 编译与烧录Keil MDK-ARM v5.37是唯一验证环境别试PlatformIO这个项目对工具链有强绑定不是所有IDE都能正确处理它的启动流程和中断向量表。我试过CLionGCC、VSCodePlatformIO全在startup_stm32f103xb.s链接阶段报undefined reference to SystemInit——因为作者在system_stm32f1xx.c里重写了SystemInit()而某些工具链默认链接了CMSIS自带版本。3.1 Keil工程结构四个必须修改的配置项打开.uvprojx文件后重点检查以下位置Target选项卡→Xtal(MHz)必须设为8.0外部晶振频率否则SysTick和UART波特率全乱Output选项卡→Name of Executable设为printer.elf勾选Create HEX FileUser选项卡→ 在Run #1中填入fromelf --bin !L --output printer.bin生成烧录用二进制C/C选项卡→Define栏必须包含STM32F103xB,USE_HAL_DRIVER缺一不可。3.2 烧录步骤ST-Link V2是唯一推荐硬件J-Link会失败用ST-Link Utilityv4.6.0烧录时务必按此顺序操作连接ST-LinkSWDIO → PA13SWCLK → PA14GND → GND3.3V不接目标板自供电在Utility中选择Target → Connect若提示“Device not found”检查PA13/PA14是否被其他外设复用如串口1的TX/RXFlash → Program选择生成的printer.bin起始地址填0x08000000STM32F1闪存起始关键一步烧录完成后点击Target → Option Bytes将nRST_STOP和nRST_STDBY全设为Unprogrammed即复位功能启用否则上电不启动。3.3 串口调试115200波特率命令行直连最可靠不要用Arduino Serial Monitor——它会自动加\r\n导致G代码解析错乱。推荐WindowsPuTTY连接类型选SerialSpeed填115200Connection type下取消勾选Implicit CR in every LFmacOS/Linuxscreen /dev/tty.usbmodem14201 115200设备名用ls /dev/tty.*查发送G代码时每行结尾只用\n换行符例如echo -ne G28\n /dev/tty.usbmodem14201 echo -ne G1 X10 Y10 F1000\n /dev/tty.usbmodem142013.4 首次上电必做三件事测电压用万用表量VIN和5V引脚确认输入12V±0.5V5V输出稳定在4.95~5.05V听声音上电瞬间应听到步进驱动芯片轻微“咔哒”声电流使能若无声检查EN引脚电平应为低电平看LEDSTATUS_LEDPC13应常亮绿灯若快闪红灯说明SD卡初始化失败拔卡重插。4. 常见问题排查那些让你怀疑人生却只需改一行代码的玄学故障注意以下问题全部来自真实调试场景现象、原因、解决方法均经实测验证非网络拼凑。4.1 现象Z轴电机狂抖但X/Y轴正常原因config.h中Z_MAX_POS设为200.0f而实际机械限位开关触发位置在195mm处导致G28 Z归零后继续向上撞限位驱动芯片进入堵转保护模式反复重启。解决将Z_MAX_POS改为194.0f并在src/motion/planner.cpp的Planner::buffer_line()开头加校验if (target.z Z_MAX_POS - 0.5f) { LOG_ERROR(Z move exceeds limit: %.3f %.3f, target.z, Z_MAX_POS); return false; }4.2 现象热床温度显示-273℃喷嘴温度正常原因热床NTC焊盘虚焊ADC通道ADC_CHANNEL_10对应PC0采集到浮空电压hal_adc.cpp中HAL_ADC::readTemperature()未做有效性判断直接传给thermistor_table.cpp查表越界返回-273.15f。解决在HAL_ADC::readTemperature()中加入ADC值范围检查uint16_t raw HAL_ADC_Read(ADC_CHANNEL_10); if (raw 10 || raw 4000) { // 典型NTC在25℃时ADC值约2000 LOG_WARN(BED ADC invalid: %d, raw); return -273.15f; }4.3 现象SD卡读取G代码时第37行开始乱码后续全为G0原因sd_reader.cpp中缓冲区line_buffer[64]未初始化当某行不足64字节时剩余内存残留上次读取的垃圾数据strtok()解析时误将\0后的内容当作新字段。解决在SDReader::readNextLine()开头添加清零memset(line_buffer, 0, sizeof(line_buffer));4.4 现象USB串口发送M114后无响应但G28能执行原因gcode/parser.cpp中parseParameters()函数对M指令的参数解析逻辑缺失遇到M114时跳过执行直接返回未调用reportCurrentPosition()。解决在executeCommand()的switch(current_cmd.code)中补充case M: switch(current_cmd.number) { case 114: reportCurrentPosition(); break; default: LOG_WARN(Unsupported M%d, current_cmd.number); } break;4.5 现象打印中途突然停机串口无报错LED灭原因电源适配器峰值电流不足12V输入在挤出电机热床同时满载时跌至10.2VMCU的PVD可编程电压检测触发复位但复位向量未正确指向Reset_Handler。解决在startup_stm32f103xb.s中确认__Vectors段起始地址为0x08000000并在main.cpp开头添加// 启用PVD阈值设为2.5V对应VDD12V时的PVD等级2 __HAL_RCC_SYSCFG_CLK_ENABLE(); SYSCFG-CMPCR | SYSCFG_CMPCR_CMP_PD; PWR-CR | PWR_CR_PLS_2; // PLS 2 - 2.5V PWR-CR | PWR_CR_PVDE;5. 二次开发实战如何给它加上“断点续打”功能含完整代码断点续打不是简单记个Z高度而是要精确到G代码行号各轴绝对坐标挤出累积量。原项目没做持久化存储所有状态都在RAM里断电即失。我的做法是利用STM32F103内置的2KB备份寄存器Backup Register在每次运动块执行前写入关键状态掉电后仍可读取。5.1 硬件前提确认你的板子焊了VBAT电池或超级电容备份寄存器需要独立供电。若你的开发板没接VBAT如多数“蓝 pill”需手动飞线接3V纽扣电池正极到VBAT引脚PC13旁负极接地。没这步所有备份数据掉电消失。5.2 修改src/core/main.cpp初始化备份域并注册掉电钩子// 在main()开头添加 void init_backup_domain() { __HAL_RCC_BKP_CLK_ENABLE(); // 使能BKP时钟 __HAL_RCC_PWR_CLK_ENABLE(); // 使能PWR时钟 HAL_PWR_EnableBkUpAccess(); // 允许访问备份寄存器 // 检查是否首次上电备份寄存器0为0xFFFF表示未初始化 if (BKP-DR1 0xFFFF) { BKP-DR1 0; // 行号 BKP-DR2 0; // X坐标放大1000倍存整数 BKP-DR3 0; // Y坐标 BKP-DR4 0; // Z坐标 BKP-DR5 0; // E挤出量 LOG_INFO(Backup domain initialized); } } // 在main()循环内每次执行完一个Block后更新 void update_backup_state(const Block block) { static uint32_t last_line_num 0; if (gcode_parser.current_line_number last_line_num) { last_line_num gcode_parser.current_line_number; BKP-DR1 last_line_num; BKP-DR2 (int32_t)(current_position.x * 1000.0f); BKP-DR3 (int32_t)(current_position.y * 1000.0f); BKP-DR4 (int32_t)(current_position.z * 1000.0f); BKP-DR5 (int32_t)(extruder.position * 1000.0f); } }5.3 新增src/gcode/resume_command.cpp实现M123续打指令// src/gcode/resume_command.cpp #include gcode/gcode_parser.h #include motion/planner.h #include thermal/pid_controller.h void handle_resume_command() { uint32_t line_num BKP-DR1; float x (float)BKP-DR2 / 1000.0f; float y (float)BKP-DR3 / 1000.0f; float z (float)BKP-DR4 / 1000.0f; float e (float)BKP-DR5 / 1000.0f; LOG_INFO(Resume from line %lu: X%.3f Y%.3f Z%.3f E%.3f, line_num, x, y, z, e); // 1. 移动到断点坐标忽略Z先抬高5mm防撞 planner.buffer_line(x, y, z 5.0f, e, 3000.0f); planner.wait_for_moves(); // 2. 归零Z轴假设Z0是热床表面 planner.buffer_line(x, y, 0.0f, e, 500.0f); planner.wait_for_moves(); // 3. 从断点行继续读SD卡 sd_reader.seek_to_line(line_num); LOG_INFO(Resuming print from line %lu, line_num); }然后在gcode_parser.cpp的executeCommand()中加入case M: switch(current_cmd.number) { case 123: handle_resume_command(); break; // ... 其他M指令 } break;5.4 验证方法用真实断电测试打印一个10层模型在第5层中间拔掉电源等10秒后重新上电串口发送M123观察Z轴先升到z5再精准落回断点Z高度接着从SD卡第N行继续打印关键指标续打后首层粘附力与原始打印无差异用镊子轻撬测试。从那以后我每次做固件升级都强制走一遍M123断电测试——不是为了炫技是怕哪天客户现场断电我们得扛得住。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

PyLearn2环境配置指南:从依赖锁定到MNIST训练验证

PyLearn2环境配置指南:从依赖锁定到MNIST训练验证

简介:Pylearn2安装包是一份深度学习框架Pylearn2的源码压缩包,源自蒙特利尔大学MILA实验室,主要面向希望研究卷积神经网络、受限玻尔兹曼机等模型的研究人员,以及有一定Python基础的开发者,解决框架安装和入门实验的痛…

2026/10/19 2:49:52 阅读更多 →
MySQL可重复读隔离级别:MVCC、ReadView与间隙锁原理详解

MySQL可重复读隔离级别:MVCC、ReadView与间隙锁原理详解

1. 先厘清一个关键概念:可重复读是目标,不是问题我见过太多人把“可重复读”当成一个bug来讨论,张口就是“数据库的RR隔离级别怎么解决可重复读”。这个说法本身就把概念搞拧了。可重复读不是数据库要解决的问题,而是数据库要提供…

2026/10/19 2:49:52 阅读更多 →
经典150题第36-40题复盘:栈、双指针与滑动窗口实战

经典150题第36-40题复盘:栈、双指针与滑动窗口实战

把面试经典150题刷到第36到第40题这个位置时,很多人会明显感觉到一道分水岭——前面还在靠朴素的遍历和暴力思路硬扛,到这里开始逼着你用数据结构本身的特性来解题了。这5道题覆盖了栈、链表双指针、快慢指针、滑动窗口、链表模拟加法,全是面…

2026/10/19 2:49:52 阅读更多 →

最新新闻

ARM 汇编 LDR 指令与 LDR 伪指令完整详解

ARM 汇编 LDR 指令与 LDR 伪指令完整详解

一、概念总述ARM 汇编中 LDR 存在两种完全不同身份,书写格式是核心区分依据:CPU 原生硬件指令 LDR:格式含方括号 [],硬件原生支持,功能为从内存读取数据到寄存器;汇编伪指令 LDR:格式含等号 &am…

2026/10/19 3:39:15 阅读更多 →
79通道ResNet改造:阿尔茨海默症MRI切片诊断模型

79通道ResNet改造:阿尔茨海默症MRI切片诊断模型

简介:本资源是一份面向深度学习初学者与课程实践者的阿尔茨海默症医学影像识别项目,聚焦ResNet模型优化与多维医学图像建模能力培养,适用于本科课程设计、工程实训及毕设选题参考。压缩包共13个文件,含6个核心Python脚本&#xff…

2026/10/19 3:39:15 阅读更多 →
AD早期识别三模态ResNet实战:2D/3D/MedicalNet医学影像分析

AD早期识别三模态ResNet实战:2D/3D/MedicalNet医学影像分析

简介:本资源是一份面向深度学习初学者与课程设计实践者的阿尔茨海默症医学影像识别项目,聚焦ResNet模型优化与多维建模方法,适用于本科课程设计、大作业及毕设前期技术验证。压缩包共13个文件,含6个核心Python脚本(实现…

2026/10/19 3:39:15 阅读更多 →
阿尔茨海默症MRI识别:ResNet结构优化与临床可解释性设计

阿尔茨海默症MRI识别:ResNet结构优化与临床可解释性设计

简介:本资源是一份面向深度学习初学者与课程实践者的阿尔茨海默症医学影像识别项目,聚焦ResNet模型优化与多维医学图像建模能力培养,适用于本科课程设计、工程实训及毕设选题参考。压缩包共13个文件,含6个核心Python脚本&#xff…

2026/10/19 3:39:15 阅读更多 →
Pi 1.0到1.1:从严格拒绝到智能接受的演进与实操指南

Pi 1.0到1.1:从严格拒绝到智能接受的演进与实操指南

1. 从“拒绝一切”到“选择性接受”:这个工具到底经历了什么第一次接触 Pi 这个工具的人,大概率会被它的“脾气”整懵。你兴冲冲地写了一段配置,点击运行,它回你一句冷冰冰的“不支持”;你换了个写法,它又告…

2026/10/19 3:39:15 阅读更多 →
开源代码审查新范式:CLI驱动的Git Diff+LLM协作框架

开源代码审查新范式:CLI驱动的Git Diff+LLM协作框架

1. 这不是又一个“AI代码审查工具”,而是一套可落地的开源协作新范式“open-code-review”这四个字乍看像某个GitHub仓库名,但背后藏着的是当前工程团队在AI时代最真实、最迫切的一次工作流重构。它既不是单纯调用某个大模型API的玩具脚本,也…

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

日新闻

频率跟踪型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/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →