現(xiàn)IRIG-B解碼:三段式狀態(tài)機(jī)與Verilog代碼詳解)
1. 為什么IRIG-B解碼值得用FPGA來(lái)做1.1 從一次授時(shí)對(duì)接踩坑說(shuō)起前兩年做一個(gè)電力監(jiān)測(cè)項(xiàng)目需要把多臺(tái)采集設(shè)備的時(shí)間戳對(duì)齊到微秒級(jí)。一開(kāi)始圖省事用MCU的輸入捕獲去測(cè)IRIG-B的脈寬結(jié)果現(xiàn)場(chǎng)電磁干擾一大解碼就飄偶爾還會(huì)把0碼元誤判成1。后來(lái)?yè)Q成FPGA做純硬件解碼同樣的干擾環(huán)境下穩(wěn)得像塊石頭。這件事讓我徹底認(rèn)清了IRIG-B這種對(duì)邊沿時(shí)序極其敏感的編碼天生就該交給FPGA處理。IRIG-BInter-Range Instrumentation Group B是一套串行時(shí)間碼標(biāo)準(zhǔn)廣泛用在電力系統(tǒng)、航天測(cè)控、軌道交通這些對(duì)時(shí)間同步要求苛刻的場(chǎng)合。它把天、時(shí)、分、秒以及控制位打包成每秒一幀的脈沖序列通過(guò)同軸電纜或雙絞線(xiàn)傳輸。接收端要做的就是從這串高低電平里把時(shí)間信息摳出來(lái)。為什么不用MCU因?yàn)镮RIG-B的碼元寬度只有10ms、5ms、2ms這幾檔靠軟件輪詢(xún)或中斷捕獲抖動(dòng)輕松上幾十微秒遇到連續(xù)相同碼元還容易丟同步。FPGA用硬件狀態(tài)機(jī)在時(shí)鐘域里數(shù)拍子分辨率直接取決于晶振做到百納秒級(jí)毫無(wú)壓力。這就是我選擇FPGA的根本原因——不是FPGA更高級(jí)而是這個(gè)場(chǎng)景下它的確定性無(wú)可替代。1.2 這篇內(nèi)容適合誰(shuí)看如果你手頭正好有一塊FPGA開(kāi)發(fā)板想做個(gè)授時(shí)接收模塊或者你在電力、工控行業(yè)做設(shè)備開(kāi)發(fā)被IRIG-B解碼折磨過(guò)那這篇就是寫(xiě)給你的。我會(huì)把整個(gè)解碼鏈路拆開(kāi)講從信號(hào)整形、邊沿檢測(cè)、碼元判決到狀態(tài)機(jī)怎么組織、幀同步怎么建立、BCD怎么轉(zhuǎn)二進(jìn)制。核心的Verilog代碼我會(huì)貼出來(lái)你可以直接拿去改。需要的基礎(chǔ)不多懂Verilog基本語(yǔ)法知道什么是時(shí)鐘沿觸發(fā)用過(guò)狀態(tài)機(jī)就行。至于IRIG-B協(xié)議本身我會(huì)在講代碼的時(shí)候順帶把關(guān)鍵點(diǎn)說(shuō)清楚不需要你提前啃標(biāo)準(zhǔn)文檔。2. IRIG-B編碼原理與解碼整體思路2.1 先把碼元規(guī)則吃透IRIG-B每秒發(fā)送一幀一幀100個(gè)碼元每個(gè)碼元占10ms。碼元有三種形態(tài)靠高電平持續(xù)時(shí)間區(qū)分碼元類(lèi)型高電平寬度含義占空比02ms二進(jìn)制020%15ms二進(jìn)制150%P位置標(biāo)識(shí)8ms幀/秒/分界80%每10個(gè)碼元有一個(gè)P碼元做位置標(biāo)記所以一幀里有10個(gè)P碼元分別落在第0、10、20……90個(gè)位置。其中第0個(gè)P碼元是整幀的起點(diǎn)叫參考位Pr。幀結(jié)構(gòu)大致是Pr BCD秒 BCD分 BCD時(shí) BCD日 控制位 校驗(yàn)位。解碼的本質(zhì)就一句話(huà)測(cè)出每個(gè)碼元的高電平寬度判斷它是0、1還是P然后按位置把比特拼成BCD數(shù)再轉(zhuǎn)成二進(jìn)制時(shí)間。2.2 解碼鏈路的四個(gè)環(huán)節(jié)我把整個(gè)解碼流程拆成四步這樣寫(xiě)代碼時(shí)思路不會(huì)亂信號(hào)整形與同步外部進(jìn)來(lái)的IRIG-B可能是差分信號(hào)RS-422或單端TTL先經(jīng)過(guò)電平轉(zhuǎn)換芯片變成FPGA能吃的3.3V單端信號(hào)再做兩級(jí)寄存器打拍消除亞穩(wěn)態(tài)。邊沿檢測(cè)與脈寬測(cè)量用計(jì)數(shù)器測(cè)量高電平持續(xù)的時(shí)鐘周期數(shù)換算成毫秒。碼元判決與狀態(tài)機(jī)推進(jìn)根據(jù)脈寬判定碼元類(lèi)型狀態(tài)機(jī)按碼元序號(hào)推進(jìn)識(shí)別P碼元建立幀同步。數(shù)據(jù)組裝與輸出把判決出的比特按BCD位權(quán)拼裝轉(zhuǎn)成二進(jìn)制加上秒脈沖輸出。這四步里狀態(tài)機(jī)是靈魂。因?yàn)镮RIG-B是嚴(yán)格按位置編碼的你必須知道當(dāng)前處理的是第幾個(gè)碼元才能知道這個(gè)比特代表什么。位置錯(cuò)了后面全錯(cuò)。2.3 為什么用三段式狀態(tài)機(jī)熱詞里三段式狀態(tài)機(jī)出現(xiàn)頻率很高這里正好用得上。所謂三段式就是把狀態(tài)機(jī)拆成三部分第一段時(shí)序邏輯描述狀態(tài)寄存器怎么隨時(shí)鐘跳轉(zhuǎn)第二段組合邏輯描述次態(tài)怎么根據(jù)當(dāng)前狀態(tài)和輸入決定第三段輸出邏輯描述輸出怎么隨狀態(tài)變化相比一段式所有邏輯揉在一個(gè)always塊和兩段式時(shí)序組合三段式的優(yōu)勢(shì)是輸出無(wú)毛刺、時(shí)序好收斂、代碼可讀性強(qiáng)。IRIG-B解碼里狀態(tài)機(jī)的輸出直接控制數(shù)據(jù)鎖存和計(jì)數(shù)器清零一旦有毛刺就可能鎖錯(cuò)數(shù)據(jù)所以三段式是穩(wěn)妥選擇。注意三段式的第二段用組合邏輯描述次態(tài)綜合工具可能會(huì)報(bào)latch警告只要default分支寫(xiě)全就不會(huì)有問(wèn)題。別為了消警告把次態(tài)邏輯改成時(shí)序的那樣就退化成兩段式了。3. Verilog核心代碼逐段拆解3.1 頂層模塊與端口定義先看頂層接口。我習(xí)慣把參數(shù)化做足方便不同時(shí)鐘頻率下復(fù)用module irig_b_decode #( parameter CLK_FREQ 100_000_000, // 系統(tǒng)時(shí)鐘100MHz parameter MS_CNT CLK_FREQ/1000 // 1ms對(duì)應(yīng)的時(shí)鐘數(shù) )( input wire clk, input wire rst_n, input wire irig_in, // 整形后的IRIG-B信號(hào) output reg [7:0] sec_bcd, // 秒 BCD output reg [7:0] min_bcd, // 分 BCD output reg [7:0] hour_bcd, // 時(shí) BCD output reg [15:0] day_bcd, // 日 BCD output reg pps, // 秒脈沖 output reg frame_valid // 幀有效標(biāo)志 );MS_CNT這個(gè)參數(shù)是關(guān)鍵。假設(shè)系統(tǒng)時(shí)鐘100MHz那1ms就是100000個(gè)時(shí)鐘周期。所有脈寬測(cè)量都以它為基準(zhǔn)。如果你用的是50MHz時(shí)鐘改一下參數(shù)就行代碼不用動(dòng)。這就是參數(shù)化的好處。irig_in進(jìn)來(lái)的是已經(jīng)整形好的單端信號(hào)。實(shí)際工程里前端通常用一片RS-422接收器比如某型號(hào)差分接收芯片把差分轉(zhuǎn)單端再送進(jìn)FPGA的普通IO。這一步別省差分傳輸抗共模干擾的能力是單端比不了的。3.2 輸入同步與邊沿檢測(cè)外部信號(hào)和FPGA內(nèi)部時(shí)鐘是異步的直接采樣會(huì)有亞穩(wěn)態(tài)風(fēng)險(xiǎn)。標(biāo)準(zhǔn)做法是打兩拍reg irig_d1, irig_d2, irig_d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin irig_d1 1b0; irig_d2 1b0; irig_d3 1b0; end else begin irig_d1 irig_in; irig_d2 irig_d1; irig_d3 irig_d2; end end wire rise_edge irig_d2 ~irig_d3; // 上升沿 wire fall_edge ~irig_d2 irig_d3; // 下降沿打兩拍消亞穩(wěn)態(tài)是常識(shí)但我多打了一拍d3目的是讓邊沿檢測(cè)更干凈。rise_edge和fall_edge都是單周期脈沖后面計(jì)數(shù)器就靠它們啟動(dòng)和停止。實(shí)操心得如果你的板子上IRIG-B信號(hào)走線(xiàn)比較長(zhǎng)建議在IO約束里加上施密特觸發(fā)或內(nèi)部上拉能明顯改善邊沿質(zhì)量。我遇到過(guò)因?yàn)樽呔€(xiàn)反射導(dǎo)致邊沿抖動(dòng)多打一拍都救不回來(lái)最后是加了端接電阻才解決。3.3 脈寬測(cè)量計(jì)數(shù)器上升沿來(lái)了開(kāi)始計(jì)數(shù)下降沿來(lái)了停止計(jì)數(shù)值就是高電平寬度reg [31:0] width_cnt; reg [31:0] width_val; reg measuring; always (posedge clk or negedge rst_n) begin if (!rst_n) begin width_cnt 32d0; width_val 32d0; measuring 1b0; end else begin if (rise_edge) begin width_cnt 32d0; measuring 1b1; end else if (fall_edge) begin width_val width_cnt; measuring 1b0; end else if (measuring) begin width_cnt width_cnt 1b1; end end endwidth_val就是高電平持續(xù)的時(shí)鐘數(shù)。要換算成毫秒除以MS_CNT即可。但除法在硬件里費(fèi)資源我一般直接拿width_val和幾個(gè)閾值比較避免除法器。判決閾值怎么定以100MHz、1ms100000周期為例2ms碼元理論200000周期取中間值判0的窗口設(shè)為 100000~3000005ms碼元理論500000周期判1的窗口設(shè)為 400000~6000008ms碼元理論800000周期判P的窗口設(shè)為 700000~900000窗口留得寬一點(diǎn)是為了容忍晶振偏差和信號(hào)抖動(dòng)。但也不能太寬否則相鄰碼元會(huì)混淆。我一般取理論值的±50%作為邊界實(shí)測(cè)下來(lái)很穩(wěn)。3.4 碼元判決邏輯localparam TH_0_MAX MS_CNT*3; // 3ms localparam TH_1_MIN MS_CNT*4; // 4ms localparam TH_1_MAX MS_CNT*6; // 6ms localparam TH_P_MIN MS_CNT*7; // 7ms reg [1:0] symbol; // 0:碼元0, 1:碼元1, 2:碼元P always (posedge clk or negedge rst_n) begin if (!rst_n) symbol 2d0; else if (fall_edge) begin if (width_val TH_0_MAX) symbol 2d0; else if (width_val TH_1_MIN) symbol 2d0; // 落在過(guò)渡區(qū)保守判0 else if (width_val TH_1_MAX) symbol 2d1; else if (width_val TH_P_MIN) symbol 2d2; else symbol 2d0; end end這里有個(gè)細(xì)節(jié)過(guò)渡區(qū)3ms~4ms之間我保守判成0。因?yàn)镮RIG-B里0碼元出現(xiàn)頻率最高誤判成0對(duì)幀同步的破壞最小。如果誤判成P狀態(tài)機(jī)會(huì)以為到了位置標(biāo)記整個(gè)幀就錯(cuò)位了。3.5 三段式狀態(tài)機(jī)主體狀態(tài)機(jī)的任務(wù)是數(shù)碼元序號(hào)識(shí)別P碼元建立幀同步。我定義幾個(gè)狀態(tài)localparam S_IDLE 3d0; // 等待第一個(gè)P localparam S_SYNC 3d1; // 已同步正常解碼 localparam S_VERIFY 3d2; // 校驗(yàn)幀第一段狀態(tài)寄存器reg [2:0] cur_state, nxt_state; reg [6:0] sym_idx; // 碼元序號(hào) 0~99 reg frame_start; always (posedge clk or negedge rst_n) begin if (!rst_n) cur_state S_IDLE; else cur_state nxt_state; end第二段次態(tài)邏輯always (*) begin nxt_state cur_state; case (cur_state) S_IDLE: begin if (symbol 2d2 sym_idx 7d0) nxt_state S_SYNC; end S_SYNC: begin if (sym_idx 7d99) nxt_state S_VERIFY; end S_VERIFY: begin if (symbol 2d2) nxt_state S_SYNC; else nxt_state S_IDLE; end default: nxt_state S_IDLE; endcase end第三段輸出與序號(hào)推進(jìn)always (posedge clk or negedge rst_n) begin if (!rst_n) begin sym_idx 7d0; frame_start 1b0; end else if (fall_edge) begin if (cur_state S_IDLE) begin if (symbol 2d2) sym_idx 7d1; // 檢測(cè)到Pr從1開(kāi)始 else sym_idx 7d0; end else if (cur_state S_SYNC) begin if (sym_idx 7d99) begin sym_idx 7d0; frame_start 1b1; end else begin sym_idx sym_idx 1b1; frame_start 1b0; end end end end這里frame_start在每幀結(jié)束時(shí)拉高一個(gè)周期作為數(shù)據(jù)鎖存的觸發(fā)信號(hào)。注意P碼元本身不攜帶數(shù)據(jù)所以序號(hào)推進(jìn)時(shí)要跳過(guò)它。踩過(guò)的坑一開(kāi)始我把sym_idx的復(fù)位值設(shè)成0結(jié)果上電后如果第一個(gè)碼元不是P狀態(tài)機(jī)就卡在IDLE出不來(lái)。后來(lái)改成只有檢測(cè)到P才進(jìn)SYNC并且sym_idx從1開(kāi)始數(shù)問(wèn)題解決。幀同步的建立必須嚴(yán)格依賴(lài)Pr碼元不能靠猜。3.6 BCD數(shù)據(jù)組裝IRIG-B的BCD數(shù)據(jù)是按位權(quán)排列的。以秒為例第1~4個(gè)碼元是秒的個(gè)位1、2、4、8第6~8個(gè)是秒的十位10、20、40。中間第5、第9是P碼元占位。reg [3:0] sec_units, sec_tens; reg [3:0] min_units, min_tens; reg [3:0] hour_units, hour_tens; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sec_units 4d0; sec_tens 4d0; end else if (fall_edge cur_state S_SYNC) begin case (sym_idx) 7d1: sec_units[0] (symbol 2d1); 7d2: sec_units[1] (symbol 2d1); 7d3: sec_units[2] (symbol 2d1); 7d4: sec_units[3] (symbol 2d1); 7d6: sec_tens[0] (symbol 2d1); 7d7: sec_tens[1] (symbol 2d1); 7d8: sec_tens[2] (symbol 2d1); // 分、時(shí)、日同理按位權(quán)映射 default: ; endcase end end分、時(shí)、日的位權(quán)映射和秒類(lèi)似只是碼元序號(hào)不同。完整的映射關(guān)系我建議對(duì)著IRIG-B標(biāo)準(zhǔn)表格一個(gè)個(gè)填別憑記憶。我當(dāng)初就是記錯(cuò)了日的十位位權(quán)導(dǎo)致日期總是差幾天查了半天才發(fā)現(xiàn)。3.7 BCD轉(zhuǎn)二進(jìn)制與輸出BCD轉(zhuǎn)二進(jìn)制用簡(jiǎn)單的乘加就行always (posedge clk or negedge rst_n) begin if (!rst_n) begin sec_bcd 8d0; min_bcd 8d0; hour_bcd 8d0; day_bcd 16d0; end else if (frame_start) begin sec_bcd {sec_tens, sec_units}; min_bcd {min_tens, min_units}; hour_bcd {hour_tens, hour_units}; day_bcd {day_hundreds, day_tens, day_units}; end end輸出直接給BCD碼上位機(jī)拿到后自己轉(zhuǎn)十進(jìn)制。如果下游需要二進(jìn)制再加一級(jí)轉(zhuǎn)換邏輯即可。我傾向于在FPGA里保持BCD因?yàn)锽CD轉(zhuǎn)二進(jìn)制會(huì)引入組合邏輯延遲放在幀結(jié)束后的空閑期做更從容。4. 實(shí)操調(diào)試與常見(jiàn)問(wèn)題排查4.1 上板調(diào)試的完整步驟代碼寫(xiě)完只是開(kāi)始真正花時(shí)間的是調(diào)試。我總結(jié)了一套流程信號(hào)源準(zhǔn)備用一臺(tái)IRIG-B信號(hào)發(fā)生器或者用另一塊FPGA自己產(chǎn)生標(biāo)準(zhǔn)IRIG-B碼。我常用后者因?yàn)榭梢跃_控制每個(gè)碼元方便定位問(wèn)題。示波器看波形先把整形后的信號(hào)接到示波器確認(rèn)高電平寬度確實(shí)是2ms/5ms/8ms。如果寬度不對(duì)先查前端電路別急著調(diào)FPGA。ILA抓內(nèi)部信號(hào)把width_val、symbol、sym_idx、cur_state都加到ILA里。觸發(fā)條件設(shè)成fall_edge看每個(gè)碼元判決是否正確。驗(yàn)證幀同步觀(guān)察sym_idx是否從0數(shù)到99然后歸零frame_start是否每秒出現(xiàn)一次。比對(duì)時(shí)間把輸出的BCD碼和信號(hào)發(fā)生器的顯示對(duì)比確認(rèn)秒、分、時(shí)、日都對(duì)得上。4.2 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因排查方向狀態(tài)機(jī)卡在IDLE沒(méi)檢測(cè)到P碼元查P碼元閾值是否過(guò)嚴(yán)示波器看8ms碼元是否達(dá)標(biāo)幀同步頻繁丟失碼元誤判檢查判決窗口看是否有碼元落在過(guò)渡區(qū)時(shí)間跳變BCD位權(quán)映射錯(cuò)誤對(duì)照標(biāo)準(zhǔn)表逐位核對(duì)sym_idx映射秒脈沖抖動(dòng)幀起始檢測(cè)不穩(wěn)確認(rèn)Pr碼元識(shí)別是否唯一加校驗(yàn)邏輯數(shù)據(jù)偶爾錯(cuò)一位亞穩(wěn)態(tài)檢查輸入打拍是否足夠IO約束是否加了時(shí)序4.3 幾個(gè)讓我印象深刻的坑坑一P碼元閾值設(shè)太窄。有次現(xiàn)場(chǎng)信號(hào)經(jīng)過(guò)長(zhǎng)電纜后8ms碼元被展寬到8.5ms我的閾值上限是9ms勉強(qiáng)能過(guò)但偶爾會(huì)飄到9.2ms就判錯(cuò)了。后來(lái)把P的窗口放寬到7ms~10ms問(wèn)題消失。閾值要按最壞情況留余量不能按理論值卡死??佣浱幚韼r?yàn)。IRIG-B幀末尾有校驗(yàn)位我一開(kāi)始沒(méi)做校驗(yàn)結(jié)果偶爾收到干擾脈沖導(dǎo)致整幀數(shù)據(jù)錯(cuò)亂。加上校驗(yàn)后錯(cuò)誤幀直接丟棄等下一幀重新同步穩(wěn)定性提升明顯??尤鐣r(shí)鐘域。如果下游模塊用的是另一個(gè)時(shí)鐘域BCD數(shù)據(jù)直接送過(guò)去會(huì)有亞穩(wěn)態(tài)。我加了一級(jí)握手信號(hào)用frame_valid做同步數(shù)據(jù)先鎖存再跨域問(wèn)題解決。獨(dú)家技巧調(diào)試時(shí)可以在ILA里加一個(gè)碼元類(lèi)型直方圖計(jì)數(shù)器統(tǒng)計(jì)一段時(shí)間內(nèi)0、1、P各出現(xiàn)了多少次。正常情況下比例應(yīng)該穩(wěn)定如果P的數(shù)量突然增多說(shuō)明有干擾脈沖被誤判成P這時(shí)候就要去查前端信號(hào)質(zhì)量了。4.4 性能與資源占用在Xilinx Artix-7上綜合這個(gè)解碼模塊的資源占用大致是資源用量說(shuō)明LUT約180主要是狀態(tài)機(jī)和比較邏輯FF約120計(jì)數(shù)器、寄存器DSP0沒(méi)用乘法器BRAM0沒(méi)用塊RAM時(shí)鐘約束到100MHz毫無(wú)壓力時(shí)序余量充足。如果資源緊張可以把32位計(jì)數(shù)器縮到24位100MHz下1ms才10000024位夠用能省一些FF。5. 從解碼到授時(shí)的延伸思考5.1 秒脈沖的精度優(yōu)化IRIG-B解碼出來(lái)的秒脈沖精度取決于Pr碼元的檢測(cè)時(shí)刻。理論上Pr的上升沿就是整秒起點(diǎn)但實(shí)際信號(hào)有傳輸延遲和邊沿抖動(dòng)。我做過(guò)一個(gè)優(yōu)化用系統(tǒng)時(shí)鐘對(duì)Pr上升沿做多周期采樣取平均值作為秒脈沖輸出時(shí)刻抖動(dòng)從原來(lái)的幾十納秒降到幾納秒。方法不復(fù)雜就是在Pr檢測(cè)后開(kāi)一個(gè)短窗口記錄邊沿到達(dá)的時(shí)鐘數(shù)多次測(cè)量取中值。5.2 多路IRIG-B的冗余切換電力系統(tǒng)里經(jīng)常要求雙路IRIG-B輸入做冗余。我的做法是兩路解碼模塊并行運(yùn)行各自輸出frame_valid和pps再用一個(gè)仲裁模塊選擇質(zhì)量更好的一路。仲裁依據(jù)包括幀校驗(yàn)是否通過(guò)、連續(xù)有效幀計(jì)數(shù)、秒脈沖間隔是否穩(wěn)定。切換時(shí)要做無(wú)擾切換保證時(shí)間不跳變。5.3 這套代碼還能怎么改如果你用的是其他時(shí)鐘頻率改CLK_FREQ參數(shù)就行。如果要支持IRIG-B的其他格式比如帶年份的擴(kuò)展格式在BCD組裝部分增加對(duì)應(yīng)的碼元映射即可。如果要做成AXI總線(xiàn)接口掛到處理器上加一層AXI-Lite從機(jī)封裝把BCD寄存器映射到地址空間處理器直接讀。我個(gè)人在實(shí)際操作中的體會(huì)是IRIG-B解碼本身不難難的是把邊界情況處理干凈。信號(hào)質(zhì)量差的時(shí)候怎么辦、幀同步丟了怎么快速恢復(fù)、多路冗余怎么無(wú)擾切換這些才是區(qū)分能跑和能用的地方。代碼貼出來(lái)只是起點(diǎn)真正讓它在現(xiàn)場(chǎng)穩(wěn)定運(yùn)行還得靠一遍遍的調(diào)試和打磨。