Arduino串口数据解析实战:从字符串分割到PWM控制应用
1. 从“逗号难题”到“数据流”一个Arduino串口解析的实战场景如果你玩过Arduino大概率遇到过这个场景你想通过电脑串口给Arduino发送一组数据比如“1024,512,256,128”希望Arduino能把这串字符拆开变成[1024, 512, 256, 128]这样的整数数组然后用来控制PWM输出、舵机角度或者电机转速。听起来很简单不就是读字符串然后转数字吗但当你真正动手会发现一堆坑等着你串口数据是流式的可能一次收不全字符串里可能有空格、换行数字转换失败怎么办多个数据怎么高效拆分这恰恰是嵌入式开发中“协议解析”的入门课处理不好你的智能小车、机械臂或者灯光系统就会行为诡异。今天我们就来彻底解决这个“Arduino串口读取多个字符串并转换为数字数组”的问题。这不是一个简单的Serial.parseInt()教程而是一个完整的、从数据流接收到稳定数组转换的实战方案。我们会从最基础的Serial.readStringUntil讲起深入到更健壮、更高效的strtok分割和状态机解析最后结合PWM控制给出实际应用案例。无论你是想用串口调试助手发送坐标控制舵机还是想用C#、Python上位机发送传感器校准参数这篇文章都能给你一套可直接“抄作业”的代码和清晰的避坑指南。2. 基础方案使用Serial.readStringUntil与atoi的快速入门当我们从串口收到像“100,200,300”这样的字符串时最直观的想法就是按逗号分割。Arduino的Serial库提供了readStringUntil函数它非常适合这种定界符清晰的数据。2.1 核心函数Serial.readStringUntil的工作原理Serial.readStringUntil的工作方式是从串口缓冲区中读取字符直到遇到指定的终止字符例如逗号,或换行符\n然后将之前读取的所有字符作为一个String对象返回。这里的关键是“阻塞”。函数会一直等待直到要么收到终止符要么超时默认1秒。这个特性决定了它最适合用在loop()函数中并且需要上位机发送的数据格式非常规范。一个最简单的实现代码如下void setup() { Serial.begin(9600); // 初始化串口波特率9600 } void loop() { if (Serial.available() 0) { // 读取直到遇到换行符常用作一帧数据的结束 String inputString Serial.readStringUntil(\n); inputString.trim(); // 非常重要去除首尾可能存在的空格或回车 // 检查字符串是否有效 if (inputString.length() 0) { // 此时inputString可能是 1024,512,256 // 我们需要进一步处理 } } }注意inputString.trim()这一行至关重要。串口调试助手发送的数据很可能末尾带着\r回车或额外的空格不去除它们会导致后续转换失败。2.2 字符串分割与整数转换indexOf与toInt的配合拿到一个完整的字符串后下一步是按逗号分割。我们可以使用String类的indexOf()和substring()方法。void parseStringToArray(String data) { int myArray[10]; // 假设最多存储10个数字 int index 0; int startPos 0; int commaPos; do { // 查找下一个逗号的位置 commaPos data.indexOf(,, startPos); String numStr; if (commaPos ! -1) { // 截取从startPos到commaPos的子字符串 numStr data.substring(startPos, commaPos); startPos commaPos 1; // 更新起始位置跳过逗号 } else { // 没有找到更多逗号截取剩余部分 numStr data.substring(startPos); } numStr.trim(); // 再次修剪子字符串可能的空格 if (numStr.length() 0) { // 使用toInt()将字符串转换为整数 myArray[index] numStr.toInt(); index; if (index 10) break; // 防止数组越界 } } while (commaPos 0); // 当还能找到逗号时继续循环 // 此时myArray[0]到myArray[index-1]存储了转换后的数字 for (int i 0; i index; i) { Serial.print(Array[); Serial.print(i); Serial.print(] ); Serial.println(myArray[i]); } }把这个parseStringToArray函数放到前面的loop()里调用基础功能就实现了。但是这个方案有几个明显的弱点内存效率低频繁创建String对象substring和readStringUntil都会产生新的String在内存有限的Arduino上容易造成内存碎片。错误处理弱如果字符串中包含非数字字符如“1024,abc,256”toInt()会返回0你可能无法区分真正的“0”和转换失败。灵活性差完全依赖String类对于更复杂的协议或高性能场景显得笨重。尽管如此对于发送频率不高、数据量小的简单项目比如通过串口调试助手手动发送几个PWM占空比这个方案足够简单有效。3. 进阶方案基于C风格字符串与strtok的高效解析当你需要更高效、更可靠地处理数据或者数据量较大时回归C语言风格的字符数组char array和标准库函数是更好的选择。这能避免String类的开销给你更精细的控制。3.1 为何要使用字符数组和strtok在嵌入式领域尤其是像Arduino Uno仅有2KB SRAM这样的资源受限环境中动态内存分配是危险的。String类虽然易用但其背后涉及动态内存的申请和释放不当使用会导致内存碎片最终使程序崩溃。而字符数组在编译时或栈上分配生命周期明确没有内存管理的隐藏成本。strtokstring tokenize是C标准库中的一个函数用于根据指定的分隔符如逗号、空格将一个字符串分割成一系列“令牌”token。它直接在原字符串上进行修改用\0替换分隔符因此效率极高不分配额外内存。3.2 实战代码健壮的strtok解析流程下面是一个完整的、健壮的示例它一次读取一行数据用strtok分割并用atoi或更安全的strtol进行转换。const int MAX_ARRAY_SIZE 20; // 定义数组最大容量 int numberArray[MAX_ARRAY_SIZE]; int numCount 0; // 实际解析出的数字个数 void setup() { Serial.begin(115200); // 可以使用更高的波特率 while (!Serial) { ; // 等待串口连接对于Leonardo/Micro等 } } void loop() { // 方案等待一行数据以换行符结尾 static char inputBuffer[100]; // 静态缓冲区存储一行数据 static int bufferIndex 0; while (Serial.available() 0) { char inChar Serial.read(); // 如果收到换行符认为一行数据接收完成 if (inChar \n) { inputBuffer[bufferIndex] \0; // 确保字符串以空字符结尾 bufferIndex 0; // 重置索引准备接收下一行 // 调用解析函数 parseLine(inputBuffer); break; // 解析完一行就跳出避免在同一个loop中处理多行 } else if (bufferIndex (sizeof(inputBuffer) - 1)) { // 存储字符但留一个位置给结尾的\0 inputBuffer[bufferIndex] inChar; bufferIndex; } else { // 缓冲区溢出清空缓冲区并报错 bufferIndex 0; Serial.println(Error: Input buffer overflow!); break; } } } void parseLine(char* line) { numCount 0; // 重置计数器 // 使用strtok进行分割分隔符指定为逗号和空格更鲁棒 char* token strtok(line, , \t\r\n); while (token ! NULL numCount MAX_ARRAY_SIZE) { // 错误处理检查token是否全是数字可选但推荐 bool isValidNumber true; for (int i 0; token[i] ! \0; i) { if (!isdigit(token[i]) token[i] ! -) { // 允许负号 isValidNumber false; break; } } if (isValidNumber) { // 使用strtol进行转换可以检测转换错误 char* endPtr; long val strtol(token, endPtr, 10); // 10表示十进制 // 检查是否整个token都被成功转换了 if (endPtr ! token (*endPtr \0)) { numberArray[numCount] (int)val; numCount; } else { Serial.print(Conversion error for token: ); Serial.println(token); } } else { Serial.print(Invalid number token: ); Serial.println(token); } // 获取下一个token注意第一个参数传NULL token strtok(NULL, , \t\r\n); } // 解析完成打印结果或进行后续处理 Serial.print(Parsed ); Serial.print(numCount); Serial.println( numbers:); for (int i 0; i numCount; i) { Serial.println(numberArray[i]); } // 在这里你可以使用numberArray中的数据了例如控制PWM applyPWMControl(); }3.3 关键细节与避坑指南缓冲区溢出防护inputBuffer的大小这里是100需要根据你预期接收的最大数据长度来设定并留有裕量。while循环中的if (bufferIndex (sizeof(inputBuffer) - 1))是防止写越界的生命线。strtok的“状态性”strtok在第一次调用时传入原始字符串指针后续调用必须传入NULL。这是因为strtok内部保存了一个静态指针指向当前处理位置。这也意味着它不是线程安全的但在单线程的Arduino程序中没问题。分隔符集合strtok(line, , \t\r\n)中的分隔符包含了逗号、空格、制表符、回车和换行。这提高了鲁棒性即使上位机发送的数据格式不那么规整如“100, 200, 300”也能正确解析。更安全的转换strtol相比atoistrtol提供了错误检测机制。endPtr会指向转换停止的字符位置。如果endPtr token说明一个数字都没转换如果*endPtr ! \0说明token中只有一部分是数字。这能帮你精准定位数据格式错误。静态变量inputBuffer和bufferIndex将它们声明为static使其在函数调用之间保持值。这对于在loop()中累积字符直到收到换行符至关重要。这套方案是大多数严肃项目的起点它在效率、可靠性和代码复杂度之间取得了很好的平衡。4. 工业级强化状态机解析与二进制协议考量对于需要极高可靠性、实时性或者与复杂上位机如C#、Python编写的工业控制软件通信的项目前面的方案可能还不够。数据帧可能不完整、可能夹杂错误字节、可能需要校验和。这时一个简单的状态机State Machine解析器是更专业的选择。4.1 为何需要状态机串口通信本质上是异步字节流。我们定义的“数据帧”例如以\n结尾的一行文本在传输过程中可能被拆分成多个数据包到达。loop()函数每次执行时串口缓冲区里可能只有半帧数据也可能有多帧数据。简单的“收到换行符就处理”逻辑在数据流密集时可能出错。状态机将解析过程划分为几个明确的状态如“等待帧开始”、“接收数据中”、“帧结束处理”使程序能从容应对数据流的各种情况。4.2 实现一个简单的接收状态机我们以实现一个“以特定字符如\n结尾的文本帧”解析器为例enum ParserState { STATE_IDLE, // 空闲等待数据 STATE_RECEIVING, // 正在接收一帧数据 STATE_COMPLETE // 一帧数据接收完成 }; ParserState currentState STATE_IDLE; char frameBuffer[128]; int frameIndex 0; const char FRAME_END \n; void handleSerialData() { while (Serial.available() 0) { char ch Serial.read(); switch (currentState) { case STATE_IDLE: // 忽略可能的垃圾字符如之前的残留直到开始接收有效数据 // 这里我们假设任何可打印字符都是帧的开始 if (ch 32 ch 126) { // 可打印ASCII字符范围 frameBuffer[0] ch; frameIndex 1; currentState STATE_RECEIVING; } break; case STATE_RECEIVING: if (ch FRAME_END) { // 帧结束符到达 frameBuffer[frameIndex] \0; // 终结字符串 currentState STATE_COMPLETE; // 触发帧处理函数注意这里不直接处理而是设置标志 processFrameFlag true; } else if (frameIndex (sizeof(frameBuffer) - 1)) { // 存储数据字符 frameBuffer[frameIndex] ch; frameIndex; } else { // 缓冲区满帧错误重置状态 Serial.println(Frame buffer overflow!); currentState STATE_IDLE; frameIndex 0; } break; case STATE_COMPLETE: // 这个状态通常由外部逻辑处理并重置 // 不应该在这里直接读取新字符 break; } } } bool processFrameFlag false; void loop() { // 1. 驱动状态机接收数据 handleSerialData(); // 2. 如果有一帧数据就绪则处理 if (processFrameFlag) { processFrameFlag false; // 调用我们之前写好的parseLine函数 parseLine(frameBuffer); // 处理完后重置状态机准备接收下一帧 currentState STATE_IDLE; frameIndex 0; } // 3. 其他主循环任务... }这个状态机清晰地分离了“数据接收”和“数据处理”。handleSerialData只负责把字节组装成完整的帧loop主循环在合适的时间处理完整的帧。这种结构更容易加入超时检测例如如果超过100ms没收到FRAME_END就重置状态机、转义字符处理用于在数据中包含FRAME_END本身等高级功能。4.3 超越文本二进制协议的优势当数据量变大或实时性要求高时文本协议发送“1024,512”字符串的效率劣势就显现了。每个数字“1024”需要4-5个字节字符而用一个uint16_t类型的二进制数据表示只需要2个字节。同时文本转换atoi也比直接内存拷贝耗时。一个简单的二进制帧结构可以是[帧头0xAA] [数据长度N] [数据1高字节] [数据1低字节] ... [数据N高字节] [数据N低字节] [校验和]对应的解析状态机就会更复杂需要识别帧头、读取长度字段、按长度收取指定字节数的数据、计算校验和。虽然实现起来更繁琐但其抗干扰能力强、效率高是产品化项目的常见选择。对于Arduino初学者我建议先从扎实的文本协议解析做起理解状态机的思想再逐步过渡到二进制协议。5. 实战应用将解析出的数组用于PWM控制解析出数字数组不是终点让它动起来才是。最常见的应用就是控制PWM输出驱动LED亮度、电机速度或舵机角度。5.1 映射数组值到PWM引脚假设我们解析出的numberArray包含4个值分别想对应控制引脚 9, 10, 11, 12 的PWM输出占空比0-255。const int pwmPins[] {9, 10, 11, 12}; // PWM引脚数组 const int numPwmPins sizeof(pwmPins) / sizeof(pwmPins[0]); void applyPWMControl() { // 确保解析出的数字个数足够控制所有引脚 int elementsToUse min(numCount, numPwmPins); for (int i 0; i elementsToUse; i) { // 将接收到的数值映射到0-255范围。假设上位机发送的是0-1000的范围。 // 注意这里必须做边界限制 int pwmValue constrain(numberArray[i], 0, 1000); // 首先限制在预期范围 pwmValue map(pwmValue, 0, 1000, 0, 255); // 然后映射到PWM范围 // 更稳健的做法是直接限制在0-255 // int pwmValue constrain(numberArray[i], 0, 255); analogWrite(pwmPins[i], pwmValue); // 可选回传设置值用于调试 Serial.print(Set Pin ); Serial.print(pwmPins[i]); Serial.print( to PWM: ); Serial.println(pwmValue); } }将这个applyPWMControl()函数插入到前面parseLine函数的末尾你的Arduino就能根据串口指令动态调整PWM输出了。5.2 与上位机如串口调试助手、C#程序的协作要点要让整个系统跑通上位机的发送格式必须与下位机Arduino的解析逻辑匹配。格式约定双方必须严格约定数据格式。例如我们之前的代码期望的是逗号分隔、换行符结尾的字符串。在串口调试助手中发送时就要勾选“发送新行”会自动添加\r\n。发送1024,512,256,128\n注意有些调试助手默认加的是\r\n我们的解析代码同时将\r和\n视为分隔符或结束符所以是兼容的。数据范围约定好数字的范围。如果PWM只接受0-255而上位机发送了1000就需要在Arduino端用constrain或map进行限制和映射或者在发送前就处理好。发送频率避免发送过快。Arduino的loop()需要时间执行。如果一帧数据还没处理完下一帧又来了会导致缓冲区溢出或解析错乱。可以在上位机每次发送后等待Arduino返回一个“OK”或回声再发送下一帧。错误反馈像我们进阶方案中那样在Arduino代码里加入错误反馈如打印“Conversion error”并通过串口打印出来对于调试至关重要。5.3 一个完整的端到端测试流程烧录代码将包含状态机解析和PWM控制功能的完整代码烧录到Arduino。连接硬件将LED通过合适电阻或舵机连接到PWM引脚如9,10,11,12。打开串口监视器设置正确的波特率如115200。发送测试数据在发送框输入100,50,200,0并发送。观察现象对应的LED亮度应发生变化。同时串口监视器会打印出解析出的数组和设置的PWM值。发送错误数据测试发送100,abc,200,。观察串口输出应该能看到“Invalid number token: abc”之类的错误信息而有效的数字100,200仍被正确解析和应用。这证明了程序的鲁棒性。通过这个从基础到进阶再到实战应用的完整链条你应该已经掌握了在Arduino上处理串口多数据字符串的核心技能。这套方法不仅适用于PWM控制同样可以用于设置PID参数、传输传感器校准表、接收运动轨迹点等任何需要传输多个数值的场景。关键在于理解数据流的本质选择与项目复杂度相匹配的解析方案并始终牢记资源限制与错误处理。

相关新闻

掌控板跨学科项目式学习:从硬件连接到课程设计的教学实践指南

掌控板跨学科项目式学习:从硬件连接到课程设计的教学实践指南

1. 项目概述:从一场比赛到一个教学范式的转变 最近,首届“掌控板”教学应用设计大赛的课程设计示范案例公布了。作为一名长期关注创客教育和信息技术与学科融合的一线教师,我第一时间就仔细研究了这些案例。这不仅仅是一场比赛的结果展示&…

2026/10/15 5:38:54 阅读更多 →
AI论文写作工具测评与本科生毕业论文高效指南

AI论文写作工具测评与本科生毕业论文高效指南

1. 论文写作工具测评的必要性 本科毕业论文和科研写作是每个学术人必经的考验。记得我第一次写毕业论文时,光是格式调整就花了整整一周,更别提文献管理和内容润色了。现在AI写作工具井喷式发展,但市面上的选择太多,质量参差不齐&a…

2026/10/15 5:38:53 阅读更多 →
终极LRC歌词制作指南:免费开源工具让歌词同步变得简单快速

终极LRC歌词制作指南:免费开源工具让歌词同步变得简单快速

终极LRC歌词制作指南:免费开源工具让歌词同步变得简单快速 【免费下载链接】lrc-maker 歌词滚动姬|可能是你所能见到的最好用的歌词制作工具 项目地址: https://gitcode.com/gh_mirrors/lr/lrc-maker 歌词滚动姬(LRC Maker&#xff09…

2026/9/28 9:26:48 阅读更多 →

最新新闻

Fontsource 字体解析:Amarante —— 基于 Art Nouveau 的中等对比度展示字体

Fontsource 字体解析:Amarante —— 基于 Art Nouveau 的中等对比度展示字体

前端CLI开发工具 【免费下载链接】fontsource Self-host Open Source fonts in neatly bundled NPM packages. 项目地址: https://gitcode.com/gh_mirrors/fo/fontsource 点击查看 免费下载 Amarante 是 Fontsource 注册表中收录的 Google 系字体之一,由…

2026/10/15 5:39:22 阅读更多 →
372.高通 EDL Sahara/Firehose 协议详解,安卓黑砖救砖 + 分区镜像校验实战源码

372.高通 EDL Sahara/Firehose 协议详解,安卓黑砖救砖 + 分区镜像校验实战源码

摘要 本文面向具备基础计算机操作能力的开发者与维修人员,系统性地阐述安卓设备刷机与维修的底层逻辑。文章从BootROM到Android系统的启动链路出发,深入解析Fastboot与EDL两种核心刷机协议的通信机制,结合真实变砖救援案例,提供完整的操作流程与可运行的Python自动化脚本。…

2026/10/15 5:39:22 阅读更多 →
直流偏磁下变压器励磁电流谐波分析:Simulink仿真与FFT指南

直流偏磁下变压器励磁电流谐波分析:Simulink仿真与FFT指南

简介:这份文档是重庆邮电大学自动化学院/工业互联网学院学生在“复杂电气工程系统设计”课程中完成的A等实验报告,主题聚焦直流偏磁下变压器励磁电流谐波分析。报告从地磁风暴与高压直流输电两大现实场景切入,系统阐述直流偏磁的产生机理、对…

2026/10/15 5:39:22 阅读更多 →
基于SSM的门面商铺租赁管理系统设计与实现

基于SSM的门面商铺租赁管理系统设计与实现

每年到这个时间点,总有准备毕设的同学来问:JavaWeb方向到底选什么题目,既能体现技术功底,又不至于把自己逼到绝路。如果你正在“基于SSM的门面商铺租赁管理系统”这个题目上犹豫,我直接说结论:这是一个非常…

2026/10/15 5:39:22 阅读更多 →
MutationObserver:高效监控DOM变化的现代API

MutationObserver:高效监控DOM变化的现代API

1. MutationObserver 基础解析MutationObserver 是现代浏览器提供的原生API,用于监测DOM树中的节点变化。与传统的Mutation Events相比,它具有显著的性能优势:异步触发机制避免频繁回调导致的性能问题批量处理DOM变更,减少不必要的…

2026/10/15 5:39:22 阅读更多 →
Multi-Agent系统Tracing实战:从线上事故到执行链记录体系

Multi-Agent系统Tracing实战:从线上事故到执行链记录体系

1. 从一次线上事故说起:为什么Multi-Agent系统必须做Tracing去年冬天,我们团队上线了一套基于多智能体协作的自动化内容处理系统,四个Agent分别负责信息抽取、逻辑校验、格式转换和最终输出。上线第三天,运营同学反馈说有一批文档…

2026/10/15 5:38:22 阅读更多 →

日新闻

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/14 7:53:34 阅读更多 →