news 2026/9/20 7:11:24

Matter Silabs 平台通用应用行为详解:LCD 屏幕、按键与 LED 状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matter Silabs 平台通用应用行为详解:LCD 屏幕、按键与 LED 状态机

Matter Silabs 平台通用应用行为详解:LCD 屏幕、按键与 LED 状态机

【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip

Matter(原 Project CHIP)SDK 中 Silicon Labs(Silabs)平台的所有示例应用共享一套由examples/platform/silabs目录实现的“通用行为”:LCD 三屏切换与二维码展示、BTN0/BTN1 双按键的配网与恢复出厂流程、LED0/LED1 双 LED 的配网状态指示。本文基于官方文档docs/platforms/silabs/silabs_common_app_behavior.md完整展开这套行为规范,并结合BaseApplicationSilabsLCDLEDWidget等源码逐条印证其底层实现,帮助你在移植或调试 Silabs Matter 示例时,快速理解每一个按钮、屏幕与 LED 闪烁模式背后的状态机。

一、总体架构:通用行为代码在哪里

文档开宗明义:所有 Silabs 示例应用(lighting-app、all-clusters-app、contact-sensor-app 等)的通用功能实现,位于示例平台目录 examples/platform/silabs 下。核心的几个文件为:

文件职责
BaseApplication.h / BaseApplication.cpp应用基类:事件队列、按键处理、工厂重置、状态 LED 模式、二维码输出、LCD 状态屏刷新
display/lcd.h / display/lcd.cppSilabsLCD类:屏幕枚举、Demo/Status/QRCode 三屏绘制与循环切换
LEDWidget.h / LEDWidget.cppLED 抽象:Set/Blink/Animate闪烁时序控制
main.cpp / MatterConfig.cppapp_init()入口,调用SilabsMatterConfig::AppInit()启动 Matter 应用

从源码结构看,这套行为以FreeRTOS 任务 + CMSIS-OS 消息队列事件驱动的方式组织(见 BaseApplication.cpp):

  • App 任务StartAppTask()创建一个默认 4096 字节栈(APP_TASK_STACK_SIZE)、默认 10 个事件容量(APP_EVENT_QUEUE_SIZE)的静态内存任务与消息队列,所有 UI/按键/定时器回调都通过PostEvent()AppEvent投递进队列,由任务统一DispatchEvent()分发,避免在不同线程上下文中直接操作 CHIP 栈;
  • 两个 CMSIS 软件定时器sFunctionTimer(一次性,按键功能/工厂重置计时)与sLightTimer(周期 10ms 的kLightTimerPeriod,驱动 LED 动画);
  • 初始化链路BaseApplication::Init()依次调用BaseInit()(注册平台事件处理器、创建任务与定时器、初始化 LCD、输出初始二维码、读取配网状态、注册FabricTable::Delegate)再调用各示例实现的虚函数AppInit()(见 BaseApplication.cpp)。

理解了这个“事件队列 + 定时器”骨架,下面所有按钮动作、LED 动画、LCD 刷新就都是这条链路上的不同事件处理分支。

二、LCD 屏幕:三个窗口与循环切换

2.1 三屏结构:Demo / Status / QRCode

文档指出:支持 LCD 的开发套件上,应用拥有三个独立窗口,按BTN0可在三者之间循环;当应用 UI 更新而当前正停在其他窗口时,LCD 会自动切换到对应窗口。

源码中这三个窗口对应SilabsLCD::Screen_e枚举(lcd.h):

typedef enum screen { DemoScreen = 0, // 应用 UI(每个示例自定义) StatusScreen, // 设备状态屏 #if SL_MATTER_QR_CODE_ENABLED QRCodeScreen, // 配网二维码屏 #endif CycleScreen, InvalidScreen, } Screen_e;

切换逻辑在SilabsLCD::CycleScreens()(lcd.cpp):当前屏加一,超过最后一屏则回卷到DemoScreen。按键触发路径为BaseApplication::ButtonHandler()中对未配网/已配网均会执行的PostUpdateDisplayEvent(SilabsLCD::Screen_e::CycleScreen)(BaseApplication.cpp),事件最终由UpdateDisplayHandler()处理:收到CycleScreen时调用UpdateDisplay()(刷新状态屏数据并CycleScreens()),收到具体屏名时直接SetScreen()(BaseApplication.cpp)。

LCD 硬件层面:SilabsLCD::Init()依次使能板载显示屏(sl_board_enable_display())、初始化 DMD 显示驱动、初始化 glib 2D 图形上下文(白底黑字),并初始化 128×128 点阵(LCD_SIZE 128,见 lcd.cpp)。这些功能受SL_MATTER_DISPLAY_ENABLED(总开关)与SL_MATTER_QR_CODE_ENABLED(二维码屏)两个编译开关保护,无 LCD 的套件(如部分 ESP 风格或最小化构建)关闭后即可完全剔除这部分代码。

2.2 应用 UI(DemoScreen)

每个示例应用有自己的“应用 UI”,用于可视化该示例的应用状态(如灯的开关/亮度)。DemoScreen的默认绘制是 display/demo-ui.c 中的demoUIDisplayApp();应用也可以注册自定义绘制回调:SilabsLCD::SetCustomUI(cb)之后,WriteDemoUI()会优先调用该回调而不是默认画面(lcd.cpp)。各示例具体的 UI 内容需参考对应示例的文档(如docs/platforms/silabs/下 Getting Started 与examples/lighting-app等示例的 README)。

2.3 状态屏(StatusScreen):字段从哪来

文档给出的状态屏字段表完整保留如下:

OpenThread 与 Wi-Fi 设备共有信息:

LCD UI说明
# fabrics设备上已配网的 fabric 数量
Connected设备是否已连接到 OpenThread 或 Wi-Fi 网络
Advertising设备当前是否正在广播开放的配网窗口
Is ICD设备是否为间歇连接设备(Intermittently Connected device)

OpenThread 设备特有:

LCD UI说明
PANID已配置 OpenThread 网络的 PANID
OT TypeOpenThread 设备类型(FTD / MTD)

Wi-Fi 设备特有:

LCD UI说明
SSID已连接 Wi-Fi 网络的 SSID

文档原文标注:PANID 信息尚未打印到 LCD、SSID 信息尚未打印到 LCD、ICD 状态屏支持“尚待完成”。注意这与当前仓库源码已有差异BaseApplication::UpdateLCDStatusScreen()(BaseApplication.cpp)现在已经通过NetworkCommissioning::GetConnectedNetwork()取到 SSID 并拷入status.networkNameWriteStatus()也会绘制 SSID/PANID 两行,并依据ICDConfigurationData输出ICD : SIT/LIT(lcd.cpp)。换言之,文档中的两条 “not yet printed” 注释属于历史遗留,当前实现中这些字段已经在状态屏呈现;而 “ICD 状态屏支持待完成” 指的是 ICD 设备专属的完整状态展示,状态屏对 ICD 的支持目前主要体现在 ICD 模式标识上。

各字段的数据来源在UpdateLCDStatusScreen()中可逐一对应:

chip::DeviceLayer::PlatformMgr().LockChipStack(); #ifdef SL_WIFI enabled = ConnectivityMgr().IsWiFiStationEnabled(); attached = ConnectivityMgr().IsWiFiStationConnected(); // 从已连接网络对象中取出 SSID 拷贝到 status.networkName #endif #if CHIP_ENABLE_OPENTHREAD enabled = ConnectivityMgr().IsThreadEnabled(); attached = ConnectivityMgr().IsThreadAttached(); #endif status.connected = enabled && attached; status.advertising = chip::Server::GetInstance().GetCommissioningWindowManager() .IsCommissioningWindowOpen(); status.nbFabric = chip::Server::GetInstance().GetFabricTable().FabricCount(); ... chip::DeviceLayer::PlatformMgr().UnlockChipStack(); slLCD.SetStatus(status);

要点:

  • Connected = enabled && attached:对 Wi-Fi 即“站点已使能且已关联”,对 Thread 即“Thread 已使能且已附着”;
  • Advertising直接查询CommissioningWindowManager::IsCommissioningWindowOpen(),因此配网窗口一开,状态屏的 Advertising 立即变 Y;
  • 状态屏的自动刷新OnPlatformEvent()监听到kThreadConnectivityChange/kInternetConnectivityChange时,若当前正在显示StatusScreen,就PostUpdateDisplayEvent(StatusScreen)触发重绘(BaseApplication.cpp)——这就是文档所说“UI 更新时 LCD 自动切换/刷新”的机制之一。
  • 整个读取过程通过LockChipStack()/UnlockChipStack()保护,因为 App 任务与 CHIP 栈任务并发运行。

DisplayStatus_t结构体(lcd.h)即文档表格的内存映射:nbFabricconnectednetworkName(Wi-Fi SSID)、advertisingicdMode

2.4 二维码屏(QRCodeScreen)

文档说明:二维码屏显示的是可用于 BLE 配网与 Basic Commissioning Mode(基本配网模式)的默认二维码,其编码内容遵循 Matter 规范定义;并提醒Basic Commissioning Mode 不如 Enhanced Commissioning Mode 安全,不推荐使用

源码侧的生成链路:

  1. BaseApplication::OutputQrCode(bool refreshLCD)(BaseApplication.cpp)通过Provision::Manager::GetInstance().GetStorage().GetSetupPayload(setupPayload)取出 Base38 格式的 Setup Payload;
  2. refreshLCD为真且开启二维码,则slLCD.SetQRCode(...)+slLCD.ShowQRCode(true)切到QRCodeScreen绘制;
  3. 同时PrintQrCodeURL(setupPayload)把二维码 URL 打印到日志——这正是文档中“按键短按后在 Log 中打印 initial 与 BCM 配网二维码”的实现;
  4. 绘制使用qrcodegen库:版本 4 二维码、每个模块 3 像素、误差等级 LOW([lcd.cpp](https://link.gitcode.com/i/77dde88e58994f08b08c07be17443d6a#L36-L38, L260-L289))。

两个关键触发点:

  • 启动时BaseInit()末尾调用OutputQrCode(true),上电后 LCD 默认就显示二维码屏(BaseApplication.cpp);
  • 配网窗口关闭时自动回退BaseApplicationDelegate::OnCommissioningWindowClosed()中,若设备已配网且当前停在QRCodeScreen,会PostUpdateDisplayEvent(DemoScreen)自动切回应用 UI(BaseApplication.cpp)。

三、按键:操作键与应用键

3.1 按键定义与硬件差异

文档规定所有示例应用按两个按键设计:

  • BTN0 = 操作键(Operation Button)
  • BTN1 = 应用键(Application Button),各示例定义其专属功能(详见各示例文档)。

源码中APP_FUNCTION_BUTTON 0APP_ACTION_BUTTON 1([BaseApplication.cpp](https://link.gitcode.com/i/ca0c78e6469912e97aebe0a118294e41#L33, L117))与之对应。文档同时给出硬件注意事项,此处完整保留:

SparkFun 开发套件(BRD2704A)没有任何按键;部分套件只支持按键或只支持 LED——默认配置以按键支持为准,仅有 LED 的套件默认配置为应用 LED。

3.2 操作键(BTN0)行为:短按与长按

文档给出的 BTN0 动作表:

执行方式行为
短按释放(Press and Release)① 若设备尚未配网,开始以快速模式广播 30 秒;30 秒后切换为较慢间隔广播;15 分钟后停止广播。② 在日志中打印 initial 与 BCM 配网二维码
按压并持有 6 秒工厂重置(Factory Reset)设备

广播“30 秒快播 → 慢速间隔 → 15 分钟停止”是 Matter 规范对 BLE 广播的标准时序,由 CHIP 栈的配网窗口机制实现;示例应用只负责“开窗”这个动作。

源码中 6 秒长按的构成是两个 3 秒超时(BaseApplication.cpp):

#define FACTORY_RESET_TRIGGER_TIMEOUT 3000 // 按住 3 秒触发“工厂重置序列” #define FACTORY_RESET_CANCEL_WINDOW_TIMEOUT 3000 // 触发后的 3 秒取消窗口

完整状态机(ButtonHandler+FunctionTimerEventHandler,[BaseApplication.cpp](https://link.gitcode.com/i/ca0c78e6469912e97aebe0a118294e41#L392-L410, L590-L649)):

  1. 按下SilabsPlatform::ButtonAction::ButtonPressed):StartFunctionTimer(FACTORY_RESET_TRIGGER_TIMEOUT),3 秒一次性计时开始;
  2. 3 秒内释放sIsFactoryResetTriggered仍为假,走“短按”分支:
    • CancelFunctionTimer()
    • 若未配网(Wi-Fi 查ConnectivityMgr().IsWiFiStationProvisioned(),其余查sIsProvisioned):锁住 CHIP 栈调用GetCommissioningWindowManager().OpenBasicCommissioningWindow()打开基本配网窗口——这会启动 BLE 广播;
    • 若已配网:打印 "Network is already provisioned, Ble advertisement not enabled";ICD 构建下还会通过ICDNotifier::NotifyNetworkActivityNotification()临时声明网络活动;
    • OutputQrCode(false)打印二维码到日志;PostUpdateDisplayEvent(CycleScreen)循环 LCD 屏。
  3. 按住满 3 秒(计时器到期):FunctionEventHandler发现sIsFactoryResetTriggered == false,调用StartFactoryResetSequence():再启动 3 秒的取消窗口计时,置位sIsFactoryResetTriggered,状态 LED 切换为 500ms 均匀闪烁(sStatusLED.Blink(500))提示“重置已启动”;
  4. 在取消窗口内松开CancelFactoryResetSequence(),取消重置,恢复原 LED 模式;
  5. 继续按住满 3+3=6 秒:第二次计时器到期时sIsFactoryResetTriggered已为真,直接ScheduleFactoryReset()执行工厂重置。

ScheduleFactoryReset()的实现(BaseApplication.cpp)值得注意:它并非逐键删除 fabric,而是走ConfigurationMgr().InitiateFactoryReset(),借助 Silabs NVM3 驱动整段删除 KVS 分区——源码注释明确说明这比Server::ScheduleFactoryReset()逐 Key 删除更快,并顺带移除 Wi-Fi 场景下的 Matter DNS-SD 服务广告。

应用键(BTN1)文档将其留给各示例自行定义(例如灯光应用用于控制灯),通用层只保留钩子:ScheduleFactoryReset()中若检测到APP_ACTION_BUTTON(即 BTN1)同时被按下,会调用Provision::Manager::SetProvisionRequired(true)标记需要重新配网。

四、LED:状态 LED 与应用 LED

4.1 双 LED 约定

文档约定:所有示例应用按两个 LED 设计——LED0 为状态 LED(Status LED),LED1 为应用 LED(Application LED);并在硬件受限时给出降级规则(只有按键的套件默认启用按键配置,只有 LED 的套件默认启用应用 LED)。

通用层通过BaseApplication::LinkAppLed(LEDWidget*)/UnlinkAppLed()把应用 LED 挂到基类上下文,使两枚 LED 的动画在同一LightEventHandler()中同步推进(BaseApplication.h)。状态 LED 实例sStatusLED仅在组件目录确认SL_CATALOG_SIMPLE_LED_LED1_PRESENT(即ENABLE_WSTK_LEDS且 LED1 存在)时启用,编号SYSTEM_STATE_LED = 0([BaseApplication.cpp](https://link.gitcode.com/i/ca0c78e6469912e97aebe0a118294e41#L114-L116, L135-L137))——这解释了“部分套件无 LED”时的编译期剔除。

4.2 状态 LED 的 5 种状态

文档的状态 LED 状态表完整如下:

状态含义
Short Flash On(50ms 亮 / 950ms 灭)设备处于未配网(未配对)状态,等待配网应用连接
Rapid Even Flashing(100ms 亮 / 100ms 灭)设备未配网,但已有配网应用通过 BLE 连接
Short Flash Off(950ms 亮 / 50ms 灭)设备已完全配网,但尚未具备完整的 Thread 网络或服务连通性
Solid On设备已完全配网,且具备完整的 Thread 网络与服务连通性
Long Even Flashing(500ms 亮 / 500ms 灭)工厂重置流程已启动

这些模式与源码严格对应。ActivateStatusLedPatterns()(BaseApplication.cpp)中按优先级判断:

if (sIsProvisioned && sIsEnabled) { if (sIsAttached) sStatusLED.Set(true); // Solid On else sStatusLED.Blink(950, 50); // Short Flash Off } else if (sHaveBLEConnections) { sStatusLED.Blink(100, 100); // Rapid Even Flashing } else { sStatusLED.Blink(50, 950); // Short Flash On }

其中sHaveBLEConnections = (ConnectivityMgr().NumBLEConnections() != 0),即“BLE 有连接”这一行由栈内 BLE 连接数直接判定。工厂重置的 500ms 均匀闪烁则来自StartFactoryResetSequence()sStatusLED.Blink(500)

驱动节拍是 10ms 的周期定时器sLightTimerkLightTimerPeriod = pdMS_TO_TICKS(10)),LightEventHandler()每次先(在非 ICD 构建下)用TryLockChipStack()非阻塞锁刷新sIsProvisioned/sIsEnabled/sIsAttached/sHaveBLEConnections,再调用ActivateStatusLedPatterns()与两枚 LED 的Animate()(BaseApplication.cpp)。使用非阻塞锁是为了在 CHIP 任务执行长加密操作时不阻塞 UI 动画。

4.3 ICD 构建下的 LED 差异(源码级补充)

从源码结构看,文档未展开的一个重要细节:上述“持续轮询刷新 LED 状态”的分支被#if !(CHIP_CONFIG_ENABLE_ICD_SERVER)包裹。也就是说ICD(间歇连接设备)构建不常态运行 LED 状态机,而是按需启停sLightTimer

  • 工厂重置序列启动/取消时StartStatusLEDTimer()/StopStatusLEDTimer()
  • Identify 集群的OnIdentifyStart/OnIdentifyStop、Trigger Effect 开始/完成时同样启停([BaseApplication.cpp](https://link.gitcode.com/i/ca0c78e6469912e97aebe0a118294e41#L729-L793, L822-L895))。

这与 ICD“大部分时间休眠”的功耗模型一致:LED 动画只在需要提示用户(重置中、Identify 中)时才消耗计时与唤醒资源。若你的示例启用了CHIP_CONFIG_ENABLE_ICD_SERVER,状态 LED 的日常配网状态指示(上表前四种)将不会自动点亮,这是设计使然而非缺陷。

4.4 应用 LED

文档将应用 LED 的语义留给各示例定义(例如用亮度同步灯亮、用闪烁提示动作结果)。通用层只负责:应用通过LinkAppLed()注册后,LightEventHandler()每 10ms 会同时推进sAppActionLed->Animate(),保证应用 LED 动画与状态 LED 动画时钟一致;不需要应用 LED 的示例可不链接,基类对sAppActionLed == nullptr已做保护。

五、把文档与源码串起来:一次典型交互的完整调用链

以“未配网设备 + 短按 BTN0”为例,把本文涉及的机制串成一条可验证链路:

  1. 板级按钮 ISR 把ButtonPressed/ButtonReleased包装成AppEvent投递队列;
  2. ButtonHandler():按下 →StartFunctionTimer(3000);释放 → 取消计时器,OpenBasicCommissioningWindow()(BLE 开始广播),OutputQrCode(false)打印二维码 URL,PostUpdateDisplayEvent(CycleScreen)
  3. LCD 事件UpdateDisplayHandler()切屏:SetScreen()按屏名分派到WriteDemoUI()/WriteStatus()/WriteQRCode()
  4. 状态 LED 在 10ms 节拍下检测到“未配网且无 BLE 连接”进入 50/950ms 闪烁;配网应用通过 BLE 连上后(NumBLEConnections() != 0)自动变为 100/100ms 快闪;
  5. 配网完成、OnCommissioningWindowClosed()触发后,LCD 若停在二维码屏会自动切回应用 UI;OnFabricCommitted()FabricCount() == 1)把UpdateCommissioningStatus(true),LED 进入 Solid On(已附着网络时)。

反向流程同样闭环:最后一个 fabric 被删除时,OnFabricRemoved()触发UpdateCommissioningStatus(false)DoProvisioningReset()——清理 Thread 栈、清除 Wi-Fi 站点配置、强制保存 NVM3 KeyMap,并重新打开基本配网窗口(BaseApplication.cpp),设备回到等待配网的初始状态。

六、参考文件索引

类别路径
官方行为文档docs/platforms/silabs/silabs_common_app_behavior.md
Silabs 平台文档索引docs/platforms/silabs/index.md
通用应用基类examples/platforms/silabs/BaseApplication.h、BaseApplication.cpp
LCD 实现examples/platforms/silabs/display/lcd.h、lcd.cpp、demo-ui.c
LED 抽象examples/platforms/silabs/LEDWidget.h、LEDWidget.cpp
应用入口examples/platforms/silabs/main.cpp、MatterConfig.cpp

适用前提与限制:本文行为描述基于当前仓库 Silabs 平台示例(EFR32/SiWx917 等套件,GN 构建)的BaseApplication公共层实现;具体某个示例的最终表现还受其.defaults/.conf组件配置影响(如是否启用SL_MATTER_DISPLAY_ENABLEDSL_MATTER_QR_CODE_ENABLED、是否 ICD 构建),且应用键 BTN1 与部分 LCD 画面由各示例自定义,需结合对应示例文档阅读。文档中关于 PANID/SSID 尚未上屏、ICD 状态屏待完成等注释,在当前源码中已有部分演进,实际以源码行为为准。

【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

达梦数据库实时主备故障模拟演练全流程实战

“你们这个主备,到底能不能在关键时候顶上去?”——这句话我几乎每次给客户做达梦数据库架构评审都会被问到。DM数据库的实时主备集群确实能扛住单点故障,但“能扛”和“真正验证过能扛”是两码事。很多团队把主备部署完毕、监视器一切正常就…

作者头像 李华
网站建设 2026/9/20 8:09:11

SQL优化实战:LIMIT n与LIMIT n,m的底层逻辑与性能优化方案

搞SQL优化的人,几乎都绕不过LIMIT这个词。它看似简单,但在实际项目里,我见过太多把LIMIT n,m和LIMIT n搞混、翻页翻到一半接口超时、查排行榜数据对不上的案例。这两个写法不只是“多了一个逗号”的区别,它们的执行逻辑、SQL优化方…

作者头像 李华
网站建设 2026/9/20 8:25:44

Pentagi:基于Docker与Neo4j的AI渗透测试代理架构

1. “Pentagi”不是工具名,而是渗透测试AI代理架构的代号级命名你搜“pentagi”,页面上跳出来的全是Docker、Neo4j、安装教程、报错日志——没有官网、没有GitHub仓库、没有文档首页,甚至没有一句像样的介绍。这很反常。一个真正开源或商用的…

作者头像 李华
网站建设 2026/9/20 8:25:35

Debian 12服务器初始化配置:SSH加固、防火墙与时间同步实战

一台刚开通的 Debian 12 服务器,默认状态其实是"半成品":root 能直接登录、SSH 密码认证开着、时区大概率还停在 UTC、时间同步服务不一定在跑、防火墙一条规则都没有、日志里很快会塞满各种扫描记录。我第一次给生产环境做初始化的时候图省事…

作者头像 李华
网站建设 2026/9/20 5:03:37

MATLAB神经网络工业级案例集:43个可直接运行的建模快照

简介:本资源是《MATLAB神经网络43个案例分析》配套的完整源代码与数据集,面向高校学生、科研人员及工程实践者,助力理解并复现BP、RBF、SOM、LVQ、Elman、Hopfield等主流神经网络模型在分类、预测、聚类、优化等典型任务中的MATLAB实现。压缩…

作者头像 李华
网站建设 2026/9/19 5:24:26

Istio 管 AI 数据中心服务网格,TaoToken 给出站调用贴 Key

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

作者头像 李华