news 2026/9/24 12:47:22

STM32G0B1 FDCAN实战:从CubeMX配置到CAN FD收发调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32G0B1 FDCAN实战:从CubeMX配置到CAN FD收发调试

STM32G0B1的FDCAN外设我前后调了两个项目,第一次是纯粹为了跑通CAN FD协议栈,第二次是把主节点68字节的大包往总线上发。如果你也卡在CubeMX配置、HAL库收发这类地方,这篇文章正好对路。STM32G0B1这颗料看起来和普通G0差不多,但内置的FDCAN让它处理大报文时比经典CAN舒服太多,配合CubeMX的图形化配置,从零建工程到跑通CAN FD通信,其实半天时间就够。下面我把实际操作过程、踩过的坑、调试工具怎么接,全部串起来写给你,尤其是波特率配置和Tx Buffer分配这两块,最容易出问题。

1. 为什么是G0B1和FDCAN,而不是经典CAN

1.1 CAN FD到底改了什么东西

经典CAN 2.0的帧最多带8字节数据,最高速率一般也就做到1Mbps,这在老式的车门控制、电池管理、工业设备里够用,但一旦总线上的节点多起来,或者需要传输固件升级包、诊断数据、标定表这类动辄几十字节的东西,8字节一帧就非常憋屈。CAN FD和经典CAN比,核心变化就是数据场长度从8字节扩到了64字节,并且允许在仲裁段使用一个波特率,进入数据段后切换到更高的波特率,这就是BRS位干的事。

很多人容易把“CAN FD”和“更高的波特率”混为一谈。实际上CAN FD有两种模式:一种是不带BRS,只有64字节数据场,但整个帧的位速率和经典CAN一样;另一种是带BRS,仲裁段用比如500kbps保证多节点仲裁稳定,数据段用2Mbps甚至更高来缩短传输时间。实际项目里基本都会开BRS,否则只换了帧格式不换速率,意义不大。CAN FD的帧头仍然是标准CAN协议格式,所以总线上的仲裁机制没有变,带CAN FD控制器的节点也能收发经典CAN帧,这点在设计兼容性时非常好用。

1.2 STM32G0B1这颗料好在哪

STM32G0B1属于Cortex-M0+内核,主频64MHz,在这个价位段里做节点控制、状态采集、通信网关都很合适。它内置了FDCAN控制器,这一点比很多同级的经典CAN控制器芯片有优势。以前要上CAN FD,要么选更贵的M4/M7系列芯片,要么外挂独立的CAN FD控制器,电路和驱动都多一层。G0B1直接把CAN FD做进去,板子上加一颗CAN收发器就能跑,BOM成本和开发成本都降下来了。

另外一个让我觉得舒服的点是,G0B1用CubeMX配置外设非常顺手。FDCAN的位时序、过滤器、中断、消息RAM分配都有图形界面,生成的HAL库代码可以直接用,不需要像老工程师那样抱着寄存器手册一行行改。对于需要快速出活的产品开发来说,这个优势很实际。当然,HAL库给你生成的是基础框架,真正常用的过滤规则、发送缓冲区管理、错误中断逻辑还是得自己写,后面我会详细展开。

1.3 项目选型时要考虑什么

不是所有场景都要换CAN FD。我在选型时会先算一笔账:如果当前总线上每帧报文基本都在8字节以内,节点数量少,总线负载率也不高,经典CAN完全够用,没必要为了“新协议”增加风险。但如果出现以下情况,升级到CAN FD很值:

  • 单条报文数据量超过8字节,比如电机控制器反馈、电池单体电压温度打包上传
  • 需要在同一个网络里跑UDS诊断,尤其是刷写Bootloader时,64字节一帧比8字节一帧效率高好几倍
  • 总线节点多、通信周期短,经典CAN的带宽已经让你不得不压缩报文内容

我这次项目就是典型的第二类,需要给设备升级固件,数据包有大几十字节。用经典CAN要拆成七八帧,还得自己搞分包重组协议,换CAN FD之后一帧就能放下大部分数据包,协议栈瞬间简单了。如果你也是类似场景,G0B1这套方案值得试。

2. CubeMX配置实操:从引脚到FDCAN参数

2.1 时钟树和引脚映射

打开CubeMX后,首先选中你手上的G0B1具体型号,然后在Pinout视图里搜“FDCAN”。软件会自动把可用的TX、RX引脚列出来,我用的是PA11和PA12,具体引脚号要以你的板子原理图为准。这里提醒一句:FDCAN引脚不要想当然地找PB8/PB9,G0系列不同型号的复用关系有差异,CubeMX里没有把外设和引脚对上,代码编译能过、功能就是不动,十有八九是引脚复用选错了。

时钟配置这块,我习惯把System Clock先设到64MHz,然后在Clock Configuration里找到FDCAN Kernel Clock,把它选到PLL输出。之所以挂PLL而不是直接用HSI,是因为后续如果你想把数据段速率跑高,内核时钟频率余量越足,分频组合越灵活。CubeMX生成的代码里会有一句类似HAL_RCC_FDCANCLKConfig(RCC_FDCANCLKSOURCE_PLL)的调用,这就是把FDCAN时钟源切到PLL了。

生成工程之前,还要在Project Manager里确认HAL库版本,我常用的是1.1.x或者更新版本,老版本FDCAN的HAL驱动在某些API行为上会有差异,尤其是HAL_FDCAN_ActivateNotification第三个参数的处理逻辑。尽量别用太老的库,否则后面代码可能对不上。

2.2 FDCAN位时序和帧格式配置

CubeMX里FDCAN的参数配置是整个工程最核心的地方。我以FDCAN Kernel Clock = 64MHz为例,给一个实测可用的配置:

  • 仲裁段波特率:500kbps
  • 数据段波特率:2Mbps
  • 采样点:约80%

对应到CubeMX的FDCAN配置里,Nominal Bit Timing填:

  • Prescaler = 8
  • SyncJumpWidth = 1
  • TimeSeg1 = 12
  • TimeSeg2 = 3

Data Bit Timing填:

  • Prescaler = 2
  • SyncJumpWidth = 1
  • TimeSeg1 = 12
  • TimeSeg2 = 3

这个计算逻辑其实很简单:波特率 = 内核时钟 / (Prescaler × (1 + TimeSeg1 + TimeSeg2))。仲裁段就是 64MHz / (8 × (1 + 12 + 3)) = 500kHz,数据段就是 64MHz / (2 × (1 + 12 + 3)) = 2MHz。采样点 = (1 + TimeSeg1) / (1 + TimeSeg1 + TimeSeg2) = 13/16 = 81.25%。CAN总线采样点放在80%左右是比较稳妥的,既能避开位开头段的信号毛刺,也留有足够余量应对线路延迟。

帧格式方面,如果你要在数据段用2Mbps,就要选FD mode with BRS,CubeMX里会对应FDCAN_FRAME_FD_BRS。如果只选FD mode without BRS,那帧格式虽然是CAN FD,但不会切到数据段高波特率,相当于白升级了。自动重传我建议打开,有些实时性要求苛刻的场景会关掉,但要自己处理发送失败后的补偿逻辑,新手阶段先把自动重传开着省心。

2.3 消息RAM、Tx Buffer和过滤器参数

FDCAN和经典CAN不太一样,它内部有一块消息RAM,用来放接收FIFO、发送缓冲区、发送事件FIFO。CubeMX里会让你填Tx Buffers数量、Tx Event FIFO大小、Rx FIFO0、Rx FIFO1大小。这个分配直接影响后面HAL API能不能正常调用。

我第一次用FDCAN时在这里吃过亏:Tx Buffers只填了0,结果调用HAL_FDCAN_AddMessageToTxBuffer一直返回错误。CubeMX里Tx Buffers最好至少填1,我习惯填3,这样在短时间内连续发送多条报文时,缓冲区不会因为前一条还没发完就拒绝新报文。Tx Event FIFO建议也开3个左右,它主要用来记录发送完成事件,调试时能查每帧的实际发送结果。Rx FIFO0我分配了6个缓冲区,应对节点多、中断偶尔延迟的情况。

消息RAM这块还涉及到一个关键词:FDCAN Tx Buffer Configuration Register,也就是寄存器手册里的TXBC。HAL库的HAL_FDCAN_AddMessageToTxBuffer最终就是通过TXBC里的起始地址、专用发送缓冲区数量、FIFO/队列大小来完成寻址的。CubeMX里你填的Tx Buffers数量会被转换成TXBC的NDTB字段。明白这层关系后,再看到“发送缓冲区满了”“发送地址不对”这类问题,第一反应就应该是去查CubeMX的消息RAM分配,而不是怀疑HAL库写错了。

过滤器配置在FDCAN里同样重要。我常用的接收过滤方式是Mask模式,只接收自己关心的ID。CubeMX里虽然没有把所有过滤参数都放在简单设置页,但生成工程后可以在MX_FDCAN1_Init函数下面自己补HAL_FDCAN_ConfigFilter。标准ID、11位掩码的情况下,如果只想收ID=0x123,就设置FilterID1=0x123、FilterID2=0x7FF。FilterID2在这里是掩码,置1的位表示必须精确匹配,置0的位表示不关心。

3. HAL库代码移植与收发逻辑实现

3.1 初始化代码与过滤器完整示例

CubeMX生成工程后,FDCAN基本初始化代码已经在main.c里了,核心结构体是FDCAN_HandleTypeDef。以我项目里的配置为例,初始化后代码大概是这样:

hfdcan1.Instance = FDCAN1; hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; hfdcan1.Init.FrameFormat = FDCAN_FRAME_FD_BRS; hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; hfdcan1.Init.AutoRetransmission = ENABLE; hfdcan1.Init.TransmitPause = DISABLE; hfdcan1.Init.ProtocolException = DISABLE; hfdcan1.Init.NominalPrescaler = 8; hfdcan1.Init.NominalSyncJumpWidth = 1; hfdcan1.Init.NominalTimeSeg1 = 12; hfdcan1.Init.NominalTimeSeg2 = 3; hfdcan1.Init.DataPrescaler = 2; hfdcan1.Init.DataSyncJumpWidth = 1; hfdcan1.Init.DataTimeSeg1 = 12; hfdcan1.Init.DataTimeSeg2 = 3; hfdcan1.Init.StdFiltersNbr = 1; hfdcan1.Init.ExtFiltersNbr = 0; hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; HAL_FDCAN_Init(&hfdcan1);

初始化完成后,接着要配过滤器并开启外设。我给自己的板子配置的接收规则是:只收标准ID 0x123,其它报文一律不进接收FIFO。

FDCAN_FilterTypeDef sFilterConfig; sFilterConfig.IdType = FDCAN_STANDARD_ID; sFilterConfig.FilterIndex = 0; sFilterConfig.FilterType = FDCAN_FILTER_MASK; sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; sFilterConfig.FilterID1 = 0x123; sFilterConfig.FilterID2 = 0x7FF; HAL_FDCAN_ConfigFilter(&hfdcan1, &sFilterConfig); HAL_FDCAN_Start(&hfdcan1); HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);

最后一行HAL_FDCAN_ActivateNotification一定要放在HAL_FDCAN_Start之后。如果你先打开中断通知再启动外设,中间恰恰来了一帧报文,可能在启动前就漏掉了。虽然这个概率不高,但调试时经常会因此觉得“为什么我收不到”,实际是开启顺序反了。

3.2 发送CAN FD报文:DataLength是个大坑

发送数据用HAL_FDCAN_AddMessageToTxBuffer,这个函数比经典CAN的发送邮箱灵活,但参数也更讲究。先看一个完整例子:

FDCAN_TxHeaderTypeDef TxHeader; uint8_t TxData[64]; uint32_t data_len = 12; memset(TxData, 0, sizeof(TxData)); for (int i = 0; i < data_len; i++) { TxData[i] = (uint8_t)i; } TxHeader.Identifier = 0x123; TxHeader.IdType = FDCAN_STANDARD_ID; TxHeader.TxFrameType = FDCAN_DATA_FRAME; TxHeader.DataLength = FDCAN_DLC_BYTES_12; TxHeader.FDFormat = FDCAN_FD_CAN; TxHeader.BitRateSwitch = FDCAN_BRS_ON; TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; TxHeader.MessageMarker = 0; while (HAL_FDCAN_AddMessageToTxBuffer(&hfdcan1, &TxHeader, TxData, FDCAN_TX_BUFFER0) != HAL_OK) { // 发送缓冲区满时等待 }

很多第一次用HAL FDCAN的人会直接在DataLength里写12,这是错的。FDCAN的DataLength字段不是普通字节数,而是一个DLC编码:0到8字节对应0到8,12字节对应9,16字节对应10,20字节对应11,24字节对应12,32字节对应13,48字节对应14,64字节对应15。HAL库里已经用宏帮你封装好了,直接用FDCAN_DLC_BYTES_12这种写法最保险。

还有一个容易忽略的点是TxHeader.FDFormatBitRateSwitch的组合。如果你发的是CAN FD帧,FDFormat要设成FDCAN_FD_CAN;如果还想在数据段切高速率,BitRateSwitch要设成FDCAN_BRS_ON。两个都设对了,总线上的实际时序才会先以仲裁段速率发完ID和BRS位,再切到数据段速率发剩下的数据。如果只开了FDFormat没开BRS,那对方收到的就是不带速率切换的CAN FD帧,虽然也能收,但传输时间并没能缩短。

发送结果的检查建议直接看返回值,HAL_OK表示已经放进发送缓冲区。HAL_FDCAN_AddMessageToTxBuffer返回非OK大概率是发送缓冲区满,可以加一个简单的重试循环,但不要用阻塞延时,否则高频发送时反而会把CPU卡死。我一般会用一个队列把待发送的数据先缓存起来,再在循环里轮询发送,这样既不会丢报文,也不会阻塞主流程。

3.3 中断接收与数据长度解析

接收我建议走中断,不要在主循环里用轮询等待,否则总线报文一多CPU基本就被占死了。FDCAN中断服务函数需要自己做好映射,CubeMX不会自动生成所有回调。我在stm32g0xx_it.c里写了:

void FDCAN1_IT0_IRQHandler(void) { HAL_FDCAN_IRQHandler(&hfdcan1); }

然后在用户代码里重写HAL库提供的弱回调函数:

void HAL_FDCAN_RxFifo0MsgPendingCallback(FDCAN_HandleTypeDef *hfdcan) { FDCAN_RxHeaderTypeDef RxHeader; uint8_t RxData[64]; uint8_t data_len; uint16_t rx_id; if (hfdcan->Instance != FDCAN1) { return; } HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData); data_len = fdcan_dlc_to_bytes(RxHeader.DataLength); rx_id = RxHeader.Identifier; // 在这里做自己的业务处理,建议只置位标志,不要做耗时操作 ProcessRxFrame(rx_id, RxData, data_len); }

因为CAN FD的DataLength是DLC编码,我在工程里放了一个专门做转换的小函数:

uint8_t fdcan_dlc_to_bytes(uint32_t dlc) { switch (dlc) { case FDCAN_DLC_BYTES_0: return 0; case FDCAN_DLC_BYTES_1: return 1; case FDCAN_DLC_BYTES_2: return 2; case FDCAN_DLC_BYTES_3: return 3; case FDCAN_DLC_BYTES_4: return 4; case FDCAN_DLC_BYTES_5: return 5; case FDCAN_DLC_BYTES_6: return 6; case FDCAN_DLC_BYTES_7: return 7; case FDCAN_DLC_BYTES_8: return 8; case FDCAN_DLC_BYTES_12: return 12; case FDCAN_DLC_BYTES_16: return 16; case FDCAN_DLC_BYTES_20: return 20; case FDCAN_DLC_BYTES_24: return 24; case FDCAN_DLC_BYTES_32: return 32; case FDCAN_DLC_BYTES_48: return 48; case FDCAN_DLC_BYTES_64: return 64; default: return 0; } }

用这种方式解析出来的长度才是真正的有效字节数。不要通过RxHeader.DataLength % 8来算,CAN FD为了在64字节内保持CRC效率,DLC是非线性的,直接用映射函数最稳妥。

中断回调里还有一点要克制:不要在回调里直接做浮点运算、打印日志、动态分配内存这类耗时操作。中断处理时间越长,后续报文堆积风险越高。我一般只把数据拷贝到全局缓存,置一个“收到新帧”标志位,主循环检测到标志后再做协议解析。

3.4 没有收发器时怎么自测:内部回环模式

如果手头没有CAN收发器,或者PCB还没打样回来,可以利用FDCAN的内部回环模式自测。CubeMX里把FDCAN的Mode从Normal改成Internal Loopback,或者在代码里把hfdcan1.Init.Mode临时改成FDCAN_MODE_INTERNAL_LOOPBACKHAL_FDCAN_Init一次。

内环模式下,发送数据会直接从控制器内部绕回接收FIFO,不需要外部总线,也不走TX/RX引脚。这样调完发送代码,马上就能看接收回调有没有触发。要注意的是,内部回环模式验证不了物理层,也验证不了通讯双方时序匹配,只能证明“控制器本身的收发路径是通的”。等板子和收发器都到位之后,一定要切回Normal模式再接总线实测。

我会用回环模式先跑一个简单的自发自收程序:定时发一帧ID=0x123的CAN FD报文,数据段12字节,再在接收中断里把收到的数据原样打出来。如果数据对得上,说明CubeMX配置、HAL API、中断路径都是通的,剩下的问题就集中在物理层和对方设备了。这个思路能帮你快速缩小排查范围,是调试FDCAN时非常高效的一步。

4. 调试工具与常见问题排查实录

4.1 用USB转CAN FD工具抓总线报文

G0B1内部FDCAN调通之后,第一件事就是上总线抓真实报文。我手边用的是周立功的USB转CAN FD接口卡,配合它的上位机软件使用。接线很简单:接口卡的CAN_H接对方CAN_H,CAN_L接CAN_L,两边共地,然后确保总线两端各有一个120Ω终端电阻。很多调试问题其实不是代码问题,而是终端电阻没接,或者双绞线太长导致回波干扰。

上位机软件的配置要比对三个方面:仲裁段波特率、数据段波特率、是否启用CAN FD BRS。我板子这边仲裁段是500kbps,数据段是2Mbps,那么上位机也要选同样的组合,同时打开CAN FD模式。如果上位机只配了仲裁段速率,没有配数据段速率,或者把数据段速率配成了和仲裁段一致,那工具就会报错或者收不到完整报文。

抓包时我通常会开两个窗口:一个看总线原始报文,一个看错误帧计数。CAN FD报文在工具里会明确显示带有BRS标记,数据场长度是64字节时一眼就能看出来。如果通信不正常,比如对方上位机一直显示错误帧,第一步先看仲裁段速率是否和帧起始部分匹配,第二步再看数据段速率是否和BRS切换后的数据部分匹配。不要一上来就改代码,先用工具把位时序对齐,能省不少时间。

4.2 典型报错和排查顺序

我把自己调FDCAN时遇到过的几类典型问题整理成了下面的表格,方便你对照:

现象可能原因处理办法
发送函数一直返回非HAL_OKTx Buffer没有分配或缓冲区已满CubeMX里确认Tx Buffers数量大于0;检查发送重试逻辑
中断没触发,但总线有报文过滤器把报文丢弃了检查FilterID1/FilterID2掩码,先设成接收全部报文验证
能收到报文但数据长度不对DataLength用的是字节数而不是DLC编码发送端用FDCAN_DLC_BYTES_xx宏,接收端用映射函数解析
工具显示错误帧或CRC错误仲裁段/数据段波特率不匹配核对CubeMX和调试工具的位时序配置
回环正常,接总线不通外部收发器不支持CAN FD高速数据段换支持CAN FD的收发器,比如TJA1044、MCP2562FD
总线偶尔丢帧缺少120Ω终端电阻或线路太长检查终端电阻,缩短总线长度,降低数据段速率
发送完成中断没进回调TxEventFifo设置或中断使能不对确认Tx Event FIFO分配了空间,并使能发送完成中断

排查顺序我建议从简到繁:先看状态寄存器返回值,再看是否进中断,再看过滤器,最后看物理层。大多数人卡在“代码明明能跑但就是收不到”的环节,基本都是过滤器或者消息RAM分配的问题。有一个小技巧:调试阶段把过滤器先配成接收所有ID,比如FilterID1=0、FilterID2=0,先把通信打通,最后再收紧过滤规则。这样可以避免“自己想收的ID被掩码算错丢掉了”这种低级问题。

4.3 几个能帮你少走弯路的配置习惯

最后分享几个我实际调试中总结出来的习惯,这些都是CubeMX生成代码之外才体现价值的地方。

第一,FDCAN内核时钟最好固定。不要一会儿选PLL一会儿选HSI,内核时钟一变,你之前算好的Prescaler、TimeSeg组合就全偏了。我每次改时钟树之后,都会重新算一遍仲裁段和数据段波特率,并且在上位机工具里实测确认,而不是只看CubeMX里的计算值。

第二,CubeMX里把Tx Buffers和Rx FIFO的数量留足一点。这两个参数看着不起眼,等通讯压力一上来,区别非常明显。Tx Buffers给3个、Rx FIFO0给6个,对大多数中小节点来说足够。如果你在跑Bootloader刷写,建议把Tx Event FIFO也打开,它能帮你确认每一帧是不是真的送出去了。

第三,代码调试阶段不要迷信自动重传。自动重传打开后,发送失败时FDCAN会一直在内部重试,如果你在代码里发了一帧,但总线上没有ACK节点,发送函数可能一直成功但报文永远发不出去。这时候如果把AutoRetransmission临时关掉,返回错误反而能提醒你“物理层没接好”。这个特性在调试初期非常有用,但上线量产时记得按需求恢复。

第四,遇到“发不出”或者“收不到”的问题,先怀疑配置再怀疑硬件。FDCAN的初始化结构体里字段很多,任何一个和波特率相关的字段填错,结果就是完全不通。我习惯在初始化后把hfdcan1.Init.NominalPrescalerDataPrescaler这些关键字段用调试器打印出来,和CubeMX里填的值逐一核对。别小看这一步,很多看起来玄学的问题,最后都是某个字段没生效。

CAN FD调试就是这么一点一点磨出来的。你只要把波特率组合、消息RAM分配、过滤器、DLC编码这四件事弄清楚,STM32G0B1的FDCAN基本就不会再难住你。后面如果项目里要上UDS on CAN FD或者Bootloader刷写,这套通信底子可以直接往上垒,省下的时间可不止半天。

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

【单片机毕业设计】基于 STM32 的水体参数阈值配置与自动换水系统设计 基于 STM32 的水质在线监测与继电器联动控制装置设计(011009)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/24 12:45:51

华为eNSP实战:安装配置、VLAN实验与故障排查

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

作者头像 李华
网站建设 2026/9/24 12:43:40

ESP32-P4 Rev 3.0低功耗实战:电源树改动与功耗调优

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

作者头像 李华
网站建设 2026/9/24 12:43:15

Pixel新机验机四步法:工程模式+IMEI+基带+网络锁核验

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

作者头像 李华
网站建设 2026/9/24 12:42:56

QQ号与微信号格式校验:ValidX规则从入门到实战

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

作者头像 李华