STM32F407上ucosii移植实战:MDK工程搭建与避坑指南
简介这份资源是面向嵌入式开发者的μC/OS-II在STM32F407平台上的完整移植工程基于ARM Cortex-M4内核与FPU浮点单元适合已具备一定RTOS基础、希望深入掌握实时系统移植与任务调度的中高级开发者。压缩包共1916个文件约19.2MB以C源码与头文件为主体辅以HTML文档、汇编启动文件、Keil工程文件及少量PDF手册覆盖内核调度、驱动适配与库文件链接等模块。资源内含完整的MDK工程可直接在uVision环境下编译调试并配有FPU设置说明与使用指南帮助读者理解任务创建、优先级管理、信号量及事件标志组等机制。目前已有1146人学习下载适合作为实时控制系统开发与移植实践的参考工程。1. ucosii在STM32F407上的移植从源码到可烧录MDK工程手里有一块 STM32F407 的开发板想跑 ucosii但网上找到的移植代码要么缺文件、要么编译报一堆错、要么下载进去直接 HardFault。这个场景我遇到过不止一次。ucosii 在 STM32F407 芯片上的移植代码完整 MDK 工程核心要解决的就是三件事内核与 Cortex-M4 的接口适配、时钟节拍的配置、以及中断优先级分组和 PendSV/SysTick 的接管。适合已经能点亮 LED、会用 Keil MDK 建工程但还没把 RTOS 跑起来的嵌入式开发者。下面按我实际移植的顺序把每一步拆开讲。2. 移植前必须想清楚的三个选型问题2.1 为什么是 ucosii 而不是 FreeRTOS先说选型。2024 年了还移植 ucosii很多人第一反应是这玩意是不是过时了。从工程角度讲选 ucosii 通常有三个现实理由一是老项目延续代码库里全是 OSTaskCreate、OSSemPend 这类 API换 RTOS 等于重写二是 ucosii 源码规模小内核约 6000 行 C 加几百行汇编全部读一遍是可行的适合学习和深度定制三是它的抢占式调度逻辑清晰没有 FreeRTOS 那么多编译期配置宏出问题时排查路径短。但代价也要说清楚ucosii 的生态和中间件支持远不如 FreeRTOS社区活跃度低遇到冷门问题基本靠自己啃源码。所以如果你的项目是全新开始、需要大量现成组件FreeRTOS 或 RT-Thread 更省事。选 ucosii 的前提是你接受自己维护内核这件事。2.2 Cortex-M4 移植需要改哪几个文件ucosii 官方源码是平台无关的移植到 STM32F407Cortex-M4 内核需要动的是与处理器相关的部分。常见做法是把移植层单独放一个目录我一般命名为UCOSII/PORT里面放这几个文件文件作用是否必须修改os_cpu.h数据类型定义、临界区宏、堆栈增长方向必须os_cpu_c.c任务堆栈初始化、钩子函数必须os_cpu_a.asmPendSV、SysTick 汇编处理必须os_cfg.h内核功能裁剪配置按需includes.h全局头文件汇总必须其中os_cpu_a.asm是移植的核心也是新手最容易翻车的地方。Cortex-M4 的 PendSV 异常是任务切换的入口SysTick 是节拍源这两个异常的汇编处理必须和启动文件里的向量表对应上。2.3 时钟节拍和优先级分组的取舍STM32F407 主频常见配置到 168MHz。ucosii 的节拍一般设 1000Hz也就是 1ms 一个 tick对应 SysTick 重装载值 168000000/1000 - 1。节拍太高会增加调度开销太低则时间分辨率不够1ms 是大多数项目的平衡点。优先级分组必须设成 NVIC_PriorityGroup_4也就是全部 4 位用于抢占优先级没有子优先级。原因很直接ucosii 用抢占优先级来判断任务优先级如果分组里留了子优先级位会出现两个任务抢占优先级相同但子优先级不同的情况而 ucosii 不支持同优先级多任务的时间片轮转除非开特定配置调度行为会变得难以预测。这个坑我在早期项目里踩过现象是任务偶尔不切换查了两天才定位到分组配置。3. 从零搭一个能编译的 MDK 工程3.1 工程目录结构和文件拷贝先建目录。我习惯的结构是这样清晰且方便后续换芯片Project/ ├── USER/ │ ├── main.c │ ├── stm32f4xx_it.c │ └── stm32f4xx_conf.h ├── FWLIB/ (标准外设库或HAL) ├── CORE/ (CMSIS core_cm4.h) ├── UCOSII/ │ ├── SOURCE/ (ucosii内核源码) │ ├── PORT/ (移植层) │ └── CONFIG/ (os_cfg.h, includes.h) └── OBJ/ (编译输出)把 ucosii 内核源码os_core.c、os_task.c、os_time.c 等放进 SOURCE移植层文件放进 PORT。注意os_cpu_a.asm在 MDK 里要作为汇编文件加入不能当 C 文件处理否则编译报错。3.2 os_cpu.h 里三个关键宏os_cpu.h是移植的第一道关。核心是这几个定义/* 临界区宏关中断和开中断 */ #define OS_CRITICAL_METHOD 3 #define OS_ENTER_CRITICAL() {cpu_sr __get_PRIMASK(); __disable_irq();} #define OS_EXIT_CRITICAL() {__set_PRIMASK(cpu_sr);} /* 堆栈增长方向Cortex-M 是满递减栈 */ #define OS_STK_GROWTH 1 /* 数据类型Cortex-M4 是 32 位 */ typedef unsigned int OS_STK; typedef unsigned int OS_CPU_SR;逻辑说明OS_CRITICAL_METHOD选 3 表示用保存和恢复 PRIMASK 的方式进出临界区这是 Cortex-M 上最稳妥的做法比直接关中断再无条件开中断安全因为它能处理临界区嵌套。OS_STK_GROWTH设 1 表示堆栈从高地址向低地址增长Cortex-M 的满递减栈就是这个方向设错了任务一启动就崩。OS_STK必须是 32 位因为 M4 的寄存器宽度是 32 位。参数说明__get_PRIMASK和__disable_irq是 CMSIS 提供的内联函数需要包含core_cm4.h。如果你的工程用的是 HAL 库这些函数同样可用不用额外引入。3.3 os_cpu_c.c 里任务堆栈初始化怎么写任务第一次被调度时需要伪造一个刚被中断打断的现场让 PendSV 能正确恢复。这个函数是OSTaskStkInitOS_STK *OSTaskStkInit(void (*task)(void *pd), void *p_arg, OS_STK *ptos, INT16U opt) { OS_STK *stk; stk ptos; /* 指向栈顶 */ *(stk) (OS_STK)0x01000000L; /* xPSRThumb位置1 */ *(--stk) (OS_STK)task; /* PC任务入口 */ *(--stk) (OS_STK)0xFFFFFFFEL; /* LR返回地址 */ *(--stk) (OS_STK)0x12121212L; /* R12 */ *(--stk) (OS_STK)0x03030303L; /* R3 */ *(--stk) (OS_STK)0x02020202L; /* R2 */ *(--stk) (OS_STK)0x01010101L; /* R1 */ *(--stk) (OS_STK)p_arg; /* R0任务参数 */ /* 手动保存的寄存器 R4-R11 */ *(--stk) (OS_STK)0x11111111L; /* R11 */ *(--stk) (OS_STK)0x10101010L; /* R10 */ *(--stk) (OS_STK)0x09090909L; /* R9 */ *(--stk) (OS_STK)0x08080808L; /* R8 */ *(--stk) (OS_STK)0x07070707L; /* R7 */ *(--stk) (OS_STK)0x06060606L; /* R6 */ *(--stk) (OS_STK)0x05050505L; /* R5 */ *(--stk) (OS_STK)0x04040404L; /* R4 */ return stk; }逻辑说明Cortex-M4 进入异常时硬件自动压栈 8 个寄存器xPSR、PC、LR、R12、R3-R0软件需要手动保存 R4-R11。这个函数就是按这个顺序在任务栈里预置好这些值。xPSR 的 bit24 必须为 1表示 Thumb 状态否则取指令会出错。PC 填任务入口地址R0 填任务参数这样任务函数第一次执行时就能拿到正确的参数。参数说明task是任务函数指针p_arg是传给任务的参数ptos是任务栈顶指针opt是选项ucosii 里一般传 0。返回值是新的栈顶指针ucosii 会把它存到任务控制块里。3.4 os_cpu_a.asm 里 PendSV 和 SysTick 的写法这是移植里最需要小心的地方。MDK 的汇编语法和 GCC 不同用 ARM 汇编器格式PendSV_Handler CPSID I ; 关中断 MRS R0, PSP ; 取当前任务栈指针 CBZ R0, PendSV_NoSave ; 如果是第一次跳过保存 STMFD R0!, {R4-R11} ; 手动保存R4-R11 LDR R1, OSTCBCur ; 取当前TCB指针 LDR R1, [R1] STR R0, [R1] ; 保存栈指针到TCB PendSV_NoSave LDR R0, OSTaskSwHook ; 调用钩子 BLX R0 LDR R0, OSPrioCur ; 更新优先级 LDR R1, OSPrioHighRdy LDRB R2, [R1] STRB R2, [R0] LDR R0, OSTCBCur ; 更新TCB LDR R1, OSTCBHighRdy LDR R1, [R1] STR R1, [R0] LDR R0, [R1] ; 取新任务栈指针 LDMFD R0!, {R4-R11} ; 恢复R4-R11 MSR PSP, R0 ; 更新PSP ORR LR, LR, #0x04 ; 确保返回后使用PSP CPSIE I ; 开中断 BX LR ; 返回硬件自动恢复剩余寄存器逻辑说明PendSV 是任务切换的实际执行者。进入时先关中断取当前任务的 PSP保存 R4-R11 到任务栈把栈指针存回 TCB。然后调用钩子函数更新当前优先级和 TCB 指针从新任务的 TCB 里取出栈指针恢复 R4-R11更新 PSP最后开中断返回。硬件会自动恢复 xPSR、PC 等 8 个寄存器从而跳到新任务继续执行。参数说明OSTCBCur、OSTCBHighRdy、OSPrioCur、OSPrioHighRdy都是 ucosii 的全局变量名字不能改。OSTaskSwHook是用户钩子可以留空函数。注意CBZ指令判断 PSP 是否为 0用于处理第一次任务切换时没有现场可保存的情况。SysTick 处理相对简单调用OS_CPU_SysTickHandler即可节拍中断里做 OSTimeTick。3.5 启动文件和中断向量表的对接STM32F407 的启动文件startup_stm32f40_41xxx.s里向量表默认把 PendSV_Handler 和 SysTick_Handler 指向弱定义。ucosii 的移植层里已经定义了同名函数链接时会覆盖弱定义。但要注意如果你用的是标准外设库的stm32f4xx_it.c里面可能也定义了 SysTick_Handler会造成重复定义。解决办法是把stm32f4xx_it.c里的 SysTick_Handler 删掉或改名让 ucosii 的版本生效。另外os_cpu_a.asm里的函数名必须和向量表里的名字完全一致MDK 下是PendSV_Handler和SysTick_Handler大小写敏感。4. 编译链接和第一次跑起来的验证方法4.1 常见编译错误的定位思路第一次编译大概率会报错按这个顺序排查undefined symbol OSTCBCur说明 ucosii 内核源码没加全检查 os_core.c、os_task.c 是否在工程里。undefined symbol PendSV_Handler汇编文件没加入工程或者汇编文件的后缀不是.s。multiply defined SysTick_Handlerstm32f4xx_it.c和移植层重复定义删掉前者。cannot open source input file includes.h头文件路径没加在 MDK 的 C/C 选项里把 UCOSII/CONFIG 加进 Include Paths。4.2 用两个任务验证调度是否正常编译通过后先别急着上复杂功能。写两个最简单的任务一个翻转 LED一个串口打印验证调度器真的在切换#include includes.h OS_STK Task1Stk[128]; OS_STK Task2Stk[128]; void Task1(void *p_arg) { while(1) { GPIO_ToggleBits(GPIOF, GPIO_Pin_9); /* 翻转LED */ OSTimeDlyHMSM(0, 0, 0, 500); /* 延时500ms */ } } void Task2(void *p_arg) { while(1) { printf(task2 running\r\n); OSTimeDlyHMSM(0, 0, 0, 1000); } } int main(void) { GPIO_InitTypeDef gpio; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_OUT; gpio.GPIO_OType GPIO_OType_PP; gpio.GPIO_Speed GPIO_Speed_100MHz; GPIO_Init(GPIOF, gpio); OSInit(); OSTaskCreate(Task1, 0, Task1Stk[127], 5); OSTaskCreate(Task2, 0, Task2Stk[127], 6); OSStart(); return 0; }逻辑说明OSInit初始化内核OSTaskCreate创建两个任务优先级分别是 5 和 6数字越小优先级越高。OSStart启动调度器从此不再返回。Task1 每 500ms 翻转一次 LEDTask2 每 1000ms 打印一次。如果 LED 在闪、串口有输出说明移植基本成功。参数说明任务栈大小 128 个 OS_STK即 512 字节对简单任务够用。栈顶传Task1Stk[127]是因为 Cortex-M 栈是满递减栈顶是数组最后一个元素。优先级 5 和 6 都低于系统任务不会冲突。4.3 用示波器看节拍是否准确LED 闪烁只能说明调度在跑不能说明节拍准确。用示波器测 LED 引脚周期应该是 1 秒500ms 高 500ms 低。如果周期明显偏大或偏小检查 SysTick 重装载值。168MHz 下 1ms 节拍的重装载值应该是 167999不是 168000。差 1 会导致节拍累积误差。5. 移植过程中最容易翻车的五个坑5.1 现象任务创建成功但一个都不执行原因OSStart之前没有正确设置OSPrioHighRdy和OSTCBHighRdy或者OS_CPU_SysTickInit没调用节拍中断没起来。解决确认OSStartHighRdy里调用了OS_CPU_SysTickInit并且 SysTick 的优先级设成最低0xFF。5.2 现象进 HardFault栈回溯指向 PendSV原因OSTaskStkInit里 xPSR 的 Thumb 位没置 1或者 R4-R11 的保存顺序和汇编里恢复顺序不一致。解决对照本文 3.3 和 3.4 的代码确认压栈和出栈顺序完全对称。xPSR 必须是 0x01000000。5.3 现象串口打印乱码或丢数据原因在中断里调用了 ucosii 的 API 但没有用OSIntEnter/OSIntExit包裹导致中断嵌套时内核状态错乱。解决所有中断服务函数里如果调用了OSSemPost、OSQPost等必须在入口调OSIntEnter()出口调OSIntExit()。5.4 现象任务延时不准越跑越偏原因SysTick 重装载值算错或者节拍中断被高优先级中断长时间阻塞。解决重装载值 主频/节拍 - 1。同时检查有没有中断执行时间超过 1ms如果有把该中断的优先级调低或缩短处理时间。5.5 现象MDK 编译通过但下载后不运行原因分散加载文件.sct里栈空间分配太小或者启动文件里的堆栈设置和 ucosii 冲突。解决在启动文件里把 Stack_Size 改大至少 0x400Heap_Size 设 0ucosii 用自己的内存管理确认分散加载文件没有把 ucosii 的段放到错误位置。6. 让移植工程更耐用的几个进阶习惯移植跑通只是开始真正让工程耐用的是几个细节习惯。第一个是节拍钩子OSTimeTickHook我一般在这里放一个空闲计数用来统计 CPU 负载。原理很简单在钩子里对一个全局变量累加主循环里定期读取并清零就能估算出空闲比例。这个数据在调优任务优先级时很有用。第二个是任务栈的监控。ucosii 提供OSTaskStkChk可以检测任务栈的实际使用量。我习惯在调试阶段每个任务都调一次把结果打印出来然后按实际用量的 1.5 倍重新分配栈大小。栈给多了浪费 RAM给少了就是随机崩溃这个平衡只能靠实测。第三个是中断优先级的统一规划。Cortex-M4 有 16 个优先级ucosii 的OS_CPU_SysTickInit会把 SysTick 和 PendSV 设成最低。用户中断的优先级必须高于这两个但又要考虑中断嵌套的深度。我的做法是画一张优先级分配表把每个中断的优先级写清楚避免后期加功能时冲突。最后一个习惯是保留一份最小可运行工程作为基线。每次改配置或加功能前先确认基线能跑出问题时可以快速对比。这个习惯帮我省过很多次改了一堆东西不知道哪里错了的时间。移植 ucosii 这件事跑通一次不难难的是让它稳定运行在各种边界条件下。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

AI替代方程求解:从符号演算到数据驱动的求解范式迁移

AI替代方程求解:从符号演算到数据驱动的求解范式迁移

1. 这不是“解方程”,而是重构问题求解的底层逻辑“用 AI 替代方程求解”——看到这个标题,我第一反应是皱眉。不是因为它夸张,而是因为它太轻描淡写。它像说“用微波炉替代灶台炒菜”,听起来只是换了个工具,但实际背后…

2026/10/14 11:10:19 阅读更多 →
短链接系统设计:高并发跳转、ID生成与实时风控实战

短链接系统设计:高并发跳转、ID生成与实时风控实战

1. 短链接不是“把长网址变短”那么简单很多人第一次听说短链接,脑子里浮现的是一串像t.cn/AbC123或bit.ly/XyZ789这样的字符——点开后跳转到原始网页。于是下意识觉得:“这不就是个字符串映射?用个哈希函数把长URL转成6位随机码&#xff0c…

2026/10/14 11:10:19 阅读更多 →
WorkBuddy 官方7条建议实测

WorkBuddy 官方7条建议实测

一、先学会选模式:别一上来就让它干活 WorkBuddy 有三种模式,很多人只用默认那种——直接开干。 问题在于,你一句话说不清楚要什么,它就得猜。猜错了,你反攻(重新描述需求)的时间,比…

2026/10/14 11:10:19 阅读更多 →

最新新闻

快速阅读不靠眼快:筛选与理解的实用框架

快速阅读不靠眼快:筛选与理解的实用框架

经常有人问我:“你读书怎么那么快?是不是一目十行、走马观花?”说实话,我刚开始试水快速阅读的时候也踩过不少坑,最典型的表现是字都看完了,合上书一问三不知。后来我花了很长时间去拆解“快速阅读”和“快…

2026/10/14 18:20:44 阅读更多 →
存量市场服务升级:低风险高回报的商业模式

存量市场服务升级:低风险高回报的商业模式

1. 为什么说"稳妥生意"比创业更值得关注最近三年,我身边有37位朋友尝试创业,最终存活下来的只有3家。这个残酷的数字让我开始思考:对于大多数普通人来说,是否存在着比创业风险更低、回报更稳定的商业机会?经…

2026/10/14 18:20:44 阅读更多 →
Decepticon 实战手册:EtherNet/IP + CIP 协议攻击全流程(44818/2222 端口下的 PLC 侦察、标签读写与模式切换)

Decepticon 实战手册:EtherNet/IP + CIP 协议攻击全流程(44818/2222 端口下的 PLC 侦察、标签读写与模式切换)

【免费下载链接】Decepticon Autonomous Hacking Agent for Red Team 项目地址: https://gitcode.com/gh_mirrors/de/Decepticon 点击查看 免费下载 本指南基于 Decepticon 仓库中 enip-cip 技能文档 编写,系统讲解针对北美工业控制系统(ICS…

2026/10/14 18:20:44 阅读更多 →
中点画圆算法详解:光栅化生成完美圆的数学原理与代码实现

中点画圆算法详解:光栅化生成完美圆的数学原理与代码实现

圆的生成算法,是计算机图形学课程里最容易被低估的一个实验。很多同学看到“画圆”两个字,第一反应是直接调用窗口库里的画圆函数,几行代码就结束。但实验真正要你做的,是把它当成一道光栅化算法题:输入圆心坐标、半径…

2026/10/14 18:20:44 阅读更多 →
把“学术导航仪”装进书匠策AI:一篇期刊论文的“造路”逻辑

把“学术导航仪”装进书匠策AI:一篇期刊论文的“造路”逻辑

官网直达:www.shujiangce.com 微信公众号搜一搜:书匠策AI 写期刊论文最折磨人的,从来不是“写”这个动作本身。 是你盯着空白文档,知道要做文献综述,却不知道从哪篇开始;知道要有理论框架,却拿…

2026/10/14 18:20:44 阅读更多 →
插件加载失败排查:从生命周期到激活机制,一篇搞定

插件加载失败排查:从生命周期到激活机制,一篇搞定

先说一句,我带过好几个插件化改造的项目,也修过无数跟插件相关的启动报错,所以看到 "failed to load plugins web boot: 2 entries did not activate" 这种日志时,第一反应不是翻框架文档,而是看插件的激活方…

2026/10/14 18:19:43 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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/14 7:53:34 阅读更多 →