In the previous article, I explored installing and running .NET Core 3.0 on a Raspberry PI running Raspbian. In this article I intend to create a simple console application that will interact with the GPIO and flash an LED.

What is a GPIO?

GPIO stands for General Purpose Input Output. A GPIO pin has no predefined purpose - i.e. they are unused by the main device and can be configured as input or output and can be controlled by an application. The Raspberry Pi has two rows of GPIO pins, as well as others.

PiGPIO

Output pins are like switches that your app running on the Raspberry Pi can turn on or off - common uses include turning on LEDs, but they can be used for more advanced communication with devices.

Input pins can be turned on and off by external devices such as switches, however they too can be used for more advanced communication with devices, such as receiving information from sensors.

.NET Core IoT Libraries

So how can we interact with the GPIO? Fortunately, there are a set of NuGet packages that do the heavy lifting for us the source is hotsed here - https://github.com/dotnet/iot.

To quote the repository:

.NET Core can be used to build applications for IoT devices and scenarios. IoT applications typically interact with sensors, displays and input devices that require the use of GPIO pins, serial ports or similar hardware.

This repository contains the System.Device.Gpio library and implementations for various boards like Raspberry Pi and Hummingboard.

The repository also contains IoT.Device.Bindings, a growing set of community-maintained device bindings for IoT components.

NOTE: This repository is still in experimental stage and all APIs are subject to changes.

This library provides the means to interact with sensors via GPIO, SPI, I2C and PWM. You can checkout the documentation here.

For this article, we will focus on plain vanilla GPIO.

Create a Simple LED Flasher

Let's create a .NET Core Console app that uses GPIO to flash an LED.

  1. Connect to your PI via a terminal.

  2. Navigate to a directory where you want to create your project folder.

  3. To create you project folder, enter the following command:

    mkdir SimpleFlasher
        
  4. To navigate into the project folder, enter the following command:

    cd SimpleFlasher
        
  5. To create the initial console application project, enter the following command:

    dotnet new console
        

    After a few moments, you will have a simple hello world app.

  6. To test the app, enter the following command:

    dotnet run
        

    After a few moments compiling, etc. you will see Hello World!.

  7. To add the package references we will be using for the GPIO communication, enter the following commands:

    dotnet add package System.Device.Gpio --source https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json
    dotnet add package Iot.Device.Bindings --source https://dotnetfeed.blob.core.windows.net/dotnet-iot/index.json
        

    The SimpleFlasher.csproj file will now look like:

    <Project Sdk="Microsoft.NET.Sdk">
    
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
    </PropertyGroup>
    
    <ItemGroup>
            <PackageReference Include="IoT.Device.Bindings" Version="1.0.0" />
            <PackageReference Include="System.Device.Gpio" Version="1.0.0" />  </ItemGroup>
    
    </Project>
        

Before we go any further though, let's explore something that will make our development much simpler - VS Code Remote Development

VS Code Remote Development

To quote the VS Code Remote Development page:

Visual Studio Code Remote Development allows you to use a container, remote machine, or the Windows Subsystem for Linux (WSL) as a full-featured development environment. You can:

  • Develop on the same operating system you deploy to or use larger or more specialized hardware.
  • Sandbox your development environment to avoid impacting your local machine configuration.
  • Make it easy for new contributors to get started and keep everyone on a consistent environment.
  • Use tools or runtimes not available on your local OS or manage multiple versions of them.
  • Develop your Linux-deployed applications using the Windows Subsystem for Linux.
  • Access an existing development environment from multiple machines or locations.
  • Debug an application running somewhere else such as a customer site or in the cloud.

We will focus on using the Remote - SSH part of the extension to aid in our development.

  1. Open Visual Studio Code and press CTRL + SHIFT + X to open the Extensions pane.

  2. To install the Remote Development extension pack, enter Remote Development and click Install.

    Note: The extension has the extension identifier ms-vscode-remote.vscode-remote-extensionpack so you can be sure you are installing the correct one.

  3. To open the command pane, press F1 and enter Remote-SSH: Connect to host

  4. When prompted to Select configured SSH host or enter user@host, enter pi@.

    A new Visual Studio Code window will open and you will be prompted to enter the user password... do so.

    You will see a notification that informs you the SSH host is being configured.

    Once the configuration is complete, Visual Studio Code will be connected to the PI and you can browse the PI filesystem.

  5. To open the SimpleFlasher folder, select File > Open Folder and browse to the location you created the project folder. Select SimpleFlasher.

  6. Once you choose the folder, you may be prompted for the password again - enter it.

    The explorer window will now display the contents of the folder.

  7. Open Program.cs.

    It will look something like this:

    using System;
    
    namespace SimpleFlasher
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("Hello World!");
            }
        }
    }
        
  8. Update the Console.WriteLine("Hello World!"); line to Console.WriteLine("Hello World - updated from VS Code!"); and save the changes.

  9. To run the app from within Visual Studio Code, press CTRL + SHIFT `

    You should see a bash terminal open connected to your PI with the current directory set to the location you have open in the file explorer.

  10. To run your revised app, enter dotnet run.

    Success! We now have the convenience of editing our code in Visual Studio Code.

Simple Flashing Sample

We'll now update our application to interact with the GPIO bus and flash an LED... I know, global electronic domination follows... BTW, I bought a kit from Freenove via Amazon that has a huge variety of components: Freenove Ultimate Starter Kit for Raspberry Pi 4 B 3 B+, 434 Pages Detailed Tutorials, Python C Java, 223 Items, 57 Projects, Learn Electronics and Programming, Solderless Breadboard

  1. In Visual Studio Code, replace the contents of the Program.cs file with the following:

    using System;
    using System.Device.Gpio;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace SimpleFlasher {
        class Program {
            static void Main (string[] args) {
                // GPIO 17 which is physical pin 11
                int ledPin1 = 17;
                GpioController controller = new GpioController ();
                // Sets the pin to output mode so we can switch something on
                controller.OpenPin (ledPin1, PinMode.Output);
    
                int lightTimeInMilliseconds = 1000;
                int dimTimeInMilliseconds = 200;
    
                while (true) {
                    Console.WriteLine ($"LED1 on for {lightTimeInMilliseconds}ms");
                    // turn on the LED
                    controller.Write (ledPin1, PinValue.Low);
                    Thread.Sleep (lightTimeInMilliseconds);
                    Console.WriteLine ($"LED1 off for {dimTimeInMilliseconds}ms");
                    // turn off the LED
                    controller.Write (ledPin1, PinValue.High);
                    Thread.Sleep (dimTimeInMilliseconds);
                }
            }
        }
    }
        

    Due to the polarity of the wiring for the circuit, pulling a pin low (PinValue.Low) turns on the LED.

  2. From the above you can see that using GpioController is pretty straightforward:

    • You create an instance of GpioController.
    • Using the instance, you can open pins and set their modes
    • You can then write (or read) values from the pin.
  3. Next, wire up a simple circuit. You will need a breadboard, an LED, a 220 Ohm resistor and some link leads:

    • Connect 3V3 to the positive rail of the breadboard
    • Connect GPIO 17 (physical pin 11) to the negative lead of the LED
    • Use the 220 Ohm resistor to connect the positive rail to the positive lead of the LED.

    SimpleFlasher_bb

  4. To test the app, enter the following command:

    dotnet run
        

    After a few moments of compilation, the app will run and you should see two things:

    • The LED flashing

    • A console log:

      LED1 on for 1000ms
      LED1 off for 200ms
      LED1 on for 1000ms
      LED1 off for 200ms
      LED1 on for 1000ms
      LED1 off for 200ms
      LED1 on for 1000ms
      LED1 off for 200ms
      LED1 on for 1000ms
      LED1 off for 200ms
      LED1 on for 1000ms
      
  5. Hit CTRL-C to stop the app.

Implementing a Binary Display

Next we will extend the sample above by adding more LEDs which we will use to display binary numbers. You can either connect individual LEDs, or an LED Bar Graph component (one is included in the kit I linked to above):

LedBraGraph

Note: It is hard to determine which way round to fit these, you may have to rotate it if none of the LEDs come on...

  1. Update the breadboard wiring as follows:

    LedBinary_bb.

    Note that the resistors supply power to one side of the LEDs, and pulling a pin low causes current to flow, illuminating the LEDs.

    Also, I have chosen pins based upon wiring convenience, rather than following an form of numbering convention - to be honest, the pin layout of the GPIO bus on the PI only makes sense based upon the convenience of the manufacturer.

    PiGPIO[1]

    Anyway, the pins I used are:

    Physical PinGPIOUsedDisplay Bit
    12GPIO 18X9
    14GND  
    16GPIO 23X8
    18GPIO 24X7
    20GND  
    22GPIO 25X6
    24GPIO 8X5
    26GPIO 7X4
    28Reserved  
    30GND  
    32GPIO 12X3
    34GND  
    36GPIO 16X2
    38GPIO 20X1
    40GPIO 21X0
  2. Once you have completed the circuit, return to Visual Studio Code.

  3. Replace the contents of the Program.cs file with the following:

    using System;
    using System.Device.Gpio;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace SimpleFlasher {
        class Program {
    
            // a list of all the pins used to display the value
            // pins are listed in the most significant bit order
            // i.e. pin 18 represents the highest value
            static int[] _pins = { 18, 23, 24, 25, 8, 7, 12, 16, 20, 21 };
            static GpioController _controller = new GpioController ();
    
    
            static void Main (string[] args) 
            {
                // Set all the pins to output mode and
                // ensure all the LEDs are off
                foreach (var pin in _pins)
                {
                    _controller.OpenPin (pin, PinMode.Output);
                    _controller.Write (pin, PinValue.High);
                }
    
                // play around with this value.
                //   0 will result in a virtually instant count
                //  10 looks cool
                int countDelay = 100;
                int value = 0;
    
                // 1 << 2 is a bit shift operation 
                // 1 << 2 == 1 * 2 * 2 == 4
                // so 1 << _pins.Length represents the max number we can display + 1
                while (value < (1<<_pins.Length)) 
                {
                    Console.WriteLine(value);
                    DisplayValue(value++);
                    Thread.Sleep (countDelay);
                }
            }
    
            static void DisplayValue(int value)
            {
                var currentBit = _pins.Length;
                while(currentBit > 0)
                {
                    // _pins[10-currentBit] accesses the pin number for the current bit
                    // (value & 1<<currentBit) performs a bitwise AND operation to check if the bit is set in the value
                    // If the bit is set, the ternary expression sets the PinValue.Low (on) or if not, PinValue.High (off)
                    _controller.Write (_pins[10-currentBit], (value & 1<<currentBit) > 0 ? PinValue.Low : PinValue.High);
                    currentBit--;
                }
            }
        }
    }
        

    The code is explained with inline comments.

  4. Compile and run the code with the following command:

    dotnet run
        

Once complete and running, the LEDs should display an incrementing value in binary.

Note: If the LEDs don't light up, turn them around - it is easy to put them in the wrong way.

OK - so now we have worked out how to flash some LEDs by using some pins in output mode. Let's add a switch to our circuit and use it to reset count.

Reading Input

Now we are going to add a switch to the circuit and check to see if the switch is pressed each time we loop.

  1. Add the following variable above the Main method:

    // The pin we will use to reset the count
    static int _switchPin = 17;
        
  2. Replace the Main method with:

    static void Main (string[] args) 
    {
        // Set all the pins to output mode and
        // ensure all the LEDs are off
        foreach (var pin in _pins)
        {
            _controller.OpenPin (pin, PinMode.Output);
            _controller.Write (pin, PinValue.High);
        }
    
        // Set the pin mode. We are using InputPullDOwn which uses a pulldown resistor to
        // ensure the input reads Low until an external switch sets it high,
        // We could have used InputPullUp if we wanted it to read High until it was pulled down Low.
        // If we just used Input, the value on the pin would wander between High and Low... not very useful in this situation.
        _controller.OpenPin(_switchPin, PinMode.InputPullDown);
    
    
        // play around with this value.
        //   0 will result in a virtually instant count
        //  10 looks cool
        int countDelay = 100;
        int value = 0;
    
        // We'll loop forever now - CTRL + C to exit
        while (true) {
            Console.WriteLine(value);
            // _pins[10-currentBit] accesses the pin number for the current bit
            // (value & 1<<currentBit) performs a bitwise AND operation to check if the bit is set in the value
            // If the bit is set, the ternary expression sets the PinValue.Low (on) or if not, PinValue.High (off)
            DisplayValue(value);
            // don't keep incrementing the value once we hit max
            if (value < (1<<(_pins.Length+1))-1)
            {
                value++;
            }
    
            // Check input value each time we loop
            if (_controller.Read(_switchPin) == PinValue.High)
            {
                // button pressed
                value = 0;
            }
    
            Thread.Sleep (countDelay);
        }
    }
        

    As usual, the code is heavily commented to explain what is going on. We now have an infinite loop and we poll the status of the input pin each loop to determine if the pin value is high - if so, we reset the value to zero.

  3. Let's update the circuit to include a switch connected between 3.3v and GPIO 17.

    LedBinaryReset_bb

  4. Compile and run the code with the following command:

    dotnet run
        

    You should now see the LEDs display an incrementing value in binary - if you leave it running long enough, it will reach 2047 and all LEDs will be lit. Pressing the switch will raise GPIO 17 high, which will cause the value to be reset to zero. Once you release the switch, the count will begin again.

So in this article we explored using the the GPIO in both output and input modes. In the next article I will start to explore I2C.

While working on various courses for Azure IoT, I have had a bunch of experience working with C/C++ and Python on Linux and C#/UWP on Windows 10 IoT Core. However I have noticed that the Raspberry Pi isn't necessarily an ideal platform for Windows 10 Core - in fact the later models, including the Raspberry Pi 4, are not yet supported. So, if I want to explore IoT in C# on the Raspberry Pi, I need to look at .NET Core.

Install .NET Core 3.0

Complete the following steps to install .NET Core 3.0 on a Raspberry PI Model 3 V2 running Raspbian Buster. Raspbian Buster is a variant of Debian 10.

Prerequisites

The .NET Core github repo contains an article that lists the following prerequisites for .NET Core 3.0 on Debian 10:

  • libicu63
  • libssl1.1
  1. In a terminal session on your Raspberry PI, to install .NET Core runtime dependencies, run the following commands:

    sudo apt-get -y update
    sudo apt-get -y install wget libicu63 libssl1.1
        

We will use wget to download the distro on the Raspberry Pi.

Download the Linux .NET Core Distro

At the moment, there is no simple way to install .Net Core via the apt-get command - instead we will need to download the tar for the distribution and install it ourselves.

Microsoft maintains a download page for .NET Core 3.0 here

  1. In a browser, open https://dotnet.microsoft.com/download/dotnet-core/3.0.

  2. Find the latest version of the Linux ARM32 SDK and click the URL.

    This will open the download page as well as automatically start the download. Useful, if you are using a browser on the PI, but not so useful if you are running on the desktop. Fortunately, the page also provides a click here to download manually link as well as the ability to copy a direct link to the download. Copy that URL.

    This is the URL I see: https://download.visualstudio.microsoft.com/download/pr/8ddb8193-f88c-4c4b-82a3-39fcced27e91/b8e0b9bf4cf77dff09ff86cc1a73960b/dotnet-sdk-3.0.100-linux-arm.tar.gz

  3. In a terminal session on your Raspberry PI, to download the distribution, run the following command:

    wget <url-from-above>
        

    For example:

    wget https://download.visualstudio.microsoft.com/download/pr/8ddb8193-f88c-4c4b-82a3-39fcced27e91/b8e0b9bf4cf77dff09ff86cc1a73960b/dotnet-sdk-3.0.100-linux-arm.tar.gz
        
  4. To create a directory to extract the tar archive, enter the following command:

    sudo mkdir -p /usr/share/dotnet
        

    The -p argument creates parent directories as necessary.

    Note: The dotnet runtime expects to be located in /usr/share/dotnet. Alternatively, you can set the DOTNET_ROOT environment variable to specify the runtime location or register the runtime location in /etc/dotnet/install_location.

  5. To extract the archive, enter the following command:

    sudo tar -zxf dotnet-sdk-3.0.100-linux-arm.tar.gz -C /usr/share/dotnet
        

    Note: This will run silently and take a few minutes.

    The arguments mean:

    • z - filter the archive through gzip (as the file name ends in .gz we know gzip was used)
    • x - extract the archive
    • f - means the archive is a file, not from tape
  6. To create a symbolic link to the dotnet command so it is accessible by the existing path settings, enter the following command:

    sudo ln -s /usr/share/dotnet/dotnet /usr/bin
        
  7. To verify the dotnet command is available, enter the following command:

    dotnet --version
        

    For the version I have installed, you should see the following:

    dotnet --version
    3.0.100
        

Create a Sample Project

In these steps we will create a simple "Hello, World" app and run it. We will then create a single file deployment, copy the output and demonstrate that it runs "standalone".

  1. Change directory to the location you wish to create the project.

  2. To create a directory for your project and then navigate into it, enter the following commands:

    mkdir helloworld
    cd helloworld
        
  3. To create a new console application, enter the following command:

    dotnet new console
        

    This command will create the source file and project file for a simple console application. It will also automatically run dotnet restore to download any dependencies.

    This is a very simple app:

    using System;
    
    namespace helloworld
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("Hello World!");
            }
        }
    }
        
  4. To run the app, enter the following command:

    dotnet run
        

    After a moment, the application will compile, run and then write the following to the console:

    dotnet run
    Hello World!
        

    Note: If you see the error below, ensure the SDK was installed in the correct location.
    A fatal error occurred. The required library libhostfxr.so could not be found.

  5. To create a self-contained release build, enter the following command:

    dotnet publish -r linux-arm -c Release --self-contained
        

    You will see something similar tp the following:

    dotnet publish -r linux-arm -c Release --self-contained
    Microsoft (R) Build Engine version 16.3.0-preview-19455-02+4a2d77107 for .NET Core
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    Restore completed in 26.79 sec for /home/pi/helloworld/helloworld.csproj.
    You are using a preview version of .NET Core. See: https://aka.ms/dotnet-core-preview
    helloworld -> /home/pi/helloworld/bin/Release/netcoreapp3.0/linux-arm/helloworld.dll
    helloworld -> /home/pi/helloworld/bin/Release/netcoreapp3.0/linux-arm/publish/
        

    The publish command will build the app in release mode and will publish the self contained app. It will create a new folder with the name of publish which will have a standalone copy of the .NET Core runtime with the .exe file. To run the app on another machine (even one with out .NET Core installed) just requires copying this folder and running the executable file.

  6. To view the files in the publish folder, run the following command:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish
        

    You will see there are a lot of files.

    To just view the application executable, enter the following command:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish/helloworld
        

    You will see something similar to:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish/helloworld
    -rwxr--r-- 1 pi pi 73400 Sep 18 17:35 bin/Release/netcoreapp3.0/linux-arm/publish/helloworld
        

    The size of the executable is pretty small, as you would expect for our small app - of course, the folder does also contain the entire set of libraries for the .NET Core runtime as well. However, with .NET Core 3.0 we can go one better and produce a single executable file.

  7. To delete the Publish folder, enter the following command:

    rm -r bin/Release
        
  8. To create a single file, run the following command:

    dotnet publish -r linux-arm -c Release /p:PublishSingleFile=true
        
  9. To view the files in the publish folder, run the following command:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish
        

    You will see something similar to:

    ls -l
    total 76476
    -rwxr--r-- 1 pi pi 78289466 Sep 18 18:30 helloworld
    -rw-r--r-- 1 pi pi      416 Sep 18 17:35 helloworld.pdb
        

    You can see we now have a single executable helloworld. However, check out the size of the file - it is massive! That is because it now contains all of the .NET Core runtime as well as our tiny hello world app.

    Thankfully, .NET Core 3.0 now allows us to trim unnecesary files before we create the single file.

  10. To delete the Publish folder, enter the following command:

    rm -r bin/Release
        
  11. To create a single, trimmed file, run the following command:

    dotnet publish -r linux-arm -c Release /p:PublishSingleFile=true  /p:PublishTrimmed=true
        
  12. To view the files in the publish folder, run the following command:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish
        

    You will see something similar to:

    ls -l bin/Release/netcoreapp3.0/linux-arm/publish
    total 30076
    -rwxr--r-- 1 pi pi 30791588 Sep 18 18:41 helloworld
    -rw-r--r-- 1 pi pi      416 Sep 18 17:35 helloworld.pdb
        

    You can see we still have a single executable helloworld. However, check out the size of the file - it is now half the size of the untrimmed file. It's not perfect - but certainly better!

So, to sum up - in this article we installed .NET Core 3.0, created a simple console app, and then explored various ways to compile and publish that app.

In the next article, I am going to explore interacting with some simple electronics via GPIO.