IoT Edge Docker Module with GPIO support on RPi

The C# .Net Core framework is pretty versatile. Next to all the operating system features and Windows features, it also supports GPIO for a variety of devices: Raspberry PI, Hummingboard, Windows 10 (core), etc.

I was interested in accessing the GPIO in an Azure IoT Edge solution on a Raspberry Pi.

I am aware of the elevated rights needed. It’s the same with serial ports access I encountered in the past.

So I did a test, quite similar to the setup of this GPIO introduction:

Let’s check out how we can get this running in an IoT Edge module.

I started with wiring the same setup with an LED and a button:

Make sure you know which pins you are referring to. The physical pin number differs from the actual pin number in your code:

Our C# module

First, we build a module with all code and try to run it as-is. So we leave out the elevated rights for now. This will not run of course but it will provide us some insights regarding the errors.

So you just create a new IoT Edge solution with a default C# module. (I named my module ‘GpioModule’).

Than, add these two references in the GpioModule.csproj so the logic for GPIO usage is referenced:

<PackageReference Include="IoT.Device.Bindings" Version="1.0.0" />
<PackageReference Include="System.Device.Gpio" Version="1.0.0" />  

This is the code inside the Program.cs:

namespace GpioModule
    using System;
    using System.Device.Gpio;
    using System.Runtime.Loader;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.Devices.Client;
    using Microsoft.Azure.Devices.Client.Transport.Mqtt;

    class Program
        // GPIO 17 which is physical pin 11
        static int ledPin = 17;

        // GPIO 27 is physical pin 13
        static int buttonPin = 27;

        static int litInMs = 1000;
        static int dimmedInMis = 250;
        static GpioController controller = null;

        static void Main(string[] args)

            // Wait until the app unloads or is cancelled
            var cts = new CancellationTokenSource();
            AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel();
            Console.CancelKeyPress += (sender, cpe) => cts.Cancel();

        /// <summary>
        /// Handles cleanup operations when app is cancelled or unloads
        /// </summary>
        public static Task WhenCancelled(CancellationToken cancellationToken)
            var tcs = new TaskCompletionSource<bool>();
            cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
            return tcs.Task;

        /// <summary>
        /// Initializes the ModuleClient and sets up the callback to receive
        /// messages containing temperature information
        /// </summary>
        static async Task Init()
            MqttTransportSettings mqttSetting = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
            ITransportSettings[] settings = { mqttSetting };

            // Open a connection to the Edge runtime
            ModuleClient ioTHubModuleClient = await ModuleClient.CreateFromEnvironmentAsync(settings);
            await ioTHubModuleClient.OpenAsync();
            Console.WriteLine("IoT Hub module client initialized.");

            // Construct GPIO controller
            controller = new GpioController ();

            // Sets the LED pin to output mode so we can switch something on
            controller.OpenPin (ledPin, PinMode.Output);

            // Sets the button pin to input mode so we can read a value 
            controller.OpenPin(buttonPin, PinMode.Input);

            Console.WriteLine("Pins opened");

            var thread = new Thread(() => ThreadBody(ioTHubModuleClient));

        private static async void ThreadBody(object userContext)
            while (true)
                Console.WriteLine($"Button: {controller.Read(buttonPin)}");

                Console.WriteLine ($"LED1 on for {litInMs}ms");
                // turn on the LED
                controller.Write (ledPin, PinValue.Low);
                await Task.Delay(litInMs);
                Console.WriteLine ($"LED1 off for {dimmedInMis}ms");
                // turn off the LED
                controller.Write (ledPin, PinValue.High);
                await Task.Delay(dimmedInMis);                 

This code in Program.cs turns on and off the LED and reads the value of the button (pushed or not).

Note: I left out error handling to limit the length of the code.

Now deploy this module to your favorite container repository. I deployed to Docker Hubs as ‘svelde/iot-edge-gpio:0.0.1-arm32v7′. It’s available for you if you want to test.

Once I deployed this module, I got these exceptions:

What I expected was something like “System.NotSupportedException: ‘No GPIO controllers exist on this system.'”. Because testing for this exception is the way to see if GPIO is available.

But instead, I got this ‘Error 13 initializing the Gpio driver’. This is related to not running this code under SUDO rights (aka super user rights, aka elevated rights).

This was to be expected 🙂 Elevated rights are needed.

Adding Elevated rights

Change the Dockerfile.arm32v7. Comment out the two lines related to the ‘moduleuser’:

FROM AS build-env

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

COPY --from=build-env /app/out ./

# RUN useradd -ms /bin/bash moduleuser
# USER moduleuser

ENTRYPOINT ["dotnet", "GpioModule.dll"]

Here we remove the reference to the moduleuser. This module is now enabled to run under elevated rights. I deployed this module under ‘svelde/iot-edge-gpio:0.0.2-arm32v7’.

And we only have to add this container create option to our updated module reference:

  "HostConfig": {
    "Privileged": true

Once updated and deployed, the new version is visible in the list of Iot Edge modules:

The LED is now turned on and off every second, our module is running with GPIO support!

And if we check the log of the ‘gpio’ module, we can also see the button being pressed:

I2C and SPI support

But how about adding I2C and SPI support in our module, will this work?

It seems that these protocols are not directy supported out-of-the-box. And documentation is hard to find.

But I traced back to this example which shows how to connect a Force sensitive resist using this MCP3008 which is a 8-Channel 10-Bit ADC With SPI Interface. And if you look in that code, you will find something like:

_adcConvertor = new Mcp3008(new SoftwareSpi(18, 23, 24, 25));

This ‘SoftwarSPIe’ brings us to the ‘Iot.Device.Spi’ namespace.

And the ‘Iot.Device’ namespace, which leads us to the IoT.Device.Bindings library:

This preview package provides a set of Device Bindings that use System.Device.Gpio package to communicate with a microcontroller.

Note: We added this library in our project above already.

This is a community driven library (where Microsoft is the owner) with a long list of implemented devices (this screenshot shows only a part of the list):

So, if you want to do something with GPIO, please check out this list first.

For example, the BPM180 with I2C communication is in this library:

The following fritzing diagram illustrates one way to wire up the BMP180 with a Raspberry Pi using I2C.

I tried the example of the BMP180 sensor because I had one laying around. I had to slice and dice the code to get it running and this is what I got from the bash line:

Finally, I managed to port the same BMP180 code to an Azure IoT Edge module. I had to relax the permissions on this one too.

I just was informed by a reader that the SenseHat project was working too! I’m now pretty sure we will be lucky with other sensor libraries too!

Some GitHub code is not yet available in the NuGet packages so I had to copy-paste some files (like the Pressure.cs coming from Units).

Do not forget to configure your Raspbian to allow SPI or I2C access. Otherwise you face a ‘Unhandled Exception: System.IO.IOException: Error 2. Can not open I2C device file ‘/dev/i2c-1′.’ error:


We have seen how we can give an IoT Edge module access to the GPIO capabilities by providing elevated rights to the module. This gives simple access to the GPIO pins, both for reading and writing.

And we have glimpsed at the code behind the Iot.Device.Bindings NuGet package. This can be an interesting source for supporting complete sensors, using SPI and/or I2C busses, in Azure IoT Edge.

We now know this works for the BMP180 sensor and the SenseHat. These can be accessed using .Net Core 2.1 C# code which is running in a Docker Image with elevated rights.