ML307R OpenCPU SDK开发实战:从环境搭建到避坑指南

简介:ML307R-OpenCpu-sdk包是面向物联网嵌入式开发者的二次开发套件,针对ML307R模组OpenCPU方案,帮助开发者跳过底层驱动搭建,直接在模组上运行应用程序,适用于智能表计、定位终端、工业网关等场景,需要具备C/C++与RTOS基础。压缩包为7z格式,共约2000个文件、235.49MB,其中1077个h与243个hpp头文件构成完整API与C++标准库接口,66个c源文件、100个o目标文件、40个a静态库及24个bin固件提供可编译的工程骨架,另有json配置、py脚本、pem证书、xml与bat构建脚本,覆盖编译、烧录与调试链路。目前已有1914人学习下载。资源包含SDK目录结构、库文件与示例工程,便于快速上手OpenCPU开发、理解模组资源调度与接口调用,并可作为二次开发与问题排查的参考底包。

1. 从一次模组侧业务迁移说起:ML307R OpenCPU SDK 到底能干什么

去年帮一个做工业数据采集的团队做技术预研,他们原本的架构是「MCU + 4G 模组」两颗芯片,MCU 跑业务逻辑,模组只负责拨号上网,两者用 AT 指令串口通信。这套方案稳定是稳定,但 BOM 成本压不下来,PCB 面积也卡得死。后来他们听说 ML307R 支持 OpenCPU,也就是把业务逻辑直接跑在模组内部的处理器上,省掉那颗外挂 MCU,于是让我帮忙评估一下这套 SDK 包到底能不能接。

ML307R 是一款 Cat.1 蜂窝通信模组,OpenCPU 是它的一种开发模式——模组内部除了跑通信协议栈,还开放了一部分 CPU 资源和外设接口给开发者,你可以用 C 语言在里面写自己的应用逻辑,直接操作 GPIO、UART、I2C、ADC 这些硬件资源,同时调用 SDK 提供的网络通信 API 完成数据收发。这个 SDK 包就是整套开发所需的头文件、静态库、编译脚本、示例代码和烧录工具的集合。

它适合谁?如果你正在做物联网终端、远程抄表、共享设备、车载追踪这类需要联网但业务逻辑不算特别复杂的场景,并且希望把硬件成本再压一压、把板子做小一圈,那这套东西值得花时间拆一拆。但如果你需要跑复杂的 GUI、大量浮点运算或者多任务实时调度,那 OpenCPU 的资源边界可能会让你难受。下面我从 SDK 的目录结构开始,一步步拆到能跑通第一个程序。

2. 拆开 SDK 包:目录结构、编译链路与第一个可烧录固件

2.1 拿到包之后先看什么

解压之后不要急着编译,先花十分钟把目录结构摸清楚。典型的 ML307R OpenCPU SDK 包大致长这样:

ml307r_opencpu_sdk/ ├── api/ # 对外暴露的头文件,按模块分目录 │ ├── hal/ # 硬件抽象层:GPIO、UART、I2C、SPI、ADC │ ├── network/ # 网络通信:socket、MQTT、HTTP、FOTA │ ├── os/ # 任务、信号量、消息队列、定时器 │ └── utils/ # 字符串、内存、日志等工具函数 ├── lib/ # 预编译静态库,核心协议栈和驱动都在这里 ├── demo/ # 官方示例,按功能分目录 │ ├── hello_world/ │ ├── tcp_client/ │ ├── mqtt_demo/ │ └── gpio_interrupt/ ├── build/ # 编译脚本和工具链配置 │ ├── Makefile │ ├── toolchain.mk │ └── output/ # 编译产物目录 ├── tools/ # 烧录工具、固件打包脚本 └── docs/ # 文档,通常有 API 手册和入门指南

重点看三个地方:api/决定了你能调用哪些能力,demo/决定了你上手的速度,build/决定了你能不能顺利编译出固件。很多新手拿到包直接翻lib/,那里面全是.a文件,看不出什么东西。

2.2 编译环境搭建与工具链配置

ML307R 内部用的是 ARM Cortex 系列内核,SDK 包通常会附带或指定交叉编译工具链。我一般会先确认工具链版本,再配环境变量。

# 假设工具链解压在 /opt/toolchain 下 export TOOLCHAIN_PATH=/opt/toolchain/bin export PATH=$TOOLCHAIN_PATH:$PATH # 验证交叉编译器是否可用 arm-none-eabi-gcc --version # 进入 SDK 的 build 目录 cd ml307r_opencpu_sdk/build # 查看 Makefile 里默认的编译目标 make help

这里有几个参数需要留意。toolchain.mk里一般会定义CROSS_COMPILE前缀,如果你的工具链命名不一致,编译会直接报command not found。另外Makefile里通常有TARGET变量,用来区分不同的模组型号或硬件版本,选错了编出来的固件烧进去可能跑不起来。

编译命令本身不复杂:

# 清理之前的编译产物 make clean # 编译 demo 中的 hello_world make demo=hello_world # 编译成功后产物在 output/ 目录下 ls -la output/

编译产物一般是一个.bin或.pac格式的固件包。.pac是展锐平台常见的打包格式,里面包含了应用固件和必要的配置文件。

2.3 烧录与串口验证

烧录工具在tools/目录下,通常是 Windows 端的 exe,也有 Linux 下的命令行版本。硬件上需要把模组的 USB 或 UART 接到电脑,进入下载模式。

烧录步骤大致是:选择固件文件 → 选择串口号 → 点击下载 → 等待进度条走完 → 模组自动重启。烧录成功后,用串口工具打开模组的日志串口,波特率一般是 115200,你应该能看到类似这样的输出:

[INFO] OpenCPU app start [INFO] hello_world running... [INFO] tick: 1000ms

如果串口没有任何输出,先检查三件事:波特率对不对、TX/RX 有没有接反、模组有没有正常上电。这三个问题占了新手翻车原因的八成以上。

提示:第一次烧录建议先用官方 demo 验证整条链路,不要一上来就烧自己写的代码。demo 跑通了,说明工具链、烧录、串口都没问题,后面出问题就只需要排查业务代码。

3. 在 OpenCPU 里写业务代码:GPIO、UART 与网络通信的实操路径

3.1 任务模型与代码入口

OpenCPU 的代码结构和裸机开发不太一样,它跑在一个轻量级 RTOS 之上,你的代码通常从一个入口函数开始,然后创建任务、注册回调。典型的入口长这样:

#include "ml307r_os.h" #include "ml307r_log.h" // 任务栈大小和优先级根据业务复杂度调整 #define APP_TASK_STACK_SIZE 4096 #define APP_TASK_PRIORITY 5 static void app_task_entry(void *param) { ML307R_LOG_INFO("app task started"); while (1) { // 业务主循环 ml307r_os_task_sleep(1000); } } int app_main(void) { ml307r_os_task_t task; // 创建应用任务 if (ml307r_os_task_create(&task, "app", app_task_entry, NULL, APP_TASK_STACK_SIZE, APP_TASK_PRIORITY) != 0) { ML307R_LOG_ERROR("task create failed"); return -1; } return 0; }

app_main是 SDK 约定的应用入口,系统启动后会调用它。ml307r_os_task_create的参数依次是任务句柄指针、任务名、入口函数、传给入口函数的参数、栈大小(单位通常是字节)、优先级。栈大小给太小会导致栈溢出,表现为任务跑着跑着就挂了或者数据莫名其妙被改写,这个坑我在早期调试时踩过不止一次。

3.2 GPIO 与 UART 的操作方式

GPIO 的操作比较直观,SDK 一般会提供初始化、写电平、读电平、注册中断这几个接口:

#include "ml307r_gpio.h" #define LED_PIN 12 void gpio_demo_init(void) { // 配置为输出模式,初始低电平 ml307r_gpio_init(LED_PIN, ML307R_GPIO_OUTPUT); ml307r_gpio_write(LED_PIN, 0); } void led_toggle(void) { static uint8_t state = 0; state = !state; ml307r_gpio_write(LED_PIN, state); }

UART 稍微复杂一点,因为涉及波特率、数据位、停止位、校验位这些参数,还要处理接收回调:

#include "ml307r_uart.h" #define UART_PORT 1 #define UART_BAUD 9600 static void uart_rx_callback(uint8_t *data, uint16_t len) { // 收到数据后直接回显,实际业务里做协议解析 ml307r_uart_send(UART_PORT, data, len); } void uart_demo_init(void) { ml307r_uart_config_t cfg = { .baudrate = UART_BAUD, .data_bits = ML307R_UART_DATA_8BIT, .stop_bits = ML307R_UART_STOP_1BIT, .parity = ML307R_UART_PARITY_NONE, }; ml307r_uart_init(UART_PORT, &cfg); ml307r_uart_register_rx_callback(UART_PORT, uart_rx_callback); }

这里有个容易忽略的点:接收回调是在中断上下文里执行的,里面不要做耗时操作,也不要用可能阻塞的 API。正确做法是把数据拷到一个缓冲区,然后发消息给任务去处理。我见过有人在回调里直接调 socket 发送,结果系统直接卡死。

3.3 网络通信:从 TCP 到 MQTT

网络能力是 OpenCPU 的核心价值。SDK 通常封装了 socket 接口,用法和标准 BSD socket 很像:

#include "ml307r_socket.h" void tcp_client_demo(void) { int fd = ml307r_socket_create(ML307R_AF_INET, ML307R_SOCK_STREAM, 0); if (fd < 0) { ML307R_LOG_ERROR("socket create failed"); return; } ml307r_sockaddr_t addr = { .sin_family = ML307R_AF_INET, .sin_port = ml307r_htons(8080), }; // 域名解析 ml307r_dns_resolve("example.server.com", &addr.sin_addr); if (ml307r_socket_connect(fd, &addr, sizeof(addr)) != 0) { ML307R_LOG_ERROR("connect failed"); ml307r_socket_close(fd); return; } const char *msg = "hello from opencpu"; ml307r_socket_send(fd, msg, strlen(msg), 0); char buf[256] = {0}; int ret = ml307r_socket_recv(fd, buf, sizeof(buf) - 1, 0); if (ret > 0) { ML307R_LOG_INFO("recv: %s", buf); } ml307r_socket_close(fd); }

如果业务用 MQTT,SDK 里一般也有封装好的客户端。参数上重点注意 keepalive 间隔和重连策略,网络抖动时这两个参数直接决定了设备能不能自己恢复。

注意:OpenCPU 的网络 API 大多是阻塞式的,在任务里调用时记得设置合理的超时,否则一个网络异常可能把整个任务卡住。

4. 避坑与排查:OpenCPU 开发中最容易翻车的五个地方

4.1 编译通过但烧录后无输出

现象:编译零报错,烧录工具提示成功,但串口没有任何日志。

原因:最常见的是烧录的固件版本和模组硬件版本不匹配,或者烧录时没有勾选正确的分区配置。另一个可能是日志串口选错了——有些模组有两个串口,一个专用于日志,一个用于 AT 通信。

解决:先确认模组型号和 SDK 版本对应关系,再检查烧录工具里的分区选项。用官方 demo 固件烧一遍,如果 demo 有输出而你的没有,那就是代码问题;如果 demo 也没输出,那就是硬件或烧录配置问题。

4.2 任务栈溢出导致随机崩溃

现象:程序运行一段时间后死机,或者某些变量值莫名其妙被改变。

原因:创建任务时栈大小给得不够,任务里的局部变量、函数调用深度超出了栈空间。

解决:把任务栈调大试试,比如从 2048 调到 4096 或 8192。SDK 一般有栈使用量查询接口,可以在调试阶段打印出来看实际用了多少。另外避免在任务里定义大数组,改用静态分配或堆分配。

4.3 中断回调里做阻塞操作

现象:系统响应变慢,严重时看门狗复位。

原因:在 GPIO 中断或 UART 接收回调里调用了可能阻塞的 API,比如 socket 发送、信号量等待、延时函数。

解决:中断回调里只做数据拷贝和消息投递,把实际处理逻辑放到任务里。这是 RTOS 开发的基本原则,但 OpenCPU 新手很容易忽略。

4.4 网络断开后无法自动恢复

现象:设备运行几天后失联,重启才能恢复。

原因:没有处理网络异常事件,socket 断开后没有重连逻辑,或者 MQTT keepalive 设置过长导致服务端已经踢掉连接但设备还不知道。

解决:注册网络状态回调,在断开事件里触发重连。MQTT 的 keepalive 建议设在 60 到 120 秒之间,并且实现心跳失败后的重连机制。重连要有退避策略,不要断开后立刻疯狂重连。

4.5 固件大小超出分区限制

现象:编译成功但打包时报错,提示固件超出分区大小。

原因:OpenCPU 的应用分区空间有限,引入太多功能或者开了高优化等级之外的调试信息,很容易超。

解决:编译时开-Os优化,去掉不必要的调试打印,检查是否有重复引入的库。如果确实功能多,看看 SDK 是否支持外挂 Flash 扩展。

5. 进阶技巧:用日志分级和内存池把调试效率提上去

5.1 日志分级与条件编译

SDK 的日志接口一般支持分级,但很多人所有日志都用INFO级别,导致串口刷屏,真正有用的信息被淹没。我的习惯是分三层:ERROR只用于不可恢复的错误,WARN用于可恢复但需要关注的情况,INFO用于关键流程节点。调试信息用DEBUG级别,并且在发布固件时通过宏关掉。

// 在工程配置里定义日志级别 #define ML307R_LOG_LEVEL ML307R_LOG_LEVEL_INFO // 发布版本直接关掉 DEBUG #ifdef RELEASE_BUILD #undef ML307R_LOG_DEBUG #define ML307R_LOG_DEBUG(...) #endif

这样发布固件时日志量能减少七八成,串口干净了,排查问题反而更快。

5.2 用内存池替代频繁 malloc/free

OpenCPU 的内存资源比较紧张,频繁动态分配容易产生碎片,跑久了就可能分配失败。对于固定大小的数据结构,我一般会自己维护一个简单的内存池:

#define POOL_ITEM_SIZE 64 #define POOL_ITEM_COUNT 16 static uint8_t mem_pool[POOL_ITEM_SIZE * POOL_ITEM_COUNT]; static uint8_t pool_used[POOL_ITEM_COUNT]; void *pool_alloc(void) { for (int i = 0; i < POOL_ITEM_COUNT; i++) { if (!pool_used[i]) { pool_used[i] = 1; return &mem_pool[i * POOL_ITEM_SIZE]; } } return NULL; } void pool_free(void *ptr) { if (!ptr) return; int index = ((uint8_t *)ptr - mem_pool) / POOL_ITEM_SIZE; if (index >= 0 && index < POOL_ITEM_COUNT) { pool_used[index] = 0; } }

这个池子实现很简单,但能避免大部分碎片问题。参数上POOL_ITEM_SIZE按你最大的结构体来定,POOL_ITEM_COUNT按并发峰值来定,宁可多留几个也不要刚好卡满。

5.3 固件升级的验证清单

FOTA 是 OpenCPU 项目绕不开的功能,但升级失败变砖的风险也真实存在。我每次做 FOTA 之前会强制走一遍这个清单:

检查项确认内容
分区表升级分区和运行分区是否独立
回滚机制升级失败后能否回退到旧固件
断电测试升级过程中断电,重新上电后能否恢复
版本校验固件版本号是否写入正确,服务端能否识别
升级超时下载超时和写入超时是否设置合理

这张表帮我挡过好几次事故。有一次就是回滚机制没配好,测试时升级包传错了,设备直接卡在 bootloader 里,最后只能拆机短接才救回来。从那以后我每次做 FOTA 都强制走一遍这个清单,一个都不跳过。

希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

四个编码器读写软件:参数配置与诊断实战指南

四个编码器读写软件:参数配置与诊断实战指南

简介&#xff1a;这份资源面向工业自动化工程师、设备调试人员及SICK编码器使用者&#xff0c;汇集了4套官方编码器读写与配置软件&#xff0c;重点解决基于HIPERFACE协议设备的通信、参数设置与故障诊断问题。压缩包共57个文件&#xff0c;约28.61MB&#xff0c;以dll动态库、…

2026/10/20 8:00:21 阅读更多 →
训练 AI 的数据去哪里找?开源数据集与商用数据渠道汇总

训练 AI 的数据去哪里找?开源数据集与商用数据渠道汇总

在人工智能技术飞速发展的今天&#xff0c;训练 AI 的数据去哪里找已成为企业与研发团队面临的核心命题。虽然开源数据集为早期研究提供了便利&#xff0c;但在面对垂直领域深耕、模型商业化落地及版权合规要求时&#xff0c;其局限性日益凸显。构建高质量、合规的AI模型&#…

2026/10/21 12:10:31 阅读更多 →
CAT12 VBM全流程实战:从安装配置到多模态形态学建模

CAT12 VBM全流程实战:从安装配置到多模态形态学建模

简介&#xff1a;本资源是面向神经影像学研究者与计算神经科学初学者的CAT12工具箱完整安装与分析套件&#xff0c;专为基于体素的形态计量学&#xff08;VBM&#xff09;研究设计&#xff0c;解决sMRI数据预处理、组织分割、统计建模与结果可视化等核心问题。包内共518个文件&…

2026/10/21 12:32:37 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年&#xff0c;我大部分时间都在跟浏览器标签页打交道&#xff0c;直到第一次把桌面应用跑起来&#xff0c;才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词&#xff0c;最开始是在某段时间频繁出现在同事的讨论里&#xff0c;一个用HTML…

2026/10/21 9:32:40 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档&#xff0c;主要面向智能锁安装人员、终端用户及售后维护者&#xff0c;帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件&#xff0c;大小4.37MB&#xff0c;内容结构清晰&#xff…

2026/10/21 9:32:40 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统&#xff0c;整车要求空载跑到8m/s&#xff0c;电机转速已经拉到额定点&#xff0c;驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令&#xff0c;转速纹丝不动&#xff0c;三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/21 9:32:40 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年&#xff0c;用它写过嵌入式驱动、做过游戏服务端&#xff0c;也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词&#xff0c;我其实挺感慨的——这门语言的难点从来不是语法多&#xff0c;而是知识点之间的耦合太深…

2026/10/21 9:32:40 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书以PDF格式发布&#xff0c;适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开&#xff0c;先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能&#xff0c;再给出完整安装步骤&…

2026/10/21 9:32:40 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历&#xff1a;翻开源码注释、论文摘要或者一份专业报告&#xff0c;每个字都认识&#xff0c;连在一起却像天书。想找人聊聊&#xff0c;身边要么没人懂&#xff0c;要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型&#xff0c;几秒钟之后&a…

2026/10/21 9:31:32 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →