很多人第一次拿到飞腾D2000的板子都会习惯性先去翻内核、搞文件系统结果卡在最前面的UBOOT引导阶段串口什么输出都没有或者内核起来一半就睡死。实际上飞腾平台的引导镜像制作和x86那套完全不同它既不是传统BIOSgrub的玩法也跟树莓派那种固件里帮你把啥都配好的路子不一样UBOOT、设备树、内核三个部分必须自己串起来任何一个环节脱节后面全是白忙。这篇东西就是把我自己的实战过程捋一遍覆盖D2000、E2000、D3000三个平台从设备树配置到镜像烧录的关键步骤希望能帮到正在调板子的朋友。先说清楚这篇适合谁手里有飞腾开发板或工控机、需要自己裁剪UBOOT做引导镜像的工程师以及刚接触国产化平台想搞明白启动链路的人。如果你是只打算直接apt装个系统用那这篇对你意义不大但如果你想弄明白那个uboot.bin或者bootable U盘到底是怎么来的可以接着往下看。1. 飞腾平台引导链路全貌为什么会把UBOOT和DTB绑在一起1.1 从上电到内核的完整路径飞腾D2000/E2000/D3000这三个平台虽然是不同定位D2000主打桌面和服务器E2000偏向嵌入式和工控D3000是最新的高性能型号但从引导层面看它们的路径是高度统一的。板子上电后片内BootROM会先执行然后加载UBOOT到L2 Cache里跑没错UBOOT在早期阶段是直接在Cache里执行的这个细节很多人会忽略。UBOOT起来之后负责初始化DDR、时钟、串口、网卡这些基础外设接着读取设备树DTB把硬件描述传给内核最后跳转到kernel。这里有个关键点飞腾平台的UBOOT和DTB并不是完全松耦合的。UBOOT本身要能跑起来它需要知道最基础的硬件信息比如串口地址、DDR配置、时钟频率而内核启动时需要的更完整的硬件描述则来自DTB。所以UBOOT里会内嵌一份简化版的设备树或者硬件配置同时外部的DTB文件也要匹配板子。如果你换了板型只改DTB而不管UBOOT多半起不来反过来只改UBOOT不改DTB内核起来之后也会有一堆外设找不到。1.2 为什么要单独制作引导镜像x86平台的用户习惯是装个grub然后靠grub.cfg去引导不同内核。飞腾这类ARM平台的选择通常是直接把UBOOT和DTB打包进一个镜像文件或者烧录工具会把它们分别写到固定的offset。做引导镜像的本质就是把编译产物UBOOT二进制、DTB文件、内核镜像按照目标平台的存储布局打包成可以直接烧进SPI NOR Flash、SD卡、eMMC或者U盘的东西。我早期踩过一个坑想省事直接拿别人编译好的uboot.bin烧进去结果板子只是偶尔能起来换了批次的内存颗粒就频繁死机。后来才发现是UBOOT里的DDR配置和实际板子不匹配别人板子的内存拓扑跟我的不一样这个必须针对自己的板子重新配置。所以引导镜像的制作一定要跟着板子走不能盲目照搬。2. 环境准备与交叉编译工具链2.1 编译环境搭建别再用老旧工具链了制作UBOOT镜像的第一步是准备交叉编译环境。飞腾平台是ARM64架构所以需要aarch64交叉编译器。我最早用的是Ubuntu 18.04自带的gcc-aarch64-linux-gnu 7.x编译UBOOT 2022.04的时候遇到了一个坑编译器太老导致某些ARMv8.2的指令不支持链接阶段直接报错。后来统一升级到Ubuntu 20.04/22.04用gcc-aarch64-linux-gnu 9.x或10.x问题就消失了。安装命令很简单sudo apt-get install gcc-aarch64-linux-gnu如果你要编译新版UBOOT2023.04及以上建议用gcc 10以上的版本同时注意交叉编译器的前缀。飞腾官方SDK里给的是aarch64-linux-gnu-和Debian/Ubuntu的aarch64-linux-gnu-前缀一致直接用就行。但如果你用的是某些厂商提供的工具链前缀可能是aarch64-none-linux-gnu-这时候需要修改UBOOT的Makefile或者通过环境变量指定CROSS_COMPILE。还有一点建议在Linux原生环境下编译不要用WSL或虚拟机挂载Windows目录否则文件权限和符号链接处理会让你怀疑人生。我实测过WSL2里编译UBOOT偶尔会出现莫名其妙“No rule to make target”的错误换成纯Linux环境就好了。如果你只有Windows机器宁可在里面跑一个正式的Ubuntu虚拟机也别用Windows目录直接编译。2.2 获取源码与版本匹配官方SDK还是主线UBOOT飞腾平台的UBOOT有两个来源一个是飞腾官方发布的SDK里带的UBOOT源码另一个是主线UBOOTdenx.de/u-boot加上飞腾社区补丁。我的建议是如果做产品优先用官方SDK里的UBOOT如果只是学习研究可以用主线版本。因为官方SDK里已经把D2000/E2000/D3000的板级配置、DDR初始化代码、PHY驱动这些都调好了你拿到手大概率能跑起来。主线UBOOT对飞腾的支持虽然也在不断完善但某些板子细节还是要自己改。以D2000为例官方SDK里通常包含这些关键文件board/phytium/d2000/板级初始化代码arch/arm/mach-phytium/SoC相关的底层初始化include/configs/phytium_d2000.hUBOOT本身的配置头文件对应的defconfigphytium_d2000_defconfigD3000由于发布时间较晚主线UBOOT支持还不算特别全面建议直接用飞腾提供的SDK。E2000则要特别注意它有多个型号E2000Q、E2000S等外设配置差别不小选defconfig的时候一定要核对型号。3. 设备树配置核心细节内存、串口、网口的常见坑3.1 内存配置先搞清你的板子是几个通道设备树里最影响启动成功率的配置首先是内存。飞腾D2000支持双通道DDR4内存控制器通过SPD来探测内存条参数但如果你的板子用的是板载内存颗粒没有SPD或者SPD信息不完整UBOOT在DDR初始化阶段就会卡住。设备树中内存节点的典型写法如下memory0 { device_type memory; reg 0x0 0x00000000 0x0 0x80000000; };这个表示起始物理地址0x0大小2GB。如果板子是4GB内存reg要改成0x0 0x00000000 0x1 0x00000000。这里有个容易出问题的点D2000的物理内存地址映射并不是简单的从0开始某些型号的地址窗口可能从0x4000000000开始具体要看芯片手册。如果你发现内核起来后能识别到内存但dmesg里地址段不对多半就是这里写错了。我在调试E2000板子时遇到过一种诡异现象UBOOT能启动也能加载内核但内核一解压就报“Kernel panic - not syncing: Out of memory”后来查了好久才发现是设备树里内存大小写的是1GB实际板子只有512MBUBOOT从ATF传递的memory信息覆盖了DTB里的值导致内核拿到了错误的内存上限。解决办法是确保UBOOT环境变量里的内存设置和设备树保持一致。3.2 串口配置调试信息不出来先查这里串口也是引导阶段最重要的外设。飞腾平台的调试串口通常是UART0地址在设备树里的写法uart0: serial28000000 { compatible arm,pl011; reg 0x0 0x28000000 0x0 0x1000; interrupts 0x0 0x1e 0x4; clock-frequency 50000000; };注意clock-frequency这个字段很多人在移植的时候容易漏掉。如果这个值配错了串口输出的波特率会产生偏差实际波特率可能不是115200而是9600或其它值看起来就像是“串口没输出”。我建议调试阶段先把波特率固定在115200等系统稳定了再考虑时钟框架自动计算。UBOOT阶段串口能否正常工作还取决于include/configs/phytium_d2000.h里的宏定义。你需要确认CONFIG_SYS_NS16550_REG_SIZE是否匹配飞腾D2000的串口寄存器是32位访问如果这个宏是1串口初始化大概率不正常。3.3 网口与PHY千兆不通的排查思路网口配置是设备树里水最深的地方尤其是PHY的地址和模式。飞腾D2000的GMAC在设备树里长这样gmac0: ethernet280c0000 { compatible phytium,dwmac; reg 0x0 0x280c0000 0x0 0x1000; phy-mode rgmii; phy-handle phy0; phy0: phy1 { reg 1; device_type ethernet-phy; }; };这里的phy-handle引用了phy1表示PHY的地址是1。实际调试中PHY地址需要根据板子原理图确定常见的是0、1、4、7这几个。如果网口不通拿mdio命令在UBOOT里扫一下 mdio list PHY 0: Linear Technology LT8618 PHY 1: ...如果列表里没有PHY说明MDIO总线配置不对或者PHY芯片的电源/复位没拉起来。网上经常有人问“fmql uboot千兆网不通”这类问题其实多半就是PHY的复位GPIO没在设备树或UBOOT阶段正确初始化。这是一个很容易被忽略但后果很严重的细节PHY的复位引脚必须在MDIO通信之前被释放。你可以通过UBOOT的gpio命令手动拉高复位引脚来验证 gpio set 0x18 gpio clear 0x18如果这样操作后网口能通那就需要在板级初始化代码里补上复位逻辑。4. UBOOT镜像制作完整实操从源码到可烧录文件4.1 选择合适的defconfigD2000/E2000/D3000别搞混交叉编译器装好之后进入UBOOT源码目录先查看支持的飞腾板卡配置make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- list_defconfig | grep phytium正常会看到类似这样的输出phytium_d2000_defconfigphytium_e2000_defconfigphytium_d3000_defconfig选配置的时候要注意同一个SoC可能对应多个板子比如D2000可能有phytium_d2000_defconfig和phytium_d2000_evb_defconfig之类的变体。如果SDK里没有你板子的defconfig你需要拿最接近的板子配置来改。以D2000为例基本命令是make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- phytium_d2000_defconfig这里重点说一下D3000。D3000平台和D2000在某些外设地址上有变化如果直接用D2000的defconfig去编译UBOOT即使能跑起来网卡和PCIe也大概率无法工作。你需要确认SDK里是否有独立的D3000配置如果没有需要核对arch/arm/mach-phytium/下的Kconfig看看是否已经添加了D3000的SoC选项。4.2 编译UBOOT并生成最终镜像配置好后执行编译make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后在源码根目录下会生成u-boot.bin。这个u-boot.bin是ELF去掉调试信息后的裸二进制但它还不能直接烧录因为飞腾平台通常需要在UBOOT镜像前面加上头信息或者打包成特定的镜像格式。这一步不同板卡差异很大。以我常用的飞腾D2000开发板为例最终的烧录文件是通过飞腾提供的打包脚本make_uboot.sh生成的它会把u-boot.bin加上文件头生成fw_uboot.bin。这个fw_uboot.bin才是可以直接烧进SPI Flash的文件。如果没有官方脚本也可以手动用mkimage来生成tools/mkimage -n phytium uboot -A arm64 -O linux -T firmware -C none -a 0x1000 -e 0x1000 -d u-boot.bin fw_uboot.bin注意-a和-e的地址要跟板子的链接地址一致。在arch/arm/mach-phytium/下的链接脚本里能找到UBOOT的入口地址比如D2000的SPL或UBOOT链接地址可能是0x1000或者0x40000000具体要看你用的是SPL模式还是直接运行完整UBOOT的模式。4.3 打包启动U盘从SD卡或eMMC启动很多飞腾板子支持从SD卡或U盘启动。制作可启动U盘的思路是在U盘第一个分区放内核镜像和DTB文件然后把UBOOT写到U盘开头的固定偏移位置。以SD卡为例通常的布局是这样的偏移内容0x0分区表0x1000UBOOT镜像0x2000预留/ATF0x100000分区1FAT32存放Image和dtb操作时注意写UBOOT时不要用dd直接盖掉整个磁盘要先把UBOOT写到正确的偏移sudo dd iffw_uboot.bin of/dev/sdb seek1 bs512 convnotrunc这里的seek1表示跳过第一个512字节的扇区因为扇区0通常是MBR。不同板子UBOOT存放的偏移不一样有的是seek1有的是seek8这个跟板级配置里的CONFIG_SYS_MMC_ENV_DEV和烧录地址有关务必查清楚再操作。写错了轻则启动失败重则把U盘的MBR冲掉导致电脑无法识别U盘分区。4.4 编译内核镜像和DTB与UBOOT配套引导镜像不只是UBOOT本身内核和DTB也是镜像的一部分。飞腾平台的内核编译相对简单标准的ARM64内核编译流程即可export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig make Image dtbs如果是银河麒麟V10或者统信UOS这类操作系统它们有自己定制过的内核建议直接用系统自带的/boot/Image和/boot/*.dtb或者从系统镜像里提取。离线环境下尤其如此自己编译内核很容易因为版本不匹配导致系统起来后显卡黑屏、网卡不工作。这里提一个专门针对银河麒麟V10国防版这类离线部署场景的实战经验内核和UBOOT的版本不需要完全一致但DTB必须匹配内核。比如内核是5.10DTB用4.19编译出来的启动时会报一堆“failed to get clock”或者GPIO请求失败的错误。正确做法是用目标内核源码里的dtbs目标去编译DTB。5. 常见问题与排查技巧实录5.1 启动卡死、无串口输出按阶段定位我把调飞腾板子时常见的故障按现象分成三类完全无输出、UBOOT启动部分输出、内核启动阶段停止。完全无输出先查电源和时钟再用示波器测DDR的VREF电压是否正常最后确认boot引脚。飞腾的BootROM启动模式由引脚电平决定比如D2000的启动模式是从SPI还是SD/eMMC如果引脚拨错了UBOOT根本没被加载。这种情况你改UBOOT源码是没用的要先检查启动引脚。UBOOT有输出但卡在DDR初始化重点看DDR配置参数比如内存颗粒的密度、位宽、时序。如果你用的是板载内存颗粒但UBOOT里按内存条配置来初始化大概率卡住。这个阶段可以用飞腾提供的DDR Training工具来生成配置参数。UBOOT能跑到命令行但启动内核就挂先检查内核Image和设备树是否匹配然后重点关注bootargs里的console参数。飞腾的串口设备名是ttyS0如果bootargs写成了ttyAMA0内核日志输出不到串口上实际可能内核已经起来了但你看不到。一个典型的内核启动命令如下 setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw load mmc 0:1 $kernel_addr_r Image load mmc 0:1 $fdt_addr_r phytium-d2000.dtb booti $kernel_addr_r - $fdt_addr_r注意booti命令ARM64内核要用booti而不是bootm。很多人用习惯了ARM 32位的bootm命令拿到ARM64平台还在用结果内核一直启动不了。5.2 设备树配置错误导致的疑难问题设备树里有一个容易忽略的内容是chosen节点里的bootargs。如果你没有在UBOOT环境变量中设置bootargs内核会直接使用DTB中chosen节点指定的参数这个细节有时候会让人摸不着头脑。另外GPIO和中断的配置也很容易出错。飞腾平台的GPIO控制器有多个设备树里需要正确引用gpio-controller和#gpio-cells属性。如果中断配置不对可能出现内核启动后某个驱动一直在等中断系统表现为“卡死”但其实CPU还在跑。遇到这种情况可以用irqtop之类的工具看看是不是中断风暴。5.3 离线部署引导镜像的完整工作流银河麒麟V10国防版这类系统在离线环境部署时一般不会让你现场编译UBOOT而是要求在开发机上把引导镜像、内核、DTB全部准备好然后打包复制到目标机上。这里分享一套我常用的离线部署工作流在开发机上准备好fw_uboot.bin、Image、phytium-d2000.dtb、ramdisk或initrd.img。把内核和DTB复制到U盘第一个分区FAT32格式。用dd把fw_uboot.bin写到U盘或SD卡指定偏移位置。板子插入U盘上电后UBOOT读取uboot.env或者默认环境变量。如果没有存储介质保存环境变量可以用saveenv先初始化环境变量。在UBOOT命令行里设置启动命令并测试能否手动启动内核。手动启动成功后用env default -a恢复默认环境然后修改bootcmd保存。这套流程里最容易出问题的是第4步UBOOT找不到环境变量存储位置时有时会把环境变量读成乱码导致启动命令异常。解决办法是在首次启动时用env default -a强制恢复默认环境。5.4 网口不通和U-Boot网卡启动失败网口问题在引导阶段特别头疼毕竟很多人想通过TFTP下载内核结果网卡起不来。先说硬件检查飞腾板子的网口PHY芯片如果是YT8521或者瑞芯微平台的PHY芯片设备树里通常要配置phy-mode为rgmii-id因为这类PHY内部已经包含了RX和TX的延时不需要在MAC侧再添加延时。如果你错误地配置成了rgmii丢包率会高得离谱甚至完全不通。UBOOT阶段调试网口首先用mdio list确认PHY是否存在前面已提到再用mii info查看协商结果 mii info PHY 0x01: OUI0x0001, Model0x0a, Rev0x01, 1000baseT, FDX如果协商速度不对检查设备树里的max-speed属性有的板子默认限制成了100Mbps。我调试的时候遇到过一种情况PHY芯片支持千兆但变压器或PCB走线有缺陷千兆模式下误码率极高表现为TFTP到一半就超时。后来把设备树改成max-speed 100;就好了但这只能算临时规避硬件问题要反馈给Layout工程师处理。5.5 UBOOT环境变量救命用的bootcmd和distro_bootcmd很多人刷完UBOOT之后发现板子无法自动启动系统原因是没有设置正确的bootcmd。UBOOT的环境变量机制在飞腾平台上非常重要因为它的默认环境变量可能不包含你的存储设备。如果板子是从SD卡启动建议把bootcmd写成这样 setenv bootcmd mmc dev 0; mmc rescan; load mmc 0:1 $kernel_addr_r Image; load mmc 0:1 $fdt_addr_r phytium-d2000.dtb; booti $kernel_addr_r - $fdt_addr_r saveenv这里有个细节load命令会返回加载的字节数如果加载失败会返回非0值bootcmd会中断执行。你可以在命令前加if判断比如 setenv bootcmd if load mmc 0:1 $kernel_addr_r Image; then load mmc 0:1 $fdt_addr_r phytium-d2000.dtb; booti $kernel_addr_r - $fdt_addr_r; fi这样即使内核文件不存在UBOOT也不会直接死掉而是回到命令行等你操作。6. 进阶给UBOOT添加开机动画和定制功能6.1 开机动画LPB模式还是自己画网上有一个热门搜索是“rk3568 uboot添加开机动画”其实飞腾平台的思路也类似。UBOOT从加载内核到显示Logo通常有两种做法一是用UBOOT的LPBLinux Patching Boot logo机制二是直接在UBOOT里跑一个简单的framebuffer显示驱动把logo画出来。如果你用的是飞腾官方SDK里面可能已经有简单的显示驱动支持只需要在defconfig里打开对应的配置项CONFIG_VIDEOy CONFIG_VIDEO_PHYTIUMy然后在板级初始化里调用video_display相关函数来初始化HDMI或DP输出。这个方法比较麻烦需要你了解飞腾GPU和显示控制器的寄存器映射。如果你不想在UBOOT阶段花太多精力建议把开机动画留到内核阶段做UBOOT只保证能从显示接口输出一行简单的“U-Boot”文字就行。6.2 定制UBOOT默认环境变量减少现场调试时间产品出货时很多工程师会希望UBOOT默认就带好bootcmd和bootargs用户上电即启动。实现方式是修改include/configs/phytium_d2000.h中的默认环境变量定义#define CONFIG_EXTRA_ENV_SETTINGS \ bootcmdmmc dev 0; mmc rescan; load mmc 0:1 $kernel_addr_r Image; load mmc 0:1 $fdt_addr_r phytium-d2000.dtb; booti $kernel_addr_r - $fdt_addr_r\0 \ bootargsconsolettyS0,115200 root/dev/mmcblk0p2 rw rootwait\0然后再重新编译UBOOT。需要注意的是如果你烧录新UBOOT之后没有清除环境变量存储区域旧的环境变量依然会覆盖默认值。所以烧录完新镜像后第一次启动时一定要执行env default -a再saveenv否则你辛苦设置的默认环境变量根本不会生效。7. 写在最后的调试体会做飞腾平台的引导镜像最大的感受就是千万别把x86那套思维带进来。x86上UBOOT的存在感极低因为BIOS/UEFI已经把大部分脏活累活干完了飞腾平台上UBOOT要直接面对DDR训练、PHY初始化、设备树传递这些底层细节任何一个环节不匹配现象都是千奇百怪的。我自己调试D2000平台的时候有一个晚上几乎全部耗在排查“串口只能输出UBOOT信息内核起来后杂字符乱飞”的问题上最后定位到是DTB里串口时钟配错了clock-frequency少写了一个0。这种问题光靠看代码是看不出来的必须对着板子一根根量信号、一遍遍看寄存器值才能找到根因。如果你也是刚开始接触飞腾平台建议按照这样的顺序来验证你的引导镜像先确保串口有UBOOT输出再确保内存大小识别正确然后是网卡或SD卡能读写最后才去跑内核。每一步都要单独验证不要一次性把所有功能都配齐再上电否则出了问题根本没法定位。最后再分享一个小经验每次编译UBOOT之前养成清理环境的习惯。交叉编译改变了某些生成的配置之后如果不执行make mrproper容易出现“配置改了但没生效”的诡异问题。我一般会写一个简单的构建脚本把mrproper、defconfig、make、打包串在一起既省时间又不容易出错。做好这一套流程飞腾平台的引导镜像制作就没那么玄乎了。
企业数字化 ERP 产品动态
相关推荐
外墙墙体渗水维修师傅 好工匠防水 高空作业 外墙裂缝修补专用材料 随着国内建筑使用年限逐步增加,以及北方特殊气候对建筑外墙的持续侵蚀,外墙防水维修市场的需求正在持续增长。京津冀区域受北方冬季冻融循环、春季持续返潮、沿海区域盐蚀、雨季强降水的多重影响,外墙渗水问题成为民居、商用建筑、工业厂房都… · 2026/9/25 8:52:11
ESP32-S3桌面AI机器人实战:全双工语音与视觉多模态交互全解析 EchoEar喵伴这个项目,实际做下来我最大的感受是:它表面上看是个桌面小玩具,本质上却是一道特别扎手的嵌入式工程题。要在ESP32-S3这颗MCU上同时搞定全双工语音交互、摄像头视觉采集、云端大模型对话,还要保证用户能随时打断机器人… · 2026/9/25 8:51:53
GD32高级定时器互补PWM输出与死区控制实战 写GD32的高级定时器,绕不开三相电机控制、全桥逆变、UPS这类场景。做这类项目的人,百分之九十九都躲不过一个需求:要输出两路相位相反、中间还夹着一小段“空白”的PWM,而且这段空白还得精确可控。这段空白就是死区,控… · 2026/9/25 8:51:53
S3 Browser:Windows原生S3管理工具深度解析 1. 为什么是 S3 Browser?——Windows 用户管理 AWS S3 的真实痛点与替代方案对比在 Windows 桌面环境里直连 AWS S3,你大概率经历过这几种“卡点”:用 AWS CLI 命令行敲半天aws s3 cp却搞不定路径斜杠方向、权限报错堆成山;用 VS … · 2026/9/25 9:32:21
Atlas 300V 24G 上部署 YOLO:从模型转换到调优的完整实战 最近不止一个人来问我同一个问题:手里有张 Atlas 300V 24G 卡,到底能不能拿来跑 YOLO,是不是真像网上说的那样“插上就能用”。还有人干脆搞混了它的定位,把它当成普通显卡去跑训练,结果一跑就懵。今天这篇就把我这段时… · 2026/9/25 9:32:09
Xred木马深度剖析:PHP WebShell的隐蔽驻留与实战排查防御 1. 从一次应急响应说起:Xred木马到底是什么我第一次接触到Xred木马,是在一次内部安全巡检中。当时一台测试服务器的CPU占用率长期飘红,排查了半天也没找到明显的异常进程,直到用netstat看到一条非常可疑的外连连接,顺藤… · 2026/9/25 9:31:38
SQL Server PIVOT 行转列实战:静态与动态写法及避坑指南 简介:这份PDF资料聚焦SQL Server中行转列的核心技术PIVOT,面向需要处理报表数据转换的数据库开发人员与数据分析初学者。内容以WEEK_INCOME收入表为例,从传统CASE配合SUM的写法切入,逐步过渡到PIVOT操作符的语法结构,并… · 2026/9/25 9:31:26
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37