Pages: [1]   Go Down
Author Topic: PWM on more channels with one timer  (Read 709 times)
0 Members and 1 Guest are viewing this topic.
Belgium
Offline Offline
Newbie
*
Karma: 0
Posts: 38
Arduino rocks
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Recently, I came across a small code example that showed how PWM could be done on more channels by using a timer that fires an interrupt.
Every time the interrupt occurred, a counter is incremented.
When the counter is zero, the pins are turned on, and on each increment, the counter is compared to see which pins should be turned off.
The problem with this approach is that you have to handle a lot of interrupts (say 256 times your refresh rate), and that the time taken in each interrupt increases as the number of channels increases.

On the ATMega168, it is also possible to have the timer itself make a comparison with the compare register, and only fire an interrupt at the right time.
Of course, there are only 2 compare registers, so it would look like you can only use the same method as the hardware PWM and get 2 channels per timer.

After checking the ATMega's datasheet, I found however that it is possible to update the compare register while the counter is running.
By sorting the list of pins you want to toggle from short PWM cycle to long PWM cycle, you can update the compare register at each interrupt to get the correct time for the next pin.

I've just finished implementing this in a small test, and it looks like it works very well with 3 channels at 244 Hz, and I think that without additional cpu power the number of channels could easily be increased, I hope to 8 channels on one timer.

The only downside is probably the work needed in the setChannel function to keep the list sorted, and that (at that frequency) 2 channels cannot have a value that is just 1 timer clock apart. (So PWM cycle of 125 and another of 126 is not possible, because the comparison will be missed)

Maybe this could be solved however by interleaving the channels on both compare registers instead of using just one. (So you could have one register at 125, and the other at 126)

What follows is the code I've used, I'm really curious what you think about it and if anyone has any ideas to improve this.

Variables and defines:
Code:
//The number of channels and some derived values
#define NB_CHANNELS 3
#define NB_CHANNELS_PLUS_1 4
#define NB_CHANNELS_MIN_1 2
//The smallest tick difference that is required between 2 pwm cycle times
//when increasing the clock speed, this may need to be increased as well
#define MINDIST 2

//Channel 0 is on pin 3, channel 1 on pin 6 and channel 3 on pin 7
//Note that only 0 to 7 are possible in the current code since PORTD is used directly instead of digitalWrite
char channelMap[NB_CHANNELS_PLUS_1] = {3,6,7 , 255};

//the first pin is set to 0, second to 2, and third to 4
char timerSteps[NB_CHANNELS_PLUS_1] = {0,2,4 , 0};  //end with zero
//pin 3 is the one that will trigger first, then pin 6, then pin 7
int  timerPins[NB_CHANNELS_PLUS_1]  = {3,6,7 , 3};
//at the start we start at step 0
int  timerStep     = 0;

Code which configures the timer. The prescaler can be changed for different frequencies. For leds, it looks like even 1024 may be acceptable, even though it only results in 62 Hz frequency.
This code can be run in setup()
I think I could also add pinMode here to set the configured pins to output pins.
Code:
void setupTimer2()
{
  //Timer2 settings:  Prescaler /256 = 62kHz, which gives us an overflow at 244 Hz
  //Prescaler 8=010;32=011;64=100;128=101;256=110;1024=111
  TCCR2A = 0;
  TCCR2B = 1<<CS22 | 1<<CS21 | 0<<CS20;
  
  //Set timer step to 0
  timerStep = 0;
  
  //Set first interrupt level
  OCR2A = timerSteps[timerStep];
  
  //Enable overflow interrupt, and compare A interrupt
  TIMSK2 = 1<<OCIE2A | 1<<TOIE2;
}

The overflow interrupt, executed at the start of each cycle turns the led on.
Note that because of the RGB led I used, my code actually sets the state LOW to turn on the led, but in most code you might need to switch this around to set the state HIGH.
If 8 channels are to be used, I think it would also be faster to use PORTD = 0 or PORTD = 255 instead of stepping through each of the channels.
PORTD could of course be replaced by PORTB to use pins 8 to 13 instead, although the arduino pin numbers no longer match the numbers required in the pin definitions in that case.
Note that this code also steps over each of the pins that don't need to be turned on at all, ensuring a true 0 is possible.
Code:
//Timer2 overflow interrupt vector handler
ISR(TIMER2_OVF_vect) {
  //ensure that we start at step 0
  timerStep = 0;

  //Turn all leds on
  for (int i = 0; i < NB_CHANNELS; i++) {
    if (timerSteps[timerStep] > 1) {
      PORTD = PORTD & (~(1 << timerPins[i])); //digitalWrite(timerPins[i], LOW);
    } else {
      PORTD = PORTD | (1 << timerPins[timerStep]);
      timerStep++;
    }
  }
  
  //Set first interrupt
  OCR2A = timerSteps[timerStep];
}

The interrupt code when the compare value is reached.
This will also step through all other pins that have the same value.
Note that timerSteps[NB_CHANNELS] is equal to 0 which makes it unnecessary to compare what value timerStep has reached.
I'm not 100% sure what happens on overflow though, will this code be run again, and if so, before or after the overflow interrupt?
It might be best to set timerSteps[NB_CHANNELS] to timerSteps[0] when doing setChannel, so that the compare value is set correct for the next cycle.
Code:
ISR(TIMER2_COMPA_vect) {
  //Turn led off at compare interrupt, do the same for all leds that should be off on the same time
  PORTD = PORTD | (1 << timerPins[timerStep]); //digitalWrite(timerPins[timerStep], HIGH);
  timerStep++;
  while (timerSteps[timerStep-1] == timerSteps[timerStep]) {
    PORTD = PORTD | (1 << timerPins[timerStep]); //digitalWrite(timerPins[timerStep], HIGH);
    timerStep++;
  }
  //Set next compare value
  OCR2A = timerSteps[timerStep];
}

Finally, the setChannel code.
First it looks up the pin number from the channel number, then it searches where the pin is now, updates the value, and moves it to the correct position.
Finally the pins before and after it are checked to ensure that the distance is large enough for the compare register to be updated.
If not, the value is changed to the same value as the previous or next pin.
This gives a small error of 1/256 (when MINDIST is 2) for some values. I don't think that's that bad though, but it might depend on the function you want to do with it.
Code:
void setChannel2(char ch, char v)
{
  char pin = channelMap[ch];
  for (int i = 0; i < NB_CHANNELS; i++) {
    if (timerPins[i] == pin) {
      timerSteps[i] = v;
      while (i > 0 && timerSteps[i-1] > v) {
        timerSteps[i] = timerSteps[i-1];
        timerSteps[i-1] = v;
        timerPins[i] = timerPins[i-1];
        timerPins[i-1] = pin;
        i--;
      }
      while (i < NB_CHANNELS_MIN_1 && timerSteps[i+1] < v) {
        timerSteps[i] = timerSteps[i+1];
        timerSteps[i+1] = v;
        timerPins[i] = timerPins[i+1];
        timerPins[i+1] = pin;
        i++;
      }
      if (i > 0 && v > timerSteps[i-1] && v - timerSteps[i-1] < MINDIST)
        timerSteps[i] = timerSteps[i-1];
      else if (i < NB_CHANNELS_MIN_1 && v < timerSteps[i+1] && timerSteps[i+1] - v < MINDIST)
        timerSteps[i] = timerSteps[i+1];
      break;
    }
  }
}
Logged

Pages: [1]   Go Up
Jump to: