无硬件SPI也能读写EEPROM:25LC256模拟SPI驱动封装与移植

简介:这份资源是面向嵌入式开发初学者与电子工程师的EEPROM 25LC256读写源码包,采用C++实现,重点演示通过模拟SPI方式完成对25LC256的完整读写控制。内容覆盖SPI接口初始化、片选与释放、命令与地址传输、读写使能、数据缓冲及错误处理等关键环节,适合用于保存配置参数、设备固件或用户数据等非易失存储场景,也可作为其他SPI设备驱动的学习模板。压缩包共20个文件,约38KB,包含c与h源码文件、obj与lst编译中间文件、hex烧录文件、uv2工程文件及a51启动代码等,结构清晰,便于直接导入Keil等环境编译调试。目前已有1259人学习下载,读者可借此掌握SPI通信时序与25LC256操作流程,理解寄存器配置、事务管理与兼容性适配思路,为嵌入式存储开发打下实践基础。

1. 一块 25LC256 的读写折腾:从裸机时序到可复用 C++ 封装

去年调一块工业采集板,主控的硬件 SPI 引脚被占满了,只剩几个普通 GPIO。板子上挂着一颗 25LC256,容量 32KB,走的是标准 SPI 协议。当时第一反应是找个现成库,翻了一圈发现要么绑死某家芯片的 HAL,要么把 CS、SCK、MOSI 全写死成宏,换块板子就得重写。后来干脆自己撸了一份 C++ 源码,用普通 IO 模拟 SPI 时序,把 25LC256 的读写、擦除、状态轮询全封进去。这份源码解决的就是「没有硬件 SPI 也能读写 EEPROM」这个具体问题,适合做嵌入式、写裸机驱动、或者需要在资源受限 MCU 上存配置参数的开发者。它不依赖任何 RTOS,纯 C++ 实现,移植时只要改几个 GPIO 操作函数就能跑起来。

2. 项目定位与技术选型:为什么用模拟 SPI 而不是硬件外设

2.1 25LC256 的通信特性决定了实现方式

25LC256 是 Microchip 的 256Kbit SPI EEPROM,换算下来 32KB 存储空间,页大小 64 字节,支持 10MHz 时钟。它的指令集很精简:读状态寄存器 RDSR、写使能 WREN、读数据 READ、写数据 WRITE、写状态寄存器 WRSR、擦除整个芯片等。每次写操作前必须先发 WREN,写完要轮询 RDSR 的 WIP 位,等芯片内部擦写完成才能发下一条指令。这些时序要求不复杂,但顺序错了就写不进去,而且不会报错,只会静默失败。

硬件 SPI 外设当然能驱动它,但很多低成本 MCU 的 SPI 引脚是复用的,一旦被 LCD 或 Flash 占用,剩下的 GPIO 就只能靠软件翻转。模拟 SPI 的好处是引脚随便选,坏处是速率上不去,通常只能跑到 1MHz 左右。对于存配置参数、日志这类低频写入场景,完全够用。

2.2 源码的整体架构

这份源码分成三层。最底层是SoftSpi类,只负责 SCK、MOSI、MISO 三个引脚的电平翻转和采样,不关心对面挂的是什么芯片。中间层是Eeprom25lc256类,实现 25LC256 的指令集,包括写使能、读状态、页写、字节写、连续读。最上层是应用接口,提供readBuffer、writeBuffer、eraseChip这些直接可调的函数。

目录结构大致是这样:

eeprom25lc256/ ├── soft_spi.h // 模拟 SPI 底层时序 ├── soft_spi.cpp ├── eeprom25lc256.h // 芯片指令封装 ├── eeprom25lc256.cpp ├── gpio_interface.h // GPIO 操作抽象,移植时改这里 └── main.cpp // 示例:写一段数据再读回来校验

gpio_interface.h是关键,它把pinWrite、pinRead、delayUs声明成纯虚函数或者函数指针,移植到 STM32、ESP32、AVR 都只需要实现这几个函数。我一般会在移植时先把这个文件改好,再编译,能省掉一大半调试时间。

2.3 模拟 SPI 的时序参数怎么定

25LC256 的 SPI 模式是 0,0,也就是空闲时 SCK 低电平,数据在 SCK 上升沿采样。模拟 SPI 的核心就是控制 SCK 的翻转节奏。源码里用一个spiDelay变量控制半周期延时,单位是微秒。比如主频 72MHz 的 MCU,一个delayUs(1)大概对应几十个时钟周期,实际 SCK 频率在 500kHz 左右。

// soft_spi.h class SoftSpi { public: void init(); uint8_t transfer(uint8_t data); // 全双工,发一个字节收一个字节 void csLow(); void csHigh(); private: void sckHigh(); void sckLow(); void mosiWrite(uint8_t bit); uint8_t misoRead(); uint32_t spiDelay = 1; // 半周期延时,单位 us };

transfer是核心函数,循环 8 次,每次先设置 MOSI,再拉高 SCK,然后读 MISO,最后拉低 SCK。顺序不能乱,因为 25LC256 在 SCK 上升沿采样 MOSI,在下降沿输出 MISO。如果先拉高 SCK 再设 MOSI,第一位数据就丢了。

uint8_t SoftSpi::transfer(uint8_t data) { uint8_t recv = 0; for (int i = 7; i >= 0; i--) { mosiWrite((data >> i) & 0x01); // 先准备数据 sckHigh(); // 上升沿,从机采样 delayUs(spiDelay); recv |= (misoRead() << i); // 读从机输出 sckLow(); // 下降沿 delayUs(spiDelay); } return recv; }

参数spiDelay调大能提高稳定性,但会降低吞吐。我一般先用 2us 跑通,再逐步降到 1us 甚至 0.5us,看什么时候开始出错,留一点余量。

3. 核心模块拆解:25LC256 指令封装与读写流程

3.1 写使能 WREN 与状态轮询 RDSR

25LC256 上电后默认是写保护状态,任何写操作前必须先发 WREN 指令,把状态寄存器的 WEL 位置 1。WREN 是单字节指令,发完拉高 CS 就生效。但注意,WREN 之后如果发了别的指令,WEL 位会自动清零,所以每次写之前都要重新发。

void Eeprom25lc256::writeEnable() { spi.csLow(); spi.transfer(CMD_WREN); // 0x06 spi.csHigh(); } void Eeprom25lc256::waitWriteComplete() { spi.csLow(); spi.transfer(CMD_RDSR); // 0x05 while (spi.transfer(0xFF) & 0x01) { // WIP 位为 1 表示芯片正在擦写,继续等 delayUs(100); } spi.csHigh(); }

waitWriteComplete里的spi.transfer(0xFF)是发一个哑字节,目的是产生时钟让从机把状态寄存器移出来。很多新手会忘了这一步,直接读 MISO 引脚,结果读到的永远是上一次的残留值。轮询间隔我设 100us,实际 25LC256 的页写周期最大 5ms,轮询太密会占 CPU,太疏会拖慢整体写入。

3.2 页写与字节写的区别

25LC256 的写操作分两种:字节写和页写。字节写一次写一个地址,页写一次最多写 64 字节,但要求这 64 字节不能跨页边界。比如从地址 0x3F 开始写 10 个字节,写到 0x40 就会回卷到 0x00,把前面的数据覆盖掉。这是血泪教训,我第一次调的时候没注意页边界,写进去的数据全乱了。

bool Eeprom25lc256::writePage(uint16_t addr, const uint8_t* data, uint16_t len) { if (len == 0 || len > PAGE_SIZE) return false; // 检查是否跨页 uint16_t pageStart = addr & ~(PAGE_SIZE - 1); if (addr + len > pageStart + PAGE_SIZE) return false; writeEnable(); spi.csLow(); spi.transfer(CMD_WRITE); // 0x02 spi.transfer((addr >> 8) & 0xFF); // 地址高字节 spi.transfer(addr & 0xFF); // 地址低字节 for (uint16_t i = 0; i < len; i++) { spi.transfer(data[i]); } spi.csHigh(); waitWriteComplete(); return true; }

PAGE_SIZE定义为 64,addr & ~(PAGE_SIZE - 1)是取页起始地址。如果调用者传的地址和长度跨页,函数直接返回 false,让上层去拆分。我一般会在writeBuffer里自动按页拆分,这样应用层就不用关心边界了。

3.3 连续读与地址自动递增

读操作比写简单,发完 READ 指令和 16 位地址后,芯片会从当前地址开始输出数据,每读一个字节地址自动加一,直到 CS 拉高。所以读整个 32KB 只需要一次 CS 拉低。

void Eeprom25lc256::readBuffer(uint16_t addr, uint8_t* buf, uint16_t len) { spi.csLow(); spi.transfer(CMD_READ); // 0x03 spi.transfer((addr >> 8) & 0xFF); spi.transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = spi.transfer(0xFF); // 哑字节产生时钟 } spi.csHigh(); }

注意spi.transfer(0xFF)的返回值才是有效数据,参数 0xFF 只是为了让 MOSI 保持高电平,对读操作没影响。如果传 0x00 也能读,但有些 SPI 从机对 MOSI 有要求,统一用 0xFF 比较稳妥。

3.4 状态寄存器与写保护

25LC256 的状态寄存器里除了 WIP 和 WEL,还有 BP0、BP1 和 WPEN 位,用来做块保护。比如把 BP0 和 BP1 都设成 1,整个芯片就只读了。这个功能在存校准参数时有用,防止程序跑飞误写。源码里提供了setBlockProtection函数,通过 WRSR 指令写状态寄存器。

void Eeprom25lc256::setBlockProtection(uint8_t bp) { writeEnable(); spi.csLow(); spi.transfer(CMD_WRSR); // 0x01 spi.transfer((bp & 0x03) << 2); // BP1 BP0 在 bit3 bit2 spi.csHigh(); waitWriteComplete(); }

参数bp取值 0 到 3,对应保护范围从无到全片。写完之后要重新上电或者发 WREN 才能解除,这个在调试时容易翻车,以为代码写错了,其实是保护位没清。

4. 避坑与常见问题排查

4.1 读出来全是 0xFF 或 0x00

现象:调用readBuffer读任何地址都返回 0xFF 或 0x00,写进去的数据读不回来。

原因:0xFF 通常是 MISO 一直高电平,说明从机没响应,可能是 CS 没拉低、SCK 没翻转、或者芯片没供电。0x00 则是 MISO 一直低,可能是 MOSI 和 MISO 接反了,或者芯片处于写保护状态。

解决:先用示波器看 CS、SCK、MOSI 三根线有没有波形。如果没有示波器,就在transfer里加翻转 GPIO 的代码,用 LED 看 SCK 有没有动。确认波形后再查芯片的 HOLD 和 WP 引脚,这两个脚如果悬空,芯片可能不工作,一般要上拉到 VCC。

4.2 写入后数据错位

现象:写 10 个字节,读回来发现从第 7 个字节开始变成别的数据。

原因:跨页了。25LC256 页写不能跨 64 字节边界,跨了就会回卷覆盖。

解决:在writeBuffer里按页拆分,每页单独调用writePage。拆分逻辑是:先算当前地址到页尾还有多少字节,取剩余长度和页尾空间的最小值,写完再算下一页。

bool Eeprom25lc256::writeBuffer(uint16_t addr, const uint8_t* data, uint16_t len) { while (len > 0) { uint16_t pageRemain = PAGE_SIZE - (addr % PAGE_SIZE); uint16_t chunk = (len < pageRemain) ? len : pageRemain; if (!writePage(addr, data, chunk)) return false; addr += chunk; data += chunk; len -= chunk; } return true; }

4.3 写操作偶尔失败

现象:大部分时候能写进去,偶尔读回来是旧数据。

原因:waitWriteComplete轮询不够,或者 WREN 没发成功。25LC256 的页写周期最大 5ms,如果轮询间隔太短,可能在 WIP 还没置位时就退出了。

解决:在waitWriteComplete里加超时机制,比如最多等 10ms,超时就返回错误。另外每次写之前都重新发 WREN,不要依赖上一次的 WEL 位。

4.4 模拟 SPI 速率上不去

现象:spiDelay降到 0 还是只能跑到几百 kHz。

原因:GPIO 翻转本身有开销,digitalWrite这类函数在有些平台很慢,一次调用可能几微秒。

解决:直接用寄存器操作替代库函数。比如 STM32 上用 BSRR 寄存器,AVR 上用 PORT 寄存器。源码里gpio_interface.h的pinWrite就是留给移植者优化的入口,我一般会把它改成宏或者内联函数。

4.5 多字节写入后芯片发热

现象:连续写大量数据时芯片明显发热。

原因:页写频率太高,芯片内部电荷泵一直在工作。

解决:每页写完后加 1ms 延时,或者降低写入频率。25LC256 的擦写寿命是 100 万次,但那是按页算的,频繁整片擦写会缩短寿命。存日志这类场景建议用环形缓冲,不要每次都擦整片。

5. 二次开发与移植技巧:从 STM32 到 ESP32 的改造路径

5.1 移植时先改 gpio_interface

这份源码的移植成本很低,核心就是把gpio_interface.h里的几个函数实现掉。以 STM32 为例,用 HAL 库的话:

// gpio_interface.h 的 STM32 实现 extern "C" void pinWrite(GPIO_TypeDef* port, uint16_t pin, uint8_t val) { HAL_GPIO_WritePin(port, pin, val ? GPIO_PIN_SET : GPIO_PIN_RESET); } extern "C" uint8_t pinRead(GPIO_TypeDef* port, uint16_t pin) { return HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_SET ? 1 : 0; } extern "C" void delayUs(uint32_t us) { // 用 DWT 或者 TIM 实现微秒延时 uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }

ESP32 的话,pinWrite换成gpio_set_level,delayUs用esp_rom_delay_us。注意 ESP32 的 GPIO 有输入输出切换开销,如果 MISO 和 MOSI 是同一根线(三线 SPI),需要在transfer里切换方向,这份源码默认是四线,三线需要自己改。

5.2 加一层缓存减少写入次数

EEPROM 的擦写寿命有限,如果应用层频繁写同一个地址,比如每秒存一次传感器数据,芯片很快就废了。我一般会在Eeprom25lc256上面再加一个CachedEeprom类,维护一个 64 字节的 RAM 缓存,只有缓存满了或者主动调用flush时才写回芯片。

class CachedEeprom { public: void writeByte(uint16_t addr, uint8_t val) { uint16_t page = addr / PAGE_SIZE; if (page != currentPage) { flush(); currentPage = page; readPage(page * PAGE_SIZE, cache); } cache[addr % PAGE_SIZE] = val; dirty = true; } void flush() { if (dirty) { eeprom.writePage(currentPage * PAGE_SIZE, cache, PAGE_SIZE); dirty = false; } } private: Eeprom25lc256 eeprom; uint8_t cache[PAGE_SIZE]; uint16_t currentPage = 0xFFFF; bool dirty = false; };

这样写同一个页里的数据,不管写多少次,实际只擦写一次。代价是掉电时可能丢最后没 flush 的数据,所以关键参数写完要立刻 flush。

5.3 用校验和判断数据有效性

EEPROM 上电时读出来的数据可能是乱的,尤其是第一次使用或者被擦除过。我习惯在存储结构里加一个 2 字节的 CRC16 或者简单的累加和,读的时候先校验,不通过就恢复默认值。

struct Config { uint16_t magic; // 0x55AA uint8_t params[60]; uint16_t checksum; // 前面所有字节的累加和 }; bool loadConfig(Config& cfg) { eeprom.readBuffer(0, (uint8_t*)&cfg, sizeof(cfg)); if (cfg.magic != 0x55AA) return false; uint16_t sum = 0; uint8_t* p = (uint8_t*)&cfg; for (int i = 0; i < sizeof(cfg) - 2; i++) sum += p[i]; return sum == cfg.checksum; }

magic用来判断是不是第一次上电,checksum用来判断数据有没有被破坏。这两个字段加起来 4 个字节,对于 32KB 的空间来说可以忽略。

5.4 调试时先用 RAM 模拟

在没有硬件或者硬件还没焊好的时候,可以把Eeprom25lc256替换成一个 RAM 数组的模拟实现,接口完全一样。这样应用层代码不用改,等硬件到了再换回真实驱动。我一般会在eeprom25lc256.h里加一个编译开关:

#ifdef EEPROM_SIMULATE class Eeprom25lc256 { uint8_t ram[32768]; public: void readBuffer(uint16_t addr, uint8_t* buf, uint16_t len) { memcpy(buf, &ram[addr], len); } // ... 其他函数类似 }; #endif

这个技巧在单元测试里特别有用,可以模拟写入失败、数据损坏这些异常情况,不用真的去拔芯片。

从那以后我每次移植 SPI 驱动,都强制先用 RAM 模拟跑一遍逻辑,再上真实硬件。这样能把时序问题和逻辑问题分开,省掉很多来回插拔杜邦线的功夫。希望帮到你。

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

相关新闻

模拟SPI驱动25LC256 EEPROM读写源码解析与移植指南

模拟SPI驱动25LC256 EEPROM读写源码解析与移植指南

简介&#xff1a;这份资源是面向嵌入式开发初学者与电子工程师的EEPROM 25LC256读写源码包&#xff0c;采用C实现SPI模拟读写操作&#xff0c;帮助读者理解非易失性存储器的通信原理与驱动编写方法。源码围绕SPI接口初始化、片选控制、命令与地址传输、读写使能、事务管理及错误…

2026/10/22 3:43:13 阅读更多 →
Streamlit应用集合实战:架构拆解、模块复用与部署避坑指南

Streamlit应用集合实战:架构拆解、模块复用与部署避坑指南

简介&#xff1a;这是一套基于 Streamlit 框架制作的网站应用示例集合&#xff0c;面向 Python 数据科学家、工程师及数据分析师&#xff0c;帮助不具备前端开发背景的读者快速搭建数据可视化与交互式应用&#xff0c;降低 Web 界面开发门槛。该库强调“代码即文档”&#xff0…

2026/10/22 1:43:58 阅读更多 →
Linux命令实战:从管道组合到服务器故障排查指南

Linux命令实战:从管道组合到服务器故障排查指南

1. 先别急着背命令清单&#xff1a;把命令当成跟系统对话早些年我刚接触 Linux 命令的时候&#xff0c;干过一件现在想起来有点好笑的事&#xff1a;找了一份《常用 Linux 命令大全》&#xff0c;把 ls、cd、cp、mv 的参数背得滚瓜烂熟&#xff0c;结果到了真正需要排查问题的时…

2026/10/22 5:02:44 阅读更多 →

最新新闻

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

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

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

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

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

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

2026/10/22 11:01:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

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

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

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

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

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

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

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

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

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

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

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

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

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

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

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

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

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

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

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

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

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

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 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/21 21:50:56 阅读更多 →
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/21 21:51:55 阅读更多 →
黑夜航拍船只数据集训练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/21 16:24:48 阅读更多 →