STM32 UART双APP IAP实战:Flash分区、Bootloader跳转与远程固件升级
简介这是一份面向STM32嵌入式开发者的IAP应用程序内编程实战工程聚焦UART串口通信下的双APP固件升级方案。资源以STM32微控制器为平台演示如何通过任意通信接口获取新版bin固件包并完成自升级可延伸至4G、WiFi模块的OTA升级以及U盘、TF卡的OTG升级场景适合已掌握基础外设驱动、希望深入Bootloader与固件管理的中级开发者。压缩包共272个文件包含150个h头文件、78个c源文件、11个bin固件、6个ld链接脚本及ioc、launch等工程配置整体约1.86MB目录结构完整覆盖APP0与APP1双分区版本管理。内容预览可见APP0、APP1多版本bin文件便于验证跳转与回滚逻辑。目前已有1722人学习下载读者可从中获取双APP分区设计、版本切换、串口传输升级等关键实现思路是理解STM32 IAP机制的实用参考工程。1. 从一次OTA升级翻车说起STM32 UART双APP的IAP到底解决了什么去年帮一个做工业采集终端的朋友救急他们的设备装在现场十几米高的料仓顶上固件有个ADC采样通道配置错了要改一行代码。按老办法得派人爬上去拆壳、插ST-Link、重新烧录一趟下来大半天还得停机。他们当时就问我STM32能不能像手机一样通过串口把新固件传进去自己升级答案就是IAP——In Application Programming在应用编程。这份「STM32 UART双APP的IAP」资源核心就是干这件事用UART串口作为传输通道在Flash里放两套APP一套负责跑业务一套负责接收新固件并搬运实现不掉电、不拆机的远程升级。它适合正在做STM32量产设备、需要现场维护固件的嵌入式开发者尤其是那些设备装在高处、井下、密闭机柜里物理接触成本极高的场景。双APP的设计比单APP更稳因为升级过程中旧固件不会被立刻擦掉万一新固件传输出错还能回滚到旧版本继续跑不至于变砖。2. 双APP IAP的Flash分区与启动逻辑Bootloader怎么决定跳哪2.1 为什么是双APP而不是单APP单APP的IAP方案很常见Bootloader占一小块FlashAPP占一大块升级时Bootloader把新固件写到APP区覆盖旧代码。问题是覆盖过程中如果断电APP区就是半新半旧的垃圾数据设备直接变砖。双APP的思路是在Flash里划出两个独立的APP槽位比如APP_A和APP_B再加一个标志区记录当前该跑哪个、哪个是待升级的。升级时往空闲槽写新固件写完整校验通过后改标志位下次重启就跳到新槽。旧槽的固件保留着相当于留了一颗后悔药。这个项目用UART作为传输通道意味着Bootloader里要跑一个简单的串口协议接收上位机发来的固件分包、写入Flash、校验、更新标志。整个流程不依赖任何RTOS纯裸机状态机就能跑通对资源紧张的STM32F103这类芯片很友好。2.2 Flash分区的典型布局以STM32F103C8T6为例64KB Flash常见做法是这样切的区域起始地址大小用途Bootloader0x0800000012KB启动判断、UART接收、Flash写入标志区0x080030002KB记录当前APP槽、升级请求、固件长度、CRCAPP_A0x0800380024KB业务固件AAPP_B0x0800980024KB业务固件B保留0x0800F8002KB参数存储或对齐这个布局不是死的具体大小要看你的APP编译出来多大。我一般会先编一版APP看map文件里的CodeRO Data大小再往上留20%余量。Bootloader本身功能简单12KB足够但如果你的UART协议复杂、要支持YMODEM可能得给到16KB。标志区的设计是关键。常见做法是定义一个结构体存magic number、当前运行槽、待升级槽、固件长度、CRC32校验值。每次Bootloader启动先读这个结构体magic不对就初始化成默认值然后根据当前槽跳转。// 标志区结构体定义放在固定地址 typedef struct { uint32_t magic; // 0x5A5A1234判断标志区是否已初始化 uint8_t active_slot; // 0APP_A, 1APP_B uint8_t upgrade_slot; // 待升级的目标槽 uint8_t upgrade_flag; // 1有升级请求 uint8_t reserved; uint32_t fw_size; // 新固件字节数 uint32_t fw_crc; // 新固件CRC32 } IAP_Flag_t; #define FLAG_ADDR 0x08003000 #define FLAG_MAGIC 0x5A5A1234逻辑说明magic用于判断这块Flash是不是第一次用如果是空白Flash读出来是0xFFFFFFFF跟magic不匹配就写入默认值。active_slot决定Bootloader跳哪个槽。upgrade_flag和upgrade_slot配合告诉Bootloader「APP_B里已经写好新固件了校验通过就切过去」。fw_size和fw_crc是给Bootloader做二次校验用的防止APP写Flash时出错。2.3 启动跳转的代码实现Bootloader的main函数里第一步就是判断标志区、决定跳转目标// 跳转到APP的通用函数 typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_ptr *(volatile uint32_t*)app_addr; // 检查栈顶指针是否在合法RAM范围防止跳到空区域 if (stack_ptr 0x20000000 || stack_ptr 0x20005000) { return; // 非法不跳 } pFunction app_entry (pFunction)(*(volatile uint32_t*)(app_addr 4)); __set_MSP(stack_ptr); // 重设主栈指针 SCB-VTOR app_addr; // 重定向中断向量表 app_entry(); // 跳过去不再返回 }参数说明app_addr是APP槽的起始地址比如0x08003800。STM32的APP固件开头4字节是栈顶地址接着4字节是复位向量。跳转前必须做两件事把MSP设成APP的栈顶把VTOR指向APP的向量表。少了VTOR重定向APP里的中断会跑到Bootloader的向量表去现象就是串口收不到数据、定时器不进中断这种玄学问题我踩过不止一次。注意APP工程里也要改两个地方。一是链接脚本里ROM起始地址改成对应槽的地址二是system_stm32f1xx.c里的VECT_TAB_OFFSET要设成相对于0x08000000的偏移。这两处不改APP编出来还是从0x08000000跑跳过去必挂。3. UART传输协议与Flash写入一包固件从串口到Flash的完整链路3.1 自定义分包协议的设计UART传固件最怕的是丢包和错位。上位机发一大串二进制Bootloader怎么知道哪是包头、哪是数据、哪是校验常见做法是定义一个简单的帧格式| 帧头(2B) | 包序号(2B) | 数据长度(2B) | 数据(N B) | CRC16(2B) |帧头固定0xAA55包序号从0开始递增数据长度一般取512或1024字节跟Flash页大小对齐。CRC16校验这一包数据Bootloader收到后先验CRC不对就回NAK让上位机重发。这种协议比YMODEM简单自己实现起来可控调试也直观。// 接收一包固件的状态机简化版 typedef enum { WAIT_HEAD_H, WAIT_HEAD_L, WAIT_SEQ_H, WAIT_SEQ_L, WAIT_LEN_H, WAIT_LEN_L, WAIT_DATA, WAIT_CRC_H, WAIT_CRC_L } RxState_t; void iap_uart_rx_byte(uint8_t byte) { static RxState_t state WAIT_HEAD_H; static uint16_t seq, len, data_idx; static uint8_t buf[1024]; static uint16_t crc_recv; switch (state) { case WAIT_HEAD_H: if (byte 0xAA) state WAIT_HEAD_L; break; case WAIT_HEAD_L: state (byte 0x55) ? WAIT_SEQ_H : WAIT_HEAD_H; break; case WAIT_SEQ_H: seq byte 8; state WAIT_SEQ_L; break; case WAIT_SEQ_L: seq | byte; state WAIT_LEN_H; break; case WAIT_LEN_H: len byte 8; state WAIT_LEN_L; break; case WAIT_LEN_L: len | byte; data_idx 0; state (len 0 len 1024) ? WAIT_DATA : WAIT_HEAD_H; break; case WAIT_DATA: buf[data_idx] byte; if (data_idx len) state WAIT_CRC_H; break; case WAIT_CRC_H: crc_recv byte 8; state WAIT_CRC_L; break; case WAIT_CRC_L: crc_recv | byte; // 校验通过则写Flash否则回NAK if (crc16(buf, len) crc_recv) { flash_write_packet(seq, buf, len); uart_send_ack(seq); } else { uart_send_nak(seq); } state WAIT_HEAD_H; break; } }逻辑说明这是一个逐字节驱动的状态机每收到一个字节就推进一次。好处是不用开大缓冲区等一整包RAM占用小。seq用于上位机确认哪包没到重发时只重发那一包。flash_write_packet里要根据seq算出目标地址目标地址 槽起始地址 seq * 每包数据长度。参数说明每包数据长度建议取Flash页大小的整数倍。STM32F1的Flash页是1KBF4是扇区大小不一。取512字节的话写的时候要凑够1KB再擦写或者用半字编程逐次写。我一般直接取1024跟页对齐省得处理跨页。3.2 Flash擦写的关键细节STM32的Flash写之前必须先擦擦的最小单位是页F1或扇区F4。擦的时候整页变0xFF所以不能边收边擦正在写的页。正确顺序是收到升级开始命令后先把目标槽的所有页擦掉然后再逐包写入。// 擦除目标APP槽 void flash_erase_slot(uint32_t slot_addr, uint32_t size) { FLASH_Unlock(); uint32_t page_addr slot_addr; uint32_t end_addr slot_addr size; while (page_addr end_addr) { FLASH_ErasePage(page_addr); // F1用页擦除 page_addr 1024; // 每页1KB } FLASH_Lock(); } // 写入一包数据半字编程 void flash_write_packet(uint16_t seq, uint8_t *data, uint16_t len) { uint32_t addr TARGET_SLOT_ADDR seq * PACKET_SIZE; FLASH_Unlock(); for (uint16_t i 0; i len; i 2) { uint16_t half_word data[i] | (data[i1] 8); FLASH_ProgramHalfWord(addr i, half_word); } FLASH_Lock(); }逻辑说明擦除必须在写入之前全部完成不能写一包擦一页。因为擦除操作会让整个页变0xFF如果这页前面已经写了数据一擦就全没了。所以协议里要有一个「开始升级」命令收到后先擦目标槽再进入收包状态。参数说明FLASH_ProgramHalfWord是F1的标准库函数每次写2字节。写入地址必须是偶数数据长度最好是偶数奇数长度要补0xFF。写完之后要读回来比对确认写入成功这一步别省Flash写入失败在电压不稳时真的会发生。3.3 升级完成后的校验与切换所有包收完后Bootloader要做三件事算整个固件的CRC32、跟标志区里记录的fw_crc比对、一致就改active_slot并清upgrade_flag然后软复位。// 升级完成校验 uint8_t iap_finalize(uint32_t fw_size, uint32_t fw_crc) { uint32_t calc_crc crc32_flash(TARGET_SLOT_ADDR, fw_size); if (calc_crc ! fw_crc) { return 0; // 校验失败不切换 } IAP_Flag_t flag; flash_read(FLAG_ADDR, (uint8_t*)flag, sizeof(flag)); flag.active_slot flag.upgrade_slot; // 切到新槽 flag.upgrade_flag 0; flash_erase_page(FLAG_ADDR); flash_write(FLAG_ADDR, (uint8_t*)flag, sizeof(flag)); return 1; }逻辑说明crc32_flash逐字节读Flash算CRC虽然慢但只在升级结束时跑一次可以接受。校验通过才改标志区不通过就保持原样设备重启后还跑旧固件。改完标志区后调用NVIC_SystemReset()软复位Bootloader重新读标志跳到新槽。参数说明fw_size和fw_crc来自上位机在「开始升级」命令里带的参数Bootloader收到后先存到标志区最后校验时用。CRC32比CRC16更可靠固件几万字节CRC16碰撞概率还是有点高。4. 避坑与排查双APP IAP最容易翻车的五个地方4.1 跳转后中断不响应现象Bootloader跳转到APP后APP的main函数能跑串口能发数据但收不到数据定时器也不进中断。原因APP工程的中断向量表偏移没设对。STM32默认从0x08000000取向量表APP在0x08003800中断发生时CPU还是去0x08000000找中断服务函数找到的是Bootloader的自然不对。解决在APP的main函数开头、初始化外设之前加一句SCB-VTOR 0x08003800;或者在system_stm32f1xx.c里改VECT_TAB_OFFSET。两个地方改一个就行我习惯在main里显式设看得见。4.2 升级到一半断电设备起不来现象升级过程中断电重新上电后设备卡在Bootloader不跳APP。原因标志区的upgrade_flag已经置1但固件没传完Bootloader启动时看到升级请求就去校验目标槽CRC不过就不跳转卡住了。解决Bootloader启动逻辑里加超时或重试机制。如果upgrade_flag为1但校验失败清掉upgrade_flag跳回原active_slot。另外标志区写入要用「先擦后写」擦除过程中断电标志区可能变0xFFmagic不匹配就初始化成默认值跳APP_A。4.3 串口收包丢字节导致CRC错现象升级过程中频繁NAK重发进度条卡住不动。原因Bootloader在写Flash时会关中断或阻塞UART接收中断被延迟上位机发太快就丢字节。STM32的Flash写入期间CPU取指会暂停如果UART没有硬件FIFO一个字节没及时读走就丢了。解决上位机每发一包等ACK再发下一包不要连续发。Bootloader写Flash时先把数据存到RAM缓冲写完再开中断。或者用DMA接收DMA不占CPUFlash写入期间也能收。4.4 APP编译出来超过槽大小现象APP编译没报错但IAP升级后跑不起来或者跑着跑着HardFault。原因APP的CodeRO Data超过了槽的24KB链接时没报错是因为链接脚本里ROM大小写的是64KB实际写进去覆盖了下一个槽或标志区。解决APP工程的链接脚本里ROM起始地址和大小都要改成槽的实际范围比如ROM (rx) : ORIGIN 0x08003800, LENGTH 24K。改完编译如果超了链接器会报region overflow提前发现。4.5 升级后旧槽数据残留影响新固件现象从APP_A升级到APP_B新固件跑起来后读写参数异常。原因APP_B槽擦除不彻底或者参数存储区跟APP槽重叠了。有些方案把参数存在APP槽末尾双APP切换后参数地址变了读出来是旧数据。解决参数存储单独划一个区不放在APP槽里。擦除时确认目标槽全部页都擦了可以擦完后读回来检查是不是全0xFF。5. 二次开发改造把双APP IAP接到你自己的协议栈上这份资源的骨架是Bootloader双APPUART协议实际项目里你大概率要改三个地方传输通道、上位机工具、APP的链接配置。先说传输通道。UART只是其中一种如果你的设备有CAN、以太网、甚至无线模块把iap_uart_rx_byte这个状态机换成对应的接收回调就行协议帧格式不用动。我做过一个CAN版本把每包数据拆成8字节的CAN帧加个多帧组包逻辑其余代码原样复用。关键是把「收字节」和「写Flash」解耦收包状态机只管攒数据攒够一包交给写Flash的函数。再说上位机。资源里如果带了上位机源码重点看它怎么分包、怎么算CRC、怎么处理NAK重发。改造时最容易出问题的是超时重发逻辑等ACK等多久、重发几次算失败、失败后是重传当前包还是从头来。我的习惯是等ACK超时500ms重发3次3次都失败就中止升级并提示。别设太短Flash擦除一页在F1上要20~40ms加上传输延迟200ms以下容易误判。最后是APP的链接配置。每个APP工程都要改ROM起始地址和大小还要改VTOR。如果两个APP共用一套代码可以用宏区分// 在APP的main.c里 #if defined(APP_SLOT_A) #define APP_BASE_ADDR 0x08003800 #elif defined(APP_SLOT_B) #define APP_BASE_ADDR 0x08009800 #endif int main(void) { SCB-VTOR APP_BASE_ADDR; // 重定向向量表 // ... 其余初始化 }编译时用-DAPP_SLOT_A或-DAPP_SLOT_B切换链接脚本也准备两份或者用变量传进去。这样一套代码编出两个槽的固件维护成本低。验证方法我一般分三步走第一步用调试器手动改标志区确认Bootloader能正确跳两个槽第二步用上位机走一遍完整升级故意在传输中途拔掉串口线看能不能回滚第三步把新固件里加一个版本号打印升级后读出来确认跑的是新版本。这三步走完基本就稳了。从那以后我每次做IAP方案都强制走一遍「断电回滚」测试——升级到一半直接拉电再上电看设备是不是还活着。这个测试比任何代码审查都管用因为Flash操作和标志区写入的时序问题只有真断电才能暴露出来。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

free-claude-code:Claude Code免费额度管理实战

free-claude-code:Claude Code免费额度管理实战

free-claude-code 是我最近整理的一个小工具,专门用来管理 Claude Code 的免费额度。很多人第一次听说 Claude Code 时,先被它那套命令行交互惊艳到,但真到自己上手,又容易被“额度到底还剩多少”“怎么在不同项目间切换不串 key”…

2026/10/16 14:14:19 阅读更多 →
stripe-integration - SKILL

stripe-integration - SKILL

name: stripe-integration description: “精通 Stripe 支付处理集成,构建稳健、符合 PCI 标准的支付流程,包括结账、订阅、Webhook 和退款。” risk: critical source: community date_added: “2026-02-27” Stripe 集成 精通 Stripe 支付处理集成&…

2026/10/16 14:13:17 阅读更多 →
stitch-design-taste - SKILL

stitch-design-taste - SKILL

name: stitch-design-taste description: “Use when generating Google Stitch DESIGN.md systems for premium typography, color, layout, motion intent, and anti-generic UI rules.” category: frontend risk: safe source: community source_repo: Leonxlnx/taste-skil…

2026/10/16 14:13:17 阅读更多 →

最新新闻

从 ViT 到 M-RoPE:TeleOCR 的 1.2B 架构里藏着哪些细节

从 ViT 到 M-RoPE:TeleOCR 的 1.2B 架构里藏着哪些细节

从 ViT 到 M-RoPE:TeleOCR 的 1.2B 架构里藏着哪些细节 【免费下载链接】TeleOCR 项目地址: https://ai.gitcode.com/XingChen-AGI/TeleOCR 当 OmiDocBench 榜单上出现一个只有 1.2B 参数的模型,以 96.87 的综合得分压过 235B 的 Qwen3-VL&#…

2026/10/16 14:55:10 阅读更多 →
XERJ语义搜索完全指南:semantic_text字段与neural嵌入模式怎么配才对?

XERJ语义搜索完全指南:semantic_text字段与neural嵌入模式怎么配才对?

XERJ语义搜索完全指南:semantic_text字段与neural嵌入模式怎么配才对? 【免费下载链接】xerj XERJ is the new way for AI to search data. Its autoindex capability activates agents to know your data without the token waste of grep and sed. One…

2026/10/16 14:55:10 阅读更多 →
MLP还是Attention?Qwen3.8-27B-singprobe两种探针结构(SingProbeMlpModel vs SingProbeAttnModel)深度对比

MLP还是Attention?Qwen3.8-27B-singprobe两种探针结构(SingProbeMlpModel vs SingProbeAttnModel)深度对比

【免费下载链接】Qwen3.8-27B-singprobe 项目地址: https://ai.gitcode.com/hf_mirrors/inclusionAI/Qwen3.8-27B-singprobe 点击查看 免费下载 Qwen3.8-27B-singprobe 是 inclusionAI 推出的轻量级大模型安全探针(Probe),基于 Q…

2026/10/16 14:55:10 阅读更多 →
Python TCP断线重连实战:原理、策略与避坑指南

Python TCP断线重连实战:原理、策略与避坑指南

做网络编程时间久了,你会发现一个很尴尬的事实——TCP连接断没断,你的代码多半是最后一个知道的。服务端崩了、网线被拔了、路由半夜重启了,客户端这边往往还在傻乎乎地等着收数据,直到超时或报错才如梦初醒。更麻烦的是&#xff…

2026/10/16 14:55:10 阅读更多 →
STM32+MPU6050四元数姿态解算:从I2C读取到互补滤波实战

STM32+MPU6050四元数姿态解算:从I2C读取到互补滤波实战

简介:针对STM32F103与MPU6050的六轴姿态检测需求,这套源码工程完整实现了基于四元数的姿态解算流程。项目由MPU6050采集加速度与角速度数据,通过IO模拟I2C与主控通信,STM32端完成四元数积分、归一化及重力向量校正,并借…

2026/10/16 14:54:08 阅读更多 →
网神SecADS 3600应用交付系统快速配置手册:从VLAN到负载均衡实战

网神SecADS 3600应用交付系统快速配置手册:从VLAN到负载均衡实战

简介:这份资源是奇安信网神SecADS 3600应用交付系统的快速配置手册,面向网络运维工程师、售前技术支持及渠道商技术人员,帮助读者快速掌握设备部署与日常配置方法。压缩包内仅含1个docx文档,大小约1.18MB,内容以图文步…

2026/10/16 14:54:08 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

2026/10/15 19:41:51 阅读更多 →
影刀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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →