{"id":1389,"date":"2026-10-23T14:43:32","date_gmt":"2026-10-23T22:43:32","guid":{"rendered":"https:\/\/www.beyond-circuits.com\/wordpress\/?p=1389"},"modified":"2026-10-23T14:43:32","modified_gmt":"2026-10-23T22:43:32","slug":"a-testbench-race-i-should-have-caught","status":"publish","type":"post","link":"https:\/\/www.beyond-circuits.com\/wordpress\/2026\/10\/a-testbench-race-i-should-have-caught\/","title":{"rendered":"A Testbench Race I Should Have Caught"},"content":{"rendered":"<p>Every so often a reader reports that the testbench for <a href=\"https:\/\/www.beyond-circuits.com\/wordpress\/tutorial\/tutorial19\/\">Tutorial 19<\/a> fails, with the comparisons coming out one sample off. Back in 2018 I said &#8220;nobody has reported this before&#8221; and asked which version of Vivado they were using. Recently a reader named detiber found a workaround, and when I finally sat down and looked at it, the bug was mine. It&#8217;s a nice little example of something I wrote about years ago in <a href=\"https:\/\/www.beyond-circuits.com\/wordpress\/tutorial\/tutorial3\/\">Tutorial 3<\/a>, so I&#8217;m going to own up to it here.<\/p>\n<h1>The code<\/h1>\n<p>The first I2S sample out of the transmitter is junk, because the I2S clock is already running before any data shows up. So the testbench waits until it has sent some data, waits for the start of a left channel word, and then turns on the output checkers.<\/p>\n<pre><code>  event enable_output_checking;\n  reg output_checking_enabled = 0;\n  initial\n    begin\n      @(enable_output_checking);\n      @(negedge ws);\n      repeat (2) @(posedge sck);\n      output_checking_enabled = 1;\n    end\n<\/code><\/pre>\n<p>The checkers are ordinary always blocks clocked on the same <em>sck<\/em>. Here&#8217;s the right channel one, trimmed down:<\/p>\n<pre><code>  always @(posedge sck)\n    if (output_checking_enabled &amp;&amp; !wsd[0] &amp;&amp; wsp)\n      util.check(shift,expected_right.dequeue(0)&amp;mask);\n<\/code><\/pre>\n<h1>The race<\/h1>\n<p>Two rising edges of <em>sck<\/em> after <em>ws<\/em> falls, the word boundary pulse <em>wsp<\/em> is still high. On that edge two blocks wake up: the initial block, which sets <em>output_checking_enabled<\/em>, and the checker, which reads it. Verilog doesn&#8217;t say which one runs first.<\/p>\n<p><script type=\"WaveDrom\">\n{ signal: [\n  { name: \"sck\", wave: \"lhlhlhlhlh\" },\n  { name: \"ws\", wave: \"1.0.......\" },\n  { name: \"wsd[0]\", wave: \"1..0......\" },\n  { name: \"wsd[1]\", wave: \"1....0....\" },\n  { name: \"wsp\", wave: \"0..1.0....\", node: \".....A\" },\n  {},\n  [ \"enable\",\n    { name: \"repeat (2)\", wave: \"0....1....\", node: \".....B\" },\n    { name: \"repeat (3)\", wave: \"0......1..\" }\n  ]\n],\n  edge: [ \"A-B race\" ]\n}\n<\/script><\/p>\n<p>The <em>ws<\/em> signal changes on the falling edge of <em>sck<\/em>, and the testbench&#8217;s little I2S receiver delays it through <em>wsd<\/em> on rising edges, so <em>wsp<\/em> is a one clock pulse at each word boundary. With <code>repeat (2)<\/code> the flag is set on the second rising edge, the same edge where the checker still sees <em>wsp<\/em> high.<\/p>\n<ul>\n<li>If the checker runs first, it sees the flag as 0 and skips. The first comparison is the left word, which is what I intended.<\/li>\n<li>If the initial block runs first, the checker sees a 1, compares the junk right channel word, and pulls an entry off the expected queue. From then on every right channel comparison is one sample behind.<\/li>\n<\/ul>\n<p>The simulator I used in 2015 happened to pick the first order. Newer versions of XSim evidently pick the second. Neither one is wrong; my testbench was.<\/p>\n<p>To prove it, I forced the bad ordering in Icarus Verilog by setting the flag a nanosecond before that edge instead of on it. The original testbench went from passing with 797 comparisons to failing with 398 errors out of 798, and every received word matched the <em>previous<\/em> expected word. That&#8217;s exactly what readers were reporting.<\/p>\n<h1>The fix<\/h1>\n<p>detiber&#8217;s workaround was to change <code>repeat (2)<\/code> to <code>repeat (3)<\/code>. That works: by the third edge <em>wsp<\/em> is low, so the checkers can&#8217;t fire no matter who goes first. But it steps around the race rather than removing it. The real fix is one character:<\/p>\n<pre><code>      output_checking_enabled &lt;= 1;\n<\/code><\/pre>\n<p>With a nonblocking assignment, every block triggered on that edge sees the old value, and the new value shows up after the edge. Simulation order no longer matters.<\/p>\n<h1>The lesson<\/h1>\n<p>This is the same rule from Tutorial 3, and it applies to testbench code just as much as RTL: if a value is written on a clock edge and read by another block on the same edge, use a nonblocking assignment. A testbench that only passes because of the order your simulator happens to pick is a testbench that will break when you upgrade.<\/p>\n<p>The fixed <a href=\"https:\/\/www.beyond-circuits.com\/repos\/tutorial\/trunk\/tests\/i2s_transmit_test.v\">i2s_transmit_test.v<\/a> is in the tutorial repository. Thanks to detiber and Zdzislaw for reporting it, and sorry it took me so long.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Every so often a reader reports that the testbench for Tutorial 19 fails, with the comparisons coming out one sample off. Back in 2018 I said &#8220;nobody has reported this before&#8221; and asked which version of Vivado they were using. &hellip; <a href=\"https:\/\/www.beyond-circuits.com\/wordpress\/2026\/10\/a-testbench-race-i-should-have-caught\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,9,5],"tags":[],"class_list":["post-1389","post","type-post","status-publish","format-standard","hentry","category-verilog","category-vivado","category-xilinx"],"_links":{"self":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1389","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/comments?post=1389"}],"version-history":[{"count":2,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1389\/revisions"}],"predecessor-version":[{"id":1391,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1389\/revisions\/1391"}],"wp:attachment":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/media?parent=1389"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/categories?post=1389"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/tags?post=1389"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}