Building an electronic speed control system for a DC motor

Closed-loop approach: setpoint, RPM counter, and feedback control

DC motors are cool. However, it is not always an easy task to accurately control their speed. To that purpose, we’re gonna be developing a closed-loop RPM control. The benefits of a closed-loop control to regulate the motor speed are:

  • Accurate speed control
  • The speed is kept constant even if the shaft load changes during its operation
  • The model of DC motor that you have doesn’t really matter: it will spin at the desired RPM.

The idea we are going to implement here is the following one:

You can see how the real RPM are fed back into the controller by substracting them to the desired ones. The result is what control systems engineers call “the error signal”

Purchase suggestion

If you need a small DC motor, I suggest you to grab this cheap 10 pcs. kit from Amazon.
Their working voltage can go from 3 up to 6 volts DC and they can spin at up to 17000 RPM.

Driving a motor from an Arduino I/O pin

There are several ways to achieve this. The simplest, and most straight-forward one is to use an NPN transistor and PWM its base through a 220 Ohm resistor. Needless to say that we will introduce a lot of noise in the motor (and the circuit, in general) if we choose to go with this option:

VCC will, in general, be provided by a power supply other than Arduino’s one. This is done because DC motors normally require more current than Arduino’s internal power supply can provide. Do not connect VCC to the 5V pin in your Arduino: you might damage your board.

Also, note that D1 can be replaced by a 1N4148 small signal diode (as a rule of thumb) if the motor draws less than 100mA (which is usually not the case). Any 1N400X will be fine here.

The selected NPN transistor for the job is a 2N2222, but if your motor draws more than 200 mA, or you exceed the power dissipation specified under the “Maximum Absolute Ratings” of this transistor datasheet, you are strongly advised to replace the 2N2222 by another one that supports more current such as the TIP 120.

If we were to implement this circuit in a breadboard, it would look like this:

The VCC is, in this case, external; and supplied through a 9mm jack connector.

Let’s say that this approach is enough for our project. For reduced noise and better control, consider using an L293D or an L298 H Bridge DC motor controller. Find out more here.

The RPM counter: LED/Photodiode selection and delay measurement

How can we determine the RPM at which the DC motor is spinning. Well, the general idea here is to place an infrared LED (Ir-LED) which will emit a light beam, that will be received by a photodiode when a slotted, spinning disk passes through that beam. The photodiode output will be LOW at all times except when the disk allows the light to pass. If we measure the distance from peak to peak, we can calculate the period of the wave and, thus the frequency, which can then be converted into RPM.

So, do we use a photodiode or a phototransistor?

Photodiodes are based on a single P-N junction, and they are generally less sensitive but have faster response than phototransistors. Phototransistors use a regular NPN transistor with its base exposed. Photons excite the base and act as a base current injected to a regular NPN transistor. They are slower, but more sensitive. As we are counting RPM (a fast process), the choice seems (and it is) obvious. I have decided to use the TEST2600 (datasheet) from Vishay Semiconductors (R).

TEST2600 photodiode, from Vishay Semiconductors (R).

TEST2600 photodiode, from Vishay Semiconductors (R).

The problem here, is that the disk might be able to spin fast enough so that the photodiode doesn’t have time to produce a spike before the next revolution is completed. That is the delay of our system (if we don’t take into account the microcontroller processing time). We can try to roughly calculate the frequency at which our photodiode needs to be able to operate (the minimum bandwidth of the system) for our RPM counter to work:

Let’s consider that a DC motor can spin, at low voltages, at approximately 1500 rpm; the frequency would then be:

f={n}/{60}={1200}/{60}=20 Hz

So we need around 20 Hz of minimum bandwidth. Let’s see how can we test that low frequency.

Measuring the delay

The delay can be measured by sending a pulse to the IR-emitting diode and using a scope to determine the phase difference between the input and the output. We will use the classic NE555 timer to generate a square wave, that will be sent to the IR LED (AM4457F3C). The square wave will be received by the photodiode, which will generate a current that will generate a voltage spike across the resistor of the magnitude:

V_{out}=R_{2}ยท(I_{dark}+\beta L)

L is the amount of received light, and \beta a linearization constant that depends on the photodiode. The circuit configuration is the following one (use this GREAT 555 calculator):

If we run this experiment, what we observe by measuring the output (between R_2 and the emitter (E terminal) of U2) is the following:

We see that we get 0.4 V of offset at the output due to the current that flows across the photodiode at 0 received light I_{dark}, but we get nice edges with almost 0 delay.

Building the widget

We now need to keep constantly switched on the led and create the square signal by spinning a slotted disk between the emitter and the receiver. The disk I made out of a cardboard box is this one:

I then used a small cardboard box to place the Ir-LED and the photodiode on it, and the disk on the lid, so that when you close it, the disk lies between both:

I then placed a potentiometer and  an OLED screen to show the setpoint, the control PWM applied and the current RPM:

Coding and wiring it

We need to connect several things to our Arduino Nano, find the summary here:

Pin numberPin ConnectionPin Mode
A010k Potentiometer voltage divider outputAnalog Input
2Photodiode output (R2 voltage)Digital Input
32N2222 base through 220 ohm resistorPWM Output
6OLED_DCSPI (OLED)
7OLED_CSSPI (OLED)
8OLED_RESETSPI (OLED)

The code for the RPM counter is the following one (please note that I tried to code it with interrupts but it yielded strange results due to the noise produced by the ‘PWMed’ DC motor):

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <AutoPID.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_DC     6
#define OLED_CS     7
#define OLED_RESET  8
#define LOGO_HEIGHT   16
#define LOGO_WIDTH    16
#define POTPIN A0
#define IRPIN 2
#define MOTORPIN 3

#define RPM_MIN 200
#define RPM_MAX 1500
#define OUTPUT_MIN 0
#define OUTPUT_MAX 255
#define CL_GAIN 0.1
#define CL_INT_GAIN 0.02

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,
  &SPI, OLED_DC, OLED_RESET, OLED_CS);


double rpm_target = RPM_MIN, rpm = 0, actuator_action = 0;
int previous_error = 0, previous_time = 0;
AutoPID myPID(&rpm, &rpm_target, &actuator_action, OUTPUT_MIN, OUTPUT_MAX, CL_GAIN, CL_INT_GAIN, 0.0);

void print_header(int rpm_target, int current_rpm);
int measureRPM();

void setup() {
  rpm_target = RPM_MIN;
  pinMode(POTPIN, INPUT);
  pinMode(IRPIN, INPUT);
  pinMode(MOTORPIN, OUTPUT);

 // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds

  // Clear the buffer
  display.clearDisplay();

  print_header(0, 0, 60);
  // display.display() is NOT necessary after every single drawing command,
  // unless that's what you want...rather, you can batch up a bunch of
  // drawing operations and then update the screen all at once by calling
  // display.display(). These examples demonstrate both approaches...
  //Start spinning the motor
  analogWrite(MOTORPIN, 60);
  myPID.setTimeStep(500);
}

void loop() {
  int error=0, current_time = 0;
  
  //Read potentiometer
  rpm_target = floor(map(analogRead(POTPIN), 0, 1023, RPM_MIN, RPM_MAX)/10.0) * 10;
  //Change motor speed (closed loop)
  rpm = measureRPM();
  // Negative feedback loop
  myPID.run();
  analogWrite(MOTORPIN, actuator_action);
  //End of  negative feedback loop
  
  print_header(rpm_target, rpm, actuator_action);
}

int measureRPM(){
  int rpm = 0; 
  unsigned long start_time = 0, end_time = 0, peak_time = 0;
  while(digitalRead(IRPIN) == LOW);
  start_time = millis();
  while(digitalRead(IRPIN) == HIGH);
  peak_time = millis() - start_time;
  while(digitalRead(IRPIN) == LOW);
  end_time = millis();
  rpm = (int)(1000.0f/(end_time-start_time)) * 60;
  return rpm;
}

void print_header(int rpm_target, int current_rpm, int applied_pwm) {
  display.clearDisplay();

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  display.println(F("RPM Speed Controller"));

  display.setTextColor(WHITE); // Draw 'inverse' text
  display.println(F("ardumakers.com // JGA"));
  
  display.setTextSize(2);             // Draw 2X-scale text
  display.setTextColor(WHITE);
  display.print(F("RPM: ")); 
  display.println(current_rpm, DEC);
  //display.setTextColor(BLACK, WHITE);
  display.print(F("SET: ")); 
  display.println(rpm_target,DEC); 
  display.print(F("PWM: ")); 
  display.println(applied_pwm,DEC); 
  display.display();
}

Final result

And here we have the final result!

If you have any doubt, or you have gotten different results, please don’t hesitate to leave a comment and let me know!

Aerospace Engineer
Aeronautical Engineer. My dream is to work for a leading company in the aerospace field to develop projects that can make the world a better place; improving people's life, safety, and comfort. I am interested in Aerospace Engineering, big data, CFD, photography, automatic control, maths and electronics.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.