news 2026/9/1 13:17:31

ZYNQ7020嵌入式平台LVGL 9.5.0移植与优化实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZYNQ7020嵌入式平台LVGL 9.5.0移植与优化实战指南

在 ZYNQ7020 这颗经典的 FPGA+ARM 双核芯片上跑 LVGL 9.5.0,核心要解决的是如何在资源受限的嵌入式平台上,流畅运行一个现代化的图形界面库。这不仅仅是“能不能跑起来”的问题,更是关于如何平衡性能、内存和开发效率。如果你手头有 ZYNQ7020 开发板,想用它做带屏幕的人机交互项目,或者从 STM32 这类 MCU 升级过来,想知道在更复杂的 SoC 上 GUI 开发有什么不同,那这篇实测记录会直接告诉你关键步骤和避坑点。

最值得关注的不是 LVGL 本身的功能,而是在 ZYNQ 的特定硬件架构(PS+PL)下,如何配置显示驱动、触摸驱动,以及如何利用双核特性优化性能。很多人卡在第一步——环境搭建和基础驱动,导致 LVGL 的丰富控件根本用不上。我会从最底层的硬件配置开始,一直讲到 LVGL 应用层的内存优化和事件处理,确保你能在板子上看到一个能响应触摸的完整界面。

1. 环境准备:别急着写代码,先理清硬件和软件栈

在 ZYNQ7020 上开发,最大的特点就是软硬件协同。你不能像在纯 MCU 上那样,只关心软件库的移植。整个环境可以拆成三层:硬件设计(Vivado)、系统构建(Petalinux)、应用开发(LVGL)。跳过一个环节,后面就可能全盘卡住。

1.1 硬件平台确认与 Vivado 基础工程

首先,确认你的 ZYNQ7020 开发板外接了什么样的显示和触摸设备。最常见的是 RGB 接口的 LCD 屏,搭配 I2C 或 SPI 接口的电阻/电容触摸屏。你需要知道屏幕的分辨率(如 800x480)、像素格式(如 RGB888)以及触摸 IC 的型号(如 GT911、FT5x06)。

第一步是在 Vivado 中创建一个能点亮屏幕的硬件工程。这里的关键是正确配置 ZYNQ7 Processing System (PS) 的外设:

  1. 使能必要的 PS 外设:在 Block Design 中双击 ZYNQ7 IP,确保以下接口已启用:

    • MIO / EMIO:用于连接触摸屏的 I2C 或 SPI 引脚。如果触摸屏接口不够用,可能需要使用 EMIO 将 PS 的 GPIO 引出到 PL 侧。
    • DDR 控制器:配置正确的 DDR 型号和时钟,这是系统内存和帧缓冲区的所在地。
    • UART0:用于调试信息输出,必不可少。
  2. 在 PL 侧添加显示控制器:ZYNQ PS 本身没有 RGB 显示接口,需要通过 PL(FPGA)部分来实现。你需要使用一个AXI VDMA (Video Direct Memory Access)和一个AXI4-Stream to Video Out的 IP 核(或者自己编写一个简单的 RGB 时序发生器)。

    • VDMA:负责将 DDR 中的帧缓冲区(framebuffer)数据通过 AXI4-Stream 高效地搬运到显示控制器。
    • Video Timing Controller:产生标准的 RGB LCD 时序信号(如 HSYNC, VSYNC, DE, PCLK)。
    • 连接关系:PS 通过 AXI Lite 配置 VDMA 和时序控制器。VDMA 的MM2S(Memory Map to Stream) 接口连接到时序控制器的s_axis_video输入。时序控制器的输出连接到你板子 LCD 接口对应的 FPGA 物理引脚。
  3. 地址分配与生成输出:为 VDMA 等 IP 核分配正确的地址空间,然后生成 HDL 输出,创建 Bitstream。最终,你会得到两个关键文件:.bit(硬件比特流文件)和.hdf(硬件描述文件,用于 Petalinux)。

注意:第一次尝试时,建议先在 Vivado 里用ILA(Integrated Logic Analyzer) 核抓一下 RGB 时序信号,确保 HSYNC、VSYNC、DE 和数据信号符合屏幕规格书的要求。这能避免硬件层面的一堆问题。

1.2 Petalinux 系统构建与驱动使能

有了硬件描述文件.hdf,下一步是构建一个包含显示和触摸驱动的 Linux 系统。Petalinux 是官方工具链。

# 1. 创建工程并导入硬件 petalinux-create -t project --template zynq --name lvgl_zynq cd lvgl_zynq petalinux-config --get-hw-description=<path_to_hdf_file> # 2. 配置内核,使能关键驱动 petalinux-config -c kernel

在内核配置中,你需要找到并启用以下驱动(使用/搜索):

  • DRM (Direct Rendering Manager)框架:这是现代 Linux 显示驱动的基石。
    • CONFIG_DRM=y
    • CONFIG_DRM_XLNX=y(Xilinx 的 DRM 驱动)
  • 显示控制器驱动:对应你在 Vivado 中创建的 IP。
    • 通常位于Device Drivers -> Graphics support -> Display Engine support -> Xilinx DRM KMS下,确保相关驱动被编译为模块 (=m) 或内置 (=y)。
  • 触摸屏驱动
    • I2C 触摸屏:如CONFIG_TOUCHSCREEN_EDT_FT5X06(FT5x06),CONFIG_TOUCHSCREEN_GOODIX(GT9xx)。
    • 确保 I2C 总线驱动也已启用。
  • Framebuffer 控制台(可选,用于早期调试):
    • CONFIG_FB=y
    • CONFIG_FB_XYLNX_FB=y
# 3. 配置根文件系统,添加 LVGL 运行所需工具 petalinux-config -c rootfs

Filesystem Packages中,你可以添加一些有用的包,如tslib(触摸屏校准工具)、evtest(输入设备测试工具)。

# 4. 编译系统 petalinux-build

编译完成后,在images/linux目录下会生成image.ub(内核+设备树+根文件系统)、zynq_lvgl_wrapper.bit(比特流)等文件。将它们拷贝到 SD 卡(FAT32 格式)的根目录,开发板从 SD 卡启动。

1.3 启动验证与驱动加载

上电启动后,通过串口登录系统。首先检查驱动是否成功加载:

# 查看 DRM 设备 cat /sys/class/drm/card0/device/uevent # 或使用 dmesg 查看内核日志 dmesg | grep -i “drm\|xlnx\|fb” # 查看帧缓冲区设备 ls -l /dev/fb* # 查看输入设备(触摸屏) ls -l /dev/input/ cat /proc/bus/input/devices # 查看详细的输入设备信息

如果能看到/dev/fb0/dev/input/event0(或类似)这样的设备节点,并且dmesg中没有相关驱动的报错,那么硬件和底层驱动层基本就通了。你可以用cat /dev/urandom > /dev/fb0来向屏幕输出噪点,快速验证显示通路是否正常。

2. LVGL 9.5.0 移植:从源码到第一个窗口

LVGL 的移植,核心是实现几个“端口”(porting)函数,主要是显示驱动接口和输入设备接口。ZYNQ7020 运行 Linux,我们可以直接使用 Linux 标准的 Framebuffer 和输入事件接口,这比在无操作系统的 MCU 上要方便得多。

2.1 获取 LVGL 源码与项目结构搭建

不建议直接修改 LVGL 官方源码树。更好的做法是建立自己的项目目录,将 LVGL 作为子模块或直接拷贝源码进来。

# 创建项目目录 mkdir zynq_lvgl_app && cd zynq_lvgl_app # 克隆 LVGL 仓库 (使用 9.5.0 版本) git clone https://github.com/lvgl/lvgl.git --branch release/v9.5 # 克隆 LVGL 的驱动程序仓库 git clone https://github.com/lvgl/lv_drivers.git # 克隆 LVGL 的示例仓库(包含很多有用的参考) git clone https://github.com/lvgl/lv_examples.git

你的项目目录结构可能如下:

zynq_lvgl_app/ ├── lvgl/ # LVGL 核心库 ├── lv_drivers/ # 官方提供的各种驱动(包括 framebuffer, evdev) ├── lv_examples/ # 示例 ├── src/ # 你的应用源码 │ ├── main.c │ └── ... ├── Makefile # 编译脚本 └── lv_conf.h # LVGL 配置文件

2.2 关键配置:lv_conf.h 与驱动选择

lv_conf.h是 LVGL 的“大脑”,所有特性开关、内存配置都在这里。从lvgl/lv_conf_template.h复制一份过来,并开始修改。

对于 ZYNQ7020(通常 DDR3 内存为 512MB 或 1GB),以下配置是关键:

// lv_conf.h #define LV_COLOR_DEPTH 32 // 如果你的屏幕是 RGB888,这里设为 32 (ARGB8888) #define LV_MEM_SIZE (32 * 1024U * 1024U) // 为 LVGL 分配 32MB 动态内存,根据你的系统总内存调整 // 使用 Linux Framebuffer 和 evdev #define LV_USE_LINUX_FBDEV 1 #define LV_USE_EVDEV 1 // 根据性能调整,如果感觉卡顿,可以降低刷新周期或渲染区域 #define LV_DISP_DEF_REFR_PERIOD 30 // 默认刷新周期(毫秒) #define LV_INDEV_DEF_READ_PERIOD 30 // 输入设备读取周期 // 启用你需要的控件和特性 #define LV_USE_LABEL 1 #define LV_USE_BTN 1 #define LV_USE_IMG 1 // ... 其他控件

接下来,在src/main.c中,你需要初始化 LVGL,并绑定 Framebuffer 和 evdev 驱动。

// src/main.c #include “lvgl/lvgl.h” #include “lv_drivers/display/fbdev.h” #include “lv_drivers/indev/evdev.h” int main(void) { // 1. 初始化 LVGL lv_init(); // 2. 初始化并注册显示驱动 (Framebuffer) fbdev_init(); // 初始化 fbdev 驱动 static lv_disp_draw_buf_t draw_buf; // 定义绘制缓冲区 static lv_color_t buf1[800 * 480]; // 定义一个与屏幕分辨率匹配的缓冲区 // 如果你的内存紧张,可以定义两个较小的缓冲区,使用双缓冲机制 // static lv_color_t buf1[800 * 100], buf2[800 * 100]; lv_disp_draw_buf_init(&draw_buf, buf1, NULL, 800 * 480); // 单缓冲 // lv_disp_draw_buf_init(&draw_buf, buf1, buf2, 800 * 100); // 双缓冲 static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.draw_buf = &draw_buf; disp_drv.flush_cb = fbdev_flush; // 设置刷新回调函数为 fbdev disp_drv.hor_res = 800; disp_drv.ver_res = 480; lv_disp_drv_register(&disp_drv); // 3. 初始化并注册输入设备驱动 (evdev 触摸屏) evdev_init(); static lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = evdev_read; // 设置读取回调函数为 evdev lv_indev_drv_register(&indev_drv); // 4. 创建你的第一个 UI lv_obj_t * btn = lv_btn_create(lv_scr_act()); lv_obj_center(btn); lv_obj_t * label = lv_label_create(btn); lv_label_set_text(label, “Hello ZYNQ7020!”); // 5. 主循环 while(1) { lv_timer_handler(); // 处理 LVGL 任务,必须在主循环中定期调用 usleep(5000); // 休眠 5ms,避免过度占用 CPU } return 0; }

2.3 编译与部署

为 ZYNQ7020 的 ARM Cortex-A9 核心交叉编译你的应用。你需要 Petalinux 生成的交叉编译工具链。

# 在主机上,source Petalinux 的环境设置脚本 source <petalinux_installation_path>/settings.sh # 或者直接使用工具链 export CC=arm-linux-gnueabihf-gcc # 编写一个简单的 Makefile # Makefile CC = arm-linux-gnueabihf-gcc CFLAGS = -I./lvgl -I./lv_drivers -I./lv_examples -O2 -Wall LDFLAGS = -lm -lpthread SRCS = src/main.c \ lvgl/src/core/*.c \ lvgl/src/draw/*.c \ lvgl/src/font/*.c \ lvgl/src/hal/*.c \ lvgl/src/misc/*.c \ lvgl/src/widgets/*.c \ # ... 添加你启用的所有 LVGL 组件源文件 lv_drivers/display/fbdev.c \ lv_drivers/indev/evdev.c TARGET = lvgl_demo all: $(CC) $(CFLAGS) $(SRCS) -o $(TARGET) $(LDFLAGS) clean: rm -f $(TARGET)

编译成功后,将可执行文件lvgl_demo和可能需要的字体文件(如lvgl/src/font/montserrat-*.c中你启用的字体)拷贝到开发板的文件系统中(例如通过 SD 卡或网络)。在开发板终端运行./lvgl_demo,你应该能看到一个带按钮的界面,并且触摸有反应。

3. 性能优化与内存管理:让界面流畅起来

在资源有限的嵌入式平台,LVGL 跑起来和“流畅地跑起来”是两回事。ZYNQ7020 的 ARM Cortex-A9 主频通常为 667MHz 或 1GHz,需要有针对性的优化。

3.1 绘制性能优化

  1. 启用双缓冲(Double Buffering):这是减少屏幕撕裂、提升流畅感最有效的方法之一。如前面代码所示,你需要分配两个绘制缓冲区(buf1 和 buf2)。LVGL 在一个缓冲区绘制时,另一个缓冲区的内容被 DMA 搬运到屏幕。这需要你的fbdev_flush回调函数支持。在 Linux Framebuffer 驱动中,这通常通过ioctl调用FBIO_WAITFORVSYNC来实现页交换或等待垂直同步。

  2. 调整刷新区域:LVGL 默认只刷新发生变化的区域。确保LV_USE_AREALV_USE_REFR_DEBUGlv_conf.h中是开启状态(用于调试)。你可以通过LV_DISP_DEF_REFR_PERIOD控制最小刷新周期,避免不必要的重绘。

  3. 优化绘制函数:对于 ZYNQ,确保在lv_conf.h中启用了针对 ARM 的优化:

    #define LV_USE_DRAW_ARM2D 0 // ZYNQ7020 没有 ARM2D 硬件加速,保持为 0 // 但可以启用通用的优化 #define LV_USE_DRAW_SW 1 #define LV_DRAW_SW_COMPLEX 1 // 启用复杂的软件绘制算法,可能更快

    更根本的优化在于使用 PL 侧硬件加速。例如,你可以用 FPGA 逻辑实现图像填充(fill)、混合(blend)、旋转缩放等常用图形操作,并通过自定义的flush_cb回调,将绘制命令分发给硬件加速器。但这属于高级主题,需要硬件设计知识。

  4. 降低颜色深度:如果屏幕支持,将LV_COLOR_DEPTH从 32 降到 16(RGB565),可以立即将帧缓冲区和绘制缓冲区的内存占用减半,同时提升内存带宽利用率,对性能提升非常明显。

3.2 内存配置策略

LVGL 使用动态内存(lv_mem_alloc)和静态内存(直接分配数组)。在lv_conf.h中配置的LV_MEM_SIZE是动态内存池的大小。

  • 监控内存使用:在lv_conf.h中打开LV_USE_MEM_MONITOR,可以在日志中查看内存使用情况,帮助你调整LV_MEM_SIZE
  • 使用静态分配:对于已知大小的对象(如主屏幕、关键控件),可以在栈或全局区静态创建,而不是动态分配。这可以减少内存碎片。
  • 字体管理:中文字库巨大,是内存消耗大户。不要一次性加载所有字库。
    • 使用 LVGL 内置的字体转换工具,只生成你项目需要的字符集(比如仅 ASCII 和几百个常用汉字)。
    • 将字体存储在外部 SPI Flash 或 SD 卡中,使用lv_font_load接口动态加载和卸载,实现“按需取字”。
  • 图片资源:同样,大图片建议使用文件系统动态加载(lv_img_set_src传入路径),而不是编译进固件作为 C 数组。

3.3 输入设备响应优化

触摸响应慢或不准,问题往往不在 LVGL,而在底层。

  1. 校准触摸屏:使用tslib库进行校准。在开发板上运行ts_calibrate,生成校准数据。在你的应用中,需要在调用evdev_init()之前,设置TSLIB_TSDEVICE环境变量指向正确的/dev/input/eventX设备,并确保evdev驱动读取的是校准后的数据。lv_drivers中的evdev.c可能需要修改以集成tslib
  2. 调整读取频率LV_INDEV_DEF_READ_PERIOD设置得太高(如 100ms)会导致触摸不跟手,设置得太低(如 1ms)会无谓消耗 CPU。从 30ms 开始调整,找到一个平衡点。
  3. 检查内核事件队列:如果触摸事件丢失,可能是内核输入子系统缓冲区满了。可以尝试在应用启动前,通过ioctl调整输入设备的缓冲区大小,但这需要内核配置支持。

4. 进阶开发与问题排查:从 Demo 到产品

当基础显示和触摸都稳定后,就可以着手构建复杂的应用了。这里有几个从 Demo 到产品级应用必须处理的环节。

4.1 多页面管理与事件处理

LVGL 使用“对象(Object)”模型。一个页面通常是一个lv_obj_t容器。管理多页面,关键在于理解lv_scr_act()(当前屏幕)和lv_scr_load()(加载新屏幕)。

// 创建两个页面 lv_obj_t * scr1 = lv_obj_create(NULL); lv_obj_t * scr2 = lv_obj_create(NULL); // 在 scr1 上放一个按钮,点击后切换到 scr2 lv_obj_t * btn = lv_btn_create(scr1); lv_obj_add_event_cb(btn, switch_screen_event, LV_EVENT_CLICKED, scr2); // 将 scr2 作为参数传递 // 事件回调函数 static void switch_screen_event(lv_event_t * e) { lv_obj_t * target_screen = lv_event_get_user_data(e); lv_scr_load(target_screen); }

事件处理要点

  • LVGL 9.x 的事件系统比旧版本更强大和统一。几乎所有交互(点击、长按、拖动、按键等)都通过lv_obj_add_event_cb来绑定回调。
  • 用户数据(User Data):通过lv_event_get_user_data(e)可以获取绑定事件时传入的任意指针,这是在不同回调函数间传递上下文信息的关键机制。
  • 避免阻塞:事件回调函数里不要执行耗时操作(如大量计算、文件读写)。如果需要,使用 LVGL 的定时器(lv_timer_create)或任务(lv_task_handler)来异步处理。

4.2 使用 SquareLine Studio 或 LVGL 官方模拟器进行 UI 设计

在 PC 上设计界面,能极大提升开发效率。

  1. SquareLine Studio:这是一个强大的 LVGL UI 设计器,支持拖拽控件、设置属性、生成 C 代码。你可以在 PC 上设计好界面,然后将生成的ui.cui.h文件集成到你的 ZYNQ 项目中。需要注意版本兼容性,确保 Studio 支持 LVGL 9.5.0。
  2. LVGL 官方模拟器:在lv_sim_<platform>项目(如lv_sim_vscode_sdl)中,你可以用完整的 LVGL 库在 Windows/Linux/macOS 上运行和调试你的 UI 代码。这非常适合前期逻辑开发和样式调试。在模拟器上调好的代码,大部分可以直接移植到 ZYNQ 目标平台。

4.3 常见问题排查清单

当你的 LVGL 应用在 ZYNQ 上出现问题时,按以下顺序排查:

  1. 屏幕无显示或花屏

    • 第一步:用cat /dev/urandom > /dev/fb0测试 Framebuffer 是否正常。如果不正常,问题在底层驱动或硬件连接。
    • 第二步:检查lv_conf.h中的LV_COLOR_DEPTH是否与屏幕及 Framebuffer 驱动设置的格式匹配(通常是 32 或 16)。
    • 第三步:检查fbdev_flush回调函数是否正确实现,特别是内存地址和长度参数。
  2. 触摸无反应或不准

    • 第一步:用evtest /dev/input/eventX命令(在开发板上)测试原始触摸事件。如果没有事件输出,是触摸驱动或硬件问题。
    • 第二步:检查evdev_init()是否打开了正确的设备节点(如/dev/input/event1)。可以通过dmesg | grep -i input查看设备分配。
    • 第三步:确认是否需要进行触摸校准(tslib)。
  3. 界面非常卡顿

    • 第一步:使用tophtop命令查看 CPU 占用率。如果lvgl_demo进程占用持续接近 100%,说明主循环太快或任务太重。调整usleep的值。
    • 第二步:在lv_conf.h中打开LV_USE_REFR_DEBUG,查看刷新区域的日志,检查是否在频繁刷新整个屏幕。
    • 第三步:检查是否使用了过于复杂的样式(如大面积渐变)或过多半透明效果,这些在软件渲染下很耗 CPU。考虑简化设计。
    • 第四步:启用双缓冲并确认 VSYNC 同步是否生效。
  4. 运行一段时间后崩溃或内存不足

    • 第一步:打开LV_USE_MEM_MONITORLV_USE_LOG,观察内存使用趋势。
    • 第二步:检查是否有内存泄漏——创建的对象(lv_xxx_create)是否在不需要时被正确删除(lv_obj_dellv_obj_del_async)。
    • 第三步:检查字体和图片的加载方式,是否无意中加载了多份。
  5. 按键事件处理(如果外接了按键):

    • LVGL 的evdev驱动主要处理指针(触摸)设备。对于 GPIO 按键,你需要自己实现一个read_cb。一种常见做法是:在一个单独的线程中轮询 GPIO 状态,将按键状态转换为lv_indev_data_t结构体数据,并调用lv_indev_read来模拟输入设备。

4.4 生产环境考量

如果项目要最终产品化,还需要考虑:

  • 启动速度:优化根文件系统,使用initramfs或将应用直接编译进内核镜像,减少从启动到显示界面的时间。
  • 稳定性:增加看门狗(Watchdog)机制,防止应用卡死。可以考虑将 LVGL 主循环放在一个独立的线程或进程中,由监控进程管理。
  • 功耗管理:在无操作时,可以通过 LVGL 的lv_disp_set_rotation和调整背光 PWM 来降低功耗。
  • 远程更新:设计通过网络或 U 盘更新应用甚至整个系统镜像的机制。

在 ZYNQ7020 上成功运行 LVGL 9.5.0,标志着你打通了从 FPGA 逻辑设计、Linux 驱动开发到上层 GUI 应用的全栈路径。整个过程里,最容易出问题的环节往往是硬件显示时序Linux 驱动配置LVGL 内存配置这三者的匹配。我的建议是,严格按照“先硬件、再驱动、最后应用”的顺序,每一步都做最小化验证。先让/dev/fb0能显示噪点,再让evtest能读到触摸坐标,最后才去编译 LVGL 应用。这样分层排查,问题定位会清晰很多。当基础框架稳定后,LVGL 丰富的控件和活跃的社区,能让你的嵌入式界面开发效率获得质的提升。

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

基于SpringBoot的非物质文化遗产管理系统(源码+lw+部署文档+讲解等)

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

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

5G NR LDPC编译码误码率仿真:MATLAB完整实现与调试指南

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

作者头像 李华
网站建设 2026/9/1 13:10:06

OpenCV车道线识别详解:从灰度化到Hough变换的完整图像处理流程

简介&#xff1a;这是一份基于Python与OpenCV实现的车道线识别项目资料&#xff0c;面向自动驾驶、智能交通领域的开发者和图像处理初学者&#xff0c;可帮助理解从图像输入到车道线输出的完整处理链路。资源包含完整的检测流程&#xff1a;图像灰度化与高斯滤波、Canny边缘检测…

作者头像 李华
网站建设 2026/9/1 13:06:48

maxkb4j前端源码解析:Java智能体平台的Vue3实现

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

作者头像 李华
网站建设 2026/9/1 13:03:17

利用迷你PC与万兆网络构建高可用Proxmox VE集群实战指南

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

作者头像 李华