{"id":1392,"date":"2026-10-23T14:52:29","date_gmt":"2026-10-23T22:52:29","guid":{"rendered":"https:\/\/www.beyond-circuits.com\/wordpress\/?p=1392"},"modified":"2026-10-23T14:52:29","modified_gmt":"2026-10-23T22:52:29","slug":"a-missing-edge-in-my-debounce-circuit","status":"publish","type":"post","link":"https:\/\/www.beyond-circuits.com\/wordpress\/2026\/10\/a-missing-edge-in-my-debounce-circuit\/","title":{"rendered":"A Missing Edge in My Debounce Circuit"},"content":{"rendered":"<p>A while back a reader named Sam asked a question about the debounce circuit in <a href=\"https:\/\/www.beyond-circuits.com\/wordpress\/tutorial\/tutorial11\/\">Tutorial 11<\/a>. In simulation, the rise and fall outputs fired two clocks after the input changed, and he expected them to wait until the bounce timer had run out. His reading was correct, and that part is by design. But while I was answering him I found a real bug, and it has been sitting in that tutorial (and in Tutorials 12 and 13, which reuse the module) since 2015.<\/p>\n<h1>How the debouncer works<\/h1>\n<p>This is a &#8220;lockout&#8221; debouncer. It reports an edge as soon as the input gets through the synchronizer, then ignores the input until a timer runs out. That gives you the lowest possible latency, which is nice for buttons. Here&#8217;s the original always block:<\/p>\n<pre><code>  always @(posedge clk)\n    if (bounce_count == 0)\n      begin\n        switch_rise &lt;= switch_shift == 2'b01;\n        switch_fall &lt;= switch_shift == 2'b10;\n        switch_out &lt;= switch_shift[0];\n        if (switch_shift[1] != switch_shift[0])\n          bounce_count &lt;= bounce_limit-1;\n      end\n    else\n      begin\n        switch_rise &lt;= 0;\n        switch_fall &lt;= 0;\n        bounce_count &lt;= bounce_count-1;\n      end\n<\/code><\/pre>\n<p>When the timer isn&#8217;t running, it looks for an edge in the two-bit shift register. If it sees one, it pulses <em>switch_rise<\/em> or <em>switch_fall<\/em> and starts the timer.<\/p>\n<h1>The bug<\/h1>\n<p>The problem is what happens when the switch changes and changes back while the timer is running. Say you tap the button for less than the debounce time. The press is reported, and the release happens during the lockout. When the timer expires, <em>switch_out<\/em> picks up the new value, so the level ends up correct. But by then there is no edge in the shift register anymore, so <em>switch_fall<\/em> never fires.<\/p>\n<p><script type=\"WaveDrom\">\n{ signal: [\n  { name: \"switch (synchronized)\", wave: \"0.1..0........\" },\n  {},\n  [ \"original\",\n    { name: \"timer\", wave: \"0.=......0....\", data: [\"lockout\"] },\n    { name: \"switch_out\", wave: \"0.1......0....\" },\n    { name: \"switch_rise\", wave: \"0.10..........\" },\n    { name: \"switch_fall\", wave: \"0.............\", node: \".........A\" }\n  ],\n  {},\n  [ \"fixed\",\n    { name: \"timer\", wave: \"0.=......=....\", data: [\"lockout\", \"lockout\"] },\n    { name: \"switch_out\", wave: \"0.1......0....\" },\n    { name: \"switch_rise\", wave: \"0.10..........\" },\n    { name: \"switch_fall\", wave: \"0........10...\" }\n  ]\n],\n  edge: [ \"A missing\" ]\n}\n<\/script><\/p>\n<p>The same thing happens the other way around: a short release while the button is held loses the following <em>switch_rise<\/em>. If all you use is <em>switch_out<\/em> you&#8217;d never notice. But if you count presses, or toggle something on <em>switch_rise<\/em> and expect a matching <em>switch_fall<\/em>, you will occasionally get out of step, and only when someone taps the button quickly. That&#8217;s the worst kind of bug to track down on the bench.<\/p>\n<h1>The fix<\/h1>\n<p>Instead of looking for an edge, compare the synchronized input with the debounced output:<\/p>\n<pre><code>  always @(posedge clk)\n    if (bounce_count == 0)\n      begin\n        switch_rise &lt;= switch_shift[1] &amp;&amp; !switch_out;\n        switch_fall &lt;= !switch_shift[1] &amp;&amp; switch_out;\n        switch_out &lt;= switch_shift[1];\n        if (switch_shift[1] != switch_out)\n          bounce_count &lt;= bounce_limit-1;\n      end\n    else\n      begin\n        switch_rise &lt;= 0;\n        switch_fall &lt;= 0;\n        bounce_count &lt;= bounce_count-1;\n      end\n<\/code><\/pre>\n<p>Now any difference left over when the timer expires gets reported, along with a fresh lockout period. The latency is unchanged. While I was at it, I switched to bit 1 of the shift register, so the input actually goes through both synchronizer flip-flops; the original used bit 0, which is only one flip-flop away from the asynchronous input.<\/p>\n<h1>Testing it<\/h1>\n<p>To make sure this was really fixed, I wrote a small testbench, <a href=\"https:\/\/www.beyond-circuits.com\/repos\/tutorial\/trunk\/tests\/debounce_test.v\">debounce_test.v<\/a>. It uses a short <em>bounce_limit<\/em> of 16 clocks so the lockout is easy to land inside. An always block counts every <em>switch_rise<\/em> and <em>switch_fall<\/em> pulse, and flags an error if both ever fire at once. Each test case changes the input, bumps the number of rises and falls it expects, and then calls a <code>check<\/code> task, which waits for everything to settle and then checks two things: that <em>switch_out<\/em> matches the input, and that the pulse counts match what was expected.<\/p>\n<p>A <code>bounce<\/code> task toggles the input a few times with random spacing before leaving it at its final value, which gives a reasonable imitation of a real switch. The test cases are:<\/p>\n<ul>\n<li>a clean press and release<\/li>\n<li>a bouncy press and release<\/li>\n<li>a short tap, pressed and released inside the lockout<\/li>\n<li>a bouncy short tap<\/li>\n<li>a short release while the button is held<\/li>\n<\/ul>\n<p>Here&#8217;s the short tap case, so you can see the pattern:<\/p>\n<pre><code>      \/\/ Press and release both inside the lockout window\n      switch_in = 1;\n      repeat (4) @(posedge clk);\n      #1 switch_in = 0;\n      expected_rises = expected_rises+1;\n      expected_falls = expected_falls+1;\n      check(\"short tap\");\n<\/code><\/pre>\n<p>Against the original module, the clean and bouncy cases pass but the three short ones fail, each one missing a pulse:<\/p>\n<pre><code>ERROR short tap: rises=3 (expected 3) falls=2 (expected 3)\nERROR bouncy short tap: rises=4 (expected 4) falls=3 (expected 4)\nERROR short release: rises=5 (expected 6) falls=5 (expected 5)\nFAIL: 3 errors\n<\/code><\/pre>\n<p>With the fix, all nine checks pass. It&#8217;s plain Verilog, so it runs in any simulator. I used Icarus Verilog:<\/p>\n<pre><code>iverilog -o debounce_test debounce_test.v debounce.v\nvvp debounce_test\n<\/code><\/pre>\n<p>The fixed <a href=\"https:\/\/www.beyond-circuits.com\/repos\/tutorial\/trunk\/rtl\/debounce.v\">debounce.v<\/a> is now in the tutorial repository, Tutorials 11, 12 and 13 all link to it, and Tutorial 11 has been updated with the new code. Thanks to Sam for the question that made me look.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A while back a reader named Sam asked a question about the debounce circuit in Tutorial 11. In simulation, the rise and fall outputs fired two clocks after the input changed, and he expected them to wait until the bounce &hellip; <a href=\"https:\/\/www.beyond-circuits.com\/wordpress\/2026\/10\/a-missing-edge-in-my-debounce-circuit\/\">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":[6,4],"tags":[],"class_list":["post-1392","post","type-post","status-publish","format-standard","hentry","category-fpga","category-verilog"],"_links":{"self":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1392","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=1392"}],"version-history":[{"count":1,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1392\/revisions"}],"predecessor-version":[{"id":1393,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/posts\/1392\/revisions\/1393"}],"wp:attachment":[{"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/media?parent=1392"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/categories?post=1392"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.beyond-circuits.com\/wordpress\/wp-json\/wp\/v2\/tags?post=1392"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}