tinyAVR Series Part 3: Timer D

About this Post

The third and final part of this article about the tinyAVR series is devoted to the Timer/Counter D, or Timer D for short. This timer is special in many respects. The main difference is that, unlike the Timers A and B, it is only used in the tinyAVR series 1.

As with the other parts of this series of articles, I would like to point out that I cannot go into all the functionalities of Timer D in full depth. I have written the example sketches for an ATtiny1614 and partly for an ATtiny3216. If you use other types, one or the other adaptation may be necessary. However, this should be limited to the assignment of certain functions to certain pins. It is advisable to take a look at the data sheet.

Here is an overview of the content of all the articles in this series:

Attention: difficult to digest article!

The Timer D is quite complex, and many of the aspects I deal with here are interdependent. It is therefore difficult to structure the topic didactically. Many things can be confusing when you first read them, so stick to the motto “learn now, understand later”. Some aspects may even only become clear through the example sketches at the very end. The timer D also gave me a few headaches when working through the data sheet!

Overview of the Timer D

Here are a few selected features of the Timer D:

  • 12-bit Timer/Counter
  • Clock: System clock, 20 MHz oscillator or external; asynchronous
  • Only the representatives of the tinyAVR series 1 have a Timer D (one, to be precise, namely TCD0).
  • Four “Waveform generation modes” for the output:
    • One Ramp Mode
    • Two Ramp Mode
    • Four Ramp Mode
    • Dual Slope Mode
  • Two input channels for events.
  • Two outputs.
  • Four compare registers or two pairs:
    • A: Compare registers CMPASET / CMPACLR
    • B: Compare registers CMPBSET / CMPBCLR
  • Each TCD cycle comprises the following:
    • DTA (Dead Time A) → OTA (On Time B) → DTB (Dead Time B) → OTB (On Time B)

Please note: If you are using the megaTinyCore board package from Spence Konde, Timer D is used by default for millis() / delay() and analogWrite(). Changes to the Timer D settings can render these functions unusable. You can change the default settings in the Arduino IDE under Tools → millis()/micros() Timer or PWM Pins.

Asynchrony of the Timer D

The clock of the TCDn core is asynchronous to the peripheral clock. This has a few side effects. Almost all TCDn registers must first be synchronized after you have written to them before the changes take effect. And if a synchronization is in progress, you cannot change certain settings but have to wait until the current synchronization is complete. You also cannot read the counter directly, but only indirectly via the two capture registers. Overall, using the Timer D is comparatively more demanding because there is much more you can do wrong. On the other hand, Timer D is a real sophisticated tool.

Is it worth the effort? Do you need the features of the Timer D? I’ll leave that up to you to answer!

The Registers of the Timer D (TCDn Module)

Register Types

Before we turn to the individual registers, I need to explain the differences between static registers, double-buffered registers, command/enable registers and normal registers/read-only registers. These register types differ primarily in terms of the conditions under which you can write to them and the conditions under which they are synchronized.

Command and enable registers: Only Control E register (TCDn.CTRLE) and the ENABLE bit in Control A register (TCDn.CTRLA) fall into this category. Synchronization takes place automatically as long as Timer D is activated. You can only make changes in TCDn.CTRLE if the CMDRDY bit in the Status register (TCDn.STATUS) is set (we will come to this later).   The ENRDY bit in the Status register must be set if you want to set or clear the ENABLE bit.

Double-buffered registers: You can write to the double-buffered registers if the CMDRDY bit is set in the status register. Their settings are only synchronized to the TCDn domain when you send a SYNC command or when you enable Timer D.

  • Double-buffered registers are: DELCTRL, DELVAL, DITCTRL, DITVAL, DBGCTRL, COMPASET, COMPBSET, CMPACLR, CMPBCLR.

Static registers: These registers cannot be written to as long as Timer D is enabled. An exception is the ENABLE bit in TCDn.CTRLA (it would be stupid if it wasn’t!).

  • Static registers are: CTRLA to CTRLD, EVCTRLA/B, INPUTCTRLA/B, FAULTCTRL.

Normal/read-only registers: These registers are not subject to any restrictions regarding synchronization, i.e. you can write or read them at any time.

  • Normal registers: INTCTRL, INTFLAGS.
  • Read-only registers: CAPTUREA, CAPTUREB, STATUS

Control A register – TCDn.CTRLA

Timer D register: TCDn.CTRLA
Control A register – TCDn.CTRLA

You can apply the following settings in the Control A register:

  • CLKSEL (Clock Select): Selection of the clock source. Available for selection:
    • 20MHZ: Internal 20 MHz oscillator (default)
    • EXTCLK: External clock
    • SYSCLK: System clock
  • CNTPRES (Counter Prescaler): Prescaler for the Timer D counter
    • DIV1 (default), DIV4, DIV32: Divider
  • SYNCPRES (Synchronization Prescaler): Prescaler for synchronization
    • DIV1 (default), DIV2, DIV4, DIV8: Divider
  • ENABLE: enables TCDn (default: disabled)

The effective frequency for the signal at output WOx is:

    \[ \text{freq} = \text{TCDn\_clock} \cdot \text{counter\_prescaler} \cdot \text{synchronization\_prescaler} \]

You can only set the ENABLE bit if the ENRDY bit is set in the Status register. To ensure this, use while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}.

Control B register – TCDn.CTRLB

Timer D register: TCDn.CTRLB
Control B register – TCDn.CTRLB

The Control B register (CTRLB) determines the waveform generation mode. You have the options:

  • ONERAMP: One Ramp mode (Default)
  • TWORAMP: Two Ramp mode
  • FOURRAMP: Four Ramp mode
  • DS: Dual Slope mode

I will go into the different modes in detail later.

Control C register – TCDn.CTRLC

Timer D register: TCDn.CTRLC
Control C register – TCDn.CTRLC

The default outputs for the signals generated by CMPASET/COMPACLR and COMPBSET/COMPBCLR (waveform A/B) are assigned to WOA and WOB respectively. Look in the data sheet under “I/O Multiplexing and Considerations” to see which pins these are. Alternatively, you can assign the signals to WOC and WOD, provided the pins are implemented on your ATtiny. In Control C register you can set which signal is assigned to which alternative output:

  • CMPDSEL (WOD):
    • PWMA: Waveform A (bit value 0, default)
    • PWMB: Waveform B (bit value 1)
  • CMPCSEL (WOC):
    • PWMA: Waveform A (bit value 0, default)
    • PWMB: Waveform B (bit value 1)

This is taken from the data sheet. However, something has gone wrong here. For example, you re-direct output of WOA to WOC and WOD:

TCD0.CTRLC |= TCD_CMPDSEL_PWMA_gc | TCD_CMPCSEL_PWMB_gc;

If you activate the FIFTY bit, the settings for CMPASET and CMPACLR are automatically adopted for CMPBSET and CMPBCLR and vice versa. If you enter values for all CMPxSET and CMPxCLR registers, the last values entered apply.

The other two bits do the following:

  • AUPDATE (Automatically Update): Automatic synchronization at the end of the TCD cycle. To be more precise, synchronization is triggered when CMPBCLRH, i.e. the upper byte of CMPBCLR, is written.
  • CMPOVR (Compare Out Value Overwrite): If this bit is set, the settings in TCDn.CTRLD will determine the output level of the various TCD phases.

Control D register – TCDn.CTRLD

Timer D register: TCDn.CTRLD
Control D register – TCDn.CTRLD

The settings of this register only take effect if you set the CMPOVR bit in CTRLC.

To understand the impact of this control register, you need to know about the waveform generation modes. If necessary, skip this chapter and come back to it later.

Let’s take an example. In Four Ramp mode, the output level at WOA / WOB changes as follows in the default setting:

  1. DTA (Dead Time A): WOA and WOB are LOW
  2. OTA (On Time A): WOA is HIGH, WOB is LOW
  3. DTB (Dead Time B): WOA and WOB are LOW
  4. OTB (On Time A): WOA is LOW, WOB is HIGH

This corresponds to the setting:

TCD0.CTRLC = TCD_CMPOVR_bm;
TCD0.CTRLD = TCD_CMPBVAL_3_bm | TCD_CMPAVAL_1_bm; 

For example, if you want not only WOB but also WOA to be HIGH during OTB, you can set this as follows:

TCD0.CTRLC = TCD_CMPOVR_bm;
TCD0.CTRLD = TCD_CMPBVAL_3_bm | TCD_CMPBVAL_1_bm | TCD_CMPAVAL_1bm; 

I hope this makes the principle clear. Important: Setting CMPOVR causes all outputs in all phases to go LOW by default. For all phases in which you want to generate a HIGH level at WOA or WOB, you must set the corresponding CMPxVAL[y] bit.

Control E register – TCDn.CTRLE

Timer D register: TCDn.CTRLE
Control E register – TCDn.CTRLE

The bits of the Control E register (CTRLE) have the following effect:

  • DISEOC (Disable at end of TCD cycle atrobe): TCDn is automatically disabled at the end of the TCD cycle.
  • SCAPTUREA / SCAPTUREB (Software capture A/B strobe): triggers a software capture of the counter to Capture A or B after next synchronization.
  • RESTART (Restart Strobe): triggers a restart of Timer D after the next synchronization.
  • SYNC (Synchronize strobe): the contents of the double-buffered registers (e.g. CMPxSET, CMPxCLR – see data sheet) are transferred to the TCD domain during the next synchronization.
  • SYNEOC (Synchronize End of TCD Cycle Strobe): the contents of the double-buffered registers are transferred to the TCD domain at the end of the TCD cycle.

The following applies to the Control E register (CTRLE):

  • This is a strobe register. Its bits are deleted after the next synchronization.
  • Writing to CTRLE deletes the CMDRDY bit in the STATUS register. The CMDRDY bit is set again with the next synchronization.
  • Setting the DISEOC bit deletes the ENRDY bit until the TCD cycle is completed.

Event Control A/B register – TCDn.EVCTRLA / TCDn.EVCTRLB

Timer D register: Event Control A/B register
Event Control A/B register

The Timer D has two input channels for events, namely A and B. The Event Control registers determine how TCDn handles incoming event signals. I’ll start here with the lowest bit:

  • TRIGEI (Trigger Event Input Enable): Enables event signals to be a trigger for the input channel. If TRIGEI is not set, the event signals have no effect (default: disabled).
  • ACTION (Event Action):
    • FAULT: The event triggers a so-called fault (default setting). Among other things, the fault can stop the counter or trigger a jump to another compare cycle. You define the effect of the fault in the INPUTCTRLA or B register.
    • CAPTURE: The event triggers a capture and a fault. A capture causes the counter reading to be written to the CAPTUREA or CAPTUREB register, depending on the input channel.
  • EDGE (Edge Selection):
    • FALL_LOW: The falling edge or the LOW state of the event is the trigger (default).
    • RISE_HIGH: The rising edge or the HIGH state of the event is the trigger.
  • CFG (Event Configuration):
    • NEITHER: “No extras” activated (default).
    • FILTER: The event must be detected for four clock cycles for it to be considered valid.
    • ASYNC: With this bit, the event input will affect the output directly. .

Interrupt Control and Interrupt Flag register – TCDn.INTCTRL / TCDn.INTFLAGS

Timer D register: Interrupt Control register / Interrupt Flag register
Interrupt Control register / Interrupt Flag register – TCDn.INTCTRL / TCDn.INTFLAGS

Timer D has three interrupts, which you enable by setting the corresponding bits in the Interrupt Control register:

  • TRIGA / TRIGB (Trigger A/B Interrupt Enable): Incoming event signals via input A or B trigger an interrupt.
  • OVF (Counter Overflow): The bit activates the overflow interrupt. The top value of the counter is always CMPBCLR.

If an interrupt is triggered, the corresponding bit in the Interrupt Flag register INTFLAGS is set. To delete it, you must write a “1” to it, e.g. TCD0.INTFLAGS = TCD_OVF_bm;. Sounds illogical, but that’s how it is.

Status register – TCDn.STATUS

Timer D Register: Status register
Status register – TCDn.STATUS

The PWMACTA/B bits are set whenever WOA or WOB change from HIGH to LOW or vice versa. To delete the bits, you must write a “1” to them.

As mentioned before, you cannot enable Timer D at any time in Control A register (CTRLA). You must wait until the ENRDY bit is set. Under certain conditions, Timer D cannot accept any new commands. In this case, the CMDRDY bit is deleted. Setting the following bits deletes CMDRDY:

  • TCDn.CTRLC: AUPDATE / Writing to the CMPBCLRH register
  • TCDn.CTRLE: SYNCEOC, SYNC, RESTART, SCAPTUREA/B

Input Control A/B register – TCDn.INPUTCTRLA / TCDn.INPUTCTRLB

TImer D register - TCDn.INPUTCTRLA / TCDn.INPUTCTRLB
Input Control A/B register – TCDn.INPUTCTRLA / TCDn.INPUTCTRLB

An incoming event signal triggers a capture and/or a so-called fault, depending on the setting in the EVCTRLA/B register. The fault affects the counter and the output. What it does exactly is determined by the input mode in the Input Control A or B register. You can find schematic diagrams of the input modes in the data sheet.

Here, as an example, is the “WAIT” input mode:

Effect of the WAIT input mode
Effect of the WAIT input mode

WOA and WOB are stopped when the event signal is received in WAIT mode. The counter jumps to the next compare cycle, e.g. from OTA to DTB. As long as the event continues, Timer D is waiting. As there are eleven different input modes in total, I won’t be able to discuss each one here. You can try out the input modes for yourself in one of my example sketches.

Please note: In Two and Four Ramp mode all input modes will work, but in One Ramp and Dual Slope mode only a selection works (see data sheet)

Fault Control register – TCDn.FAULTCTRL

Timer D register - TCDn.FAULTCTRL
Fault Control register – TCDn.FAULTCTRL

In the Fault Control register (FAULTCTRL), you enable the default outputs WOA and WOB or the alternative outputs WOC and WOD. You also define the level of the output after an event signal is received or after a reset (except the power-on reset).

  • CMPxEN (Compare x Enable): enables output WOx
  • CMPx (Compare x Value): If the bit is set, the output WOx is HIGH after a reset (except power-on reset) or with an event signal; otherwise it is LOW.

Further Registers

I will not go into the following registers in detail. Some of them should be self-explanatory, for the others, please refer to the data sheet.

  • DLYCTRL (Delay Control): Control register for delays.
  • DLYVAL (Delay Value): Contains the delay value.
  • DITCTRL (Dither Control): If you cannot create a certain frequency exactly, you can apply a dither value. The principle is the same as for leap years.
  • DITVAL (Dither Value): Contains the dither value.
  • DBGCTRL (Debug Control): Settings for the Debug mode.
  • CAPTUREA / CAPTUREB (Capture A/B): 12-bit Capture register.
  • CMPASET / CMPBSET (Compare A/B Set): 12-bit register for the Compare Set values.
  • CMPACLR / CMPBCLR (Compare A/B Clear): 12-bit register for the Compare Clear values.

Waveform Generation Modes

I will now come back to the waveform generation modes. These determine the effect of CMPxSET and CMPxCLR on the output.

One Ramp Mode

A TCD cycle basically consists of the phases: DTA (Dead Time A), OTA (On Time A), DTB (Dead Time B) and OTB (On Time B). This is the case in One Ramp mode if CMPASET < COMPACLR < CMPBSET < CMPBCLEAR:

One ramp mode with CMPASET &lt; CMPACLR &lt; CMPBSET &lt; CMPBCLR
One ramp mode with CMPASET < CMPACLR < CMPBSET < CMPBCLR

If your values deviate from this sequence, certain phases can also be omitted. For example, if your CMPBSET is lower than CMPASET, then there is no DTB phase:

One Ramp mode with CMPBSET &lt; CMPASET &lt; CMPACLR &lt; CMPBCLR
One Ramp mode with CMPBSET < CMPASET < CMPACLR < CMPBCLR

The TCD cycle always ends when CMPBCLR is reached. This is independent of the mode. If CMPACLR or COMPxSET are greater than CMPBCLR, these values have no effect.

As CMPBCLR is the top value of the counter, it determines the frequency of the TCD cycle:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPBCLR} +1)} \]

Two Ramp Mode

As the name suggests, a TCD cycle in Two Ramp mode consists of two ramps. The first ends with CMPACLR. The counter is then set to zero and the second ramp runs until CMPBCLR is reached.  

Timer D - Two Ramp mode
Two Ramp mode

Here, it is not relevant for the sequence whether CMPACLR is smaller or bigger than CMPBCLR. The following applies to the frequency of the TCD cycle:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} \]

Therefore, you should always bear in mind that DTA still has a length of one TCD counter, even if you set CMPACLR to 0.

Four Ramp Mode

In Four Ramp mode, all CMPxSET and CMPxCLR values have their own ramp. The CMPxSET ramps generate the dead time x phases, the CMPxCLR ramps generate the on time x phases. Overlapping of the phases is not possible.

Four Ramp mode

The frequency of the TCD cycle is:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPASET} +\text{CMPACLR} +\text{CMPBSET} + \text{CMPBCLR}+4)} \]

Dual Slope Mode

In dual slope mode, the counter counts down from CMPBCLR to 0 and then up again to CMPBCLR. A compare match of CMPASET causes WOA to go HIGH when counting down and LOW when counting up. For CMPBSET, the opposite applies. In the first TCD cycle, however, WOB starts in the LOW state and only goes to HIGH on the ascending side with the compare match of CMPBSET. The following cycles look like this:

Timer D - Dual Slope mode
Dual Slope mode

CMPACLR is irrelevant in dual slope mode.

The following applies to the frequency of the TCD cycle:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{2 \cdot (\text{CMPBCLR} +1)} \]

Example Sketches

Now to the practice – what a relief after so much information.

Please note: The board package megaTinyCore uses the Timer D as well as other timers used in my sketches for millis(), analogWrite(), Servo etc. These functions may refuse to work. For the example sketches, I recommend the setting “millis()/micros() Timer: RTC (no micros)” or “TCB0 (breaks tone() and Servo)” in the Arduino IDE.

Waveform Generation Modes Examples

Basic example

In the first example, we are applying waveform generation using the Two Ramp mode. Here is the sketch:

void setup(){
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; // enable WOA and WOB   

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_TWORAMP_gc;
    TCD0.CMPASET = 499;   // DTA = ~8.3 %
    TCD0.CMPACLR = 1999;  // OTA = 25 %
    TCD0.CMPBSET = 2999;  // DTB = 50 %
    TCD0.CMPBCLR = 3999;  // OTB = ~16.7 % 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV4_gc | TCD_SYNCPRES_DIV8_gc | TCD_ENABLE_bm; 
}

void loop(){} 

If you apply settings to the Timer D, you must follow a certain sequence and take precautions for some registers before writing to them:

  1. If Timer D is enabled, you must disable it by clearing the ENABLE bit in Control A register (CTRLA). I do this in the sketch using TCD0.CTRLA = 0;. To retain the other settings, you could also clear the bit selectively using TCD0.CTRLA &= ~TCD_ENABLE_bm;.
    • You can only write to TCD0.CTRLA if the ENRDY bit is set in the status register. This is ensured by line 3.
  2. If you want to use the WOx outputs, you must set the corresponding pins to OUTPUT. You can find out which pins these are in the data sheet.
  3. To enable the outputs, set the corresponding CMPxEN bits in the Fault Control register.
    • To disable the write protection of the FAULTCTRL register beforehand, you need to write the bit group IOREG (unlock protected I/O registers) to the CCP register (Configuration Change Protection).
  4. Then select the Wave Form Generation mode and set the values for CMPxSET and CMPxCLR.
  5. In the last step, activate Timer D and set the clock source, the counter prescaler and the synchronization prescaler if necessary. 
Discussion of the compare values

I have selected the internal 20 MHz oscillator as the clock source. The counter prescaler is 4, the synchronization prescaler is 8, so the effective TCDcounter frequency is 20 MHz / 32 = 625 kHz.

The frequency of the TCD cycle is:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} = \frac{625000}{6000} = ~104 [\text{Hz}] \]

I calculate the duty cycle (DC) using the WOB output as an example:

    \[ \text{DC}_{\text{WOB}} = \frac{ (\text{CMPBCLR} + 1) - (\text{CMPBSET} + 1)}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} = \frac{1000}{6000} \stackrel{\wedge}= ~16.7 [\%] \]

The calculations are of course specific to the Two Ramp mode!

Now you can try out different modes and settings for CMPxCLR and CMPxSET. The effect on the output at WOA and WOB can be easily observed with the oscilloscope. But since not everyone has an oscilloscope, I will now show you a slow motion variant.

Slow Motion variant

Even if we use the system clock as clock source, reduce the system clock to 1 MHz and set the prescalers for the counter and synchronization to the maximum (32 and 8 respectively), the maximum period of a TCD cycle in One Ramp mode is (32 * 8 * 4096) / 1 MHz = ~1.05 seconds. This is a little short to track the TCD phases optically with LEDs.

We therefore “build” an external clock generator at WO1 (PB1 on my ATtiny14) using Timer A, which we connect to the input for external clock sources (EXTCLK = PA3).

Then we connect a push-button to PA1, but we won’t need it until later.

Here is the circuit:

Circuit for (most) example sketches
Circuit for (most) example sketches

And here is the sketch:

void setup(){
    setupExternalClock();
 
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; 

    /* Change Output Polarity */
    // TCD0.CTRLC = TCD_CMPOVR_bm; 
    // TCD0.CTRLD = TCD_CMPBVAL_2_bm | TCD_CMPAVAL_0_bm;    

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_TWORAMP_gc;
    TCD0.CMPASET = 499;  //DTA = ~8.3 %
    TCD0.CMPACLR = 1999; //OTA = 25 %
    TCD0.CMPBSET = 2999; //DTB = 50 %
    TCD0.CMPBCLR = 3999; //OTB = ~16.7 % 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){} 

void setupExternalClock(){
    /* Crate a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

 

The only difference between the slow motion sketch and the previous sketch is the additional function setupExternalClock(), which provides us with a 1kHz signal and the selection of the external clock in Control A register A (line 26). Thanks to the 1 kHz signal, one timer count corresponds to one millisecond. The TCD cycle has a period of 6 seconds, the two LEDs light up for 1.5 and 1 second respectively. Now you can play around with the waveform generation modes and the other settings yourself.

Another suggestion

If you wish, uncomment lines 16 and 17 → HIGH and LOW are swapped on the respective ramp. Or you can set CMPAVAL_1 and CMPAVAL_3 to combine the outputs of WOA and WOB at WOA.

Input mode examples

Slowing down the TCD clock using the external source also helps us when testing the input modes because making the input mode effects visible is a certain challenge even on the oscilloscope.

void setup(){
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    setupPA1Event();

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; // | TCD_CMPA_bm | TCD_CMPB_bm;  

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 499;  //DTA = 0.5s
    TCD0.CMPACLR = 1999; //OTA = 2s
    TCD0.CMPBSET = 1499; //DTB = 1.5s
    TCD0.CMPBCLR = 2999; //OTB = 3s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

void setupPA1Event(){
    /* Event setup: Event = PA1->LOW */
    PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; // A1 pullup
    PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is async. event generator for channel 0
    EVSYS.ASYNCUSER6 = EVSYS_ASYNCUSER6_ASYNCCH0_gc;  // ASYNCUSER6 = TCD0_EV0 (Input A) = channel 0 user 

    /* Event Control TCD0 */
    TCD0.EVCTRLA = TCD_CFG_ASYNC_gc | TCD_EDGE_FALL_LOW_gc | TCD_ACTION_FAULT_gc | TCD_TRIGEI_bm;
    TCD0.INPUTCTRLA = TCD_INPUTMODE_WAIT_gc;   
}

 

The sketch is based on the previous example wave_form_generation_slow.ino. The following has been added or modified:

  • The Four Ramp mode is used.
  • The function setupPA1Event() defines pin A1 as an asynchronous event generator and TCD0 as an event user. If you need help with events, look here. What the event actually does is defined in the TCD0 registers EVTCTRLA and INPUTCTRLA:
    • TCD_CFG_ASYNC_gc: the event has a direct effect on the output.
    • TCD_EDGE_FALL_LOW_gc: The event condition is a falling edge or LOW state.
    • TCD_ACTION_FAULT_gc: the event acts on the output, no capture.
    • TCD_TRIGEI_bm: Trigger Event Input Enable.
    • TCD_INPUTMODE_WAIT_gc: Input mode is “WAIT”.

Press the button and see what happens. Have fun playing with the different input modes! You could also set the CMPA and CMPB bits in line 15. Then, as long as you press the button, the LEDs will light up.

Changes to the Timer D settings during operation

So far, we have only made the settings on timer D once at setup(). If you want to modify the settings during operation, you have to pay attention to a few more things than with Timers A and B. As an example, we dim two LEDs in maximum resolution (i.e. with 4096 different duty cycles) and use the Dual Slope mode. Each duty cycle shall only be used for one TCD cycle and then increased or reduced. I would like to present two variants for this.

LED Dimmer – Variant 1

Here is the sketch:

void setup(){
    /* Disable Timer D */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR |= PIN4_bm | PIN5_bm;  
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc;    
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;    
    
    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_DS_gc;
    TCD0.CMPBCLR = 4095; // TCD cycle freq: 20 MHZ / (2 * 4096) = ~2.441 kHz
}

void loop(){
    for(int i=0; i<=4095; i++){ // maximum resolution
        TCD0.CMPASET = i;
        TCD0.CMPBSET = i;  
        TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
        while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // Check Readiness
        TCD0.CTRLE = TCD_DISEOC_bm; // disable at end of TCD Cycle
        while(!(TCD0.STATUS & TCD_ENRDY_bm)){;} // Wait for end of Cycle
    }
    for(int i=4094; i>0; i--){ 
        TCD0.CMPASET = i;
        TCD0.CMPBSET = i; 
        TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
        while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // Check Readiness
        TCD0.CTRLE = TCD_DISEOC_bm; // disable at end of TCD Cycle
        while(!(TCD0.STATUS & TCD_ENRDY_bm)){;} // Wait for end of cycle
    }
} 

 

The frequency of the TCD cycles under these conditions is 20 MHz / (2 * 4096) = ~2.441 kHz. Dimming up or down once happens in 4096 steps, i.e. in approx. 4096 / 2441 = ~1.678 seconds. I took the time for 10 complete dimming cycles with the stopwatch and the result matched well.

But the main point I want to discuss is the timer setting. When we start, Timer D is disabled. We set CMPASET and CMPSET and enable the timer. To ensure that Timer D only runs through one cycle, we set the DISEOC bit (Disable at End of Cycle). Before we do this, we check with TCD0.STATUS & TCD_CMDRDY_bm whether CTRLE is ready to receive commands. We determine the end of the TCD cycle by checking the ENRDY bit. If it is set, the cycle is completed and we start again.

The sketch is completely blocking because we permanently query the status of the ENRDY bit so as not to lose any time between cycles. This is not particularly nice, but my main aim was to illustrate the principle.  

LED Dimmer – Variant 2

Alternatively, we can also be notified about the end of the TCD cycle by the TCD overflow interrupt. This is exactly what the following sketch does:

volatile bool endOfCycle = false;

 ISR(TCD0_OVF_vect){  // ISR for Timer D overflow
    TCD0.INTFLAGS = TCD_OVF_bm; // Clear the interrupt flag (needed!)
    endOfCycle = true;   
}
 
 void setup(){
    /* Disable Timer D */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR |= PIN4_bm | PIN5_bm;  
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc;    
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;    
    
    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_DS_gc;
    TCD0.CMPBCLR = 4095;
    TCD0.CTRLC = TCD_AUPDATE_bm; // automatic sync 
    TCD0.INTCTRL = TCD_OVF_bm;
        
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){
    static bool ascending = true;
    static int cmpValue = 0; 
    if(endOfCycle){
        if(ascending){
            if(cmpValue < 4095){
                cmpValue++;
            }
            else{
                cmpValue--;
                ascending = false;
            }
        }
        else{
            if(cmpValue > 0){
                cmpValue--;
            }
            else{
                cmpValue++;
                ascending = true;
            }
        }
        TCD0.CMPASET = cmpValue;
        TCD0.CMPBSET = cmpValue;  
        TCD0.CMPBCLR = 4095; // needed to initiate synchronisation
        // while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;}
        // TCD0.CTRLE = TCD_SYNC_bm;
        endOfCycle = false;
    }
} 

 

When the end of the cycle is reached, the next CMPASET and CMPBSET values are calculated. This is done by the nested if(ascending){...} construction. By setting the AUPDATE bit (line 23), synchronization is automatically requested at the end of the cycle. Then we come to a crucial point: CMPBCLRH (i.e. the high byte of CMPBCLR) must be written so that the synchronization is actually triggered. We therefore write 4095 in CMPBCLR again (line 54), although we do not change the value. This was not necessary in the previous example, as we enabled the timer for each TCD cycle and the CMPxSET values were synchronized as a result.

Instead of the SYNC bit, you could also set the SYNCEOC bit. A little pedantry: but then you would also have to set it once before the Timer D is enabled first time (line 26/27); otherwise there will be no update in the first TCD cycle.

Alternatively, you can comment out lines 23 and 54 and uncomment lines 55 and 56. This will request the synchronization “manually”. Do not forget to check the CMDRDY bit beforehand.  

This sketch is not blocking. However, if you add time-consuming tasks to loop(), this could extend the dimming cycle.

Input Capture

You can only read the Timer D counter indirectly via the CAPTUREA or CAPTUREB registers. The counter reading is written to the capture register via events or software-controlled by setting the SCAPTUREA or SCAPTUREB bit.

Event-controlled Capture

In the first example, we try the event-controlled variant. The sketch input_mode_testing.ino serves as the basis, whereby I have changed or added the following:

  • The event caused by the pressed button triggers the Trigger A interrupt. The setting is determined in the Interrupt Control register. The ISR sets the bool variable trigEvent to true, and we can then check this in loop().
  • In the Event Control register, we select TCD_ACTION_CAPTURE_gc for the ACTION bit, i.e. the incoming event signal triggers a capture and a fault in addition to the trigger interrupt.
    • With input mode NONE, however, the fault has no further effect.
    • The capture copies the counter reading to the Capture register when the button is pressed.
  • I have selected the Four Ramp mode as waveform generation mode. DTA and DTB are reduced to the minimum (4 milliseconds). OTA and OTB are set to 8 seconds.

The external clock is also used here again – purely for training purposes. You only need to remove setUpExternalClock() and change the clock in line 35 to speed up again.

volatile bool trigEvent = false;

ISR(TCD0_TRIG_vect){  // ISR for Timer D Trigger
    TCD0.INTFLAGS = TCD_TRIGA_bm; // Clear the interrupt flag (needed!)
    trigEvent = true; 
}
 
 void setup(){
    Serial.begin(115200);
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    setupPA1Event();

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;  

    /* Enable Trigger A Interrupt */
    TCD0.INTCTRL = TCD_TRIGA_bm;

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 0;  //DTA = 4 ms
    TCD0.CMPACLR = 1999; //OTA = 8 s
    TCD0.CMPBSET = 0; //DTB = 4 ms
    TCD0.CMPBCLR = 1999; //OTB = 8 s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV4_gc | TCD_ENABLE_bm; 
}

void loop(){
    if(trigEvent){
        Serial.println(TCD0.CAPTUREA);
        trigEvent = false;
    }
} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty Cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

void setupPA1Event(){
    /* Event setup: Event = PA1->LOW */
    PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; // A1 pullup
    PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is async. event generator for channel 0
    EVSYS.ASYNCUSER6 = EVSYS_ASYNCUSER6_ASYNCCH0_gc;  // ASYNCUSER6 = TCD0_EV0 (Input A) = channel 0 user 

    /* Event Control TCD0 */
    TCD0.EVCTRLA = TCD_CFG_ASYNC_gc | TCD_EDGE_FALL_LOW_gc | TCD_ACTION_CAPTURE_gc | TCD_TRIGEI_bm;
    TCD0.INPUTCTRLA = TCD_INPUTMODE_NONE_gc;   
}

You can see how the two LEDs light up alternately for 8 seconds. When you press the button, you can see the current counter reading. That’s all. Not particularly exciting, but hopefully makes the principle clear.  

Software Capture

In the software-controlled version, we omit the event control via push-button. Accordingly, there is also no trigger interrupt. The two LEDs remain lit for eight seconds each in four ramp mode. The counter reading is displayed every two seconds.

To do this, we set the SCAPTUREB bit in control register E. Before we check whether the CMDRY bit is set, i.e. whether Control E register can accept commands.

This is the sketch:

void setup(){
    Serial.begin(115200);
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;  

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 0;  //DTA = 4 ms
    TCD0.CMPACLR = 1999; //OTA = 8 s
    TCD0.CMPBSET = 0; //DTB = 4 ms
    TCD0.CMPBCLR = 1999; //OTB = 8 s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV4_gc | TCD_ENABLE_bm; 
}

void loop(){
    while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // check readiness
    TCD0.CTRLE = TCD_SCAPTUREB_bm; 
    while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // wait for synchronisation
    Serial.println(TCD0.CAPTUREB);
    delay(2000);
} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // needed for the Arduino board package
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

Line 29 is important as it checks whether the capture instruction has already been synchronized. If you comment it out, you will see that you do not get the latest counter readings. The first counter reading output after the LED jumps over is so big that it obviously still belongs to the lighting phase of the other LED. There is a certain delay between requesting the counter reading and the actual capture.

Copy settings from CMPxSET / CMPxCLR

If you want to copy the settings for CMPASET and CMPACLR to CMPBSET and CMPBCLR (or vice versa), simply set the FIFTY bit in control register C:

TCD0.CTRLC |= TCD_FIFTY_bm;

The last setting for CMPxSET / CMPxCLR is duplicated. You can try this out with the sketch wave_form_generation_slow.ino. I will save myself a separate sketch for this.

Using WOC and WOD

Whether you can use WOC and WOD instead of WOA and WOB depends on whether the outputs are actually implemented as pins. On the ATtiny1614, which I used for most of the examples, WOC and WOD are on PC0 and PC1. These pins are only available on the 24-pin VQFN version. Since I don’t have that, I used an ATtiny3216 instead.

Here, too, you could modify the sketch wave_form_generation_slow.ino accordingly:

PORTC.DIR = PIN1_bm | PIN0_bm; // WOD / WOC (z.B. ATtiny3216)

/* assign PWMx to WOx */ 
TCD0.CTRLC |= TCD_CMPDSEL_PWMB_gc | TCD_CMPCSEL_PWMA_gc; 

/* Enable Output Channels */ 
CPU_CCP = CCP_IOREG_gc; // enable write protected register 
TCD0.FAULTCTRL = TCD_CMPCEN_bm | TCD_CMPDEN_bm;

Leave a Reply

Your email address will not be published. Required fields are marked *