
簡介正弦脈寬調制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 就從邏輯設計走到了上板實測每一層的驗證手段都對應一種典型的失敗模式。下次再遇到逆變器項目你可以直接復用這套架構把精力花在閉環算法和功率電路上。本文還有配套的精品資源點擊獲取