嵌入式RTOS线程间通信实战:CMSIS-RTOS2消息队列、信号量与互斥锁应用详解
1. 项目概述为什么嵌入式开发绕不开线程间通信在嵌入式系统开发里尤其是基于ARM Cortex-M这类资源受限的MCU一旦项目复杂度上来多线程或者说多任务几乎是必然选择。你可能会用FreeRTOS、ThreadX或者像我一样直接上ARM官方为Cortex-M系列量身定制的CMSIS-RTOS2接口层。但不管用哪个RTOS只要任务一多一个核心问题就摆在了面前这些并发的线程它们之间怎么“说话”怎么安全、高效地传递数据、同步状态、协调工作这就是“线程间通信”要解决的全部事情。我见过不少项目功能跑起来没问题但系统行为诡异比如某个按键响应时快时慢或者屏幕刷新偶尔会卡一下。深究下去十有八九是线程间通信没处理好。要么是数据在传递过程中被意外覆盖要么是某个任务在空等资源导致整个系统“饿死”。所以理解并熟练运用CMSIS-RTOS2提供的通信机制不是锦上添花而是构建稳定、可靠嵌入式系统的基石。它直接决定了你系统的实时性、可靠性和可维护性。CMSIS-RTOS2本身不是一个具体的RTOS它是一套标准化的API接口。你可以把它理解为一个“适配层”底层可能是Keil RTX5也可能是FreeRTOS但上层应用代码通过调用CMSIS-RTOS2的API来编写这样你的代码就具备了在不同RTOS间移植的能力。而线程间通信正是这套API里最核心、最常用的一组工具。接下来我们就抛开理论直接从实战角度拆解这些通信机制到底该怎么用以及背后那些容易踩坑的细节。2. 核心通信机制全解析不止是队列和信号量很多人一提到线程通信脑子里就是队列和信号量。这没错它们是中流砥柱但CMSIS-RTOS2提供的工具箱更丰富。理解每种工具的适用场景比死记API更重要。2.1 消息队列数据传递的“高速公路”消息队列是线程间传递数据块最直接的方式。你可以把它想象成一个带格子的流水线生产者线程把数据包消息放入格子消费者线程按顺序取出。CMSIS-RTOS2的队列是FIFO的保证了顺序。创建队列的关键在于两个参数msg_count和msg_size。osMessageQueueId_t myQueue; myQueue osMessageQueueNew(10, sizeof(myData_t), NULL);这里10是队列深度即最多能缓存多少条消息sizeof(myData_t)是每条消息的字节数。这个myData_t通常是你自定义的结构体。实操心得一队列深度不是随便设的。设太小生产者太快时容易丢消息队列满osMessageQueuePut返回错误osErrorResource。设太大又浪费宝贵的RAM。我的经验是先分析生产者和消费者的最坏情况执行周期。比如生产者每10ms产生一条消息消费者最慢50ms处理一条。那么在消费者一次处理期间生产者最多能产生5条消息。队列深度至少设为5再加一点余量比如2设为7或8就比较安全。盲目设为32、64在内存紧张的MCU上是奢侈的。发送和接收消息myData_t txData { .sensorValue 123, .timestamp osKernelGetTickCount() }; osStatus_t status; status osMessageQueuePut(myQueue, txData, 0, 0); // 优先级0 不等待 if (status ! osOK) { // 处理发送失败可能是队列满了 } myData_t rxData; status osMessageQueueGet(myQueue, rxData, NULL, osWaitForever); // 无限等待消息 if (status osOK) { // 成功处理rxData }注意osMessageQueueGet的第四个参数这里是osWaitForever意味着如果队列为空这个线程会挂起等待直到有消息到来。这避免了消费者线程空转消耗CPU。你也可以指定一个超时时间比如100表示100个内核tick实现非阻塞查询。2.2 信号量资源计数与任务同步的“发令枪”信号量像一个计数器用来管理对共享资源如一段内存、一个外设的访问或者用于简单的任务同步。CMSIS-RTOS2提供了二值信号量和计数信号量。二值信号量只有0和1两个值常用于互斥访问或任务同步。比如一个SPI总线只能被一个任务使用osSemaphoreId_t spiMutex; spiMutex osSemaphoreNew(1, 1, NULL); // 初始计数为1最大计数为1任务在使用SPI前获取信号量if (osSemaphoreAcquire(spiMutex, 100) osOK) { // 等待最多100个tick // 安全地使用SPI // ... osSemaphoreRelease(spiMutex); // 一定要释放 } else { // 获取超时处理错误可能其他任务占用太久 }这里有个巨坑务必确保Acquire和Release成对出现并且在任何分支路径如函数提前返回、发生错误都要释放。否则会导致死锁整个系统卡住。我习惯在获取后立刻写释放的代码然后再填充中间的逻辑。计数信号量用于管理多个同类资源。比如你有3个ADC通道可以被多个任务复用osSemaphoreId_t adcChannels; adcChannels osSemaphoreNew(3, 3, NULL); // 初始3个资源都可用任务需要ADC时获取用完释放。如果所有通道都被占用后续任务会阻塞等待。实操心得二区分“互斥锁”和“二值信号量”。CMSIS-RTOS2有专门的互斥锁osMutex它和二值信号量关键区别在于“优先级继承”。当一个低优先级任务持有互斥锁时如果高优先级任务来申请系统会临时提升低优先级任务的优先级让它尽快执行完并释放锁从而减少高优先级任务的阻塞时间。而信号量没有这个机制。所以保护共享资源尤其是可能被多个优先级不同任务访问的资源优先使用互斥锁。信号量更适用于任务同步比如告诉另一个任务“某件事已发生”。2.3 事件标志组多条件等待的“瑞士军刀”事件标志组允许一个任务等待多个事件中的任意一个或全部发生。每个事件用一个位flag来表示非常节省资源。假设一个显示任务需要等待两种事件1有新的数据显示事件A2用户请求刷新屏幕事件B。osEventFlagsId_t displayEvents; displayEvents osEventFlagsNew(NULL); // 在数据生产者线程中设置事件A osEventFlagsSet(displayEvents, 0x01); // 设置bit0 // 在用户输入线程中设置事件B osEventFlagsSet(displayEvents, 0x02); // 设置bit1 // 在显示任务线程中等待任一事件发生 uint32_t flags; flags osEventFlagsWait(displayEvents, 0x01 | 0x02, osFlagsWaitAny, osWaitForever); if (flags 0x01) { // 处理新数据 } if (flags 0x02) { // 处理刷新请求 }osFlagsWaitAny表示等待任意一个指定事件发生。你也可以用osFlagsWaitAll等待所有事件同时发生。注意事项事件标志没有队列。如果在显示任务等待之前数据生产者已经设置了事件A那么这次设置会被“记住”显示任务一进来就会立刻得到这个标志并继续执行。但如果生产者连续快速设置两次事件A而显示任务只处理了一次那么第二次设置的效果会覆盖第一次因为位已经是1了你可能会“丢”掉一次事件。所以事件标志更适合通知“状态”的变化而不是计数。对于需要计数的场景还是用消息队列或信号量。2.4 互斥锁共享资源的“护身符”前面提到过互斥锁是保护共享资源的最佳选择。创建和使用很简单osMutexId_t sharedVarMutex; sharedVarMutex osMutexNew(NULL); osMutexAcquire(sharedVarMutex, osWaitForever); // 临界区安全地读写共享变量 g_sharedCounter; osMutexRelease(sharedMutex);核心要点保持临界区代码尽可能短。互斥锁持有的时间越长其他等待任务被阻塞的时间就越久会影响系统实时性。不要在临界区内做延时操作如osDelay或等待其他信号量这极易引起死锁。3. 实战架构设计构建一个数据采集与处理系统光讲机制太抽象我们设计一个模拟系统来串联运用。假设我们要做一个传感器数据采集系统任务ASensor_Task每50ms读取一次温度传感器模拟I2C操作。任务BProcess_Task对原始温度数据进行滤波处理。任务CDisplay_Task将处理后的数据刷新到LCD屏。任务DAlert_Task如果温度超过阈值通过LED和蜂鸣器报警。它们之间如何通信A - B原始数据传递。使用消息队列Queue_RAW因为传递的是结构化的传感器数据。B - C处理后的数据传递。同样使用消息队列Queue_PROCESSED。A - D报警触发。使用事件标志组。任务A检测到超温设置一个事件标志如ALERT_FLAG。任务D等待这个标志。为什么不用队列因为报警事件是一个“状态”通知且可能连续发生我们只关心“是否发生过”不需要计数事件标志更轻量。共享资源保护如果多个任务都可能读写一个全局的“系统状态”变量比如g_systemMode使用互斥锁。外设互斥假设I2C总线同时被任务A读温度和一个偶尔存在的任务E读写EEPROM共享使用互斥锁保护I2C底层驱动函数。系统初始化代码框架// 创建通信对象 osMessageQueueId_t Queue_RAW, Queue_PROCESSED; Queue_RAW osMessageQueueNew(5, sizeof(sensor_data_t), NULL); Queue_PROCESSED osMessageQueueNew(5, sizeof(processed_data_t), NULL); osEventFlagsId_t Event_Alert; Event_Alert osEventFlagsNew(NULL); osMutexId_t Mutex_I2C, Mutex_SystemState; Mutex_I2C osMutexNew(NULL); Mutex_SystemState osMutexNew(NULL); // 创建任务 osThreadNew(Sensor_Task, NULL, attr_A); osThreadNew(Process_Task, NULL, attr_B); // ... 其他任务Sensor_Task的关键部分void Sensor_Task(void *argument) { sensor_data_t raw_data; while(1) { osMutexAcquire(Mutex_I2C, osWaitForever); raw_data.value read_temperature_sensor(); // 模拟I2C读取 raw_data.timestamp osKernelGetTickCount(); osMutexRelease(Mutex_I2C); // 发送原始数据到处理队列 if (osMessageQueuePut(Queue_RAW, raw_data, 0, 0) ! osOK) { // 队列满可以增加错误计数或丢弃最旧数据 } // 检查报警 if (raw_data.value ALERT_THRESHOLD) { osEventFlagsSet(Event_Alert, ALERT_FLAG); } osDelay(50); // 50ms周期 } }这个架构清晰地将数据流队列和控制流事件、互斥分开每个通信对象职责单一易于调试和维护。4. 高级技巧与性能优化当系统复杂后一些进阶问题就会出现。4.1 优先级反转与死锁预防这是多线程系统的经典难题。优先级反转低优先级任务L持有锁中优先级任务M就绪不需求该锁抢占CPU导致高优先级任务H等待L释放锁而被阻塞结果中优先级任务M先于H执行。解决方案使用互斥锁的优先级继承特性CMSIS-RTOS2的互斥锁默认支持。死锁任务A持有锁X等待锁Y任务B持有锁Y等待锁X互相等待。解决方案固定顺序所有任务按相同顺序如先X后Y申请锁。超时机制申请锁时使用超时参数如osMutexAcquire(mutex, 100)超时后回退并释放已持有的锁。避免嵌套锁尽量只持有一个锁。如果必须多个仔细设计逻辑。4.2 内存管理与动态创建上面的例子都在系统初始化时静态创建了通信对象。CMSIS-RTOS2也支持动态创建和删除osMessageQueueDelete,osSemaphoreDelete等。但强烈不建议在任务运行周期内频繁动态创建和删除对象。原因动态分配malloc在小型RTOS中可能产生碎片导致后续分配失败。删除正被其他任务等待的对象会导致未定义行为。最佳实践在系统启动时main函数或第一个任务中一次性创建所有需要的通信对象。这虽然增加了初始内存开销但换来了整个运行期的确定性和可靠性。4.3 调试与问题排查线程通信出问题最难的是复现和定位。有几个实用方法使用调试器观察对象状态像Keil MDK这类IDE可以在调试时查看RTOS对象队列、信号量的内部状态比如队列中当前的消息数、信号量的计数值、哪些任务在等待等。这是最直接的武器。添加监控代码在osMessageQueuePut/Get、osSemaphoreAcquire/Release等调用前后增加计数器或日志输出通过串口。记录操作成功/失败、等待时间等。这能帮你发现“偶尔”出现的队列满、信号量获取超时等问题。系统负载分析使用CMSIS-RTOS2的osKernelGetTickCount来测量关键任务的执行时间和周期。如果某个任务执行时间波动很大很可能是在通信原语上阻塞等待了太久。简化与隔离当问题复杂时尝试注释掉部分任务和通信链路构建一个最小复现系统逐步添加功能直到问题出现。5. 常见问题与避坑指南实录这里记录了我踩过或见别人踩过的典型坑。问题1队列满了数据丢失怎么办现象生产者任务osMessageQueuePut频繁返回osErrorResource。排查检查队列深度是否足够。用调试器查看队列的msg_count最大容量和当前消息数。解决方案A推荐增加队列深度。但需权衡内存。方案B提高消费者任务优先级让它更快取走消息。方案C生产者任务在发送失败时可以选择丢弃最旧的一条消息先osMessageQueueGet一个不处理再放入新消息。这适用于“只关心最新数据”的场景如实时显示。方案D使用osMessageQueuePut的带超时版本让生产者阻塞等待一小段时间而不是立即失败。但这可能影响生产者的周期性。问题2系统运行一段时间后死锁所有任务停止。现象调试器显示所有任务状态都为osThreadBlocked。排查检查每个互斥锁的持有者。是否有任务持有锁但没有释放比如函数中途return了。检查是否存在循环等待A等B持有的锁B等A持有的锁。检查是否有任务在持有锁的同时调用了osDelay或等待另一个信号量。解决为所有Acquire操作配对Release并使用__try/__finally模式或确保函数所有出口都释放锁。统一锁的申请顺序。绝对避免在临界区内调用任何可能引起任务切换的API如osDelay,osMessageQueueGetwith wait,osSemaphoreAcquire。问题3使用事件标志组感觉偶尔会“丢”事件。现象生产者设置了两次标志但消费者只响应了一次。原因这不是“丢”是事件标志的特性。它只是一个位osEventFlagsSet只是把位设为1。如果消费者还没来得及等待和清除该标志生产者又设置了一次位还是1没有变化。解决如果需要计数请换用信号量或消息队列。事件标志只适合做状态通知比如“按键已按下”、“数据准备就绪”。问题4低优先级任务总得不到执行好像“饿死”了。现象低优先级任务如日志上传一直处于就绪态但很少被调度。排查检查高优先级任务是否在“忙等待”比如用while循环查询而非阻塞等待。这会独占CPU。解决确保所有任务在无事可做时都应调用阻塞式API如osDelay,osMessageQueueGet,osSemaphoreAcquire主动让出CPU。合理设置任务优先级。通信对象的生产者和消费者之间优先级设置需谨慎。通常让消费者优先级略高于或等于生产者可以避免队列堆积。问题5在中断服务程序中使用通信API。注意很多CMSIS-RTOS2 API有对应的“中断安全”版本函数名以FromISR结尾如osMessageQueuePut对应osMessageQueuePutFromISR。在ISR中必须使用FromISR版本因为ISR中不能进行任务调度。典型错误在串口接收中断中直接调用osMessageQueuePut将数据放入队列。这在高频中断下可能导致系统崩溃。正确做法void USART_IRQHandler(void) { if (USART_ReceiveData()) { char data USART-DR; osMessageQueuePutFromISR(uartQueue, data, 0, 0); // 注意FromISR版本可能需要处理上下文切换 // 如果该调用唤醒了更高优先级任务有些RTOS需要手动调用调度器挂起/恢复函数 // 具体需参考底层RTOS如FreeRTOS的说明。 } }线程间通信是嵌入式RTOS应用的骨架设计得好系统健壮流畅设计得不好则bug隐蔽难调。我的经验是初期设计时宁可保守一点多用队列传递数据多用互斥锁保护资源虽然开销稍大但稳定性高。等对整个系统数据流和时序了然于胸后再针对性能瓶颈做优化比如将某些队列换成事件标志或者调整任务优先级。最后一定要善用调试工具给关键通信操作加上状态监控这样当系统行为异常时你才能快速定位到是哪个环节的“对话”出了

相关新闻

TensorFlow GPU安装验证全攻略:从环境检查到性能测试

TensorFlow GPU安装验证全攻略:从环境检查到性能测试

1. 项目概述:为什么GPU版的TensorFlow安装后必须验证? 刚装好TensorFlow GPU版本,命令行里显示“Successfully installed tensorflow-gpu”,是不是就万事大吉了?作为一个在深度学习项目里摸爬滚打多年的老手&#xff…

2026/10/14 12:44:14 阅读更多 →
万用表入门实战:从零学会电压、电阻、通断测量与家庭电路排查

万用表入门实战:从零学会电压、电阻、通断测量与家庭电路排查

这类工具最值得先看的不是功能列表,而是能不能在普通环境里稳定跑起来。万用表,或者说数字万用表,是电子维修、电路调试甚至家电简单排查里最基础、也最实用的工具。很多人觉得它复杂,其实核心就几个档位,弄明白了&…

2026/10/10 13:47:07 阅读更多 →
Python+Tableau构建毕业生就业趋势可视化分析系统

Python+Tableau构建毕业生就业趋势可视化分析系统

1. 毕业生就业趋势可视化:数据背后的故事最近帮学校就业指导中心做了个有趣的项目——用数据可视化呈现近五年毕业生的就业流向。当我把这些冷冰冰的表格转换成动态图表时,意外发现了许多藏在数字里的秘密:计算机专业学生去互联网大厂的比例从…

2026/9/19 21:11:47 阅读更多 →

最新新闻

安装VSCode插件与安装Vue项目:用TaoToken统一Key打通本地开发链路

安装VSCode插件与安装Vue项目:用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/15 13:24:55 阅读更多 →
Zabbix 4.2集成Orabbix监控Oracle 12c:安装部署全指南

Zabbix 4.2集成Orabbix监控Oracle 12c:安装部署全指南

有人可能会问:监控Oracle数据库又不是非要用Orabbix,Zabbix自己也有原生模板,为什么还得额外搭一套东西?这个问题的答案,等你真正拿Zabbix自带的Oracle模板去接一套Oracle 12c RAC的时候就明白了——光补齐那些采集项的…

2026/10/15 13:24:55 阅读更多 →
MQTT客户端从选型到排障:QoS、会话、遗嘱与心跳实战指南

MQTT客户端从选型到排障:QoS、会话、遗嘱与心跳实战指南

简介:这是一套围绕 MQTT 协议的自写测试客户端资源,面向物联网开发者、嵌入式工程师及协议学习者,讲解客户端与 Broker 建立连接、发布订阅消息及 QoS 服务质量保障机制,适合初步接触 MQTT 或需要自测通信链路的工程场景。压缩包内…

2026/10/15 13:24:55 阅读更多 →
Node.js浮点精度与%.4f格式化:从原理到工程实践

Node.js浮点精度与%.4f格式化:从原理到工程实践

1. 从C语言的%.4f说起:Node.js里第一次怀疑人生1.1 你搜索的「04f」到底是什么在 C 语言的 printf 里,printf("%.4f", value)的含义是:把 value 按定点小数输出,小数点后固定保留 4 位,写出来就是3.1416、2.…

2026/10/15 13:24:55 阅读更多 →
会写梯形图就能搞定产线?先弄清单点指令与系统集成的真实差距

会写梯形图就能搞定产线?先弄清单点指令与系统集成的真实差距

很多人以为工业自动化就是坐在电脑前敲几行梯形图代码。不少初入行的朋友常有一种错觉:只要把点位启保停背熟,软件仿真跑得顺畅,自己就已经是一名合格的自动化工程师了。 直到真正走进车间现场,面对轰鸣的伺服电机、急停报警的机械…

2026/10/15 13:24:55 阅读更多 →
context-mode:一条命令实现多项目开发上下文的无缝切换

context-mode:一条命令实现多项目开发上下文的无缝切换

说实话,这个项目的源头特别朴素:我在同时维护两个项目,一个是Java后端的微服务,一个是Python数据分析脚本。每天最崩溃的不是写代码,而是切换上下文——cd到对应目录、激活虚拟环境、设置环境变量、加载一堆alias、还得…

2026/10/15 13:23:54 阅读更多 →

日新闻

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/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/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 阅读更多 →