IIR滤波器这个东西我在刚接触FPGA那会儿被它折腾得够呛。明明MATLAB里一个butter函数就能跑出完美曲线搬到FPGA上却要么输出全是零要么波形抖得像心电图。后来踩的坑多了才明白IIR滤波器的FPGA实现难点根本不在滤波器本身而在系数量化、定点运算位宽规划、以及反馈环路的时序收敛这三件事上。这篇内容就把我从零搭一套IIR滤波器、在Vivado里跑通仿真的完整流程拆开讲清楚包括每一步为什么这么做、参数怎么算、哪里最容易翻车。适合已经懂一点Verilog、用过Vivado基本流程、想真正把数字信号处理算法落到FPGA上的朋友也适合正在做课程设计或项目Demo、需要一套可复现方案的人。下面这套流程我实测能跑通从MATLAB系数生成到Vivado行为仿真出波形一条龙。1. 整体设计思路与方案选型拆解1.1 为什么选IIR而不是FIR先想清楚代价很多人一上来就问IIR和FIR哪个好这问题本身没意义得看你的约束条件。FIR的优点是线性相位、绝对稳定、实现简单就是一堆乘加缺点是同样的过渡带指标下阶数高得离谱。举个实际数字一个截止频率0.2π、阻带衰减60dB的低通FIR可能要60阶以上意味着60个乘法器而IIR用巴特沃斯或者椭圆逼近8阶左右就能达到类似效果乘法器数量直接降一个数量级。但IIR的代价也很明确相位非线性对波形保真要求高的场景要谨慎、存在反馈环路导致稳定性问题系数量化误差可能让极点跑出单位圆、反馈路径带来时序收敛压力这是FPGA实现最头疼的点。所以我的选型逻辑是资源紧张、对相位不敏感比如音频均衡、传感器信号平滑、通信中的某些信道均衡、指标要求陡峭选IIR要求线性相位、绝对稳定、开发周期短选FIR。这次的项目场景我设定为一个典型的传感器信号低通平滑采样率50kHz要滤掉2kHz以上的高频噪声通带纹波要求不严。这种场景IIR是更经济的选择。1.2 二阶节级联结构这是IIR落地的标准答案直接型Direct Form I/II实现高阶IIR在FPGA上是个灾难。原因很实在高阶滤波器的极点对系数极其敏感一个8阶直接型滤波器系数量化到16位定点后极点位置可能偏移到让系统直接发散。而且直接型的反馈路径把所有历史输出加权求和组合逻辑延迟大时序根本收敛不了。工程上的标准做法是分解成二阶节Second-Order Section, SOS级联。一个高阶IIR传递函数可以分解成若干个二阶子系统相乘每个二阶节独立实现、独立量化极点敏感性大幅降低。8阶滤波器就是4个二阶节串起来每个二阶节内部只有2个反馈抽头时序压力小得多。我这次用的是4级SOS级联每级结构选Direct Form I而不是Direct Form II。这里解释一下为什么Direct Form II虽然用的延迟单元少省寄存器但它的反馈路径和输入路径共用延迟链中间节点的数据动态范围大定点实现时更容易溢出Direct Form I的延迟单元多一倍但输入侧和输出侧分离每级的中间结果范围可控调试时也更容易逐级观察。FPGA里寄存器不算稀缺资源我宁愿用DF-I换稳定性和可调试性。1.3 定点量化方案这是IIR最容易翻车的地方浮点系数直接搬到FPGA是不现实的除非你用浮点IP核资源和时序代价都大。必须做定点量化而量化位宽的选择直接决定滤波器还能不能用。我的方案是系数用16位有符号定点Q13格式1位符号2位整数13位小数范围约±4精度2^-13≈0.000122。为什么是Q13因为归一化后的IIR系数绝对值一般小于2巴特沃斯低通系数通常小于1留2位整数位足够防止个别系数超范围13位小数位提供的精度对大多数音频/传感器应用够用。如果系数精度不够极点偏移会导致通带响应畸变甚至不稳定这个后面会讲怎么验证。数据通路我用18位有符号定点Q15格式。为什么数据位宽比系数宽因为乘加运算会产生位宽增长而且反馈环路的累加结果需要额外余量防止溢出。18位是Xilinx DSP48E1切片原生支持的乘法位宽25×18用18位数据配16位系数正好塞进一个DSP切片不浪费。注意位宽规划不是拍脑袋后面第3节我会给出具体的位宽增长计算过程这里先记住结论——系数Q13、数据Q15、累加器留足余量。2. 核心细节解析与实操要点2.1 MATLAB侧系数生成别直接用butter的输出第一步是在MATLAB里把滤波器系数算出来。很多人直接[b,a] butter(8, 0.08)就完事了但这样拿到的是高阶直接型系数你需要的是SOS矩阵。正确做法是用tf2sos转换fs 50000; % 采样率 fc 2000; % 截止频率 wn fc/(fs/2); % 归一化截止频率 0.08 [b, a] butter(8, wn); % 8阶巴特沃斯低通 [sos, g] tf2sos(b, a); % 转成二阶节级联sos是一个4×6矩阵4个二阶节每行是[b0 b1 b2 1 a1 a2]。注意g是总的增益因子要乘到某一级的系数上我习惯乘到第一级的b系数上。这里有个关键细节tf2sos默认的配对和排序策略会影响定点实现的效果。MATLAB默认按极点和零点的接近程度配对但定点实现时更关心的是每级的动态范围。我实测下来默认排序对巴特沃斯这种全极点滤波器问题不大但如果你用的是椭圆滤波器有零点建议手动调整SOS顺序把增益小的节放前面避免中间级溢出。生成系数后用freqz(sos)画出理论频响曲线存下来这是后面FPGA仿真结果对比的基准。我一般会把理论曲线的数据点导出成文本方便在Python或Excel里和FPGA输出做叠加对比。2.2 系数量化的验证这一步不能省拿到浮点SOS系数后先别急着写Verilog。必须在MATLAB里做一次定点量化仿真确认量化后的滤波器仍然满足指标。做法是用fi对象或者手动量化q13 2^-13; sos_q round(sos / q13) * q13; % 量化到Q13 % 对比量化前后的频响 [H1, w] freqz(sos, 512, fs); [H2, ~] freqz(sos_q, 512, fs); plot(w, 20*log10(abs(H1)), w, 20*log10(abs(H2)));如果量化后频响曲线和理论曲线在通带内偏差超过0.5dB或者阻带衰减掉了超过3dB说明Q13精度不够得加位宽。我这次8阶巴特沃斯Q13量化后通带最大偏差0.12dB阻带衰减从60dB掉到58.7dB完全可接受。还有一个更狠的验证把量化后的系数代回极点计算确认所有极点仍在单位圆内。用sos2zp把量化后的SOS转回零极点检查每个极点的模是否小于1。巴特沃斯滤波器本身极点离单位圆有距离Q13量化一般不会出问题但如果你做的是窄带或者高Q值的滤波器这一步能救命。2.3 单级二阶节的Verilog结构设计每个二阶节的差分方程是y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]Direct Form I的实现结构是输入侧两个延迟寄存器存x[n-1]、x[n-2]输出侧两个延迟寄存器存y[n-1]、y[n-2]五个乘法并行做然后求和。这里的关键设计决策是乘法用DSP切片还是逻辑资源。16位×18位的乘法综合工具一般会自动推断到DSP48但你要在代码里保证乘法的两个操作数都是带符号的signed否则工具可能推断成无符号乘法结果全错——这个坑我踩过仿真输出一直是零查了两天才发现是漏了signed声明。另一个细节是流水线设计。五个乘法如果在一个时钟周期内全部完成再求和组合逻辑路径太长50MHz以上就跑不动了。我的做法是在乘法输出后插一级寄存器求和树用两级流水先两两相加再合并这样每个二阶节引入3个时钟延迟。4级级联总共12个时钟延迟对50kHz采样率来说完全无所谓采样周期20μs12个100MHz时钟才120ns。实操心得每个二阶节的输出要做一个饱和截断处理而不是简单截位。定点运算中累加结果位宽比输出宽直接截掉低位会引入直流偏置直接截掉高位会溢出翻转产生爆音。正确做法是判断高位是否超出输出范围超了就钳位到最大/最小值。这个饱和逻辑多花几十个LUT但能避免调试时听到莫名其妙的咔哒声。2.4 级联的位宽管理逐级放大还是统一位宽4级级联时有个容易忽略的问题每级输出的位宽怎么定理论上每级乘加后位宽都会增长如果逐级放大到第4级位宽可能到30位以上资源浪费严重。我的做法是每级输出统一截断回18位Q15但截断前做饱和保护。这里要算一笔账单个二阶节的增益直流增益是(b0b1b2)/(1a1a2)巴特沃斯低通的每个SOS节直流增益接近1但中间某些节可能有1.5左右的峰值增益。如果输入信号幅度接近满量程经过增益大于1的节就可能溢出。所以我在MATLAB里预先算好每级的增益如果某级增益大于1就在该级输出后加一个右移相当于除以2的幂来缩放同时记录总的缩放因子最后一级再补回来。这次设计的4级SOS每级增益都在0.8~1.2之间我统一在每级后右移0位即不缩放靠18位的动态范围Q15范围±4扛住实测输入满量程正弦波没有溢出。3. 实操过程与核心环节实现3.1 工程搭建与文件组织Vivado工程我习惯这样组织rtl/放Verilog源码sim/放测试平台matlab/放系数生成和验证脚本doc/放频响对比图。源码文件拆成三个iir_sos_stage.v单个二阶节、iir_cascade.v4级级联顶层、iir_top.v加输入输出接口和时钟域处理。这样拆的好处是单级可以独立仿真验证出问题好定位。器件我选的是常见的Artix-7系列具体型号不重要DSP48E1切片数量够4级×5个乘法20个就行。时钟约束设100MHz实际采样使能50kHz用计数器分频产生sample_en脉冲而不是真的用50kHz时钟——全局时钟走100MHz采样率靠使能控制这是FPGA信号处理的标配做法避免多时钟域带来的时序问题。3.2 单级二阶节的完整代码实现先看单级二阶节的核心代码。系数通过parameter传入方便级联时例化不同参数module iir_sos_stage #( parameter signed [15:0] B0 16sd0, parameter signed [15:0] B1 16sd0, parameter signed [15:0] B2 16sd0, parameter signed [15:0] A1 16sd0, parameter signed [15:0] A2 16sd0 )( input wire clk, input wire rst_n, input wire sample_en, input wire signed [17:0] x_in, output reg signed [17:0] y_out ); reg signed [17:0] x_d1, x_d2; reg signed [17:0] y_d1, y_d2; reg signed [35:0] acc; // 乘法结果寄存器流水线第一级 reg signed [35:0] p_b0, p_b1, p_b2, p_a1, p_a2; reg signed [35:0] s1, s2, s3; reg signed [35:0] sum_all; reg signed [17:0] y_sat; always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_d1 0; x_d2 0; y_d1 0; y_d2 0; p_b0 0; p_b1 0; p_b2 0; p_a1 0; p_a2 0; s1 0; s2 0; s3 0; sum_all 0; y_out 0; end else if (sample_en) begin // 延迟链更新 x_d1 x_in; x_d2 x_d1; y_d1 y_out; y_d2 y_d1; // 五个乘法DSP48推断 p_b0 B0 * x_in; p_b1 B1 * x_d1; p_b2 B2 * x_d2; p_a1 A1 * y_d1; p_a2 A2 * y_d2; // 两级流水求和 s1 p_b0 p_b1; s2 p_b2 - p_a1; s3 -p_a2; sum_all s1 s2 s3; // 饱和截断Q13系数×Q15数据Q28右移13位回Q15 if (sum_all[35:31] ! 5b00000 sum_all[35:31] ! 5b11111) y_sat (sum_all[35]) ? 18sh20000 : 18sh1FFFF; else y_sat sum_all[30:13]; y_out y_sat; end end endmodule这段代码有几个点值得展开说。位宽计算系数是Q1316位数据是Q1518位乘积是Q2834位有效我留了36位余量。五个乘积相加最坏情况位宽再增长3位36位够用。右移13位后回到Q15格式取sum_all[30:13]正好18位。饱和判断看sum_all[35:31]这5位是否全0或全1符号扩展正确不是就钳位。流水线乘法结果先打一拍p_b0等求和分两级s1/s2/s3一拍sum_all一拍加上输出寄存单级延迟4个时钟。4级级联16个时钟延迟在100MHz下160ns对50kHz采样完全无感。反馈路径的时序注意y_d1 y_out用的是上一拍的输出而y_out本身是寄存输出所以反馈路径上有一个寄存器的延迟这保证了反馈环路不会形成组合逻辑环。这是IIR在FPGA上能收敛的关键——反馈必须打拍。3.3 级联顶层与系数例化顶层把4个二阶节串起来系数从MATLAB生成的SOS矩阵里取转成Q13整数iir_sos_stage #( .B0(16sd8192), .B1(16sd16384), .B2(16sd8192), .A1(-16sd14320), .A2(16sd6234) ) u_stage1 ( ... );系数怎么来的以第一级为例MATLAB里sos(1,:)大概是[0.0625 0.125 0.0625 1 -1.748 0.761]乘2^13取整0.0625×81925120.125×81921024……等等我这里写的8192是示意实际数值要根据你的滤波器算。关键提醒a1、a2在差分方程里是减号但SOS矩阵里存的是a1、a2分母是1 a1·z^-1 a2·z^-2所以代码里乘法后要取负。我在代码里用p_a1 A1 * y_d1然后求和时s2 p_b2 - p_a1等价于减去a1项这个符号千万别搞反搞反了滤波器直接变振荡器。3.4 测试平台与仿真验证Testbench的写法直接决定你能不能看出问题。我的做法是用Verilog的$readmemh从一个文本文件读入激励数据这个文本文件由MATLAB生成——里面是低频信号高频噪声的混合波形定点Q15格式。同时MATLAB把理论滤波输出也算好存成另一个文件仿真时把FPGA输出和理论输出都打印出来对比。激励生成脚本核心逻辑t (0:N-1)/fs; sig 0.5*sin(2*pi*500*t) 0.3*sin(2*pi*8000*t); % 500Hz有用8kHz噪声 sig_q round(sig * 2^15); % Q15量化 fid fopen(stimulus.txt,w); fprintf(fid, %04x\n, typecast(int16(sig_q),uint16));注意这里有个坑负数的十六进制表示。typecast转成uint16再打印读入时用$readmemh读进reg signed [15:0]这样负数能正确还原。我最早直接fprintf(%d)打十进制读进来全是错的。仿真跑起来后重点看三个东西输出是否收敛不发散、不振荡、通带内幅度是否正确500Hz分量幅度应接近0.5、阻带是否被压制8kHz分量应衰减到很小。我一般会把仿真输出导出到文本用Python做FFT对比理论频响这样比肉眼看波形靠谱得多。3.5 资源与时序的实测结果综合实现后看报告4级SOS共20个乘法器全部推断到DSP48E1用了20个DSP切片LUT用了约1200个主要是饱和逻辑和求和树寄存器约800个。时序方面100MHz时钟下WNS最差负裕量为正反馈路径没有成为关键路径——这得益于反馈打拍的设计。如果时序不收敛第一招是给乘法再加一级流水第二招是把求和树拆得更细。4. 常见问题与排查技巧实录4.1 输出全零或全噪声先查符号和位宽这是最高频的问题。输出恒为零九成是乘法操作数没声明signed综合成了无符号乘法负数系数被当成大正数结果荒谬。输出全是噪声或发散先查反馈系数的符号有没有搞反前面说过再查饱和逻辑是不是写错了导致截位错误。我整理了一个速查表按现象对因排查现象最可能原因排查动作输出恒为0乘法无符号化 / 复位未释放检查signed声明、复位极性输出发散振荡反馈系数符号反 / 极点出单位圆核对a1/a2符号、MATLAB验证极点模输出幅度极小系数量化精度不足 / 右移过多检查Q格式、右移位数输出有直流偏置截位而非饱和 / 舍入方式改用饱和四舍五入通带内有纹波畸变某级溢出 / 级间缩放不当逐级观察中间节点波形时序不收敛反馈路径组合逻辑过长反馈打拍、乘法加流水4.2 逐级探针定位问题在哪一级4级级联出问题时最有效的办法是在仿真里把每一级的输出都拉出来看。我在顶层给每级输出都加了(* mark_debug true *)属性或者直接在testbench里层次化引用u_stage1.y_out打印。正常情况下信号经过每一级高频分量应该逐级减弱如果某一级输出突然爆掉或者变成零问题就锁定在那级。这个技巧帮我省了无数次盲目改代码的时间。4.3 量化误差的隐蔽影响有一种问题很隐蔽滤波器能跑波形看着也对但信噪比就是上不去。这往往是舍入方式的问题。直接截位truncation会引入负向直流偏置长期累积影响低频性能。改成加0.5再截位的舍入rounding或者用 convergent rounding能改善几个dB。代价是多几个LUT我觉得值。另外如果发现低频段有缓慢漂移检查一下是不是某级增益略大于1导致极低频累积放大可以在该级后加一个极小的泄漏因子乘以0.999来稳住但这会轻微改变频响要重新验证。4.4 从仿真到上板的落差行为仿真通过不代表上板就对。上板后如果发现输出有周期性毛刺先查采样使能的生成逻辑——sample_en如果是组合逻辑产生的可能有毛刺导致误触发改成寄存器输出。如果输出整体偏移或幅度不对查ADC/DAC接口的定点格式是否和内部Q15对齐接口处的移位有没有搞错。我一般会在上板前先用ILA集成逻辑分析仪抓内部节点确认数据流和仿真一致再去看模拟输出。5. 我踩过的坑与几条实在建议说几个文档里不会写、但实际做的时候一定会遇到的点。第一别迷信MATLAB的默认SOS排序尤其是高阶或者带零点的滤波器手动调顺序能省掉很多溢出调试。第二系数位宽宁宽勿窄Q13不够就上Q15DSP切片支持18位系数多两位精度对稳定性帮助很大资源代价几乎为零。第三反馈打拍是铁律任何试图在反馈路径上省寄存器的做法都会在时序收敛时加倍还回来。第四仿真激励一定要包含极端情况——满量程阶跃、直流、接近截止频率的正弦这些才是暴露溢出和稳定性问题的场景光跑一个普通正弦看不出毛病。这套流程我从系数生成到仿真验证完整跑下来一个下午能搭完调试时间主要花在量化验证和逐级排查上。IIR滤波器的FPGA实现说到底是个精度管理的活——系数精度、数据位宽、累加余量、舍入方式每一处都得算清楚算清楚了它就老老实实工作算漏一处它就给你脸色看。后面如果要做多通道或者更高阶思路是一样的无非是SOS级数增加、资源翻倍把单级验证透了级联就是复制粘贴的事。
企业数字化 ERP 产品动态
相关推荐
Marp CLI:用Markdown高效制作技术演示文稿 1. Marp CLI 是什么? Marp CLI 是一个基于命令行的 Markdown 幻灯片转换工具,它能够将简单的 Markdown 文件转换为各种格式的演示文稿。作为开发者,我经常需要在技术分享、产品演示等场合使用幻灯片,而 Marp CLI 完美解决了传统幻… · 2026/11/2 14:48:05
MATLAB风光水火储协调调度双层优化源程序解析与IEEE30算例复现 简介:本资源为电力系统优化调度方向的毕业设计配套源程序,面向电气工程、能源动力等专业的高年级本科生与研究生,针对风电、光伏大规模并网后系统调峰能力不足的问题,提供一套可复现的风光水火储多能系统互补协调优化调度方案。压… · 2026/11/2 14:48:05
PHPlivechat修复版部署实战:PHP 7.4兼容、无限坐席与APP客服端联调 简介:这是一套面向中小企业与个人站长的PHP在线客服系统源码,基于PHPlivechat构建,主打无限坐席与手机APP客服端,适合需要自建客服渠道、替代第三方付费工具的技术人员部署使用。压缩包共451个文件,约133.83MB… · 2026/11/2 14:48:05
关键信息基础设施全栈安全防护架构解析 1. 项目背景与核心价值这份73页的PPT方案实际上是一套针对关键信息基础设施保护的完整技术架构设计。在数字化转型浪潮下,能源、交通、金融等行业的核心系统正面临前所未有的安全挑战。去年某大型制造企业因工控系统漏洞导致全线停产的事故,让行业真正意… · 2026/11/3 3:52:38
液氧甲烷火箭任务控制系统的分布式架构与关键技术 1. 项目背景与核心挑战当SpaceX用不锈钢材料颠覆了传统航天器的设计理念时,整个行业都意识到:低成本、高可靠性的重复使用运载器时代已经到来。两级液氧甲烷不锈钢火箭正是这一理念的典型代表,而它的任务控制中心(MCC)… · 2026/11/3 3:52:38
虚幻引擎打包全流程:从Development到Shipping的实战指南 1. 为什么“打包”这件事值得单独拎出来讲刚接触虚幻引擎那阵子,我一度以为把场景搭好、蓝图连好、角色跑起来,这个项目就算“做完了”。直到第一次要把东西发给别人看,才发现自己连一个能双击运行的成品都拿不出来。编辑器里跑得再流畅&… · 2026/11/3 3:52:38
本地大模型部署选型:Ollama、vLLM与Hugging Face 简介:《本地大模型部署利器——Ollama工具解析及应用》是一份面向NLP开发者、运维人员及本地化部署爱好者的PDF技术文档,聚焦数据隐私、云端响应延迟与部署门槛等痛点,系统梳理开源工具Ollama的定位与用法。文档从安装配置、模型管理、自定义… · 2026/11/3 3:52:38
Camera MetaData实战指南:从读取、写入到批量处理避坑 1. 从一张“颜色不对”的照片说起前阵子帮朋友处理一批手机拍摄的素材,同一台设备、同一个场景,导进电脑之后有几张照片明显偏绿,另外几张又偏暖,白平衡像是各拍各的。一开始怀疑是后期软件的色彩管理出了问题,折腾了半… · 2026/11/3 3:52:38
2024年TensorFlow实战指南:从Keras到部署,对比PyTorch 打开PyPI的下载统计页,TensorFlow依然是整个深度学习生态里下载量最大的框架之一。但在很多新手群里,一提TensorFlow总有人觉得这是“老框架”,好像已经被PyTorch全面超越了。我自己的感觉是,这种印象有对的部分,也有很… · 2026/11/3 3:51:38
双极步进电机控制实战: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/2 4:42:45
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/2 5:17:18
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/2 4:42:45
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/2 5:17:18