Simple multi-channel PWM

mborgerson

Well-known member
In response to a thread discussing issues with the interval timer in the "Suggestions and Bug Reports" forum:
(https://forum.pjrc.com/threads/62827-IntervalTimer-update-and-begin-documentation-improvement
I decided to see what minimum amount of generic C code it takes to implement a multi-channel PWM generator suitable for ~12-bit resolution and a PWM period of 50milliSeconds.

It turns out that you only need a single IntervalTimer, a few data arrays and about 40 lines of code to implement a 4-channel PWM generator. The timer IRQ handler only takes about 60nanoSeconds to execute on a T4.0 at 60MHz. Of course, you need some more code to set things up and make the values change so that you can evaluate things with an oscilloscope. Still, the demo would be less than 100 lines were it not for my verbose comments at the beginning ;-)

Code:
/**************************************************
   Simple multi-channel PWM output demo
   This PWM generator can use any digital output pin
   and needs only a single interval timer.

   This is a lot like other software PWM functions in that
   it is best suited to fairly long PWM periods if you
   need high resolution.

   This demo code has a few constraints:

   * all the PWM channels have the same period.
   * The channels are synchronized in that all channel outputs
     go high at the same time--that could be problematic in 
     when controlling high-power hardware.

    * unlike many of the fancier libraries, this is all standard C 
      except for the IntervalTimer and it is written for the T4.x, but
      will probably work on the T3.X, with a higher fraction of CPU usage.


     With some more complex data structures, this algorithm could be enhanced
     to have different periods and resolutions on different channels (within the 
     constraints of the fundamental timer interrupt interval)

     mborgerson 9/4/2020
*******************************************************************/

IntervalTimer pwmTimer;

#define PWMCHANNELS 4
#define PWMRESOLUTION 10  // Smallest increment of pulse width in microseconds
#define PWMPERIOD 5000   // Period of PWM  in increments of PWMRESOLUTION---5000 x 10  gives 20Hz frequency

// Oscilloscope  marker to show duration of timer chore
#define TMRLOW digitalWriteFast(tmrpin, LOW);
#define TMRHI  digitalWriteFast(tmrpin, HIGH);

const uint16_t pwmin = 1;
const uint16_t pwmax = PWMPERIOD - 1; // 4999 for demo

// set pins near T4.0 board end as PWM outputs
const uint16_t chanpins[PWMCHANNELS] = {11, 12, 13, 14};
const int tmrpin = 0;

// Pwm High Counts set how long output is high at start of period
// these values get set in main program and are read by timer interrupt service routine
volatile uint16_t pwmHighCounts[PWMCHANNELS]; // initial values set in InitilizePWM()

const char compileTime [] = "\n\nInterval Timer PWM Test compiled on " __DATE__ " " __TIME__;

// set the pins as outputs and set an initial value for PWM
void initializePWM(uint16_t numchans, const uint16_t *pins, uint16_t initialvalue) {
  uint16_t i;
  for (i = 0; i < numchans; i++) {
    pinMode(pins[i], OUTPUT);
    digitalWriteFast(pins[i], 0);
    pwmHighCounts[i] = initialvalue;
  }
}


// pwm interval timer interrupt service routine
//NOTE: with 4 channels of PWM, the PWM chore executes in 30 to
//      60 nanoseconds with T4.x at 600MHz. (<1% of CPU bandwidth);
void pwmChore(void){
  uint16_t i;
  static uint16_t pcount;  // for longer periods, this might need to be uint32_t
  TMRHI
  for(i=0; i< PWMCHANNELS; i++){
    if(pcount >= pwmHighCounts[i]) digitalWriteFast(chanpins[i],LOW); 
  }
  pcount++;
  if(pcount >= PWMPERIOD){// set all pins high and restart count
    for(i=0; i< PWMCHANNELS; i++) digitalWriteFast(chanpins[i], HIGH);
    pcount = 0;
  }
  TMRLOW
}

void setup() {
  Serial.begin(9600);
  delay(1000);  // wait for PC to connect
  Serial.println(compileTime);
  pinMode(tmrpin, OUTPUT);  // pin to time length of pwmChore

  initializePWM(PWMCHANNELS,chanpins, 10);
  pwmTimer.begin(pwmChore, PWMRESOLUTION);
  Serial.println("Starting PWM output");
}

void loop() {
  // Generate sawtooth ramp changes to pwm channels
  // Channel values are changed ~10 times per second
  delay(100);  // sets speed of channel updates
  RampUp(0,pwmHighCounts, 5);
  RampDown(1, pwmHighCounts,7);
  RampUp(2,pwmHighCounts, 25);  // This one is the LED pin
  RampDown(3,pwmHighCounts, 30);
} // end of loop()


// Simple functions to vary the PWM outputs
void RampDown(uint16_t cnum, volatile uint16_t *hcounts, uint16_t increment){
  uint16_t newcount;
  newcount = hcounts[cnum];  // get existing value
  if(newcount < increment){  //underflow takes us back to max
    newcount = pwmax;
  } else {
    newcount-= increment;
  }
  noInterrupts();
  hcounts[cnum] = newcount;
  interrupts();
}


void RampUp(uint16_t cnum, volatile uint16_t *hcounts, uint16_t increment){
  uint16_t newcount;
  newcount = hcounts[cnum];  // get existing value
  if((newcount+increment) > pwmax){  //overflow takes us back to pwmin
    newcount = pwmin;
  } else {
    newcount+= increment;
  }
  noInterrupts();
  hcounts[cnum] = newcount;
  interrupts();  
}
 
Back
Top