STM32串口IAP固件升级实战:分区规划、向量表重定向与避坑指南
简介这份源码包面向使用STM32F103C8T6进行产品开发或嵌入式学习的工程师聚焦串口IAP在线升级固件这一常见需求帮助读者在资源受限的Cortex-M3平台上搭建可远程更新程序的Bootloader与App框架。压缩包共430个文件约2.23MB以86个h头文件与86个c源文件为核心配合48个s汇编、18个ld链接脚本、24个lsl、18个asm启动代码及多套工程配置uvproj、cproject、ewp等覆盖Keil、IAR、GCC等多种工具链另含bin、hex、map等编译产物便于直接验证。资源围绕串口通信、Flash分区、跳转与校验等关键环节组织读者可据此理解IAP升级的完整链路掌握固件搬运、中断向量重映射与升级失败回滚等排错思路并对照博客文章完成移植与调试。目前已有5135人学习下载适合具备一定STM32基础、希望补齐Bootloader实战经验的开发者参考。1. 串口IAP升级固件为什么它是量产设备最省心的“后悔药”手里有一块 STM32F103C8T6 的最小系统板焊在设备里之后才发现固件有 bug拆机接 SWD 又费时费力——这是很多做小家电、工控模块、传感器节点的工程师都遇到过的场景。串口 IAPIn-Application Programming就是解决这个问题的让 MCU 在运行状态下通过 UART 接收新固件并写入自身 Flash不用拆机、不用仿真器一根串口线就能完成升级。它适合谁适合那些设备已经部署到现场、外壳封死、但需要远程或本地维护固件的开发者。核心难点不在“能不能写 Flash”而在分区规划、跳转逻辑、中断向量表重定向和传输可靠性这四件事上。把这四件事做对串口 IAP 就是量产设备最省心的后悔药做错一件升级变砖的概率就会直线上升。2. 分区规划与 Bootloader 最小系统先把 Flash 地图画清楚2.1 为什么 STM32F103C8T6 的 64KB Flash 要精打细算STM32F103C8T6 的 Flash 只有 64KBSRAM 20KB。这个资源量做 IAP 并不宽裕所以第一件事不是写代码而是画 Flash 地图。常见做法是把 Flash 分成三段Bootloader 区、APP 区、参数/备份区。Bootloader 负责上电判断、接收固件、擦写 APP 区、跳转APP 区放业务代码参数区存升级标志、固件长度、CRC 校验值。我一般会这样分区域起始地址大小用途Bootloader0x0800000012KB启动判断、串口接收、Flash 擦写APP0x0800300044KB业务固件参数区0x0800E0004KB升级标志、固件长度、CRC保留0x0800F0004KB预留防止越界这里有个关键点APP 区的起始地址必须是 Flash 页大小的整数倍。STM32F103C8T6 的 Flash 页大小是 1KB所以 0x08003000 是合法的。Bootloader 给 12KB 是因为串口接收缓冲、CRC 计算、Flash 操作函数加起来差不多这个量级留太少后面加功能会捉襟见肘。2.2 Bootloader 的最小可运行框架Bootloader 不需要 RTOS裸机跑一个状态机就够了。核心流程是上电 → 读参数区升级标志 → 如果标志有效则进入接收模式 → 否则检查 APP 区是否有效 → 跳转 APP。// bootloader main.c 核心逻辑 #include stm32f10x.h #include flash_if.h #include uart_if.h #define APP_ADDR 0x08003000 #define PARAM_ADDR 0x0800E000 #define UPGRADE_FLAG 0x5A5A5A5A typedef struct { uint32_t flag; uint32_t fw_len; uint32_t fw_crc; } param_t; int main(void) { param_t param; flash_read(PARAM_ADDR, (uint8_t *)param, sizeof(param)); if (param.flag UPGRADE_FLAG) { // 进入升级模式等待串口数据 uart_init(115200); if (iap_receive_firmware() 0) { // 升级成功清除标志 param.flag 0; flash_write(PARAM_ADDR, (uint8_t *)param, sizeof(param)); } } // 校验 APP 区栈顶指针是否合法 uint32_t app_stack *(volatile uint32_t *)APP_ADDR; if ((app_stack 0x2FFE0000) 0x20000000) { jump_to_app(APP_ADDR); } while (1); // 兜底不应该到这里 }这段代码的逻辑说明先读参数区判断是否有升级请求如果有进入串口接收流程升级完成后清除标志。然后校验 APP 区第一个字栈顶指针是否落在 SRAM 范围0x20000000 起始合法才跳转。参数说明UPGRADE_FLAG是一个魔数避免 Flash 空白时误判app_stack 0x2FFE0000是检查高 14 位是否为 0x2000这是 STM32 判断 APP 是否有效的常用手法。2.3 跳转 APP 前必须做的三件事跳转不是一句函数指针调用就完事。血泪经验是跳转前必须关中断、复位外设、设置主栈指针。少做一件APP 跑起来就可能 HardFault。// 跳转到 APP 的完整实现 typedef void (*app_func_t)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_addr; app_func_t app_entry; // 1. 关闭所有中断 __disable_irq(); // 2. 复位 SysTick 和所有外设 SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; RCC-APB1ENR 0; RCC-APB2ENR 0; // 3. 设置主栈指针为 APP 区第一个字 __set_MSP(*(volatile uint32_t *)app_addr); // 4. 取复位向量地址并跳转 jump_addr *(volatile uint32_t *)(app_addr 4); app_entry (app_func_t)jump_addr; app_entry(); }逻辑说明__set_MSP把主栈指针指向 APP 区第一个字这是 APP 编译时链接脚本决定的栈顶。app_addr 4是复位向量地址因为 Cortex-M3 的向量表前两个字分别是栈顶和复位入口。参数说明app_addr就是 0x08003000必须和 APP 的链接脚本一致。注意RCC-APB1ENR 0会关掉所有外设时钟如果 APP 里重新初始化外设这是安全的但如果 APP 依赖 Bootloader 留下的外设状态就不能这么干。3. APP 端改造中断向量表重定向与链接脚本3.1 为什么 APP 必须重定向向量表STM32F103C8T6 的向量表默认在 0x08000000。APP 被放到 0x08003000 后如果中断发生时 CPU 还去 0x08000000 取向量就会跳到 Bootloader 的中断服务函数结果要么跑飞要么 HardFault。所以 APP 启动后第一件事就是重定向向量表。// APP main.c 开头必须做的事 #include stm32f10x.h #define VECT_TAB_OFFSET 0x3000 // 相对于 0x08000000 的偏移 int main(void) { // 重定向向量表到 APP 区 NVIC_SetVectorTable(NVIC_VectTab_FLASH, VECT_TAB_OFFSET); // 使能全局中断 __enable_irq(); // 后续业务初始化 uart_init(115200); while (1) { // 业务逻辑 } }逻辑说明NVIC_SetVectorTable是标准库函数内部写 SCB-VTOR 寄存器。参数说明NVIC_VectTab_FLASH表示向量表在 Flash 中VECT_TAB_OFFSET是相对于 0x08000000 的偏移这里 APP 在 0x08003000所以偏移是 0x3000。这个值必须和 Bootloader 里的 APP_ADDR 一致否则中断会跳错地方。3.2 Keil 链接脚本怎么改光改代码不够链接脚本也要改否则编译出来的 APP 还是从 0x08000000 开始链接烧进去直接覆盖 Bootloader。在 Keil MDK 中打开 Options for Target → Target 选项卡把 IROM1 的 Start 改成 0x08003000Size 改成 0xB00044KB。或者在分散加载文件里写; APP 的分散加载文件 app.sct LR_IROM1 0x08003000 0x0000B000 { ER_IROM1 0x08003000 0x0000B000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (RW ZI) } }逻辑说明LR_IROM1是加载域起始地址 0x08003000大小 0xB000。ER_IROM1是执行域地址相同。RW_IRAM1是 RAM 区20KB 的 SRAM 用 0x5000 表示。参数说明Size 0xB000 是 44KB因为 64KB 减去 12KB Bootloader 再减去 4KB 参数区再减去 4KB 保留剩下 44KB。如果 APP 编译后超过这个大小链接会报错这时候要么压缩代码要么重新规划分区。3.3 串口接收固件的协议设计Bootloader 接收固件不能裸收必须有协议保证完整性。常见做法是自定义一个简单帧格式帧头 长度 数据 CRC。// 串口接收状态机 typedef enum { STATE_IDLE, STATE_HEADER, STATE_LEN, STATE_DATA, STATE_CRC } rx_state_t; #define FRAME_HEADER 0xAA55 #define MAX_FRAME 1024 int iap_receive_firmware(void) { uint8_t buf[MAX_FRAME]; uint16_t len, crc, recv_crc; uint32_t total 0; while (1) { // 接收帧头 if (uart_recv_byte_timeout(header, 1000) ! 0) break; if (header ! FRAME_HEADER) continue; // 接收长度 uart_recv_bytes((uint8_t *)len, 2, 1000); if (len MAX_FRAME) continue; // 接收数据 uart_recv_bytes(buf, len, 5000); // 接收 CRC uart_recv_bytes((uint8_t *)recv_crc, 2, 1000); crc crc16(buf, len); if (crc ! recv_crc) { uart_send_nack(); continue; } // 写入 Flash flash_write(APP_ADDR total, buf, len); total len; uart_send_ack(); // 判断是否接收完毕由上位机发送结束帧 if (len 0) break; } // 校验整个固件 CRC return iap_verify_firmware(total); }逻辑说明状态机逐字节接收先找帧头 0xAA55再读长度再读数据最后校验 CRC16。每帧写入 Flash 后回 ACK上位机收到 ACK 才发下一帧。参数说明MAX_FRAME设为 1024是因为 STM32F103C8T6 的 RAM 只有 20KB缓冲区不能太大uart_recv_byte_timeout的超时时间 1000ms 是等待帧头的超时5000ms是等待数据的超时实际项目中可以根据波特率调整。注意 Flash 写入前必须先擦除STM32F103C8T6 的 Flash 擦除是按页1KB进行的所以写入地址要按页对齐。4. 避坑与排查串口 IAP 最容易翻车的五个地方4.1 跳转后 APP 不运行卡在 HardFault现象Bootloader 跳转后APP 没有任何输出调试器显示 HardFault。原因最常见的是向量表没重定向或者跳转前没关中断导致中断触发时跳到 Bootloader 的向量表。解决确认 APP 的NVIC_SetVectorTable在main第一行调用确认跳转前__disable_irq()已执行确认 APP 链接脚本的起始地址和 Bootloader 里的APP_ADDR一致。4.2 升级过程中断电设备变砖现象升级到一半断电重新上电后 Bootloader 还在但 APP 区是半截固件跳转后跑飞。原因没有备份机制APP 区被擦除后新固件没写完。解决在参数区加一个“升级中”标志上电时如果发现这个标志有效就停留在 Bootloader 等待重新升级不跳转 APP。另外可以保留一个备份区升级前先把旧固件备份失败后回滚。4.3 串口接收丢数据CRC 校验频繁失败现象上位机发送固件Bootloader 经常回 NACK或者接收到的数据长度不对。原因波特率太高比如 115200 以上时Bootloader 在擦写 Flash 期间无法及时接收串口数据导致溢出。解决擦写 Flash 时先关闭串口接收中断擦完再开或者降低波特率到 57600或者用 DMA 接收但 STM32F103C8T6 的 DMA 通道有限需要权衡。我一般会在 Flash 擦除期间让上位机等待擦除完成后发一个“就绪”信号再继续。4.4 APP 区大小超限链接报错现象Keil 编译 APP 时报 “region IROM1 overflowed”。原因APP 代码量超过了分区规划的大小。解决先看 map 文件找出占用最大的模块如果是标准库或 HAL 库占用大可以换成 LL 库或直接寄存器操作如果是业务代码大考虑压缩分区比如把 Bootloader 从 12KB 减到 8KB参数区从 4KB 减到 2KB。但要注意 Bootloader 太小可能放不下串口协议和 Flash 操作函数。4.5 升级成功后 APP 不启动但手动复位可以现象升级完成后Bootloader 跳转 APP 失败但按复位键后 APP 能正常运行。原因升级完成后没有清除升级标志或者清除标志后没有重新校验 APP 区。解决升级成功后先写参数区清除标志再软复位NVIC_SystemReset()让 Bootloader 重新走一遍上电流程。不要直接跳转因为跳转前的环境可能已经被升级流程污染。5. 进阶技巧用 CRC32 校验和双备份把升级成功率拉到 99%5.1 为什么 CRC16 不够什么时候该上 CRC32CRC16 在固件小于 32KB 时碰撞概率已经很低但 STM32F103C8T6 的 APP 区最大 44KB加上参数区和备份区总数据量接近 64KB。如果对可靠性要求极高比如医疗设备或工业控制器建议上 CRC32。CRC32 的计算量比 CRC16 大但在 72MHz 的 STM32F103C8T6 上44KB 数据的 CRC32 计算时间大约几十毫秒完全可以接受。// CRC32 查表法实现片段 static const uint32_t crc32_table[256] { ... }; uint32_t crc32_calc(const uint8_t *data, uint32_t len) { uint32_t crc 0xFFFFFFFF; while (len--) { crc (crc 8) ^ crc32_table[(crc ^ *data) 0xFF]; } return crc ^ 0xFFFFFFFF; }逻辑说明CRC32 查表法用 256 个 32 位表项每次处理一个字节。参数说明初始值 0xFFFFFFFF最终结果异或 0xFFFFFFFF这是标准 CRC32 算法。表可以放在 Flash 里不占 RAM。5.2 双备份升级A/B 分区怎么划如果 Flash 够大可以做 A/B 双备份。STM32F103C8T6 只有 64KB做双备份比较紧张但可以这样分Bootloader 8KBAPP A 区 24KBAPP B 区 24KB参数区 4KB保留 4KB。升级时先写 B 区写完后校验校验通过后把参数区的“当前运行区”指向 B 区然后复位。如果 B 区校验失败继续用 A 区。这样即使升级失败设备也不会变砖。区域起始地址大小用途Bootloader0x080000008KB启动判断、串口接收APP A0x0800200024KB当前运行固件APP B0x0800800024KB升级备份固件参数区0x0800E0004KB运行区标志、CRC保留0x0800F0004KB预留这种分区的代价是 APP 最大只能 24KB对代码量大的项目不友好。所以双备份适合业务逻辑简单、但可靠性要求高的场景。5.3 上位机发送端的配合技巧Bootloader 做得再好上位机拖后腿也会翻车。我一般会在上位机做三件事第一每帧数据之间加 10ms 延时给 Bootloader 留出 Flash 擦写时间第二收到 NACK 后重发当前帧最多重试 3 次第三整个固件发送完成后等待 Bootloader 回一个“升级成功”的确认帧再关闭串口。上位机可以用 Python 写用 pyserial 库代码量不大但能显著提升升级成功率。# 上位机发送固件的核心逻辑Python import serial import time import struct def send_firmware(port, firmware_path): ser serial.Serial(port, 115200, timeout2) with open(firmware_path, rb) as f: data f.read() total_len len(data) offset 0 while offset total_len: chunk data[offset:offset1024] frame struct.pack(H H, 0xAA55, len(chunk)) chunk crc crc16(chunk) frame struct.pack(H, crc) ser.write(frame) # 等待 ACK ack ser.read(1) if ack b\x06: offset len(chunk) else: time.sleep(0.01) # 重试前延时 time.sleep(0.01) # 帧间延时 # 发送结束帧 ser.write(struct.pack(H H H, 0xAA55, 0, 0)) print(升级完成)逻辑说明每帧 1024 字节帧头 0xAA55长度 2 字节数据CRC16 2 字节。收到 ACK0x06后偏移量增加否则延时后重发。参数说明timeout2是串口读超时time.sleep(0.01)是帧间延时实际项目中可以根据 Bootloader 的擦写速度调整。注意 Python 的struct.pack默认小端序和 STM32 一致。5.4 一个我踩过的坑Flash 擦除时间被低估STM32F103C8T6 的 Flash 擦除一页1KB典型时间是 20ms最大 40ms。如果 Bootloader 在擦除期间还开着串口接收中断上位机发来的数据就会丢失。我一开始没注意上位机连续发数据Bootloader 擦一页丢几帧CRC 频繁失败。后来改成擦除前发一个“忙”信号上位机收到后暂停发送擦除完成后再发“就绪”信号问题才解决。这个细节在数据手册里有写但很容易被忽略。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

CEF 浏览器控件实战:从多进程架构到 JS 通信的避坑指南

CEF 浏览器控件实战:从多进程架构到 JS 通信的避坑指南

简介:这份资源面向需要在Windows桌面端嵌入网页渲染能力的C开发者,尤其是使用MFC框架、希望用Chromium内核替代传统IE控件的工程师。内容围绕谷歌CEF(Chromium Embedded Framework)浏览器控件的集成与使用展开,覆盖从C…

2026/10/15 17:30:50 阅读更多 →
C++运算符全解析:从算术到位运算的避坑指南

C++运算符全解析:从算术到位运算的避坑指南

1. 为什么运算符值得单独写一篇笔记 很多人学 C 的时候,对运算符的态度就是"这玩意儿还用学?加减乘除谁不会"。我一开始也这么想,直到有次写一个图像处理的 Demo,用位运算做像素掩码,结果因为搞混了 & …

2026/10/15 17:30:50 阅读更多 →
C#调用C++结构体:内存布局对齐与P/Invoke避坑指南

C#调用C++结构体:内存布局对齐与P/Invoke避坑指南

简介:本资源是一套完整的C#调用C结构体混合编程实战项目,面向Windows平台下具备基础C#和C语法能力的中高级开发者,解决跨语言数据结构互通与DLL互操作这一高频工程难题。资源包含C动态链接库(MyLib.dll)源码、C#调用端…

2026/10/15 17:30:50 阅读更多 →

最新新闻

【ESP32笔记】ESP中的分区表

【ESP32笔记】ESP中的分区表

本节目标:看懂分区表 CSV 的每一列,会给工程添加自定义分区,并用 esp_partition API 完成擦除、写入、读取一、本节要解决的问题 ESP32 的程序不是"整个 bin 一股脑烧到某个固定地址",而是把整块 flash 划分成多个分区&…

2026/10/15 19:04:55 阅读更多 →
亲和路由(inspect)

亲和路由(inspect)

Inspect 要解决的是“多轮对话在普通选路下被分到不同渠道”导致缓存失效和兼容性出错的问题;它通过回答“值不值得亲和、按什么粒度分组、用哪些稳定字段”三个问题,输出分类、触发原因和内容哈希;它被设计成无外部状态的纯分析器&#xff0…

2026/10/15 19:04:55 阅读更多 →
AI本体建模-------GB/T 48000.3—2026 标准详解

AI本体建模-------GB/T 48000.3—2026 标准详解

AI本体建模之 GB/T 48000.3—2026 标准详解 当大模型与 Agent 逐渐成为企业基础设施,真正的护城河正从「数据」转向「可被机器理解、推理、校验、共享的知识」。而 本体(Ontology) 正是这套知识体系的「模式层」与「契约层」。 2026 年 1 月&…

2026/10/15 19:04:55 阅读更多 →
线上 Redis 慢查询,不一定就是命令本身慢

线上 Redis 慢查询,不一定就是命令本身慢

上周值班的时候,监控突然弹出 Redis 慢查询告警,慢查询日志里全是 HGETALL,单条耗时经常冲到 80ms 以上。第一反应,又是经典问题,大 Hash 别用 HGETALL,这个知识点群里都说烂了。 一开始我也是这么处理的&a…

2026/10/15 19:04:55 阅读更多 →
有效三角形个数:排序+双指针经典算法详解

有效三角形个数:排序+双指针经典算法详解

做算法题的时候有一种快乐是:题目本身只有一行描述,但背后藏着一整类优化思路。有效三角形个数就是这样——表面上是几何问题,实际上是排序和双指针的经典练兵场,也是我算法题解系列第五篇想重点讲透的题目。题目很直白&#xff1…

2026/10/15 19:04:55 阅读更多 →
SDSegmentedControl避坑指南:iOS 6/7兼容处理、IB集成技巧与现代化改造建议

SDSegmentedControl避坑指南:iOS 6/7兼容处理、IB集成技巧与现代化改造建议

【免费下载链接】SDSegmentedControl A drop-in remplacement for UISegmentedControl that mimic iOS 6 AppStore tab controls 项目地址: https://gitcode.com/gh_mirrors/sd/SDSegmentedControl 点击查看 免费下载 SDSegmentedControl 是一个可以直接替换系统 U…

2026/10/15 19:02:09 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →