PIC-based Speed Control (work in slow progress)

Eric Behr, July 1998

A few weeks ago I wrote about the advantages of microcontroller speed controls over analog ones. I said that it's "trivial" to program all the necessary functions into the PIC.

I was young and stupid then... Here are a few remarks about the things I learned while writing a speed control program for the Arizona Microchip 16C84 (or 16F84) processor. I'm hoping that the same code will work in a 12C508, thus reducing the size.

Basic assumptions

The program must be able to do two basic things:
  1. watch for pulses from the receiver (at about 50 Hz), and measure their length (supposed to be 1-2 ms, but in reality I see 1.2-2 with both a JR and a Futaba radio)
  2. output a square wave with frequency of several kHz and duty cycle from 0 to 100% depending on the length of the input pulse
A 16C84 running at 4 MHz executes one instruction in a microsecond. The program must have some sort of loop of constant length which will serve as a "clock". The output will be held high for several iterations (depending on the desired duty cycle), and then held low for some number of steps. If we want - say - 32 possible output duty cycles, and the output pulse is to repeat about 2000 times per second, we only have about 15 CPU cycles for that innermost loop. Of course getting higher output frequency means either shortening the loop, or reducing the output resolution.

Various questions cropped up during my experiments. I hope you can get better answers than I did.

To interrupt, or not to interrupt?

The 16C84's TMR0 interrupt is very useful, because the timer gets incremented no matter what else happens in the program. One can therefore rely on it if accurate timing, e.g. the output "on/off" duration, is desired.

My first attempt used TMR0 in just such a way: figure out how long we should stay on, load TMR0 with a suitable value; when it overflows and an interrupt happens, load it with a value suitable for the "off" period and just keep going like that.

Unfortunately the job of timing the input pulse is then relegated to a secondary status, and it should also be reasonably accurate. Handling interrupts takes a while (one has to save the vital registers, clear interrupt flags, do the work required, then restore the program's status). This made measurement of the input quite unreliable, even when quite fancy average and deviation computations were put in.

Since the input pulse is quite a bit longer than the desired output, maybe the input transition should be handled as an interrupt ("unusual event"), and the rest should be taken care of in a predictable loop? That was the second attempt, and it also had flaws: the most important thing, I think, is getting a relatively clean output, and the overhead of handling an interrupt prevented that - except less often this time.

To remove the uncertainty introduced by interrupts I decided to do away with them altogether. If the chip ran at 10 MHz instead of 4, the timings might have been less critical. But with only a handful of CPU cycles in the main loop, all deviations from the expected program path upset the balance too much.

Even though interrupt mechanism isn't used, the interrupt flags still get set. So one can rely on TMR0 being incremented, and on the port B change flag being set. That's what I ended up using: TMR0 with a suitable prescaler for accumulating the input "on" time, and the RB flag for checking whether the input changed state.

The critical branch

Except for initialization, and setting aside various safety checks, each program loop is one of the following types:
  1. if the "on/off" counter runs out, handle the transition of the output
  2. if the input changed, either set TMR0 to zero (off->on) or save its value for later processing (on->off)
  3. if none of the above happened, and we are in a state which will not require action for a while, calculate the next "on" and "off" output values
The calculation routine can be called up when the output doesn't have to change for some time, and thus can take as many cycles as it needs - as long as it also decrements the running counter by an appropriate amount.

But of all the other tasks, I was surprised to learn, the case when input goes low and the pulse measurement has to be stored takes the longest. This is in part because it is one of the low priority items (so it's far down the line, after several GOTOs which take 2 cycles each), and in part because it also needs to clear the RB interrupt flag, which must be preceded by a read/write on the port, and that takes an extra cycle. One way or another, I haven't managed to make this critical branch shorter than 14 CPU cycles. I tried various other solutions such as a state machine, computed GOTOs and such, and I can't improve it.

Resolution versus speed

This means that a loop which can change the output duty cycle in 32 discrete steps will take up 448 microseconds, so the output frequency will be just over 2.2 KHz. This is probably acceptable for most applications. An 18-step output resolution gives just under 4 KHz, which is definitely more than enough for an ordinary electronic throttle.

But the more loop iterations are needed for the calculation routine, the bigger error we can (randomly) make in timing the input pulse. It might be possible to put at least one check for the input transition in the middle of the calculation, but that adds complexity.

Our simple "compute" routine takes as much time as 3 loops (with some time to spare, filled with NOPs), so we may fail to catch the input change for up to 42 microseconds. Since the input pulse length of about 1000 to 2000 us is timed using the TMR0 prescaler of 1:8, and then scaled down even further (by a factor of 4) to be more in line with the output resolution, the error gets cut down by that much and the measurement shouldn't be off by more than one unit of the output duty cycle. This can be further smoothed in the "compute" routine (e.g. by keeping track of the last two pulses and recomputing only if the sum of changes isn't zero).

Note that if the output is timed in steps of N CPU cycles, then the "compute" routine, including the call and return, should take an integer multiple of N plus one cycles to make all loop iterations take a predictable amount of time:

...   (k cycles into the loop up to here)
btfsc    do_compute
call     compute
...   (k+2 cycles, or k + 1 + compute time)
Here is a "skeleton" version of the code which you may want to use as a basis for your actual program. Please heed the warnings in the comments - this is not a ready-to-use product!

Digital is easy

The software issues are easy in comparison with interfacing the PICs output to the FETs. The motor induces noise, the servos change the supply voltage when they move, the load has capacitance and inductance, you name it. Here is a picture of my pretty output pulse (lower trace) and what it looks like after being fed into a FET and a lightbulb (upper trace):

still waiting for the prints ;-)

When a motor without adequate protection is hooked up, things look even worse. Always use at least one 0.1 uF capacitor across the motor's terminals (preferably two, from each terminal to the metal case), and a relatively heavy-duty diode (Schottky is best) across the motor, of course with polarity opposite to the voltage being supplied by the ESC. Without those the circuit will act up at random, or you'll fry the FETs, or both.

The main worry is the receiver - if it goes out of whack due to interference etc., you lose control. And a BEC unit should definitely have undervoltage protection, so when the battery is low at least the receiver will work for a while. This can be pretty easily achieved with a few extra parts which would keep one of the PICs input high as long as it's safe to keep the motor turning (that input would then be checked by the "compute" routine). Coupling the receiver input and the ESC with an optoisolator is a good idea, but it is of little use in BEC designs: the garbage will get to the receiver through the supply rail. Pick your chances.

I liked the idea from Elektor 2/97 (my copy has the authors' names cut off! I can't even give proper attribution...) in which a dual optocoupler drives the FET's gate low and high by alternately "shorting" it to ground and pulling up to V+. Having a full 0->V+ swing is important when you use standard (as opposed to logic-level) FETs. Here is a simplified variant of that circuit in PDF and PostScript formats (no brake or reverse).

Loads such as motors - which we are interested in - seem to be much less responsive at the lower end of the voltage supplied to them. More precisely, it takes quite a bit of energy to get them going than to sustain the given speed once the motor is turning. An ideal ESC would have some hysteresis, making the output go up fast, and decreasing at a normal rate.

Much of the throttle's "intelligence" should be in the setup routine. For example, it should recognize whether the stick moving up increases or decreases the input pulse. This of course depends on the user starting from a predefined position, e.g. stick low. It should also measure the shortest and longest pulses and set up computation parameters accordingly. Those might be stored in the PIC's EEPROM until reset by means of a button or a jumper.

Conclusion

It's been great fun (although some tedious work remains). I don't think I'd be able to get anywhere with this project if I didn't have a decent programmer and simulator, a good oscilloscope (not my picture! mine are always sharp ;-), and a lot of understanding from my family. I hope you can use my experiences in your adventures, or that you will share yours with me.