新闻详情

新闻详情

首页 / 资讯中心 / 详情

MCU开发全流程:编译、烧录、仿真调试原理与避坑指南

发布时间:2026/9/29 2:00:11来源:尧图网络
MCU开发全流程:编译、烧录、仿真调试原理与避坑指南
前阵子同事新画了一块板子第一次送焊回来驱动、时钟都检查过程序在Keil里编译也全部通过结果烧录时报错折腾了一下午最后发现是J-Link和板子之间的SWD线拖得太长接触不良。这种经历搞嵌入式的朋友多少都遇过——编译、烧录、仿真这三件事看起来是开发流程里最基础的环节但任何一个环节掉链子都够你喝一壶的。今天我就把这套流程从头到尾捋一遍从源码到HEX/BIN文件从下载算法到Flash写入从硬件断点到实时变量观察把每个环节的原理、操作、常见坑全部讲透。不管你用的是STM32、GD32这类Cortex-M内核芯片还是ESP32、AVR、RISC-V核心思路都是通用的只是工具和细节略有差异。这篇文章适合刚接触MCU开发的初学者也适合做了几年项目但一直只点编译下载按钮、没搞明白背后原理的朋友。1. 整体流程拆解1.1 从源码到可执行文件中间到底发生了什么很多新手对编译的理解就是点一下Keil里的Build按钮其实这个动作背后包含了完整的工具链工作链预处理、编译、汇编、链接。这四步每一步都有明确的输入输出。先是预处理展开头文件、处理宏定义、删除注释。然后是编译把C代码翻译成汇编指令这是针对特定架构的比如Cortex-M内核就对应ARM指令集。再往后是汇编把汇编指令转成机器码生成目标文件.o文件。最后是链接把多个目标文件、启动文件、标准库合并在一起分配地址生成最终的ELF或AXF文件。在MCU场景下这个流程和PC端开发最大的区别在于交叉编译和地址分配。交叉编译的意思是你在一台x86的电脑上编译出ARM架构的代码这两者指令集不同所以编译器和链接器都必须指定目标架构。地址分配则是MCU特有的——我们的程序不是存在硬盘上的而是存在芯片内部的Flash里运行的时候CPU从Flash取指数据放到RAM。所以链接器必须知道Flash从哪个地址开始、多大RAM从哪个地址开始、多大。1.2 工具链选型不是只有Keil一种选择Keil MDK是Cortex-M开发最普及的工具STM32玩家几乎人手一个。它的优势在于图形化界面、工程管理简单、调试器集成度高装好Pack包就能干活。但它也有让人头疼的地方付费授权、编译器AC6和AC5之间的差异、工程文件改起来不友好。如果你追求免费开源GCC工具链是更灵活的选择。arm-none-eabi-gcc配合Makefile或者CMake再搭一个OpenOCD做烧录调试整套链路都是免费的。我见过不少团队从Keil迁移到VSCode加GCC环境的迁移成本不高关键是阅读代码和代码审查的体验好很多。IAR则是另一套体系编译优化能力业界公认很强代码密度高但同样是商业软件界面风格也比较复古。选型这块我的建议很直接如果你刚入门、只是想快速把例程跑起来用Keil别纠结如果你想深入理解编译链接过程、想让构建流程可自动化、可持续集成老老实实学GCC加Makefile这套东西走到哪里都通用。1.3 构建系统让编译自动化、可复现手动在IDE里点编译按钮问题是编译选项、宏定义、源文件列表都散落在工程文件里不同人改一版出来了不同的配置非常不透明。所以稍微正规一点的项目都会用构建系统来管理。以CMake为例一个简单的MCU项目可以这样组织cmake_minimum_required(VERSION 3.16) project(blink_demo C ASM) set(CMAKE_TOOLCHAIN_FILE arm-none-eabi-gcc.cmake) add_executable(${PROJECT_NAME} src/main.c src/system_clock.c startup/startup_stm32f103xb.s ) target_link_libraries(${PROJECT_NAME} -T stm32f103xb_flash.ld) target_compile_options(${PROJECT_NAME} PRIVATE -mcpucortex-m3 -mthumb -Os -ffunction-sections -fdata-sections) target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)这套配置的核心价值在于编译参数、链接脚本、源文件清单全部显示化、版本可控。任何人拉下代码执行一条cmake --build命令得到的结果是一模一样的。对于团队协作、持续集成、批量发布固件这个价值非常大。2. 编译环节的核心细节与实操要点2.1 Keil工程配置里容易被忽略的设置项Keil工程配置里坑不少我挑几个关键项说。第一个是芯片型号选择不是随便选的它决定了编译器默认的宏定义、头文件路径、启动文件、链接脚本、烧录算法。型号选错最常见的症状是编译过了但下载时提示Flash算法不匹配或者程序运行起来某些外设地址不对。第二个是C/C选项卡里的Define和Include Paths。Define里常要填一些芯片相关的宏比如STM32F103系列一般要定义USE_STDPERIPH_DRIVER和STM32F10X_MD少了这些宏固件库里很多条件编译的代码就不参与编译程序链接时甚至可能出现Undefined symbol的错误。第三个是优化等级。Debug模式下建议用-O0或者-O1这样生成的调试信息完整变量观察、单步执行都比较直观。Release模式下再开-O2或者-Os追求代码体积和性能。有一个高频坑是Debug模式开了-O2之后明明代码在跑但Watch窗口里变量不变化甚至断点都不停这是因为局部变量被优化进了寄存器部分断点行没有生成对应的指令。第四个是从V5编译器切换到V6编译器即AC5换到AC6。AC6基于Clang语法检查更严格编译速度更快但有些旧代码在AC5下编译通过、AC6下报错比如一些隐式类型转换、未使用的变量。我的经验是换编译器版本之前先在Release和Debug两种模式下各编译一次把所有警告全部处理干净再切换。2.2 启动文件与链接脚本程序能跑起来的地基启动文件startup_xxx.s是所有MCU程序的入口。它做的事情包括初始化栈指针、调用SystemInit配置时钟、复制.data段到RAM、清零.bss段、调用main函数最后设置异常向量表。用Keil新建工程时会自动拷贝对应的启动文件但如果用GCC工具链这个启动文件需要自己找或者自己写很多人第一次用GCC编译MCU程序失败多半就是启动文件缺失或者写得不匹配。链接脚本.ld或者.sct决定了代码和数据放到哪里是MCU程序非常核心的文件。一个典型的STM32F103RB链接脚本内容如下MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (xrw) : ORIGIN 0x20000000, LENGTH 20K }这个文件定义了两个区域Flash从0x08000000开始共128KBRAM从0x20000000开始共20KB。你的程序被链接器分割成代码段、只读数据段、初始化数据段和BSS段分别放到这两个区域里。链接脚本写错了最常见的两个问题是程序烧进去上电直接死机向量表地址不对、栈指针不对或者编译的时候提示region memory overflow芯片容量不够用了。2.3 编译报错实战三类高频错误和排查思路第一类是语法错误和语义错误这类最好解决。Keil下方Output窗口会直接给出错误信息和代码行号双击错误信息可以跳到对应代码位置。这类错误的排查技巧是优先看第一行错误很多时候后面的错误都是第一行错误引发的连锁反应。第二类是链接错误最典型的就是Undefined Symbol。出现这个错误说明某个符号被引用但找不到定义常见原因有三种源文件没有加入工程、对应的静态库没有链接、某个宏定义控制的条件编译把定义屏蔽掉了。排查思路也很清晰——先看是哪个符号再想这个符号应该在哪个文件里定义再用交叉搜索功能全工程搜一遍。第三类是内存溢出错误比如area main.o (code) exceeds limit或者region FLASH overflowed by xxx bytes。这时候打开工程生成的MAP文件文件末尾会有完整的Flash和RAM使用报告各函数占多少空间、哪些变量占了多少栈一目了然。我对这类问题的处理顺序是先砍优化过度的内联函数再看全局大数组能不能挪到Flash最后考虑换更大容量的芯片。还有一个容易被忽视的编译期问题是宏定义里的括号不匹配。这个错误在预处理阶段是不会报的没有语法错误但展开后的表达式完全变了。排查的方法是右键宏名选择Go To Definition看宏展开之后的实际内容尤其是涉及运算符优先级时时间长了养成习惯就好。3. 烧录环节实操与排查技巧3.1 烧录的本质与三种主流下载方式对比烧录的本质是把编译生成的HEX或BIN文件写入芯片内部的Flash存储区同时配置相关的选项字节。与PC程序装到硬盘不同MCU的程序是直接放在Flash里由CPU执行的这个差异决定了烧录过程要有特殊的时序和协议。Cortex-M内核芯片主流的下载方式有三种调试器下载JTAG/SWD、串口ISP下载、USB DFU下载。调试器下载是最常用的一种J-Link、ST-Link、DAP-Link都属于这一类它们通过调试接口和芯片内部的调试模块通信能直接写Flash、也能读写内核寄存器还支持在线调试。串口ISP不走调试接口而是利用出厂固化的Bootloader通过串口接收数据后写入应用Flash区难度低、成本低但速度慢而且Bootloader版本不同支持的协议也有差异。USB DFU是芯片出厂内置的USB引导程序适用于需要免调试器量产烧录的场景。它们之间的核心差别在于调试器能同时做烧录和仿真调试所以开发阶段几乎都靠它串口ISP和USB DFU只能烧录不能调试但在产线批量烧录时有明显的成本优势。ESP32系列用户喜欢用esptool通过串口烧录Arduino用户用一个AVR ISP或者通过引导程序用串口烧录原理也是一样——都利用了芯片内置的引导加载逻辑。3.2 Keil烧录配置的完整步骤与复位选项以STM32F103加ST-Link为例Keil里第一次烧录需要在Options for Target的Debug选项卡选择ST-Link Debugger然后进入Settings配置接口和速度。接口选SWD只占用两根线比JTAG省IO速度在调试阶段不要拉满4MHz以下比较稳妥信号质量差的时候降到1MHz甚至更低能解决很多莫名其妙的问题。Flash Download选项卡同样关键。这里的Download Function建议勾选Erase Sectors和ProgramReset and Run建议勾选。如果只勾选Erase Full Chip每次烧录都会把整个芯片擦一遍速度慢不说还会把选项字节一起擦掉有时候会导致芯片锁死或者启动模式不对。Add Programming Algorithm这里必须选对芯片对应的Flash算法文件选错的话Keil会提示找不到算法或者校验失败。Reset and Run勾上之后烧录结束芯片自动复位运行否则你还要手动断电重新上电调试节奏慢不少。3.3 烧录失败的7大常见原因与排查顺序我在实际开发中总结了一套烧录失败后的排查顺序你按着这个顺序来能少走很多弯路。第一个是连接失败表现为No target connected或者Can not connect to target。先排除硬件问题测量目标板供电是否正常SWD两根线有没有接反有没有虚焊调试器和电脑的USB线是不是只能供电不能传数据。接着排查软件配置调试器类型、目标芯片型号、SWD速度都是检查项。第二个是地址错误。芯片选型对了、也擦除了但烧录时总提示验证失败检查一下Flash算法文件是否匹配以及程序链接的起始地址和芯片Flash的起始地址是否一致。ST芯片用0x08000000有些国产芯片用的是0x00000000映射或者0x20000000的RAM地址下载时如果Keil按照默认0x08000000去写自然校验失败。第三个是读保护锁死。芯片如果之前被设置过读保护RDP调试器就无法正常访问Flash提示会有Internal error或者RDD相关字样。STM32的解决方法是使用ST-Link Utility工具选择Connect under reset、然后执行Full Chip Erase把读保护位擦掉但代价是芯片里的程序也一并没了。第四个是芯片保护位状态异常尤其是用国产Cortex-M芯片时比较常见。这类芯片有些出厂烧录过bootloader或者选项字节不对SWD接口被禁用此时需要进入Boot模式用串口ISP连接后执行全片擦除恢复默认选项字节。第五个是供电不足。调试器通过SWD接口给目标板供电时如果板子上外设电流大电压跌落会导致通信失败。这时候外接一个稳定的3.3V电源测试再烧一次。第六个是复位电路问题。目标板复位引脚接了太大电容或者被别的器件拉死调试器连接时会尝试控制复位脚导致复位时序异常。排查方法是用示波器看复位引脚的波形或者直接断开复位电容再试。第七个是信号线过长和接触不良。SWD信号频率高一点之后对线长和干扰都很敏感我之前那次同事的板子就是杜邦线拖太长信号衰减加上接触不良烧录时好时坏。解决办法是缩短线缆、降低SWD频率、改用屏蔽线或者直接画板时走短走粗。4. 仿真调试环节实操与经验4.1 软件模拟与硬件在线调试如何分工仿真调试分为软件模拟仿真和硬件在线调试两种两者的工作方式差别很大。软件模拟是让Keil或者IAR模拟出一个完整的Cortex-M内核不需要真实硬件就能跑程序寄存器、内存、外设都是在PC端模拟出来的。硬件在线调试则是通过调试器连接真实芯片直接控制CPU的取指、执行、暂停实时读写寄存器和内存。软件模拟适合早期算法验证、没有硬件或者硬件没回来之前先写逻辑。它的缺点是外设行为不真实——模拟器能把GPIO寄存器给你翻转了但没有真实的电平变化延时函数模拟出来的时间也不准确。所以我一般只在做纯算法模块、比如PID参数整定、数据处理协议栈的时候使用软件模拟。硬件在线调试才是开发阶段的主力能看到真实物理状态能设置硬件断点能实时观察外设寄存器。顺带说一句有的项目会用wokwi这类在线仿真平台来跑逻辑验证尤其是Arduino和ESP32项目这类平台把软件仿真做得比较完善对初学者友好可以直接在线编辑代码然后看串口输出。但到了真实产品阶段还是要回到硬件调试器这条路上。4.2 硬件断点、单步、Watch窗口的实战用法硬件在线调试的核心操作是设断点。在Keil里双击代码行号左侧就能设置断点程序运行后会停在断点所在行此时你可以查看变量值、寄存器值、内存数据。单步执行有Step Into、Step Over、Step Out的区别Step Into进入函数内部Step Over跳过当前行直接执行完并停在下一行Step Out跳出当前函数。调试中断服务程序的时候注意函数调用关系优先用Step Over不然一不小心就跳进库函数里面出不来了。Watch窗口是我用得最多的窗口。在Watch窗口里输入变量名就能看到变量当前值。这里有个关键细节局部变量只有当前处于该函数作用域内才能看到全局变量则任何时候都能看。另外在调试优化等级较高的代码时变量因为被优化进寄存器而无法显示解决办法是把变量声明为volatile强制内存读写方便观察。Memory窗口用于查看任意地址的内存内容。调试时经常需要看某一块数据缓冲区的具体内容比如串口接收缓冲区的数据是否接收到位直接在Memory窗口输入buffer_name或者绝对地址比如0x20000000选择字节、半字或字显示方式就能逐字节观察。Peripherals窗口更直观它按照芯片型号把所有的外设寄存器分组展示比如你看GPIOA的ODR寄存器是否翻转、CAN控制器的错误状态寄存器有什么变化不用自己算寄存器地址效率高不少。4.3 调试低功耗和看门狗场景的实战经验低功耗调试是一个深坑。STM32进入Stop模式或Standby模式后内核停止运行调试器会失去连接Keil提示Cannot access target。解决办法是在进入低功耗代码之前给程序加一个延时等待或者使用调试器的唤醒功能——在调试选项里打开Debug in Low Power Mode有些调试器支持在低功耗状态下唤醒内核。看门狗又是一个经典坑。如果代码里开了独立看门狗IWDG或者窗口看门狗WWDG调试时单步执行的速度远远跟不上看门狗的喂狗周期芯片会被看门狗反复复位程序根本停不稳定。解决方案有两个一是调试阶段在初始化代码里把看门狗临时禁用二是在中断服务程序里喂狗但调试时会发现断点经常停在喂狗位置体验还是不好。最省事的做法是调试宏里控制编译比如#if defined(DEBUG) IWDG_Disable(); #else IWDG_Enable(IWDG_PRESCALER_64, 1250); #endif这样调试版本不开启看门狗Release版本才启用互相不干扰。5. 常见问题与排查技巧实录5.1 高频故障速查表我整理了嵌入式MCU开发编译、烧录、仿真三个环节的高频问题做成了一张速查表建议收藏。现象可能原因排查方向Keil编译报Undefined symbol源文件未加入工程、头文件路径缺失检查工程文件列表交叉搜索符号出处编译提示Flash溢出代码量超芯片容量看MAP文件优化代码或者换大容量芯片编译提示RAM溢出全局数组过大、栈空间不足看MAP文件设置栈大小优化大数组下载时No target connected接线错误、供电异常、芯片锁死检查SWD接线、目标板供电、芯片读保护状态烧录后校验失败Flash算法选错、芯片型号不符确认芯片型号核对Download算法选项程序烧录成功但没运行启动文件异常、时钟配置错误、复位脚被拉低检查Reset引脚核对启动文件和SystemInit变量在Watch窗口看不到优化等级过高、变量被优化提升调试对比度声明volatile变量调试时芯片反复复位看门狗未喂狗调试版禁用看门狗或缩短单步间隔串口ISP连接无响应Boot模式未进入、波特率不对确认BOOT引脚电平核对ISP协议和波特率编译AC5通过AC6报错编译器版本不兼容、语法差异优先处理警告禁用GNU扩展切换C标准5.2 一条完整的排查演练编译通过但板子不跑这个场景是最典型的一个案例我拿它做一次完整排查。现象是代码在Keil里编译零错误零警告烧录也没报错但板子没有任何运行迹象LED不闪、串口无数据。第一步检查硬件最小系统——供电、地、晶振、复位这些是MCU工作的基础。用万用表量VDD电压是否为3.3V复位引脚是否在高电平。第二步确认烧录地址正确。打开工程的Options for Target看到芯片型号是STM32F103C8Flash地址从0x08000000开始烧录算法也匹配这一关过了。第三步查启动文件。打开启动文件看栈大小Stack_Size是不是设置了足够空间Reset_Handler里的SystemInit调用是否注释掉了不少教程为了节省Flash把SystemInit注释了导致时钟配置缺失程序跑起来也是错误状态。检查后发现启动文件是正常的。第四步查链接脚本。有些国产芯片虽然兼容STM32F103但Flash和RAM的地址有差异比如Flash在0x08000000而RAM在0x20000000但容量大小不同链接脚本是按默认的C8容量配置的程序链接没问题但烧进去跑了不正常。这一步也要确认。第五步是上调试器看实际执行。直接Keil快捷键进入Debug模式看PC寄存器的值。发现PC停在0x08000000附近但没有往main方向跑说明异常向量表或者启动流程有问题。一步步Step发现卡在SystemInit里的一个等待时钟Ready死循环。最后排查到底发现是外部晶振没有起振——买的板子用的晶振不是8MHz而是一个12MHz的而SystemInit按照8MHz配置倍频参数导致PLL锁不住程序卡死在等待循环。换掉晶振或者修改配置之后程序正常运行。这个案例说明了一个道理编译、烧录、仿真是一条链路哪个环节出问题都可能从另一个环节暴露出来。怀疑哪一步就一步步验证最好是用硬件调试器看PC寄存器实际执行路径而不是对着代码猜。5.3 让流程更顺手的几条工作习惯踩过这么多坑之后我养成了几个固定习惯分享出来。第一新板子第一次烧录前先别急着烧自己的程序先烧一个官方例程或者一个最简单的LED闪烁程序。这样能先确认硬件最小系统和烧录链路是通的再排查自己的程序问题否则两个变量混在一起很难定位。第二每个工程固定维护一个MAP文件检查动作。编译完强制自己看一眼MAP文件的最后几行确认Flash和RAM占用率。占用率超过85%的时候就要警惕了提前规划优化方案别等到加功能时才发现空间不够。第三要掌握一个生成BIN文件的方法。Keil里默认只生成HEX或者AXF但很多批量烧录工具需要BIN格式。在Keil的After Build选项卡里加一行命令fromelf --bin --outputoutput.bin build/project.axf这样每次编译完自动生成BIN文件做产线烧录、做OTA升级测试都用得上。第四学会看反汇编。有些诡异问题在C语言层面完全看不出原因比如变量明明赋值了但结果不对比如结构体对齐导致的问题。进入Debug模式后在Disassembly窗口里看编译器生成的汇编指令能帮你确认编译器到底对你的代码做了什么。这个技能门槛不高但对Cortex-M汇编指令不熟的话可以先从简单的赋值、条件判断入手慢慢积累。第五凡是导出Release固件一定要在Release模式下重新编译一次、烧录一次、跑一次完整测试。Debug模式下正常不代表Release模式没问题尤其在优化开启之后时序敏感代码、中断延迟、浮点计算都可能发生微妙的变化。我在实际项目中还发现一个很管用的做法在代码里加一个版本宏编译时把编译日期和Git Commit号注入固件比如#define FW_VERSION 1.2.3 #define BUILD_TIME __DATE__ __TIME__然后把这两个字符串通过调试串口打印出来或者通过调试器Memory窗口去读。这样无论固件经过多少轮烧录、多少个人手拿到手里就能确认它跑的是哪个版本、哪次提交编译出来的排查现场问题会省很多事。嵌入式MCU的开发流程说到底是工程能力而不是理论能力编译原理、链接原理、烧录协议这些东西理解得越深排查问题的速度越快。我见过不少工程师热衷于调业务逻辑、写各种外设驱动却对编译烧录调试这套基本功不上心结果一次烧录失败就能卡半天。反而是那些能把编译选项、启动文件、链接脚本讲得清清楚楚的人遇到任何开发板都能快速上手因为工具链和芯片变来变去背后的这套设计逻辑从来没变过。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

杰理强制升级工具4.0:蓝牙SoC芯片救砖与固件恢复实战 2026/9/29 4:42:04

杰理强制升级工具4.0:蓝牙SoC芯片救砖与固件恢复实战

先从一次真实的工作经历说起。去年我帮客户做一个蓝牙音箱的返修项目,板子用的是杰理AC6928方案,样机在测试阶段一切正常,结果量产贴片回来后有一批板子在写固件时突然报错,下载到一半提示“芯片无应答”,随后整片板子…

阅读更多 →
DiffSeg:不训练不调参,用Stable Diffusion的自注意力实现零样本分割 2026/9/29 4:42:04

DiffSeg:不训练不调参,用Stable Diffusion的自注意力实现零样本分割

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

阅读更多 →
面阵相机选型全攻略:从分辨率、靶面到帧率的实战指南 2026/9/29 4:41:58

面阵相机选型全攻略:从分辨率、靶面到帧率的实战指南

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

阅读更多 →
高速电路信号完整性分析:从原理到实战避坑指南 2026/9/29 4:41:51

高速电路信号完整性分析:从原理到实战避坑指南

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

阅读更多 →
Win7安装不识别USB 3.0?Intel_USB3_Win7驱动注入与排错指南 2026/9/29 4:41:51

Win7安装不识别USB 3.0?Intel_USB3_Win7驱动注入与排错指南

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

阅读更多 →
DDR协议、时序与PHY实战:MIG配置到AXI带宽优化 2026/9/29 4:41:51

DDR协议、时序与PHY实战:MIG配置到AXI带宽优化

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉