Cerbuino Bee – Reading the Tacho-Signal of a PC-Fan

Hello,
I had some spare time and a Cerbuino Bee Board laying around from my last project and a PC CPU Fan, so I decided to do some experimenting with it. Unfortunately it doesn’t work as expected and since I can’t find my mistake, probably due to my lack of knowledge in electrical engineering (I study software engineering), I was hoping maybe someone here could give me a hint.

What I was trying to do: According to my research, those fans can have up to 4 pins (as mine does). Two for the power supply, one RPM/Tacho-Signal which gives two pulses per turn and a PWM pin to control the speed of the fan.

Connecting only the two power pins worked as expected.

The PWM-Pin next seems to work too. I can’t really measure it, but I can clearly see that the fan is running much slower when the duty cycle is low. So I guess that works too.

Unfortunately, the Tacho-Signal doesn’t work quite as expected.

I wrote a little test-Program that increases the duty cycle of the PWM Pin from 0 to 1 in 0.1 steps and displays the measured RPM. I’ll attach the code to this post.

It seems to work fine at a duty cycle of 0 (quite solid 550 rpm) and 1 (solid 1250 rpm). But in between it jumps between values like (not exact) 0, 1000, 600, 1250, 1800, 2300 and 2500 and stays constant 0 at 0.99. I can’t find any pattern but it seems to go up the closer it gets to a duty cycle of 1, but it even goes up over 2000 which it doesn’t even do at a duty cycle of 1. Especially the 0 is awkward, since that means there was no time between the two readings.

When it reached the duty cycle of 1, it seems to react fine on changes of the speed. If I manually slow the fan down a little, it gets represented on the display.

Thoughts I had:
Instead of measuring the milliseconds between to pulses, I could count them for one second and then multiply them 30. But when doing it like this, the RPM obviously changes in steps of 30. Depending of the timing, it jumps by 30-60 every time, which doesn’t look good.

I also tried connecting the InterruptPort and PWM to the Gadgeteer Socket 3 of the board (Pin 3 and 7) instead of the arduino headers, but that showed the same result.

Maybe this behavior is due to the fact, that I run the fan at only 9 volts instead of the required 12, since I connect both the board and the fan to the same source. I think that manual speed regulation on the pc that some cases have is done by running them at lower voltages, but maybe that effects it somehow. Or maybe the fan is defective itself?

Is the timestamp of the InterruptPort exact enough for this?

I would expect something around 1250 RPM at 9V, so this should be about 43 pulses per second. Is this too fast for the InterruptPort?

Sorry for the long text and thanks for any help!

Alex


using System;
using System.Collections;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Controls;
using Microsoft.SPOT.Presentation.Media;
using Microsoft.SPOT.Presentation.Shapes;
using Microsoft.SPOT.Touch;

using Gadgeteer.Networking;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer.Modules.Seeed;
using Microsoft.SPOT.Hardware;
using GHI.OSHW.Hardware;
using Gadgeteer;

namespace SimpleFanControl
{
    public partial class Program    
    {
        /// <summary>
        /// The used width of the display
        /// </summary>
        const byte width = 115;
        /// <summary>
        /// The used height of the display
        /// </summary>
        const byte height = 20;
        /// <summary>
        /// The frequence in Hz for the PWM        
        /// </summary>
        const int pwmFreq = 25000;      
        /// <summary>
        /// The duty for the PWM
        /// </summary>
        double duty = 0;
        /// <summary>
        /// True to take a reading of the tacho-Signal
        /// </summary>
        bool takeSample = true;        
        /// <summary>
        /// True to skip the next reading of the tacho-Signal
        /// </summary>
        bool skip = true;        
        /// <summary>
        /// True if the next reading of the tacho-Signal is the first one 
        /// </summary>
        bool isFirstSample = true;
        /// <summary>
        /// True if there a two valid samples of the tacho-Signal
        /// </summary>
        bool rpmReady = false;
        /// <summary>
        /// The first reading of the tacho-Signal
        /// </summary>
        DateTime firstSample = DateTime.Now;
        /// <summary>
        /// The second reading of the tacho-Signal
        /// </summary>
        DateTime secondSample = DateTime.Now;
        /// <summary>
        /// The PWM-Pin
        /// </summary>
        PWM pwm;
        /// <summary>
        /// The Interrupt GPIO Pin for the tacho-Signal
        /// </summary>
        InterruptPort tacho;

        void ProgramStarted()
        {        
            Debug.Print("Program Started");
            Thread pwmThread = new Thread(new ThreadStart(pwmWork));            
            Thread tachoThread = new Thread(new ThreadStart(tachWork));
            Thread displayThread = new Thread(new ThreadStart(displayWork));
            pwmThread.Start();
            tachoThread.Start();
            displayThread.Start();                                                                       
        }

        /// <summary>
        /// Updates the PWM Signal
        /// </summary>
        void pwmWork()
        {                      
            pwm = new PWM(Cpu.PWMChannel.PWM_4, pwmFreq, duty, false); //Arduino Header A2            

            //A Timer that counts up the duty cycle from 0 to 1 on 0.1 steps
            GT.Timer pwmTimer = new GT.Timer(5000, GT.Timer.BehaviorType.RunContinuously);
            pwmTimer.Tick += (t1) =>
            {
                if (duty < 1)
                {
                    duty += 0.1;
                    if (duty > 1)
                        duty = 1;
                    pwm.Stop();
                    pwm.DutyCycle = duty;
                    pwm.Frequency = pwmFreq;
                    pwm.Start();
                }
            };
            pwm.Start();
            pwmTimer.Start();
        }

        /// <summary>
        /// Reads the tacho-Signal
        /// </summary>
        void tachWork()
        {
            tacho = new InterruptPort(GHI.Hardware.FEZCerb.Pin.PA4, false, Port.ResistorMode.PullUp, Port.InterruptMode.InterruptEdgeLow); //Arduino Header A5            

            //Stores the second and third reading of the tacho-Pin into firstSample and secondSample and sets rpmReady to true when secondSample is set
            tacho.OnInterrupt += (data1, data2, time) =>
            {
                if (takeSample)
                {
                    if (skip)
                    {
                        skip = false;
                    }
                    else
                    {                        
                        if (isFirstSample)
                        {
                            firstSample = time;
                            isFirstSample = false;
                        }
                        else
                        {
                            secondSample = time;
                            takeSample = false;
                            rpmReady = true;
                        }                        
                    }
                }
            };        
         
        }

        /// <summary>
        /// Calculates the RPM and displays them on the display
        /// </summary>
        void displayWork()
        {
            //The Buffers for the display
            Bitmap bitmap = new Bitmap(width, height);
            byte[] framebuffer = new byte[width * height * 2];
            
            //The used font
            Font fontNinaB = Resources.GetFont(Resources.FontResources.NinaB);
                        
            long rpm = 0;
            GT.Timer displayTimer = new GT.Timer(500, GT.Timer.BehaviorType.RunContinuously);
            //Calculates the RPM and displays them on the display
            displayTimer.Tick += (t) =>
            {
                
                bitmap.Clear();
                
                if (rpmReady) //There a two valid readings
                {
                    TimeSpan pulse = secondSample.Subtract(firstSample); //The TimeSpan between the two samples
                    long ms = pulse.Days * 86400000 + pulse.Hours * 3600000 + pulse.Minutes * 60000 + pulse.Seconds * 1000 + pulse.Milliseconds; //The ms between the two samples
                    rpm = ms == 0 ? 0 : 30000 / ms; //2 Pulses per Round                    
                    rpmReady = false;
                    bitmap.DrawText("RPM (" + (int)(duty * 100) + "): " + rpm, fontNinaB, GT.Color.Magenta, 0, 0);
                }
                else //There are no or only one valid reading
                {
                    bitmap.DrawText("RPM (" + (int)(duty * 100) + "): N/A", fontNinaB, GT.Color.Magenta, 0, 0);
                }                    
            
                //Convert the bitmap and draw it
                Util.BitmapConvertBPP(bitmap.GetBitmap(), framebuffer, Util.BPP_Type.BPP16_BGR_BE);
                oledDisplay.FlushRawBitmap(0, 0, width, height, framebuffer);
                
                //Reset the variables
                skip = true;
                isFirstSample = true;
                takeSample = true;

            };

            displayTimer.Start();                 
        }

    }
}