Go Down

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

ronin101

:smiley-eek-blue:  :smiley-roll-blue:  :smiley-roll-blue:

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.

ronin101

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:

https://pastebin.com/fSdSyDUu

ronin101

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.

ronin101

I GOT IT WORKING !!  :'(  :'(  :D  :D

Several little mistakes but the major issue was the use of a prescaler on TCC0
Code: [Select]
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:
Code: [Select]
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:
Code: [Select]
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:

https://pastebin.com/AQhdJQMF

MartinL

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:

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


ronin101

Hi Martin,

I did'nt have time to test all this during this week yet but I'll look into it.

Very interesting insight about the use of the prescaler vs the generic clock,
it could have cause me a lot of time figuring out the potential side effects of
this technical choice.

WonS

Hello Martin,

I must thank you first for your tremendous contribution to the popularization of arduino programming by helping many including me for the register-level coding which could be daunting many times.  I learned a large part of how to code bare-metal from reading your posts.   

As I need some advice on TCC PWM pulse outputs, I thought this thread is relevant. After trying many different attempts without success, I finally post a question! 
I read most of the relevant topics in the forum and I tried to apply them to my application using TCC ISR which worked until I needed some more features.

As in the attached image, there is an external pulse-inputs (from TCC1) that triggers synchronized output pulses with configurable widths.  There are 4 output pulses generated per external spike in ONE-SHOT mode, only 2 pulses of interest shown in the image.

The goal is to generate alternating pulses that differ in timing (or phase) in one of the two outputs (WO[4] in this case). In other words, the two diferent pulses profiles should repeat every other cycle.   My code can runs fine if there is no alternative requirement.    As turned out, this was not straight forward.

I attempted to change the count direction in TCC0_Handler with 2-stage state machine, at each state change the count direction so that single slope PWM but for unknown reason the count direction did not change as desired at every cycle.   Here is one of my attempts:




Code: [Select]


void TCC0_Handler()           // ISR TCC0 overflow callback function
{
  // Check for overflow (OVF) interrupt
  if (TCC0->INTFLAG.bit.OVF)// && TCC0->INTENSET.bit.OVF)
  {
    // NOTE: this ISR update of CCx doesn't work in TCC1_Handler.
    switch (period_state) {
    case 'A':
      period_state = 'B';
     
      TCC1->PERB.reg = tcc1_per ;
      while (TCC1->SYNCBUSY.bit.PERB);
      TCC0->PERB.reg = h2_width<<2;
      while (TCC0->SYNCBUSY.bit.PERB);

      //// switch polarity of wave output. (result not what expected, at least the speed at which the update happens seems to be too slow   
      //TCC0->WAVE.reg |= TCC_WAVE_POL(0x1);      // Single slope PWM operation     
      //while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

      while (TCC0->SYNCBUSY.bit.CTRLB) {
        /* Wait for sync */
      }
      SerialUSB.println("Upcounting");

      if (TCC0->CTRLBSET.bit.DIR) {
        TCC0->CTRLBCLR.reg = TCC_CTRLBCLR_DIR; //
      }   
      //TCC0->CTRLBSET.reg = TCC_CTRLBSET_DIR_Pos;  //  // count down. (not working here)
      else {
        TCC0->CTRLBSET.reg = TCC_CTRLBSET_DIR;
      }

      TCC0->CCB[0].reg = h1_width;
      while (TCC0->SYNCBUSY.bit.CCB0);
      TCC0->CCB[1].reg = h2_width;
      while (TCC0->SYNCBUSY.bit.CCB1);

      TCC0->INTFLAG.bit.OVF = 1;         // Clear the OVF interrupt flag
      break;
    case 'B':
      period_state = 'A';

      TCC1->PERB.reg = tcc1_per;
      while (TCC1->SYNCBUSY.bit.PERB);

      TCC0->PERB.reg = h2_width<<2;
      while (TCC0->SYNCBUSY.bit.PERB);

      //// switch polarity of wave output. ( result not what expected, at least the speed at which the update happens seems to be too slow 
      //TCC0->WAVE.reg |= TCC_WAVE_POL(0x0);// | TCC_WAVE_POL1_Pos;          // Single slope PWM operation     
      //while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization


      while (TCC0->SYNCBUSY.bit.CTRLB) {
        /* Wait for sync */
      }       
      SerialUSB.println("Down counting");
     
      if (TCC0->CTRLBSET.bit.DIR) {
        TCC0->CTRLBCLR.reg = TCC_CTRLBCLR_DIR; //
      }   
      //TCC0->CTRLBSET.reg = TCC_CTRLBSET_DIR_Pos;  //  // count down. (not working here)
      else {
        TCC0->CTRLBSET.reg = TCC_CTRLBSET_DIR;
      }
 
      TCC0->CCB[0].reg = h1_width;
      while (TCC0->SYNCBUSY.bit.CCB0);
      TCC0->CCB[1].reg = h2_width;
      while (TCC0->SYNCBUSY.bit.CCB1);
      break;
    default:
      SerialUSB.println("Default");
    }
    TCC0->INTFLAG.bit.OVF = 1;         // Clear the OVF interrupt flag
  }
}





In setup, I used attachIntterupt for external trigger:


Code: [Select]


pinMode(PIN_PUSLEIN, INPUT_PULLUP); // C2_PULSEIN pin (3) 
attachInterrupt(PIN_PUSLEIN, interrupt_pulsegen_pulsein, RISING);



And the following is what the attachInterrupt calls...


Code: [Select]


// One shot pulse triggered by external spike
void interrupt_pulsegen_pulsein()
{

  if (millis() - ignited_time <= tcc1_duration) {

    if (TCC0->STATUS.bit.STOP)                                 // Check if the previous pulse is complete
    {
      // count restarts...
      TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;         // Retrigger the timer's One/Multi-Shot pulse
      while (TCC0->SYNCBUSY.bit.CTRLB);                        // Wait for synchronization
    }

    if (TCC2->STATUS.bit.STOP)                                 // Check if the previous pulse is complete
    {
      TCC2->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;         // Retrigger the timer's One/Multi-Shot pulse
      while (TCC2->SYNCBUSY.bit.CTRLB);                        // Wait for synchronization
    }

  }
}





I tried to only use TCC0_Handler to change the count direction but maybe I am fundamentally doing it wrong?
As I was refraining from using the "event systems" because I wasn't sure if I am overcomplicating the task. However, after reading your prev posts,  I learned that using event systems for generating interruptions from the same ISR may free-up the CPU intervention.  Could it be the case that I must use event systems to toggle count direction at each cycle? 

I would really appreciate if you could help me to achieve the desired output pulse shape by upgrading my code.

Thanks a lot.

Eric

MartinL

Hi Eric,

It very much depends on the exact timing of your PWM pulses, including duty-cycle (phase), period (frequency) and resolution.

One possible technique is to decrease the timer's period into smaller time slices and reconstruct the pulses with a predetermined sequence of duty-cycles. This can be achieved using the SAMD21's DMAC, or if the PWM frequency is low, using the timer's interrupt service routine.

Another option, if the lag on output 1 isn't too large, is to use dead-time insertion, but this can only insert a small number of GCLK cycles to delay the pulse.

If you need to generate 4 pulses then it's probably easier to use the timer in continous PWM mode and switch the duty-cycle to 0% at the end of the sequence, rather than using oneshot mode.


WonS

Thanks for your advices Martin. I have been testing your ideas since, especially the first one with small time sliced periods with ISR.   Basically trying to segment a cycle into 3 slices (state machine),  first two slices for pulses and the third for long silence period. However, there are some problems that I face as shown in the oscilloscope which leaves me with not much idea.  :o




Zoomed out in time scale:

 

My fear at this point is that this may not be a small grammatical mistake but a design problem in more higher level.  If I don't have to alternate the pulse sequence like in the pulse diagram the simple single slope PWM seems to work.   Would you be kind to take a look?   To run, the only wire need to be connected is between the PIN_C2_PULSEOUT (PA07) and PIN_PULSEIN (PA02), which is the path for trigger pulses.  I need to have the output pulse per incoming short pulse as a trigger.

My application requires low frequency of <400Hz (period=2.5ms), duty cycle  of 5 ~ 20% and no stringent requirement on the resolution.    I am wondering if the interrupt service routine with TCC0_handler is not adequate for this and indeed needs to use DMA?  Any help really appreciated.

Eric



MartinL

Hi Eric,

I've managed to produce the waveform you require.

The following code sets up port pin PA19 as an input that routes pulses through to the event system, (a 12-channel peripheral-to-peripheral highway), via the External Interrupt Controller (EIC). After a 2us progagation delay, these pulses (or events) are received by timers TCC0 and TCC1. The fact they these timers are triggered by the same event effectively synchronizes them.

Upon receiving an event both timers retrigger, TCC0 generating a 2500us pulse on channel 1 and an alternating 1250us and 2500us pulse on channel 0. The alternating pulse widths is achieved by enabling circular CC0/CCB0 buffering in the TCC0's WAVE register.

Timer TCC1 meanwhile calls its interrupt service routine. Once at the beginning of a cycle, to alternately switch on and off pattern generation and once at the end of the pulse, to turn pattern generation off.

When the pulse on channel 0 is 1250us, pattern generation is switched off for the entire cycle and the pulse is output as normal. However, when the pulse on channel 0 is 2500us, pattern generation is switched on for the first 1250us, forcing the output low. After 1250us, pattern generation is switched off allowing the pulse to return high again for its remaining 1250us.

A 5us pulse interval is generated from timer TCC2 on port pin PA16. Connecting PA16 to PA19 feeds the pulses through to timers TCC0 and TCC1.

Here's the resulting output, 5us interval pulses (yellow), alternate phase shifted pulse on TCC0 channel 0 (light blue) and standard pulse on TCC0 channel 1 (purple):



Please find the code attached:

WonS

Hi Martin, thank you very much for your kind help in tackling this task. :)  The wave screenshot looks awesome. I will study the code tonight and seeks to apply it to my project and come back if I have another question. Thanks! 

WonS

Hi Martin, 

Your solution enlightens me to a new world of pattern generator. 
Very elegant way! I would never knew how to use it by myself reading the data sheet. Thank you for letting me study this.

Some follow up question...
1) I can't wrap my head around how the PATT.bit.PGE4 in TCC1_Handler is able to generate the alternating pattern.    When it is set to 0 at MC0  (@ count=1250), the TCC0 ch1 is zeroed,  but I don't get what the PATT.bit.PGE4=1 or 0 at and at count=4000 (@OVF) does.  Do you mind to explain a bit more?  By reading the data sheet the use of pattern generator it is very unclear...

2) When I disconnect the EIC pin, TCC0 still outputs some patterns even though it is supposed to be a one-shot mode, meaning one pulse input maps to one pulse output per channel. Is there a way to correct this?

3) Just conceptually, what would be the advantage of using DMA here?  I am in my early stage in understanding DMAC and wondering if in the future it might be beneficial to use it. For example, in addition to my current setting , I plan to have TCC0 or TCC2 to trigger three ADC reading per cycle and would like to hear your perspective.  THanks!


Eric



 

MartinL

Hi Eric

1)In the PATT register, setting the pattern generator's bit PGE4 (Pattern Generator Enable Channel 4) simply overrides the channels output, setting the output high or low depending on the value defined by the bit PGV4 (Pattern Generator Output Value Channel 4).

The TCC0 alternates cycles on channel 0, both aligned to the rising edges of the input signal. On one cycle TCC0 generates standard a 1250us pulse, on the other it generates a 2500us pulse, but the TCC1 timer actives the TCC0's pattern generator to set the first 1250us of the pulse low. This gives the output of a 1250us pulse with a phase delay.

2) I'm not seeing this behaviour on my board. If I disconnect the PA19 input both PA22 and PA23 outputs go low.

There is an issue that if the input pulses are stopped, the TCC1's ISR and the TCC0's circular buffer can get out of sync with one another.

3) Unfortunately, in this instance the DAMC won't work, as it doesn't have the same flexibility as an interrupt service routine.

The problem is that the PGE4 bit in the PATT register needs to be set every other cycle by the TCC1 overflow (OVF) and cleared by the match compare 0 (MC0). This requires two DMAC channels, one to trigger on TCC1 overflow and the other on match compare 0, however the two channels cannot simultaneously share the single destination PATT register.

The TCCx timers can generate events on the event system, to trigger the ADC. The DMAC can used to automatically copy the results from the ADC into memory.

WonS

Thank you so much for your explanation Martin.  I am indebted to your teaching.  Have a good day.

Go Up