Arduino Compatible MC-2100 Controller and Lathe Tachometer

The completed circuit in operation

After getting the simple 555 based MC-2100 driver circuit working, we moved on to a controller with more features. I’d been looking for an excuse to make an arduino-compatible board (here’s a description if you’re not familiar), and this seemed like the ticket.

The initial spec for the controller included the following functions:

  • Read user input from potentiometer
  • Send the 50ms period PWM signal to the MC-2100
  • Sense the lathe’s spindle speed using a magnetic reed switch or equivalent
  • Display the spindle speed on a 7 segment LED display

At this point, the controller meets the requirements laid out above. I’ll discuss the implementation of each feature into the controller in the order listed above.

Read User Input

This is fairly simple. The potentiometer is set up as a voltage divider, with the wiper connected to an analog pin on the ATMega328p. A simple bit of code removes the jitter from the reading, only taking a new value once the reading changes by a threshold value. This reduces the resolution, but it’s still more than enough for my purposes.

 //Read and condition pot value
 potTemp = analogRead(POT_READ);
 potCheck = abs(potTemp - potValue);
 if(potCheck >= POT_DIF) { //Only accept new value if it's far enough from the current accepted value
 potValue = potTemp;
 }
 

I ended up adding an on/off switch to the input as well, so that the lathe can be stopped and restarted at the same speed. For this, I used a debounce function that Joe and I had developed for one of his guitar pedal projects. It watches an input, and waits until it’s steady for a certain amount of time (TO_HIGH_DELAY or TO_LOW_DELAY) before changing the output.

Here’s the function declaration:


////////////////////////////////////////////////////////////////////////////////////////////
/* Function for debouncing digital inputs

 Arguments:
 _debouncePin - ID of pin to be read/debounced
 *lastReading - pointer to variable storing the previous reading (HIGH/LOW) of the input pin
 *lastDebounceTime - pointer to variable storing the last time (ms) the input changed (not debounced)
 _toLowDelay - debounce time for HIGH to LOW transition
 _toHighDelay - debounce time for LOW to HIGH transition

 Returns:
 _state - debounced state (HIGH/LOW) of _debouncePin
 */
////////////////////////////////////////////////////////////////////////////////////////////

byte debounce(byte _debouncePin, byte *lastReading, unsigned long *lastDebounceTime, int _toLowDelay, int _toHighDelay)
{
 byte _reading = digitalRead(_debouncePin);
 byte _state = *lastReading;

if (_reading != *lastReading) { // pin state just changed
 *lastDebounceTime = millis(); // reset the debouncing timer
 }

if ((millis() - *lastDebounceTime) >= _toLowDelay && _reading == LOW) {
 // whatever the reading is at, it's been there for longer
 // than the hold delay, so take it as the actual current state for use in the rest of the script
 _state = _reading;
 *lastReading = _reading;
 return _state;
 }

if ((millis() - *lastDebounceTime) >= _toHighDelay && _reading == HIGH){
 // whatever the reading is at, it's been there for longer
 // than the hold delay, so take it as the actual current state for use in the rest of the script
 _state = _reading;
 *lastReading = _reading;
 return _state;
 }
 *lastReading = _reading;
 return _state;
}

And the function call:

onOffState = debounce(ON_OFF, &lastonOffState, &lastOnOffTime, TO_LOW_DELAY, TO_HIGH_DELAY);

The result is passed through a simple IF statement to decide whether to send a speed command to the MC-2100 or not.

Send 50ms PWM to MC-2100

I wasn’t sure how to do this one at first. We started by hardcoding the pwm signal using millis() and conditional statements. It worked, but I figured there had to be a cleaner way. I eventually discovered the Timer1 Library, which worked perfectly for our purposes. After adding the library to my arduino installation, it was a cinch to get the signal sent to the MC-2100.

The PWM is initialized like this:


Timer1.initialize(PWM_CYCLE*1000); //Set pin 9 and 10 period to 50 ms

Timer1.pwm(PWM_OUT, 0); //Start PWM at 0% duty cycle

Where PWM_CYCLE is the desired period in milliseconds, and PWM_OUT is the output pin.

The PWM duty cycle is then updated like this, including the if statements for ON/OFF functionality:


//Send output signal only if ON/OFF switch is in ON position
 if (onOffState == LOW){ //Off
 Timer1.setPwmDuty(PWM_OUT, 0); //Shut down MC-2100
 }

if (onOffState == HIGH){ //ON
 Timer1.setPwmDuty(PWM_OUT, speedLevel); //Send speed command to MC-2100
 }

With that small amount of code, we can control the MC-2100’s speed level just like the simple circuits from earlier posts. Now it’s time to get the speed readout working.

Sense Spindle Speed

In order to sense the spindle speed of the lathe, I attached two neodymium magnets (I’ll discuss the lathe setup in a future post) and placed a reed switch nearby to pick up when they go past. The reed switch is connected to a simple conditioning circuit as follows:

Reed Swtich Input Low Pass Filter Circuit

This circuit includes a pull up resistor (R16) and a low pass filter (R7 and C7) to condition the reed switch and send it to the ATMega’s input pin.

To read the signal with the Arduino, I used one of the ATMega’s external interrupts to ensure that I never missed a pulse. The interrupt is initialized as follows:


attachInterrupt(0, intervalCalc, FALLING); //Attach interrupt to pin 2 for spindle speed sensing

Where intervalCalc is the Interrupt Service Routine, the function that gets called each time the interrupt is triggered. Here’s the function definition:


////////////////////////////////////////////////////////////////////////////////////////////
void intervalCalc() //Interrupt Service Routine - Store time interval from reed switch each magnet pulse
{
 if ((millis() - reedTime) >= REED_DELAY) { //ignore switch bounce for (REED_DELAY) ms (not the normal debounce routine)
 if (reedTime !=0) { //Is this the first interrupt since reset?
 reedSet = 1; //set flag
 reedInterval = millis() - reedTime; //calculate interval
 }
 reedTime = millis(); //record last interrupt time
 }
}

To keep the ISR as short as possible, it only records the time it was entered, and checks and resets a couple flags for use in the main body of the code.

The reedInterval value from the ISR is then recorded, averaged, smoothed, and converted to RPM in the main() function.

if (reedSet == 1) { //Interrupt triggered since last loop, calculate RPM
 reedSet = 0; //reset interrupt flag
 intervalCount++; //increment counter (used for averaging first few pulses)

//Store interval from ISR in vector for averaging
 for(int n=NUM_SAMPLES-1; n>0; n--) {
  intervals[n]=intervals[n-1]; //Shift all values to the right. Oldest value discarded
 }
 intervals[0] = constrain(reedInterval,0,RESET_DELAY); //new value at beginning of vector

//Caculate RPM
 intervalSum = 0; //reset sum value
  for(int n=0; n<NUM_SAMPLES; n++) { intervalSum += intervals[n]; //add intervals together
 }
 avgInterval = intervalSum/min(intervalCount,NUM_SAMPLES); //calculate average interval
 rawSpindleSpeed = 60000/(avgInterval); //convert to RPM
 spindleSpeed = smooth(rawSpindleSpeed, SMOOTH_LEVEL, spindleSpeed); //smooth RPM reading
 Multiplex7SegGen::loadValue(spindleSpeed); // display measured RPM on 7 segment
}

I added in a reset function to reset the display to zero quickly once the spindle stops. With only two magnets on the spindle, there can be quite a bit of time between inputs. This limits the minimum readable speed to around 40 RPM.

if ((millis() - reedTime) >= RESET_DELAY) { //If no pulses received for (RESET_DELAY) ms, reset all counters/intervals
 for(int n=0; n<NUM_SAMPLES; n++) {
  intervals[n] = 0;
  intervalCount = 0;
  reedTime = 0;
  spindleSpeed = 0;
  pendingDecel = LOW; //The spindle has stopped, OK to reverse direction. (Not used yet)
 }
 Multiplex7SegGen::loadValue(0); // reset display to 0 RPM
}

That wraps up the speed sensing circuit. On to the 7 segment display!

Display Spindle Speed on LED Display

For the display portion of the build, I used a couple two digit 7 segment displays from a treadmill’s dash panel. A brief overview of interfacing 7 segment displays with Arduino can be found here. Like most microcontroller topics, this one has two parts: the hardware and the software. For the hardware, I built a fairly standard 7 segment display circuit, with one exception. My displays were common anode (all the LED’s in the display share their positive pin), while most are common anode cathode. This meant that instead of pulling the cathodes down through NPN transistors, I pulled the anodes up with PNP transistors. Simple enough, right?

7 Segment Display Circuit

The software side of things was almost really easy… There are a few different 7 segment libraries available for arduino that have all the functionality required for this display. The only problem is that the best one I found was set up for common cathode displays instead of common anode, so the display would be inverted. I ended up learning how libraries worked, which wasn’t a bad thing, and modified an existing library to accept different display types. The modified library can be downloaded here if you’re interested.

With the library ready to go, all that was left was inserting the proper commands into the code. The display is initialized like this:


#define D1 8 //7 segment Digit Selection Pins
#define D2 11
#define D3 12
#define D4 A5
#define S1 A2 //7 segment Segment Selection Pins
#define S2 A1
#define S3 7
#define S4 10
#define S5 6
#define S6 A3
#define S7 A4
#define S8 5

......

byte digitPins[] = {
  D1,D2,D3,D4};
byte segmentPins[] = {
  S1,S2,S3,S4,S5,S6,S7};

......

void setup()
{

......

Multiplex7SegGen::set(1, 4, digitPins, segmentPins, LOW, LOW); // initialize 7 segment library as common anode

.....

}

If I were using a common cathode display, the last two arguments would be HIGH, HIGH. The source library had these values hard coded in the .cpp file.

Updating the display is simple as well:


Multiplex7SegGen::loadValue(spindleSpeed); // display measured RPM on 7 segment

Future Plans

I’m pretty happy with the controller as it stands today. It’s made my lathe much easier to use, and enabled the use of a more refined motor controller than I was using previously.

To take things a step farther, I’d like to add an external reversing/braking circuit to the MC-2100, using the Arduino to control a relay H Bridge with a dynamic braking resistor. The second switch you see on the input panel will be used to select forward and reverse, and I’ll build a relay circuit that goes between the MC-2100 and the lathe motor. That will have to wait till I get a few more projects out of the way though…

Summary

Gut Shot:

Internals. In this picture, the arduino portion is on the upper board, and the 7 segment display and user inputs are on the lower.

Here’s the full schematic, including the arduino circuitry and the 7 segment display (click here to download the Eagle file)::

Arduino Compatible MC-2100 Controller and Tachometer

And the full arduino code (click here to download):


/*
 MC-2100 Treadmill Motor Controller Interface
 Lathe Motor Controller via PWM
 Seven Segment Tachometer
 ON/OFF Toggle

 Joe Schoolcraft
 Brian Schoolcraft
 May 2013
 https://sonsofinvention.wordpress.com/
*/
#include  //Modified from here: http://code.google.com/p/mutiplex7seg/ (adapted to work with any 7 segment display type, mine is common anode)
#include  //http://playground.arduino.cc/code/timer1

#define POT_READ A0 //Wiper of pot connected as voltage divider (Speed Command Input)
#define SPEED_SENSE 2 //connected to reed switch - magnet closes switch, pulls to ground (Spindle Speed Input - 2 pulses per rev)
#define PWM_OUT 9 //Connected to blue wire of MC2100 (50ms period PWM out)
#define REV_OUT 1 //Reverse Relay Output (Not Used)
#define FWD_OUT 0 //Forward Relay Output (Not Used)
#define FWD_REV 3 //Fwd/Rev Switch Input (Not Used)
#define ON_OFF 13 //On/Off Switch Input

#define PWM_CYCLE 50.0 //Output Signal PWM Period (50ms)

#define POT_DIF 4 //Change detection threshold on pot
#define MAX_DUTY 869 //Max Duty Cycle expected by MC-2100 (85% of 1023)
#define MIN_DUTY 0 //Min Duty Cycle expected by MC-2100 (0% of 1023)
#define REED_DELAY 20 //Deboounce time for reed switch
#define RESET_DELAY 1000 //Time delay before resetting speed to 0
#define NUM_SAMPLES 2 //Number of Samples to average before smoothing function
#define SMOOTH_LEVEL 0.5 //Input to smoothing function
#define TO_LOW_DELAY 50 //Debounce time for HI to LO switch transition
#define TO_HIGH_DELAY 50 //Debounce time for LO to HI switch transition

#define D1 8 //7 segment Digit Selection Pins
#define D2 11
#define D3 12
#define D4 A5
#define S1 A2 //7 segment Segment Selection Pins
#define S2 A1
#define S3 7
#define S4 10
#define S5 6
#define S6 A3
#define S7 A4
#define S8 5

int potTemp;
int potValue;
int lastPotValue;
int potCheck;
int speedLevel;

byte digitPins[] = {
 D1,D2,D3,D4};
byte segmentPins[] = {
 S1,S2,S3,S4,S5,S6,S7};

long intervals[NUM_SAMPLES] = {0,0}; //Last NUM_SAMPLES reedInterval values for averaging spindle speed
long intervalSum; //Sum of intervals vector
int avgInterval; //Averaged interval value
int spindleSpeed = 0; //Spindle speed converted from avgInterval and smoothed
int rawSpindleSpeed = 0; //Spindle speed converted from avgInterval
int intervalCount = 0;

unsigned long lastFwdRevTime = 0;
unsigned long lastOnOffTime = 0;
byte reading = 0;
byte fwdRevState = 0;
byte onOffState = 0;
byte lastfwdRevState = 0;
byte lastonOffState = 0;
byte pendingDecel = 0;

//ISR Variables stored as volatile (kept in RAM)
volatile unsigned long reedTime = 0; //Last time reed switch closed
volatile unsigned long reedInterval; //Time since last reed switch closure
volatile boolean reedSet = 0; //Reed switch tracker

void setup()
{
 pinMode(POT_READ, INPUT);
 pinMode(PWM_OUT, OUTPUT);
 pinMode(SPEED_SENSE,INPUT);

pinMode(ON_OFF, INPUT_PULLUP); //Enable internal pullup resistor to simplify external circuit

Multiplex7SegGen::set(1, 4, digitPins, segmentPins, LOW, LOW); // initialize 7 segment library as common anode

attachInterrupt(0, intervalCalc, FALLING); //Attach interrupt to pin 2 for spindle speed sensing

Timer1.initialize(PWM_CYCLE*1000); //Set pin 9 and 10 period to 50 ms
 Timer1.pwm(PWM_OUT, 0); //Start PWM at 0% duty cycle
}

void loop()
{
 //Read and condition pot value
 potTemp = analogRead(POT_READ);
 potCheck = abs(potTemp - potValue);
 if(potCheck >= POT_DIF) { //Only accept new value if it's far enough from the current accepted value
 potValue = potTemp;
 }

 speedLevel = map(potValue,0,1023,0,MAX_DUTY); //Convert Pot input to pwm level to send to MC-2100

//Read and Debounce Switches
 //Not Used Yet fwdRevState = debounce(FWD_REV, &lastfwdRevState, &lastFwdRevTime, TO_LOW_DELAY, TO_HIGH_DELAY);
 onOffState = debounce(ON_OFF, &lastonOffState, &lastOnOffTime, TO_LOW_DELAY, TO_HIGH_DELAY);
 //Send output signal only if ON/OFF switch is in ON position
 if (onOffState == LOW){ //Off
 Timer1.setPwmDuty(PWM_OUT, 0); //Shut down MC-2100
 }

if (onOffState == HIGH){ //ON
 Timer1.setPwmDuty(PWM_OUT, speedLevel); //Send speed command to MC-2100
 }

////////////////////////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////Relays/Braking not implemented at this time
 // //Set Relay States and MC-2100 PWM Levels
 //
 // if (fwdRevState != lastfwdRevState){ //The direction switch has been flipped
 // pendingDecel = HIGH; //Set decel flag. This gets reset low once the spindle stops
 // }
 //
 // if (onOffState == LOW){ //Off
 // digitalWrite(FWD_OUT, LOW); //Set relays for braking
 // digitalWrite(REV_OUT, LOW);
 // Timer1.setPwmDuty(PWM_OUT, 0); //Shut down MC-2100
 // }
 //
 // if (onOffState == HIGH){ //On
 // if (pendingDecel == LOW){
 // if (fwdRevState == HIGH){ //Fwd
 // digitalWrite(FWD_OUT, HIGH); //Set relays for FWD operation
 // digitalWrite(REV_OUT, LOW);
 // Timer1.setPwmDuty(PWM_OUT, speedLevel); //Send speed command to MC-2100
 // }
 // if (fwdRevState == LOW){ //Rev
 // digitalWrite(FWD_OUT, LOW); //Set relays for REV operation
 // digitalWrite(REV_OUT, HIGH);
 // Timer1.setPwmDuty(PWM_OUT, speedLevel); //Send speed command to MC-2100
 // }
 // }
 // else{
 // digitalWrite(FWD_OUT, LOW); //Set relays for braking
 // digitalWrite(REV_OUT, LOW);
 // Timer1.setPwmDuty(PWM_OUT, 0); //Shut down MC-2100
 // }
 // }
 ////////////////////////////////////////////////////////////////////////////////////////////

 //Calculate RPM from interrupt routine output, display on 7 segment

if (reedSet == 1) { //Interrupt triggered since last loop, calculate RPM
 reedSet = 0; //reset interrupt flag
 intervalCount++; //increment counter (used for averaging first few pulses)

//Store interval from ISR in vector for averaging
 for(int n=NUM_SAMPLES-1; n>0; n--) {
 intervals[n]=intervals[n-1]; //Shift all values to the right. Oldest value discarded
 }
 intervals[0] = constrain(reedInterval,0,RESET_DELAY); //new value at beginning of vector

//Caculate RPM
 intervalSum = 0; //reset sum value
 for(int n=0; n<NUM_SAMPLES; n++) {  intervalSum += intervals[n]; //add intervals together  } avgInterval = intervalSum/min(intervalCount,NUM_SAMPLES); //calculate average interval rawSpindleSpeed = 60000/(avgInterval); //convert to RPM  spindleSpeed = smooth(rawSpindleSpeed, SMOOTH_LEVEL, spindleSpeed); //smooth RPM reading Multiplex7SegGen::loadValue(spindleSpeed); // display measured RPM on 7 segment  } if ((millis() - reedTime) >= RESET_DELAY) { //If no pulses received for (RESET_DELAY) ms, reset all counters/intervals
 for(int n=0; n<NUM_SAMPLES; n++) {  intervals[n] = 0;  intervalCount = 0;  reedTime = 0;  spindleSpeed = 0;  pendingDecel = LOW; //The spindle has stopped, OK to reverse direction. (Not used yet)  }  Multiplex7SegGen::loadValue(0); // reset display to 0 RPM  } } //////end loop //////////////////////////////////////////////////////////////////////////////////////////// void intervalCalc() //Interrupt Service Routine - Store time interval from reed switch each magnet pulse {  if ((millis() - reedTime) >= REED_DELAY) { //ignore switch bounce for (REED_DELAY) ms (not the normal debounce routine)
 if (reedTime !=0) { //Is this the first interrupt since reset?
 reedSet = 1; //set flag
 reedInterval = millis() - reedTime; //calculate interval
 }
 reedTime = millis(); //record last interrupt time
 }
}

////////////////////////////////////////////////////////////////////////////////////////////
/* Function for debouncing digital inputs

 Arguments:
 _debouncePin - ID of pin to be read/debounced
 *lastReading - pointer to variable storing the previous reading (HIGH/LOW) of the input pin
 *lastDebounceTime - pointer to variable storing the last time (ms) the input changed (not debounced)
 _toLowDelay - debounce time for HIGH to LOW transition
 _toHighDelay - debounce time for LOW to HIGH transition

 Returns:
 _state - debounced state (HIGH/LOW) of _debouncePin
 */
////////////////////////////////////////////////////////////////////////////////////////////

byte debounce(byte _debouncePin, byte *lastReading, unsigned long *lastDebounceTime, int _toLowDelay, int _toHighDelay)
{
 byte _reading = digitalRead(_debouncePin);
 byte _state = *lastReading;

if (_reading != *lastReading) { // pin state just changed
 *lastDebounceTime = millis(); // reset the debouncing timer
 }

if ((millis() - *lastDebounceTime) >= _toLowDelay && _reading == LOW) {
 // whatever the reading is at, it's been there for longer
 // than the hold delay, so take it as the actual current state for use in the rest of the script
 _state = _reading;
 *lastReading = _reading;
 return _state;
 }

if ((millis() - *lastDebounceTime) >= _toHighDelay && _reading == HIGH){
 // whatever the reading is at, it's been there for longer
 // than the hold delay, so take it as the actual current state for use in the rest of the script
 _state = _reading;
 *lastReading = _reading;
 return _state;
 }
 *lastReading = _reading;
 return _state;
}
///////////////////////////////////////////////////////////////Function for smoothing sensor readings: http://playground.arduino.cc//Main/Smooth
int smooth(int data, float filterVal, int smoothedVal){

if (filterVal > 1){ // check to make sure param's are within range
 filterVal = .99;
 }
 else if (filterVal <= 0){
 filterVal = 0;
 }

smoothedVal = (data * (1 - filterVal)) + (smoothedVal * filterVal);

return (int)smoothedVal;
}