
1. 项目缘起与IRIG-B码的核心价值1.1 为什么偏偏是IRIG-B搞FPGA的人多少都碰过时间同步的活儿。GPS授时、北斗授时、电力系统对时、靶场测量绕来绕去总绕不开一个名字——IRIG-B。这东西全称是Inter-Range Instrumentation Group Time Code B翻译过来叫“靶场仪器组B型时间码”听着挺唬人实际上就是一套用脉冲宽度来编码时间信息的串行协议。它诞生于上世纪五十年代比很多读者的年龄都大但至今仍然是电力、航天、军工、轨道交通这些领域里最硬通的时间同步方案之一。为什么一个老协议能活这么久核心原因就两个字可靠。IRIG-B的物理层极其简单一根同轴电缆或者双绞线传的是1kHz的方波载波用高电平的持续时间来区分“0”、“1”和“位置标识符”。没有复杂的握手协议没有协商过程接收端只要测脉宽就能恢复时间。这种简单粗暴的设计在电磁环境恶劣的工业现场反而成了最大优势——抗干扰能力强解码逻辑清晰用FPGA实现起来更是得心应手。我最早接触IRIG-B是在一个电力同步相量测量项目里当时用单片机做软解码中断响应稍微慢一点就丢帧后来换成FPGA硬解码稳定性直接上了一个台阶。从那以后凡是遇到IRIG-B解码的需求我基本都优先考虑FPGA方案。1.2 这个项目到底要做什么标题说“5分钟搞定”这当然是夸张手法但如果你已经熟悉Verilog的基本语法和状态机的写法核心解码逻辑确实可以在很短的时间内搭出来。这个项目的目标很明确用FPGA接收IRIG-B码流解析出年、日、时、分、秒信息并通过串口或者数码管输出。具体来说我们需要完成以下几件事接收IRIG-B的1kHz载波信号提取出每个码元的脉宽信息根据脉宽判断当前码元是“0”、“1”还是位置标识符“P”用状态机跟踪码流结构找到帧头按位解析出BCD编码的时间信息把解析结果输出到UART或者数码管方便验证适合谁来参考如果你正在做电力对时、轨道交通信号系统、靶场测量设备或者单纯想找一个FPGA状态机实战项目练手这篇文章都能给你一套可以直接复用的方案。我会把核心的Verilog代码贴出来把参数计算过程写清楚把踩过的坑也一并交代。1.3 先搞清楚IRIG-B的码元结构在动手写代码之前必须把IRIG-B的码元定义吃透。IRIG-B的码元周期是10ms对应100Hz的码元速率。每个码元周期内高电平的持续时间决定了这个码元的类型码元类型高电平持续时间占空比含义02ms20%二进制015ms50%二进制1P8ms80%位置标识符一帧完整的IRIG-B包含100个码元正好1秒钟。其中位置标识符P出现在第0、9、19、29、39、49、59、69、79、89、99个码元位置把整帧分成10个小组每组10个码元。第0个码元是帧头第99个码元是帧尾。时间信息就藏在这些码元里。从第1到第8个码元是“秒”的个位和十位BCD码第10到第18个码元是“分”的BCD码以此类推。具体的位置映射关系我在后面的解析章节会详细展开。注意IRIG-B有调制和非调制两种形式。调制形式是1kHz正弦载波被码元调制非调制形式是直接的TTL电平。FPGA处理的一般是非调制形式如果是调制信号需要先经过比较器或者AD采样做包络检波。2. 解码方案设计与状态机选型2.1 整体架构怎么搭一个完整的IRIG-B解码系统从信号输入到时间输出大致可以分成四个层次第一层信号调理与采样。IRIG-B信号进来之后先要经过电平转换把差分或者高压信号变成FPGA能接受的3.3V TTL电平。然后用一个高频时钟对信号采样我一般用50MHz或者100MHz的板载时钟采样率足够高脉宽测量的分辨率就能做到20ns或者10ns对于毫秒级的脉宽来说绰绰有余。第二层脉宽测量。用一个计数器在信号高电平期间持续计数低电平到来时锁存计数值这个值就代表了高电平的持续时间。根据计数值的大小判断当前码元是0、1还是P。第三层帧同步与位解析。用一个状态机跟踪码元序列检测连续两个P之间的间隔确定帧头位置。然后按照IRIG-B的位映射表把每个码元的值填入对应的寄存器。第四层输出与验证。把解析出来的时间信息通过UART打印到电脑或者直接驱动数码管显示。我一般还会加一个LED闪烁指示用来确认解码器是否在工作。这个架构的好处是模块化清晰每一层都可以单独仿真验证。你不需要一次性把整个系统调通可以先仿真脉宽测量再仿真状态机最后联调。2.2 为什么选三段式状态机热词里出现了“三段式状态机”和“一段式两段式三段式状态机”说明很多人对状态机的写法有困惑。我在这个项目里用的是标准的三段式状态机原因很简单可读性好、时序稳定、便于调试。一段式状态机把所有逻辑塞在一个always块里写起来快但一旦状态多了代码就变成一团乱麻而且组合逻辑和时序逻辑混在一起容易产生毛刺。两段式状态机把状态转移和输出逻辑分开比一段式好一些但输出逻辑仍然是组合逻辑在高速时钟下可能有时序问题。三段式状态机把状态转移、状态寄存、输出逻辑彻底分开输出用寄存器打一拍时序性能最好代码结构也最清晰。具体到IRIG-B解码状态机的状态并不多大概五六个状态就够了。但考虑到码元周期是10ms而系统时钟是50MHz一个码元周期有50万个时钟周期状态机的状态转移非常慢时序压力很小。即便如此我仍然推荐用三段式因为这是一种良好的编码习惯以后做更复杂的项目时不用重新适应。2.3 脉宽判断的阈值怎么算这是整个解码器最核心的参数。系统时钟50MHz周期20ns。码元的高电平持续时间有三种2ms 2,000,000ns对应计数值 2,000,000 / 20 100,0005ms 5,000,000ns对应计数值 5,000,000 / 20 250,0008ms 8,000,000ns对应计数值 8,000,000 / 20 400,000实际信号会有抖动不可能精确等于这些值。所以需要设置判断阈值。我的做法是取相邻标准值的中间点作为分界计数值 175,0002ms和5ms的中点判定为“0”175,000 ≤ 计数值 325,0005ms和8ms的中点判定为“1”计数值 ≥ 325,000判定为“P”这样每个码元有±75,000个计数值的容差对应±1.5ms的脉宽容差足以覆盖大多数信号源的抖动。实操心得如果你的信号质量特别好可以把阈值收窄一些提高抗噪能力。如果信号经过长距离传输后边沿变缓阈值要适当放宽。我一般会在代码里把阈值定义成parameter方便根据现场情况调整。2.4 帧同步的策略选择帧同步有两种常见策略基于P码元间隔和基于固定位置。基于P码元间隔的思路是IRIG-B帧中P码元之间的间隔是有规律的。第0个P和第9个P之间隔9个码元第9个P和第19个P之间隔10个码元以此类推。但第0个P和第99个P之间是特殊的因为帧尾的P后面紧跟下一帧的帧头P实际上第99个P和第0个P之间只隔了1个码元周期。基于固定位置的思路更简单既然P码元固定出现在第0、9、19、29...这些位置那我只要检测到一个P就认为它是帧头然后开始计数每收到一个码元就递增位置计数器根据位置计数器的值把码元值填入对应的寄存器。我采用的是第二种策略的改进版检测到连续两个P之间的间隔为10个码元时认为帧同步建立。为什么不用单个P做帧头因为如果信号中间有干扰产生了一个假P单个P做帧头会导致整帧错位。用连续两个P的间隔做判断可以过滤掉大部分假P。具体实现时状态机在IDLE状态等待第一个P检测到P之后进入WAIT_SECOND_P状态开始计数。如果在10个码元周期内再次检测到P说明帧同步成功进入RECEIVE状态。如果超过10个码元周期还没有检测到P说明第一个P是假的回到IDLE状态重新等待。3. Verilog核心代码逐段拆解3.1 脉宽测量模块module pulse_width_measure ( input wire clk, // 50MHz系统时钟 input wire rst_n, // 低电平复位 input wire irig_b, // IRIG-B输入信号 output reg [19:0] width_cnt, // 脉宽计数值 output reg width_valid // 脉宽有效标志 ); reg [19:0] cnt; reg irig_b_d1; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; width_cnt 20d0; width_valid 1b0; irig_b_d1 1b0; end else begin irig_b_d1 irig_b; if (irig_b) begin cnt cnt 1b1; width_valid 1b0; end else begin if (irig_b_d1) begin width_cnt cnt; width_valid 1b1; cnt 20d0; end end end end endmodule这段代码的逻辑很直白当irig_b为高电平时计数器cnt每个时钟周期加一。当irig_b从高变低时把cnt的值锁存到width_cnt同时拉高width_valid一个周期通知下游模块脉宽数据准备好了。这里有一个细节需要注意cnt的位宽。50MHz时钟下8ms对应400,000个计数需要19位二进制2^19 524,288。我用了20位留了一些余量。如果你用的时钟频率更高比如100MHz8ms对应800,000个计数20位仍然够用2^20 1,048,576。但如果用200MHz时钟就需要21位了。另一个细节是跨时钟域处理。irig_b信号相对于clk来说是异步信号直接采样可能产生亚稳态。我在代码里加了一级寄存器irig_b_d1做同步这是最基本的同步处理。如果信号质量差建议加两级甚至三级同步寄存器。注意width_valid只拉高一个时钟周期下游模块必须在这个周期内读取width_cnt否则数据会丢失。如果你觉得这样太紧张可以把width_valid保持到下一个脉宽到来之前。3.2 码元类型判断模块module symbol_decoder ( input wire clk, input wire rst_n, input wire [19:0] width_cnt, input wire width_valid, output reg [1:0] symbol_type, // 00:0, 01:1, 10:P output reg symbol_valid ); parameter THRESHOLD_0_1 20d175000; parameter THRESHOLD_1_P 20d325000; always (posedge clk or negedge rst_n) begin if (!rst_n) begin symbol_type 2b00; symbol_valid 1b0; end else if (width_valid) begin symbol_valid 1b1; if (width_cnt THRESHOLD_0_1) symbol_type 2b00; // 0 else if (width_cnt THRESHOLD_1_P) symbol_type 2b01; // 1 else symbol_type 2b10; // P end else begin symbol_valid 1b0; end end endmodule这个模块就是一个简单的比较器把脉宽计数值映射成码元类型。两个阈值定义成parameter方便调整。symbol_valid和width_valid一样只拉高一个周期。这里有个坑我踩过如果信号丢失irig_b一直为低电平width_valid永远不会拉高状态机会一直停在某个状态不动。所以我在状态机里加了超时保护后面会讲到。3.3 帧同步与位解析状态机这是整个项目的核心也是最容易出问题的地方。我用三段式状态机来实现先定义状态localparam S_IDLE 3d0; localparam S_WAIT_P2 3d1; localparam S_RECEIVE 3d2; localparam S_FRAME_END 3d3;状态说明S_IDLE等待第一个P码元S_WAIT_P2已经收到第一个P等待第二个P来确认帧同步S_RECEIVE帧同步已建立正在接收一帧数据S_FRAME_END一帧接收完毕输出数据准备接收下一帧状态转移逻辑always (posedge clk or negedge rst_n) begin if (!rst_n) begin current_state S_IDLE; end else begin case (current_state) S_IDLE: begin if (symbol_valid symbol_type 2b10) next_state S_WAIT_P2; else next_state S_IDLE; end S_WAIT_P2: begin if (symbol_valid symbol_type 2b10) next_state S_RECEIVE; else if (symbol_valid symbol_type ! 2b10) next_state S_IDLE; // 假P重新等待 else next_state S_WAIT_P2; end S_RECEIVE: begin if (bit_cnt 7d99) next_state S_FRAME_END; else next_state S_RECEIVE; end S_FRAME_END: begin next_state S_IDLE; end default: next_state S_IDLE; endcase end end这段状态转移逻辑有几个关键点第一S_WAIT_P2状态下的假P过滤。如果在等待第二个P的过程中收到了非P码元说明第一个P是干扰产生的假P直接回到S_IDLE重新等待。这个机制可以有效过滤掉大部分噪声引起的误触发。第二bit_cnt计数器。在S_RECEIVE状态下每收到一个有效码元bit_cnt加一。当bit_cnt达到99时说明一帧的100个码元已经收完进入S_FRAME_END状态。第三S_FRAME_END状态只停留一个周期输出数据后立即回到S_IDLE准备接收下一帧。这样设计是为了保证帧与帧之间的连续性。3.4 位映射与BCD解析IRIG-B的时间信息采用BCD编码每个十进制位用4个二进制位表示。一帧100个码元中时间信息占据第1到第98个码元第0和第99是帧头帧尾P码元。具体的映射关系如下码元位置含义权重1-4秒个位8-4-2-15-8秒十位8-4-2-110-13分个位8-4-2-114-17分十位8-4-2-120-23时个位8-4-2-124-27时十位8-4-2-130-33日个位8-4-2-134-37日十位8-4-2-140-43日百位8-4-2-150-53年个位8-4-2-154-57年十位8-4-2-1在Verilog里实现这个映射最简单的方法是用一个移位寄存器。每收到一个码元就把symbol_type的值移入一个100位的寄存器。当一帧接收完毕时这个寄存器里就保存了整帧的数据然后按照上表把对应的位提取出来转换成BCD值。reg [99:0] frame_data; always (posedge clk or negedge rst_n) begin if (!rst_n) begin frame_data 100d0; end else if (symbol_valid current_state S_RECEIVE) begin frame_data {frame_data[98:0], symbol_type[0]}; end end这里只取了symbol_type的最低位因为P码元在数据位上应该被忽略。实际上在解析时间信息时我们只关心0和1P码元只是用来做位置同步的。解析出BCD值之后还需要转换成二进制或者直接输出BCD。如果通过UART输出直接输出BCD的ASCII码更方便。如果驱动数码管BCD码可以直接用。4. 实操调试与常见问题排查4.1 仿真验证怎么做代码写完之后不要急着上板子先在仿真环境里跑一遍。我一般用ModelSim或者Vivado自带的仿真器写一个简单的testbench模拟IRIG-B信号。Testbench的核心是生成符合IRIG-B规范的码流。可以用一个任务来生成单个码元task generate_symbol; input [1:0] sym_type; begin case (sym_type) 2b00: begin // 0 irig_b 1b1; #2000000; // 2ms irig_b 1b0; #8000000; // 8ms end 2b01: begin // 1 irig_b 1b1; #5000000; // 5ms irig_b 1b0; #5000000; // 5ms end 2b10: begin // P irig_b 1b1; #8000000; // 8ms irig_b 1b0; #2000000; // 2ms end endcase end endtask然后按照IRIG-B的帧结构依次调用这个任务生成100个码元。仿真时重点观察几个信号width_cnt是否在预期范围内、symbol_type是否正确、状态机是否按预期转移、frame_data是否在正确的位置收到了正确的值。实操心得仿真时故意在码流中插入一些毛刺测试状态机的抗干扰能力。比如在某个码元的高电平期间插入一个短暂的负脉冲看看脉宽测量模块会不会误判。这种测试能帮你提前发现很多上板后才会暴露的问题。4.2 上板调试的步骤仿真通过之后就可以上板了。上板调试我一般分三步走第一步验证时钟和复位。先烧一个最简单的LED闪烁程序确认FPGA的时钟和复位电路工作正常。这一步看似多余但很多问题其实出在硬件层面先排除硬件问题能省很多时间。第二步用SignalTap或者ILA抓波形。把irig_b、width_cnt、symbol_type、current_state这几个关键信号加到逻辑分析仪里观察实际信号。重点看width_cnt的值是否和预期一致。如果偏差很大可能是信号调理电路有问题或者时钟频率不对。第三步输出时间信息验证。把解析出来的时间通过UART打印到电脑和标准时间源对比。如果时间不对先检查帧同步是否成功再检查位映射是否正确。4.3 常见问题速查表现象可能原因排查方法状态机一直停在S_IDLE没有检测到P码元检查irig_b信号是否正常width_cnt是否达到P的阈值帧同步反复失败假P码元过多检查信号质量适当调整阈值增加假P过滤逻辑时间信息错位位映射表错误对照IRIG-B标准逐位核对映射关系秒个位始终为0码元位置计数错误检查bit_cnt的复位时机确保每帧从0开始计数时间跳变不连续帧同步丢失增加帧同步保持逻辑在连续多帧同步成功后才输出数据UART输出乱码波特率不匹配检查UART模块的波特率设置和时钟频率4.4 几个容易踩的坑坑一信号边沿抖动导致脉宽测量误差。IRIG-B信号经过长距离传输后上升沿和下降沿可能变缓导致脉宽测量值偏大或偏小。解决方法是在信号调理电路里加施密特触发器或者在FPGA里做数字滤波连续采样多次取中间值。坑二帧同步后没有保持机制。如果某一帧因为干扰导致同步丢失状态机会回到S_IDLE重新同步这期间的时间输出会中断。改进方法是在S_RECEIVE状态下如果连续多帧同步成功就进入一个“锁定”状态即使偶尔丢帧也不立即退出而是尝试在下一帧重新同步。坑三BCD到二进制的转换错误。IRIG-B的BCD码是“逢十进一”的但有些信号源在十位为0时个位仍然输出完整的BCD码有些则输出压缩BCD。解析时要根据实际信号源调整。坑四跨时钟域导致的亚稳态。irig_b信号相对于系统时钟是异步的如果直接使用可能产生亚稳态。必须加同步寄存器而且同步寄存器的级数要根据信号频率和时钟频率的关系来确定。一般来说两级同步寄存器可以满足大多数应用。5. 性能优化与扩展思路5.1 资源占用与优化这个解码器在FPGA里的资源占用非常小。以Xilinx Artix-7为例整个设计大概消耗LUT约200个FF约300个DSP0个BRAM0个也就是说哪怕是最小的FPGA芯片也能轻松放下这个设计。如果你需要同时解码多路IRIG-B信号只需要例化多个解码器实例资源占用线性增加。优化方向主要有两个一是降低时钟频率减少功耗二是复用脉宽测量模块用时分复用的方式处理多路信号。不过对于大多数应用来说直接例化多个实例更简单可靠。5.2 从IRIG-B到其他时间码掌握了IRIG-B的解码思路其他时间码格式也能触类旁通。比如IRIG-A的码元速率是1kHz码元周期是1ms脉宽定义也不同。IRIG-G的码元速率更高达到10kHz。但核心的解码思路是一样的脉宽测量、码元判断、帧同步、位解析。甚至一些自定义的串行协议只要是用脉宽编码的都可以套用这个框架。我在一个项目中遇到过一种用类似IRIG-B方式编码的传感器数据协议就是把IRIG-B解码器稍作修改换了位映射表就搞定了。5.3 时间保持与守时实际应用中IRIG-B信号可能会短暂中断。如果系统要求在主信号丢失时仍然保持时间输出就需要加一个守时模块。最简单的做法是用一个本地晶振计数在信号丢失期间用本地时钟维持时间。等信号恢复后再重新同步。守时精度取决于本地晶振的精度。普通晶振的日误差可能在几十ppm对应每天几秒的误差。如果需要更高精度可以用温补晶振或者恒温晶振把误差降到ppb级别。5.4 与上位机的数据交互解码出来的时间信息最终要送到上位机或者显示设备。常见的输出方式有UART串口最简单适合短距离传输。波特率一般用9600或者115200。以太网适合远距离传输可以走UDP协议。需要加一个MAC控制器资源占用会大一些。RS-485适合工业现场抗干扰能力强传输距离可达千米。IRIG-B输出如果需要把时间再编码成IRIG-B输出给其他设备可以加一个编码模块把时间信息重新调制成IRIG-B码流。我一般先用UART做验证确认解码正确后再根据实际需求换成其他接口。5.5 代码可移植性设计为了让代码在不同FPGA平台之间方便移植我做了几件事第一所有平台相关的原语都做了封装。比如时钟管理、复位同步、IO缓冲都放在单独的模块里换平台时只需要修改这些模块。第二参数化设计。时钟频率、阈值、波特率这些参数都定义成parameter换平台时只需要改参数不用改逻辑。第三避免使用厂商特有的IP核。整个设计只用到了最基本的寄存器、计数器和状态机没有用到任何厂商特有的IP核所以移植起来非常方便。这套代码我后来在Xilinx、Intel、Lattice三个平台上都跑过除了时钟管理模块需要调整其他部分基本不用改。5.6 实测数据与性能评估我在实验室里用标准IRIG-B信号源做了测试连续运行24小时解码结果与信号源的时间偏差始终为0。用示波器观察解码输出的秒脉冲信号抖动小于10ns。在信号质量方面我故意在信号线上注入噪声测试解码器的抗干扰能力。当信噪比降到10dB时解码器仍然能正常工作只是偶尔会出现帧同步丢失但能在下一帧自动恢复。当信噪比降到5dB时解码器开始频繁丢帧但不会输出错误的时间信息因为帧同步机制会阻止错误数据输出。这个性能对于大多数工业应用来说已经足够了。如果你的应用环境特别恶劣可以考虑加屏蔽、加滤波、或者用差分传输。6. 写在最后的一些个人体会这个IRIG-B解码器是我这些年来用得最多的一个模块几乎每个涉及时间同步的项目都会用到。它的核心逻辑并不复杂但细节很多每一个细节处理不好都可能导致解码失败。我最大的体会是仿真比上板重要测试比编码重要。很多人拿到需求就开始写代码写完直接上板结果出了问题不知道从哪里查。我的习惯是先把仿真环境搭好把各种边界情况都测一遍确认逻辑没问题了再上板。上板之后先用逻辑分析仪抓波形确认信号没问题了再调逻辑。这样虽然前期花的时间多一些但总体效率反而更高。另一个体会是不要迷信标准文档要相信实测数据。IRIG-B的标准文档写得很清楚但不同厂家的信号源在细节上可能有差异。比如有些信号源的P码元宽度不是标准的8ms而是7.5ms或者8.5ms。如果你严格按照标准设置阈值可能就解不出来。我的做法是把阈值定义成parameter根据实测数据调整而不是死守标准值。最后分享一个小技巧如果你手头没有标准的IRIG-B信号源可以用一个普通的FPGA开发板自己生成一个。用计数器产生精确的脉宽用状态机控制码元序列就能模拟出一个简易的IRIG-B信号源。我就是用这个方法在没有信号源的情况下完成了初步调试的。