The first program (T2MultiServo1.ino) is pretty much a copy of the Adafruit code. All I did was to remove the Flasher class since I didn't need it. SIGNAL was changed to ISR since SIGNAL is now deprecated. I also added the external interrupt for the reset button. That code is part of the same Adafruit example. The purpose here was simply to see how the code worked and check out the interface board I built. Although the code is running on a Teensy 2, it will run equally well on an Arduino Uno or a Mega. It WILL NOT run on a Teensy 3 or other ARM-based Arduinos. I'll need to figure out more about the ARM timers and perhaps the interrupt structure. Future work!
- Servos need independent power! I'm using a 6V, 1A wall wart. Ran 6 SG-5010 and 2 SG90 servos simultaneously for a few minutes and WW was just barely warm. I didn't try to find the limits of this setup. It worked for testing.
- I built a simple board to connect power and the control signals. The schematic and a picture are attached. Doesn't get much simpler.
- The two big caps are there to smooth out surges during servo motion.
Adding Control of Angle and Time
In T2MultiServo2.ino, I modified the Sweeper class to allow a move to a specified position at the programmed rate, then stop until directed to a new position. I also added a bit of code in loop() to exercise it. One perhaps interesting detail is the use of elapsedMillis objects to control servo position changes at independent times. The elapsedMillis class is just one more elegant, convenient implementation we can thank Paul Stoffregen for!
T2MultiServo3.ino adds programmable sweep time. Specifying the time allowed for a move to a new angle will be translated to the time interval between incremental moves. The only caveat, and one for which there is *no* checking, is that the move to a new position cannot happen faster than one degree per millisecond. So if we want to move 180 degrees say, in 100 milliseconds, it ain't gonna happen! Not yet anyhow. Since my concern is to move physical objects at reasonable rates, then this restriction is not an issue for me. But since I'm releasing the code to the wild, this problem must be dealt with. More Future Work. It would also be useful to be able to move less than 1 degree per time increment if very slow moves were desired. Right now, such slow moves will appear jerky, but at least they will happen.
Array Control: Enabling Group Moves
If you've followed the details so far, then T2MultiServo4.ino is your reward! It implements group moves of servo motors in a controlled manner. The program should allow you to implement your own design easily. Following is useful information for those who want to know more.
This final version of the Sweeper class requires no arguments to instantiate a Sweeper object. An array of Sweepers can now be created. This keeps the code for creating, initializing, and updating the servos nice and clean. Individual Sweeper objects (servos) are referred to by index. Indexing in this manner is very useful, as illustrated by the functions described below.
This version of the Sweeper class provides separate functions to initialize a move with destination and time, and to enable the move. Doing this allows several moves to be set up, then all started at the same time, for a group move. Now it is likely that this is not necessary in most cases, but I thought it would be nice to have and would eliminate worries caused by starting moves at staggered times. There is also a function that will produce the old behaviour if desired. Another useful function is resetTo() which moves a servo immediately to a desired position. This is most useful at startup to put all the servos into a known state. Note that the motors should be put in this position prior to powering down. Then there will be no sudden jerks as the program starts again.
The changes just described support a "group move" function. A group of servos may be easily defined (as an array), along with a corresponding array of target angles and an allowed time for the move. The size of the group is defined as a const, then used to specify the arrays. Even though the arrays are initialized, thus defining their size, having the size declared causes the compiler to check that the number of entries in each array matches the intended number. One look at the groupMove function should make it obvious that a groupSetup function and a groupTrigger function could be easily created. The code in (program name) will demonstrate the use of groupMove.
Conditionals at the start of the program allow testing of an asynchronous move, or a synchronous move. Actually, these two are really just alternate implementations; I include them as useful examples. As implemented, you must choose set one and only one conditional. Obviously, these can be removed for your own implementation.
While I was developing this code I had a concern that I couldn't seem to rationalize away: Suppose all the Sweepers need to move on the same tick. Will there be time or will there be a timing conflict? To answer this important question, I made a group with all of the sweepers (9 in my design). Using groupMove(), I moved the entire group at one time. Then I take a pin high when the Timer 0 Compare A interrupt service routine is entered, and take the pin low when the ISR exits. Using a scope, it's easy to find the instants when updates happen and to see how long it takes. Example code for using 9 Sweepers and setting them up to all update at once using a group move is included in the program. Just set the right conditional, recompile, hook the scope to digital out pin 6, and measure the widest pulse captured. See the screen shot. TODO.
A few details of the Adafruit program are worth explaining.
The ATmega328P (as used on Arduino UNO) and the ATmega32U4 (as used on Teensy2) have many hardware resources that are not used, or not fully exploited, by Arduino. A full description is well beyond this discussion, but a small consideration is appropriate. Let's focus narrowly on the Timer/Counters. Specifically, let's look at T/C0. T/C0 is configured in Arduino to overflow and cause an interrupt at a rate very close to 1 KHz. This interrupt updates millis. Of course, this action is mostly invisible to the Arduino user. The millis function just magically works - or so it seems. What Bill Earl did was to read the data sheet for the ATMEGA328P very closely and discover that T/C0 could be used to do more than just keep track of millis. T/C0 can also generate another interrupt whenever a specific count value is reached. The Output Compare A of Timer 0 will be configured to generate an interrupt in addition to the one used for millis. That interrupt is used to update the commands to the servos to control their movement and rate of movement. You can learn a lot more about other resources of the AVR processors at the AVRFreaks web site.
T/C0 is configured to count continuously starting at 0 and continuing to 255. Upon reaching 255, T/C0 resets to 0 and starts counting again. If the Output Compare A register of Timer 0 has a value in it, and its interrupt is enabled, then, when the T/C0 count reaches that value, an interrupt will occur. When an interrupt occurs, the processor saves the system state and jumps to an Interrupt Service Routine for the specific interrupt. There is a table of ISRs used for this purpose. To create an entry in this table, the macro ISR is invoked with the name of the interrupt as an argument. In our case, the line
ISR (TIMER0_COMPA_vect)declares the code in braces following the declaration to be that which will be executed whenever the T/C0 Compare A overflows. An entry for the vector is put into the ISR table. The name of the ISR vector must be one that is known to the compiler for the processor being used. No further definition of the name is needed. The names (and other hardware information specific to a processor) may be found in [arduino base]/hardware/tools/avr/avr/include/avr. For UNO, look at iom328p.h and for Teensy 2, look at iom32u4.h. The appropriate data sheet will also indicate the desired interrupt vector. Table 9-1 in Section 9.1 of the ATmega32u4 data sheet gives the name of the vector needed. The precise name must be found in iom32u4 data sheet. Note that this is hardly a complete description of interrupt handling, just enough to explain what's going on and to pique your curiosity.
To make use of the T/C0 OCR0A, we must do two things. First, we must put a value into the register that will be compared to the T/C0 count. That's OCR0A. Then we must set the correct bit in the T/C0 control register to enable the interrupt. That bit is OCIE0A and we must OR the bit into register TIMSK0. More careful reading of the data sheet required! The code looks like this:
OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A);
You won't find these names defined in the Arduino code - at least not in an obvious place. They come from the processor data sheets. The Arduino user is mostly spared the details, but if you want to do the sort of work Bill Earl does, you'll want to learn all about this stuff by reading the data sheets! For T/C0, you'll find the names of the registers and the bit definitions in Section 13.8 of the ATmega32u4 data sheet.
To actually move the servos, the Arduino Servo library is used. It can control up to 12 servos and uses Timer Counter 1 for appropriate interrupt-based timing. The Update function in the Sweeper class calls the Servo library to make the moves when it's time for each servo.
Because this program uses T/C0 Output Compare A, that output is unavailable for PWM. Same for T/C1 which is used by the Servo library. For UNO, this means no PWM on D9, D10, or D6. For Teensy 2, no PWM on D4, D14, or D15.