A seemingly simple project is notable for two reasons. First it took me on a long wild goose chase to control the jitter in a micro hobby servo, and second, it gave me a reason to really teach myself transistor physics.
This project was to serve as a demonstration model for a robotics class that I teach. I lifted the
curriculum without shame from Gerald Recktenwald at Portland State University The project is an exercise in design for the laser cutter, and in control of servos and DC motors using PWM, transistors, switches, potentiometers, and servo pulses.
I made several ill-advised mistakes early on the project. The first was to test the components individually, and then expect them to work together. In this case, the components were a micro-switch, a Solarbotics A108 microservo, a 10k potentiometer, a 2N2222 transistor and a toy DC motor. Test programs showed that the servo worked fine with the Arduino Servo library, and that separately the transistor drove the DC motor fine with speed varying based on the angle of the potentiometer. All pretty standard stuff.
I made a little stand for the servo and a holder for the motor on the laser. I milled a PCB board, soldered all the components in place, mounted the servo and the motor, and gave it a run. The servo worked fine, as did the sensors, but the toy motor did nothing. A quick search online revealed that the servo library uses Timer 1 on the Arduino to control pulse widths. Timer 1 also controlled PWM pins 9 and 10, and my transistor base was being driven by pin 9. Damn. I didn't see this conflict until I ran all the components in the same Arduino sketch.
So I removed the PCB, went back to a breadboard setup and wired the servo to pin 3, leaving the fan in pin 9.
The DC motor and the servo both worked now, but there was a new unexpected behavior. The servo would twitch like crazy when the fan was on. Interestingly, the servo seemed to be fine when the fan was off. See the video.
This is where I made my second mistake. Based on the research I had just conducted with timers, I figured that somehow the fan was messing up the timing of the servo, resulting in varying pulse widths and hence the jitter of the servo. I chose not to look at an oscilloscope; instead I went into research mode and looked at ways to make my own pulses.
My understanding of timers and generating pulses is based on the website "
The Perfect Pulse" of "Josh". There are three timers for the atmega328, timers 0, 1 and 2. Timers 0 and 2 are 8 bit timers, meaning that you start counting at 0 and end at 2^8 - 1 = 255 before restarting at 0. Timer 1 is a 16 bit timer, so it counts from 0 to 2^16 - 1= 65535. Timer 0 is used by the Arduino function millis and delay. It is also used for PWM pins 5 and 6. Timer 1 is used by the Servo library and also controls PWM pins 9 and 10. Finally, Timer 2 is used by the tone() function and PWM pins 3 and 11.
Josh's perfect pulse uses timer 2. There are two bytes associated with timer two:
TCCR2A
|
|
|
|
|
|
|
|
COM2A1
|
COM2A0
|
COM2B1
|
COM2B0
|
Reserved
|
Reserved
|
WGM21
|
WGM20
|
Compare output mode
|
Compare output mode
|
|
|
|
|
Waveform generation bit
|
Waveform generation bit
|
TCCR2B
|
|
|
|
|
|
|
|
FO2A
|
FO2B
|
Reserved
|
Reserved
|
WGM22
|
CS22
|
CS21
|
CS20
|
|
|
|
|
Waveform generation bit
|
Prescaler
|
Prescaler
|
Prescaler
|
The basic rule is that there is a counter which ticks at a
certain rate. It can tick at the
CPU clock speed, but you can also change the prescaler values in order to
divide the CPU clock rate and slow the counter down.
The counter (TCNT2) starts at 0, and when it reaches a
variable called OCR2A (the "top"), it resets to zero.
The timer can be linked to an output pin (3 or 11 for timer 2 based on
the first four bits of TCCR2A). When the counter resets to 0, the output pin is set low. If at anypoint the counter equals a second variable called OCR2B (the "match"), the output pin is set
high. By changing the value of the
"match" variable (between 0 and 255) and by modifying the prescaler values, you
can generate tons of patterns including a customized PWM signal or a servo
control signal.
The important variables are shown below.
// counter TCNT2
|
// TOP OCR2A
|
// MATCH OCR2B
|
The base pulse program that I pulled offline works as
follows. We set the following bits equal to one: COM2B1, COM2B0, WGM21, WGM20, WGM22. The WGM bits setup a particular mode of using
the counter (Fast PWM Mode 7), which is the one I am describing. I don’t understand the other modes.
The easy way to set bits is using _BV( bit name), which sets
bit name to 1. For example,
TCCR2A = _BV(COM2B0) | _BV(COM2B1) | _BV(WGM20) |
_BV(WGM21);
Makes COM2B1, COM2B0, WGM21, and WGM20 equal to 1. The definition of _BV(bit) is 1<<bit
We setup the prescaler bits
//
Clock Select Bits: set with
TCCR2B
|
//
CS22 CS21 CS20
|
//
0 0 0
//timer/counter stopped
|
//
0 0 1
// no prescaler
//
0 1 1
// /32 prescaler
//
1 0 0
// /64 prescaler
//
1 0 1
// /128 prescaler
//
1 1 0
// /256 prescaler
//
1 1 1
// /1024 prescaler
|
We attach an output pin to the timer:
DDRD |= _BV(3); // Set pin to output (Note that OC2B = GPIO port PD3 =
Digital Pin 3)
To tread water, we set
TCNT2 = 0x00; // Start counting at bottom.
|
OCR2A = 0; // Set TOP to 0.
|
These means that the counter starts at zero, the top is
zero, and so the counter remains zero always. Any
attached pin is kept low.
There are three functions that I use:
OSP_SET_WIDTH(width)
|
OSP_FIRE()
|
OSP_INPROGRESS()
|
The first sets the value of the MATCH bit to be 255 – (width
– 1): #define OSP_SET_WIDTH(cycles) (OCR2B = 0xff-(cycles-1))
The second function sets the counter to be one less than the
match: #define OSP_FIRE() (TCNT2 = OCR2B - 1). This means that on the next counter increment,
the output (pin 3 in this case) will go high, and will stay high until the counter
reaches top. Note that since the top is
255, and match = 255 – (width – 1), the duration of the pulse will be whatever
width is.
The final function is the boolean TCNT2>0.
It is used in my program to delay until
the pulse is done by using a blank while loop:
while (OSP_INPROGRESS());
The picture here is an illustration of a sample pulse
with width 10. The OSP_FIRE() function
is run at time t
0. This sets
the counter equal to 245. On the next
cycle (counter = 246, time t
1), output pin 3 goes high. It stays high until the counter reaches 255
(time t
2). Then the counter
resets and the output pin 3 goes low. The
pulse stayed on while the counter went from 246 to 255, which is 10 counts.
I measured pulse widths for some values of the prescaler
using an oscilloscope. In all cases I
set “width” = 10.
Prescaler
|
Pulse Width
|
Calculated Time per count
|
32
|
20 us
|
2 us
|
64
|
40 us
|
4 us
|
128
|
80 us
|
8 us
|
256
|
160 us
|
16 us
|
1024
|
646 us
|
64.6 us
|
For the servo control signal, we need pulses between 1-2ms
with a period of roughly 20ms. Using the 1024 prescaler, a width of 20 gives
1.28 ms (calculated = 20 x .0646ms = 1.29ms) and a width of 30 gives 1.94 ms
(calculated = 30 x 0.0646ms = 1.94ms).
So in the sample program, the width is just adjusted going between 20
and 30 to sweep the servo through its range of values. A delay of 20ms is used to space the servo pulses to the appropriate frequency.
I tried a couple other solutions... the Software Servo library worked great to move a servo, but did not remove the problem related to the motor.
I tried to clean up the servo power by running the servo with a separate power supply from the rest of the circuity, and by installing an LM7805 voltage regulator with a bunch of different valued capacitors (0.1, 1, 10 uF) across the output. Neither of these tests made a difference.
Ultimately, I tried a few other servos and saw that the problem seemed to be worst with the Solarbotics A108. With Hitec servos, the problem was much more understated. In the end, I picked up a Tower Pro SG90 from Vetco and the problem went away.
An oscilloscope trace of the servo signal shows that it picks up a lot of noise when the motor is turned on (at about 14 seconds into the video). This was an electrical engineer's take: "I believe the problem comes from electrical noise from the fan motor getting into the Arduino's timing circuits. I'm assuming that it is a PM motor without any filtering. Here is why I say that. In a PM motor as the armature rotates, the brush(s) bridges the space between the commutator bar segments shorting the winding (a coil, right?) between the two adjacent segments. Herein lies the problem: A large Ldi/dt current usually towards the end of commutation causes an arc between the commutator bars. Further, the coil or winding current is reversed when the rotor continues on its journey and the torque direction is preserved. Reversing current is the maximum di/dt, right? Think of the zero crossing of a sine wave mathematically. These current spikes can find their way through less than adequate filtering into sensitive high impedance digital circuits." - Larry R.
I interpret his words in the following graph:
Once we had established that the problem was caused by the DC motor, the solution was just to use a servo with better filtering circuitry to ignore the current spikes. The final prototype is shown in the picture below.
The SoftwareServo code also works fine:
code here.
The code I use with my classes, which includes debugging information
is located here.
The last piece of this project was a fairly comprehensive document that I wrote on transistor physics. The picture below links to a webpage which has that document (called transistor.pdf).