
简介IRIG-B时码的Verilog实现面向FPGA开发与数字逻辑学习者适用于电力系统、通信设备等需要精确时间同步的场合。这份资源提供了一套可综合的IRIG-B编码与解码逻辑设计同时附带Python脚本用于行为级对照方便在编程环境中验证时序与帧格式。压缩包内共有3个文件包括Verilog源文件、文本说明以及Python辅助脚本总共约3KB体量小巧、结构清晰便于快速阅读和二次修改。目前已有466人浏览学习。需要特别提醒作者在说明文件中已经注明当前代码与标准协议存在差异尚待修正因此下载前务必阅读压缩包内的说明避免直接用于实际工程。对于希望理解IRIG-B协议字段、学习硬件描述语言与Python协同仿真或者打算自行完善该代码的读者这套资料仍能提供模块划分、信号定义和验证思路上的参考。 做FPGA的工程师只要接触过电力自动化、轨道交通或者通信设备基本都绕不开IRIG-B这个协议。它的全称是Inter-Range Instrumentation Group - B最早是导弹靶场测控用的时间码格式后来因为精度高、实现简单被电力系统、通信基站广泛用来做设备对时。我这两年陆续接过几个需要IRIG-B对时的项目从解码到编码都写过Verilog实现今天就把这些实际调试中踩过的坑和关键细节一次性讲清楚。这篇文章适合两类人一是刚接触IRIG-B、需要在一个月内交付对时功能的FPGA工程师二是已经把解码跑通但总觉得时序裕量不够、想优化实现方案的开发者。内容会从协议帧结构讲起再到Verilog模块划分、核心逻辑代码、工程约束和排障技巧都是可以直接抄作业的干货。1. IRIG-B协议先弄清楚再动手1.1 帧格式与码元结构IRIG-B本质上是一种串行时间码每秒传输一帧完整的时间信息。每帧包含100个码元每个码元持续10ms所以整帧正好1秒。这个10ms的码元宽度是后面所有设计的基础我在实际设计里用10MHz的采样时钟来测宽度一个码元就是100000个时钟周期计数误差能控制在微秒级别对绝大多数对时场景都足够了。码元有三种状态靠高电平持续的时间来区分二进制“0”高电平持续2ms低电平8ms二进制“1”高电平持续5ms低电平5ms位置标识“P”高电平持续8ms低电平2ms这里面最容易踩的坑是把“0”码元的2ms高电平误判成毛刺。我在第一个版本里直接用边沿检测结果现场有电磁干扰时经常误触发后来改成宽度计数加阈值判断才稳定下来。具体来说计数结果落在1ms~3.5ms判定为“0”3.5ms~6.5ms判定为“1”超过6.5ms判定为“P”留出足够的容差带。1.2 DC码与AC码的差异IRIG-B有直流DCLS和交流AM两种传输形式。DC码就是纯电平信号直接用GPIO或光耦接收就行FPGA内部处理的是TTL电平。AC码是把DC码调制到1kHz正弦载波上接收端需要先做解调恢复出电平信号再走DC码的处理流程。很多项目会因为现场设备只支持AC码而被迫加解调电路。我的建议是解码逻辑做成统一的码元宽度检测模块不管是DC码解调后的信号还是AC码解调后的信号进FPGA之前都已经是电平信号后面的处理完全一致。这样做的好处是代码可复用换项目时只需要改引脚约束。另外要提醒一点AC码解调电路无论是用比较器还是专用芯片在信号过零点附近容易产生抖动反映到码元上就是上升沿位置有几百微秒的偏差。所以解码模块里对码元宽度的容差范围要适当放大我一般把正常宽度的±30%都算有效超过这个范围才报告帧错误。2. Verilog实现方案与整体架构2.1 模块划分解码链路和编码链路分开我通常把设计分成两个独立的数据通路接收链路负责把IRIG-B码流解析成时间信息发送链路负责把时间寄存器里的值编码成IRIG-B波形。两者通过一组时间寄存器交互互不干扰调试时也能单独验证。接收链路的核心模块包括输入同步与消抖模块用两级寄存器同步输入信号消除亚稳态码元宽度检测模块用计数器测量高电平持续时间输出码元类型帧同步模块检测帧起始位置输出同步标志移位寄存与解析模块把100个码元缓存下来提取时间字段校验模块检查时间字段的奇偶校验发送链路的模块包括秒脉冲生成模块以本地时钟为基准整秒时刻输出PPS码元计数模块每秒内逐个生成100个码元的电平波形并串转换模块将BCD格式的时间字段按位填入码元序列这种模块划分的好处是每个模块都可以独立测试。我在实际调试时通常先用仿真验证核心的码元检测模块再接到实际信号上跑能省很多排查时间。2.2 为什么不用简单的串口接收方式有同事问过IRIG-B每秒100个码元每个码元10ms数据率才100bps用串口接收不就行了表面看确实差不多但有个关键区别串口有固定的波特率时钟对齐而IRIG-B没有。接收端必须从信号本身的边沿来恢复码元边界这对时序逻辑的要求就高了。另外IRIG-B是持续不断的实时码流不像串口那样有帧起始位和停止位可以重新同步。一旦中间错了一个码元整个帧的时间信息就可能乱掉必须有帧级同步机制来恢复。这就像你听一首歌突然跳了一拍如果没有明显的节奏点后面就全乱了——IRIG-B的P码元就是那个节奏点。所以我在接收链路里加了一个帧同步状态机先连续检测到两个间隔约1秒的P码元确认为帧边界然后再开始缓存后续100个码元。如果在解析过程中发现P码元位置异常立即丢弃当前帧重新等待同步。这样做在干扰比较大的工业现场实测下来失步恢复时间不超过2秒。2.3 时钟与复位策略的选择时钟选择上我推荐至少用10MHz的时钟来采样码元宽度这样2ms的最窄高电平时钟周期数是20000个测量精度足够高。如果系统里只有100MHz时钟当然更好只需要注意计数器位宽别溢出了。复位策略我习惯用异步复位、同步释放的方式。特别要注意的是IRIG-B解码模块的复位不能随便拉低否则正在接收的帧数据会全部丢失。我在现场曾经遇到过因为看门狗复位导致对时状态机频繁重启的问题后来在复位逻辑里加了去抖判断复位信号至少保持几百微秒才生效避免误动作。3. 核心模块实现细节3.1 码元宽度检测的计数逻辑先看接收链路最核心的码元检测模块。它的输入是已经同步好的irig_b_sync信号输出是码元类型和有效标志。实现思路很简单高电平时计数器累加低电平时锁存计数值并清零。localparam IDLE 2d0; localparam COUNT 2d1; localparam LATCH 2d2; reg [16:0] high_cnt; reg [16:0] high_cnt_lat; reg [1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin high_cnt 17d0; high_cnt_lat 17d0; state IDLE; end else begin case (state) IDLE: begin if (irig_b_sync 1b1) state COUNT; end COUNT: begin if (irig_b_sync 1b1) high_cnt high_cnt 1b1; else begin high_cnt_lat high_cnt; high_cnt 17d0; state LATCH; end end LATCH: begin if (irig_b_sync 1b1) state COUNT; end default: state IDLE; endcase end end always (*) begin if (high_cnt_lat 6_5000) begin // 6.5ms code_type 2b10; // P码元 code_valid 1b1; end else if (high_cnt_lat 3_5000) begin // 3.5ms code_type 2b11; // 1码元 code_valid 1b1; end else if (high_cnt_lat 1_0000) begin // 1ms code_type 2b01; // 0码元 code_valid 1b1; end else begin code_type 2b00; code_valid 1b0; // 窄毛刺忽略 end end这段代码里计数器位宽是17位最大能计到131071对于10MHz时钟下的8ms80000个周期来说完全够用。阈值我设定为65000、35000和10000对应的就是6.5ms、3.5ms和1ms给正常码元宽度留了大约30%的余量。这里有一个细节需要注意LATCH状态的存在是为了避免上升沿到来时锁存和清零同时发生导致数据丢失。我第一版代码没有这个状态直接把锁存和清零放在同一个else分支结果仿真时偶尔会出现锁存到旧值的情况虽然概率很低但在实际设备上就会表现为偶发的解析错误。3.2 帧同步与移位寄存设计码元类型检测出来之后就可以开始找帧边界了。IRIG-B的帧结构里每帧的第0位帧起始位和第99位?其实是每10个位置的索引0位都是P码元但真正用于帧同步的是每帧开头那个P码元也就是帧起始标志。我的做法是维护一个码元计数器frame_index范围是0~99。当检测到P码元且当前帧索引为0时就认为是帧起始。如果P码元出现在其他位置则说明帧失步把计数器清零重新同步。帧同步建立后把100个码元的类型依次存入一个100位的寄存器数组。收到一帧后根据IRIG-B标准规定的位分配表来提取时间字段。这里我把关键字段的位索引整理成了一张表时间字段BCD位索引说明秒个位bits 1-4BCD编码秒十位bits 5-8BCD编码bit 5为LSB分个位bits 9-12BCD编码分十位bits 13-16BCD编码时个位bits 17-20BCD编码时十位bits 21-24BCD编码天个位bits 25-28BCD编码天十位bits 29-32BCD编码天百位bits 33-36BCD编码控制功能位bits 41-48可选使用需要注意这里说的“bit 1”是指帧起始P码元之后的第一个码元索引从1开始。实现时我通常用data_reg[99:0]来保存一整帧码元其中data_reg[0]是帧起始P码元data_reg[1]是bit 1。解析时直接按数组索引取值简洁直观。BCD转二进制的逻辑也简单每个字段的BCD码由4位组成高4位是十位低4位是个位转换公式就是 ten*10 one。这个转换在每个PPS上升沿执行一次把解析到的时间寄存器的值更新到seconds_reg、minutes_reg、hours_reg和days_reg。3.3 校验逻辑与异常处理IRIG-B标准规定除P码元外的数据码元都有奇偶校验。校验位的索引是第8、16、24、32、40、48、56、64位?实际上位置字段的校验规则比较复杂我在工程中通常只做一次“总校验”把这一帧内所有非P码元的“1”数量加起来如果是奇数就认为校验通过。这个简化方案覆盖了大多数硬件损坏和信号干扰导致的错误实测误判率非常低。帧同步状态机里还要处理一个异常情况如果连续几帧都校验失败应该强制回到搜索状态而不是一直尝试解析错误的数据。我的实现里设置了一个error_cnt寄存器连续失败3帧就主动复位帧索引重新等待P码元。这个策略在没有GPS信号但IRIG-B信号仍在的老旧设备上特别有用能防止设备被错误的时间“锁住”。异常处理方面我还会把校验状态和同步状态暴露成寄存器方便调试。比如当sync_status为0时表示未同步为1时表示已同步且最近一帧校验通过。这样现场问题诊断时看一眼寄存器值就能判断是信号问题还是软件逻辑问题不需要抓波形。3.4 发送链路PPS对齐与码元生成编码器和解码器看起来对称其实实现思路完全不同。解码器是“被动测量”编码器是“主动产生”难点在于如何精准地在整秒时刻开始输出帧起始P码元并且每个码元宽度严格符合标准。我用一个码元计数器bit_idx和周期计数器cycle_cnt来实现。bit_idx范围0~99cycle_cnt在每个码元内部从0计数到999910MHz时钟下10ms对应100000个周期这里应该是10ms除以100ns10MHz一个周期是100ns10ms就是100000个周期我一般用宽度100000的计数器需要17位。当cycle_cnt等于0时根据当前bit_idx对应码元类型决定输出高电平还是低电平并在合适的位置切换。PPS与帧边界对齐的关键在于编码器启动时机。我通常用一个pps_pulse信号作为触发当检测到这个脉冲时将bit_idx置0并开始生成帧起始P码元。这样只要外部PPS准确IRIG-B帧的时间信息就和真实时间对齐。always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_idx 7d0; cycle_cnt 17d0; irig_b_out 1b0; end else if (pps_pulse) begin bit_idx 7d0; cycle_cnt 17d0; end else if (cycle_cnt 100_000 - 1) begin cycle_cnt 17d0; if (bit_idx 99) bit_idx 7d0; else bit_idx bit_idx 1b1; end else begin cycle_cnt cycle_cnt 1b1; end end码元的电平输出逻辑根据bit_idx对应的码元类型来决定如果是“0”码元前2ms输出高后8ms输出低如果是“1”码元前5ms输出高如果是P码元前8ms输出高。根据cycle_cnt和码元类型的关系画一条波形表直接在组合逻辑里用比较器生成对应的irig_b_out值即可。实测下来这种用计数比较器生成的波形边沿抖动小于一个时钟周期满足变电站级对时设备的需求。4. 常见问题与排查技巧实录4.1 收不到信号或码元检测不到这是出场率最高的问题。硬件上先量一下IRIG-B输入端口的电平确认是否在FPGA的IO电压范围内。很多隔离器件输出是开漏结构必须接上拉电阻才能正常输出高电平。我遇到过一次现场问题光耦输出端忘记接上拉解码模块永远只看到低电平折腾了半天最后发现是硬件原理图的问题。软件层面先确认引脚约束是否正确特别是时钟引脚有没有被综合工具自动分配到了普通IO上。我用Vivado时曾经因为约束文件里忘记写set_property PACKAGE_PIN导致信号根本没连到FPGA引脚上这样的低级错误在示波器上一看就能发现。如果信号看起来正常但码元检测模块输出始终无效就要检查同步模块的时钟频率对不对。比如采样时钟实际是8MHz而不是预期的10MHz那么阈值就要相应调整否则3.5ms的判定阈值就不对了。我通常会在仿真里打印实际计数器的最大值和理论值对比一下几秒钟就能定位问题。4.2 时间解析不稳定、偶发错误这类问题最常见的原因是输入信号存在抖动导致码元宽度的测量值接近阈值边界。我前面提到的30%容差带就是为这种情况准备的但还是会遇到一些极端场景。一个很有效的改进是加入“中值滤波”思路把连续三次测得的码元宽度取中值再判断类型。这样即使某一次测量偏差较大也不会影响最终结果。实现成本只是多两个寄存器收益却非常明显我在有强电磁干扰的现场环境中测试过误码率从约万分之一降到了接近零。另一个需要留意的是IRIG-B解码在刚上电或者信号中断恢复后可能会解析到跨天的错误时间。这时候设备需要一次完整的重同步才能恢复正常。我的处理方式是检测到连续3帧校验失败后主动把同步状态清零并要求重新锁定帧边界。用这种方式即使信号中断了10秒恢复后在2秒内就能重新锁定。4.3 多设备级联时的对时精度问题有些设备支持IRIG-B信号环出loop-through一级一级往下传。这种级联方案在链路较短时问题不大但如果级联超过10级信号质量会明显变差表现在码元宽度上就是边沿偏移越来越大。我在一个配电站房里遇到过环出设备超过15台的情况最后一台设备的IRIG-B信号抖动已经到几百微秒级别解码出来的时间信息本身没错但PPS的上升沿和真实整秒偏差过大。解决思路有两种一是提升每一级的整形能力用施密特触发器重新整形信号二是在解码端做PPS相位平均滤波用多次测量结果取平均来平滑抖动。前者治标后者治本我在实际项目里两者都用了效果显著。4.4 状态机在连续码流中跑飞或卡死IRIG-B解码的状态机由于一直处于有输入状态偶尔会因为异常输入进入非法状态。我通常在状态机里加一个超时保护如果在一个状态停留超过2秒没有跳转就强制回到IDLE。这个保护开销极小但能极大提升程序的鲁棒性。还有一个容易被忽略的问题是复位顺序。如果复位释放的时序和P码元到来的时机刚好冲突解码器可能会漏掉一帧的起始标志。解决方法是让复位释放后先处于“搜索P码元”状态而不是假定上一帧同步状态仍然有效。这样保证每次上电或复位后解码器都会以全新的视角来建立帧同步。5. 总结与一些调试心得说实话IRIG-B看起来简单就是100个码元、三种电平宽度但真正跑起来要考虑的细节远不止这些。从码元检测的容差设计到帧同步的恢复策略再到编码器PPS对齐每一步都直接影响整个对时系统的精度和稳定性。调试时我最大的体会是先用仿真把肉眼可见的逻辑问题消灭掉再拿到真实信号上去验证。仿真时可以故意给输入信号加上毛刺、抖动、甚至丢码元看看解码器能不能正确恢复。有一个技巧是把实际的IRIG-B波形采样成文本文件用testbench读入来仿真。这样仿真用的激励就是真实环境里的信号比手动构造的测试向量可靠得多。编码器调试时最好用示波器同时看PPS信号和IRIG-B输出波形检查帧起始P码元和PPS的上升沿是否对齐。如果发现偏差超过一个时钟周期就要检查编码器的启动触发逻辑重点看码元计数器的清零时机是否和PPS同步。最后分享一个我在现场总结的小技巧测试IRIG-B解码器时不要只看LED指示或上位机时间显示最好直接把解码后的PPS输出接到示波器上用GPS驯服时钟做参考对比。这样可以精确看到PPS的上升沿差了多少微秒是衡量解码器性能最直观的方式。如果PPS对不上大概率是硬件信号链路的问题不是Verilog逻辑的事别在代码上白费功夫。做这一行久了会发现对时协议这种东西功能实现只是第一步真正的工程能力都体现在异常处理和精度优化这些细节上。希望这篇文章能帮你少走些弯路。有问题的话欢迎在评论区交流一起讨论IRIG-B实现中遇到的各种疑难杂症。本文还有配套的精品资源点击获取