汽车行驶记录仪代码:CAN报文采集与断电保护存储实战
简介这份资源是面向嵌入式开发与汽车电子方向学习者的汽车行驶记录仪俗称“黑匣子”STM32代码工程适合具备一定C语言与单片机基础、希望深入理解车载数据采集与存储实现的中高级开发者。代码围绕加速度传感器接口、数据记录与Flash/SD卡存储、实时超速与碰撞报警、GPS时间同步与NMEA解析、电源管理及故障自检等模块展开完整呈现了行驶数据记录与分析的技术链路。资源包共422个文件以94个h头文件、78个c源文件为核心辅以69个o目标文件、68个crf交叉引用文件及uvproj、uvopt等Keil工程配置另含少量doc、docx说明文档与bin、hex固件整体约15.99MB工程结构完整、便于直接编译与二次开发。目前已有1825人学习下载读者可借此掌握传感器通信、数据格式化存储、实时事件触发与低功耗设计等关键实现思路对提升汽车电子系统研发能力具有实际参考价值。1. 汽车行驶记录仪代码从CAN报文到合规存储的完整链路很多做车载终端的同行第一次接触汽车行驶记录仪代码都会以为它就是个“带GPS的SD卡录像机”。真上手才发现这套代码要同时处理CAN总线上的车速、刹车、转向信号要按固定周期把原始报文落盘还要在事故后能还原出最后几十秒的车辆状态。它解决的不是“记录”问题而是“可信记录”问题——数据不能被篡改、不能丢帧、断电后还能续上。适合谁看做T-Box、车队管理终端、车载DVR的嵌入式工程师以及需要把记录仪数据接进自己平台的开发者。下面按我实际调过的顺序从数据采集讲到存储格式再讲几个让我熬夜的坑。2. 汽车行驶记录仪代码的数据采集层CAN报文怎么收才不丢2.1 为什么轮询方式在记录仪里必翻车记录仪代码最底层是CAN控制器驱动。很多新手直接用主循环轮询接收寄存器觉得“我循环够快就行”。但车辆CAN总线在急加速、ABS触发时报文密度会瞬间拉高轮询一旦被SD卡写入或GPS解析阻塞接收FIFO就溢出。溢出意味着什么刹车信号丢了事故分析时这段数据就是黑匣子里的空白。常见做法是开中断接收把报文塞进环形缓冲区主循环只负责从缓冲区取数据。环形缓冲区大小要按最坏情况算假设总线负载率60%500kbps下每秒约3000帧记录仪主循环最坏阻塞200ms那缓冲区至少能存600帧。我一般留1024帧的余量。// can_rx_buffer.h 环形缓冲区定义 #define CAN_RX_BUF_SIZE 1024 // 必须为2的幂方便用位运算取模 typedef struct { uint32_t id; uint8_t data[8]; uint8_t dlc; uint32_t timestamp_ms; } can_frame_t; typedef struct { can_frame_t buf[CAN_RX_BUF_SIZE]; volatile uint32_t head; // 中断里写 volatile uint32_t tail; // 主循环读 } can_ring_t; // 中断服务函数里调用只做入队不做任何耗时操作 static inline bool can_ring_push(can_ring_t *r, const can_frame_t *f) { uint32_t next (r-head 1) (CAN_RX_BUF_SIZE - 1); if (next r-tail) { return false; // 缓冲区满记录溢出计数不要死等 } r-buf[r-head] *f; r-head next; return true; }这段代码的关键点head和tail用volatile修饰防止编译器优化导致中断和主循环看到的值不一致。缓冲区大小取2的幂取模用位与代替除法中断里省几十个时钟周期。can_ring_push返回false时不要阻塞而是累加一个溢出计数器后续可以把这个计数写进记录文件头作为数据可信度的标记。2.2 时间戳从哪来CAN报文自带的还是本地打的汽车行驶记录仪代码里时间戳的精度直接决定事故还原的可信度。CAN报文本身没有全局时间戳只有本地接收时刻。我见过两种做法一种是用MCU的SysTick打毫秒时间戳另一种是接GPS的PPS秒脉冲做同步。纯SysTick的问题在于晶振温漂跑一天可能差几秒。GPS PPS能到微秒级但隧道里没信号。我的做法是混合有GPS时用PPS校准本地时钟没GPS时用MCU内部RTC保持同时记录“时钟源状态”标志位。这样事后分析时能知道哪段数据的时间戳是校准过的哪段是自由运行的。// 时间戳生成逻辑放在CAN中断里 static uint32_t last_pps_tick 0; static uint32_t pps_utc_sec 0; uint32_t get_can_timestamp(void) { uint32_t now systick_get_ms(); if (gps_pps_valid()) { // 有PPS用PPS对齐后的时间 return pps_utc_sec * 1000 (now - last_pps_tick); } else { // 无PPS用本地RTC标记时钟源为自由运行 return rtc_get_ms(); } }参数说明last_pps_tick在PPS中断里更新pps_utc_sec从GPS报文解析。注意now - last_pps_tick可能溢出但32位毫秒计数要49天才溢出记录仪单次行程不会超过这个数可以不做溢出处理但要在代码注释里写清楚这个假设。2.3 过滤与降采样不是所有报文都值得存一辆车CAN总线上可能有上百种报文记录仪全存的话32GB卡也撑不过几天。汽车行驶记录仪代码必须做过滤。法规通常要求记录车速、刹车、转向、灯光、发动机转速、故障码。其他舒适性报文空调、座椅直接丢弃。过滤表用ID掩码实现比逐个比较快// 需要记录的CAN ID白名单用掩码匹配 typedef struct { uint32_t id; uint32_t mask; } can_filter_t; static const can_filter_t record_filters[] { {0x0C0, 0x7F0}, // 发动机转速 {0x1A0, 0x7F0}, // 车速 {0x2F0, 0x7F0}, // 刹车与转向 {0x3B0, 0x7F0}, // 灯光状态 }; bool should_record(uint32_t id) { for (int i 0; i sizeof(record_filters)/sizeof(record_filters[0]); i) { if ((id record_filters[i].mask) record_filters[i].id) { return true; } } return false; }掩码匹配的好处是能覆盖同一功能的不同变体ID。比如车速报文可能有0x1A0和0x1A1两个ID掩码0x7F0能把它们都匹配上。降采样方面车速这种变化慢的信号可以每100ms存一次刹车信号必须每帧都存因为刹车抖动可能只有几十毫秒。3. 汽车行驶记录仪代码的存储格式二进制块与断电保护3.1 为什么不能用CSV或JSON直接写我见过一个团队用CSV格式存记录数据每帧一行方便用Excel打开。结果跑了两天就翻车SD卡写入速度跟不上文件系统碎片化断电后整个文件损坏。汽车行驶记录仪代码的存储必须用二进制块按固定大小对齐配合日志式追加写入。我的做法是每512字节一个块块头16字节包含块序号、时间戳范围、帧数、CRC32。块内是紧凑的帧记录每帧12字节4字节时间戳4字节ID4字节数据。这样一帧只占12字节比CSV的50多字节省了75%空间。// 存储块结构定义 #define BLOCK_SIZE 512 #define BLOCK_HEADER_SIZE 16 #define FRAME_SIZE 12 typedef struct { uint32_t block_seq; // 块序号用于检测丢块 uint32_t start_ts; // 块内第一帧时间戳 uint32_t end_ts; // 块内最后一帧时间戳 uint16_t frame_count; // 本块帧数 uint16_t reserved; uint32_t crc32; // 块头数据的CRC } block_header_t; typedef struct { uint32_t ts; // 毫秒时间戳 uint32_t id; // CAN ID uint8_t data[4]; // 只存前4字节记录仪够用 } frame_record_t;参数说明block_seq从0开始递增如果读取时发现序号跳变说明中间有块丢失。crc32覆盖块头和块内所有帧写入前计算读取时校验。data只存4字节是权衡结果——记录仪关心的车速、刹车、转速都在前4字节里存8字节会浪费空间。3.2 断电保护双块交替与写前擦除记录仪最怕的是写卡过程中断电。如果正在擦除一个扇区时断电那个扇区可能变成半擦除状态数据全丢。汽车行驶记录仪代码的常见做法是双块交替准备两个物理块A和B写A的时候B是完整的写B的时候A是完整的。每次写完一个块更新一个“当前有效块”标志。// 双块交替写入逻辑 static uint32_t active_block 0; // 0或1 static uint32_t block_addr[2] {0x1000, 0x2000}; bool write_record_block(const uint8_t *data, uint32_t len) { uint32_t next_block 1 - active_block; uint32_t addr block_addr[next_block]; // 先擦除下一个块 if (!flash_erase_sector(addr)) { return false; } // 写入新数据 if (!flash_write(addr, data, len)) { return false; } // 更新有效块标志这一步是原子的 if (!flash_write_meta(active_block, next_block)) { return false; } active_block next_block; return true; }关键点flash_write_meta只写一个字节的标志位这个操作要么成功要么失败不会出现半写状态。如果断电发生在擦除或写入过程中active_block仍然指向旧块数据不丢。恢复时读active_block标志就知道哪个块是完整的。3.3 文件系统选型FATFS还是裸块管理很多记录仪代码跑在RTOS上直接用FATFS写文件。FATFS的好处是PC能直接读卡坏处是文件系统本身有元数据开销断电时FAT表可能损坏。我的选择是小容量记录仪8GB用裸块管理自己维护块映射大容量用FATFS但配合日志式写入每次写完调用f_sync。裸块管理的代码量不大核心就是块分配表和坏块标记。块分配表存在Flash的固定区域每记录一个块就在表里标记已用。读取时按块序号顺序读跳过坏块。这种方式没有文件系统开销断电后恢复也简单——扫描块头CRC有效的就保留。// 裸块管理的块分配表 #define MAX_BLOCKS 16384 // 8GB / 512B static uint8_t block_map[MAX_BLOCKS / 8]; // 位图1表示已用 bool alloc_block(uint32_t *out_seq) { for (uint32_t i 0; i MAX_BLOCKS; i) { if (!(block_map[i/8] (1 (i%8)))) { block_map[i/8] | (1 (i%8)); *out_seq i; return true; } } return false; // 卡满需要触发覆盖策略 }位图占2KB可以放在RAM里每次分配后写回Flash。注意写回频率不要太高可以累积16个块再写一次减少Flash擦写次数。4. 汽车行驶记录仪代码避坑五个让我熬夜的现场问题4.1 现象记录文件里车速突然跳到65535原因CAN信号解析时没有做物理值转换。原始报文里车速是2字节单位0.01km/h直接当整数存了。65535对应655.35km/h明显是无效值。解决在解析层加物理值转换和范围校验。车速有效范围0~250km/h超出范围的标记为无效帧不写入记录。// 车速解析与校验 #define SPEED_INVALID 0xFFFF uint16_t parse_vehicle_speed(uint8_t *data) { uint16_t raw (data[0] 8) | data[1]; if (raw SPEED_INVALID || raw 25000) { // 250km/h 25000 return 0xFFFF; // 标记无效 } return raw; // 单位0.01km/h }4.2 现象SD卡写满后记录仪死机原因代码里f_write返回失败后没有处理继续循环调用导致任务阻塞。SD卡满时写入返回FR_DISK_ERR或FR_OK但实际没写进去。解决写失败时触发覆盖策略——删除最旧的记录块释放空间。同时记录一个“覆盖计数”事后分析时知道哪些数据被覆盖了。// 写失败时的覆盖策略 if (f_write(file, buf, len, bw) ! FR_OK || bw ! len) { // 删除最旧的10%块 delete_oldest_blocks(MAX_BLOCKS / 10); // 重试一次 f_write(file, buf, len, bw); }4.3 现象GPS时间跳变导致记录时间戳乱序原因GPS冷启动时输出的是错误时间热启动后跳变到正确时间。如果直接用GPS时间打时间戳会出现时间倒流。解决加一个时间合理性检查——新时间与旧时间差超过5秒时标记为“时间跳变”后续帧用本地RTC继续直到GPS时间稳定。// GPS时间合理性检查 static uint32_t last_gps_ts 0; bool gps_time_valid(uint32_t new_ts) { if (last_gps_ts 0) { last_gps_ts new_ts; return true; } int32_t diff (int32_t)(new_ts - last_gps_ts); if (diff -5000 || diff 5000) { // 跳变超过5秒 return false; } last_gps_ts new_ts; return true; }4.4 现象CAN总线负载高时记录仪丢帧原因中断里做了太多事情比如直接写SD卡或解析GPS。中断服务函数应该只做入队其他都在主循环做。解决把中断里的逻辑精简到只读寄存器和入队。我见过最夸张的代码在CAN中断里调用printf一帧打印几十毫秒不丢帧才怪。4.5 现象记录文件在PC上打不开原因裸块管理没有文件系统PC不认。或者FATFS的簇大小设置不对大卡用小簇导致文件系统开销过大。解决如果要用PC读加一个导出工具把裸块转成CSV或二进制文件。或者用FATFS但簇大小设为32KB以上减少FAT表项数量。5. 汽车行驶记录仪代码的进阶技巧用环形日志做数据回溯5.1 环形日志的设计思路记录仪存储空间有限不可能无限追加。我的做法是把整个存储卡当成一个环形日志写满后从最旧的块开始覆盖。但覆盖不是简单的从头写而是维护一个“写指针”和“最旧有效块指针”。写指针追上最旧指针时最旧指针前移。// 环形日志管理 typedef struct { uint32_t write_seq; // 下一个要写的块序号 uint32_t oldest_seq; // 最旧的有效块序号 uint32_t total_blocks; // 总块数 } ring_log_t; void ring_log_advance(ring_log_t *log) { log-write_seq (log-write_seq 1) % log-total_blocks; if (log-write_seq log-oldest_seq) { // 写指针追上最旧指针最旧指针前移 log-oldest_seq (log-oldest_seq 1) % log-total_blocks; } }这个设计的精妙之处在于不需要删除操作覆盖就是写新数据。读取时从oldest_seq读到write_seq中间的就是有效数据。断电恢复时扫描所有块头找到CRC有效的块重建write_seq和oldest_seq。5.2 数据回溯的查询接口有了环形日志查询“某时间段的数据”就很简单遍历有效块按块头的时间戳范围过滤。我一般会加一个内存索引记录每个块的时间范围查询时先查索引再读块避免全卡扫描。// 块索引项 typedef struct { uint32_t seq; uint32_t start_ts; uint32_t end_ts; } block_index_t; // 查询指定时间段的块 int query_blocks(block_index_t *index, int count, uint32_t start, uint32_t end, uint32_t *out_seqs, int max_out) { int found 0; for (int i 0; i count found max_out; i) { if (index[i].end_ts start index[i].start_ts end) { out_seqs[found] index[i].seq; } } return found; }索引存在RAM里启动时从块头重建。索引项12字节16384个块占192KB对于带外部RAM的MCU可以接受。如果没有外部RAM可以只索引最近1000个块更早的靠全卡扫描。5.3 验证方法用模拟CAN回放测试代码写完后怎么验证我一般用PCAN或周立功的CAN卡把实车采集的报文回放给记录仪。回放时故意制造总线负载尖峰和断电看记录仪能不能正确记录和恢复。测试用例包括正常回放10分钟、总线负载80%回放、写入过程中断电、GPS信号丢失、SD卡拔插。每个用例跑完把记录文件导出来用Python脚本解析对比原始报文和记录数据统计丢帧率和时间戳误差。# 解析记录文件并对比 import struct def parse_record_file(path): frames [] with open(path, rb) as f: while True: header f.read(16) if len(header) 16: break seq, start_ts, end_ts, count, _, crc struct.unpack(IIIHHI, header) for _ in range(count): ts, cid, data struct.unpack(II4s, f.read(12)) frames.append((ts, cid, data)) return frames # 对比原始报文和记录数据 orig parse_record_file(original.bin) recorded parse_record_file(recorded.bin) lost len(orig) - len(recorded) print(f丢帧率: {lost/len(orig)*100:.2f}%)这个脚本能快速给出丢帧率。如果丢帧率超过0.1%就要检查环形缓冲区大小和中断延迟。时间戳误差用recorded[i][0] - orig[i][0]统计正常应该在±10ms以内。5.4 一个让我后悔的教训早期版本我没加块头CRC觉得SD卡写入不会出错。结果有一次现场返回的数据里某个块的数据全是0xFF明显是擦除后没写成功。因为没有CRC读取时无法判断这个块是否有效只能靠块序号跳变来猜。后来加了CRC读取时先校验无效块直接跳过数据可信度立刻上来了。所以我的习惯是任何落盘的数据都要有校验任何校验失败都要有明确的处理路径。记录仪代码不是功能跑通就行它要在各种异常下都能给出可信的结果。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

W5500 TCP Server连接频繁断开?硬件协议栈寄存器调试与稳定性修复实战

W5500 TCP Server连接频繁断开?硬件协议栈寄存器调试与稳定性修复实战

简介:面向嵌入式与物联网开发者,本工程提供W5500 TCP Server稳定通信的完整参考实现,专门解决网络波动、路由重置或设备休眠导致的连接偶发断开问题,核心采用TCP Keep-Alive心跳机制,适合基于STM32F10x系列MCU配合W550…

2026/10/13 22:56:13 阅读更多 →
MATLAB害虫检测识别系统:从图像预处理到分类器选型的完整实现

MATLAB害虫检测识别系统:从图像预处理到分类器选型的完整实现

简介:这份MATLAB害虫检测识别系统面向植物保护、农业信息化方向的本科生与研究生,尤其适合作为毕业设计或课程设计参考,解决叶片虫害等级自动判别的问题。系统基于颜色特征实现四分类,可区分正常、轻度、中度与严重虫害&#xff0…

2026/10/13 22:56:13 阅读更多 →
小程序完整源码本地运行与二次开发避坑指南

小程序完整源码本地运行与二次开发避坑指南

简介:这份小程序完整源码资源包面向希望系统学习微信小程序开发的开发者,覆盖从基础到进阶的多种应用场景,可作为研究框架原理与实战技巧的参考资料。包内共收录多种类型的小程序项目,压缩包约130.08MB,以zip格式打包&…

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

最新新闻

金融时间序列建模全流程:从平稳性检验到GARCH波动率预测

金融时间序列建模全流程:从平稳性检验到GARCH波动率预测

1. 从第一节课到第四章:金融时间序列到底在研究什么如果你正在啃金融相关的课程,大概率会在一门叫《金融时间序列分析》或类似名字的课上卡住。这门课和传统统计学、计量经济学都不一样,它研究的是股票收益率、汇率波动、债券利差这类有“先后…

2026/10/14 0:48:55 阅读更多 →
AgenticOps工程:企业自动化运维的自主决策革命

AgenticOps工程:企业自动化运维的自主决策革命

1. AgenticOps Engineering 的本质与价值AgenticOps Engineering 正在重新定义企业自动化运维的边界。与传统自动化工具不同,这种新型工程范式通过赋予系统自主决策能力,使企业IT基础设施能够像拥有"数字员工"一样主动感知环境变化、预测问题并…

2026/10/14 0:48:55 阅读更多 →
用插件重构知识工作流:浏览器、编辑器与知识库的自动化实践

用插件重构知识工作流:浏览器、编辑器与知识库的自动化实践

知识工作者这个词听起来有点抽象,但落到日常就非常具体:你可能天天在写方案、查资料、写代码、记笔记、做汇报,一天结束回头一看,真正用在"思考"上的时间反而不多,大量时间被复制粘贴、切换窗口、找文件、重…

2026/10/14 0:48:55 阅读更多 →
ABAP单元测试效率提升:ADT工具链实战

ABAP单元测试效率提升:ADT工具链实战

1. 项目概述:当ABAP单元测试遇上效率革命在SAP开发领域,ABAP Unit作为核心测试框架已经存在多年,但真正能将其融入日常开发的团队却不多见。问题不在于框架本身,而在于传统测试流程中存在太多效率断层——从编写测试用例到执行测试…

2026/10/14 0:48:55 阅读更多 →
Python远程批量关机实战:WinRM/WMI/SMB三路径选型与避坑

Python远程批量关机实战:WinRM/WMI/SMB三路径选型与避坑

简介:本资源是一份面向Windows系统管理员与IT运维人员的Python自动化脚本工具包,解决多台设备集中管理场景下的远程批量关机需求,适用于机房巡检、课室终端维护、故障应急处置等实际运维任务。压缩包共3个文件(923B)&a…

2026/10/14 0:48:55 阅读更多 →
尖叫声音样本数据集eva:从包结构到训练基线的完整实践

尖叫声音样本数据集eva:从包结构到训练基线的完整实践

简介:EVA尖叫声音样本数据集是一份面向音乐信息检索(MIR)与音频分类研究的人声尖叫样本库,由台湾清华大学MIR课程项目整理,旨在为尖叫检测、异常声音识别、SVM分类等实验提供稀缺的领域数据。压缩包共63个文件&#xf…

2026/10/14 0:47:55 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →