
简介正弦脉宽调制SPWM的FPGA实现是电力电子与数字逻辑设计结合的典型应用。压缩包提供了一套基于ALTERA FPGA、使用Verilog硬件描述语言通过查表法生成三电平SPWM的完整工程面向FPGA学习者和逆变器开发人员适合数字信号处理与电力电子交叉领域。压缩包共476个文件大小仅4.22MB除了核心的.v源码还包含大量Quartus工程文件如.qpf、.qsf、.sof、.pof以及.vwf仿真波形、.hex正弦查找表、.bsf符号文件和readme说明能直接打开工程查看综合、仿真与下载流程。内容预览显示设计划分为Sinx、PWM、Main、div、Communication等子模块分别承担正弦查表、脉宽输出、顶层控制、分频和通信功能通过计数器、比较器和状态机实现频率控制、角度计算、脉冲生成及同步逻辑对理解三电平SPWM的查表法高效实现很有帮助。已有361人学习下载读者可借鉴其模块划分、工程组织方式以及从查找表到实际波形的完整设计思路。1. 从查表法说起为什么三电平 SPWM 值得在 ALTERA FPGA 上复现做逆变器或者电机驱动的人应该都遇到过这样的困扰用 MCU 的定时器输出 SPWM占用了大量中断资源载波频率稍微提上去CPU 就忙着进中断了几乎没有余量去做闭环算法。我在一个异步电机交流调速项目里也踩过这个坑后来把 SPWM 生成逻辑整体下沉到 FPGA 里用 Verilog 实现查表法三电平 SPWM整个控制周期瞬间轻松了。这套东西的核心思路并不神秘预先算好正弦波样本存在 ROM 里运行时用计数器寻址查表再结合比较器产生带死区控制的 PWM 脉冲。真正有价值的是「三电平」带来的波形质量和效率提升以及在 ALTERA 器件上落地时的工程细节。对于正在学 Verilog 的 FPGA 初学者或者想把 SPWM 从 MCU 挪到硬件逻辑里的嵌入式工程师这套设计是一个相当完整的数字系统训练样本。2. 三电平 SPWM 的调制原理与查找表设计2.1 两电平与三电平的本质差异传统两电平 SPWM 的输出电压只有正负两个电平逆变桥同一桥臂上下管交替导通输出波形是正负交替的等幅脉冲序列调制深度只能靠脉宽变化来体现。三电平 SPWM 则多了一个零电平状态桥臂可以输出正、零、负三种电平波形的台阶数翻倍等效开关频率在相同器件开关频率下提高了一倍谐波含量明显下降输出滤波器的体积和成本都能降下来。从频谱角度看三电平输出在相同载波比下的最低次谐波频率更高意味着截止频率可以设计得更大电机的电流纹波更小、噪音更低。从开关损耗看每次电平切换的电压摆幅只有两电平的一半即使开关次数增加总损耗也不会恶化太多。这些特性让它成为中高压变频器和并网逆变器的常见选择。2.2 查找表为什么不直接用 CORDICCORDIC 算法适合在线计算任意角度的正弦值但它是一个迭代过程在 FPGA 里要么用流水线展开要么用状态机串行迭代都会消耗额外的寄存器和组合逻辑。对 SPWM 这种周期性、固定频率输出的场景正弦值是可以预先确定的一串固定序列查表法只需一个 ROM 加一个地址计数器资源消耗极低时序也容易收敛。还有一个工程上的原因查表法天然带相位精度上限表的深度决定了角度分辨率。比如表深度 1024 对应 360 度内采样 1024 个点角度分辨率约 0.35 度对绝大多数 SPWM 应用足够了。CORDIC 虽然理论上角度精度可以无限提高但实际系统的输出滤波特性决定了高次谐波占比很小查表法带来的量化误差在可接受范围内。2.2.1 正弦表生成的两种路径常见做法有两种。第一种是用 MATLAB 生成 .mif 文件再用 Quartus 的 ROM IP 核加载第二种是写成 Verilog 的 case 语句或 initialize 数组。我一般推荐前者因为 mif 文件可以直接用$readmemh读入仿真和综合都能复用同一份数据源避免手写 case 语句时出现重复项或漏项。depth 1024; width 16; t linspace(0, 2*pi, depth); sin_val round((sin(t) 1) / 2 * (2^width - 1)); fid fopen(spwm_sin.mif, w); fprintf(fid, WIDTH %d;\n, width); fprintf(fid, DEPTH %d;\n, depth); fprintf(fid, ADDRESS_RADIX DEC;\n); fprintf(fid, DATA_RADIX DEC;\n); fprintf(fid, CONTENT BEGIN\n); for i 1:depth fprintf(fid, %d : %d;\n, i-1, sin_val(i)); end fprintf(fid, END;\n); fclose(fid);这段脚本把正弦波的单极性半波或全波采样数据写进 mif 文件。注意这里做了归一化处理把正弦值从 [-1, 1] 映射到 [0, 65535] 的无符号范围方便直接与三角载波的计数值做比较。如果做的是双极性调制需要保留有符号数格式比较逻辑也会不同。2.2.2 ROM 读取与地址生成地址计数器是经典的分频器结构输入时钟经过分频得到载波频率fcarr计数器从 0 累加到表深度减 1 再回绕。每次回绕代表一个正弦周期的结束也对应载波的一个完整周期。reg [9:0] addr; always (posedge clk or negedge rst_n) begin if (!rst_n) addr 10d0; else if (addr 10d1023) addr 10d0; else addr addr 1b1; end wire [15:0] sin_data; spwm_rom spwm_rom_inst ( .address(addr), .clock(clk), .q(sin_data) );地址位宽和表深度必须严格匹配ROM IP 核生成时选择 1024 深度地址就是 10 位。这个设计里载波和调制波是同步的地址回绕周期就是调制波周期。如果要做调频可以改分频系数或者直接载入不同的预设值但改了之后要重新计算 ROM 地址步进否则输出频率和预期对不上。2.3 三电平 SPWM 的比较逻辑三电平 SPWM 的调制策略很多最常用的是载波层叠法。三角载波分成上下两层层叠正弦调制波分别与上下载波比较输出结果组合成三电平状态。实现上可以简化为两个比较器加少量逻辑reg pwm_high, pwm_low; always (*) begin if (sin_data up_carr) pwm_high 1b1; else pwm_high 1b0; end always (*) begin if (sin_data low_carr) pwm_low 1b1; else pwm_low 1b0; end assign pwm_out pwm_high ? 2b10 : pwm_low ? 2b01 : 2b00;这里的up_carr和low_carr是上下两个载波的瞬时值。正弦值大于上载波时输出正电平小于下载波时输出负电平中间状态输出零电平。组合逻辑输出后必须经过寄存器打拍再送到引脚否则会引起毛刺这个细节在下一章展开。3. 顶层模块设计从分频到死区控制3.1 模块划分与信号流整套设计可以分为五个子模块分频器div.v、地址发生器、正弦 ROM、比较器和死区模块Communication.v里的通信接口部分。文件命名参考了压缩包里的Sinx.v.bak、PWM.v.bak、Main.v.bak、div.v.bak这些备份文件能看出原作者也是逐步迭代、边改边备份的工程文件里保留.bak是调试期的好习惯。顶层Main.v的职责是实例化所有子模块并连线不写具体逻辑。这样每个子模块可以独立仿真的同时顶层代码保持简洁后期替换查表数据或者改调制策略时只动对应子模块。module spwm_top ( input wire clk_50m, input wire rst_n, output wire [1:0] pwm_a, output wire [1:0] pwm_b, output wire [1:0] pwm_c ); wire clk_carr; div #(.DIV_NUM(50)) u_div ( .clk_in(clk_50m), .rst_n(rst_n), .clk_out(clk_carr) ); wire [15:0] sin_a; luts_spwm u_lut_a ( .clk(clk_carr), .addr(phase_a), .sin_out(sin_a) ); // 三相相位偏移 120 度通过初始地址偏移实现 luts_spwm u_lut_b ( .clk(clk_carr), .addr(phase_b), .sin_out(sin_b) ); luts_spwm u_lut_c ( .clk(clk_carr), .addr(phase_c), .sin_out(sin_c) ); pwm_gen #(.PHASE_SEL(0)) u_pwm_a ( .clk(clk_carr), .sin_data(sin_a), .pwm(pwm_a) ); pwm_gen #(.PHASE_SEL(1)) u_pwm_b ( .clk(clk_carr), .sin_data(sin_b), .pwm(pwm_b) ); pwm_gen #(.PHASE_SEL(2)) u_pwm_c ( .clk(clk_carr), .sin_data(sin_c), .pwm(pwm_c) ); endmodule三相逆变器的关键在相位互差 120 度。由于查表法用的是同一个 ROM实现相位差的方式是给每个相的地址加固定偏移。比如表深度 1024120 度对应约 341 个地址点B 相地址初始值偏移 341C 相偏移 682。地址发生器的实现需要支持初始偏移量否则三相输出同相位逆变器就失效了。3.2 死区控制与时序收敛I/O 缓存的打拍逻辑建议放在独立的pwm_output_register模块中这样核心调制逻辑的时序分析可以单独收敛I/O 约束可以灵活调整。死区时间的具体值取决于功率管的关断延迟和栅极驱动电路常见做法是 1 到 3 微秒。reg [7:0] dead_cnt; reg [1:0] pwm_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_reg 2b00; dead_cnt 8d0; end else if (pwm_in ! pwm_reg) begin if (dead_cnt DEAD_CYCLE) dead_cnt dead_cnt 1b1; else begin pwm_reg pwm_in; dead_cnt 8d0; end end end这种延时切换策略的问题是死区时间内输出保持原状态对三电平会有电平跳变不完整的现象。更好的做法是先判断目标状态在死区时间内强制输出零电平等死区结束后再切到目标电平。不过上面的代码足够理解基本思路实际项目里我一般会改成状态机来控制时序。3.3 仿真验证的关键点testbench 里要检查的这几项不能漏占空比变化范围是否与调制深度一致、有无毛刺、脉冲宽度是否单调变化、三相相位差是否精确为 120 度。initial begin clk_50m 0; forever #10 clk_50m ~clk_50m; end initial begin rst_n 0; #100 rst_n 1; #10000; $finish; end always (posedge clk_50m) begin $display(addr%d sin%d pwm_a%b, phase_a, sin_a, pwm_a); end仿真时把DEAD_CYCLE参数设小一点比如 2可以快速确认逻辑正确性综合时改成实际需求的 100 或更大。死区计数器位宽要够DEAD_CYCLE100时 8 位计数器够用但如果你把载波频率提高一倍死区周期数翻倍此时要扩展计数器位宽。4. 载波比与频率分辨率参数选择的工程边界4.1 载波比的选择依据载波比是载波频率与调制波频率的比值。同步调制保持载波比恒定适合电机变频调速这样频率需要连续变化的场景。查表法天然适合同步调制因为调制波周期和地址回绕强绑定载波比由表深度决定。若表深度 1024输出调制波一个周期内载波完整周期数也是 1024载波比就是 1024在工程上偏高适合滤波器设计但对功率管的开关频率压力大。选择的实际边界取决于功率管类型和开关损耗预算。IGBT 的开关频率一般限制在 10 kHz 到 20 kHz要知道调制波频率是 50 Hz那么载波比最多 200 到 400这时可以降低表深度或提高输出基波频率方案。一个比较稳妥的方案是表深度 256载波比 25650 Hz 时载波 12.8 kHz在 IGBT 可接受范围内。4.2 调频实现的两种路径频率从 50 Hz 往下调时查表法有一个隐藏问题如果地址计数器每个载波周期步进 1地址回绕时间是固定的调制波频率也是固定的。要让频率可调不能改 ROM 数据只能改地址步进。常见做法有两种一种是把固定增量改为可变增量但非整数步进会导致波形畸变另一种是保留固定步进通过动态调整分频系数改变载波频率载波频率和调制波频率同步变化保持载波比不变。我一般用第二种配合锁相环或直接计数器分频动态配置。注意在改变分频系数时加同步使能信号避免切换瞬间出现长度异常的载波周期否则输出的 SPWM 波形在切换点附近会产生次谐波。4.3 调制深度与死区效应补偿调制深度m定义为调制波幅值与载波幅值之比查表法里就是正弦 ROM 数据最大值与比较器参考值的关系。ROM 数据是量化后的定值要改变调制深度有两种实现方式改变 ROM 数据和调整比较阈值。我建议用后者比如把三角载波的幅值上限动态调整到某个寄存器值等效于改变调制深度。这样做的好处是 ROM 数据不用重新生成运行时也可以实时调压。死区效应是低频输出时波形畸变的主要来源。死区时间内的电平被钳位实际输出电压比理想值偏低电流过零附近还会产生非线性。工程上可以用电压补偿表或者反馈补偿但先说清楚现象在波形上体现为窄脉冲丢失和电压基波幅值下降。// 调制深度调节改变比较器的参考上限 reg [15:0] amplitude_reg; wire [15:0] up_carr_limit; assign up_carr_limit amplitude_reg; // 比较器内部用幅值缩放后的 sin_data wire [15:0] scaled_sin; assign scaled_sin (sin_data * amplitude_reg) 16;这种提前缩放的方案不会产生额外的乘法延迟吗实际上会所以scaled_sin的计算需要插入流水寄存器。调制深度需要快速响应时在闭环控制里每次比较都要用到此时可以把 ROM 数据直接换成预缩放版本只不过在调节时需要重新加载整张表。5. ALTERA 器件适配与上板调试技巧5.1 不同 ALTERA 器件的资源差异ALTERA FPGA 里 ROM 资源主要由 M9K 或 MLAB 提供。Cyclone IV 系列有大量 M9K 块每块 9 Kbit1024 深度乘 16 位宽的数据只占 16 Kbit两个 M9K 块就能装下资源几乎可以忽略。但如果表深度扩展到 4096数据宽度到 18 位单个 ROM 占用的块数会变多布局布线时更容易出现时序问题。Cyclone 10 系列资源更好但逻辑设计完全相同主要差异在 IP 核名字和 SDC 约束细节。如果在 Cyclone IV 上跑过的工程要迁移到 Cyclone 10ROM IP 核需要重新生成但这仅供参考不要当成迁移指导来读。关键在于设计时把 ROM 例化独立成模块换器件只改这一处。5.2 SDC 约束与常见时序错误时序约束的核心是定义时钟和输入输出延时。如果不写 SDCQuartus 默认所有路径按理想时序分析上板后大概率出现问题。至少需要三条约束create_clock -name clk_50m -period 20.000 [get_ports {clk_50m}] set_output_delay -clock clk_50m -max 5.0 [get_ports {pwm_a*}] set_output_delay -clock clk_50m -min 2.0 [get_ports {pwm_a*}]set_output_delay的值取决于外部功率驱动电路的建立保持时间要求常见做法是从驱动芯片数据手册里查输入时序参数留 20% 到 30% 余量即可。如果不做约束Quartus 默认输出延迟为零布局布线会偏乐观板子上可能出现随机的毛刺或窄脉冲丢失。5.3 上板后的实测验证方法软件仿真只能验证 RTL 逻辑正确性上板后的信号完整性、死区实际效果、开关管驱动能力都需要示波器测量。我习惯接一个简单的阻感负载来观察电流波形同时用电流探头配合检查过零点的死区效应。# Quartus 中编译完成后通过 JTAG 下载 sof 文件 quartus_pgm -c USB-Blaster -m JTAG -o p;output_files/spwm_top.sof下载后用示波器通道 1 量 PWM_H通道 2 量 PWM_L用 Math 功能做 A-B 得到桥臂输出电压。要检查的关键点包括一对互补 PWM 的上升沿和下降沿是否完全错开、是否有异常窄脉冲、死区时间是否与配置值吻合。窄脉冲问题要重点关注如果 PWM 脉宽小于功率管开关时间实际输出就会出现脉冲丢失电机电流噪音明显变大。5.4 Verification license 报错的应对搜索热词里有一条「17.1 error: failure to obtain a verilog simulation license」很典型。这个问题在 Quartus 17.1 及之后的 ModelSim Starter Edition 里常见但深入说会涉及 license 机制不符合我要分享的范围。工程上的临时方案是改用iverilog加 GTKWave 做功能仿真已经完全验证逻辑后再进 Quartus 综合。OS 层面的操作符合主流工具链的常见用法没有任何不可描述的问题。iverilog -o spwm_tb.vvp spwm_top.v luts_spwm.v pwm_gen.v div.v tb_spwm.v vvp spwm_tb.vvp gtkwave spwm_tb.vcd这套流程对没有商业仿真 license 的场景非常适用功能验证足够速度也不慢。不过要注意 iverilog 对部分 Verilog 2001 语法支持不完整编译报错时先查自己代码里有没有数组初始化或 generate 块的兼容写法。另外 IP 核生成的 ROM 在 iverilog 里不能用需要准备一个行为级 ROM 模型仿真或者用$readmemh读外部数据文件。6. 基于 SignalTap 的在板调试与波形截获SignalTap II 是 ALTERA 自带逻辑分析仪上板后可以直接观察 FPGA 内部信号不用引出引脚。对 SPWM 调试这种信号频率较高的场景特别好用因为内部信号无法直接用示波器测量但 SignalTap 可以任意观察。配置 SignalTap 时要注意采样深度和触发条件。采样深度设 2048 足够观察几十个载波周期的波形触发条件建议用地址回绕信号wrap_en的上升沿每次从正弦周期起点开始采集波形容易对齐观察。set_instance_assignment -name SIGNALTAP_FILE stp1.stp -to u_spwm_top set_instance_assignment -name SIGNALTAP_ENABLE ON -to u_spwm_top在 Quartus 的 SignalTap 编辑器里添加信号addr、sin_data、pwm_a、pwm_b、pwm_c编译下载后运行采集。如果发现三相波形相位差不稳定重点检查地址发生器的初始偏移量是否正确以及复位时序是否满足所有模块同时退出复位。常见问题是异步复位释放时各个模块的计数状态不一致解决方法是把复位信号打两拍再分发到子模块同时取消全局异步复位或改用同步复位。查看波形时要把 SignalTap 的数据导入 MATLAB 或者 Python 做频谱分析确认谐波分布是否正常。一个快速验证方法是检查 PWM 脉冲宽度的包络是否接近正弦。如果包络出现台阶状跳变说明 ROM 数据量化位数不够或者表深度偏小此时应该把数据宽度从 16 位提到 18 位或者深度翻倍重新生成 mif 文件。如果包络正常但电流波形噪音大优先怀疑死区时间和功率管驱动电路不是 FPGA 逻辑问题。调试到这里一套完整的查表法三电平 SPWM 就从逻辑设计走到了上板实测每一层的验证手段都对应一种典型的失败模式。下次再遇到逆变器项目你可以直接复用这套架构把精力花在闭环算法和功率电路上。本文还有配套的精品资源点击获取