Can one synchronize more then 3 PWM channels on an Arduino Due?

I am trying to use 6 (of the 8) PWM channels of the Arduino Due (SAM 3x8) in a synchronized fashion. However, the moment I define more than three synchronized channels (setting 4 bits of the PWM_SCM register to '1') only 3 channels work, and one of the three which worked before does not work anymore? I do not change anything else in the program. And I can get any combo of three channels out of the 6 to work in synch'ed fashion but not more than 3! (I tried all these combo's to ensure that I do not have errors in enabling the PWM outputs to the IO's of the Arduino due board). I read the SAM3x8 (144qfp) manual up and down, but I do not find that there is a limitation to a max. of 3 synchronized channels. Anyone has experience here with more then 3 channels synch'd? Please note, I do NOT use timers, I use the built-in PWM Macro. (Chapter 38 of the SAM3x manual)

give details of the signals you are attempting to generate?

@horace - In the end I want to have signals to drive a dual H-bridge for 3-phase motors. For the time being I have just rather static signals just to test what is going not well.

#include <stdio.h>
//
// PMC constants are in:
// /Users/.../Library/Arduino15/packages/arduino/hardware/sam/1.6.12/system/CMSIS/Device/ATMEL/sam3xa/include/component/component_pmc.h
// PIO  in.  ..... component_pio.h
// .

//
/                Synchro channel PWML0....,  with PDC DMA trigger on Compare              /
//

// new:  wavesize=64  -  64 columns voor sinus
//  50 freq values: 1..50Hz, min. 200 pulses per column (@50Hz) 1.... 10000 pulses (@1Hz)
// the clock should be as close to 640 KHz as possible.  84MHz/131 = 641221 Hz.  131 = 0x83

#define sinsize  (64)
#define Wavesize (64)

uint16_t freq_values[50]={10000, 5000, 3333, 2500, 2000, 1667, 1429, 1250, 1111, 1000,  // 1...10 Hz
909,  833,  769,  714,  667,  625,  588,  556,  526,  500,  // 11..20 Hz
476,  455,  435,  417,  400,  385,  370,  357,  345,  333,  // 21..30 Hz
323,  313,  303,  294,  286,  278,  270,  263,  256,  250,  // 31..40 Hz
244,  238,  233,  227,  222,  217,  213,  208,  204,  200}; // 41..50 Hz
int freq_index = 0;
#define min_freq_index (0)
#define max_freq_index (49)

#define NbCh      (8)                 // Max. Number of synch channels = 1, -determines the DMA buffer size

#define DUTY_BUFFER_LENGTH      (Wavesize * NbCh) // Half words

uint16_t Duty_Buffer[DUTY_BUFFER_LENGTH][2];  // double buffer
int index_buffer =  0;   // can be 0 or 1, points to the buffer currently in use.

#define UpdatePeriod_Msk (0b1111)     // or 0b0000 ?
#define UpdatePeriod    (UpdatePeriod_Msk & 0b1000) //Defines the time between duty cycle update of
// the synchronous channels
//This time equals to (UpdatePeriod + 1) periods of the Reference channel 0

int16_t Sin_Duty[sinsize];     // to calculate the number of pulses in each sin-interpolation interval
char Output_string[130];     // general buffer to write output data to console

void setup() {

Serial.begin(57600);

// PMC_PCERx = peripheral clock controller. x= 0,1
// clock 36 - see sam3x manual section 7.1 pg. 32.

PMC->PMC_PCER1 |= PMC_PCER1_PID36;  // set bit 4 of peripheral clock enable register 1 (PCER1) -- that is clock 36.
// this sets the "PMW controller ON"

// PWML0 on PIO-controller C output PC2, peripheral type B
// PIO Controller see chapter 31.
// PIOC = 0x400E1200  (defined in sam3x8c.h)
// note: writing a '1' to the PDR (PIO Disable Register) will set the bit to '0' which indicates peripheral control

// PWML0 on PC2 ; Peripheral B
PIOC->PIO_PDR |= PIO_PDR_P2;
// PIO_PDR set such (0) that pin PC2 is controlled by the on-chip peripheral (31.5.2 section)
PIOC->PIO_ABSR |= PIO_PC2B_PWML0;  // peripheral A or B select register (ABSR) - select peripheral B (31.5.3) PWML0 - PC2=digital pin 34
// PWML1 on PC4 ; Peripheral=B
PIOC->PIO_PDR |= PIO_PDR_P4;      // idem for pin PC4 - connect to peripheral    -  PC4 = digital pin 36.
PIOC->PIO_ABSR |= PIO_PC4B_PWML1;    // connect to PWML1 - select peripheral B
// PWML2 on PC6 ; Peripheral type B
PIOC->PIO_PDR |= PIO_PDR_P6;      // similar for pin PC6  - PC6 = digital pin 38.
PIOC->PIO_ABSR |= PIO_PC6B_PWML2;  // connect to PWML2
// PWML3 on PC8 ; Peripheral type B  PC8 = Digital Pin 40
PIOC->PIO_PDR |= PIO_PDR_P8;
PIOC->PIO_ABSR |= PIO_PC8B_PWML3;
// PWML4 on PC21 ; Peripheral type B PC21 = Digital Pin 9
PIOC->PIO_PDR |= PIO_PDR_P21;
PIOC->PIO_ABSR |= PIO_PC21B_PWML4;
// PWML5 on PC22 ; Peripheral type B PC22 = Digital Pin 8
PIOC->PIO_PDR |= PIO_PDR_P22;
PIOC->PIO_ABSR |= PIO_PC22B_PWML5;
// PWML6 on PC23 ; Peripheral type B PC23 = Digital Pin 7
PIOC->PIO_PDR |= PIO_PDR_P23;
PIOC->PIO_ABSR |= PIO_PC23B_PWML6;
// PWML7 on PC24 ; Peripheral type B PC24 = Digital Pin 6
PIOC->PIO_PDR |= PIO_PDR_P24;
PIOC->PIO_ABSR |= PIO_PC24B_PWML7;

// disable PWM output of channel 0, 1, 2, 3, 4, 5, 6, 7: - so that we can safely manipulate channel settings
PWM->PWM_DIS = PWM_DIS_CHID0 | PWM_DIS_CHID1 | PWM_DIS_CHID2 | PWM_DIS_CHID3  | PWM_DIS_CHID4 | PWM_DIS_CHID5 | PWM_DIS_CHID6 | PWM_DIS_CHID7;

// PWM_SCM = PWM Sync Channels Mode Register:
// here we write channel 0,1,2,3,4,5,6,7 or less, to be synchronous and update-mode=2: automatic write of duty-cycle..
PWM->PWM_SCM  = PWM_SCM_SYNC0       //   pin 34
| PWM_SCM_SYNC1     //   pin 36
| PWM_SCM_SYNC2     //   pin 38
| PWM_SCM_SYNC3     //   pin 40
| PWM_SCM_SYNC4     //   pin 9
| PWM_SCM_SYNC5     //   pin 8
| PWM_SCM_UPDM_MODE2;  //Automatic write of duty-cycle update registers by the PDC and
//                       automatic update of synchronous channels */

// Set synchronous channels update register
//  UpdatePeriod = 0b0000 -->  set lower 4 bits to 0.  so UPR = 1 clock.
PWM->PWM_SCUP = PWM_SCUP_UPR(UpdatePeriod);

// Set the PWM Reference channel 0 i.e. : Clock/Frequency/Alignment
PWM->PWM_CLK = PWM_CLK_PREA(0b0000) | PWM_CLK_DIVA(131);  // Set the PWM clock rate for +/- 641 kHz.
// PREA=0b0000, DIVA = 131  (0x83)

PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;
// The period is left aligned, clk src=CLKA on channel 0  - CPRE=0x0000000B

PWM->PWM_CH_NUM[0].PWM_CPRD = freq_values[freq_index];
// Set the PWM frequency (84MHz/3)/PERIOD_VALUE/wavesize +- 1  Hz
Serial.print("PWM_CLK: ");
sprintf(Output_string,"%#.8lX",PWM->PWM_CLK);
Serial.println(Output_string);

Serial.print("PWM_SCM: ");
sprintf(Output_string,"%#.8lX",PWM->PWM_SCM);
Serial.println(Output_string);

// Set Interrupt events
PWM->PWM_IER2 = PWM_IER2_WRDY;   //Write Ready for Synchronous Channels Update Interrupt Enable
//synchro with ENDTX End of TX Buffer Interrupt Enable

fill_duty_cycle_buffer();

PWM->PWM_ENA = PWM_ENA_CHID0;    // Enable PWM for all channels, channel 0 Enable is sufficient

PWM->PWM_TPR  = (uint32_t)&Duty_Buffer[0][index_buffer];        // FIRST DMA buffer
PWM->PWM_TCR  = DUTY_BUFFER_LENGTH;                             // Number of Half words in first DMA buffer
PWM->PWM_TNPR = (uint32_t)&Duty_Buffer[0][index_buffer];        // Next DMA buffer - initial the same as the first DMA buffer
PWM->PWM_TNCR = DUTY_BUFFER_LENGTH;

NVIC_EnableIRQ(PWM_IRQn);    // enable interrupt in Nested Vectored Interrupt Controller

Serial.print("Length: ");    //  note: when we print after transmitter transfer enable, we see a gradual
sprintf(Output_string,"%u",PWM->PWM_TCR);   // reduced value of PWM->PWM_TCR
Serial.println(Output_string);

PWM->PWM_PTCR = PWM_PTCR_TXTEN;  // enable the peripheral DMA controller
}

void loop() {

// For debugging only :
// Check correct updates of CDTY via CDTYUPD by DMA
// Menu > Tools > Serial Plotter -- 57600 baud
//printf("%d,%d,%d\n",
//  PWM->PWM_CH_NUM[0].PWM_CDTY,
//  PWM->PWM_CH_NUM[1].PWM_CDTY,
//  PWM->PWM_CH_NUM[2].PWM_CDTY
// );
// If a PWM frequency update of PWM0 (and therefore PWM1, PWM2, ... PWM7)
// Do it with PWM_CPRDUPD in loop()
if (Serial.available() > 0)  {
//  there is some input
char inputChar = Serial.read();
Serial.print("Input read: '");
Serial.println(inputChar);
Serial.println("'");

if (inputChar == 'u') {
Serial.println("up");    // freq. up means period value goes down
if (index_buffer == 0) {index_buffer=1;}    // switch index to other buffer
else index_buffer=0;
if (freq_index < max_freq_index) { freq_index=freq_index+1;}
fill_duty_cycle_buffer();
PWM->PWM_CH_NUM[0].PWM_CPRDUPD = freq_values[freq_index];
Serial.print("Freq is now: ");
Serial.print(freq_index+1);
Serial.println(" Hz");
}  // inputChar == u
if (inputChar == 'd') {
Serial.println("down");    // freq. up means period value goes down
if (index_buffer == 0) {index_buffer=1;}    // switch index to other buffer
else index_buffer=0;
if (freq_index > min_freq_index) { freq_index=freq_index-1;}
fill_duty_cycle_buffer();
PWM->PWM_CH_NUM[0].PWM_CPRDUPD = freq_values[freq_index];
Serial.print("Freq is now: ");
Serial.print(freq_index+1);
Serial.println(" Hz");
}
PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;  // set this bit so the CPRD update will happen
Serial.print("PWM_CPRD[0]: ");
sprintf(Output_string,"%#.8lX",PWM->PWM_CH_NUM[0].PWM_CPRD);
Serial.println(Output_string);
} // end if Serial.available
}  // end loop()

void PWM_Handler() {  // move PDC DMA pointers to next buffer

PWM->PWM_ISR2;      // Clear interrupt status register 2
PWM->PWM_TNPR = (uint32_t)&Duty_Buffer[0][index_buffer];
PWM->PWM_TNCR = DUTY_BUFFER_LENGTH;
}

void fill_duty_cycle_buffer() {
// Fill duty cycle buffer for channels 0, x, y ...
// Duty_Buffer is a double buffer of Half Words(H_W) composed of N lines whose structure model for each duty cycle update is :
// [ H_W: First synchro channel 0 duty cycle Mandatory ]/[ H_W: Second synchro channel duty cycle ] ... and so on
// index is the index of the buffer to fill ( the other index is in use )
//      PWMLx waveform between 0° and 360°    //
// this genertes a series of duty-cycle values from 0...freq_values[freq_index]
for (int i = 0; i < sinsize ; i++) {
//  Sin_Duty[i] = (freq_values[freq_index] * (abs(sinf( i * 2 * PI / sinsize )) + 0));    // sinf is single precision sinus: #define sinf(x) (float)sin((double)(x))
//  Sin_Duty[i]=(freq_values[freq_index](abs(sinf(PI/sinsize(1+2i))) + 0));
Sin_Duty[i]=(freq_values[freq_index]((sinf(PI/sinsize*(1+2i))) + 0));
}
/     Fill Duty_Buffer for all synchro channels  /
/ note that L1 = sin(x), L2= sin(x-pi/3) = sin(x+2pi/3), L3=sin(x-2pi/3)=sin(x+pi/3) /
//  channel 0 =  sin(x) if sin(x) > 0  0 else      L1
//  channel 1 = -sin(x) if sin(x) < 0  0 else
//  channel 2 =  sin(x-pi/3) if sin(x-pi/3) > 0  else 0   L2
//  channel 3 = -sin(x-pi/3) if sin(x-pi/3) < 0  else 0
//  channel 4 =  sin(x-2pi/3) if sin(x-2pi/3) > 0 else 0  L3
//  channel 5 = -sin(x-2pi/3) if sin(x-2pi/3) < 0 else 0
for (uint32_t i = 0; i < sinsize; i++) {
if ( Sin_Duty[i] > 0 ) {  // L1
Duty_Buffer[i * NbCh + 0][index_buffer] = 4000; // Sin_Duty[i];
Duty_Buffer[i * NbCh + 1][index_buffer] = 4000; } //  0; }
else {
Duty_Buffer[i * NbCh + 0][index_buffer] = 3000;  // 0;
Duty_Buffer[i * NbCh + 1][index_buffer] = 3000; } // abs(Sin_Duty[i]); }
if ( Sin_Duty[(i + sinsize / 3) % sinsize] > 0 )  {       //  note that this is L3 !
Duty_Buffer[i * NbCh + 4][index_buffer] = 2500; // Sin_Duty[(i + sinsize / 3) % sinsize];
Duty_Buffer[i * NbCh + 5][index_buffer] = 2500; } //0; }
else {
Duty_Buffer[i * NbCh + 4][index_buffer] = 2000; // 0;
Duty_Buffer[i * NbCh + 5][index_buffer] = 2000; } //abs(Sin_Duty[(i + sinsize / 3) % sinsize]); }
if (Sin_Duty[(i + 2 * sinsize / 3) % sinsize] > 0 ) {       //  note that this is L2 !
Duty_Buffer[i * NbCh + 2][index_buffer] = 1500; // Sin_Duty[(i + 2 * sinsize / 3) % sinsize];
Duty_Buffer[i * NbCh + 3][index_buffer] = 1500; } //0; }
else {
Duty_Buffer[i * NbCh + 2][index_buffer] = 1000; // 0;
Duty_Buffer[i * NbCh + 3][index_buffer] = 1000; } // abs(Sin_Duty[(i + 2 * sinsize / 3) % sinsize]);
if (i<32) {
Duty_Buffer[iNbCh+6][index_buffer] = 1200;     // fill channel 6
Duty_Buffer[iNbCh+7][index_buffer] = 4500;     // fill channel 7
}
else {
Duty_Buffer[iNbCh+6][index_buffer] = 600;     // fill channel 6
Duty_Buffer[i*NbCh+7][index_buffer] = 2010;     // fill channel 7
}
}  // end for..

/* for (int i=0; i<DUTY_BUFFER_LENGTH; i++) {
Serial.print("i=");
Serial.print(i);
Serial.print(" Duty buffer=");
Serial.println(Duty_Buffer[i][index_buffer]);
} */

}  // end fill_duty cycle buffer

void print_D_B_formatted() {
Serial.println("L1+:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+0][index_buffer]);
}
Serial.println("L1-:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+1][index_buffer]);
}
Serial.println("L2+:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+2][index_buffer]);
}
Serial.println("L2-:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+3][index_buffer]);
}
Serial.println("L3+:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+4][index_buffer]);
}
Serial.println("Ch 6:");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[iNbCh+6][index_buffer]);
}
Serial.println("Ch 7");
for (int i = 0; i < sinsize ; i++) {
Serial.print("i= ");
Serial.print(i);
Serial.print(" Duty_buffer=");
Serial.println(Duty_Buffer[i*NbCh+7][index_buffer]);
}
} // end print_D_B_formatted


have you tried the Arduino Due PWMC library?

Hi @tonie2

Here's a Due PWM controller example using 8 synchronous channels and the PDC DMAC, to load their respective duty-cycle update registers.

The code operates at a frequency of 50Hz and continuously fires off five pulses at a duty-cycle of 25%, another five at 50% and a further five at 75% on pins: DAC1, A8, A9, A10, D9, D8, D7 and D6 for PWM channels 0 through to 7.

// Enable synchronous, single-slope PWM at 50Hz on 8 channels (0 to 7), at predefined duty-cycles:
// five pulses at 25%, five at 50% and five 75% on all channels
uint16_t data[] = { 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000,   // Synchronous channels 0 to 7 index 0
                    5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000,   // Synchronous channels 0 to 7 index 1
                    5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000,   // Synchronous channels 0 to 7 index 2, etc...
                    5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000,
                    5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000,
                    10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                    10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                    10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                    10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                    10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 
                    15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000,
                    15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000,
                    15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000,
                    15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000,
                    15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000 }; 

// Enable synchronous, single-slope PWM at 50Hz on 8 channels
void setup() {
  // PWM set-up on pins DAC1, A8, A9, A10, D9, D8, D7 and D6 for channels 0 through to 7 respectively
  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                                               // Enable PWM 
  PIOB->PIO_ABSR |= PIO_ABSR_P19 | PIO_ABSR_P18 | PIO_ABSR_P17 | PIO_ABSR_P16;     // Set the port B PWM pins to peripheral type B
  PIOC->PIO_ABSR |= PIO_ABSR_P24 | PIO_ABSR_P23 | PIO_ABSR_P22 | PIO_ABSR_P21;     // Set the port C PWM pins to peripheral type B
  PIOB->PIO_PDR |= PIO_PDR_P19 | PIO_PDR_P18 | PIO_PDR_P17 | PIO_PDR_P16;          // Set the port B PWM pins to outputs
  PIOC->PIO_PDR |= PIO_PDR_P24 | PIO_PDR_P23 | PIO_PDR_P22 | PIO_PDR_P21;          // Set the port C PWM pins to outputs
  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84);                               // Set the PWM clock A rate to 1MHz (84MHz/84)
  PWM->PWM_SCM |= PWM_SCM_UPDM_MODE2 |                                             // Automatically load the duty-cycle register with the PDC each timer cycle 
                  PWM_SCM_SYNC7 | PWM_SCM_SYNC6 | PWM_SCM_SYNC5 | PWM_SCM_SYNC4 |  // Set the PWM channels as synchronous
                  PWM_SCM_SYNC3 | PWM_SCM_SYNC2 | PWM_SCM_SYNC1 | PWM_SCM_SYNC0;
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;      // Enable single slope PWM and set the clock source as CLKA for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CPRD = 19999;                 // Set the PWM frequency 1MHz/(19999 + 1) = 50Hz for all synchronous channels
  for (uint8_t i = 0; i < PWMCH_NUM_NUMBER; i++)       // Loop for each PWM channel (8 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTY = 0;                   // Set the initial PWM duty cycle to 0%
  } 
  NVIC_SetPriority(PWM_IRQn, 0);                       // Set the Nested Vector Interrupt Controller (NVIC) priority for the PWM controller to 0 (highest) 
  NVIC_EnableIRQ(PWM_IRQn);                            // Connect PWM Controller to Nested Vector Interrupt Controller (NVIC)
  PWM->PWM_IER2 = PWM_IER2_WRDY;                       // Enable interrupt when Write Ready (WRDY) is set
  
  PWM->PWM_TPR = (uint32_t)data;                       // Set the address of the transmit data pointer
  PWM->PWM_TCR = 120;                                  // Set the length of the transmit data
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 120;                                 // Set the next transmit counter
  PWM->PWM_PTCR |= PWM_PTCR_TXTEN;                     // Enable the Peripheral DMA Controller 
  PWM->PWM_ENA = PWM_ENA_CHID0;                        // Enable all synchronous PWM channels       
}

void loop() {}

void PWM_Handler()                                     // PWM Controller Interrupt Service Routine (ISR)
{
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 120;                                 // Set the next transmit counter
  PWM->PWM_ISR2;                                       // Clear the interrupt status register 2
}

Found the solution!! Duty_Buffer is a double buffer to fill with new values while the other is still running, BUT the last index causes values to be stored in contiguous memory, which is needed to get the DMA working correctly. So changing Duty_Buffer[DUTY_BUFFER_LENGTH][2] to Duty_Buffer[2][DUTY_BUFFER_LENGTH] solves the problem! And now it works perfectly!

What I find strange is that I copied most of the code from another entry either in this forum or stackoverflow (or so), where someone showed a 3-sinewave solution. So this 3-sinewave solution can not work as it stands.