eventos-interrupciones-gpio-nanoframework

GPIO Events and Interrupts in nanoFramework

  • 5 min

A GPIO event is a notification that fires when the state of a pin changes without having to constantly poll it.

In the previous post, we saw how to read a button using the Polling method (constantly checking). While it works for simple examples, in the real world it’s a technique to avoid.

Imagine you are waiting for an important letter.

  • Polling is going down to the mailbox every 5 minutes to see if it has arrived. You spend the whole day going up and down the stairs, tired and unable to do anything else.
  • Interrupts is installing a doorbell on the mailbox. You dedicate yourself to reading a book or watching a series, and only when the mailman puts the letter in, the doorbell rings and you go down.

In nanoFramework, hardware interrupts are exposed through C# events.

Using events allows us to free up the main thread (Main Thread) to perform complex tasks (like maintaining a Wi-Fi connection) while the hardware monitors the pins for us.

What is a hardware interrupt?

At the electrical level, the microcontroller has specific circuits connected to the GPIO pins. These circuits can detect voltage changes (edges) without CPU intervention.

When the voltage changes (for example, we press a button and it goes from 3.3V to 0V), this circuit “wakes up” the CPU and tells it: “Stop what you are doing and handle this!”.

In nanoFramework, the CLR captures that signal and transforms it into a C# event called ValueChanged.

Trigger types

We can configure when we want the event to fire:

  • Rising Edge: When the signal goes from Low (0) to High (1). (e.g., Releasing a button with Pull-Up).
  • Falling Edge: When the signal goes from High (1) to Low (0). (e.g., Pressing a button with Pull-Up).
  • Both: Notifies us on any change.

Subscribing to the ValueChanged event

Let’s refactor our previous code. We no longer need a while loop to check the button.

The structure in C# is the standard for event subscription (+=):

// 1. Open the pin (same as before)
GpioPin button = gpio.OpenPin(15, PinMode.InputPullUp);

// 2. Define which changes we want to listen to
// This is important for filtering noise
button.SetPinMode(PinMode.InputPullUp); 

// 3. Subscribe to the event
button.ValueChanged += Button_ValueChanged;
Copied!

Then, we define the handler method (the Event Handler):

private static void Button_ValueChanged(object sender, PinValueChangedEventArgs e)
{
    // This code runs ONLY when the button is pressed
    Debug.WriteLine($"Button changed to: {e.ChangeType}");
}
Copied!

Complete example: switch using events

Let’s make the LED toggle its state every time we press the button. Notice how the main Main loop looks.

using System;
using System.Device.Gpio;
using System.Diagnostics;
using System.Threading;

namespace GpioEvents
{
    public class Program
    {
        // Declare the LED as static to access it from the event
        private static GpioPin _led;

        public static void Main()
        {
            GpioController gpio = new GpioController();

            // LED configuration
            _led = gpio.OpenPin(2, PinMode.Output);

            // Button configuration
            GpioPin button = gpio.OpenPin(15, PinMode.InputPullUp);

            // Event subscription
            // Using '+=' Visual Studio will suggest creating the method automatically with TAB
            button.ValueChanged += Button_ValueChanged;

            Debug.WriteLine("System ready. Waiting for events...");

            // Main loop
            // NOTE HERE: We do nothing anymore.
            // We put the thread to sleep indefinitely. CPU consumption drops dramatically.
            Thread.Sleep(Timeout.Infinite);
        }

        private static void Button_ValueChanged(object sender, PinValueChangedEventArgs e)
        {
            // Filter: We only want to act when PRESSED (Falling Edge)
            // If we don't filter, the event would also fire when releasing the button
            if (e.ChangeType == PinEventTypes.Falling)
            {
                Debug.WriteLine("Button Pressed!");
                _led.Toggle(); // Invert the LED
            }
        }
    }
}
Copied!

Using Thread.Sleep(Timeout.Infinite) tells the operating system (RTOS) that this thread does not need clock cycles. The processor can enter low-power modes or attend to other threads (like the network one).

The problem of bouncing (debouncing)

If you test the previous code with a real physical button (not a high-quality one), you’ll notice something strange: sometimes, when pressing once, the LED turns on and off instantly. Or it flickers.

This is called Bouncing.

In the real world, a mechanical switch is a metal plate hitting another. When they come together, they microscopically “bounce” several times before stabilizing. The microcontroller is so fast that it detects each bounce as a separate press.

Software debouncing

Although external circuits (capacitors) exist to fix this, in nanoFramework we can easily solve it in code by checking the time between presses.

Let’s improve our Event Handler:

// Variable to store the last press time
private static DateTime _lastBounce = DateTime.MinValue;

private static void Button_ValueChanged(object sender, PinValueChangedEventArgs e)
{
    if (e.ChangeType == PinEventTypes.Falling)
    {
        // Check if at least 200ms have passed since the last time
        if ((DateTime.UtcNow - _lastBounce).TotalMilliseconds > 200)
        {
            Debug.WriteLine("Valid press");
            _led.Toggle();
            
            // Update the last bounce timestamp
            _lastBounce = DateTime.UtcNow;
        }
    }
}
Copied!

With this simple if, we ignore any spurious signal that occurs within 200ms after the initial press. Problem solved!