Go Down

Topic: PWM generation for a very low duty cycle and phase shifting capabiliy on SAMD21 (Read 12327 times) previous topic - next topic

ronin101

Hello forum,
long time reader first writer as they say ;-)

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.

MartinL

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.

ronin101

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.

MartinL

Quote
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?

ronin101

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.

Quote
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.

MartinL

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?

MartinL

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):



Here's the code:

Code: [Select]
// 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() {}

MartinL

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

MartinL

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

ronin101

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. :D

ronin101

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:

Code: [Select]
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:

Code: [Select]
// 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.

ronin101

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 ..  :smiley-sad-blue:

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 ?



MartinL

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:



In the example code (above) I changed:

TCC1 CC0 = 12000
TC4 CC0 = 173999

ronin101

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.

MartinL

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?

Go Up