ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

EtherCAT DC 分布式时钟同步的 ModelSim 仿真:从帧模板到从站行为建模

EtherCAT DC 分布式时钟同步的 ModelSim 仿真:从帧模板到从站行为建模 EtherCAT DC 分布式时钟同步的 ModelSim 仿真从帧模板到从站行为建模关键词EtherCAT、DC 分布式时钟、ESC、0x0918、0x0110 DL Status、ModelSim、Verilog testbench、行为级建模、线型拓扑适用人群正在做 EtherCAT 主站 FPGA 实现、需要验证 DC 同步流程的工程师开篇为什么 DC 同步必须靠仿真验证EtherCAT 的 DCDistributed Clock分布式时钟是精密运动控制的地基。多轴协同能做到亚微秒级同步靠的就是每个从站 ESC 里的本地时钟被主站反复校准。但 DC 同步有个特点它是一个长流程状态机。一次完整的 DC 初始化要经历广播写0x0900锁存时间戳读0x0918拿各从站接收时间戳算 offset广播写 offset读端口时间戳算传播延迟pdelay / 系统延迟写系统延迟 时钟漂移补偿周期性发 ARMW/FRMW 做运行期补偿每一步都要发不同的帧模板、等不同的应答、解析不同的寄存器。32 个从站的话这个流程要跑几千个时钟周期。上板调试的问题抓波形只能看到最后成没成中间哪一步卡住、是帧没发出去还是应答没回来、DL status 解析对不对——全都看不见。所以正确的做法是先搭一套能跑的仿真环境把状态机每一步都跑通再上板。本文以一个真实的 DC 同步模块ecat_sync_dc为例讲解配套 testbench 的搭建方法。核心是三件事怎么模拟发送crc32_tx_cnt字节计数器怎么模拟应答resp_ack_sync 200µs 超时怎么模拟从站时间戳 DL status含线型拓扑的末从站识别一、整体架构tb_ecat_sync_dc ├── sub1行为级模型 ← 64 位减法器替代 Altera LPM_ADD_SUB IP ├──ecat_frame_module ← 46 字节 EtherCAT 帧模板 ├── ecat_sync_dcDUT ← 被测模块DC 同步状态机 └── TB 模拟部分 ├── ecat_tx 模型 ← crc32_tx_cnt 发送字节计数74 拍/帧 ├── ecat_rx 模型 ← resp_ack_sync 应答 overtime 超时 └── 从站模型 ← esc_rec_time / esc_cap_time / DL status1.1 sub1四两拨千斤的 IP 替代//-------------------------------------------------------------------------------- // sub1 行为级模型 (64 位减法: result dataa - datab) // 替代 Altera LPM_ADD_SUB IP 核, 用于 offset / pdelay 计算 //-------------------------------------------------------------------------------- module sub1 ( input [63:0] dataa, input [63:0] datab, output [63:0] result ); assign result dataa - datab; endmodule这一句是整个 tb 能脱离 Quartus 独立运行的关键。DC 同步要算 64 位的时间差offset 参考时钟 - 本地时钟、pdelay 等RTL 里通常例化了 Altera 的LPM_ADD_SUB。仿真时如果直接拉这个 IP会遇到需要220model.v/altera_mf.v仿真库ModelSim 要配incdir和库映射第三方Icarus / Verilator根本跑不了解法同名模块sub1行为级一行实现。仿真时 tb 里的sub1覆盖了工程里的 IP 例化或者在仿真文件列表里排在前面。⚠️注意这只是仿真替身。综合上板时必须换回真正的 LPM IP否则 64 位减法直接用assign会综合出一根巨大的减法器链时序大概率不过。建议在文件头注释里写清楚或者用ifdef SIMULATION隔离。1.2 DUT 例化端口分组看得很清楚 | 方向 | 信号 | 含义 | |---|---|---| | 输入 | wkc_max | 从站个数本例 32 | | 输入 | crc32_tx_cnt | 发送字节计数帧发送进度 | | 输入 | resp_ack_sync | 帧应答从站回来了 | | 输入 | overtime_plus_sync | 200µs 超时脉冲 | | 输入 | esc_rec_time | 从站 0x0918 接收时间戳 | | 输入 | esc_cap_time | 端口时间戳 / DL status | | 输入 | ecat_module_data | 帧模板 | 输出 | sync_sm | 同步状态机当前状态 | | 输出 | ecat_module_adr | 帧模板地址 | | 输出 | sync_done | 同步完成标志 | **ecat_module_adr 是理解整个流程的钥匙**——DUT 通过它选不同的帧模板tb 只要监视这个地址就知道 DUT 当前在发什么帧、走到哪一步了。 ## 二、关键设计 1发送字节计数器74 拍一帧 这是 tb 里**最容易写错**的部分。必须严格对照真实以太网帧结构 verilog // // 模拟 ETH_DAT_TX ETH_TX: 发送字节计数器 crc32_tx_cnt // 完整以太网帧结构 (对照 ETH_TX.v / ETH_DAT_TX.v): // crc32_tx_cnt 1~7 : 前导码 0x55 (7 字节) // crc32_tx_cnt 8 : SFD 0xD5 // crc32_tx_cnt 9~14 : 目的 MAC (6 字节) // crc32_tx_cnt 15~20 : 源 MAC (6 字节) // crc32_tx_cnt 21~22 : EtherType (2 字节) ← ecat_sync 在 cnt22 加载 send_buf // crc32_tx_cnt 23~68 : EtherCAT 报文 (46 字节, 每拍左移 send_buf 输出 1 字节) // crc32_tx_cnt 69~72 : CRC32 (4 字节) // crc32_tx_cnt 73 : ETH_TX 停止写 RAM (tx_wea0) // 一帧共需 crc32_tx_cnt 从 0 递增到 73 (74 拍) // reg [8:0] tx_cnt; reg tx_active; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_cnt 9d0; tx_active 1b0; end else begin if (sync_respone_valid !tx_active) begin tx_active 1b1; // DUT 请求发帧 → 启动 tx_cnt 9d0; end else if (tx_active) begin if (tx_cnt 9d73) tx_cnt tx_cnt 1b1; else tx_active 1b0; // 一帧发完 end end end always (*) crc32_tx_cnt tx_active ? tx_cnt : 9d0;2.1 为什么是 74 拍7(前导) 1(SFD) 6(目的MAC) 6(源MAC) 2(EtherType) 46(EtherCAT报文) 4(CRC) 1(停止) 73 拍增量2.2 关键节拍点crc32_tx_cnt事件21~22EtherTypeDUT 在此加载frame_tx_buf23~68逐字节输出 46 字节 EtherCAT 报文73发送结束⚠️坑crc32_tx_cnt必须在非发送期间归零。原 tb 用always (*) crc32_tx_cnt tx_active ? tx_cnt : 9d0;处理了这点。如果忘了归零DUT 会误判一直在发帧状态机卡死。三、关键设计 2应答与超时模型reg [15:0] overtime_cnt; reg res_ack_pending; always (posedge clk or negedge rst_n) begin if (!rst_n) begin res_ack_sync 1b0; overtime_plus_sync 1b0; overtime_cnt 16d0; res_ack_pending 1b0; end else begin res_ack_sync 1b0; // 默认清零单拍脉冲 overtime_plus_sync 1b0; if (tx_active (tx_cnt 9d73)) begin // 帧发送完成, 开始等待应答 res_ack_pending 1b1; overtime_cnt 16d0; end else if (res_ack_pending) begin overtime_cnt overtime_cnt 1b1; // 模拟从站应答延迟 (约 2 拍) if (overtime_cnt 16d2) begin res_ack_sync 1b1; res_ack_pending 1b0; end else if (overtime_cnt 16d8000) begin // 200us 超时 (40MHz × 200µs 8000 拍) overtime_plus_sync 1b1; res_ack_pending 1b0; end end end end3.1 两个数字的含义数值换算说明2拍50ns模拟从站应答延迟实验室短距离几乎无延迟8000拍200µs200µs 超时门限200µs 这个数字不是随便定的——它是 EtherCAT 主站判断帧丢了的常用门限对应 40MHz 下 8000 个周期。3.2 怎么测超时场景把应答关掉即可// 测试超时注释掉应答分支只留超时分支 if (overtime_cnt 16d8000) begin overtime_plus_sync 1b1; resp_ack_pending 1b0; end或者做成可控的reg ack_enable; // 1正常应答, 0模拟丢帧 ... if (ack_enable (overtime_cnt 16d2)) begin resp_ack_sync 1b1; ... end四、关键设计 3从站行为建模本文核心这是整个 tb 最有技术含量的部分——怎么让 tb 扮演32 个从站。4.1 时间戳基础reg [63:0] slave_time_base; reg [31:0] port_time_base; always (posedge clk or negedge rst_n) begin if (!rst_n) begin slave_time_base 64d0; port_time_base 32d0; end else begin slave_time_base slave_time_base 64d25; // 从站时钟运行 (40MHz, 25ns/拍) port_time_base port_time_base 32d25; end end每拍 25ns模拟从站本地时钟自由运行。4.2 影子计数器tb 怎么知道我是第几个从站这是行为级建模的核心妥协。真实硬件里从站是根据自身物理位置返回 DL status 的——它知道自己上游有没有连接、下游有没有连接完全不依赖主站内部计数器。4.3 DL status 线型拓扑策略EtherCAT 常见的是线型拓扑daisy chain主站 → 从站1 → 从站2 → … → 从站N末端。主站 ──Port0──[从站1]──Port3──┬──Port0──[从站2]──Port3──┬─...─┬──Port0──[从站N] 链路 链路 (末从站) Port3 悬空DL status 寄存器 0x0110 的位定义16-bit位含义bit 0PDI Operationalbit 1DL User Watchdogbit 2Extended Link Detectionbit [7:4]物理链路 LINKPort0~Port31 link upbit [15:12]通信环路 LOOPPort0~Port31 在环路内活跃 LINK LOOP两个位都为 1 才算这个口真正通着。对应到 tbalways (*) begin if (reg918_rec_valid) esc_rec_time slave_time_base; // 从站 0x0918 时间戳 else esc_rec_time 64d0; if (rec_port_time_valid) esc_cap_time port_time_base; // 端口时间戳 else if (sync_aux_cnt 3d1) begin // DL status 0x0110: 末从站 Port3 不活跃 if (tb_slave_idx (wkc_nu - 1b1)) begin // 末从站: 仅 Port0 活跃 (bit[4]1, bit[9]1) // byte00x10, byte10x02 → port3_active0, is_port3_inactive1 esc_cap_time 32h0000_0210; end else begin // 非末从站: Port0 Port3 均活跃 // byte00x90(bit[4,7]1), byte10x82(bit[9,15]1) esc_cap_time 32h0000_8290; end end else esc_cap_time 32d0; end把这两个常量拆开看关键非末从站 0x0000_8290 0000_0000_0000_0000_1000_0010_1001_0000 (bin) │ │ │ │ bit15 ┘ bit9 ┘ │ └─ bit4 (Port0 LINK) (Port3 LOOP) └───── bit7 (Port3 LINK) → Port0: LINK1(bit4) LOOP1(bit9) ✓ 活跃 → Port3: LINK1(bit7) LOOP1(bit15) ✓ 活跃 末从站 0x0000_0210 0000_0000_0000_0000_0000_0010_0001_0000 (bin) │ └─ bit4 (Port0 LINK) bit9 ┘ → Port0: LINK1(bit4) LOOP1(bit9) ✓ 活跃 → Port3: LINK0(bit7) LOOP0(bit15) ✗ 不活跃末从站的 Port3 没有下游连接所以 LINK 和 LOOP 都是 0——这正是主站判断拓扑到头了的依据。tb_slave_idx 0 1 2 ... 30 31(末) esc_cap_time 8290 8290 8290 ... 8290 0210 ↑ 最后一拍突变 port3_active 1 1 1 ... 1 0截图要点跑完 32 个从站看esc_cap_time在前 31 个是0x8290、最后一个变成0x0210。这一拍的突变就是末从站识别逻辑被验证成功的标志。4.4 为什么末从站识别这么重要主站靠 DL status 判断拓扑结构用于确定环网是否闭合末从站 Port3 不活跃 线型Port3 活跃且连回主站 环型计算传播延迟时排除未连接的端口电缆冗余场景判断断线位置如果这段建模错了比如末从站也返回0x8290主站会认为拓扑没到头继续等待导致sync_done永远不出来——这是 DC 仿真最常见的跑不完原因之一。五、测试流程initial begin $display(); $display(TEST: my_ecat_sync (DC Synchronization)); $display(); test_cnt 0; pass_cnt 0; fail_cnt 0; // 初始化 rst_n 1b0; wkc_nu 7d32; // 32 个从站 crc32_tx_cnt 9d0; res_ack_sync 1b0; overtime_plus_sync 1b0; esc_rec_time 64d0; esc_cap_time 32d0; #100; rst_n 1b1; $display([Reset released]); // 等待 DC 同步完成或超时 sync_done_flag 1b0; timeout_flag 1b0; fork begin (posedge sync_done); sync_done_flag 1b1; $display([sync_done asserted at time%0t], $time); end begin #50_000_000; // 50ms 超时 timeout_flag 1b1; $display([WARNING: sync_done timeout]); end join // 检查结果 check(sync_done_flag, DC sync completed successfully); check(sync_error 1b0, no sync error); check(error_code 8d0, error code is 0); check(sync_rst_n 1b1, sync_rst_n released); $display(); $display(SUMMARY: %0d total, %0d passed, %0d failed, test_cnt, pass_cnt, fail_cnt); if (fail_cnt 0) $display( ALL TESTS PASSED ); else $display( %0d TESTS FAILED , fail_cnt); $display(); $finish; end5.1 这个 fork/join 有个隐患fork begin (posedge sync_done); sync_done_flag 1b1; end begin #50_000_000; timeout_flag 1b1; end joinfork...join会等两条分支都结束才继续。所以即使sync_done在第 5ms 就来了tb 也要白等到 50ms超时分支走完才继续。这不是功能 bug但很浪费仿真时间——32 从站的 DC 流程本来就长每次跑都白等 45ms。改进ModelSim 支持join_any的话fork begin (posedge sync_done); sync_done_flag 1b1; end begin #50_000_000; timeout_flag 1b1; end join_any // ← 任一分支结束就继续 disable fork; // 可选杀掉其余分支如果用的是 ModelSim 10.1b 等不支持join_any的老版本用 flag 轮询// 兼容写法超时计数器 标志轮询 timeout_cnt 0; while (!sync_done_flag (timeout_cnt 50_000)) begin (posedge clk); timeout_cnt timeout_cnt 1; end if (!sync_done_flag) $display([WARNING: sync_done timeout]);5.2 check 任务task check; input cond; input [255:0] msg; begin test_cnt test_cnt 1; if (cond) begin pass_cnt pass_cnt 1; $display( PASS: %0s, msg); end else begin fail_cnt fail_cnt 1; $display( FAIL: %0s (time%0t), msg, $time); end end endtask简单够用。建议在FAIL分支里多打几个关键信号的值方便定位$display( FAIL: %0s (time%0t) sm%0d idx%0d err%0d code%0d, msg, $time, sync_sm, ecat_index_sync, sync_error, error_code);六、仿真结果分析与截图指南6.1 波形 do 文件# wave_dc.do vlog defineSIMULATION ../rtl/*.v tb_my_ecat_sync.v vsim -voptargsacc work.tb_my_ecat_sync set TOP sim:/tb_my_ecat_sync set DUT ${TOP}/u_ecat_sync # --- 全局控制 --- add wave -divider {--- Control ---} add wave -radix binary ${TOP}/clk add wave -radix binary ${TOP}/rst_n add wave -radix unsigned ${TOP}/wkc_nu # --- 状态机最重要--- add wave -divider {--- FSM ---} add wave -radix unsigned -color Chartreuse ${TOP}/sync_sm add wave -radix unsigned ${TOP}/ecat_const_adr add wave -radix unsigned ${TOP}/ecat_index_sync add wave -radix unsigned ${TOP}/sync_aux_cnt add wave -radix binary -color Green ${TOP}/sync_done add wave -radix binary -color Red ${TOP}/sync_error add wave -radix unsigned ${TOP}/error_code # --- 帧收发 --- add wave -divider {--- Frame TX/RX ---} add wave -radix binary -color Yellow ${TOP}/sync_respone_valid add wave -radix unsigned ${TOP}/crc32_tx_cnt add wave -radix hex ${TOP}/tx_data_sync add wave -radix binary ${TOP}/res_ack_sync add wave -radix binary -color Orange ${TOP}/overtime_plus_sync # --- 从站数据 --- add wave -divider {--- Slave Data ---} add wave -radix decimal ${TOP}/esc_rec_time add wave -radix hex -color Magenta ${TOP}/esc_cap_time add wave -radix binary ${TOP}/reg918_rec_valid add wave -radix binary ${TOP}/rec_port_time_valid add wave -radix unsigned ${TOP}/tb_slave_idx configure wave -timelineunits ns wave zoom full run -all七、这个 tb 的三个隐患与改进隐患 1fork...join白等 50ms问题前面说过join等两条分支都结束即使sync_done早就来了也要等满 50ms。改进用join_any新版本或 flag 轮询老版本见 5.1。隐患 2tb_slave_idx是白盒建模问题触发条件sync_sm 7d3 sync_aux_cnt 3d7 res_ack_sync硬编码对齐 DUT 内部时序。DUT 一改就失效且失效方式隐蔽返回错误的 DL status看起来像拓扑错了。改进改用 DUT 输出信号ecat_index_sync作为从站索引来源见 4.2。如果 DUT 没有这个输出建议给 DUT 加一个——这是个很有价值的调试输出。隐患 3esc_recv_time用组合逻辑赋 64 位问题always (*) begin if (reg918_rec_valid) esc_recv_time slave_time_base; else esc_recv_time 64d0; endreg918_recv_valid是 DUT 输出slave_time_base是 tb 寄存器。组合逻辑赋值可能与 DUT 的采样沿竞争——DUT 在这个时钟沿采样esc_rec_time时tb 的slave_time_base也刚更新读到的是新值还是旧值取决于仿真器调度顺序。八、完整 checklist搭 DC 同步仿真环境时逐条确认时钟timescale 1ns/1ps#12.540MHz自检 100 拍 2500nsIP 替代sub1等行为级模型仿真脱离 Quartus 库帧结构74 拍一帧关键节拍 22加载/ 73结束非发送期crc32_tx_cnt归零应答延迟 2 拍超时门限 8000 拍200µs从站时钟每拍 25ns影子计数器触发条件与 DUT 对齐或改用 DUT 输出DL status非末从站0x8290、末从站0x0210esc_recv_time/esc_cap_time用时序赋值避免竞争fork用join_any或 flag 轮询不白等check失败时打印sync_sm/ecat_index_sync/error_codevlog defineSIMULATION断言生效vsim -voptargsacc内部信号可见改了源码重新 vlog九:仿真图十、结语DC 同步仿真的难点不在 RTL而在怎么让 tb 扮演好32 个从站。三件事做对了环境就通了发送节拍严格对照真实以太网帧结构74 拍应答/超时用明确的拍数建模2 拍 / 8000 拍从站行为用 DL status 反映真实拓扑末从站 Port3 不活跃其中最容易被忽视、也最容易出错的是第三点——末从站的 DL status 如果建模错了主站会一直等拓扑到头sync_done永远出不来你会以为是状态机有 bug其实是 tb 的锅。另外提醒一句tb 里凡是依赖 DUT 内部信号的地方都是白盒建模属于技术债。能改成用 DUT 输出信号的尽量改——DUT 一改 tb 就废的教训踩过一次就记住了。如果这篇文章对你有帮助欢迎点赞收藏。有问题可以在评论区交流——尤其是你也在做 EtherCAT 主站 DC 同步的话欢迎一起探讨拓扑扫描、电缆冗余那些坑。
返回列表