Project tutorial
Arduino-Based Universal AC Motor Speed Controller

Arduino-Based Universal AC Motor Speed Controller © GPL3+

Closed loop universal motor control system using PID algorithm.

  • 3,290 views
  • 42 comments
  • 14 respects

Components and supplies

About this project

Introduction

WARNING!!! At first, I'll write a quotation:

STOP!!! This circuit is attached to a 110-220 voltage. Do not build this if you are not confident about what you are doing. Unplug it before coming even close to the PCB. The cooling plate of the Triac is attached to the mains. Do not touch it while in operation. Put it in a proper enclosure/container.

WAIT!!! Let me just add a stronger warning here: This circuit is safe if it is built and implemented only by people who know what they are doing. If you have no clue or if you are doubting about what you do, chances are you are going to be DEAD!!! DO NOT TOUCH WHEN IT IS CONNECTED TO THE GRID!!!

Now let me introduce my project. It's an Arduino-controlled motor speed regulator which uses phase cut dimming method and PID algorithm.

Controller main features:

  • Two speed ranges for quicker desired RPM change.
  • Rotary encoder lets to set desired RPM before motor start.
  • Push button of encoder starts and stops the motor.
  • 2x16 LCD display for status and RPM display.
  • Motor soft start.
  • Keeps RPM and torque at load.
  • Speed and torque control by PID algorithm.
  • Motor stuck (or speed sensor malfunction) protection.
  • Overspeed (usually when triac is damaged) protection.

There is the video where you can watch the controller at work:

Motor stuck protection at work:

How it all started

After watching this video (in Russian language):

I decided to build a similar lathe. And successfully repeated this project. With some changes of course. Only one thing left - the motor. At first I used the asynchronous one phase motor with running capacitor. Main disadvantages of this kind of motor:

  • No cheap speed adjustment. Nor mechanical, nor electronic. Have to use set of pulleys or expensive electronic controller.
  • Limited speed - only 1400 RPM.
  • Limited working time - 10 minutes run / 6 minutes idle. Otherwise it goes hot.

As you could notice the guy from video used the motor which was salvaged from old washing machine. The same motor I had in my workshop. Only one thing left - the motor speed controller. Without it the motor will spin at it's maximum 15000-19000 RPM. It's too much for wood lathe. To control the motor peed we could use a SCR voltage regulator, but at low RPM the motor will be weak with no torque. Fortunately this kind of motors has tacho sensors and we can do a closed loop system to have a stable RPM even at load and control the torque.

Searching for solution

There is well known chip TDA1085 which is specially designed to control motors with speed sensors speed. But I didn't had this chip and to see the RPM I had to make a tachometer. At Chinese stories I found a cheap AC motor speed controller with RPM stabilization feature. I bought one and tested. Everything is fine except few things:

  • Only 400W. (could be increased by changing the triac)
  • Max RPM is 1450! After my used pulleys will be around 480 RPM only!
  • No RPM display.

After surfing the internet I found several speed controller projects and decided to make my own controller using found ideas.

There is a list of resources I used:

  • A lot of theory. Also I used a tacho sensing circuit part from here.
  • Also NXP application note. Lots of useful information.
  • Some theory, useful code and schematic is here.
  • Got some ideas and took some pieces of code from here (Russian).
  • Dimming code I used from here (IMHO the best dimmer).
  • RPM counting code took from here (Russian).
  • Took some pieces of PID usage code from here.
  • The PID library.
  • The PID library description. Also here.
  • Some useful information about PID library usage.

Schematic and components

I won't give a theory how AC phase cut works because there is nothing new. I provided some links with dimming and motor control theory above (the first and the second link). NXP and Microchip has lot of useful information about controlling motors.

The schematic diagram drawn as separate blocks:

  • Arduino Nano V3
  • 16x2 HD44780 LCD with PCF8574 I2C module. (Given module schematic is not exact!).
  • Tacho pulses detection. Uses LM393 comparator to convert tacho pulses to microcontroller level.
  • Zero cross detection. Every time when AC line crosses zero point microcontroller receives a signal. High voltage circuit is isolated from microcontroller by using optocoupler.
  • Relay control circuit made by using simple NPN switching transistor.
  • Motor control circuit is isolated by optocoupler and uses a triac with snubber circuit (C4, R14). It's possible to use a snubberless triacs (no C4 and R14 required then).
  • AC/DC power module. 5V, 0.5-1A is enough. I used an old phone USB charger.
  • Rotary encoder, 10A power line switch with indication, any 3 position switch for RPM range switching.

All components are soldered on protoboard. For more controllers I'll trace a PCB. Some pics:

I used a BTA41 triac because I had this in my stock. It's possible to use a 10-16 Amperes triac. I.e. BTA16.

Full listing of used components you could find in txt file in the zip archive.

Construction

I had the plastic enclosure in my workshop which met my requirements. So I used it for this project. The box dimensions: H 150mm (~5.9"), W 70mm (~2.76"), L 110mm (~4.33"),

Few words about code

I tried a lot of motor control and phase cut synchronization algorithms but most of them had own disadvantages. Motor control wasn't stable. Sometimes it jumped during start, sometimes during run. Sometimes motor ran till its maximum RPM for unknown reason. Finally I decided to use and understand a PID control method.

The code uses 2 external interrupts. One for zero crossing, one for tacho sensor. A timer for triac pulses delay control. A PID algorithm for output control in relation of setpoint and input. For soft motor start I made a RAMP acceleration algorithm. During start PID parameters has lower values and returns to normal values during motor run. This prevents against hard motor starting (jumping).

LCD refresh interval is 2 seconds. It's enough to observe real RPM change. Making this faster can affect the system stability. It's because the LCD library uses delay functions.

I used a lot of global variables to simplify a system tuning by your needs and different motors. Later I'll include test and tuning sketches into the archive.

All used libraries can be found in the zip archive.

Conclusion

I'm happy how my DIY controller works. Now I need to mount the motor on lathe and test in real environment.

I want to thank colleagues from Arduino groups on Facebook for help. And thank my wife for patience :D

Comments an questions are welcome.

Sorry for my English. ;-)

Update

I added one new parameter to my code. It's pulley ratio. In my case it's 2.96. It's difference between smaller pulley on the motor and a bigger on the spindle. Pulleys I used were salvaged from dumped vehicles. Use a sketch without ratio parameter or set it to 1 if no pulleys will be used.

I mounted the motor on my lathe and tested it a little. I'm happy. Everything works like expected. It's enough torque even at low speeds.

Soon I will make a cover for motor, holder for control box and etc.

Code

Universal_AC_motor_PID_control.inoC/C++
#include <avr/io.h>
#include <avr/interrupt.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Rotary.h>
#include <PID_v1.h>

#define TACHO 3            // tacho signals input pin
#define DETECT 2           // zero cross detect pin
#define GATE 17            // TRIAC gate pin
#define RANGE1 9           // range one switch pin
#define RANGE2 10          // range two switch pin
#define BUTTON 4           // rottary encoder button pin
#define RELAY 5            // relay pin
#define PULSE 2            // number of triac trigger pulse width counts. One count is 16 microseconds
#define TACHOPULSES 8      // number of pulses per revolution 

unsigned int RPM;                   // real rpm variable
unsigned int count;                 // tacho pulses count variable
unsigned int lastcount = 0;         // additional tacho pulses count variable
unsigned long lastcounttime = 0;
unsigned long lastflash;
unsigned long lastpiddelay = 0;
unsigned long previousMillis = 0;
unsigned long lastDebounceTime = 0;

const int sampleRate = 1;           // Variable that determines how fast our PID loop
const int rpmcorrection = 86;       // had to add this parameter to have real RPM equal to desired RPM
const int lcdinterval = 2000;       // lcd refresh interval in milliseconds
const int protection = 2000;        // protection will switch on when real rpm exceeds desired by value
const int debounceDelay = 50;       // the debounce time; increase if the output flickers
const int minoutputlimit = 80;      // limit of PID output
const int maxoutputlimit = 540;     // limit of PID output
const int mindimminglimit = 80;     // the shortest delay before triac fires
const int maxdimminglimit = 625;    // for 60Hz will be 520
const int minrpmR1 = 300;           // min RPM of the range 1
const int maxrpmR1 = 1500;          // max RPM of the range 1
const int minrpmR2 = 1500;          // min RPM of the range 2
const int maxrpmR2 = 3500;          // max RPM of the range 2
const int risetime = 100;           // RPM rise time delay in microseconds (risetime x RPM)

int dimming = 540;                  // this should be the same as maxoutputlimit
int counterR1;                      // desired RPM counter for range 1
int counterR2;                      // desired RPM counter for range 2
int desiredRPM;
int tempcounter = 100;

byte range;
byte lastRangeState = 0;
byte relayState = LOW;              // the current state of the relay pin
byte buttonState;                   // the current reading from the input pin
byte lastButtonState = HIGH;        // the previous reading from the input pin

bool loopflag = false;              // flag for soft start
bool startflag = false;             // flag for motor start delay
bool runflag = false;               // flag for motor running state

double Setpoint, Input, Output;       // define PID variables
double sKp = 0.1, sKi = 0.2, sKd = 0; // PID tuning parameters for starting motor
double rKp = 0.25, rKi = 1, rKd = 0;  // PID tuning parameters for runnig motor

LiquidCrystal_I2C lcd(0x26, 16, 2);   // set the LCD address to 0x26 for a 16 chars and 2 line display
Rotary r = Rotary(12, 11);            // define rottary encoder and pins
PID myPID(&Input, &Output, &Setpoint, sKp, sKi, sKd, DIRECT); // define PID variables and parameters

void setup() {
  Serial.begin(115200);
  // set up pins
  pinMode(BUTTON, INPUT);             // set the button pin
  pinMode(RELAY, OUTPUT);             // set the relay  pin
  pinMode(DETECT, INPUT);             // set the zero cross detect pin
  pinMode(GATE, OUTPUT);              // set the TRIAC gate control pin
  pinMode(TACHO, INPUT);              // set the tacho pulses detect pin
  pinMode(RANGE1, INPUT);             // set the range 1 switch pin
  pinMode(RANGE2, INPUT);             // set the range 1 switch pin
  digitalWrite(BUTTON, HIGH);         // turn on pullup resistors
  digitalWrite(RANGE1, HIGH);         // turn on pullup resistors
  digitalWrite(RANGE2, HIGH);         // turn on pullup resistors
  digitalWrite(RELAY, relayState);    // initialize relay output

  counterR1 = minrpmR1;               // assign start value for range 1
  counterR2 = minrpmR2;               // assign start value for range 2
  Input = 200;                        // asiign initial value for PID
  Setpoint = 200;                     // asiign initial value for PID

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(minoutputlimit, maxoutputlimit);
  myPID.SetSampleTime(sampleRate);    // Sets the sample rate

  // set up Timer1
  OCR1A = 100;                        // initialize the comparator
  TIMSK1 = 0x03;                      // enable comparator A and overflow interrupts
  TCCR1A = 0x00;                      // timer control registers set for
  TCCR1B = 0x00;                      // normal operation, timer disabled

  // set up zero crossing interrupt IRQ0 on pin 2.
  // set up tacho sensor interrupt IRQ1 on pin3
  attachInterrupt(0, zeroCrossingInterrupt, RISING);
  attachInterrupt(1, tacho, FALLING);

  lcd.init();        // initialize the lcd
  lcd.backlight();   // turn on the backlight

  // check the RPM range state at startup and display it
  int rangeOne = digitalRead(RANGE1);
  int rangeTwo = digitalRead(RANGE2);

  if (rangeOne == 1 && rangeTwo == 1) {
    range = 0;
    range0();
  }
  if (rangeOne == 0 && rangeTwo == 1) {
    range = 1;
    RPMrange1();
  }
  if (rangeOne == 1 && rangeTwo == 0) {
    range = 2;
    RPMrange2();
  }
}

// Interrupt Service Routines
void zeroCrossingInterrupt() { // zero cross detect
  TCCR1B = 0x04;               // start timer with divide by 256 input
  TCNT1 = 0;                   // reset timer - count from zero
  OCR1A = dimming;             // set the compare register brightness desired.
}


ISR(TIMER1_COMPA_vect) {       // comparator match
  if (startflag == true) {     // flag for start up delay
    digitalWrite(GATE, HIGH);  // set TRIAC gate to high
    TCNT1 = 65536 - PULSE;     // trigger pulse width
  }
}

ISR(TIMER1_OVF_vect) {         // timer1 overflow
  digitalWrite(GATE, LOW);     // turn off TRIAC gate
  TCCR1B = 0x00;               // disable timer stops unintended triggers
}

// RPM counting routine
void tacho() {
  count++;
  unsigned long tachotime = micros() - lastflash;
  float time_in_sec  = ((float)tachotime + rpmcorrection) / 1000000;
  float prerpm = 60 / time_in_sec;
  RPM = prerpm / TACHOPULSES;
  lastflash = micros();
}

void loop() {

  // check the RPM range switch state
  int rangeOne = digitalRead(RANGE1);
  int rangeTwo = digitalRead(RANGE2);

  if (rangeOne == 1 && rangeTwo == 1) {
    range = 0;
  }
  if (rangeOne == 0 && rangeTwo == 1) {
    range = 1;
    desiredRPM = counterR1;
  }
  if (rangeOne == 1 && rangeTwo == 0) {
    range = 2;
    desiredRPM = counterR2;
  }

  // check the RPM range switch state changes
  if (range != lastRangeState) {
    if (range == 0) {
      range0();
      runflag = false;
      startflag = false;            // flag to turn off triac before relay turns off
      delay (300);                  // delay to prevent sparks on relay contacts
      digitalWrite(RELAY, LOW);
      relayState = LOW;
    }
    if (range == 1) {
      RPMrange1();
    }
    if (range == 2) {
      RPMrange2();
    }
    if (relayState == LOW && range != 0) {
      motorStateStop();
    }
  }
  lastRangeState = range;

  // check the start / stop button state
  if (range != 0) {
    int reading = digitalRead(BUTTON); // read the state of the switch into a local variable:
    if (reading != lastButtonState) {  // If the switch changed, due to noise or pressing
      lastDebounceTime = millis();     // reset the debouncing timer
    }
    if ((millis() - lastDebounceTime) > debounceDelay) {
      if (reading != buttonState) {     // if the button state has changed:
        buttonState = reading;
        if (buttonState == LOW) {       // only toggle the relay if the new button state is LOW
          relayState = !relayState;
          if (relayState == HIGH) {
            loopflag = true;
            digitalWrite(RELAY, relayState); // set the Relay:
            delay (300);                     // delay to prevent sparks on relay contacts
            startflag = true;                // flag to start motor
          }
          if (relayState == LOW) {
            Setpoint = 200;
            Input = 200;
            runflag = false;
            startflag = false;
            delay (300);                     // delay to prevent sparks on relay contacts
            digitalWrite(RELAY, relayState); // set the Relay:
            motorStateStop();
          }
        }
      }
    }
    lastButtonState = reading;            // save the reading. Next time through the loop, it'll be the lastButtonState:
  }

  //rotarry encoder process
  unsigned char result = r.process();
  if (range == 1 && result == DIR_CW) {
    if (counterR1 >= 500)
    {
      counterR1 += 50;
    }
    else counterR1 += 20;
    if (counterR1 >= maxrpmR1) {
      counterR1 = maxrpmR1;
    }
    RPMrange1();
  }

  else if (range == 1 && result == DIR_CCW) {
    if (counterR1 <= 500)
    {
      counterR1 -= 20;
    }
    else counterR1 -= 50;
    if (counterR1 <= minrpmR1) {
      counterR1 = minrpmR1;
    }
    RPMrange1();
  }

  if (range == 2 && result == DIR_CW) {
    counterR2 += 100;
    if (counterR2 >= maxrpmR2) {
      counterR2 = maxrpmR2;
    }
    RPMrange2();
  }
  else if (range == 2 && result == DIR_CCW) {
    counterR2 -= 100;
    if (counterR2 <= minrpmR2) {
      counterR2 = minrpmR2;
    }
    RPMrange2();
  }

  //soft start
  if (loopflag == true) {
    myPID.SetTunings(sKp, sKi, sKd);        // Set the PID gain constants and start
    int i = (desiredRPM - tempcounter);
    for (int j = 1; j <= i; j++) {
      Input = RPM;
      Setpoint = tempcounter;
      myPID.Compute();
      dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // inverse the output
      dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
      tempcounter++;
      delayMicroseconds (risetime);
    }
    if (tempcounter >= desiredRPM) {
      lastcounttime = millis();
      lastpiddelay = millis();
      loopflag = false;
      runflag = true;
      tempcounter = 100;
    }
  }

  // normal motor running state
  if (relayState == HIGH && loopflag == false) {
    unsigned long piddelay = millis();

    if ((piddelay - lastpiddelay) > 1000) {     // delay to switch PID values. Prevents hard start
      myPID.SetTunings(rKp, rKi, rKd);          // Set the PID gain constants and start
      lastpiddelay = millis();
    }

    Input = RPM;
    Setpoint = desiredRPM;
    myPID.Compute();
    dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // reverse the output
    dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
  }
  // diagnose a fault and turn on protection

  unsigned long counttime = millis();
  if (counttime - lastcounttime >= 1000) {
    if (count == 0 && relayState == HIGH && runflag == true) {
      startflag = false;            // flag to turn off triac before relay turns off
      delay (300);                  // delay to prevent sparks on relay contacts
      digitalWrite(RELAY, LOW);
      relayState = LOW;
      stuckerror();
    }
    lastcount = count;
    count = 0;
    lastcounttime = millis();
  }

  //reset rpm after motor stops
  if (count == 0 && relayState == LOW) {
    RPM = 0;
  }

  // protection against high rpm. i e triac damage
  if (relayState == HIGH && RPM > desiredRPM + protection) {
    startflag = false;            // flag to turn off triac before relay turns off
    delay (300);                  // delay to prevent sparks on relay contacts
    digitalWrite(RELAY, LOW);
    relayState = LOW;
    exceederror();
  }
  // real RPM display
  if (relayState == HIGH && range != 0) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= lcdinterval) {
      previousMillis = currentMillis;
      int rpmdisplay = RPM;
      lcd.setCursor(0, 1);
      lcd.print("Real RPM:   ");
      if (rpmdisplay >= 1000) {
        lcd.setCursor(12, 1);
        lcd.print(rpmdisplay);
      }
      else if (RPM < 1000) {
        lcd.setCursor(12, 1);
        lcd.print(" ");
        lcd.setCursor(13, 1);
        lcd.print(rpmdisplay);
      }
    }
  }
}

void range0() {
  lcd.setCursor(0, 0);
  lcd.print(" Please  select ");
  lcd.setCursor(0, 1);
  lcd.print(" the RPM range! ");
}

void motorStateStop() {
  lcd.setCursor(0, 1);
  lcd.print ("  Press  START  ");

}

void RPMrange1() {
  lcd.setCursor(0, 0);
  lcd.print("R1 RPM set: ");
  if (counterR1 >= 1000) {
    lcd.setCursor(12, 0);
    lcd.print(counterR1);
  }
  else {
    lcd.setCursor(12, 0);
    lcd.print(" ");
    lcd.setCursor(13, 0);
    lcd.print(counterR1);
  }
}

void RPMrange2() {
  lcd.setCursor(0, 0);
  lcd.print("R2 RPM set: ");
  lcd.setCursor(12, 0);
  lcd.print(counterR2);
}

void exceederror() {
  lcd.clear();
  while (1) {
    lcd.setCursor(5, 0);
    lcd.print("ERROR!");
    lcd.setCursor(2, 1);
    lcd.print("TRIAC DAMAGE");
  }
}

void stuckerror() {
  lcd.clear();
  while (1) {
    lcd.setCursor(5, 0);
    lcd.print("ERROR!");
    lcd.setCursor(2, 1);
    lcd.print("MOTOR STUCK!");
  }
}
Universal_AC_motor_PID_control_pulley_ratio.inoC/C++
#include <avr/io.h>
#include <avr/interrupt.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Rotary.h>
#include <PID_v1.h>

#define TACHO 3            // tacho signals input pin
#define DETECT 2           // zero cross detect pin
#define GATE 17            // TRIAC gate pin
#define RANGE1 9           // range one switch pin
#define RANGE2 10          // range two switch pin
#define BUTTON 4           // rottary encoder button pin
#define RELAY 5            // relay pin
#define PULSE 2            // number of triac trigger pulse width counts. One count is 16 microseconds
#define TACHOPULSES 8      // number of pulses per revolution 

unsigned int RPM;                   // real rpm variable
unsigned int count;                 // tacho pulses count variable
unsigned int lastcount = 0;         // additional tacho pulses count variable
unsigned long lastcounttime = 0;
unsigned long lastflash;
unsigned long lastpiddelay = 0;
unsigned long previousMillis = 0;
unsigned long lastDebounceTime = 0;

const int sampleRate = 1;           // Variable that determines how fast our PID loop
const int rpmcorrection = 86;       // had to add this parameter to have real RPM equal to desired RPM
const int lcdinterval = 2000;       // lcd refresh interval in milliseconds
const int protection = 2000;        // protection will switch on when real rpm exceeds desired by value
const int debounceDelay = 50;       // the debounce time; increase if the output flickers
const int minoutputlimit = 80;      // limit of PID output
const int maxoutputlimit = 540;     // limit of PID output
const int mindimminglimit = 80;     // the shortest delay before triac fires
const int maxdimminglimit = 625;    // for 60Hz will be 520
const int minrpmR1 = 300;           // min RPM of the range 1
const int maxrpmR1 = 1500;          // max RPM of the range 1
const int minrpmR2 = 1500;          // min RPM of the range 2
const int maxrpmR2 = 3500;          // max RPM of the range 2
const int risetime = 100;           // RPM rise time delay in microseconds (risetime x RPM)

int dimming = 540;                  // this should be the same as maxoutputlimit
int counterR1;                      // desired RPM counter for range 1
int counterR2;                      // desired RPM counter for range 2
int desiredRPM;
int tempcounter = 100;

byte range;
byte lastRangeState = 0;
byte relayState = LOW;              // the current state of the relay pin
byte buttonState;                   // the current reading from the input pin
byte lastButtonState = HIGH;        // the previous reading from the input pin

bool loopflag = false;              // flag for soft start
bool startflag = false;             // flag for motor start delay
bool runflag = false;               // flag for motor running state

double ratio = 2.96;                  // pulley ratio. Set to 1 if no pulley
                                      // ratio needed (must be higher than 0!)
double Setpoint, Input, Output;       // define PID variables
double sKp = 0.1, sKi = 0.2, sKd = 0; // PID tuning parameters for starting motor
double rKp = 0.25, rKi = 1, rKd = 0;  // PID tuning parameters for runnig motor

LiquidCrystal_I2C lcd(0x26, 16, 2);   // set the LCD address to 0x26 for a 16 chars and 2 line display
Rotary r = Rotary(12, 11);            // define rottary encoder and pins
PID myPID(&Input, &Output, &Setpoint, sKp, sKi, sKd, DIRECT); // define PID variables and parameters

void setup() {
  Serial.begin(115200);
  // set up pins
  pinMode(BUTTON, INPUT);             // set the button pin
  pinMode(RELAY, OUTPUT);             // set the relay  pin
  pinMode(DETECT, INPUT);             // set the zero cross detect pin
  pinMode(GATE, OUTPUT);              // set the TRIAC gate control pin
  pinMode(TACHO, INPUT);              // set the tacho pulses detect pin
  pinMode(RANGE1, INPUT);             // set the range 1 switch pin
  pinMode(RANGE2, INPUT);             // set the range 1 switch pin
  digitalWrite(BUTTON, HIGH);         // turn on pullup resistors
  digitalWrite(RANGE1, HIGH);         // turn on pullup resistors
  digitalWrite(RANGE2, HIGH);         // turn on pullup resistors
  digitalWrite(RELAY, relayState);    // initialize relay output

  counterR1 = minrpmR1;               // assign start value for range 1
  counterR2 = minrpmR2;               // assign start value for range 2
  Input = 200;                        // asiign initial value for PID
  Setpoint = 200;                     // asiign initial value for PID

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(minoutputlimit, maxoutputlimit);
  myPID.SetSampleTime(sampleRate);    // Sets the sample rate

  // set up Timer1
  OCR1A = 100;                        // initialize the comparator
  TIMSK1 = 0x03;                      // enable comparator A and overflow interrupts
  TCCR1A = 0x00;                      // timer control registers set for
  TCCR1B = 0x00;                      // normal operation, timer disabled

  // set up zero crossing interrupt IRQ0 on pin 2.
  // set up tacho sensor interrupt IRQ1 on pin3
  attachInterrupt(0, zeroCrossingInterrupt, RISING);
  attachInterrupt(1, tacho, FALLING);

  lcd.init();        // initialize the lcd
  lcd.backlight();   // turn on the backlight

  // check the RPM range state at startup and display it
  int rangeOne = digitalRead(RANGE1);
  int rangeTwo = digitalRead(RANGE2);

  if (rangeOne == 1 && rangeTwo == 1) {
    range = 0;
    range0();
  }
  if (rangeOne == 0 && rangeTwo == 1) {
    range = 1;
    RPMrange1();
  }
  if (rangeOne == 1 && rangeTwo == 0) {
    range = 2;
    RPMrange2();
  }
}

// Interrupt Service Routines
void zeroCrossingInterrupt() { // zero cross detect
  TCCR1B = 0x04;               // start timer with divide by 256 input
  TCNT1 = 0;                   // reset timer - count from zero
  OCR1A = dimming;             // set the compare register brightness desired.
}


ISR(TIMER1_COMPA_vect) {       // comparator match
  if (startflag == true) {     // flag for start up delay
    digitalWrite(GATE, HIGH);  // set TRIAC gate to high
    TCNT1 = 65536 - PULSE;     // trigger pulse width
  }
}

ISR(TIMER1_OVF_vect) {         // timer1 overflow
  digitalWrite(GATE, LOW);     // turn off TRIAC gate
  TCCR1B = 0x00;               // disable timer stops unintended triggers
}

// RPM counting routine
void tacho() {
  count++;
  unsigned long time = micros() - lastflash;
  float time_in_sec  = ((float)time + rpmcorrection) / 1000000;
  float prerpm = 60 / time_in_sec;
  RPM = prerpm / TACHOPULSES;
  lastflash = micros();
}

void loop() {

  // check the RPM range switch state
  int rangeOne = digitalRead(RANGE1);
  int rangeTwo = digitalRead(RANGE2);

  if (rangeOne == 1 && rangeTwo == 1) {
    range = 0;
  }
  if (rangeOne == 0 && rangeTwo == 1) {
    range = 1;
    desiredRPM = counterR1 * ratio;
  }
  if (rangeOne == 1 && rangeTwo == 0) {
    range = 2;
    desiredRPM = counterR2 * ratio;
  }

  // check the RPM range switch state changes
  if (range != lastRangeState) {
    if (range == 0) {
      range0();
      runflag = false;
      startflag = false;            // flag to turn off triac before relay turns off
      delay (300);                  // delay to prevent sparks on relay contacts
      digitalWrite(RELAY, LOW);
      relayState = LOW;
    }
    if (range == 1) {
      RPMrange1();
    }
    if (range == 2) {
      RPMrange2();
    }
    if (relayState == LOW && range != 0) {
      motorStateStop();
    }
  }
  lastRangeState = range;

  // check the start / stop button state
  if (range != 0) {
    int reading = digitalRead(BUTTON); // read the state of the switch into a local variable:
    if (reading != lastButtonState) {  // If the switch changed, due to noise or pressing
      lastDebounceTime = millis();     // reset the debouncing timer
    }
    if ((millis() - lastDebounceTime) > debounceDelay) {
      if (reading != buttonState) {     // if the button state has changed:
        buttonState = reading;
        if (buttonState == LOW) {       // only toggle the relay if the new button state is LOW
          relayState = !relayState;
          if (relayState == HIGH) {
            loopflag = true;
            digitalWrite(RELAY, relayState); // set the Relay:
            delay (300);                     // delay to prevent sparks on relay contacts
            startflag = true;                // flag to start motor
          }
          if (relayState == LOW) {
            Setpoint = 200;
            Input = 200;
            runflag = false;
            startflag = false;
            delay (300);                     // delay to prevent sparks on relay contacts
            digitalWrite(RELAY, relayState); // set the Relay:
            motorStateStop();
          }
        }
      }
    }
    lastButtonState = reading;            // save the reading. Next time through the loop, it'll be the lastButtonState:
  }

  //rotarry encoder process
  unsigned char result = r.process();
  if (range == 1 && result == DIR_CW) {
    if (counterR1 >= 500)
    {
      counterR1 += 50;
    }
    else counterR1 += 20;
    if (counterR1 >= maxrpmR1) {
      counterR1 = maxrpmR1;
    }
    RPMrange1();
  }

  else if (range == 1 && result == DIR_CCW) {
    if (counterR1 <= 500)
    {
      counterR1 -= 20;
    }
    else counterR1 -= 50;
    if (counterR1 <= minrpmR1) {
      counterR1 = minrpmR1;
    }
    RPMrange1();
  }

  if (range == 2 && result == DIR_CW) {
    counterR2 += 100;
    if (counterR2 >= maxrpmR2) {
      counterR2 = maxrpmR2;
    }
    RPMrange2();
  }
  else if (range == 2 && result == DIR_CCW) {
    counterR2 -= 100;
    if (counterR2 <= minrpmR2) {
      counterR2 = minrpmR2;
    }
    RPMrange2();
  }

  //soft start
  if (loopflag == true) {
    myPID.SetTunings(sKp, sKi, sKd);        // Set the PID gain constants and start
    int i = (desiredRPM - tempcounter);
    for (int j = 1; j <= i; j++) {
      Input = RPM;
      Setpoint = tempcounter;
      myPID.Compute();
      dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // inverse the output
      dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
      tempcounter++;
      delayMicroseconds (risetime);
    }
    if (tempcounter >= desiredRPM) {
      lastcounttime = millis();
      lastpiddelay = millis();
      loopflag = false;
      runflag = true;
      tempcounter = 100;
    }
  }

  // normal motor running state
  if (relayState == HIGH && loopflag == false) {
    unsigned long piddelay = millis();

    if ((piddelay - lastpiddelay) > 1000) {     // delay to switch PID values. Prevents hard start
      myPID.SetTunings(rKp, rKi, rKd);          // Set the PID gain constants and start
      lastpiddelay = millis();
    }

    Input = RPM;
    Setpoint = desiredRPM;
    myPID.Compute();
    dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // reverse the output
    dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
  }
  // diagnose a fault and turn on protection

  unsigned long counttime = millis();
  if (counttime - lastcounttime >= 1000) {
    if (count == 0 && relayState == HIGH && runflag == true) {
      startflag = false;            // flag to turn off triac before relay turns off
      delay (300);                  // delay to prevent sparks on relay contacts
      digitalWrite(RELAY, LOW);
      relayState = LOW;
      stuckerror();
    }
    lastcount = count;
    count = 0;
    lastcounttime = millis();
  }

  //reset rpm after motor stops
  if (count == 0 && relayState == LOW) {
    RPM = 0;
  }

  // protection against high rpm. i e triac damage
  if (relayState == HIGH && RPM > desiredRPM + protection) {
    startflag = false;            // flag to turn off triac before relay turns off
    delay (300);                  // delay to prevent sparks on relay contacts
    digitalWrite(RELAY, LOW);
    relayState = LOW;
    exceederror();
  }
  // real RPM display
  if (relayState == HIGH && range != 0) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= lcdinterval) {
      previousMillis = currentMillis;
      int rpmdisplay = RPM / ratio;
      lcd.setCursor(0, 1);
      lcd.print("Real RPM:   ");
      if (rpmdisplay >= 1000) {
        lcd.setCursor(12, 1);
        lcd.print(rpmdisplay);
      }
      else if (rpmdisplay < 1000) {
        lcd.setCursor(12, 1);
        lcd.print(" ");
        lcd.setCursor(13, 1);
        lcd.print(rpmdisplay);
      }
    }
  }
}

void range0() {
  lcd.setCursor(0, 0);
  lcd.print(" Please  select ");
  lcd.setCursor(0, 1);
  lcd.print(" the RPM range! ");
}

void motorStateStop() {
  lcd.setCursor(0, 1);
  lcd.print ("  Press  START  ");

}

void RPMrange1() {
  lcd.setCursor(0, 0);
  lcd.print("R1 RPM set: ");
  if (counterR1 >= 1000) {
    lcd.setCursor(12, 0);
    lcd.print(counterR1);
  }
  else {
    lcd.setCursor(12, 0);
    lcd.print(" ");
    lcd.setCursor(13, 0);
    lcd.print(counterR1);
  }
}

void RPMrange2() {
  lcd.setCursor(0, 0);
  lcd.print("R2 RPM set: ");
  lcd.setCursor(12, 0);
  lcd.print(counterR2);
}

void exceederror() {
  lcd.clear();
  while (1) {
    lcd.setCursor(5, 0);
    lcd.print("ERROR!");
    lcd.setCursor(2, 1);
    lcd.print("TRIAC DAMAGE");
  }
}

void stuckerror() {
  lcd.clear();
  while (1) {
    lcd.setCursor(5, 0);
    lcd.print("ERROR!");
    lcd.setCursor(2, 1);
    lcd.print("MOTOR STUCK!");
  }
}
Code for FranciscoC/C++
It's reduced code for Francisco. It has no LCD, no range switch. Only push button to start/stop the motor. Simply enter desired RPM value in the sketch.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <PID_v1.h>

#define TACHO 3            // tacho signals input pin
#define DETECT 2           // zero cross detect pin
#define GATE 17            // TRIAC gate pin
#define BUTTON 4           // rottary encoder button pin
#define RELAY 5            // relay pin
#define PULSE 2            // number of triac trigger pulse width counts. One count is 16 microseconds
#define TACHOPULSES 8      // number of pulses per revolution 

unsigned int RPM;                   // real rpm variable
unsigned int count;                 // tacho pulses count variable
unsigned int lastcount = 0;         // additional tacho pulses count variable
unsigned long lastcounttime = 0;
unsigned long lastflash;
unsigned long lastpiddelay = 0;
unsigned long previousMillis = 0;
unsigned long lastDebounceTime = 0;

const int sampleRate = 1;           // Variable that determines how fast our PID loop
const int rpmcorrection = 86;       // sito kazkodel reikia, kad realus rpm atitiktu matuojamus
const int protection = 2000;        // protection will switch on when real rpm exceeds desired by value
const int debounceDelay = 50;       // the debounce time; increase if the output flickers
const int minoutputlimit = 80;      // limit of PID output
const int maxoutputlimit = 540;     // limit of PID output
const int mindimminglimit = 80;     // the shortest delay before triac fires
const int maxdimminglimit = 625;    // for 60Hz will be 520
const int risetime = 100;           // RPM rise time delay in microseconds (risetime x RPM)
const int desiredRPM = 1000;        // ENTER DESIRED RPM HERE

int dimming = 540;                  // this should be the same as maxoutputlimit
int tempcounter = 100;

byte relayState = LOW;              // the current state of the relay pin
byte buttonState;                   // the current reading from the input pin
byte lastButtonState = HIGH;        // the previous reading from the input pin

bool loopflag = false;              // flag for soft start
bool startflag = false;             // flag for motor start delay
bool runflag = false;               // flag for motor running state

double Setpoint, Input, Output;       // define PID variables
double sKp = 0.1, sKi = 0.2, sKd = 0; // PID tuning parameters for starting motor
double rKp = 0.25, rKi = 1, rKd = 0;  // PID tuning parameters for runnig motor

PID myPID(&Input, &Output, &Setpoint, sKp, sKi, sKd, DIRECT); // define PID variables and parameters

void setup() {
  Serial.begin(115200);
  // set up pins
  pinMode(BUTTON, INPUT);             // set the button pin
  pinMode(RELAY, OUTPUT);             // set the relay  pin
  pinMode(DETECT, INPUT);             // set the zero cross detect pin
  pinMode(GATE, OUTPUT);              // set the TRIAC gate control pin
  pinMode(TACHO, INPUT);              // set the tacho pulses detect pin
  digitalWrite(BUTTON, HIGH);         // turn on pullup resistors
  digitalWrite(RELAY, relayState);    // initialize relay output

  Input = 200;                        // asiign initial value for PID
  Setpoint = 200;                     // asiign initial value for PID

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(minoutputlimit, maxoutputlimit);
  myPID.SetSampleTime(sampleRate);    // Sets the sample rate

  // set up Timer1
  OCR1A = 100;                        // initialize the comparator
  TIMSK1 = 0x03;                      // enable comparator A and overflow interrupts
  TCCR1A = 0x00;                      // timer control registers set for
  TCCR1B = 0x00;                      // normal operation, timer disabled

  // set up zero crossing interrupt IRQ0 on pin 2.
  // set up tacho sensor interrupt IRQ1 on pin3
  attachInterrupt(0, zeroCrossingInterrupt, RISING);
  attachInterrupt(1, tacho, FALLING);
}

// Interrupt Service Routines
void zeroCrossingInterrupt() { // zero cross detect
  TCCR1B = 0x04;               // start timer with divide by 256 input
  TCNT1 = 0;                   // reset timer - count from zero
  OCR1A = dimming;             // set the compare register brightness desired.
}

ISR(TIMER1_COMPA_vect) {       // comparator match
  if (startflag == true) {     // flag for start up delay
    digitalWrite(GATE, HIGH);  // set TRIAC gate to high
    TCNT1 = 65536 - PULSE;     // trigger pulse width
  }
}

ISR(TIMER1_OVF_vect) {         // timer1 overflow
  digitalWrite(GATE, LOW);     // turn off TRIAC gate
  TCCR1B = 0x00;               // disable timer stops unintended triggers
}

// RPM counting routine
void tacho() {
  count++;
  unsigned long time = micros() - lastflash;
  float time_in_sec  = ((float)time + rpmcorrection) / 1000000;
  float prerpm = 60 / time_in_sec;
  RPM = prerpm / TACHOPULSES;
  lastflash = micros();
}

void loop() {

  // check the start / stop button state
  int reading = digitalRead(BUTTON);  // read the state of the switch into a local variable:
  if (reading != lastButtonState) {   // If the switch changed, due to noise or pressing
    lastDebounceTime = millis();      // reset the debouncing timer
  }
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {     // if the button state has changed:
      buttonState = reading;
      if (buttonState == LOW) {       // only toggle the relay if the new button state is LOW
        relayState = !relayState;
        if (relayState == HIGH) {
          loopflag = true;
          digitalWrite(RELAY, relayState); // set the Relay:
          delay (300);                     // delay to prevent sparks on relay contacts
          startflag = true;                // flag to start motor
        }
        if (relayState == LOW) {
          Setpoint = 200;
          Input = 200;
          runflag = false;
          startflag = false;
          delay (300);                     // delay to prevent sparks on relay contacts
          digitalWrite(RELAY, relayState); // set the Relay:
        }
      }
    }
  }
  lastButtonState = reading;               // save the reading. Next time through the loop, it'll be the lastButtonState:

  //soft start
  if (loopflag == true) {
    myPID.SetTunings(sKp, sKi, sKd);        // Set the PID gain constants and start
    int i = (desiredRPM - tempcounter);
    for (int j = 1; j <= i; j++) {
      Input = RPM;
      Setpoint = tempcounter;
      myPID.Compute();
      dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // inverse the output
      dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
      tempcounter++;
      delayMicroseconds (risetime);
    }
    if (tempcounter >= desiredRPM) {
      lastcounttime = millis();
      lastpiddelay = millis();
      loopflag = false;
      runflag = true;
      tempcounter = 100;
    }
  }

  // normal motor running state
  if (relayState == HIGH && loopflag == false) {
    unsigned long piddelay = millis();

    if ((piddelay - lastpiddelay) > 1000) {     // delay to switch PID values. Prevents hard start
      myPID.SetTunings(rKp, rKi, rKd);          // Set the PID gain constants and start
      lastpiddelay = millis();
    }

    Input = RPM;
    Setpoint = desiredRPM;
    myPID.Compute();
    dimming = map(Output, minoutputlimit, maxoutputlimit, maxoutputlimit, minoutputlimit); // reverse the output
    dimming = constrain(dimming, mindimminglimit, maxdimminglimit);     // check that dimming is in 20-625 range
  }

  // diagnose a fault and turn on protection
  unsigned long counttime = millis();
  if (counttime - lastcounttime >= 1000) {
    if (count == 0 && relayState == HIGH && runflag == true) {
      startflag = false;            // flag to turn off triac before relay turns off
      delay (300);                  // delay to prevent sparks on relay contacts
      digitalWrite(RELAY, LOW);
      relayState = LOW;
    }
    lastcount = count;
    count = 0;
    lastcounttime = millis();
  }

  //reset rpm after motor stops
  if (count == 0 && relayState == LOW) {
    RPM = 0;
  }

  // protection against high rpm. i e triac damage
  if (relayState == HIGH && RPM > desiredRPM + protection) {
    startflag = false;            // flag to turn off triac before relay turns off
    delay (300);                  // delay to prevent sparks on relay contacts
    digitalWrite(RELAY, LOW);
    relayState = LOW;
  }
}

Schematics

universal_ac_motor_pid_control_CLgotSOvLR.zip
The archive includes libraries, schematic, components list and sketch.
universal_ac_motor_pid_control_CLgotSOvLR.zip

Comments

Similar projects you might like

Control a 220VAC Universal AC Motor with Arduino

Project showcase by microst

  • 1,253 views
  • 0 comments
  • 3 respects

Arduino - Motor PID Speed Control

Project tutorial by whitebank

  • 37,552 views
  • 21 comments
  • 17 respects

NRF24L01 Wireless Motor Speed Control System with Arduino

Project tutorial by ICStation Team

  • 6,035 views
  • 1 comment
  • 10 respects

Outboard Motor Throttle Controller

Project tutorial by Terry Towle

  • 6,348 views
  • 2 comments
  • 14 respects
Add projectSign up / Login