简介面向电子科学与技术、FPGA入门学习者与课程设计备课教师这份资料围绕Verilog HDL数字频率计展开重点解决10Hz10MHz信号测量、六位数码管显示与闸门时间切换的实现问题。内容包含分频器、门控电路、计数器、锁存译码显示及顶层电路的代码设计与仿真并给出ISE软件实现流程、ELB实验板验证、技术指标说明和误差分析读者可据此对照1S、0.1S、0.01S闸门选择、复位、溢出标志、闸门标志与记忆显示等环节快速理解模块划分和调试思路也可作为实验报告撰写与代码复现的参考。压缩包共1个doc文件约412KB目录与章节较完整已有707人学习适合需要系统梳理数字频率计设计流程的读者。1. 频率计的精度不由计数器决定闸门关上的那一刻数才成立一个反直觉的事实是在一台基于 FPGA 的数字频率计里最不容易出错的部分恰恰是计数器。10MHz 的信号在 1 秒闸门时间里要被数一千万次六位 BCD 逢十进位逻辑简单到几乎不可能算错。真正决定读数对不对的是另外两件事——闸门开了多久以及关闸之后那串数字怎么被搬进锁存器、再送上数码管。这份 Verilog 数字频率计的实现把这两件事拆成了四个模块时基分频给出基准门控电路排出开闸、关闸、锁存、清零的次序计数器只管在窗口内累加显示模块负责把结果稳住。搞懂后两者的时序关系比背下整份代码更有价值因为所有仿真对了、上板不对的翻车几乎都出在这条时序链上。2. 时基分频四个计数器怎么切出 1Hz、10Hz、100Hz、1kHz 四路闸门闸门时间不是随手定的它是整个测量的标尺。1s 闸门意味着计数值直接以 Hz 为单位0.1s 闸门意味着每个计数代表 10Hz。档位一变同一个计数值的物理含义就变了。所以四路时基之间必须严格成比例而不是各自凑一个差不多的频率。这一章先把分频模块拆开看清 10 倍关系是怎么落到代码里的。2.1 翻转法分频阈值一到就取反而不是让计数器自然溢出分频模块用的是最朴素的数满翻转一个 30 位计数器从 0 数到某个阈值到点清零并把输出取反。取反这个动作让输出成为方波占空比恒为 50%周期等于阈值乘以 2 再除以时钟频率。module count(rest, clk, clk_1hz, clk_10hz, clk_100hz, clk_1khz); input clk, rest; output reg clk_1hz, clk_10hz, clk_100hz, clk_1khz; reg [29:0] count1, count2, count3, count4; always (posedge clk) begin if (!rest) begin // 低电平复位四路计数器与输出一起归零 count1 0; count2 0; count3 0; count4 0; clk_1hz 0; clk_10hz 0; clk_100hz 0; clk_1khz 0; end else begin if (count1 24000000) begin // 数满阈值清零并翻转 count1 0; clk_1hz ~clk_1hz; end else count1 count1 1; if (count2 2400000) begin count2 0; clk_10hz ~clk_10hz; end else count2 count2 1; if (count3 240000) begin count3 0; clk_100hz ~clk_100hz; end else count3 count3 1; if (count4 24000) begin count4 0; clk_1khz ~clk_1khz; end else count4 count4 1; end end endmodule四个阈值 24000000、2400000、240000、24000 是严格 10 倍递减的这并非巧合。按翻转法的关系式f_out f_clk / (2N)四路输出的频率比完全由阈值比决定只要阈值差 10 倍输出就严格差 10 倍与时钟频率是多少无关。这意味着即使板载晶振换了型号只要改四个阈值四路时基的比例关系依然成立——这是这段代码比每位单独算频率更稳的地方。需要留意的是阈值的绝对值。f_out f_clk / (2N)若时钟是 48MHzN24000000 给出的正好是 1Hz若时钟是 24MHz同样这个 N 给出的是 0.5Hz整机读数会差一倍。读这段代码的第一个动作应该是先确认时钟源频率再倒推阈值而不是默认写着 1hz 就是 1Hz。2.2 四路时基的相位关系决定了档位切换时会不会出窄脉冲四个计数器虽然共用同一个clk和同一个复位但它们是各自独立的计数实体彼此的相位没有约束。上电复位后四路同时从 0 开始比例关系严格运行中如果单独复位了某一路或者四路复位释放的时刻有细微差别比如复位信号是异步按键相位就会漂移。相位漂移本身不影响测量精度因为门控电路只关心 gate 的上升沿间隔不关心它相对于其他路的相位。但它会影响档位切换的瞬间按键从 1s 档切到 0.1s 档时gate从一个正在翻转的方波直接切换到另一个方波切换点可能落在新 gate 的高电平中间于是产生一个宽度不足半个周期的窄脉冲。这个窄脉冲会触发门控状态机的posedge gate让count多走一步整个开闸—锁存—清零的节拍就乱了。常见的处理思路是在按键切换时把状态机的count强制清零并等一个完整的 gate 周期再启动或者给 gate 加一个由clk_1khz采样的同步使能切换只在 1kHz 的节拍点上生效。这份实现选择了前者之外的另一条路——按键直接改gate的来源靠ff编码在显示端做补偿代价是切换瞬间可能有一帧异常读数但因为锁存器保留了上一次结果肉眼基本看不出来。2.3 用分频输出当时钟省事但踩在衍生时钟的坑上仔细看门控模块的敏感列表always (posedge clk_1khz)。也就是说clk_1hz、clk_100hz、clk_1khz这些分频出来的信号是被当作时钟用的而不是当作数据。这在功能仿真里完全没问题波形干净、逻辑正确但落到真实器件上就是另一回事。FPGA 里的时钟资源是分等级的专用的全局时钟树偏斜小、扇出大普通布线资源则偏斜大、延迟不可控。由逻辑单元产生的分频信号默认走普通布线当它驱动多个触发器的时钟端时不同触发器看到的边沿时刻可能差几个纳秒。对于 1kHz 这种慢时钟几纳秒的偏斜无关痛痒但如果某个下游模块也用这个信号做数据采样就可能在时序分析里报出建立时间违例而综合工具未必会把它当作时钟来约束于是违例被静默忽略。更稳妥的写法是把分频改成时钟使能全系统统一用主时钟分频计数器产生一个单周期宽的tick脉冲下游写if (tick) begin ... end。这样只有一路真实时钟时序分析能覆盖全部路径也不会产生衍生时钟。代价是多写几行使能逻辑以及所有下游模块都要改成同步使能风格。这份实现选了前者因为它更贴近一个模块一个时钟的直觉也更容易在课程设计的周期内调通。3. 门控状态机一个 count 变量怎么排出开闸、关闸、锁存、清零四拍如果只允许保留一段代码来解释这台频率计为什么能工作我会选门控模块。它用一个 4 位计数器count把闸门开多久、什么时候锁存、什么时候清零三件事编排成一条严格的串行时序。所有关于读数为什么会跳、为什么会是 0的疑问答案基本都能在这里找到。3.1 posedge gate 触发的状态机与非阻塞赋值带来的一拍偏差先看核心片段。always (posedge gate) begin count count 1; // 每个 gate 上升沿推进一步 if (count 9) begin // 注意这里判断的是自增前的旧值 gate_out1 1; clk_clear 0; clk_latch 0; end else if (count 11) begin gate_out1 0; clk_clear 0; clk_latch 1; // 锁存 end else if (count 13) begin gate_out1 0; clk_clear 1; clk_latch 0; // 清零并回卷 count 0; end else begin gate_out1 0; clk_clear 0; clk_latch 0; end end这段代码最容易被读错的地方是count count 1和紧随其后的if (count 9)之间的关系。Verilog 的非阻塞赋值把更新推迟到当前时间步结束if判断用的仍然是自增前的旧值。所以第一个posedge gate到来时旧count是 0条件成立gate_out1立刻置 1此后每个上升沿旧值依次是 1、2……9都满足9闸门一直开着。直到第十一个上升沿旧值是 10条件不成立gate_out1才归 0。数一数就清楚了闸门从第一个 gate 上升沿打开到第十一个上升沿关闭中间跨过整整 10 个 gate 周期。如果 gate 是 10Hz周期 0.1s闸门时间正好 1s与 1s 档的标称值吻合。这个旧值判断的细节如果理解错会误以为闸门只开了 9 个周期进而怀疑整机读数偏小 10%——这是一个典型的读代码读出来的假 bug。后面两拍同理。count旧值为 11 时置clk_latch此时闸门已经关了整整一个 gate 周期计数器早已停止累加数据是稳定的。count旧值为 13 时置clk_clear并把count回卷到 0清零脉冲持续一个 gate 周期后自动撤销状态机回到初始态等待下一个 gate 上升沿重新开闸。整个循环共 14 个 gate 周期其中 10 拍测量、4 拍用于搬运数据。3.2 锁存和清零为什么必须错开而不能同时来假设把clk_latch和clk_clear放在同一拍产生会发生什么锁存器的敏感列表是posedge clk_latch清零信号则直接作用在计数器的同步复位端。两者同时有效时锁存器采到的到底是清零前的旧值还是清零后的新值取决于两条路径的延迟差——这是一个典型的竞争。更糟的情况是清零先于锁存生效。计数器被清成 0锁存器随后把 0 搬进显示寄存器数码管上就出现一帧全零。虽然下一轮测量会把它覆盖掉但如果闸门时间较长比如 1s 档这一帧全零会明晃晃地闪一下观感很差。这份实现用两个 gate 周期的间隔把两者彻底分开第 11 拍锁存第 13 拍清零。中间的 12 拍是一个空拍什么都不做纯粹用来隔开。这种宁可多等一拍也不让信号重叠的做法在时序设计里很常见代价是每次测量多花 20% 的时间在搬数据上。对 1s 档来说多花 0.4s 意味着每秒只能出 0.7 次读数刷新率下降到肉眼可感知的程度对 0.01s 档则完全无感因为 0.4 个 gate 周期只有 4ms。这也解释了为什么实测表里 1s 档的数据要等一会儿才稳定——不是电路慢是状态机主动留了余量。3.3 异步的 start 进入 clk_in 域亚稳态藏在哪门控模块和计数器模块之间只传一个信号gate_out1接到计数器的start端。问题在于两者的时钟域不同——gate_out1是posedge gate产生的gate可能是 10Hz、100Hz 或 1kHz而计数器工作在posedge clk_inclk_in是被测信号与 gate 毫无关系。于是start对计数器来说是一个纯异步输入。当start的跳变恰好落在clk_in上升沿的建立保持窗口内cnt1的使能端会进入亚稳态输出既不是 0 也不是 1而是在中间电平徘徊一段时间后才随机落到某一侧。这段时间里计数器可能多加一个数也可能少加一个数表现为读数偶尔跳变一两个 LSB最低有效位。在 10Hz 的低频输入下这个概率极低因为被测信号的周期是 100ms而建立窗口只有纳秒级撞上的机会微乎其微。但在 10MHz 输入下每 100ns 就有一个采样沿闸门打开的瞬间撞上窗口的概率就不可忽略了。实测表里 9MHz 附近出现的 899.995、919.994 这类偏差除了闸门时间的量化误差亚稳态也是一个可能的贡献项。标准的处理是两级同步用clk_in对start打两拍取第二拍的输出作为计数使能。两级触发器把亚稳态的逃逸时间压缩到一个时钟周期内让后级逻辑看到的永远是稳定电平。这份实现没有做这一步因为它在功能上能用而课程设计的验收标准是读数大致正确不是每一位都稳如磐石。真正做产品时这两级触发器是省不掉的。4. 计数与显示链路六位 BCD 怎么变成数码管上不跳变的读数计数器模块负责在闸门开启的窗口内累加输出六组 4 位 BCD 码显示模块负责把这六组码锁存、译码、扫描到六只数码管上。两者之间隔着一个锁存器这个锁存器看似多余实则是记忆显示这个技术指标的全部实现。4.1 手工展开的 BCD 级联进位边界条件最容易写错先看计数器的进位结构。六个 4 位寄存器cnt1到cnt6cnt1是个位cnt6是最高位。每来一个clk_in上升沿从个位开始判断本位是 9 就清零并向高位进位否则加 1。// 低五位全 9最高位加 1低五位归零 else if ((cnt54b1001)(cnt44b1001)(cnt34b1001) (cnt24b1001)(cnt14b1001)) begin cnt1 4b0000; cnt2 4b0000; cnt3 4b0000; cnt4 4b0000; cnt5 4b0001; // 这里应该是 0000 cnt6 cnt6 4b0001; end逐条if-else展开的写法好处是每一级的进位条件都写得明明白白一眼能看出哪些位满了才轮到这一位加 1。坏处也很明显六个分支要写六遍每一位的置零和进位都要手写任何一处笔误都不会被综合工具报错只会在跑到那个特定数值时暴露。上面这段就有一处。低五位全 9 时正确的动作是把低五位全部清零、最高位加 1也就是 099999 变成 100000。但代码里cnt5被置成了0001而不是0000实际跳变是 099999 → 110000中间整整跳过了 10000 个数。这个错误在仿真波形里极难发现因为要跑满十万个计数周期才会经过一次只有把闸门时间设到足够长、输入频率足够高让计数器冲过 99999 这个点读数才会突然跳变。更可靠的写法是把进位条件提成线网让每一位的规则完全一致避免手写出错wire c1 (cnt1 4d9); wire c2 c1 (cnt2 4d9); wire c3 c2 (cnt3 4d9); wire c4 c3 (cnt4 4d9); wire c5 c4 (cnt5 4d9); wire c6 c5 (cnt6 4d9); always (posedge clk_in) begin if (!rest || clk_clear) begin {cnt6,cnt5,cnt4,cnt3,cnt2,cnt1} 24d0; yichu 1b1; end else if (start) begin if (c6) begin // 六位全满溢出整体归零并拉低溢出标志 {cnt6,cnt5,cnt4,cnt3,cnt2,cnt1} 24d0; yichu 1b0; end else begin cnt1 c1 ? 4d0 : cnt1 4d1; cnt2 c2 ? 4d0 : (c1 ? cnt2 4d1 : cnt2); cnt3 c3 ? 4d0 : (c2 ? cnt3 4d1 : cnt3); cnt4 c4 ? 4d0 : (c3 ? cnt4 4d1 : cnt4); cnt5 c5 ? 4d0 : (c4 ? cnt5 4d1 : cnt5); cnt6 c6 ? 4d0 : (c5 ? cnt6 4d1 : cnt6); end end end六个wire构成一条进位链每一位的更新规则统一成本位满 9 则清零所有低位满 9 则加 1。规则一致意味着不会出现某一位写错的情况而且新增位数时只需要多写一行扩展性也好得多。yichu的极性值得单独说一句。它的初值是 1溢出时被置成 0说明这是低有效信号最终驱动 LED 时靠灌电流点亮。这跟七段码的低有效是同一套逻辑整个设计里凡是驱动 LED 或数码管的信号基本都是低有效读代码时如果按高有效去理解会得出上电就全亮的错误结论。4.2 锁存器为什么显示值必须记住上一次计数器在每个闸门周期结束时被清零准备下一次测量。如果数码管直接接计数器的输出那么在清零的那几拍里显示会瞬间变成 000000然后下一轮又从 0 开始往上跳——用户看到的是一串疯狂闪烁的数字根本读不出稳定值。锁存器就是为这个场景存在的。它的敏感列表是posedge clk_latch只在门控状态机发出的锁存脉冲到来时才把in1到in6搬进latch1到latch6。always (posedge clk_latch) begin latch1 in1; latch2 in2; latch3 in3; latch4 in4; latch5 in5; latch6 in6; end锁存之后即使计数器被清零、重新开始累加latch里的值也纹丝不动数码管持续显示上一次的完整测量结果直到下一个锁存脉冲到来才更新。这就是技术指标里记忆显示的全部含义——显示的永远是上一次测量结束时的快照而不是当前计数器的瞬时值。从时序上看锁存脉冲落在闸门关闭后的第一个 gate 周期此时计数器已经停止累加至少一个周期所有进位链都稳定了。这个间隔不能省BCD 进位链是串行的个位从 9 变 0 会触发十位加 1十位从 9 变 0 又触发百位最坏情况下要经过六级门延迟才能全部稳定。如果锁存脉冲紧贴闸门关闭沿很可能锁到进位传到一半的中间值显示出一个不存在的数字。4.3 1kHz 动态扫描与共阳极段码的位序六只数码管如果同时驱动需要 6×742 根段选线用动态扫描只需要 7 根段选加 6 根位选共 13 根。扫描的原理是同一时刻只点亮一只数码管靠人眼视觉暂留把六只合成一个整体。扫描频率必须足够高单只数码管的刷新率低于 50Hz 就会看出闪烁。always (posedge clk_1khz) begin count count 1; // 3 位计数器0~7 循环 end always (posedge clk_1khz) begin case (count) 0: begin sign_out4 latch1; se count; end // 个位 1: begin sign_out4 latch2; se count; end 2: begin sign_out4 latch3; se count; end 3: begin sign_out4 latch4; se count; end 4: begin sign_out4 latch5; se count; end 5: begin sign_out4 latch6; se count; end // 十万位 default: sign_out4 4b1111; endcase endclk_1khz每次上升沿让count加 1count的 0 到 5 依次选通六只数码管6 和 7 是空拍。这样每只数码管每秒被点亮约 166 次远高于视觉暂留的阈值看起来是连续点亮的。空拍的存在让位选信号在切换时有一段全灭的死区避免上一只的段码串到下一只上形成重影。段码译码用的是共阳极编码0 电平点亮数字segs[7:1] (a b c d e f g)点亮的段00000001a b c d e f11001111b c20010010a b d e g30000110a b c d g41001100b c f g50100100a c d f g6本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
TC264嵌入式视觉系统:裸机AI车道识别与实时PID控制 1. 项目概述:这不是玩具车,是嵌入式视觉系统的移动实验室“第21届智能车 人工智能视觉组之逐飞演示车模浅析”——光看标题,很多人第一反应是“哦,大学生竞赛的遥控小车”。但如果你拆开这台车模的底盘、拔掉摄像头排线、翻看它主… · 2026/11/3 21:29:40
AI工程从零开始:手写GPT到RAG与量化部署的完整路线 你拿到一个项目名,叫“ai-engineering-from-scratch”。先别急着当成又一门“三个月精通大模型”的网课标题,我刚看到这串词的时候,第一反应是:终于有人把“从零开始”和“工程”这两个词放在一起了。太多人学AI是从论文和公式开始… · 2026/11/3 21:29:40
什么是后见之明?从认知偏差到五步复盘法的实践指南 我一直觉得英文里有些词特别有意思,看起来平平无奇,但放到真实生活里一琢磨,后劲很大。hindsight就是其中一个。词典给的解释是“后见之明”,听起来像是个不痛不痒的抽象名词,但只要你稍微回忆一下自己过去半年做过的决… · 2026/11/3 21:29:40
GPT your dot:用大模型重构开发者配置管理 1. 这句“GPT your dot!”到底在喊什么?你刷到过这句话吗?——“GPT your dot!都给我去用!!”不是拼写错误,不是中英混搭的尴尬现场,更不是某个小众编程语言的新语法。它是一句带着强… · 2026/11/3 22:16:08
后见之明不是马后炮:一套让复盘产生行动力的实战方法论 “hindsight”这个词,放在中文语境里最常被翻译成“后见之明”,但真到了工作场景中聊起来,绝大多数人听到它的第一反应是:马后炮。我第一次意识到这个理解偏差,是被一次线上事故的复盘给狠狠启蒙的。那次复盘开了整整两… · 2026/11/3 22:16:08
SpringBoot+LBS开发城市路线分享系统实践 1. 项目概述这个基于SpringBoot的位置服务城市路线分享系统小程序,是一个典型的LBS(基于位置服务)应用开发案例。作为毕业设计选题,它融合了后端开发、移动端开发和地理信息处理三大技术方向,具有很高的实践价值和商业… · 2026/11/3 22:16:08
SAP MRP运行参数配置与优化实战指南 1. SAP MRP运行的核心控制参数解析作为SAP系统中最核心的生产计划功能,MD01事务码的MRP运行效果直接决定了企业物料供应的准确性与及时性。在实际操作中,我发现很多计划员只关注基础物料主数据,却忽略了MD01界面那些看似简单的参数选项。这些… · 2026/11/3 22:16:08
buzz:基于Python与Markdown的极简开源博客系统 1. 项目概述:buzz到底是什么,为什么值得一试如果你平时喜欢折腾自托管、写博客或者建个人知识库,大概早就注意到,“buzz”这个关键词最近在各个技术社区里被频繁提起。单纯看名字,你可能会以为它是个实时消息推送工具或… · 2026/11/3 22:16:08
hindsight:基于Python+Docker的可观测性因果推理系统 1. 项目概述:hindsight 不是“事后诸葛亮”,而是一套可落地的工程化复盘系统“hindsight”这个词在日常语境里常被译作“后见之明”,带点调侃意味——事情办砸了,才恍然大悟“早该这么干”。但在我过去八年做技术基建、AI工具链和… · 2026/11/3 22:15:07
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46