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.
Various questions cropped up during my experiments. I hope you can get better answers than I did.
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.
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.
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!
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.