PWM generation for a very low duty cycle and phase shifting capabiliy on SAMD21

Hello forum,
long time reader first writer as they say :wink:

I need to generate a very low duty cycle PWM ( like between 1 and 5 % ) on a Samd21 M0 and be able to phase shift it dynamically

I've tried every type of PWM generation using timer counters. I though the dual-slope critical pulse width modulation was the solution but after tinkering with it I realized that since both CCx control must be on each side of the TOP of the counter, I can't move a very thin voltage peak to the side, it only works with 50% duty cycles.

For example on the figure below, I can't set CC0 to 1 and CC2 to 2 and have a signal peak between le orange 1 and the orange 2, the peak will be between orange 1 and red 2. So I can't shift the signal peak to the left nor the right of the center.

I'm pretty new at direct register manipulation but I have read the PWM part of the Samd21 extensively as well as this forum and I'm now stuck. I've read about circular buffer, dead time and RAMP alternatives but I don't understand those features enough to know if this the right direction.

Any info to put me in the right direction would be appreciated.

Hi ronin101,

Unfortunately, there's no straightforward way to achieve this. The solution depends upon your application, but changing the waveform's phase requires changing it's period, using either the SAMD21's period PER or buffered period PERB registers. The buffered PERB register only takes effect on the next timer update, to prevent glitches from appearing on the waveform. Changes to the PER register take effect immediately.

Separation between the pulses can be achieved by adjusting the period in the timer's overflow (OVF) interrupt service routine. Alternatively, the RAMP2 functionality can be employed to interleave two PWM channels, one to generate the pulse the other the distance between them. If you're using a repeating waveform pattern then it's possible to use the Direct Memory Access Controller (DMAC) with a circular descriptor in conjuction with the timer. Examples of RAMP2 and DMAC operation are described here: https://forum.arduino.cc/index.php?topic=659141.0.

Thank you for your answer MartinL,

My application need is to synchronize from an input signal and be able to change
the phase between this input and my generated signal.

I'm already able to smoothly change the period using PERB so that my output signal
match the input signal period. Do you suggest changing the phase by updating
the period for only one loop, then returning to the input signal period ? This would
induce positive or negative delay from the input signal and induce shifting.

I didn't know the OVF interrupt feature, I'll check what it can do in the documentation.

Also thank you for confirming that RAMP2 may be a solution, I didn't want to start studying it
without any confirmation that it may be useful because it seems a bit daunting. I'll check the link
you provided.

My application need is to synchronize from an input signal and be able to change
the phase between this input and my generated signal.

Do the pulse widths of you input and generated signal need to match exactly?

Is the generated output waveform just a delayed version of the input?

Between input and output signals, the frequency must match but the pulse widths are totaly unrelated.
I must be able to set the width needed on the output but the input width is fixed and does not change.

Is the generated output waveform just a delayed version of the input?

Only in the sense that they have the same frequency and they are both basic PWM signals.
The output pulse width and phase have to be dynamicaly settable

I could however reingeneer my project for the output to be a delayed ( postivily or negativily )
version of the input but with a different pulse width. So if you have an idea in mind about that
I would be glad to hear it.

Hi ronin101,

It's possible to use the SAMD21's External Interrupt Controller (EIC) and Event System to route the input pulses to an internal TCC/TC timer. The edge of the pulses can be used to trigger the timer. This timer can then measure the required delay and in turn trigger another second timer on the Event System to output a PWM pulse.

The event system is just a 12-channel peripheral to peripheral highway that allows them to communicate with each other without CPU intervention.

Would this idea meet your project's requirements?

Hi ronin101,

Here's an example of using the External Interrupt Controller (EIC), Event System and TC/TCC timers to generate a delay on the output.

The code uses a PWM test output of a 1ms pulse repeated every 4ms, or in other words a 4ms (250Hz) period with a 25% duty-cycle. This signal is generated on digital pin D8 with timer TCC1.

Connecting D8 to the input on digital pin D12, routes this signal through the EIC and on to timer TC4 (working in conjuction with TC5 in 32-bit mode) via the event system.

This signal retriggers TC4 from zero and sets it counting at 48MHz. When this timer reaches the value in it's Counter Compare CC0 register, TC4 also generates an event that's picked up by timer TCC0. Upon receiving TC4's event, TCC0 retriggers and outputs a single oneshot 1ms pulse on digital pin D3.

The delay is set by value of TC4's CC0 register, where:

Delay = CC0 / 48000000

Note that the timer peripherals don't require any CPU intervention during operation. The loop() function is empty.

Here the scope displays the input pulses (yellow) and the delayed output by 1ms (light blue):

Pulse Delay.png

Here's the code:

// Setup timer TC4/TC5 to trigger on input event and set time delay for TCC0 output pulse, (TCC1 test input)
// Test output: D8, Input: D12, PWM Output: D3
void setup()
{
  // Generic Clocks (GCLK) //////////////////////////////////////////////////////////////////////////////
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |        // Enable generic clock
                      GCLK_CLKCTRL_GEN_GCLK0 |    // Select the 48MHz GCLK0
                      GCLK_CLKCTRL_ID_TCC0_TCC1;  // Set GCLK0 as a clock source for TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |        // Enable generic clock
                      GCLK_CLKCTRL_GEN_GCLK0 |    // Select the 48MHz GCLK0
                      GCLK_CLKCTRL_ID_TC4_TC5;    // Set GCLK0 as a clock source for TC4 and TC5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Port Multiplexing //////////////////////////////////////////////////////////////////////////////////
  
  // Enable the port multiplexer D3, D8 and D12
  PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[8].ulPort].PINCFG[g_APinDescription[8].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;
  
  // Set-up the pin multiplexers
  PORT->Group[g_APinDescription[3].ulPort].PMUX[g_APinDescription[3].ulPin >> 1].reg |= PORT_PMUX_PMUXO_E;
  PORT->Group[g_APinDescription[8].ulPort].PMUX[g_APinDescription[8].ulPin >> 1].reg |= PORT_PMUX_PMUXE_E;
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXO_A;

  // External Interrupt Controller (EIC) (Input) ///////////////////////////////////////////////////////////
  
  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO3;         // Enable event output on external interrupt 3
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE3_HIGH;    // Set event detecting a HIGH level
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT3;        // Clear the interrupt flag on channel 3
  EIC->CTRL.reg |= EIC_CTRL_ENABLE;                // Enable EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                // Wait for synchronization

  // Event System /////////////////////////////////////////////////////////////////////////////////////////
  
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TC4_EVU);                // Set the event user (receiver) as timer TC4 event
  
  EVSYS->USER.reg = EVSYS_USER_CHANNEL(2) |                                // Attach the event user (receiver) to channel 1 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TCC0_EV_0);              // Set the event user (receiver) as timer TCC0, event 1

  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |                // No event edge detection
                       EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                   // Set event path as asynchronous
                       EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_3) |    // Set event generator (sender) as external interrupt 3
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0
  
  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |                // No event edge detection
                       EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                   // Set event path as asynchronous
                       EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_TC4_MCX_0) |       // Set event generator (sender) as TC4 Match Compare channel 0
                       EVSYS_CHANNEL_CHANNEL(1);                           // Attach the generator (sender) to channel 1

  TC4->COUNT32.EVCTRL.reg |= TC_EVCTRL_MCEO0 |               // Output event on Match Compare channel 0
                             TC_EVCTRL_TCEI |                // Enable the TC event input
                             //TC_EVCTRL_TCINV |             // Invert the event input 
                             TC_EVCTRL_EVACT_RETRIGGER;      // Set event to RETRIGGER timer TC4
  
  TCC0->EVCTRL.reg |= //TCC_EVCTRL_TCEI1 |                     // Enable the TCC event 1 input
                      TCC_EVCTRL_TCEI0 |                     // Enable the TCC event 0 input
                      //TCC_EVCTRL_TCINV1 |                    // Invert the event 1 input
                      //TCC_EVCTRL_TCINV0 |                   // Invert the event 0 input                     
                      TCC_EVCTRL_EVACT0_RETRIGGER;           // Set event 0 to count the incoming events

  // Timer TCC1 (Test Output at 250Hz, 25% duty-cycle) /////////////////////////////////////////////////////////

  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;             // Set the TCC1 timer counter to normal PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE);                    // Wait for synchronization

  TCC1->CC[0].reg = 48000;                            // Set duty cycle 25% with 1ms pulse
  while (TCC1->SYNCBUSY.bit.CC0);                     // Wait for synchronization

  TCC1->PER.reg = 191999;                             // Set period to 4ms
  while (TCC1->SYNCBUSY.bit.PER);                     // Wait for synchronization

  TCC1->CTRLA.bit.ENABLE = 1;                         // Enable TCC1
  while (TCC1->SYNCBUSY.bit.ENABLE);                  // Wait for synchronization

  // Timer TC4/TC5 (Delay Timer) /////////////////////////////////////////////////////////////////////////////

  TC4->COUNT32.CTRLA.reg |= TC_CTRLA_WAVEGEN_NFRQ |   // Set TC4 to normal frequency mode (NFRQ)
                            TC_CTRLA_MODE_COUNT32;    // Enable 32-bit timer mode (in conjuction with TC5)

  TC4->COUNT32.CC[0].reg = 47999;                     // Set the delay to 1ms
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);           // Wait for synchronization
  
  TC4->COUNT32.CTRLBSET.reg = TC_CTRLBSET_ONESHOT;    // Enable oneshot operation
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);           // Wait for synchronization
  
  TC4->COUNT32.CTRLA.bit.ENABLE = 1;                  // Enable TC4
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);           // Wait for synchronization

  // Timer TCC0 (PWM Output) /////////////////////////////////////////////////////////////////////////////////

  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;             // Set the TCC0 timer counter to normal PWM mode (NPWM)
  while (TCC0->SYNCBUSY.bit.WAVE);                    // Wait for synchronization

  TCC0->CC[1].reg = 47999;                            // Set duty cycle 100% with 1ms pulse on channel 1
  while (TCC0->SYNCBUSY.bit.CC1);                     // Wait for synchronization

  TCC0->PER.reg = 47999;                              // Set period to 1ms
  while (TCC0->SYNCBUSY.bit.PER);                     // Wait for synchronization

  TCC0->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT;          // Enable oneshot operation
  while (TCC0->SYNCBUSY.bit.CTRLB);                   // Wait for synchronization

  TCC0->DRVCTRL.reg |= TCC_DRVCTRL_NRE1;              // Set the non-recoverable state output to 0 when inactive
                                                                                      
  TCC0->CTRLA.bit.ENABLE = 1;                         // Enable TCC0
  while (TCC0->SYNCBUSY.bit.ENABLE);                  // Wait for synchronization
}

void loop() {}

Pulse Delay.png

Propagation delay using this method (with TC4's CC0 register set to 0) is around 190ns.

The propagation delay of 190ns accounts for 9 processor clock cycles at 20.83ns (1/48MHz).

Looking at the SAMD21 datasheet the interrupt using level detection without filtering takes 3 clock cycles. I can only assume that asynchronous retriggering the TC and TCC0 timers on the event system also takes 3 clock cycles each.

To account for the propagation delay:

Delay = (CC0 + 9) / 48000000

Wow ! Thanks A LOT for this very detailled explanation and example. Let me test it and adapt it to my project and I'll get but to you to confirm this works for my needs. This is very promising. :smiley:

Just to update on my progress on the subject, I’ve managed to do what I wanted via a workaround using
the one shot mode of the output PWM.

I enable the oneshot mode with:

REG_TCC0_CTRLBSET |= TC_CTRLBSET_ONESHOT;
        while(TCC0->SYNCBUSY.bit.CTRLB);

On the other hand I have a frequency timer counter that measures the period of the input signal. Each time the frequency counter detects the end of the period I restart the output PWM manually like this:

// Check if the previous pulse is complete
if (TCC0->STATUS.bit.STOP)                                    
          {
              LOGD("retrigger for period: %d tic %d ms\n", 
FreqMeasurementTimer::periodInTic, tic2us(FreqMeasurementTimer::periodInTic)/1000l);
// Retrigger the timer's One/Multi-Shot pulse
              TCC0->CTRLBSET.reg |= TCC_CTRLBSET_CMD_RETRIGGER;
 // Wait for synchronization
              while (TCC0->SYNCBUSY.bit.CTRLB);                        
          }

By using the one shot mode I no longer need to match the period of the output signal on the period of the input signal because technically a one shot pulse has no period. Si I just adjust PER and CC0 to get a pulse where I want in relation to the input pulse. If PER and CC0 are the same this gives a pulse with no phase, on each input pulse. If PER > CC0 I get positive phase.

I’ve attached a screenshot of the result.

I still have problem when using a negative phase and a pulse duration that overlaps the input pulse.

Anyway; I’m well aware of Martin’s solution being well superior has it uses internal event system whereas my solution is a hack mixing timer counters and C code. But maybe it can help someone else for a related but slightly different problem.

Hello Martin, now that I have and alpha version more or less working with my hack explained in the previous post I’m studying your solution in detail. Unfortunalty I didn’t notice you used the one shot mode too … :disappointed_relieved:

So I guess, despite being cleaner and more optimised your solution does not allow to have a negative phase and an overlap of the pulses either ?

On this image the yellow signal is the input, the pink is the output.
Is this situation possible ? Where pink pulses have negative phases but overlap
the whole yellow peak duration ?

Hi ronin101,

Although the rising edge of timer TCC0's output pulse (light blue) must start within the input signal's (yellow) period, the output pulse itself is able to continue beyond and into the input signal's next period:

Pulse Delay2.png

In the example code (above) I changed:

TCC1 CC0 = 12000
TC4 CC0 = 173999

Pulse Delay2.png

Brilliant ! Thank you.
I'was not able to successfuly run your example yet. I only get the input signal but I don't see
any output signal. Maybe again a difference between the zero and the m0 pro though I noticed
you didn't use D2 or D4. I'm working in on it.

Other than the fact that D2 and D4 are swapped and that the M0 Pro lacks the additional ATN pin, hardware-wise the M0 Pro and Arduino Zero should be identical.

Have you plugged the test output signal on D8 into input D12?

:fearful: :roll_eyes: :roll_eyes:

Silly me !!!! I though this was done by code via the Event Sytem which I did not understand.
Well at least it forced me to study the code in depth to understand what was happening.

Ahhh !!. Register coding is soooo frustrating. I tried to mix the code Martin posted with some code I had that measure the input signal frequency and duty cycle.

The original PWM code with phasing control was working this way:

TCC1 -> output D8 ---> input D12 -> int3 -> TC4 ---> delay ----> TCCO ---> output D2
( originaly it was D3 but I changed it to D2 for my project need)

I repurposed TC4 as my frequency counter timer, so I used TC5 as the new delay counter
( note that I don't need 32bits accuracy so I can use both of them in 16 bits)
My original idea was to retrigger TC5 manually in the TC4_Handler() when each period is done.
It dit not work ...

I also tried to channel int3 both to TC4 and TC5 so TC5 will trigger directly via the Event System.
It did not work either ...

I tried a LOT of combinations, it almost worked but never exactly has intended.

I'm posting the code that I have, in case somebody is courageous enough to look at it. This is really refrustrating as I feel I'm very close but never got the exact right parameters to make it work. Unless I'm missing something important and what I want to do is just not possible ...

Code is too long for the forum so I post it here:

I'm starting to think I used too low values for PER and CC0 which makes my code fail in limit cases for the microprocessor. I used a 20 us tic and PER/CC0 value of 1/1 so it was easy to check on the oscilloscope but starting again from scratch I'm noticing better results with bigger values.
I suspect the values I chose may have been too short for the event system.

I GOT IT WORKING !! :cry: :cry: :smiley: :smiley:

Several little mistakes but the major issue was the use of a prescaler on TCC0

REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV8 | TCC_CTRLA_ENABLE;

I have absolutely no idea why this causes the PWM to fail but if I don't use any prescaler
and compensate for the lack of it by multiplying the number of tics needed it works.

This works:

TCC0->CC[0].reg = 8;                  
while (TCC0->SYNCBUSY.bit.CC0);

TCC0->PER.reg = 8;                     
while (TCC0->SYNCBUSY.bit.PER);
                                        
TCC0->CTRLA.bit.ENABLE = 1;
while (TCC0->SYNCBUSY.bit.ENABLE);

This does not:

TCC0->CC[0].reg = 1;                  
while (TCC0->SYNCBUSY.bit.CC0);

TCC0->PER.reg = 1;                     
while (TCC0->SYNCBUSY.bit.PER);
                                      
REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV8 |
                  TCC_CTRLA_ENABLE; 
while (TCC1->SYNCBUSY.bit.ENABLE);

Here is the final working code:

Hi ronin101,

Glad to hear that you got it working.

The issue with the prescaler might be to do with the “Prescaler and Counter Synchronization” or PRESCSYNC bitfield in the TCC and TC timers’ CTRLA registers. This configures the whether the next wraparound should reset or retrigger the timer: on the next generic clock (GCLK), prescaler clock (PRESC), or the next generic clock while resetting the prescaler (RESYNC).

In your case, it might be worth setting the PRESCSYNC bitfield to RESYNC, to reset the prescaler when the timers are retriggered, for example:

TC4->COUNT32.CTRLA.reg = TC_CTRLA_PRESCALER_DIV16 |     // Set prescaler to 16
                         TC_CTRLA_PRESCSYNC_RESYNC |    // Set to retrigger timer on GCLK with prescaler reset
                         TC_CTRLA_MODE_COUNT32;         // Set the counter to 32-bit mode

Also, I noticed in your code that you’re dividing down generic clock 5 (GCLK5 by 120), in order to generate 400kHz. While this will clock the timers at the desired frequency, it will also clock the timers’ registers at the same rate, leading to large synchronization delays for every register access.

Register synchronization delay is given by the forumla:

5 * Period(GCLK) + 2 * Period(APB) < D < 6 * Period(GCLK) + 3 * Period(APB)

where:
Period(GCLK) = period of the Generic Clock
Period(APB) = period of the Advanced Peripheral Bus (APB) at 48MHz
D = synchronization delay

Therefore with the GCLK set to 400kHz, the worst case synchronization delay is:

D = 6 * 1/400kHz + 3 * 1/48MHz = 15ms (for every synchronized TCC or TC register access)

This is OK if your pulse generation application is the microcontroller’s only task, but might start to cause problems if it’s required to perform other duties as well. For this reason, it’s usually preferable to try use timer prescaler (if possible), rather than dividing down the generic clock.