I've had a heap of fun, and just a tad amount of pain recently building a PWM Fan Controller for my Pi. I really, really wanted to get all this to work with:

  1. dotnet/iot
  2. Raspberry Pi running Raspian (Linux based)
  3. C#
  4. Visual Studio 2017/2019 and Visual Studio Code

Sometimes, its easier to use Python for these things as there's are a lot of examples out there. But stubbornly I vowed, "I shall build this in Visual Studio with C#" which is still my preferred development IDE and language. At this time I couldn't find a single example out there for using the PWM functionality in (still in pre-release) dotnet/iot. I'm sure before too long it will be there, but in the meantime here is a first demo IOT project, using all of the above techie things.

I often find the Hello World examples, handy, but not practical. So in this post, I've put together:

  • A hello world / simple example
  • A read world / structured example

This way you can start off easy before BAM!, you get hit with all the realities of how to build this in a robust way.

TL:DR;

  • I've put up the code for this dotnet-iot-demos on Github
  • Controlling the fan and reading the tachometer works well
  • You have to enable the PWM mode in the OS if using Raspbian
  • You can debug the code on the Pi, using VS Code and deploy automatically via a batch file

Pinnout

RPi-PwmFan-fritz

If you put this together make sure you connect the fan up correctly, see http://www.pavouk.org/hw/fan/en_fan4wire.html.
I used:

  1. GRND - Black
  2. VCC - Red (12 volts)
  3. Sense / Tachometer - Yellow
  4. PWM - Blue

You might notice there is a common ground here, between the 12volt circuit and a ground pin on the GPIO block.

The project

For this project I want to run a process that monitors a temperature probe and then controls a fan with a variable speed. As PWM fans vary in airflow and RPM, I want to monitor the speed of the fan so I can correctly calibrate against different brands of fans.

(In this post I'm not going to show the Temperature probe, but instead focus on the PWM Fan speed and reading the RPM.)

A simple example

So let's get started with a Hello World. In this example I'm not reading the tachometer, as that's not really Hello Worldish, but I'll cover that further down. What we are doing is starting up the fan, changing the speed, and then shutting it off.
The following code runs the following duty cycles with 2 second waits between them:

  1. 100%
  2. 70%
  3. 30%
  4. 0
static void PWMFanSimpleExample()
{
    Console.WriteLine("Starting PWM Controller - Simple Demo");
    using (var controller = new PwmController())
    {
        double dutyCycle = 100;
        var chip = 0;
        var channel = 0;
        var hertz = 25;
        controller.OpenChannel(chip, channel);
        controller.StartWriting(chip, channel, hertz, dutyCycle);
        Console.WriteLine("Duty cycle " + dutyCycle);
        Task.Delay(new TimeSpan(0, 0, 10)).Wait(); //10 second wait to give fan time to power up
        ReadTachometer();

        dutyCycle = 70;
        controller.ChangeDutyCycle(chip, channel, dutyCycle);
        Console.WriteLine("Duty cycle " + dutyCycle);
        Task.Delay(new TimeSpan(0, 0, 2)).Wait(); //2 second wait
        ReadTachometer();

        dutyCycle = 30;
        controller.ChangeDutyCycle(chip, channel, dutyCycle);
        Console.WriteLine("Duty cycle " + dutyCycle);
        Task.Delay(new TimeSpan(0, 0, 2)).Wait(); //2 second wait
        ReadTachometer();

        controller.ChangeDutyCycle(chip, channel, 0); //
        controller.StopWriting(chip, channel);
        controller.CloseChannel(chip, channel);
        Console.WriteLine("Finished - Simple Demo");
    }
}


Here's the output with a Noctua NS-R8 1800 PWM fan.

Starting PWM Controller - Simple Demo
Duty cycle 100
Fan is running at 1622 revolutions per minute
Duty cycle 70
Fan is running at 1497 revolutions per minute
Duty cycle 30
Fan is running at 60 revolutions per minute
Finished - Simple Demo

It didn't quite reach the 1800 but was pretty close. The tachometer is sampled over 5 seconds, but you could change this to whatever you like.

Note: If your code errors, there's a good chance that that your fan will keep spinning after the program terminates. Check out the real world example to solve this by disposing your objects.

Reading the Tachometer Pin

To read the Tachometer on the fan, you have to sample the number of pulses over a period of time, and divide it by 2. A 12v PC Fan operates on 25 hertz cycle. It will pulse twice for each revolution.

To read the pulses, you have to subscribe to an event and count the number of pulses, then calcuate the result based on the elapsed duration.

To read the pulses, you can use the GpioController.RegisterCallbackForPinValueChangedEvent method. Here's the ReadTachometer function from the above example:

static void ReadTachometer()
{
    var pin = 23;
    var pulses = 0;
    var startTime = DateTime.Now;
    var sampleMilliseconds = 5000; 
    PinChangeEventHandler onPinEvent = (object sender, PinValueChangedEventArgs args) => { pulses++; };
    using (var controller = new GpioController())
    {
        controller.OpenPin(pin, PinMode.InputPullUp);
        controller.RegisterCallbackForPinValueChangedEvent(pin, PinEventTypes.Rising, onPinEvent);
        Task.Delay(new TimeSpan(0, 0, 0, 0, sampleMilliseconds)).Wait(); //wait
        var milliSeconds = (DateTime.Now - startTime).TotalMilliseconds;
        var revsPerSecond = (pulses / 2) / (milliSeconds / 1000);
        var rpm = Convert.ToInt32(revsPerSecond * 60);
        controller.UnregisterCallbackForPinValueChangedEvent(pin, onPinEvent);
        Console.WriteLine($"Fan is running at {rpm} revolutions per minute");
    }
}

Don't fall into the trap where you think this wait will be exactly 1 second:

Task.Delay(new TimeSpan(0, 0, 0, 0, 1000)).Wait();

The actual time could be more than 1 second, that's why I'm tracking the elapsed time with the startDate variable.

You might also notice that the code is Unregistering from that event. It's an important step, or your code could act in unexpected ways. One problem with this example is that if the code errors before the Unregister is called, the onPinEvent method will keep getting called. In the read world example I've wrapped this in a class to ensure it gets unregistered in the Dispose method.

Compiling, Deploying and Debugging with the Pi

Doing all this from Visual Studio 2019 just wasn't possible. Deploying to a Linux based operating system, and being able to debug the code in real time needs more than VS 2019 can provide. Here's what I needed to get a full, automated developer process working:

  • Visual Studio Code (to compile, deploy and debug)
  • Putty/PLINK.exe (to handle communication between the Pi and my Dev PC)
  • Visual Studio 2017/2019 (this is optional, but I use it to develop)
  • A remote debugger installed on the PI
  • A batch file to glue this all together

Keep in mind, I'm running the Linux OS, not Windows IOT Core. With Windows IOT Core, you can deploy and debug without all the hassle I've had to go through. But I found running a snappy little Linux OS great as I have more control.

Scott Hanselman has done a great post that I poured over for a few hours. I highly recommend a read of Remote debugging with VS Code on Windows to a Raspberry Pi using .NET Core on ARM. I had to make a tweak or 2, but it really helped me find my own "Internal Developer Loop" with this project.

Here's a quick summary of what you'll need to do

  1. Install the remote debugger on the Pi. See Hanselman's Post
  2. Install DotNet Core SDK 2.2

Run the following in the Pi to install .net 2.2:

sudo apt-get -y update
sudo apt-get -y install libunwind8 gettext
wget https://download.visualstudio.microsoft.com/download/pr/35c09285-4114-44f7-aa7d-47fe75a55eda/ac5a8f1bc324f2a6cd021237528441d4/dotnet-sdk-2.2.100-linux-arm.tar.gz
wget https://download.visualstudio.microsoft.com/download/pr/860e937d-aa99-4047-b957-63b4cba047de/da5ed8a5e7c1ac3b4f3d59469789adac/aspnetcore-runtime-2.2.0-linux-arm.tar.gz
sudo mkdir /opt/dotnet
sudo tar -xvf dotnet-sdk-2.2.100-linux-arm.tar.gz -C /opt/dotnet/
sudo tar -xvf aspnetcore-runtime-2.2.0-linux-arm.tar.gz -C /opt/dotnet/
sudo ln -s /opt/dotnet/dotnet /usr/local/bin
sudo rm dotnet-sdk-2.2.100-linux-arm.tar.gz
sudo rm aspnetcore-runtime-2.2.0-linux-arm.tar.gz

PWM on the Raspberry Pi

This one stumped me. I ran some code, opened a channel, specified a chip, counted my widgets, aligned my bitstreams... but kept getting an error along the lines of "The chip number 0 is invalid or is not enabled".
It happened when I tried to run this code:

using (var controller = new PwmController())

Cause
On the PI, the default Raspian configuration does not enable the PWM pin mode.

Solution
Open a session to your Pi using Putty or SSH app of your choice.
Edit the /boot/config.txt
add "dtoverlay=pwm-2chan" to the end of the file. (Without quotes)
Thanks to this post for the solution.

The open source dotnet/iot libraries

The dotnet/iot libraries are in development and there is a small note on the readme warning that they may change. You can download them as nuget packages. But to get the latest pre-release you'll need to add a new package source:
Add this package source in Visual Studio:
Tools -> Nuget Package Manager -> Package Manager Settings
https://dotnet.myget.org/F/dotnet-core/api/v3/index.json

You can add these in the Package Manager console:

install-package System.Device.Gpio -PreRelease

Or you can use the dotnet CLI

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

The nice thing about these libraries is they are cross platform and work in the Linux world. Is that the dark side, or the light side? I'm never sure....

A real world example

If you've checked out the simple example, you'll probably notice there's a few things you need to do, and in a certain order. These are:

  1. Create a controller
  2. Open a channel
  3. Start writing to the channel
  4. Change the duty cycle as needed
  5. Stop writing to the channel
  6. Close the channel
  7. Dispose the controller

If you try to write before opening, it will error. If you forget to close your channel or don't dispose, then you're fan could keep running, and the channel could possibly be blocked if you try to run your program again. Striving for good system design, I wanted to encapsulate that very specific knowledge into an easy to use component system. I've put together a Repo on github dotnet-iot-demos to demonstrate this, along with some additional code for reading the Tachometer on the fan.

Here's the code using the PWMController and a few classes to make things easier.

var pwmController = new PwmController();
var gpioController = new GpioController();
try
{
    var chip = 0; //On the Rapsberry Pi this will be GPIO 18
    var channel = 0; //On the Rapsberry Pi this will be GPIO 18
    var hertz = 25; //12v PWM PC Fans run on a 25 hertz frequency
    int? tachoPin = 23; //A normal GPIO pin connected to the tachometer pin on the fan
    using (var fan = new PwmFan(gpioController, pwmController, chip, channel, hertz, tachoPin))
    {
        fan.On();
        Task.Delay(new TimeSpan(0, 0, 0, 2)).Wait(); //Wait for a bit for inital fan start up.
        var dutyCycle = 100;
        while (dutyCycle >= 0)
        {
            fan.SetSpeed(dutyCycle);
            Task.Delay(new TimeSpan(0, 0, 0, 2)).Wait(); //2 seconds
            Console.WriteLine($"DutyCycle: {dutyCycle}, RPM: {fan.RPM}");
            dutyCycle = dutyCycle - 10;
        }

        fan.Off();
    }
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex}");
}
finally
{
    pwmController.Dispose();
    gpioController.Dispose();
}

The above example cycles down through the duty cycles at -10 increments, starting at 100%. It also reads the Tachometer, which I've connected to GPIO Pin 23.

Jump into the repo if you want to see more details around how I built the classes.

Running the code

Download the repo or use git

git clone https://github.com/TjWheeler/dotnet-iot-demos

Open the dotnet-iot-demos/src/PwmFanControllerDemo folder with VS Code.

If you have 2 monitors, what works quite well here is VS Code on one, and Visual Studio on the other. Both will refresh the files as they change. This way you can debug in VS Code, and develop in VS. Or, if you prefer, just use VS Code.

There are a few files used by VS code to get all this to work:

  • .vscode/launch.json
  • .vscode/tasks.json
  • Publish.cmd

If you want more details on how this works, check out Hanselmans post Remote debugging with VS Code on Windows to a Raspberry Pi using .NET Core on ARM

You will need to update the Publish.cmd file with your password and the name of your device or IP address.

pscp -pw "password" -v -r .\*.* pi@deviceIpOrName:/home/pi/Desktop/pwmfancontrollerdemo

The above command uses pscp to copy your files over to the Pi using SSH.
If you want to improve performance after your initial copy, change the line to:

pscp -pw "password" -v -r .\pwmfancontrollerdemo.* pi@deviceIpOrName:/home/pi/Desktop/pwmfancontrollerdemo

In the launch.json file, there is a setting where the pipeTransport uses PLINK.EXE. This file comes with Putty. You'll need to download and install it.

One last thing, you'll need to create the folder /home/pi/Desktop/pwmfancontrollerdemo. Open a Putty session to your device and run mkdir /home/pi/Desktop/pwmfancontrollerdemo.

Then from Visual Studio Code you should be ready to go.
Select ".NET Core Launch (remote console)" from the configuration drop down.
IMG-000-3-07-2019

Press F5 to Debug, or CTRL+F5 to run without debugging.

Conclusion

Getting this working end to end was really satisfying. Bringing the strengths of C# and Dotnet to the small device world made me feel right at home and debugging on my Raspberry Pi 2 model B was suprisingly quick. However, a lack of documentation and samples made getting going on this project difficult and time consuming. Hopefully with more people using the great open source dotnet/iot libraries, and a bit more time to evolve the documentation, this will be a good option for real device development.