Single phase 220vac fan speed control.


I am almost finished my project which involves controlling the speed of two single phase 220vac fans 75W each. The speed control doesn't have to be very precise, just speeding up or down depending on some temperature readings when necessary.

I have used two bpa16 600w triacs for the job, with an interrupt for zero-cross detection and by "cutting away" part of the sine wave I can effectively control the speed.

The problem I am having is the buzzing noise which turns out to be louder than I had anticipated.

I have read about other projects for controlling a motor similar to this, but it doesn't seem very simple. The simplest solution I have found is using a multipoint tranformer and a bunch of relays, to achieve various fixed speeds. Any other solution seems electronically complex.

I've seen this universal controller, which also seems simple, and might suit my needs.

Does anybody know of an already built shield for arduino to achieve this? Or something that might do the trick? And cheap?

Thank you very much.

It could be your code. Post it (using code tags) if you want further help.

If your triac triggering skips a cycle or some other anomaly, it could cause the motor noise. The only way to know with certainty is to use an oscilloscope and look at the motor waveform but that is not something I would recommend unless you have a differential voltage probe and you’re comfortable with mains.

Have you looked at the code for the other project? The triggering logic would be the same so it may be worth studying how the triac is controlled and how the zero crossing signal is handled.

Hello again and thank you for your help.

Here's my code. Unfinished and a mess.

It's a project meant to control two fans for a heat recovery system depending on the readings of three temperature sensors, dallas d18b20. They are slowing the loop considerably, apparently it's due to the library if what I've read is correct. I don't know if there's a solution for that.

For the motor speed variation I have seen various examples on the internet and chose one that worked initially with one fan only. After adding all the rest of the code, and adding another fan it started to make the noise.

The way I have decided to make it work is to adjust the fan speed depending on the efficiency of the heat recovery, prioritizing the heat recovery over air renewal/removal.

I am learning, so I don't mind if you make fun of my very crude programming. More than half of the code is just the crude menu system I came up with, which is probably far from ideal, but my own creation.

I hope it's not too long.

After waiting five minutes for being a newbie here, I am getting an error that says I am exceeding the 9000 character limit. I am going to have to try and summarize the code it for you to read. I will start by removing all the comments and see if it fits. And that'll take me more than five minutes :)

Stripped out more than half the code related to the lcd menu system and data input.

#include <OneWire.h>
#include <DallasTemperature.h>
#include <TimerOne.h>
#include <LiquidCrystal.h>

///////////////////// USER ADJUSTABLE VARIABLES /////////////////////////////////////////////

int thresLO = 10;        // Temperature threshold variable (Multiply by 10 to have increased precision using integers)
int thresHI = 31;        // Temperature threshold variable 
int target = 22;         // Target temperature
int tardev = 2;          // Target temp. deviation
int spdmax=0;            // Fan maximum speed variable 0=max 128=min
int spdmin=60;           // Fan minimum speed variable 0=max 128=min
int spdoff=128;          // Fan off speed variable 0=max 128=min
int spddiff=0;           // Difference in speed between air renewal and removal fan. 
int minimumspd = 60;     // The minimum speed at which the fans rotate.

/////////////////////// VARIABLES THAT SHOULDN'T BE TOUCHED /////////////////////////////////

//LCD pins to Arduino
const int pin_RS = 8; 
const int pin_EN = 9; 
const int pin_d4 = 4; 
const int pin_d5 = 5; 
const int pin_d6 = 6; 
const int pin_d7 = 7; 
const int pin_BL = 10; 
LiquidCrystal lcd( pin_RS,  pin_EN,  pin_d4,  pin_d5,  pin_d6,  pin_d7);

int x;                          //  Pressed button variable
const int AC_pinREN = 11;       // Output to Opto Triac for air renewal fan
const int AC_pinREM = 3;        // Output to Opto Triac for air removal fan
unsigned long now=millis();     // Timing variable 
volatile int y=0;               // Variable to use as a counter volatile as it is in an interrupt
volatile boolean zero_cross=0;  // Boolean to store a "switch" to tell us if we have crossed zero
int ren = 128;                  // Speed level for air renewal (0-128)  0 = full speed, 128 = 0ff
int rem = 128;                  // Speed level for air removal fan (0-128)  0 = full speed, 128 = 0ff
int avg = 100;
int avg1 = 100;
int avg2 = 100;
int avg3 = 100;
double tempOUTSIDE=0;           // Outside temperature variable
double tempINSIDE=0;            // Inside temperature variable
double tempOUTGOING=0;          // Outgoing temperature variable
double meff = 100;              // Measured efficiency.
const int DSdatapin = 12;       // Pin where the DS18B20 sensors are connected
const int freqStep = 80;        // This is the delay-per-brightness step in microseconds. This probably needs tweaking. In my case this value worked best.
                                // To calculate freqStep divide the length of one full half-wave of the power
                                // cycle (in microseconds) by the number of brightness steps. 
                                //// For 60 Hz it should be 65
                                // It is calculated based on the frequency of your voltage supply (50Hz or 60Hz)
                                // and the number of brightness steps you want. 
                                // Realize that there are 2 zerocrossing per cycle. This means
                                // zero crossing happens at 120Hz for a 60Hz supply or 100Hz for a 50Hz supply. 
                                // (120 Hz=8333uS) / 128 brightness steps = 65 uS / brightness step
                                // (100Hz=10000uS) / 128 steps = 75uS/step

OneWire oneWireObjeto(DSdatapin);                   // Start OneWire instance
DallasTemperature sensorDS18B20(&oneWireObjeto);    // Start DallasTemperature instance

int MENU=0;                       // Current Menu
int MENUMAX=8;                    // Max menu entries
int VIEW=1;                       // Current View 
int VIEWMAX=3;                    // Max number of views
int buttondelay=100;              // Small delay to avoid key press fast repetition
int sel=0;                        // Select button press 
int up=0;                         // Up button press
int down=0;                       // Down button press
int left=0;                       // Left button press
int right=0;                      // Right button press
unsigned long backlight=millis(); // 
unsigned long backlighttime=600000;//
unsigned long ontime=0;           //  


void setup() {                                      // Begin setup
  Serial.begin(9600);                               // Set baud rate
  sensorDS18B20.begin();                            // Start 1-Wire bus
  pinMode(AC_pinREN, OUTPUT);                       // Set the Triac pin as output
  pinMode(AC_pinREM, OUTPUT);                       // Set the 2nd Triac pin as output
  attachInterrupt(0, zero_cross_detect, RISING);    // Attach an Interupt to Pin 2 (interupt 0) for Zero Cross Detection
  Timer1.initialize(freqStep);                      // Initialize TimerOne library for the freq we need
  Timer1.attachInterrupt(ren_check, freqStep);      // Use the TimerOne Library to attach an interrupt
                                                    // to the function we use to check to see if it is 
                                                    // the right time to fire the triac.  This function 
                                                    // will now run every freqStep in microseconds. 
lcd.begin(16, 2);
lcd.print("Please wait...");
lcd.print(" ");  


void zero_cross_detect() {          
  zero_cross = true;                                // set the boolean to true to tell our renming function that a zero cross has occured
  digitalWrite(AC_pinREN, LOW);                     // turn off TRIAC (and AC)
  digitalWrite(AC_pinREM, LOW);                     // turn off 2nd TRIAC


void ren_check() {                                   // Turn on the TRIAC at the appropriate time            
  if(y>=ren) {                     
      digitalWrite(AC_pinREN, HIGH);                 // turn on air renewal fan       
  if(y>=rem) {                     
      digitalWrite(AC_pinREM, HIGH);                 // turn on air removal fan       
      y++;                                           // increment time step counter                     


void loop() {

sensorDS18B20.requestTemperatures();                                                          // Asking sensors for temperature...
tempOUTSIDE = (sensorDS18B20.getTempCByIndex(0));                                          // Setting temp variable to actual temperature
tempOUTGOING = (sensorDS18B20.getTempCByIndex(1));                                           // Setting temp variable to actual temperature
tempINSIDE = (sensorDS18B20.getTempCByIndex(2));                                         // Setting temp variable to actual temperature


if ((tempOUTSIDE >= thresHI) || (tempOUTSIDE <= thresLO)){ren=spdoff;rem=spdoff;}

if ((tempOUTSIDE >= ( target - tardev )) && (tempOUTSIDE <= (target + tardev))){ren = spdmax; rem = spdoff;}


  if ((abs(tempOUTSIDE-tempINSIDE))<0){meff = 100;}
       {meff = abs(100-(100/(abs(tempOUTSIDE-tempINSIDE))*(abs(tempOUTSIDE-tempOUTGOING))));} 

  if (meff>100){meff=100;}

  if (meff<0){meff=0;}


  if (ren < spdmax){ren=spdmax;}

  if (ren > spdmin){ren=spdmin;}



if (rem > minimumspd){rem=spdoff;}


Where does zero_cross get reset?

Yes, sorry about that. I am very new to this arduino stuff really, and my programming is still a bit of a mess. That "zero_cross = true;" was from a previous version where I just copied stuff from one place to another. There was an if statement later on using that variable in the ren_check subroutine (is that how you call it?). After a bit of thinking about how it worked, I thought that was unnecesssary.

I just edited my code slightly, but haven't tested it yet. Shouldn't this work?

void zero_cross_detect() {          
  digitalWrite(AC_pinREN, LOW);                     // turn off TRIAC (and AC)
  digitalWrite(AC_pinREM, LOW);                     // turn off 2nd TRIAC

void ren_check() {                                   // Turn on the TRIAC at the appropriate time            

  if(y>=ren) {                     
      digitalWrite(AC_pinREN, HIGH);                 // turn on air renewal fan       
  if(y>=rem) {                     
      digitalWrite(AC_pinREM, HIGH);                 // turn on air removal fan       
      y++;                                           // increment time step counter                     

When void zero_cross_detect() runs, y = 0, and that's all is needed to be reset. Am I wrong?

After that, void ren_check() runs every freqstep microseconds (80us) and increments y by one with y++. The two triacs for the two fans are off until the variables ren or rem are equal or greater than variable y.

I think it's the really sharp rising edge of the sine wave when the triacs are turned on that causes harmonics to make the coils to vibrate and cause the very audible noise. A pure 50hz sine wave, which can also be heard when the fan is plugged straight into the 220 mains, but it diminishes as the fan picks up speed. But a 50Hz sine wave is less audible because our hearing is less sensitive to that frequency range. But the chopped sine wave has lots of harmonics in the order or hundreds of hertz, making the noise more audible. I guess that's what's happening.

eddie3000: The problem I am having is the buzzing noise which turns out to be louder than I had anticipated.

Induction motors don't take kindly to triac control, as you are discovering - you'll need a suitable snubber circuit to protect the triac from re-triggering and from over-dissipation due to inductive voltage step on switch-off.

Some induction motors stall and burn up if you try to run them slowly this way - they simply aren't designed to run slowly or with distorted waveforms.

All induction motors are very inefficient (ie get hot) when not running at the nominal speed set by the the mains frequency - this is why industrial controllers use V/f method, varying the frequency and voltage together to set the speed while maintaining the same current. Even then a motor can burn up at low speed simply because its not self-cooling enough.