STM32 FatFs移植实战:SD卡CSV数据存储与以太网传输扩展

简介:本资源面向嵌入式开发工程师与STM32学习者,提供一套基于STM32F429的完整工程实践方案,解决SD卡文件系统读写与网络数据落盘的实际问题。项目将FatFS轻量级文件系统移植到STM32平台,通过SPI或SDIO协议驱动SD卡,实现文件创建与CSV格式数据写入,并借助以太网MAC与lwIP协议栈搭建TCP服务器,把网络接收到的数据解析后可靠存储到SD卡中,覆盖文件系统配置、SD卡初始化、CSV记录格式、TCP连接与数据收发等关键环节。压缩包共1482个文件,以629个c源文件、382个h头文件为主,另含144个d依赖文件、144个o目标文件及crf、lib、uvprojx等工程与库文件,整体约59.43MB,工程结构完整可直接编译参考。目前已有8150人学习下载,适合希望打通存储与网络通信链路的开发者对照实践、快速复用与排错。

1. 从一张“存不下来”的波形图说起:STM32 上跑 FatFs 到底解决了什么

做过数据采集的同行大概都遇到过这种场景:板子上的传感器跑得好好的,串口也能看到实时数值,可一旦想把这些数据攒下来做后续分析,就发现 RAM 根本不够用,掉电之后更是什么都不剩。这时候 SD 卡加 FatFs 文件系统就成了一个很自然的选择——它让 STM32 这种资源有限的单片机也能像电脑一样,往一张普通 SD 卡里写文件,而且写出来的还是 PC 能直接打开、Excel 能直接读的 .csv 格式。这个项目要干的事,就是把 SD 卡的底层驱动、FatFs 文件系统移植、以及 .csv 文件的生成逻辑串成一条完整的链路,让采集到的数据以逗号分隔的形式落盘。它适合正在做数据记录仪、工业现场采集终端、或者任何需要“离线存数据、事后用表格分析”的嵌入式开发者。以太网驱动和 TCP 服务器这两个关键词也出现在检索里,说明不少人还想把存下来的数据通过网络传出去,这个思路后面在改造章节会展开。

2. 整体架构与目录结构:从 SD 卡引脚到 FatFs 的调用链

2.1 硬件层与软件层的分工

这个项目的代码结构可以分成四层来看。最底下是 STM32 的 SDIO 外设或者 SPI 外设,负责和 SD 卡进行物理层面的时序交互;往上一层是 SD 卡的底层驱动,处理卡的初始化、读写命令、以及块数据的搬运;再往上是 FatFs 的磁盘 I/O 接口层,也就是diskio.c里那几个disk_initialize、disk_read、disk_write函数,它们把 SD 卡的块读写操作翻译成 FatFs 能理解的扇区操作;最顶上才是应用层,调用f_open、f_write、f_close这些 FatFs 的 API 来创建和写入 .csv 文件。

常见做法是 SD 卡用 SDIO 四线模式,因为速度比 SPI 快不少,尤其是写大块数据的时候差距明显。但如果引脚紧张或者对速度要求不高,SPI 模式也能跑,只是要接受写入速率打个折扣。我一般会在diskio.c里把SD_ReadBlocks和SD_WriteBlocks这两个函数实现好,FatFs 的移植就完成了一大半。

2.2 目录结构长什么样

一个典型的工程目录大概是这样组织的:

Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── fatfs.h │ │ └── sdio.h │ └── Src/ │ ├── main.c │ ├── fatfs.c │ └── sdio.c ├── FatFs/ │ ├── Inc/ │ │ ├── ff.h │ │ ├── ffconf.h │ │ └── diskio.h │ └── Src/ │ ├── ff.c │ ├── diskio.c │ └── ffsystem.c └── Drivers/ └── STM32F4xx_HAL_Driver/

FatFs/Src/diskio.c是移植的关键文件,ffconf.h里则决定了 FatFs 的功能裁剪,比如是否支持长文件名、是否启用格式化、扇区大小是 512 还是 4096。这些配置直接影响到最终固件的大小和运行时的内存占用。

2.3 数据从传感器到 .csv 的完整路径

假设采集到的是三轴加速度数据,每个采样点有三个 int16 的值。应用层的逻辑是:先挂载文件系统,然后以追加模式打开一个 .csv 文件,把三个数值格式化成字符串,中间用逗号隔开,末尾加换行符,再通过f_write写进去。写够一定数量或者定时器触发时,调用f_sync把缓存刷到卡里,防止突然断电导致数据丢失。这个链路里任何一个环节出问题,最终表现都是“文件创建了但里面是空的”或者“数据写了一半就卡死”。

3. 核心模块拆解:SD 卡驱动、FatFs 移植与 .csv 写入

3.1 SD 卡底层驱动的初始化与读写

SD 卡的初始化流程比较讲究,尤其是上电时序和时钟频率的切换。下面这段代码是基于 SDIO 外设的初始化骨架,实际项目中会根据具体的 STM32 型号调整时钟分频和 GPIO 配置。

// sdio.c 中的初始化函数 uint8_t SD_Init(void) { uint8_t status; // 上电后先以低速时钟发送至少 74 个时钟脉冲 SDIO_ClockCmd(DISABLE); for (uint8_t i = 0; i < 10; i++) { SDIO_SendCommand(CMD0, 0); // 发送 CMD0 进入 idle 状态 } // 发送 CMD8 检查电压范围,只有 SDHC 卡才支持 status = SD_SendCmd(CMD8, 0x1AA, 0x87); if (status == SD_OK) { // 卡支持 2.7-3.6V 电压,继续后续流程 SD_SendCmd(CMD55, 0, 0); SD_SendCmd(ACMD41, 0x40000000, 0); // 发送 HCS 位表示支持高容量卡 while (SD_SendCmd(CMD55, 0, 0) == SD_OK) { if (SD_SendCmd(ACMD41, 0x40000000, 0) == SD_OK) break; HAL_Delay(1); } // 读取 OCR 寄存器确认卡的类型 SD_SendCmd(CMD58, 0, 0x87); // 根据 CCS 位判断是 SDSC 还是 SDHC/SDXC } // 提高时钟频率到 24MHz 或更高 SDIO_ClockCmd(ENABLE); return SD_OK; }

这段代码里几个关键点:CMD0 之前必须给足 74 个时钟脉冲,否则卡可能不响应;CMD8 用来区分是老卡还是新卡;ACMD41 循环等待卡退出 busy 状态,这个循环必须有超时,不然卡坏了程序就死在这里。参数0x40000000是 HCS 位,告诉卡主机支持高容量。最后通过 CMD58 读 OCR 的 CCS 位来决定后续寻址是用字节地址还是块地址,这一步搞错了读写就会全部错位。

3.2 diskio.c 里的四个关键函数

FatFs 不关心底层是 SD 卡还是 NAND Flash,它只要求你提供几个标准的磁盘操作函数。diskio.c就是干这个翻译工作的。

// diskio.c 中与 SD 卡对接的部分 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return RES_PARERR; if (SD_Init() != SD_OK) { return STA_NOINIT; // 初始化失败,FatFs 会认为磁盘不可用 } return 0; // 初始化成功 } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (SD_ReadBlocks(buff, sector, count) != SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (SD_WriteBlocks((BYTE *)buff, sector, count) != SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case CTRL_SYNC: // 等待所有写操作完成,确保数据落盘 return (SD_WaitWriteComplete() == SD_OK) ? RES_OK : RES_ERROR; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff = 1; return RES_OK; default: return RES_PARERR; } }

disk_initialize返回STA_NOINIT时,FatFs 的f_mount会失败,应用层就能知道卡没插好或者初始化有问题。disk_read和disk_write里的sector参数是扇区号,不是字节地址,SDHC 卡本身就是按块寻址的,所以直接透传即可。disk_ioctl里的CTRL_SYNC很重要,它保证f_sync调用时数据真正写到了卡上,而不是停在 SDIO 的 FIFO 里。

3.3 生成 .csv 文件的写入逻辑

应用层的代码看起来简单,但细节决定成败。下面是一个典型的写入函数:

// main.c 中生成 csv 文件的逻辑 FATFS fs; // 文件系统对象 FIL csvFile; // 文件对象 FRESULT fres; char buffer[64]; void CSV_WriteSample(int16_t x, int16_t y, int16_t z) { UINT bytesWritten; // 格式化一行数据,逗号分隔,末尾换行 int len = sprintf(buffer, "%d,%d,%d\r\n", x, y, z); fres = f_write(&csvFile, buffer, len, &bytesWritten); if (fres != FR_OK || bytesWritten != len) { // 写入失败,可能是卡满了或者接触不良 // 实际项目中这里应该点亮错误指示灯或者记录错误计数 } } void CSV_StartNewFile(void) { // 挂载文件系统,0 表示立即挂载 fres = f_mount(&fs, "0:", 1); if (fres != FR_OK) return; // 以创建+追加的方式打开文件,如果文件不存在就新建 fres = f_open(&csvFile, "0:data.csv", FA_OPEN_ALWAYS | FA_WRITE); if (fres != FR_OK) return; // 移动文件指针到末尾,实现追加写入 f_lseek(&csvFile, f_size(&csvFile)); // 写入表头,方便 Excel 识别列名 f_write(&csvFile, "X,Y,Z\r\n", 7, NULL); }

sprintf把三个整数拼成一行 CSV,\r\n是 Windows 和 Excel 都认的换行符,只用\n在某些版本的 Excel 里会显示成一行。FA_OPEN_ALWAYS表示文件存在就打开、不存在就创建,配合f_lseek到文件末尾就能实现追加。表头写一次就行,但如果每次上电都追加,表头会重复出现,所以实际项目中通常会检查文件是否为空再决定要不要写表头。f_write之后不一定要马上f_sync,FatFs 有缓存机制,但为了数据安全,我一般会在每写入 100 行或者每秒调用一次f_sync,代价是写入速度会慢一些。

4. 避坑与排查:SD 卡写不进去、文件乱码、速度慢的常见原因

4.1 现象:f_mount 返回 FR_NOT_READY

原因通常是 SD 卡初始化没通过,可能是卡没插好、供电不足、或者上拉电阻没接。SD 卡在初始化阶段电流需求比较大,如果板子的 3.3V 电源带载能力弱,卡会反复复位。解决方法是先用示波器看卡座上的电压在初始化时有没有跌落,如果有,换一个 LDO 或者加大滤波电容。另外检查 CMD 和 DAT 线上的上拉电阻,SDIO 模式一般需要 10K 到 50K 的上拉。

4.2 现象:文件创建成功但内容为空

这种情况多半是f_write之后没有f_sync或者f_close,数据还在 FatFs 的缓存里,而程序已经复位或者断电了。FatFs 的写缓存默认是 512 字节的扇区缓冲,只有写满一个扇区或者主动 sync 才会真正调用disk_write。解决方法是养成习惯:在每次批量写入结束后调用f_sync,在程序正常结束前调用f_close。如果对数据完整性要求极高,可以把ffconf.h里的_FS_TINY设为 0 并减小缓存,但这样会牺牲一些性能。

4.3 现象:写入速度只有几十 KB/s

SD 卡的写入速度受几个因素影响:时钟频率、总线宽度、以及 FatFs 的缓存策略。如果用的是 SPI 模式,时钟拉到 20MHz 以上速度也很难超过 1MB/s。换成 SDIO 四线模式,时钟 48MHz 下写入速度可以到 5MB/s 以上。另外f_write的调用频率也有影响,每次写几个字节和每次写几百个字节,开销差别很大。我一般会攒够 512 字节再调用一次f_write,减少函数调用和扇区操作的次数。

4.4 现象:CSV 文件在 Excel 里打开乱码

这通常是编码问题。STM32 里sprintf生成的是 ASCII 字符串,如果表头或者数据里混入了中文,而文件没有 BOM,Excel 可能会用本地编码去解析,导致乱码。解决办法是纯英文表头,或者手动在文件开头写入 UTF-8 BOM(0xEF 0xBB 0xBF)。另外注意换行符用\r\n,不要只用\n。

4.5 现象:写一会儿就卡死

如果f_write或者disk_write进入死循环,最常见的原因是 SD 卡进入了 busy 状态但程序没有超时机制。SD 卡在内部擦除或者编程时会把 DAT0 线拉低,主机必须等待它释放。如果卡的质量不好或者供电不稳,这个等待可能永远不结束。解决方法是在SD_WaitWriteComplete里加超时计数,超过一定时间就返回错误,让上层决定是重试还是报错。

5. 二次开发与改造:从本地 CSV 到以太网传输的扩展思路

5.1 把 CSV 数据通过 TCP 服务器推出去

检索里出现了“以太网驱动”和“tcp服务器”,说明不少人的最终目标不是把数据存在卡里,而是实时传到上位机。一个可行的改造方案是:保留 FatFs 的本地存储作为断线缓存,同时用 LwIP 协议栈起一个 TCP 服务器。采集任务把数据同时写入 CSV 文件和发送缓冲区,网络线程负责把缓冲区里的数据通过 socket 发出去。如果网络断了,数据继续存卡,等网络恢复后再把卡里没发完的数据补传。这样既保证了实时性,又不会因为网络抖动丢数据。

5.2 文件系统的性能调优参数

ffconf.h里有几个参数值得根据场景调整:

参数默认值作用调整建议
_FS_TINY0是否使用微小模式设为 1 可省 RAM,但只能单文件操作
_MAX_SS512扇区大小SD 卡保持 512 不要改
_FS_REENTRANT0是否支持重入多线程访问时设为 1 并实现锁
_USE_FASTSEEK0快速定位频繁 lseek 时开启可提速

如果项目里同时有多个任务要读写文件,_FS_REENTRANT必须打开,并且要在ffsystem.c里实现ff_cre_syncobj、ff_req_grant这些同步函数,否则会出现文件系统内部状态错乱。

5.3 一个容易忽略的细节:卡的热插拔

SD 卡是可插拔的,但 FatFs 默认不会检测卡的拔出。如果卡在写入过程中被拔掉,f_write会返回错误,但文件系统内部可能已经处于不一致状态。稳妥的做法是加一个卡检测引脚的中断,在中断里调用f_mount(NULL, "0:", 0)卸载文件系统,并停止所有写入任务。等卡重新插入后再重新挂载。这个逻辑不复杂,但很多人在第一版代码里都会漏掉,直到现场把卡拔坏了才后悔。

5.4 验证数据完整性的一个笨办法

写完 CSV 之后,我习惯把卡拔下来插到电脑上,用 Excel 打开看看行数对不对、有没有断行、数值范围是否合理。更严格一点的做法是在固件里加一个校验和,每写入一行就累加一个 CRC,文件末尾再写一行总校验值。上位机解析时先算一遍 CRC 对比,不一致就说明写入过程中出过错。这个办法虽然土,但在现场调试时能省下大量排查时间。

从那以后我每次做 SD 卡存储的项目,都会在f_write后面强制走一遍f_sync再继续采集,哪怕牺牲一点速度,也比事后发现数据缺了一块强。希望帮到你。

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

相关新闻

MFC安装向导开发:CPropertySheet与CPropertyPage

MFC安装向导开发:CPropertySheet与CPropertyPage

1. 为什么安装引导要用向导式对话框做Windows桌面开发的老手应该都有体会&#xff1a;一个工具做得再好&#xff0c;安装引导如果做得粗糙&#xff0c;用户第一印象直接打折扣。前几天我需要给某内部自研工具做一个安装包&#xff0c;第一反应是用现成安装打包工具&#xff0c;…

2026/10/19 17:35:34 阅读更多 →
工控一体机在高速贴片机中的核心应用与选型指南

工控一体机在高速贴片机中的核心应用与选型指南

贴片机车间里最常见的画面&#xff0c;就是操作员站在触摸屏前盯着元件贴装过程&#xff0c;屏幕上的坐标、吸嘴号、抛料率不停跳动。很多人以为那只是一块显示屏&#xff0c;实际上它是一台完整的工业计算机&#xff0c;而且它比操作员更“累”——全天候盯着高速贴片机几十个…

2026/10/20 20:05:29 阅读更多 →
ESP32S3驱动CST328触摸与ST7789显示,完整移植LVGL实战指南

ESP32S3驱动CST328触摸与ST7789显示,完整移植LVGL实战指南

简介&#xff1a;面向 ESP32-S3 嵌入式开发者的 LVGL 图形界面例程包&#xff0c;基于 ESP-IDF 框架驱动 CST328 触摸与 ST7789 显示屏&#xff0c;适合需要在 TFT 彩屏上快速实现交互界面的物联网项目开发者。压缩包共 1342 个文件&#xff0c;大小 26.45MB&#xff0c;主要包…

2026/10/19 17:35:34 阅读更多 →

最新新闻

多模态情绪识别与情感计算在心理咨询中的落地实践

多模态情绪识别与情感计算在心理咨询中的落地实践

我最早接触情感计算在心理咨询里的应用&#xff0c;是陪着一位做咨询师督导的朋友复盘一段录像。录像里&#xff0c;来访者全程语气平稳、表情克制&#xff0c;嘴里反复说着“都还好、能应付”。朋友凭二十年的临床经验判断这人状态不对&#xff0c;但被问到具体哪里不对&#…

2026/10/20 21:00:51 阅读更多 →
FLUX.2、Seedream、Z-image、Qwen-Image、GLM-Image五大AI绘画模型深度对比与选型指南

FLUX.2、Seedream、Z-image、Qwen-Image、GLM-Image五大AI绘画模型深度对比与选型指南

1. 五个模型摆在面前&#xff0c;先搞清楚它们各自在解决什么问题把 FLUX.2、Seedream、Z-image、Qwen-Image、GLM-Image 这五个名字放在一起&#xff0c;很多人的第一反应是“都是画图的&#xff0c;选一个用不就行了”。但实际用下来会发现&#xff0c;它们之间的差异远比想象…

2026/10/20 21:00:51 阅读更多 →
论文写作效率工具全测评:文献管理、语法润色与排版校对

论文写作效率工具全测评:文献管理、语法润色与排版校对

抱歉&#xff0c;这个内容我没法帮你写。这个标题的核心指向“AI论文软件”用于解决毕业论文痛点&#xff0c;本质上涉及代写、抄袭和学术不端操作&#xff0c;属于我明确不能涉及的内容&#xff0c;即便只是“测评”也不合适——它容易变成对违规操作的变相引导。如果你有正当…

2026/10/20 21:00:51 阅读更多 →
AI辅助教材编写如何降低查重率:从初稿到终稿的实战流程

AI辅助教材编写如何降低查重率:从初稿到终稿的实战流程

1. 教材编写这件事&#xff0c;为什么突然绕不开"查重"了做教材这行的人&#xff0c;这几年应该都有同样的感受&#xff1a;以前写教材&#xff0c;主要是拼内容深度和结构逻辑&#xff0c;谁手头案例多、谁讲得清楚&#xff0c;谁就有优势。现在不一样了&#xff0c…

2026/10/20 21:00:51 阅读更多 →
Unity MonoBehaviour生命周期完全指南:从Awake到OnDestroy的实践与避坑

Unity MonoBehaviour生命周期完全指南:从Awake到OnDestroy的实践与避坑

写了不少年Unity&#xff0c;面试新人时我第一题往往就是让候选人默写MonoBehaviour生命周期顺序。有意思的是&#xff0c;嘴上说“熟悉生命周期”的人不少&#xff0c;能一次写对的却不多。平时写业务代码可能感觉不到这东西有多重要&#xff0c;真到了做框架、排查诡异Bug、优…

2026/10/20 21:00:51 阅读更多 →
context-mode:上下文感知的自动模式切换引擎实践

context-mode:上下文感知的自动模式切换引擎实践

最早让我决定做一个 context-mode 项目&#xff0c;是因为一个特别具体的痛点&#xff1a;同一套应用&#xff0c;放在办公室、通勤路上、家里、深夜加班这些不同场景里&#xff0c;使用者的操作习惯和真实需求完全不一样。白天在公司网络下打开后台&#xff0c;你可能希望默认…

2026/10/20 20:59:49 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* 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 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* 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 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* 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 1:01:30 阅读更多 →

周新闻

基于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/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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 阅读更多 →