Close

PWM Waveform Generation

A project log for Stubby the (Teaching) Hexapod

100% open source robot platform with accessability and affordability in mind: teaching children of all ages about robots & programming

the-big-oneThe Big One 04/10/2014 at 16:146 Comments

When I first started this project, I was using the PWM library I wrote a while back, for use with my quadcopter.  It is pretty simple: you can configure as many pins as you want, set a single period for all of them, and set individual phases on each pin.  This works beautifully in theory for controlling servos, since they all have the same period (20ms) and the individual phases (between 1ms and 2ms nominally) control the angle.

It didn't work.

The behaviour I experienced was that, when setting the legs to a default neutral position (1500us, or 1.5ms), everything was fine.  However I needed the ability to set a calibration offset on each joint individually, since tolerances in the assembly didn't allow for exactly the same angle on all legs.  What I was seeing, was that if I set an offset for one leg, occasionally multiple legs would shift position.

After breaking out the logic probes with Warren, we figured out what was happening.  To explain, first I will go into a bit on how I implemented this code.

One 16 bit timer is used.  This timer has two compare values: COMPA and COMPB.  COMPA is used to set the period; when it fires, all PWM pins are brought high.  We then look for the first pin (i.e. the lowest phase number), and set COMPB to that number.  When COMPB fires, we look for the next highest, etc.

This works fine in theory, but looking through all 18 pins takes time.  Sometimes hundreds of microseconds worth of time.  It doesn't sound like a lot, but when you keep in mind that 1000us moves the servo 180 degrees, 100us is about 18 degrees.  Good luck moving the legs precise distances when you have a margin of error of 10%!

The solution is to pre-calculate all the hard calculations.  Every time a new phase is set (i.e. every time I try to move a leg), I now sort all the phase values for all pins.  I then assemble a struct which contains the bit mask for turning on all pins (in COMPA), as well as bit masks and associated COMBP values for each pin.  Then, when COMPA fires it is just 4 lines to set all the pins (PORTA |= maska, PORTB |= maskb, etc).  I set the initial COMPB as the COMPB value of the first sorted struct item.  When COMPB fires, I set the pins low according to the bit mask, and look at the next struct in line to find the next COMPB value.  No looping is required, so it scales to as many pins as you could want without problem.

Using this new and improved method, it takes 6us in the COMPB ISR.  Not bad considering the last one was over 100!  This translates to a margin of error of about 1 degree, which from what I can tell is not much different than the analog control circuitry in the servos themselves (the servos I am using have a dead-band width of 8us), and even if it wasn't, is still well within tolerances for this application.

I am planning some future improvements by using assembly for certain instructions.  Most notably, the first step in the COMPB ISR is to turn off the clock so we don't miss other phase events; however, even though it is the first line in the ISR, the compiler has added a bunch of extra stuff to save register state.  I am planning on defining the ISR in pure assembly, and turning off the clock before any register values are saved.  I will then call the normal C function which does all the work, which itself will save and restore register state before and after it has been called.


So what did we learn here?

First off, ISRs are not meant for heavy lifting.  If you are doing a loop in one, you are probably doing it wrong.

Second, rethinking the problem can let you arrive at a much more elegant solution.

Third, it always comes back to the memory / CPU tradeoff.  Feel free to use a few extra bytes of memory (especially on a platform like the ATMega 1284 where you have TONS of RAM - who would ever need more than 16k?) in order to shave microseconds off some time critical code.

Honestly, as a web application / multi tier software developer during the day, nothing I did here was new to me.  It is just that when programming on modern servers, you tend to take things like memory and tight inner loops for granted.  Code maintainability tends to be more important than saving a few bytes of ram here, or a few microseconds there.  It is somewhat refreshing to be this close to the processor.

Discussions

deʃhipu wrote 01/11/2016 at 20:35 point

Hi, I'm experimenting with my #Servo Controller project, and I tried to use your PWM library with it. I simply used the pwm.* files from your repository (using the most recent version). However, I have a problem compiling the pwm.S file, and I don't know this assembler enough to guess what is wrong. It's probably something trivial, so I decided to ask you. The command I use and the error that I'm getting is:

/home/sheep/dev/hardware/arduino-1.6.5/hardware/tools/avr/bin/avr-gcc -c -g -x assembler-with-cpp -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=10605 -DARDUINO_AVR_PRO -DARDUINO_ARCH_AVR -I/home/sheep/.arduino15/packages/arduino/hardware/avr/1.6.5/cores/arduino -I/home/sheep/.arduino15/packages/arduino/hardware/avr/1.6.5/variants/eightanaloginputs /tmp/build784886641344142549.tmp/pwm.S -o /tmp/build784886641344142549.tmp/pwm.S.o 
/tmp/build784886641344142549.tmp/pwm.S: Assembler messages:
/tmp/build784886641344142549.tmp/pwm.S:23: Error: constant value required
/tmp/build784886641344142549.tmp/pwm.S:23: Error: number must be positive and less than 64
/tmp/build784886641344142549.tmp/pwm.S:26: Error: constant value required
/tmp/build784886641344142549.tmp/pwm.S:26: Error: number must be positive and less than 64
Error compiling

It seems to have problems with the "PORTA" macro -- if I replace it with PORTB, it gets past this part. Any ideas? Anything more I could check?

  Are you sure? yes | no

The Big One wrote 01/11/2016 at 20:39 point

Yeah, PORTA is not defined on the 328.  In my build script I have a -DPWM_PORTA_UNUSED which removes all references to PORTA from the code; with Arduino creating your build scripts you will probably not be able to set that, so will have to just look through the code (.c and .S) and remove everything between the #ifdef PWM_PORTA_UNUSED and #endif declarations.  If you do that, it should work properly... let me know either way.

Cheers

  Are you sure? yes | no

deʃhipu wrote 01/11/2016 at 20:42 point

Yup, that's it, thanks a lot!

  Are you sure? yes | no

deʃhipu wrote 01/12/2016 at 16:02 point

One suggestion on how to make this code a little bit more robust, as I just spent whole evening debugging that: clear the prescaler flags, in case something else set them before:

TCCR1B &= ~(_BV(CS12) | _BV(CS11) | _BV(CS10));
TCCR1B |= _prescaler_mask;

  Are you sure? yes | no

The Big One wrote 01/12/2016 at 16:04 point

Good call, I will look at adding that.

Edit: Committed change to Github.

  Are you sure? yes | no

deʃhipu wrote 01/11/2016 at 20:42 point

Ah, nevermind, turns out that the Pro Mini that I'm using doesn't use PORTA.

  Are you sure? yes | no