計)
最近幫朋友調(diào)試一臺設(shè)備輸出的一路方波信號頻率很低大概在幾十赫茲到幾十千赫茲之間來回變占空比還會跟著工況變化。手頭沒有頻率計示波器雖然能看波形但總不能一直讓人盯著屏幕讀數(shù)據(jù)。我干脆用手里一塊閑置的FPGA開發(fā)板寫了一個低頻方波測量模塊把頻率和占空比直接測出來顯示在數(shù)碼管上順便通過串口發(fā)給上位機。做完之后發(fā)現(xiàn)這個需求其實非常典型很多做傳感器、電機驅(qū)動、電源控制的人都會遇到所以把這個項目的完整思路、代碼結(jié)構(gòu)、仿真方法和踩過的坑整理出來。這個方案的核心思路是用FPGA內(nèi)部的高頻基準時鐘去“數(shù)”輸入方波一個周期內(nèi)跑了多少拍以及高電平期間跑了多少拍然后反算出頻率和占空比。聽上去不復雜但真正做起來有幾個容易翻車的地方輸入信號怎么整形、計數(shù)器位寬怎么定、除法怎么做、數(shù)碼管怎么刷新不閃爍、低頻信號仿真花的時間太長怎么辦。這篇文章會一個一個說清楚并且給出可以直接拿來改的Verilog代碼框架。適合剛?cè)腴TFPGA、想做一個完整小項目的朋友也適合已經(jīng)在做信號測量、想了解測周期法和測頻法區(qū)別的工程師。1. 需求拆解與方案選型1.1 低頻方波測量到底難在哪如果只測頻率很多人第一反應是“數(shù)脈沖不就行了”開一個固定時間窗口比如1秒統(tǒng)計這個窗口內(nèi)來了多少個上升沿頻率就是計數(shù)結(jié)果。這個方法叫測頻法原理很簡單但對低頻信號非常不友好。假設(shè)被測信號是10Hz1秒窗口內(nèi)只能數(shù)到10個脈沖計數(shù)結(jié)果稍微波動一下顯示就變成9Hz或11Hz誤差直接到10%。就算把窗口時間拉長到10秒分辨率也只能到0.1Hz而且刷新速度慢得讓人抓狂。低頻方波的周期長這是它的物理特性決定的想要用固定窗口測頻法拿到高分辨率代價非常大。占空比測量就更麻煩一點。占空比等于高電平時間除以整個周期時間要得到這兩個量必須精確知道信號什么時候變高、什么時候變低。如果信號邊沿不干凈帶著毛刺或者上升沿很緩計數(shù)結(jié)果就會忽大忽小顯示值跳得沒法看。所以這個項目真正要解決的不是“怎么數(shù)數(shù)”這種簡單問題而是三個層面的問題信號進來之后怎么處理才能讓計數(shù)器穩(wěn)定工作計數(shù)器和計算邏輯怎么設(shè)計才能保證精度結(jié)果顯示怎么做才能既直觀又不閃爍。1.2 為什么選測周期法而不是測頻法既然固定窗口測頻法不適合低頻那就反過來想不數(shù)“一個窗口內(nèi)有多少個被測信號周期”而是數(shù)“一個被測信號周期內(nèi)有多少個基準時鐘周期”。這就是測周期法。具體做法是FPGA內(nèi)部跑一個高頻基準時鐘比如100MHz。被測信號一個完整的周期內(nèi)基準時鐘會走過很多個時鐘沿用計數(shù)器把這些時鐘沿數(shù)出來記作N。那么被測信號的周期就是N乘以基準時鐘周期頻率就是基準時鐘頻率除以N頻率 100MHz / N舉個例子被測信號是1kHz時周期是1ms100MHz時鐘在一個周期內(nèi)能數(shù)到100000拍。用這個計數(shù)值反推頻率分辨率遠高于測頻法。而且被測信號頻率越低一個周期內(nèi)數(shù)到的拍數(shù)越多測量精度越高。這和固定窗口測頻法正好相反天生就是為低頻信號準備的。我這次把目標測量范圍定在1Hz到100kHz占空比范圍1%到99%。在這個范圍內(nèi)測周期法的精度完全夠用。如果被測信號頻率很高比如幾兆赫茲以上才需要考慮等精度測量法或者純測頻法。對于低頻方波測量這個題目測周期法是最合適的方案沒有之一。1.3 設(shè)計指標與整體架構(gòu)動手之前先定了幾個指標后續(xù)所有設(shè)計都圍繞這幾個指標展開指標項設(shè)計目標頻率測量范圍1Hz ~ 100kHz頻率分辨率最低1Hz低頻段遠優(yōu)于0.1Hz占空比測量范圍1% ~ 99%占空比顯示精度0.1%輸入信號電平3.3V / 5V兼容基準時鐘100MHz刷新率每個被測信號周期更新一次整體架構(gòu)分成五塊信號預處理電路、邊沿檢測與同步模塊、雙計數(shù)器模塊、計算模塊、顯示輸出模塊。信號進來先經(jīng)過施密特觸發(fā)器整形變成干凈的方波然后進FPGA做兩級同步消除亞穩(wěn)態(tài)再進入計數(shù)器。計數(shù)器在每個被測信號周期內(nèi)同時統(tǒng)計周期計數(shù)值和高電平計數(shù)值鎖存后送給計算模塊做除法最后把頻率和占空比送到數(shù)碼管和串口。2. 硬件預處理與核心參數(shù)計算2.1 輸入信號整形與電平適配FPGA的IO不能直接接亂七八糟的信號。很多傳感器輸出的所謂“方波”實際邊沿很緩甚至帶有明顯的振鈴和毛刺。如果直接把這種信號送進FPGA引腳計數(shù)器會在邊沿附近反復觸發(fā)測出來的數(shù)據(jù)完全是亂的。我習慣在輸入端加一級74HC14施密特觸發(fā)器。施密特觸發(fā)器的特點是有遲滯特性輸入電壓超過上門限才輸出翻轉(zhuǎn)低于下門限才再次翻轉(zhuǎn)中間有一段“死區(qū)”。這個死區(qū)能天然濾掉一部分毛刺同時能把緩慢變化的邊沿整形成陡峭的標準方波。實測下來輸入信號哪怕是從幾百毫伏緩慢爬升的三角波經(jīng)過74HC14之后也能變成質(zhì)量很好的方波。電平適配方面要特別注意。FPGA的Bank電壓如果是3.3VIO引腳一般不能直接接受5V信號。如果被測設(shè)備輸出5V方波必須做電平轉(zhuǎn)換。最簡單的辦法是電阻分壓把5V分到3.3V以下。我在BNC接口后面放了一個10kΩ和6.8kΩ的分壓電阻配合穩(wěn)壓管鉗位實測很穩(wěn)。如果被測信號是12V甚至更高的電壓就不要用分壓了直接上光耦隔離更安全順便把地環(huán)路也斷開了。2.2 時鐘選擇與計數(shù)器位寬推導基準時鐘直接決定測量分辨率。我用了開發(fā)板上的100MHz有源晶振時鐘周期10ns。為什么不用更常見的50MHz道理很簡單50MHz時鐘周期20ns同樣是1kHz信號一個周期只能數(shù)到50000拍而100MHz能數(shù)到100000拍分辨率提高了一倍。功耗和資源占用在這個量級幾乎沒差別但測量精度實打?qū)嵦嵘艘槐哆@個性價比很劃算。計數(shù)器位寬需要根據(jù)最低測量頻率來算。最低頻率1Hz周期1秒100MHz時鐘在一個周期內(nèi)會走100000000個時鐘沿。100000000轉(zhuǎn)換成二進制需要27位。為了留余量、防止意外情況導致計數(shù)溢出我直接全部用32位計數(shù)器。FPGA上32位計數(shù)器也就占用十幾個邏輯單元完全不用省。這里有個容易忽略的點計算頻率時要把計數(shù)值加1再拿去用。因為計數(shù)器在上升沿清零下一次上升沿到來時鎖存實際數(shù)到的值會比真實周期少一個時鐘周期。這是測量原理帶來的系統(tǒng)性偏差雖然只有10ns但如果不修正頻率高的時候誤差會明顯偏大。計算模塊里我統(tǒng)一做了加1修正。2.3 顯示與通信接口選擇顯示部分我做了兩路輸出。頻率用一組四位數(shù)碼管顯示單位Hz直接顯示整數(shù)部分就夠用。占空比用另一組兩位數(shù)碼管加一個小數(shù)點顯示顯示成百分比形式比如“50.0%”。數(shù)碼管動態(tài)掃描容易出問題后面專門講。我的建議是如果條件允許優(yōu)先把UART串口調(diào)通用串口把頻率和占空比打出來調(diào)試。串口數(shù)據(jù)能直接看到完整數(shù)值還能看到原始計數(shù)值排查問題時比數(shù)碼管直觀得多。數(shù)碼管更適合最終產(chǎn)品使用而串口更適合開發(fā)調(diào)試階段。這次我兩個都做了但在開發(fā)的前半段幾乎所有問題都是靠串口打印發(fā)現(xiàn)的。3. FPGA核心模塊實現(xiàn)3.1 頂層模塊劃分整個工程在Vivado里面建了一個頂層文件下面掛了六個子模塊clk_gen時鐘管理把有源晶振的100MHz時鐘處理一下必要時加PLLsync_edge輸入信號兩級同步同時產(chǎn)生上升沿和下降沿脈沖counter雙計數(shù)器統(tǒng)計周期計數(shù)值和高電平計數(shù)值calc計算模塊做除法和格式轉(zhuǎn)換display數(shù)碼管動態(tài)掃描驅(qū)動uart_tx串口發(fā)送把測量結(jié)果格式化輸出模塊化設(shè)計的好處是每個模塊可以單獨寫testbench驗證出了問題定位很快。很多初學者習慣把代碼全部堆在一個always塊里看起來省事但一旦結(jié)果不對很難判斷是計數(shù)器問題還是計算問題還是顯示問題。頂層例化的時候要注意counter模塊輸出的valid信號要連到calc模塊表示“本次計數(shù)值有效”。calc模塊內(nèi)部有一個除法器除法器不是組合邏輯需要幾個時鐘周期才能出結(jié)果所以valid信號也要經(jīng)過相應延遲保證數(shù)據(jù)同步。3.2 跨時鐘域同步與邊沿檢測輸入信號來自外部設(shè)備和FPGA的100MHz時鐘沒有確定的相位關(guān)系屬于異步信號。異步信號直接進觸發(fā)器有可能采到介于高低電平之間的中間狀態(tài)也就是亞穩(wěn)態(tài)。亞穩(wěn)態(tài)會導致寄存器輸出不確定嚴重的時候整個邏輯都會卡死。解決亞穩(wěn)態(tài)的標準做法是兩級寄存器同步。輸入信號打兩拍第二拍輸出的信號基本可以認為是和100MHz時鐘同步的了。邊沿檢測在這兩級同步之后做用同步后的信號和它延遲一拍的信號做比較reg [1:0] sync_reg; reg sig_dly; wire rising; wire falling; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync_reg 2b00; sig_dly 1b0; end else begin sync_reg {sync_reg[0], sig_in}; sig_dly sync_reg[1]; end end assign rising sync_reg[1] ~sig_dly; assign falling ~sync_reg[1] sig_dly;這里有一個細節(jié)rising信號比真正的輸入邊沿晚了一個到兩個時鐘周期。這個延遲是固定的對周期測量沒有影響因為鎖存和清零用的都是同一個延遲后的邊沿信號誤差只是整體平移不會影響周期值。真正影響測量值的是上一節(jié)說的“少一拍”問題通過加1修正解決。3.3 周期計數(shù)與高電平計數(shù)時序核心計數(shù)器邏輯是整個項目的靈魂我貼出關(guān)鍵代碼reg [31:0] cnt_total; reg [31:0] cnt_high; reg [31:0] period_cnt; reg [31:0] high_cnt; reg valid; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_total 32d0; cnt_high 32d0; period_cnt 32d0; high_cnt 32d0; valid 1b0; end else begin valid 1b0; cnt_total cnt_total 1b1; if (sync_reg[1] 1b1) cnt_high cnt_high 1b1; if (rising) begin period_cnt cnt_total 1b1; // 加1修正 high_cnt cnt_high 1b1; // 占空比測量同樣修正 cnt_total 32d0; cnt_high 32d0; valid 1b1; end end end這個邏輯的思路是cnt_total 一直在走不管輸入信號是高還是低每個clk都加1cnt_high 只在輸入信號為高的時候加1。每檢測到一個上升沿說明上一個周期結(jié)束了把當前計數(shù)值鎖存到period_cnt和high_cnt然后把兩個計數(shù)器清零開始新一周期的計數(shù)。上升沿鎖存的是“上一周期”的數(shù)據(jù)這一點要特別注意。第一次上電后需要等待一個完整周期才會輸出第一個有效數(shù)據(jù)這是正常的。valid信號會在鎖存數(shù)據(jù)的那一個時鐘周期拉高告訴后面的計算模塊“數(shù)據(jù)來了”。占空比的計算公式就是占空比百分比 high_cnt / period_cnt × 100%注意high_cnt在信號剛好等于50%占空比時理論上應該等于period_cnt的一半。由于計數(shù)器清零和邊沿檢測存在最多1拍的相位差實際會差幾個計數(shù)值但這在低頻測量中完全可以忽略。3.4 頻率占空比計算從除法到定點數(shù)頻率計算公式是100000000除以period_cnt占空比公式是high_cnt乘100再除以period_cnt。這里涉及FPGA最頭疼的運算之一除法。FPGA里實現(xiàn)除法有兩種常見方式。一種是直接用廠商提供的除法器IP核Vivado里面有Divider GeneratorQuartus里面有LPM_DIVIDE。配置好位寬和流水線級數(shù)把被除數(shù)和除數(shù)十進制輸入幾十個時鐘周期后能拿到商和余數(shù)。另一種是自己寫移位相減除法器結(jié)構(gòu)簡單但占資源速度也慢。我建議這個項目直接用IP核原因很簡單數(shù)據(jù)更新頻率很低高頻除法器根本沒有必要IP核雖然延遲幾十個時鐘周期但結(jié)果穩(wěn)定不會出錯。自己寫除法器如果位寬沒控制好很容易出現(xiàn)商和余數(shù)錯位的問題排查起來非常頭疼。計算模塊的狀態(tài)機大致是這樣檢測到valid信號后把period_cnt和100000000送入除法器等到除法器輸出有效信號時把商和余數(shù)鎖存轉(zhuǎn)換成BCD碼送入顯示模塊。占空比計算類似先把high_cnt乘100直接移位加幾個加法器就能實現(xiàn)乘以100再送除法器。顯示部分用到BIN轉(zhuǎn)BCD。四位十進制數(shù)可以用純組合邏輯的查表法也可以用移位加3算法。我這次用的是移位加3寫了一個小函數(shù)逐位處理代碼很短效果穩(wěn)定。3.5 結(jié)果輸出數(shù)碼管動態(tài)顯示與UART數(shù)碼管動態(tài)顯示的原理并不復雜就是利用人眼的視覺暫留效應快速輪流點亮每一位數(shù)碼管。四位數(shù)碼管每一位點亮1ms整個循環(huán)4ms刷新率250Hz看起來就是同時亮的。這里有個非常容易踩的坑動態(tài)掃描一定要保證每一位的顯示數(shù)據(jù)在切換時不出現(xiàn)毛刺。我一開始直接在時鐘上升沿同時切換位選和段選結(jié)果數(shù)碼管亮度不均勻還出現(xiàn)鬼影。后來把操作拆成兩步先在時鐘低電平準備好段碼數(shù)據(jù)再在時鐘上升沿切換位選問題就消失了。其實就是給段碼一個建立時間道理和普通時序約束一樣。UART部分我寫了一個簡單的發(fā)送模塊波特率96008位數(shù)據(jù)1位停止位。上位機用串口助手接收每秒刷新一次。發(fā)送的內(nèi)容格式化之后大概長這樣F: 1000Hz D: 50.0%串口調(diào)試最重要的作用是能看到原始計數(shù)值。我把period_cnt和high_cnt也一起打出來格式是P:100000 H:50000 F:1000Hz D:50.0%這樣一旦顯示值不對立刻能看出來是計數(shù)階段出了問題還是計算階段出了問題。開發(fā)調(diào)試階段這個輸出格式幫我省了大量時間。4. 仿真驗證與實測結(jié)果4.1 Testbench編寫要點仿真驗證階段我寫了兩個testbench。第一個很簡單產(chǎn)生一個固定1kHz方波占空比40%100MHz時鐘下理論上period_cnt應該大約是100000high_cnt應該是40000左右。產(chǎn)生帶占空比的信號Verilog里最直接的方式是用兩組延時控制initial begin clk 0; forever #5 clk ~clk; // 100MHz時鐘 end initial begin sig_in 0; #10000; // 先給10us低電平 forever begin sig_in 1; #400; // 高電平保持400us占空比40% sig_in 0; #600; // 低電平保持600us end end這里有個細節(jié)被測信號1kHz一個周期1ms仿真至少要跑到幾個完整周期才能看到有效輸出。在ModelSim里1ms仿真時間對應的時鐘沿數(shù)量是100000個跑起來還算流暢但如果測的是10Hz信號一個周期100ms仿真跑完就是1000萬個時鐘沿這時候ModelSim會明顯變慢。所以仿真階段用1kHz以上的信號驗證邏輯正確性實測階段再去驗證低頻場景這樣可以節(jié)省大量仿真時間。第二個testbench模擬了上電后立刻有信號的場景用來驗證第一個測量周期是否會產(chǎn)生異常值。實測發(fā)現(xiàn)上電后第一個有效輸出之前計算模塊可能拿到一個無效的中間值需要通過一個計數(shù)器屏蔽前兩次輸出保證穩(wěn)定后再刷新顯示。4.2 精度分析誤差從哪里來測周期法的理論誤差來源主要是量化誤差也就是計數(shù)器只能數(shù)整數(shù)個時鐘周期結(jié)果和真實值之間最多差1個時鐘周期。100MHz時鐘下這個量化誤差就是10ns。不同頻率下的理論誤差可以列表對比被測頻率周期周期計數(shù)值頻率理論誤差1Hz1s100000000約0.000001%100Hz10ms1000000約0.0001%1kHz1ms100000約0.001%10kHz100us10000約0.01%100kHz10us1000約0.1%從表格能清楚看到頻率越低精度越高。在100kHz時誤差到了0.1%這個量級對于通用測量還是可以接受的但如果你要做精密測量就不要用純測周期法了得換等精度測量方案。占空比的誤差除了量化誤差還受高電平計數(shù)的精度影響。高電平時間同樣是10ns分辨率所以占空比誤差大約是占空比誤差 ≈ 2 × 10ns / 周期低頻時這個誤差幾乎可以忽略高頻時會逐漸變大。實測下來1kHz信號占空比測量的誤差在0.1%以內(nèi)完全滿足設(shè)計指標。4.3 實測數(shù)據(jù)對比與波形觀察用信號發(fā)生器輸出了幾組標準信號和FPGA模塊的顯示值做了對比信號發(fā)生器設(shè)定FPGA實測頻率FPGA實測占空比100Hz / 50.0%100.0Hz50.0%1kHz / 40.0%1000Hz40.1%10kHz / 60.0%9999Hz59.9%50kHz / 30.0%50014Hz30.2%頻率越高偏差越明顯這和理論誤差分析的趨勢完全一致。整體來看低頻段精度表現(xiàn)非常好100kHz以內(nèi)誤差都在0.3%以內(nèi)符合預期。有一個有意思的細節(jié)信號發(fā)生器輸出50%占空比示波器上看起來也是完美的方波但FPGA讀出來卻是49.8%左右。一開始以為是代碼問題查了很久才發(fā)現(xiàn)是74HC14的閾值電壓不是正好在輸入電壓中點導致輸出高電平時間比理論值略短。這不是FPGA測錯了是整形電路固有的偏移。做高精度占空比測量的話這個偏移可以在軟件里做校準。5. 常見問題與排查技巧5.1 計數(shù)溢出或長時間無更新如果被測信號頻率高于設(shè)計上限period_cnt會變得很小此時除法器的結(jié)果跳動會很厲害。比如輸入1MHz信號一個周期只數(shù)到100拍頻率值可能從990kHz跳到1010kHz看起來就像電路壞了。這個問題可以通過增加檔位判斷解決當period_cnt小于某個閾值比如1000時自動切換到測頻法模式兩種方法結(jié)合就能覆蓋更寬的頻率范圍。長時間無更新先檢查邊沿檢測信號有沒有輸出。可以用調(diào)試邏輯分析儀或者直接把rising信號引到LED上看有沒有閃爍。如果LED不閃說明輸入信號沒有正確進入FPGA檢查外部整形電路和引腳分配如果LED閃但顯示不刷新問題在計數(shù)器清零邏輯或valid信號鏈路上。5.2 顯示亂跳、數(shù)值不穩(wěn)定顯示亂跳最常見的原因是輸入信號帶毛刺。毛刺會觸發(fā)額外的上升沿導致計數(shù)器在錯誤位置清零周期值和高電平值完全是亂的。解決辦法是外部加施密特觸發(fā)器并且在FPGA內(nèi)部對同步后的信號再做一次毛刺濾波比如連續(xù)采樣到多個高電平才認為信號真正變高。另一個原因是數(shù)碼管動態(tài)掃描刷新率太低。如果每位點亮時間太長人眼能看到明顯的閃爍如果太短數(shù)碼管亮度不夠而且可能顯示不均勻。我最終把每位點亮時間設(shè)為1ms四位數(shù)碼管一個循環(huán)4ms刷新率250Hz既看不到閃爍亮度也足夠。5.3 輸入引腳損壞與接口保護這個坑我替大家踩過了。最開始圖省事直接從信號發(fā)生器輸出5V方波接到FPGA板子的3.3V Bank引腳上結(jié)果用了不到半天那個引腳就失靈了。FPGA的IO引腳不是5V兼容的超壓輸入會直接損壞引腳內(nèi)部的ESD保護結(jié)構(gòu)。正確做法是做好防護再進FPGA。我在輸入端加了串聯(lián)電阻和二極管鉗位串聯(lián)電阻選100Ω左右既能限制電流又和引腳寄生電容組成低通濾波能稍微濾掉一些高頻噪聲。如果輸入信號來自工業(yè)現(xiàn)場強烈建議用光耦隔離雖然光耦會引入額外的上升沿延遲但對于低頻方波測量來說這點延遲完全不影響精度。光耦還能徹底解決地電位不一致的問題避免地環(huán)路干擾。5.4 ModelSim仿真常見坑仿真低頻信號最大的問題是時間尺度太大。我第一次用10Hz信號仿真跑了20分鐘波形窗口里還看不到一個完整周期后來想通了仿真只是驗證邏輯正確性不是驗證低頻性能完全可以用1kHz或10kHz信號代替邏輯只要對了低頻下只是計數(shù)值更大不會有本質(zhì)區(qū)別。另一個常見坑是仿真時寄存器初始值不定。如果復位信號設(shè)計不好一開始所有寄存器都是X態(tài)計數(shù)器加來加去還是X波形一片紅。我的做法是設(shè)計一個異步復位、同步釋放的復位信號并且在testbench里一開始就拉低復位釋放之后等幾個時鐘周期再送被測信號。同時用ModelSim的“wave”窗口觀察內(nèi)部信號時重點關(guān)注reset、sync_reg、cnt_total這幾個關(guān)鍵信號從源頭排查比瞎猜快得多。這個模塊做完之后我又把它改成了脈沖寬度測量的小工具原理完全一樣只是換了一下計數(shù)觸發(fā)條件。如果你也想做一個類似的東西我的建議是不要一上來就搞數(shù)碼管先UART把數(shù)據(jù)打出來確認計數(shù)和計算都對再做顯示部分這樣問題不會混在一起。低頻方波測量這個題目看似簡單但把信號處理、跨時鐘域、計數(shù)器設(shè)計、除法運算、顯示刷新這些點全部串起來對理解FPGA開發(fā)流程很有幫助。