news 2026/9/30 1:07:13

AUTOSAR诊断开发:用“DTC的一生”讲透DEM模块核心机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AUTOSAR诊断开发:用“DTC的一生”讲透DEM模块核心机制

干了多年AUTOSAR诊断开发,要说哪个模块最磨人,我会毫不犹豫投DEM一票。别误会,不是因为DEM本身多难,而是它牵扯的环节实在太多:上层应用怎么报故障、底层怎么存储、UDS服务怎么读、NvM怎么写、BswM怎么切状态,一个DTC从产生到老化,几乎跟半个ECU都有关系。网上关于DEM的资料不少,但大多停留在PPT层面,翻来覆去就那几张状态机图。今天这篇文章我想换个角度,用“DTC的一生”这条时间线,把AUTOSAR的DEM模块从头到尾讲透。不管你是刚接触AutoSAR配置,还是已经被PMC排查折磨到失眠,这篇都值得你花二十分钟读完。

先说清楚DEM能解决什么问题:让一个诊断事件(比如“传感器对地短路”)从发生、确认、存储、上报,到被诊断仪读取、清除,整个过程有一套标准化机制来管理,不靠应用层自己记标志位。适合谁来参考?正在用Davinci Configurator或EB tresos做AUTOSAR集成的软件工程师、搞UDS/OBD诊断的测试工程师,以及想从“会点灯”进阶到“会诊断”的嵌入式新人。

1. 先搞懂DEM在整个AUTOSAR里到底站在哪个位置

1.1 DEM不是一个人单打独斗:DCM、NvM、BswM的职责分工

很多人一开始会混淆DCM和DEM。DCM(Diagnostic Communication Manager)管的是“通信”:解析诊断仪发过来的UDS报文,比如0x19、0x14、0x22、0x2E这些服务,它只管“车对外的话术”。DEM(Diagnostic Event Manager)管的是“事件记账”:到底当前哪些故障是发生了的、哪些是被确认过的、哪些是历史存储的、它们应该显示成什么状态位,这些都是DEM自己的活。

我给你打个比方。DCM是前台接待,负责接电话、记订单;DEM是财务和仓库,负责记录这批货到底有没有入库、库存是多少、还能不能查旧账。真正干脏活累活的是DEM,但客户(诊断仪)只跟DCM对话。

所以DEM本身就带了一堆对外的接口,它需要跟周围的模块打交道:

  • DCM:诊断仪请求服务时,DCM调用DEM的接口,比如Dem_GetStatusOfDTC、Dem_ClearDTC。
  • NvM(非易失存储管理):DTC是否确认、老化计数器、冻结帧数据、扩展数据,这些都是要掉电保存的。DEM不直接操作Flash,而是通过NvM的接口来做。
  • BswM(模式管理):DEM能驱动BswM做故障响应,比如某个DTC确认后,BswM可以降级控制策略,或者进入跛行模式。反过来,BswM也可以在ECU唤醒时通知DEM做初始化。
  • 应用层SWC(Software Component):它是故障信号的来源,通过RTE接口调用Dem_SetEventStatus或者直接通过Port读写的方式,告诉DEM“我现在检测到哪路电压异常了”。
  • EcuM:ECU启动、唤醒时,DEM需要跟着初始化NvM,这个时序是EcuM在管。

整个模块握手关系一多,问题就来了:集成的时候经常出现“DEM没起来、NvM还不可读、DCM已经开始记录诊断请求”这种时序冲突。这是后面排查的重灾区。

1.2 “事件”才是DEM的心脏:为什么AUTOSAR不直接叫故障码管理

AUTOSAR里有一组容易绕晕的概念:DemEvent(诊断事件)、DTC(诊断故障码)、DTCOrigin(故障码来源)、DiagStatus。你如果直接看SWS_DEM文档,很可能被这些术语搞疯。

我说一个基本结论:DEM管理的最小单位是Event,而不是直接管理DTC。一个Event可以对应一个DTC,多个Event也可能映射到同一个DTC。打个比方,DTC P0123是“节气门位置传感器电路故障”,它可以由“对电源短路”“对地短路”“信号合理性错误”三个Event映射而来。诊断仪读到DTC P0123时,它是被至少一个Event触发的,具体触发的是哪个Event,可以用扩展数据区分。

作为配置者,我们至少要建立三种映射关系:

  • Event → DTC编号:定义这个事件最终对外显示成什么DTC码。
  • Event → 状态位集合:当前这个事件该置哪些状态位(见2.2节)。
  • Event → 存储槽位:当它确认故障时,数据存到NvM哪个区域、冻结帧存多长。

这个概念一旦想通,后面读配置工具就有方向了:不是配“DTC”,而是配“Event”,然后把两个概念绑定起来。

2. 拆解DTC的一生:一个故障从发生到被人遗忘

2.1 故障探测:应用层拿什么信号判断“坏了”

故障不会凭空出现在DEM里,一定是应用层SWC先检测到某个物理量异常,然后把结果告诉DEM。怎么告诉?最常见的是通过RTE,在SWC里有一个Port连接到DemEvent,应用层用Dem_SetEventStatus(EventId, EventStatus)这个函数直接更新事件状态。

举个例子,检测发动机水温传感器对地短路:

/* 应用层SWC周期任务,每10ms执行 */ FUNC(void, App_10msTask)(void) { uint16_t adcValue = Adc_ReadValue(SENSOR_CH); if (adcValue > 5000) { /* 电压超过5V,认为对电源短路 */ Dem_SetEventStatus(DemConf_DemEventParameter_EvtSensorShortToPower, DEM_EVENT_STATUS_PREFAIL); Dem_SetEventStatus(DemConf_DemEventParameter_EvtSensorShortToPower, DEM_EVENT_STATUS_FAILED); } else { Dem_SetEventStatus(DemConf_DemEventParameter_EvtSensorShortToPower, DEM_EVENT_STATUS_PASS); } }

这里有几个点容易被新手误解。首先,DEM_EVENT_STATUS_PREFAIL和DEM_EVENT_STATUS_FAILED的区别:PREFAIL表示“进入故障的预备阈值还没到,但信号已经异常”,FAILED表示“达到故障判定阈值,基本算确诊”。如果你自己实现了防抖逻辑,可以直接传DEM_EVENT_STATUS_FAILED;如果你想让DEM内部的防抖功能参与判定,那就先给PREFAIL,由DEM自己翻转状态。

其次,事件状态的更新频率很关键。一般建议跟传感器的采样周期同步,异常信号你半天才扫一次,故障确认时间会被拉长,OBD的IUR(In-Use Rate)统计会受影响。实测下来,10ms周期是大多数项目的“甜点”,100ms也能用,但诊断体验会明显迟钝。

2.2 防抖与状态管理:状态掩码的真正含义

DTC状态掩码(StatusOfDTC)是UDS 0x19服务的核心,也是DEM数据在诊断仪上最直观的体现。它是个8位字节,每位代表一种诊断状态。标准ISO 14229-1定义如下:

位掩码含义什么情况下置1
bit00x01testFailed当前测试判定失败
bit10x02testFailedThisOperationCycle本次上电循环内测试失败过
bit20x04pendingDTC待确认状态,故障出现但还没满足确认条件
bit30x08confirmedDTC已确认故障,这是存储和显示的关键位
bit40x10testNotCompletedSinceLastClear自上次清除后测试还没完成过
bit50x20testFailedSinceLastClear自上次清除后测试失败过
bit60x40testNotCompletedThisOperationCycle本次上电循环内测试未完成
bit70x80保留/厂商自定义一般不用

假如诊断仪发一个0x19 02(按状态掩码读DTC),请求掩码是0x20(testFailedSinceLastClear),那DEM就需要把所有testFailedSinceLastClear为1的DTC报出去。这就是为什么你在实车上读到的DTC状态是类似0x28这样的值——0x28等于confirmedDTC(0x08)加上testFailedSinceLastClear(0x20),意思是“这个故障确实发生过,并且一直没有被清除掉”。这非常符合直觉:一个故障一旦被记录为已确认,在清除之前,它的confirmedDTC位就该一直保持为1。

那么pendingDTC呢?这个位专门用来“提前预告”。防抖机制里,当故障信号第一次出现但还没满足最终确认条件时,可以把pendingDTC置位。比如温度超过了阈值的一次采样,但防抖需要连续3次超阈值才能确诊,第1次时可以先标pending。等第3次到了,再置confirmed。这么做的目的,是让诊断仪能够及时发现“有苗头”的故障,而不必等完整防抖周期结束,这对法规诊断(如OBD)尤其重要。

状态位之间的翻转不是乱翻的,DEM内部有一套标准流程:一个Event从FAILED状态被Dem_SetEventStatus写入之后,DEM会去检查它的防抖条件,把testFailed、confirmedDTC、pendingDTC的状态位更新;当故障消失,状态会变成PASS,然后进行老化处理。配置防抖时常用的策略有三个:计数器法(Counter)、时间法(Time)、直接映射(None/Immediate)。

  • 计数器法:每次故障信号,计数器加1;每次通过信号,计数器减1。计数器超过阈值,确认故障。典型配置:阈值10,单周期加1。
  • 时间法:连续故障持续时间超过阈值,比如150ms。适合转速、电压这类连续模拟量。
  • 直接映射:应用层已自行判断,DEM不需要再防抖,Event一收到FAILED就置confirmed。省事,但前提是应用层逻辑足够可靠。

注意一点:同一个Event如果既配了计数法又配了老化周期,它的故障退出条件不只是故障信号消失,还要老化计数器走完。这个“时间差”在实车测试中非常容易让人误判“为什么故障清不掉”。

2.3 故障存储与老化:为什么DTC不会永远亮着

DTC一旦确认,它总不能只存在RAM里吧,下电就没了那还聊什么。DEM会把确认状态、老化计数器、冻结帧数据等按配置写入NvM。这里有一个概念叫“Fault Memory”(故障存储器),它通常被划分为Primary Memory和Secondary Memory。

  • Primary Memory:存真正的故障信息,比如状态位、事件ID、老化计数器。容量小但频繁读写。
  • Secondary Memory:存扩展内容,包括冻结帧(Freeze Frame)、扩展数据记录(Extended Data)。容量大但写入频率低。

NvM的写入也有讲究。DEM支持直接写入(Immediate)和延迟写入(Deferred)。直接写入适用于关键DTC,比如安全相关故障,每次状态变化立即写Flash。延迟写入则为了减少Flash擦写次数:先记在RAM,等ECU下电前统一把NvM刷下去。很多项目默认是延迟写入,于是你会遇到一个经典问题:ECU在故障刚发生时就瞬间掉电,状态还没来得及存盘,DTC就丢了。解决方法有两个:一是把关键事件改成Immediate存储;二是确保EcuM的下电时序足够长,让NvM有充分时间完成写操作。

再说“老化(Aging)”,这是个被低估的功能。已确认的DTC不是永远钉在故障码表里的,只要故障不再出现,经过若干个驾驶循环后,它会被自动清除。这就是ISO 14229里说的“Aging”:当DTC处于confirmed状态,但连续多个操作循环都没有再次检测到故障,老化计数器就会累加,达到设定的循环阈值后,confirmedDTC位清零,这个DTC就不再对外报出。

配置里有几个参数要格外留意:DemAgingCycleThreshold(老化阈值),按照OBD法规通常设为40次驾驶循环;DemAgingIterationCount(已经老化过的循环次数)。如果这两个参数在NvM里没有一个合适的初始值,会出现“明明跑了足够多的循环,老是不老化”的怪象。原因多半是计数器的初值被NvM恢复成了0,而不是从1开始。

2.4 故障清除:UDS 0x14清除诊断信息的完整链路

诊断仪发0x14(ClearDiagnosticInformation)时,DCM会检查安全等级和会话模式。注意:很多ECU要求0x14必须在扩展会话或编程会话下,并且通过了27服务的安全解锁才能执行。DCM验证通过后,才会调用到DEM的Dem_ClearDTC。

Dem_ClearDTC会做三件事:

  • 把指定的DTC或者全部DTC的confirmedDTC、pendingDTC、testFailedSinceLastClear这些状态位清干净;
  • 清除老化计数器;
  • 删除对应的冻结帧和扩展数据记录。

清完之后,之前存的NvM数据也会被重置。所以如果你测试时发现0x14清不掉,先别怀疑DEM,先查DCM那边的SecurityLevel和Session配置对不对,这个坑我踩过不止一次。

3. 从零配置DEM:以Davinci Configurator为例的实操全过程

3.1 配置前的准备:SWC接口先把好,别在RTE上栽跟头

用Vector的Davinci Configurator的话,DEM的配置无非两大块:一是ECU层面的Dem模块参数,二是SWC和RTE的接口关联。别一上来就闷头在Dem界面里点,先把SWC想清楚。

我建议的顺序是:先在Davinci Developer里定义好应用层的Port,明确哪个Port输出故障状态,哪个Port接收DEM反馈,然后再到Configurator里把DemEvent关联到这些Port上。

常见的接口方案有两种:一是应用层调用Dem_SetEventStatus这种服务接口;二是定义专用的SenderReceiverPort,周期性地把状态写进Port,DEM通过RTE去读。后者在工具链上更“AUTOSAR味”,但会引入RTE生成时端口方向、DataElement类型不一致的风险。

避坑指南里最实用的一条:如果你用Port方式,注意选择正确的Runnable。我遇到过一个问题——SWC里明明写了Rte_Write_xxx,生成的代码里却怎么也找不到这个函数。最后发现是Runnable没映射到对应的Port。换句话说,Port只是“数据通路”,真正触发写入的Runnable必须和Port绑定,而且要挂到正确的周期Task上。建议每个周期Runnable里只写本周期使用的Port,不要图方便把好几个逻辑塞到一个Runnable里,后期调试RTE满天飞的时候会崩溃。

3.2 DemEvent核心参数逐一过一遍

打开Dem模块,创建Event。我个人习惯把Event的命名直接写成“Evt_信号名_故障类型”,比如Evt_WaterTempSensor_ShortToGround,这样DTC映射、冻结帧、故障码表导出来之后一眼能看懂。

每个Event需要关注的关键参数我列一下:

  • DTC编号:3字节标准,按ISO 14229的格式填写,比如P0123对应0x0123(厂商部分可能定义成0xC123)。注意DTC的字节序,配置工具里显示的是32位值,但实际UDS报文里发的是高字节在前还是低字节在前,各厂商习惯不同,一定要跟诊断调查表对齐。
  • Debounce策略:选择Counter或Time,并给参数。计数器法就填DebounceCounterThreshold、DebounceCounterIncrementStep、DebounceCounterDecrementStep。时间法就填DebounceTimeThreshold,单位通常是毫秒。
  • Event存储位置:Primary还是Secondary,是否允许用Deferred模式写入NvM。
  • 老化相关:是否启用Aging,阈值填多少,单位是“个操作循环”还是“个时间周期”。
  • Port关联:这个Event通过哪个RTE端口跟SWC连接。
  • OBD相关:是否属于OBD监控,需要给到0x19 01的服务里。

这里有一个很重要的细节:一个DTC可能由多个Event共享,那么你在配置每个Event的DTC编号时,工具会默认生成一个DTC实体。此时要留意“DemDtcId”的唯一性。曾经有项目配了两个Event引用同一个DTC号,但DemDtcId被工具生成了两个,直接导致0x19 02查状态时同一个DTC出现两次,测试报告直接被客户打回。

3.3 生成代码与集成检查

配置完成后,生成代码是关键一步。生成后的文件主要包括Dem_Cfg.h、Dem_Cfg.c、Dem_PBcfg.c(含配置描述,有时在GeneratedArtifacts目录下)。先别急着编译,花十分钟检查这几个点:

  • 检查Dem_Cfg.c里有没有生成你需要的EventId宏,比如DemConf_DemEventParameter_EvtSensorShortToPower,不出意外它已经是一个枚举枚举值。
  • 检查生成的Dem_Cfg.c中DTC状态掩码相关的映射表是否正确。
  • 检查NvM的Block是否已经与DEM的存储需求关联。DEM会生成一块或多个NvM Block,如果NvM里没配,启动时DEM读取NvM会失败,DTC状态会变成“未初始化”状态。

然后编译,烧录,用CANoe/CDS或PCAN连上ECU,发UDS 0x19 02去读。第一次能读出预期DTC,基本就说明DEM的“接收-存储-上报”链路通了。但别高兴太早,这只是开始,后面有更多坑等着。

3.4 与0x19、0x85、0x28这些“邻居”服务怎么配合

DEM不是孤立存在的,诊断仪最终是通过DCM的服务来操作DEM的数据。除了0x14清除,我们还要看三个常见服务:

  • 0x19 01/02/04:按DTC状态或分组读取,核心就是DEM的Dem_GetStatusOfDTC;
  • 0x85(ControlDTCSetting):当诊断仪发送“DTCOn/Off”控制时,DEM要暂时停止DTC记录。这个功能在产线标定时很有用,可以避免测试过程中产生一堆干扰故障码。配置时注意:0x85的On/Off状态要让DEM和DCM都知道,否则会出现“关掉了还在记录”的问题。
  • 0x28(CommunicationControl):这个不直接归DEM管,但它会影响ECU的通信和网络唤醒,如果通信关掉了,后续的诊断请求就发不进来了。在配置时要留意模块间的交互:0x28把通信关掉后,如果ECU还处于诊断会话,DEM和NvM之间依然可以正常工作,因为NvM不依赖通信;但网络管理报文不发了,也会影响外部诊断仪发现ECU的能力。

AUTOSAR网络管理是另一条线。网络管理状态(Network Mode、Prepare Bus-Sleep Mode)决定了ECU是否保持唤醒。如果你的ECU在做DTC老化测试时,网络管理状态切换太快,直接进入Bus-Sleep模式,那么DTC状态的更新和NvM存储都没有足够时间完成。实测中,很多“DTC丢了”“老化没执行”的案例,归根到底不是DEM配置错了,而是网络没“撑住”。建议在EcuM和NvM的时序里做一次确认:在下电前,NvM先写完,再切报文唤醒网络,再进睡眠。

4. 实测中最常见的几个DEM坑与排查

4.1 DTC状态位不更新、状态掩码错误

这是测试日报里最常出现的问题,典型的“明明清了故障,状态掩码还是0x28”“明明是新的故障,confirmedDTC位居然一直是0”。排查思路可以按这几步来:

  1. 用调试器确认应用层是否真的调用了Dem_SetEventStatus,断点打在调用处,看看传入的EventId和status值。
  2. 查看EventId是否被宏定义正确。工具版本升级后,旧的DemConf_DemEventParameter_xxx枚举名可能变化,如果你代码里用的是旧名称,编译能过就怪了。
  3. 检查防抖计数器的阈值。如果阈值设置得很大,比如100,而你的故障信号只持续了3个周期,那pendingDTC肯定置不了,更容易直接PASS。
  4. 确认NvM有没有正确读到旧的DTC状态。如果NvM数据全是0xFF(未初始化),DEM会认为没有DTC存储记录。

另外还有一类“状态掩码是乱值”的问题,比如0x51这种不合逻辑的组合。多数情况下是DTC状态字节在配置工具里被手动修改过,和DEM生成的逻辑码表不一致。解决办法是重新生成代码并做一遍出厂默认值设置。

4.2 NvM存储/老化异常,掉电后DTC丢失

我们项目里就遇到过:故障确认后,诊断仪一读,状态位完全正常;但ECU断电重启再读,DTC变成了“曾经有过”的pending状态,或者干脆什么都没了。排查下来,问题出在NvM Block的“Deferred”机制上:配置中是延迟写入,ECU掉电太快,NvM根本来不及把RAM里的状态刷进Flash。

还有一类老化和NvM相关的坑:老化条件已经满足,但DTC就是“清不掉”。看实现代码会发现,老化计数器是在NvM里保存的,如果NvM写失败,DEM永远认为还差一次循环。所以遇到“老化卡住”的Bug,去翻NvM的读写状态,往往比改DEM参数更有效。

4.3 事件ID冲突与DTC编号重复

多个Event共享一个DTC编号是允许的,但工具上可能会有两种配置方式:一种是在Event里直接填DTC编号,另一种是先定义DTC实体,再在Event里引用。这俩方式看起来差不多,但生成出来的DTC排查表会不一样。如果你采用“DTC实体+Event引用”的方式,在生成DTC列表时,多个Event会被合并成一个DTC实体,不会出现重复项;如果你在每个Event里都填一遍DTC编号,有的工具就会生成两个DTC实体,到时候0x19 01列表里就会看到两个一模一样的DTC。

排查方法很简单:打开生成的Dem_Cfg.c,对比一下每个DemDtcId对应的DTC值;再用UDS测试仪发0x19 01,扫一遍响应列表,应该不会出现同一个DTC码出现两次的情况。

4.4 多核与中断:Port访问/RTE通信问题

现在ECU跨核越来越多,DEM如果放在Core0,而应用层SWC在Core1,RTE跨核通信就会引入延迟和一致性风险。典型问题是:Core1把故障状态写到Port,Core0的DEM要等下一个周期才读到,导致状态位响应慢了一拍。如果PLC或HIL测试里对“故障确认时间”有精确要求,这就会成为问题。

另一个更具杀伤力的问题是:在中断服务里调用Dem_SetEventStatus。DEM内部有临界区保护,但在中断上下文里调用可能会导致优先级反转或者死锁。建议的做法是:中断里只置普通变量,由周期任务读取并喂给DEM。如果非得在中断里调用,一定要仔细审查DEM的Dem_SetEventStatus实现是否有OS级别的中断保护,以及调用的OS优先级是否匹配。

4.5 排查速查表

现象优先排查点常见解决方向
DTC状态掩码一直不变应用层EventId、防抖阈值检查SWC是否真的调用了Dem_SetEventStatus;检查Debounce配置是否过于苛刻
掉电后DTC丢失NvM写时序、Block关联改为Immediate写入,或延长下电时序
老化不执行AgingCycleThreshold、NvM恢复值检查老化计数器初值,确认NvM读出的值不是0
同一个DTC出现两次DTC实体重复检查Event的DTC引用方式,合并实体
0x14清不掉DCM的Session/Security、NvM写失败先读DCM的日志,确认27解锁是否成功
故障确认慢Runnable周期、降级跨核缩短SWC周期或改为直接映射
诊断请求无响应网络管理状态、0x28配置让ECU保持网络模式,延长Bus-Sleep时间

最后再分享一点个人习惯。每完成一个新项目的DEM配置,我都会做一张完整的“DTC生命周期测试矩阵”,覆盖:首次故障确认、掉电重启、老化循环、0x14清除、0x85关闭记录、0x28关闭通信这6个维度。在ECU上跑一遍再交付给测试组,能省掉后面一大半扯皮。DEM这东西,说难不难,但细节是真的多,能把每个DTC从出生到消亡的路径都捋清楚,你就算真正入门AUTOSAR诊断了。如果你在配置过程中遇到特别奇葩的DEM问题,欢迎在评论区发出来,我可能在下期专门挑几个有代表性的案例拆一拆。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:07:11

Java在线教育系统源码:部署、改造与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:05:52

分类、回归与目标检测评价指标:从混淆矩阵到mAP避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:05:20

EMQX MQTT ACL 发布订阅权限配置与排障实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:04:55

Django/Flask项目打包成exe全过程:从PyInstaller到Inno Setup

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:04:46

Pygame五子棋工程化框架:分层架构与可扩展设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:04:44

NVMe驱动开发入门:从PCIe枚举到块设备注册的完整链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华