Fast 6 channel PWM on Arduino

Hello All,

I though I might post this to contribute back to the community from whom I learned so much… I found a way to generate a fast (up to 32 kHz) PWM signal on the outputs 8 to 13 of the arduino UNO R3 board (the whole PORTB). Each output can have its own duty cycle but they all will work at the same frequency. The outputs could be changed to any other port with little work. Using the same code you could even add PWM channels…In theory there is no limit :slight_smile: Originally it is intended to drive LED lights but you could do many other things.

Here is the first part (second part in next post) of the (working) sketch. Just connect 6 LEDs with resistors to the port B of your arduino (outputs 8 to 13 on the UNO) and enjoy…
I would also be interrested if anyone with the right equipment (oscilloscope) could test the limits of this routine. Especially what are the limitations in terms of frequency and lowest duty cycle…

// PWM sketch. Uses Timer 1. PWM frequency:
// From 32 to 20 kHz with prescaler at 8
// From 250 Hz to 32 kHz with prescaler at 0. Be careful the resolution will go down as you increase the frequency..
// Note that these values are theoretical and have not been tested with an oscilloscope.
// Maximum frequency depends on the desired resolution, but it can be quite high especially with no prescaler. 
// In theory 
// ALl channels are turned on at the same time. Then each channel is turned off in turn, from the lowest duty cycle
// to the highest duty cycle. A function is necessary () to first sort the channels according to the duty cycles from
// the lowest to the highest. Then another function () is called to calculate the preload values to be used with Timer 1
// to generate the interrupts. 

// Author : Hadrien C.

// Definitions for PWM function
//----------------------------------------------------------------------------

byte chanPin[] = {0, 1, 2, 3, 4, 5}; // put the PORTB ucontroller PINs, not the arduino pinout pins.
// PWM only on PORTB, so 6 channels max. Mapping :
// Arduino  PORTB
//   13       5
//   12       4
//   11       3
//   10       2
//   9        1
//   8        0
//  PORTB 6 and 7 are reserved for Xtal so cannot be used
byte chanPinSorted[] = {0, 1, 2, 3, 4, 5};                  // As many elements as channels !
float dutyCycle[] = {0, 0, 0, 0, 0, 0};                     // duty cycles for each channel. As many elements as channels !
                                                            // Numbers between 0 and 100.
float dutyCycleSorted[] = {0, 0, 0, 0, 0, 0};               // duty cycles for each channel. As many elements as channels !
unsigned int preloadValuesSorted[]={0, 0, 0, 0, 0, 0, 0};   // for n channels there are n + 1 preload values

volatile byte D_index = 0;                                  // This is used in the Timer1 ISR.
byte chanCount;                                             // Will contain the number of PWM channels  

#define pwmFrequency 150                                    // 32 Hz mini with prescaler at 8, 250 Hz with no prescaler
#define sysClk 16000000                                     // For 16 MHz system clock.
#define prescaleT1 8                                        // prescaler used, 1 or 8. Do not forget to change the T1 prescaler configuration bits in the Setup function

float dutyDiffMini = 1 / ((float)sysClk / ((float)pwmFrequency * 100 * (float)prescaleT1));
// This is needed to avoid issues of two duty cycles are axactly the same. This allows the ISR to stay simple and fast.

// Other definitions
//----------------------------------------------------------------------------

unsigned long timeMillis;

// Fast port access macros
//----------------------------------------------------------------------------

#define CLR(x,y) (x&=(~(1<<y)))                             // Improved greatly (by a factor of 25) the speed at which the port bits are changed w.r.t. digitalWrite
#define SET(x,y) (x|=(1<<y))                                // This is needed to minimise the time spent in ISR and to keep the lag to a minimum.
                                                            // This is how to use it : CLR(PORTB, 0) ;  SET(PORTB, 0) ; etc.

// Init function
//----------------------------------------------------------------------------

void setup()
{
  // Start serial com with PC
  //Serial.begin(9600);

  // Disable all interrupts
  noInterrupts();           

  TCCR1A = 0;
  TCCR1B = 0;
  TIMSK1 = 0;  
  TCNT1 = 0;    // preload timer

  chanCount = sizeof(chanPin);
  
  // Set PWM pins to output
  for (int i = 0; i < chanCount; i++)
  {
    SET(DDRB, chanPin[i]); 
  }
 
 //T1 prescaler configuration bits
 //-------------------------------
  //SET(TCCR1B,CS10);      // no prescaling (=1) 
  SET(TCCR1B,CS11);      // 8 prescaling 
  
  // Everytime you change one or several duty cycle you need to call these two functions in that order.
  sort_arrays(dutyCycleSorted, dutyCycle, chanPin, chanPinSorted, chanCount);
  calculate_timer_preload (preloadValuesSorted, dutyCycleSorted, pwmFrequency, sysClk, prescaleT1, chanCount + 1);
  
  SET(TIMSK1,TOIE1);     // enable timer overflow interrupt
  
  interrupts();             // enable all interrupts*/    
}

// ISR for Timer 1 overflow
//----------------------------------------------------------------------------

ISR(TIMER1_OVF_vect)         
{
  // interrupt service routine of TIMER1, used to generate PWM signals on all PORTB pins (6 channels)
  // Direct port access is used here since the speed of the conventionnal port access function
  // is not acceptable for this application.
  //------------------------------------------------------------------------------------------
  
  if (D_index == 0) 
  {
    // If D_index = 0, turn ON all the PWM channels
    //---------------------------------------------
    
    /*for (int i = 0; i < chanCount; i++)
    {
      SET(PORTB, chanPinSorted[i]);
    }*/
    PORTB = PORTB | B00111111; // This is even faster than the loop but less flexible. Change things here if you don't use all 6 channels.
    TCNT1 = preloadValuesSorted[D_index];
    D_index += 1;

  }
  else if (D_index < chanCount)
    // If D_index < chanCount, turn OFF the D_index - 1 channel and load the appropriate
    // value in TMR1 for the next channel turn OFF
    //----------------------------------------------------------------------------------
    
  {
    //digitalWrite(chanPinSorted[D_index - 1], LOW);
    CLR(PORTB, chanPinSorted[D_index - 1]);
    TCNT1 = preloadValuesSorted[D_index];
    D_index += 1;
  }
  else
  {
    // If D_index = chanCount, turn OFF the last PWM channel and load the appropriate
    // value in TMR1 for the general turn ON, and set D_index back to 0.
    //----------------------------------------------------------------------------------
    
    //digitalWrite(chanPinSorted[D_index - 1], LOW);
    CLR(PORTB, chanPinSorted[D_index - 1]);
    TCNT1 = preloadValuesSorted[D_index];
    D_index = 0;
  }
 }

// Main loop
//----------------------------------------------------------------------------

void loop()
{
  if (millis() - timeMillis > 1000)
  {
    
   timeMillis = millis();

      for (int i = 0; i < chanCount; i++)
     {
       dutyCycle[i] = (float)random(0,1000)/100;
     }
     CLR(TIMSK1, TOIE1);
     sort_arrays(dutyCycleSorted, dutyCycle, chanPin, chanPinSorted, chanCount);
     calculate_timer_preload (preloadValuesSorted, dutyCycleSorted, pwmFrequency, sysClk, prescaleT1, chanCount + 1);
     SET(TIMSK1, TOIE1);
   }
}

And here is the second part :slight_smile:

void sort_arrays (float* dutySorted, float* duty, byte* pins, byte* pinsSorted, byte sizeofArray)
  {
    float minimum = 100;
    byte indexmin = 0;
    float tempDuty = 0;
    byte tempPin = 0;
    
    for (int i = 0; i < sizeofArray; i ++)
    {
      dutySorted[i] = duty[i]; 
      pinsSorted[i] = pins[i];
    }

    for (int sortedIndex = 0; sortedIndex < sizeofArray - 1; sortedIndex++)
    {  
      for (int i = sortedIndex; i < sizeofArray; i++)
      {        
        if ( dutySorted[i] < minimum )
        {
          minimum = dutySorted[i];
          indexmin = i;
        }        
      }
      tempDuty = dutySorted[sortedIndex];
      dutySorted[sortedIndex] = dutySorted[indexmin];
      dutySorted[indexmin] = tempDuty;
      
      tempPin = pinsSorted[sortedIndex];
      pinsSorted[sortedIndex] = pinsSorted[indexmin];
      pinsSorted[indexmin] = tempPin;
      
      minimum = 100;
    }
  }
  
  void calculate_timer_preload (unsigned int* preloadValueSorted, float* dutySorted, int pwmFreq, long clkFreq, byte prescale, byte sizeofPreloadArray)
  {    
    int tick_per_percent = clkFreq / ((long)pwmFreq * 100 * (long)prescale); // prescale factor on clock for timer
    for (byte i = 0; i < sizeofPreloadArray; i++)
    {
      if (i == 0)
      {
        if (dutySorted[ i ] < dutyDiffMini)
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * dutyDiffMini;
        }
        else
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * dutySorted[ i ];
        }     
      }
      else if (i < sizeofPreloadArray - 1)
      {
        if (dutySorted[ i ] - dutySorted[ i - 1 ] < dutyDiffMini)
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * dutyDiffMini;
        }
        else
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * ( dutySorted[ i ] - dutySorted[ i - 1 ] );
        }
      }
      else
      {
         if (100 - dutySorted[ i - 1 ] < dutyDiffMini)
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * dutyDiffMini;
        }
        else
        {
          preloadValueSorted[i] = 65536 - tick_per_percent * (100 - dutySorted[ i - 1 ]);
        }        
      }
    }
  }

The following line:

dutyCycle[i] = (float)random(0,1000)/100;

will only generate duty cycles from 0 to 10 %. Divide by only 10 instead of 100 to get the full range. This is for demo only.

Cheers, Hadrien

Bump.. Would someone with a scope be willing to measure the max frequency achievable with this method ? Also minimum duty cycle vs frequency ?

I set the prescale to 1, then forced to the ticks_per_percent value in calculate_timer_preload() to 2, getting about 20kHz, but didn't seem to be much accuracy in the duty cycle.

Setting ticks_per_percent to 10 gave just over 10kHz with much more duty cycle resolution.

Setting ticks_per_percent to 100 gave just over 1.5kHz.

Leaving it as calculated (655) gives 241.7Hz