XIAO PWM modifying duty cycle inside an interrupt

Hi guys,
I'm working on Seeduino XIAO with SAMD21 architecture.

I have found a code that output a square wave on D3 with 50% duty cycle.

Here it is

// Code from: https://www.hackster.io/voske65/high-speed-pwm-on-arduino-atsamd21-859b06

/*

HIGH FREQ PWM SETUP FOR MKR1000 - SAMW25 = SAMD21 with some other pinning

MKR1000's pins than can be defined as TCC timer pins and their associated channel (WO[X])
unless stated otherwise the TCC timers are on peripheral F:

A0 - PA02 - None
A1 - PB02 - None
A2 - PB03 - None
A3 - PA04 - TCC0/WO[0] (same channel as TCC0/WO[4])
A4 - PA05 - TCC0/WO[1] (same channel as TCC0/WO[5])
A5 - PA06 - TCC1/WO[0]
A6 - PA07 - TCC1/WO[1]
D0 - PA22 - TCC0/WO[4] (same channel as TCC0/WO[0])
D1 - PA23 - TCC0/WO[5] (same channel as TCC0/WO[1])
D2 - PA10 - TCC1/WO[0]
*D3 - PA11 - TCC1/WO[1]
D4 - PB10 - TCC0/WO[4] (same channel as TCC0/WO[0])
D5 - PB11 - TCC0/WO[5] (same channel as TCC0/WO[1])
D6 - PA20 - TCC0/WO[6] (same channel as TCC0/WO[2])
D7 - PA21 - TCC0/WO[7] (same channel as TCC0/WO[3])
D8 - PA16 - TCC0/WO[6] (same channel as TCC0/WO[2]) on peripheral F, TCC2/WO[0] on peripheral E
D9 - PA17 - TCC0/WO[7] (same channel as TCC0/WO[3]) on peripheral F, TCC2/WO[1] on peripheral E
D10 - PA19 - TCCO/WO[3] (same channel as TCC0/WO[7])
*D11 - PA08 - TCC1/WO[2] (same channel as TCC1/WO[0]) on peripheral F, TCC0/WO[0] on peripheral E
D12 - PA09 - TCC1/WO[3] (same channel as TCC1/WO[1]) on peripheral F, TCC0/WO[1] on peripheral E
D13 - PB22 - None
D14 - PB23 - None

Note the timer TCC0 has only 4 channels (0-3 and 4-7 are the same),
while TCC1 and TCC2 each have 2, giving you 8 channels in total.

*/
#include <Arduino.h>
#include <Adafruit_SH110X.h>

void setupTimers();
void setup()
{
    setupTimers();
}

void loop()
{
    for (uint16_t i = 0; i < 256; i++)
    {
        REG_TCC1_CC1 = i;
        delayMicroseconds(1);
        while (TCC1->SYNCBUSY.bit.CC1)
            ;
    }

    for (uint16_t i = 0; i < 256; i++)
    {
        REG_TCC1_CC1 = 255 - i;
        delayMicroseconds(10);
        while (TCC1->SYNCBUSY.bit.CC1)
            ;
    }
}

// Output PWM 24Khz on digital pin D3  and D11 using timer TCC1 (10-bit resolution)
void setupTimers()
{
    REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) | // Divide the 48MHz clock source by divisor N=1: 48MHz/1=48MHz
                      GCLK_GENDIV_ID(4);   // Select Generic Clock (GCLK) 4
    while (GCLK->STATUS.bit.SYNCBUSY)
        ; // Wait for synchronization

    REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |         // Set the duty cycle to 50/50 HIGH/LOW
                       GCLK_GENCTRL_GENEN |       // Enable GCLK4
                       GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source
                       GCLK_GENCTRL_ID(4);        // Select GCLK4
    while (GCLK->STATUS.bit.SYNCBUSY)
        ; // Wait for synchronization

    // Enable the port multiplexer for the digital pin D3 and D11  **** g_APinDescription() converts Arduino Pin to SAMD21 pin
    PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;
    // PORT->Group[g_APinDescription[11].ulPort].PINCFG[g_APinDescription[11].ulPin].bit.PMUXEN = 1;

    // Connect the TCC1 timer to digital output D3 and D11 - port pins are paired odd PMUO and even PMUXE
    // F & E specify the timers: TCC0, TCC1 and TCC2
    PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg = PORT_PMUX_PMUXO_E; // D3 is on PA11 = odd, use Device E on TCC1/WO[1]
    // PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg = PORT_PMUX_PMUXE_F; // D11 is on PA08 = even, use device F on TCC1/WO[0]

    // Feed GCLK4 to TCC0 and TCC1
    REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |       // Enable GCLK4 to TCC0 and TCC1
                       GCLK_CLKCTRL_GEN_GCLK4 |   // Select GCLK4
                       GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed GCLK4 to TCC0 and TCC1
    while (GCLK->STATUS.bit.SYNCBUSY)
        ; // Wait for synchronization

    // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
    REG_TCC1_WAVE |= TCC_WAVE_POL(0xF) |      // Reverse the output polarity on all TCC0 outputs
                     TCC_WAVE_WAVEGEN_DSBOTH; // Setup dual slope PWM on TCC0
    while (TCC1->SYNCBUSY.bit.WAVE)
        ; // Wait for synchronization

    // Each timer counts up to a maximum or TOP value set by the PER register,
    // this determines the frequency of the PWM operation: Freq = 48Mhz/(2*N*PER)
    REG_TCC1_PER = 256; // Set the FreqTcc of the PWM on TCC1 to 24Khz
    while (TCC1->SYNCBUSY.bit.PER)
        ; // Wait for synchronization

    // Set the PWM signal to output , PWM ds = 2*N(TOP-CCx)/Freqtcc => PWM=0 => CCx=PER, PWM=50% => CCx = PER/2
    // REG_TCC1_CC1 = 128; // TCC1 CC1 - on D3  50%
    // while (TCC1->SYNCBUSY.bit.CC1) ; // Wait for synchronization
    // REG_TCC1_CC0 = 500; // TCC1 CC0 - on D11 50%
    // while (TCC1->SYNCBUSY.bit.CC0) ; // Wait for synchronization

    // Divide the GCLOCK signal by 1 giving  in this case 48MHz (20.83ns) TCC1 timer tick and enable the outputs
    REG_TCC1_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 | // Divide GCLK4 by 1
                      TCC_CTRLA_ENABLE;          // Enable the TCC0 output
    while (TCC1->SYNCBUSY.bit.ENABLE)
        ; // Wait for synchronization
}

BUT ! I want to vary the duty cycle in an interrupt rather than in the loop.
On the arduino uno, I was using
TIMSK1 |= (1 << OCIE1A); // Enable output compare match interrupt on OCR1A for enabling ISR(TIMER1_COMPA_vect) and change the OCR1A limit for toggling the square wave dutycycle.

But I have searched in the XIAO datasheet, I can't see the bit that needed to change to activate a comp or ovf interrupt.

The Seeeduino XIAO is a SAMD21 based board which is the same as for the MKR-boards
so the same methods for interrupts apply

using interrupts with Seeeduino XIAO
https://sigmdel.ca/michel/ha/xiao/seeeduino_xiao_01_en.html#interrupts

Seeeduino XIAO setup pwm

best regards Stefan

Thanks u for ur anwswer.

The first link talk about external interrupt. The thing is I want an interruption fired a 8Khz frequency without the need of external actions.

The second link is what I need to put in the timer interruption, thanks !

Hi @sercurio

Are you looking to change the PWM duty-cycle in the interrupt service routine to ensure that no glitches appear on your output signal, or for another reason, for example to do a small amount of additional processing?

The reason I ask, is because if it's just the former then it's possible to use the SAMD21's buffered CCBx registers. The buffered register only update the duty-cycle at the beginning of the timer cycle and thereby prevent changes in duty-cycle from appearing immediately at the output. Otherwise, you'll require an interrupt service routine.

Hey @MartinL,
In fact, I'm using the PWM for audio things, so what's ur talking about interest me for avoiding audio glitches.
But I also want to change the dutycycle at a fixed rate of 8Khz, so I think I should use an interrupt service routine also.

Hi @sercurio

Here's some example code that outputs PWM at 8kHz on the Xiao's digital D3 pin. It includes the use of the buffered CCB registers, as well as calling the TCC1's interrrupt service routine TCC1_Handler() function:

// Output 8kHz single slope PWM on Seeeduino Xiao's digital pin D3 (PA11) with interrupts
void setup()
{
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0 as a clock source
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Route GCLK0 to timers TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  PORT->Group[PORTA].PINCFG[11].bit.PMUXEN = 1;    // Enable the port multiplexer for pin D3 on port pin PA11
  PORT->Group[PORTA].PMUX[11 >> 1].reg |= PORT_PMUX_PMUXO_E;   // D3 is on ODD port pin PA11 and TCC1/WO[1] channel 1 is on peripheral E
  
  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Setup single slope PWM on TCC1
  while (TCC1->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  // Unnecessary if prescaler set to 1
  //TCC1->CTRLA.reg = TC_CTRLA_PRESCALER_DIV1 |      // Set prescaler to 1, 48MHz/1 = 48MHz
  //                  TC_CTRLA_PRESCSYNC_PRESC;      // Set the reset/reload to trigger on prescaler clock
  
  TCC1->PER.reg = 5999;                            // Set the frequency of the PWM on TCC1 to 8kHz: 48MHz / (1 * 5999 + 1) = 8kHz
  while (TCC1->SYNCBUSY.bit.PER);                  // Wait for synchronization
  
  TCC1->CC[1].reg = 3000;                          // TCC1 CC1 - 50% duty cycle
  while (TCC1->SYNCBUSY.bit.CC0);                  // Wait for synchronization
 
  NVIC_SetPriority(TCC1_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
  NVIC_EnableIRQ(TCC1_IRQn);         // Connect TCC1 to Nested Vector Interrupt Controller (NVIC)

  TCC1->INTENSET.reg = TCC_INTENSET_OVF |          // Activate overflow (OVF) interrupts
                       TCC_INTENSET_MC1;           // Activate match compare channel 1 (MC1) interrupts
  
  TCC1->CTRLA.bit.ENABLE = 1;                      // Enable the TCC1 counter
  while (TCC1->SYNCBUSY.bit.ENABLE);               // Wait for synchronization
}

void loop() {}

void TCC1_Handler()                                // Timer TCC1 interrupt service routine
{                   
  static bool toggle = false;                      // Toggle switch
  
  if (TCC1->INTFLAG.bit.OVF)                       // Test if an OVF interrupt has occured
  {
    TCC1->INTFLAG.bit.OVF = 1;                     // Clear the OVF interrupt flag
    // Add your overflow handler code here...
    // For example...
    TCC1->CCB[1].reg = toggle ? 4500 : 1500;       // TCC1 CCB1: !toggle => 25% duty-cycle / toggle => 75% duty-cycle
    while (TCC1->SYNCBUSY.bit.CCB1);               // Wait for synchronization
    toggle = !toggle;                              // Flip the toggle
  }
  if (TCC1->INTFLAG.bit.MC1)                       // Test if an MC1 interrupt has occured
  {
    TCC1->INTFLAG.bit.MC0 = 1;                     // Clear the MC1 interrupt flag
    // Add your overflow handler code here...          
  }
}
1 Like

Thank u so much mate !

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.