Get counter value before timer reset

As the title mentions, I've connected PA18 to the evsys, the timer will be reset on PA18 falling edge. This all works OK. Part of the code:

 // Enable the port multiplexer on port pin PA18
  PORT->Group[PORTA].PINCFG[18].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) peripheral on PA18
  PORT->Group[PORTA].PMUX[18 >> 1].reg |= PORT_PMUX_PMUXE_A;
 
  //EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO2;                                 // Enable event output on external interrupt 2
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE2_FALL;                            // Set event detecting a FALLING edge
  //EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT2;                              // Disable interrupts on external interrupt 2
  EIC->CTRL.reg |= EIC_CTRL_ENABLE;                                        // Enable EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                                        // Wait for synchronization
 
  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TCC0_EV_0);              // Set the event user (receiver) as timer TCC0, event 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_EIC_EXTINT_2) |    // Set event generator (sender) as external interrupt 2
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

  TCC0->EVCTRL.reg |= TCC_EVCTRL_TCEI0 |                                   // Enable TCC0 event 0 inputs                               
                      TCC_EVCTRL_EVACT0_RETRIGGER;                         // Retrigger timer TCC0 on receiving event

My question is: is it possible to get the timer counter value at the falling edge of PA18 just before the timer resets?

Hi LeCrAm,

As far as I know, the only way to access the TCC0's COUNT register is using interrupts.

In theory you could use the event system in conjuntion with the DMAC (Direct Memory Access Controller), to trigger a DMAC read of the TCC0's COUNT register upon reiceving an input pulse event and then get the DMAC to gerate another event which retriggers/resets the TCC0 timer. This would all happen without CPU intervention. However there's a problem.

The problem is that the COUNT register is not only read synchronized, but also requires a read request by setting the READSYNC bit in the TCC0's CTRLB register. In my experience, it's not possible to get the DMAC to read the COUNT register, or if it is possible, it certainly isn't discussed in the SAMD21 datasheet.

Here's the somewhat convoluted process of reading the TCC0 COUNT register, (while it's running):

TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_READSYNC;  // Trigger a read synchronization on the COUNT register
while (TCC0->SYNCBUSY.bit.CTRLB);                // Wait for the CTRLB register write synchronization
while (TCC0->SYNCBUSY.bit.COUNT);                // Wait for the COUNT register read sychronization
SerialUSB.println(TCC0->COUNT.reg, HEX);         // Print the result

It is however possible to read the pulse with and period of an input pulse using TCC capture mode, with or without the DMAC.

Thanks Martin, I'm aware of the TCC capture mode, but that will not work for me.

Hi LeCrAm,

I've investigated the situation a bit further, it appears possible to get the DMAC to transfer the timer COUNT register to a "count" variable in memory, but only if the a TCC READSYNC command is continually triggered in the loop().

Unfortunately, there's just not enough information in the SAMD21 datasheet to determine the interaction between the TCC0 timer and the DMAC.

Hi LeCrAm,

As I couldn't get the DMAC to play ball with the TCC timer, I changed tack and used the TC timer instead. Now it's working.

The following code outputs a 1kHz test output on PA16, which should be connected to the the input PA18. PA18 passes the signal through the EIC and on to the event system. The event triggers a transfer on the DMAC channel 0, copying the COUNT register to a variable called "count". On completion an event is sent to retrigger the TC3 timer. Also, upon transfer the DMAC channel is suspended and an (optional) interrupt is called setting the "dataReady" flag.

In the loop() function, the "count" variable is output everytime the "dataReady" flag is set. The output value on the console is around 48000 (48MHz / 1kHz) as expected.

Here's the code part I:

// Read the TC3 COUNT register and retrigger (reset) upon receiving a falling edge trigger on PA18
// 1ms (1kHz) test trigger source generated on PA16

volatile uint16_t count = 0;
volatile boolean dataReady = false;

typedef struct                                                                  // DMAC descriptor structure
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint32_t srcaddr;
  uint32_t dstaddr;
  uint32_t descaddr;
} dmacdescriptor ;

volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16)));        // Write-back DMAC descriptors
dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16)));  // DMAC channel descriptors
dmacdescriptor descriptor __attribute__ ((aligned (16)));                       // Place holder descriptor

void setup()
{
  SerialUSB.begin(115200);                         // Activate the Serial port
  while(!SerialUSB);                               // Wait for the console to open
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;           // Switch on the event system peripheral
 
  ////////////////////////////////////////////////////////////////////////////////////////
  // Genric Clock Initialisation 
  ////////////////////////////////////////////////////////////////////////////////////////

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable the generic clock...
                      GCLK_CLKCTRL_GEN_GCLK0 |     // ....on GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed the GCLK0 to TCC2 and TC3
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  ////////////////////////////////////////////////////////////////////////////////////////
  // Direct Memory Access Controller (DMAC) Initialisation 
  ////////////////////////////////////////////////////////////////////////////////////////

  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;                 // Set the descriptor section base address
  DMAC->WRBADDR.reg = (uint32_t)wrb;                                 // Set the write-back descriptor base adddress
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xF);       // Enable the DMAC and priority levels

  DMAC->CHID.reg = DMAC_CHID_ID(0);                                  // Select DMAC channel 0
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |                          // Set the channel priority level to 0 (lowest)
                      DMAC_CHCTRLB_TRIGACT_BEAT |                    // Set the trigger action every beat
                      DMAC_CHCTRLB_EVIE |                            // Enable event inputs
                      DMAC_CHCTRLB_EVOE |                            // Enable event outputs
                      DMAC_CHCTRLB_EVACT_TRIG;                       // Trigger transfer upon receiving an event
  descriptor.descaddr = (uint32_t)&descriptor_section[0];            // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&TC3->COUNT16.COUNT.reg;            // Read the current value in the TC3 COUNT register
  descriptor.dstaddr = (uint32_t)&count;                             // Copy it into "count" variable
  descriptor.btcnt = 1;                                              // Operation takes 1 beat
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                 // Suspend channel after transfer
                      DMAC_BTCTRL_BEATSIZE_HWORD |                    // Set the beat size to HWORD (16-bits)
                      DMAC_BTCTRL_VALID |                            // Flag the descriptor as valid
                      DMAC_BTCTRL_EVOSEL_BEAT;                       // Set an event after every transfer (beat)
  memcpy(&descriptor_section[0], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 0 descriptor

  NVIC_SetPriority(DMAC_IRQn, 0);           // Set the Nested Vector Interrupt Controller (NVIC) priority for the DMAC to 0 (highest) 
  NVIC_EnableIRQ(DMAC_IRQn);                // Connect the DMAC to the Nested Vector Interrupt Controller (NVIC)

  DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable transfer suspend (SUSP) interrupt on channel 0

Here's the code part II:

  ////////////////////////////////////////////////////////////////////////////////////////
  // TCC2 Initialisation - generate pulse every 1ms (1kHz) on port pin PA16
  ////////////////////////////////////////////////////////////////////////////////////////

  // Enable the port multiplexer on port pin PA16
  PORT->Group[PORTA].PINCFG[16].bit.PMUXEN = 1;
  // Set-up the pin as a TCC2/WO[0] peripheral on PA16
  PORT->Group[PORTA].PMUX[16 >> 1].reg |= PORT_PMUX_PMUXE_E;

  TCC2->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;            // Set the TCC2 timer to normal PWM mode (NPWM)
  while(TCC2->SYNCBUSY.bit.WAVE);                    // Wait for synchronization
 
  TCC2->PER.reg = 47999;                             // Set the period (PER) register for a PWM frequency of 1kHz (1ms)
  while(TCC2->SYNCBUSY.bit.PER);                     // Wait for synchronization

  TCC2->CC[0].reg = 24000;                           // Set the counter compare 0 (CC0) register for a PWM duty-cycle of 50%
  while(TCC2->SYNCBUSY.bit.CC0);                     // Wait for synchronization

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

  /////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Event System Initialisation - set up to allow DMAC channel 0 to receive incoming pulses on PA18,
  //                               the DMAC copies the TC3 COUNT register then sends an event to 
  //                               retrigger (reset) the TC3 timer                                     
  /////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  // Enable the port multiplexer on port pin PA18
  PORT->Group[PORTA].PINCFG[18].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) peripheral on PA18
  PORT->Group[PORTA].PMUX[18 >> 1].reg |= PORT_PMUX_PMUXE_A;
  
  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO2;                                 // Enable event output on external interrupt 2
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE2_HIGH;                             // Set event detecting a HIGH level
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT2;                                // Disable interrupts on external interrupt 2
  EIC->CTRL.reg |= EIC_CTRL_ENABLE;                                        // Enable EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                                        // Wait for synchronization
  
  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_2) |    // Set event generator (sender) as EIC, channel 2
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_DMAC_CH_0);              // Set the event user (receiver) as timer DMAC, 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_DMAC_CH_0) |       // Set event generator (sender) as DMAC, channel 0
                       EVSYS_CHANNEL_CHANNEL(1);                           // Attach the generator (sender) to channel 1

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

  TC3->COUNT16.EVCTRL.reg |= TC_EVCTRL_TCEI |                              // Enable TC3 event inputs                               
                             TC_EVCTRL_EVACT_RETRIGGER;                    // Retrigger timer TC3 on receiving event 
                                                                         
  TC3->COUNT16.CTRLA.bit.ENABLE = 1;                                       // Enable TC3
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);                                // Wait for synchronization 

  DMAC->CHID.reg = DMAC_CHID_ID(0);                                        // Select DMAC channel 0
  DMAC->CHCTRLA.bit.ENABLE = 1;                                            // Enable DMAC channel 0
}

void loop()
{  
  if (dataReady)                                     // Wait for the DMAC to flag that a new TCC0 count value is ready
  {  
    SerialUSB.println(count);                        // Display the count value, should output 48000 or thereabouts    
    dataReady = false;                               // Clear the data ready flag
  }
}

void DMAC_Handler()                                  // DMAC Interrupt Service Routine
{
  DMAC->CHID.reg = DMAC_CHID_ID(0);                  // Switch to DMAC channel 0
  if (DMAC->CHINTFLAG.bit.SUSP)                      // If DMAC suspend (SUSP) flag has been set
  {
    DMAC->CHINTFLAG.bit.SUSP = 1;                    // Clear the SUSP interrupt flag
    dataReady = true;                                // Set the data ready flag   
  }    
  DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;      // Resume the DMAC channel          
}

Thanks! Really appreciate your support Martin! I will definitly try this. What is your expetation about additional delay? Because first the TC3 counter needs to be read, then TC3 should be reset. My application is time critical, but I can offer some delay (it's a bit vague, I agree). If you don't know, I will just measure the latency with analyser.

Hi LeCrAm,

There's no (or at least minimal) delay. Upon triggering from an external event, the DMAC automatically copies the TC3's COUNT register into the "count" variable then retriggers the timer by generating its own event. This happens without CPU intervention and should only take a few processor clock cycles.

I just included the DMAC interrupt handler and "dataReady" flag for demonstration purposes and they can be omitted if your application doesn't require them.

Just comment out this line:

DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable transfer suspend (SUSP) interrupt on channel 0

and this DMAC descriptor bitfield:

DMAC_BTCTRL_BLOCKACT_SUSPEND

Hi Martin,

Just tried the code you showed me, I'm using TC4 for this, it works and I'm able to retrieve the counts. However, I need to output a pin until compare match of TC4.

So overal I need to setup a single PWM timer that does:

  • 10KHz freq with fixed duty-cycle (normal PWM)
  • Reset timer on incoming (falling edge) PA18 pulse
  • Get number of counts just before reset by PA18
  • Set PA22 on timer reset, clear on CC (normal PWM)

I will update CC value in timer ISRs. I had everything working OK except for the pulse count by DMA using TCC0 timer.

Hi LeCrAm

Looks like there's a latency of around 300ns, or in other words about 14 to 15 processor clock cycles, in the system. The "count" variable is consistently 14 to 15 less that you'd expect it to be. This remains constant irrespective of the TCC2's input frequency.

I imagine this is due to a double synchronisation with each sychronisation taking between 146ns and 187ns. The first due the DMAC's read of the TC4's COUNT register and the second due to the TC4 retriggering.

Hi Martin,

I've seen this constant latency. What about "to output a pin until compare match of TC4"? Seems the TC4 PWM output are limited compared to the TCC timers?

Hi LeCrAm,

The TC timers are more limited than the TCC timers, however in this instance is simply a case of routing the TC4's output to port pin PA22, setting the timer to Normal PWM mode and loading the CC0 register with the desired output pulse width:

  ////////////////////////////////////////////////////////////////////////////////////////
  // TC4 Initialisation - generate pulse on reset, clear on CC0 on port pin PA22
  ////////////////////////////////////////////////////////////////////////////////////////

  // Enable the port multiplexer on port pin PA22
  PORT->Group[PORTA].PINCFG[22].bit.PMUXEN = 1;
  // Set-up the pin as a TC4/WO[0] peripheral on PA22
  PORT->Group[PORTA].PMUX[22 >> 1].reg |= PORT_PMUX_PMUXE_E;

  TC4->COUNT16.CTRLA.reg = TC_CTRLA_WAVEGEN_NPWM;    // Set the TC4 timer to normal PWM mode (NPWM)

  TC4->COUNT16.CC[0].reg = 1200;                     // Set the TC4 timer to output a 25us pulse

To set the TCC2 to a 10kHz test frequency:

  ////////////////////////////////////////////////////////////////////////////////////////
  // TCC2 Initialisation - generate pulse every 100us (10kHz) on port pin PA16
  ////////////////////////////////////////////////////////////////////////////////////////

  // Enable the port multiplexer on port pin PA16
  PORT->Group[PORTA].PINCFG[16].bit.PMUXEN = 1;
  // Set-up the pin as a TCC2/WO[0] peripheral on PA16
  PORT->Group[PORTA].PMUX[16 >> 1].reg |= PORT_PMUX_PMUXE_E;

  TCC2->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;            // Set the TCC2 timer to normal PWM mode (NPWM)
  while(TCC2->SYNCBUSY.bit.WAVE);                    // Wait for synchronization
 
  TCC2->PER.reg = 4799;                              // Set the period (PER) register for a PWM frequency of 10kHz (100us)
  while(TCC2->SYNCBUSY.bit.PER);                     // Wait for synchronization

  TCC2->CC[0].reg = 2400;                            // Set the counter compare 0 (CC0) register for a PWM duty-cycle of 50%
  while(TCC2->SYNCBUSY.bit.CC0);                     // Wait for synchronization

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

Here's the output I'm getting from TC4 (light blue), triggered on the falling edge of TCC2 (yellow) at 10kHz:

TC4_DMAC_Read.png

Total latency from the falling edge TCC2 to the rising edge of TC4 is around 700ns.

TC4_DMAC_Read.png

In addition, I'm triggering off the falling edge of TCC2, by setting the interrupt detection to a LOW level:

EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE2_LOW;       // Set event upon detecting a LOW level

Thanks Martin, will check asap

"As I couldn't get the DMAC to play ball with the TCC timer, I changed tack and used the TC timer instead. Now it's working."

What is preventing the DMA to work with the TCC0 count register you think?

Hi LeCrAm,

"As I couldn't get the DMAC to play ball with the TCC timer, I changed tack and used the TC timer instead. Now it's working."

What is preventing the DMA to work with the TCC0 count register you think?

Me neither. I think it has something to do with the fact that TCC timer's COUNT register is read synchronized through a CTRLB register request, prior to being read:

TCC0->CTRLBSET.reg = TCC_CTRLBCLR_CMD_READSYNC;
It appears that this requirement prevents the COUNT register from being accessed by the DMAC, something that doesn't affect the TC timers as their read synchronization procedure is different.