{"id":52267,"date":"2026-11-01T10:50:48","date_gmt":"2026-11-01T10:50:48","guid":{"rendered":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/"},"modified":"2026-11-01T10:50:48","modified_gmt":"2026-11-01T10:50:48","slug":"interrupts-and-timers","status":"publish","type":"siratek_tutorial","link":"https:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/","title":{"rendered":"Responding Immediately with Interrupts"},"content":{"rendered":"<body><p><\/p>\n<h2>What this covers<\/h2>\n<p>Everything so far has worked by checking: <code>loop()<\/code> runs round, reads a pin, and decides. That is fine while the loop is quick, and it starts to fail when the loop has real work in it.<\/p>\n<p>An interrupt is the other way round. You tell the chip \u201cwhen this pin changes, stop whatever you are doing and run this function\u201d. The hardware watches the pin, not your code, so the response happens in microseconds regardless of what the sketch was in the middle of.<\/p>\n<p>It is the right tool for a narrow set of jobs and the wrong one for most. Knowing which is which is most of this page.<\/p>\n<h2>Before you begin<\/h2>\n<p>Nothing new to wire. A push button on pin 2 is enough, and <em>Read a Push Button<\/em> already built it.<\/p>\n<h3>When you need one<\/h3>\n<ul>\n<li><strong>A pulse too short to catch by polling.<\/strong> A rotary encoder turned quickly, or a flow meter\u2019s pulses. Miss one and the count is permanently wrong.<\/li>\n<li><strong>Something that must respond immediately<\/strong>, such as an emergency stop.<\/li>\n<li><strong>Waking a sleeping board<\/strong>, which is how a battery project lasts months instead of days.<\/li>\n<\/ul>\n<h3>When you do not<\/h3>\n<ul>\n<li><strong>A button a person presses.<\/strong> Human fingers are slow and <code>millis()<\/code> polling catches every press. Using an interrupt here adds bounce problems and buys nothing.<\/li>\n<li><strong>Reading a sensor regularly.<\/strong> That is what the <code>millis()<\/code> pattern is for.<\/li>\n<li><strong>Anything that takes time to handle.<\/strong> The handler must be short, and most work is not.<\/li>\n<\/ul>\n<p>The honest summary: if <em>Timing Without delay()<\/em> solves it, use that. Interrupts are for things polling genuinely cannot catch.<\/p>\n<figure class=\"siratek-tutorial-figure\"><img decoding=\"async\" src=\"http:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg\" alt=\"Two timelines showing a short pulse missed between two polling checks, and the same pulse caught every time by an interrupt handler\" loading=\"lazy\"><figcaption>A pulse that starts and ends between two checks is lost for ever. The hardware does not have to be looking.<\/figcaption><\/figure>\n<h2>The two interrupt pins<\/h2>\n<p>On an Uno only <strong>pin 2 and pin 3<\/strong> support <code>attachInterrupt()<\/code>. That is the whole list.<\/p>\n<pre><code>attachInterrupt(digitalPinToInterrupt(2), myHandler, FALLING);<\/code><\/pre>\n<p><code>digitalPinToInterrupt()<\/code> converts the pin number into the interrupt number the hardware uses. Writing <code>attachInterrupt(0, ...)<\/code> happens to mean pin 2 on an Uno and something else on a Mega, so use the macro and the sketch stays portable.<\/p>\n<p>The modes are <code>RISING<\/code>, <code>FALLING<\/code>, <code>CHANGE<\/code> and <code>LOW<\/code>. <code>FALLING<\/code> is right for a button to ground with <code>INPUT_PULLUP<\/code>.<\/p>\n<p>A Mega has six interrupt pins and an ESP32 allows any pin, which is one more reason to check the board before designing around two.<\/p>\n<h2>The rules for a handler<\/h2>\n<p>An interrupt handler, or ISR, runs with the rest of the sketch frozen. That imposes real constraints, and breaking them produces faults that are very hard to find because they are intermittent.<\/p>\n<p><strong>Keep it short.<\/strong> Set a flag, increment a counter, record <code>micros()<\/code>. Nothing else. While it runs, nothing else on the board does.<\/p>\n<p><strong>Do not use <code>delay()<\/code>.<\/strong> It needs interrupts to work and they are off. It will hang.<\/p>\n<p><strong>Do not use <code>Serial.print()<\/code>.<\/strong> It needs interrupts too. It sometimes appears to work, which is worse than failing.<\/p>\n<p><strong><code>millis()<\/code> does not advance.<\/strong> You can read it, and it will not move while the handler runs. <code>micros()<\/code> is usable for short measurements.<\/p>\n<p><strong>Variables shared with <code>loop()<\/code> must be <code>volatile<\/code>.<\/strong> This is the rule most often broken, so it gets its own section.<\/p>\n<h3>Why volatile, and what goes wrong without it<\/h3>\n<p>The compiler optimises. If it sees a loop reading a variable that nothing in that loop changes, it is entitled to read it once and reuse the value.<\/p>\n<p>That is correct reasoning and wrong here, because the ISR does change it, invisibly.<\/p>\n<pre><code>volatile bool pressed = false;      \/\/ volatile: the ISR changes this\n\nvoid handler() {\n  pressed = true;\n}\n\nvoid loop() {\n  if (pressed) {                    \/\/ without volatile, this may be read once, forever\n    pressed = false;\n    Serial.println(\"pressed\");\n  }\n}<\/code><\/pre>\n<p>The symptom is a sketch that works at one optimisation level and not another, or works until you add an unrelated line. Mark every variable shared with an ISR <code>volatile<\/code> and the problem does not arise.<\/p>\n<h3>Reading a multi-byte variable safely<\/h3>\n<p><code>volatile<\/code> is not enough on its own for anything larger than a byte.<\/p>\n<p>An ATmega328P is 8-bit, so reading a <code>long<\/code> takes four separate operations. If an interrupt fires between the second and the third, you get half the old value and half the new one, which can be a number that never existed.<\/p>\n<pre><code>volatile unsigned long pulseCount = 0;\n\nunsigned long readCount() {\n  noInterrupts();                   \/\/ briefly block, so the read is not split\n  unsigned long value = pulseCount;\n  interrupts();\n  return value;\n}<\/code><\/pre>\n<p>Keep the blocked section to the copy itself. Anything longer starts costing you the interrupts you went to the trouble of setting up.<\/p>\n<h2>Bounce, which interrupts make worse<\/h2>\n<p>A mechanical button bounces: one press produces several electrical transitions. Polling with a debounce delay hides this; an interrupt catches every one of them.<\/p>\n<p>So an interrupt-driven button counts one press as three or four. If you need a button on an interrupt, debounce in the handler by time:<\/p>\n<pre><code>volatile unsigned long lastInterrupt = 0;\nvolatile int count = 0;\n\nvoid handler() {\n  unsigned long now = micros();\n  if (now - lastInterrupt &gt; 200000) {    \/\/ ignore anything within 200 ms\n    count++;\n    lastInterrupt = now;\n  }\n}<\/code><\/pre>\n<p><code>micros()<\/code> works in a handler, and comparing unsigned values this way survives the rollover for the same reason the <code>millis()<\/code> pattern does.<\/p>\n<h2>A worked example: counting fast pulses<\/h2>\n<p>This is a job polling cannot do reliably.<\/p>\n<pre><code>\/\/ Siratek: counting pulses that are too short and too frequent to poll for.\n\nconst int pulsePin = 2;\n\nvolatile unsigned long pulseCount = 0;\n\nvoid onPulse() {\n  pulseCount++;                     \/\/ short, and nothing else\n}\n\nunsigned long readCount() {\n  noInterrupts();\n  unsigned long value = pulseCount;\n  interrupts();\n  return value;\n}\n\nvoid setup() {\n  pinMode(pulsePin, INPUT_PULLUP);\n  attachInterrupt(digitalPinToInterrupt(pulsePin), onPulse, FALLING);\n\n  Serial.begin(9600);\n  Serial.println(\"Siratek: counting pulses on pin 2.\");\n}\n\nvoid loop() {\n  \/\/ The loop can be as slow as it likes. No pulse is missed.\n  delay(1000);\n\n  Serial.print(\"pulses so far: \");\n  Serial.println(readCount());\n}<\/code><\/pre>\n<p>The <code>delay(1000)<\/code> is deliberate. It would make a polling version useless, and here it costs nothing: the hardware is counting whether the sketch is paying attention or not. That is the whole argument for interrupts in one sketch.<\/p>\n<h2>Timer interrupts, and a warning<\/h2>\n<p>The other kind fires on a timer rather than a pin, which is how you run something at an exact interval regardless of the loop.<\/p>\n<p>It is genuinely useful, for refreshing a multiplexed display or sampling at a fixed rate. It is also where beginners break things, because <strong>the Uno\u2019s three timers are already in use<\/strong>:<\/p>\n<ul>\n<li><strong>Timer0<\/strong> runs <code>millis()<\/code>, <code>micros()<\/code> and <code>delay()<\/code>. Touch it and all three break.<\/li>\n<li><strong>Timer1<\/strong> drives PWM on pins 9 and 10, and the <code>Servo<\/code> library takes it.<\/li>\n<li><strong>Timer2<\/strong> drives PWM on pins 3 and 11, and <code>tone()<\/code> and IRremote take it.<\/li>\n<\/ul>\n<p>So there is no free timer on an Uno, only a choice about what to give up. Before reconfiguring one, know which pins and libraries stop working.<\/p>\n<p>The <strong>MsTimer2<\/strong> and <strong>TimerOne<\/strong> libraries wrap this up safely and are the sensible starting point. Writing timer registers directly is worth learning and is not the place to begin.<\/p>\n<p>If you want a fixed interval and do not need microsecond accuracy, the <code>millis()<\/code> pattern is simpler, portable, and breaks nothing.<\/p>\n<h2>Check yourself<\/h2>\n<ol>\n<li>A button on an interrupt counts three presses for one. Why?<\/li>\n<li>A sketch works, you add an unrelated line, and a flag set by an ISR stops being noticed. What is missing?<\/li>\n<li>Why can <code>Serial.print()<\/code> not go in a handler?<\/li>\n<li>You reconfigure Timer1 and the servo stops working. Why?<\/li>\n<\/ol>\n<p>Answers in order: contact bounce, which an interrupt catches every transition of; debounce by time in the handler. <code>volatile<\/code> on the shared flag. Serial transmission depends on interrupts, which are disabled inside a handler. The <code>Servo<\/code> library uses Timer1, and reconfiguring it takes that away.<\/p>\n<h2>Troubleshooting<\/h2>\n<table>\n<thead>\n<tr>\n<th>Symptom<\/th>\n<th>Likely cause<\/th>\n<th>Check or fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Handler never runs<\/td>\n<td>Not on pin 2 or 3<\/td>\n<td>Only those two on an Uno<\/td>\n<\/tr>\n<tr>\n<td>Flag set but never noticed<\/td>\n<td>Missing <code>volatile<\/code><\/td>\n<td>Mark every shared variable<\/td>\n<\/tr>\n<tr>\n<td>Board hangs when it triggers<\/td>\n<td><code>delay()<\/code> in the handler<\/td>\n<td>Remove it<\/td>\n<\/tr>\n<tr>\n<td>Serial output garbled or missing<\/td>\n<td><code>Serial.print()<\/code> in the handler<\/td>\n<td>Set a flag; print in <code>loop()<\/code><\/td>\n<\/tr>\n<tr>\n<td>Counter occasionally absurd<\/td>\n<td>Multi-byte read torn<\/td>\n<td>Copy it inside <code>noInterrupts()<\/code><\/td>\n<\/tr>\n<tr>\n<td>One press counted several times<\/td>\n<td>Bounce<\/td>\n<td>Debounce by time in the handler<\/td>\n<\/tr>\n<tr>\n<td><code>millis()<\/code> frozen<\/td>\n<td>Expected inside a handler<\/td>\n<td>Use <code>micros()<\/code> for short spans<\/td>\n<\/tr>\n<tr>\n<td><code>delay()<\/code> and <code>millis()<\/code> both broken<\/td>\n<td>Timer0 reconfigured<\/td>\n<td>Leave Timer0 alone<\/td>\n<\/tr>\n<tr>\n<td>Servo stopped after adding a timer<\/td>\n<td>Timer1 taken<\/td>\n<td>Use a different timer, or the <code>millis()<\/code> pattern<\/td>\n<\/tr>\n<tr>\n<td>Works on an Uno, not a Mega<\/td>\n<td>Interrupt pin numbers differ<\/td>\n<td>Use <code>digitalPinToInterrupt()<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Next steps<\/h2>\n<ul>\n<li><strong>Easier first:<\/strong> <em>Timing Without delay()<\/em>, which solves most of what people reach for interrupts to solve.<\/li>\n<li><strong>Related:<\/strong> <em>Making a Button Behave<\/em>, for why bounce matters here.<\/li>\n<li><strong>Next:<\/strong> a rotary encoder or a flow meter, which are the classic jobs for this.<\/li>\n<\/ul>\n<h2>Sources and attribution<\/h2>\n<p>All text and code in this tutorial are original Siratek work.<\/p>\n<ul>\n<li>ATmega328P datasheet for the external interrupt pins, the timer assignments and the 8-bit register access that makes torn multi-byte reads possible.<\/li>\n<li>Arduino core documentation for <code>attachInterrupt()<\/code>, <code>digitalPinToInterrupt()<\/code> and the timers used by <code>millis()<\/code>, <code>Servo<\/code> and <code>tone()<\/code>.<\/li>\n<\/ul>\n<p>No third-party tutorial text, diagrams, photographs or code has been reproduced here.<\/p>\n<p><\/p>\n<\/body>","protected":false},"excerpt":{"rendered":"<p>Everything so far has worked by checking: loop() runs round, reads a pin, and decides. That is fine while the loop is quick, and it starts to fail when the loop has real work in it.<\/p>\n","protected":false},"featured_media":0,"template":"","siratek_tutorial_level":[],"class_list":["post-52267","siratek_tutorial","type-siratek_tutorial","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Responding Immediately with Interrupts - siratek<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Responding Immediately with Interrupts - siratek\" \/>\n<meta property=\"og:description\" content=\"Everything so far has worked by checking: loop() runs round, reads a pin, and decides. That is fine while the loop is quick, and it starts to fail when the loop has real work in it.\" \/>\n<meta property=\"og:url\" content=\"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/\" \/>\n<meta property=\"og:site_name\" content=\"siratek\" \/>\n<meta property=\"og:image\" content=\"http:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/\",\"url\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/\",\"name\":\"Responding Immediately with Interrupts - siratek\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/siratek.net\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/#primaryimage\"},\"image\":{\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/#primaryimage\"},\"thumbnailUrl\":\"http:\\\/\\\/siratek.net\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/interrupts-and-timers-polling-vs-interrupt.svg\",\"datePublished\":\"2026-11-01T10:50:48+00:00\",\"breadcrumb\":{\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/#primaryimage\",\"url\":\"https:\\\/\\\/siratek.net\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/interrupts-and-timers-polling-vs-interrupt.svg\",\"contentUrl\":\"https:\\\/\\\/siratek.net\\\/wp-content\\\/uploads\\\/2026\\\/11\\\/interrupts-and-timers-polling-vs-interrupt.svg\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"http:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/interrupts-and-timers\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/siratek.net\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Tutorials\",\"item\":\"https:\\\/\\\/siratek.net\\\/index.php\\\/tutorials\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Responding Immediately with Interrupts\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/siratek.net\\\/#website\",\"url\":\"https:\\\/\\\/siratek.net\\\/\",\"name\":\"siratek\",\"description\":\"Your Best Electronic Retail Shop\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/siratek.net\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Responding Immediately with Interrupts - siratek","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/","og_locale":"en_US","og_type":"article","og_title":"Responding Immediately with Interrupts - siratek","og_description":"Everything so far has worked by checking: loop() runs round, reads a pin, and decides. That is fine while the loop is quick, and it starts to fail when the loop has real work in it.","og_url":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/","og_site_name":"siratek","og_image":[{"url":"http:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg","type":"","width":"","height":""}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/","url":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/","name":"Responding Immediately with Interrupts - siratek","isPartOf":{"@id":"https:\/\/siratek.net\/#website"},"primaryImageOfPage":{"@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/#primaryimage"},"image":{"@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/#primaryimage"},"thumbnailUrl":"http:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg","datePublished":"2026-11-01T10:50:48+00:00","breadcrumb":{"@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/#primaryimage","url":"https:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg","contentUrl":"https:\/\/siratek.net\/wp-content\/uploads\/2026\/11\/interrupts-and-timers-polling-vs-interrupt.svg"},{"@type":"BreadcrumbList","@id":"http:\/\/siratek.net\/index.php\/tutorials\/interrupts-and-timers\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/siratek.net\/"},{"@type":"ListItem","position":2,"name":"Tutorials","item":"https:\/\/siratek.net\/index.php\/tutorials\/"},{"@type":"ListItem","position":3,"name":"Responding Immediately with Interrupts"}]},{"@type":"WebSite","@id":"https:\/\/siratek.net\/#website","url":"https:\/\/siratek.net\/","name":"siratek","description":"Your Best Electronic Retail Shop","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/siratek.net\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"}]}},"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/siratek_tutorial\/52267","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/siratek_tutorial"}],"about":[{"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/types\/siratek_tutorial"}],"version-history":[{"count":0,"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/siratek_tutorial\/52267\/revisions"}],"wp:attachment":[{"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/media?parent=52267"}],"wp:term":[{"taxonomy":"siratek_tutorial_level","embeddable":true,"href":"https:\/\/siratek.net\/index.php\/wp-json\/wp\/v2\/siratek_tutorial_level?post=52267"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}