tinyAVR Series Part 2: Timer A/B, CCL, SLPCTRL

About this Post

This is the second part of my article about the tinyAVR series 0, 1 and 2. In the first part I explained how register programming for the tinyAVR series works in principle and went into some modules. This sequel deals with Timers A and B, the Configurable Custom Logic (CCL) and the Sleep Control Module (SLPCTRL). Timer D, which is used in the tinyAVR Series 1, is still missing. I will deal with this in the third part.

Here is an overview and links to all topics:

Timer A – TCAn module

First, a few general comments on Timer A:

  1. All representatives of the tinyAVR series have exactly one Timer A (TCA0).
  2. TCA0 can be operated as a 16-bit timer (single mode) or split into two 8-bit Timers (split mode).
  3. TCA0 is coupled to the system clock, but it can be slowed down with a divider (prescaler) in the TCA0.SINGLE.CTRLA or TCA.SPLIT.CTRLA register.
  4. In the default setting of the megaTinyCore board package, the TCA0 prescaler is set to 64. In addition, the Timer A is in split mode to get the maximum number of PWM pins. Changes to the settings can affect the PWM function accordingly. In addition, the representatives of the tinyAVR 0 series use TCA0 for millis(), micros() and delay() by default. Changes to the prescaler make these functions unusable. However, you can assign millis() / micros() to other Timers or deactivate them (Arduino IDE → Tools).
  5. TCA0 counts up to the maximum value PER in Normal (Single) Mode. Three compare channels are available. The Compare Values are CMP0, CMP1 and CMP2.
  6. In Split Mode, the 16-bit Timer A turns into two 8-bit Timers with 3 compare values each. With this, you can generate up to 6 PWM outputs. The PWM frequency is identical for each of the three outputs.
  7. You address the registers for the Normal Mode with TCA0.SINGLE.registername, and the registers for Split Mode with TCA0.SPLIT.registername.

This is an ultra-short summary. I recommend reading the documentation by Spence Konde on the subject of Timers and PWM.

Timer A Registers for Normal (Single) Mode

Some registers are identical in Normal and Split Mode. I have highlighted this accordingly.

This article is not a complete register documentation, but rather a starting aid. It is best to consult the data sheet at the same time.

Control A Register TCAn.CTRLA

In the Control Register CTRLA you set the clock divider and enable Timer Counter A.

tinyAVR - TCAn.CTRLA Register
TCAn.CTRLA Register

The CLKSEL (Clock Select) bit group defines the divider. The following bit group configuration masks are available for selection:

tinyAVR - CLKSEL Options for TCA0
CLKSEL – Options

Use the ENABLE bit to enable TCA0. You can check the setting of CTRLA with Serial.println(TCA0.SPLIT.CTRLA, BIN). The output should be 0b1011, i.e. the Timer A is enabled, and the divider is 64, so you do not need to enable the timer yourself. And once again: When using the megaTinyCore package, you must be aware of the side effects when changing the TCA0 settings.

The RUNSTDBY bit is only available for the tinyAVR series 2. If you set it, you send the Timer/Counter A into standby mode.

Control B Register TCAn.CTRLB

TCAn.CTRLB Register
TCAn.CTRLB Register

You set the Compare-n-Enable bits (CMPnEN) to activate the Compare Channel outputs. You can find out where the outputs are located in the “I/O Multiplexing and Considerations” section of the data sheet. The outputs are called WOx and correspond to CMPx. In some cases, the outputs can be assigned to alternative pins (PORTMUX module). However, not all tinyAVR representatives have all outputs (and the alternatives) actually implemented as pins.

I will not go into the Auto Lock Update Bit (ALUPD) here. The bit group WGMODE[2:0] defines the Wave Form Generation Mode:

tinyAVR series: WGMODE options for TCA0 in Normal Mode
Wave Form Generation Mode Options

Control D Register TCAn.CTRLD

The Split Mode is set in the Control D register. It is not relevant whether you make the setting via TCA0.SINGLE.CTRLD or TCA0.SPLIT.CTRLD.

tinyAVR Register: TCAn.SINGLE.CTRLD
Register TCAn.SINGLE.CTRLD / TCAn.SPLIT.CTRLD

Interrupt Control Register TCAn.INTCTRL

In the Interrupt Control register, you define which interrupts are to be activated. Interrupts are available for Compare Matches and Overflow:

tinyAVR Register: TCAn.SINGLE.INTCTRL / TCAn.SINGLE.INTFLAGS
TCAn.SINGLE.INTCTRL / TCAn.SINGLE.INTFLAGS

The Interrupt Flag Register INTFLAGS is similar to INTCTRL and uses the same bit names and positions.

Event Control Register TCAn.EVCTRL

You can set Timer A so that it is not controlled by the system clock but by external events. To do this, set the “Enable Count On Event Input” bit CNTEI in the Event Control Register EVCTRL.

tinyAVR Register: TCAn.SINGLE.EVCTRL
TCAn.SINGLE.EVCTRL

You can use the Bit Group Configuration EVACT to determine which events are counted and how they are counted:

EVACT Bit Group Configurations
EVACT Bit Group Configurations

With the first two settings, you count the edges, i.e. the number of events. With the other two settings, the system clock (with divider if necessary) is used to count for as long as the state caused by the event lasts.

Further Registers

Then there are a number of other registers that only contain numbers:

  • CNT (16 bit): is the Counter Register for Timer A.
  • PER (16 bit): is the TOP value for most Wave Form Generation Modes.
  • CMP0, CMP1, CMP2 (16 bit): contain the Compare Values.
  • PERBUF: you can write PER directly to the PER register. Under certain circumstances, however, an immediate change of PER (e.g. in the middle of a PWM period) is undesirable. If you write PER in PERBUF, the new PER is only applied when the old PER is reached.
  • CMP0BUF, CMP1BUF, CMP2BUF: see PERBUF.

In Normal (Single) Mode, the Timer/Counter A has a size of 16 bits. However, it can also be split into two 8-bit Timers (Split Mode). In Normal Mode there are three Compare Channels available, in Split Mode there are six. Accordingly, 3 or 6 PWM outputs can be realized using Timer A (provided the output pins are available).

Example sketches for Timer A in Normal Mode

After all the theory, let’s have some example sketches.

Overflow Interrupt

We start quite simply with an Overflow Interrupt. We use this to toggle an LED attached to PA2.

ISR(TCA0_OVF_vect){  // ISR for Timer A overflow
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear the interrupt flag (needed!)
    PORTA.OUTTGL = PIN2_bm; // Toggle PA2 
}

void setup() {
    PORTA.DIRSET = PIN2_bm; // PA2 auf OUTPUT
    TCA0.SINGLE.CTRLD = 0; // change to Single Mode
    TCA0.SINGLE.PERBUF = 62499; // Set TOP for Timer A 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.CTRLB = 0; // no output, wave form: normal;
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm; // enable Timer A overflow interrupt
}

void loop() {}

With a clock rate of 20 MHz, a divider of 64 and a PER of 62499 (= 62500 steps), the overflow frequency is 20000000 / (64 * 62500) = 5 Hz.

Single Slope PWM

In the next example, we generate a PWM signal with a frequency of 250 Hz and a duty cycle of 20% at WO2 (output for CMP2). You can find out which pin WO2 is assigned to in the data sheet. We achieve the 250 Hz by setting PER to 1249 (at 20 MHz). The duty cycle is CMPn / (PER +1).

void setup() {
    PORTB.DIRSET = PIN2_bm;  // PB2 = WO2 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.PERBUF = 1249;
    TCA0.SINGLE.CMP2BUF = 250;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
}

void loop() {}

On an ATtiny202/402, WO2 is assigned to pin PA2 (just as an example). Accordingly, you need to replace PORTB with PORTA in the example.

Frequency Mode

In Frequency Mode, the TCA0 Counter counts up to CMP0 instead of PER. When CMP0 is reached, the CMP0 output (WO0) is toggled. This sets the duty cycle in this mode to 50 %. Here is a small example:

void setup() {
    PORTB.DIRSET = PIN0_bm;  // PB0 = WO0 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    TCA0.SINGLE.CMP0 = 1249;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_FRQ_gc;
}

void loop() {}

At 20 MHz, the toggle frequency is 250 Hz, i.e. the PWM frequency is 125 Hz.

Dual Slope Modes

In Dual Slope Modes, TCA0.CNT counts up from 0 to PER and then back down to 0. In the event of a Compare Match when counting up, the CMPx output goes LOW and when counting down it goes HIGH. As a result, the PWM frequency is halved compared to single slope mode.

void setup() {
    PORTB.DIRSET = PIN0_bm;  // PB0 = WO0 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.PERBUF = 1249;
    TCA0.SINGLE.CMP0BUF = 250;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
}

void loop() {}

The only difference between the WG modes DSTOP, DSBOTTOM  and DSBOTH is their Overflow behavior. In the case of DSTOP, the Overflow is reached at PER, for DSBOTTOM it is 0 and for DSBOTH at 0 and PER. The PWM signal is identical for all Dual Slope Modes, provided that all other parameters are also identical.

Counting Events

In this example, we control Timer A via events. I have written the sketch for the ATtiny1614. Since the Event Modules of the different tinyAVR series differ quite a lot from each other (see part 1 of the article), some adjustments may be necessary for the tinyAVR representative of your choice (good practice!).

A rising edge at PB1 serves as an event here. To do this, connect PB1 to GND via a push-button. PB1 is pulled to HIGH level via the internal pull-up resistor. The event is therefore generated when the button is released. You can also invert the logic, but then you will need an additional external pull-down resistor.

The sketch informs us when the button has been pressed 10 or 20 times:

volatile bool tca0Ovf = false;  // Flag for TCA OVF
volatile bool cmp0Match = false;  // Flag for CMP0 Match

ISR(TCA0_OVF_vect){  // TCA0 Overflow ISR
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; /* Clear the interrupt flag */
    tca0Ovf = true;
}

ISR(TCA0_CMP0_vect){  // TCA0/CMP0 compare match ISR
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_CMP0_bm; /* Clear the interrupt flag */
    cmp0Match = true;
}

void setup() {
    Serial.begin(115200);
    PORTB.DIRCLR = PIN1_bm; // PB1 as input 
    PORTB.PIN1CTRL = PORT_PULLUPEN_bm; // Pullup enable
    EVSYS.SYNCCH1 = EVSYS_SYNCCH1_PORTB_PIN1_gc; // PB1 is sync. event generator
    EVSYS.SYNCUSER0 = EVSYS_SYNCUSER0_SYNCCH1_gc;  // TCA0 is sync. event user
    TCA0.SINGLE.CTRLD = 0; // set single mode
    TCA0.SINGLE.CTRLA = 0;  // switch off the timer, set clock divider to DIV1 
    TCA0.SINGLE.PER = 19;  // TCA0 counter limit
    TCA0.SINGLE.CMP0 = 9;  // Compare match value
    TCA0.SINGLE.CNT = 0;  // Set TCA0 counter to 0
    TCA0.SINGLE.EVCTRL = TCA_SINGLE_EVACT_POSEDGE_gc | TCA_SINGLE_CNTEI_bm; // count pos. edge of events (RISING) & enable event count
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc; // normal mode => TOP = PER
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // clear the overflow interrupt flag
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_CMP0_bm | TCA_SINGLE_OVF_bm; // enable CMP0 and OVF interrupts 
    TCA0.SINGLE.CTRLA = TCA_SINGLE_ENABLE_bm;
}

void loop(){
    if(cmp0Match){
        Serial.println("Key has been pressed 10 times");
        cmp0Match = false;
    }
    
    if(tca0Ovf){
        Serial.println("Key has been pressed another 10 times");
        Serial.println("Timer/Counter A restarts");
        tca0Ovf = false;
    }
    // Serial.println(TCA0.SINGLE.CNT);  // uncomment to see the TCA0 Counter
    //delay(100); 
}

 

I have tried to explain the code in the comments. I hope that is sufficiently understandable. However, a few annotations are still necessary:

  • We switch off Timer A so that it does not disturb us during the settings.
  • We are informed via Compare 0 Interrupt and Overflow Interrupt when the button has been pressed ten or twenty times.
    • The interrupts are only triggered when CMP0 and PER are exceeded. This is why CMP0 is 9 and PER is 19.
  • Setting the Compare 0 Register and the PER Register via the Buffer Registers (CMP0BUF, PERBUF) is not suitable here. You would have to press the button repeatedly until you reach the previously set PER. Only then will the new PER be cpoied from PERBUF. This can take a long time with a 16-bit counter!
  • Because of the button bounce, you probably have to press the button less than ten times to reach the limits.

Timer A Register in Split Mode

In Split Mode, Timer A is divided into a high and a low section. Accordingly, the 16-bit CNT register is split into an 8-bit HCNT and an 8-bit LCNT register. These two counter registers count in the same clock, but independently of each other. The PER register and the Compare registers are also split. PER becomes HPER and LPER, CMPn becomes HCMPn and LCMPn and so on.

The outputs for Timer A in Split Mode are activated in the Control B register (TCA0.SPLIT.CTRLB):

tinyAVR Register: TCAn.SPLIT.CTRLB
Register TCAn.SPLIT.CTRLB

The LCMPnEN bits are responsible for the WOn outputs, the HCMPnEN bits for the WO(n+2) outputs.

You can set interrupts for the Low Compare Channels in the Control D register. No interrupts are available for the high Compare Channels.

In Split Mode, the HCNT and LCNT registers count downwards. Accordingly, there are no Overflow Interrupts, but Underflow Interrupts (“UNF”). The bits for activating the interrupts are HUNF and LUNF.

tinyAVR Register: TCAn.SPLIT.CTRLB
Register TCAn.SPLIT.CTRLB

The corresponding names of the Interrupt Vectors are created according to the scheme TCA0_Interruptbit_vector, e.g. TCA0_LCMP1_vector.

Example sketch for Timer A in Split Mode

Let us try Timer A in Split Mode. The aim is to generate a PWM signal with a duty cycle of 25% at WO3 (HCMP0). The high part of the Counter Register of Compare Channel 0 is responsible for this. WO3 should be assigned to PA3 for all tinyAVR representatives. But if in doubt, check this. I have not looked at every data sheet!

void setup() {
    PORTA.DIRSET = PIN3_bm;  // PA3 = WO3 for many tinyAVR MCUs, please adjust
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // needed for the Arduino board package
    TCA0.SPLIT.HPER = 255; // PWM frequency = 20 MHz/(64 * 256) = ~1.22 kHz
    TCA0.SPLIT.HCMP0 = 64; // Duty cycle = 64 / (255 + 1) * 100 = 25 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV64_gc | TCA_SPLIT_ENABLE_bm; // redundant for megaTinyCore
    TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm; // enable output HCMP0 (WO3)
}

void loop() {}

Timer B – TCBn Module

The tinyAVR MCUs have either one or two 16-bit Timers B (TCB0 / TCB1):

  • 0-series: 1 Timer B
  • 1-series up to 8 kB Flash: 1 Timer B
  • 1-series from 16 kB Flash: 2 Timer B
  • 2-series: 2 Timer B

The megaTinyCore board package uses Timer(s) B for millis(), micros(), delay(), tone() or the Servo. Changes to the TCBn settings can render these functions unusable. However, you can also assign some of the functions to other timers. Look in the Arduino IDE under the menu item “Tools”.

You can find out to which pins the outputs for the TCBn Compare Matches are assigned and which alternatives are available in the data sheet under “I/O Multiplexing and Considerations”. Unfortunately, the designation of the outputs is inconsistent. For example, you will find “TCBn WO”, “n, WO” or “WOn”. You can assign outputs to alternative pins via the PORTMUX module.

Timer B Registers

Here, too, I would first like to point out once again that my article does not cover all available registers.

Control A Register TCBn.CTRLA

The most important settings in the Control A registers of Timer B are the clock rate (CLKSEL[2:0]) and ENABLE. The system clock, the system clock divided by two or the clock of Timer A can be selected as clock source.

tinyAVR Register: TCBn.CTRLA
Register TCBn.CTRLA

The default setting in the megaTinyCore package for CLKSEL is CLKTCA, i.e. the system clock with a divider of 64 is used.

Control B Register TCBn.CTRLB

In the Control B Register, you specify one of the eight Count Modes that make Timer B a versatile tool. I will only list the Count Modes here for now, as they are best explained using the following examples.

tinyAVR Register TCBn.CTRLB
Register TCBn.CTRLB
CTNMODE[2:0] Bit Group Configurations
CTNMODE[2:0] Bit Group Configurations

The other bits have the following effect:

  • ASYNC (Asynchronous Enable): allows asynchronous updates of the TCB output signal in Single Shot Mode.
  • CCMPINIT (Compare / Capture Pin Initial Value): sets the initial level of the output pin (0 = LOW, 1 = HIGH).
  • CCMPEN (Compare / Capture Output Enable): activates the Compare / Capture Output. In contrast to timer A, there is only one compare output per Timer B.

Interrupt Control Register TCBn.INTCTRL

The Interrupt Control Register is simple. You can activate the Capture Interrupt there, that’s all. The condition for triggering the interrupt depends on the Count Mode. I will come back to this in the examples. You can find an overview in the data sheet in the register description of TCBn.INTFLAGS.

tinyAVR Register: TCBn.INTCTRL
TCBn.INTCTRL / TCBn.INTFLAGS

The Interrupt Vector is called TCBn_INT_vect and not – as one would expect – TCBn_CAPT_vect. That’s a bit confusing.

Event Control Register TCBn.EVCTRL

The effect of the EDGE bit in the Event Control Register EVCTRL also depends on the Count Mode. Wait for the examples or have a look at the overview in the data sheet in the register description of TCBn.EVTCTRL.

The FILTER bit activates a noise filter that ensures that the event status must be constant for four clock cycles before the event is considered valid.

The Capture Event Input Enable Bit CAPTEI activates the Input Capture Event. In contrast to Timer A, Timer B cannot count edges (events), but the edge starts or stops the counter.

tinyAVR Register: 
TCBn.EVCTRL
TCBn.EVCTRL

Further registers

The 16-bit register CNT contains – unsurprisingly – the counter value of Timer B. Strictly speaking, there are two registers that you can address together via TCBn.CNT or separately via TCBn.CNTL and TCBn.CNTH.

As the name suggests, the Capture / Compare Register CCMP has various tasks. As a Compare Register, it controls interrupts and PWM, as a Capture Register it records counter readings as measured values.

Example sketches for Timer B

We will now go through example sketches for each Count Mode. The sketches may have to be adapted for the tinyAVR representative of your choice regarding the pins and possibly also regarding the Event Module.

Periodic Interrupt

In Periodic Interrupt Mode, Timer B counts up to CCMP, triggers the CAPT Interrupt (if activated) and starts again from the beginning. In the following example sketch, we use the CAPT Interrupt to toggle the board LED. Since the megaTinyCore package uses the Timer A clock (default setting DIV64) and we set the CCMP to 62500, the toggle frequency at 20 MHz is: 20 MHz / (64 * 62500) = 5 Hertz. 

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

void setup() {
    PORTA.DIRSET = PIN2_bm;
    PORTA.OUTSET = PIN2_bm;
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm;
    TCB0.CCMP = 62500; // Toggle frequency = 5 Hz @ 20 MHz
    TCB0.INTCTRL = TCB_CAPT_bm;
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use TCA clock / enable
}

void loop() {}

If you uncomment line 9, you increase the clock divider from 64 to 1024, i.e. by a factor of 16. This reduces the toggle frequency to 0.3125 Hz (→ period 3.2 s). But don’t forget: changing the Timer A clock may have an effect on millis() & Co. when using the megaTinyCore package.

This sketch should work unchanged on any tinyAVR microcontroller.

8-Bit PWM Mode

PWM signals are easy to create using Timer B. The lower byte of the Compare Register CCMP, namely CCMPL, defines the PWM frequency together with the clock. The duty cycle results from the ratio of CCMPH (i.e. the upper byte) to CCMPL. You can write to the CCMPH and CCMPL registers separately or together.

    \[ DutyCycle = \frac{\text{CCMPH}}{\text{CCMPL + 1}} \cdot 100\;\; [\%] \]

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 = TCB0 WO for ATtiny1614, please adjust
    TCB0.CCMP = 0x40FF;
    /* Alternative:
    TCB0.CCMPL = 0xFF; // 255
    TCB0.CCMPH = 0x40; */ // 64
    TCB0.CTRLB |= TCB_CCMPEN_bm | TCB_CNTMODE_PWM8_gc;
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm;
}

void loop(){}

In this example, we use the Timer A clock. This means that the frequency is 20 MHz / (64 * 256) = ~1.22 kHz. The duty cycle is 25%.

Single Shot Mode

In Single Shot Mode, the Timer counts up to CCMP and then stops until it is reset to a value less than CCMP. To try out the following sketch, connect an LED to the output for TCB0 (= PA5 for ATtiny1614).

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 = TCB0 WO for ATtiny1614, please adjust
    TCB0.CTRLA = 0; // disable TCB0
    TCB0.CCMP = 62500; // 0.2s signal length
    TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_SINGLE_gc; // enable output / single shot mode
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // enable TCB0, use TCA clock
}

void loop(){
    delay(1000);
    TCB0.CNT = 0; // restarts the counter
}

If your ATtiny is running at 20 MHz and you have not changed the Timer A clock, the LED will light up every second for 0.2 seconds.

Now we combine this with the Event System. To do this, connect a push-button to a suitable pin. The other side of the button is connected to GND. I have written the following sketch for an ATtiny1614 and selected PB1 as the push-button pin. Pushing the button (Event Generator) will start the TCB0 (Event User).

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 (TCB0 WO / ATtiny1614) as output
    PORTB.DIRCLR = PIN1_bm; // PB1 as input
    PORTB.PIN1CTRL = PORT_PULLUPEN_bm;
    TCB0.CTRLA = 0; // disable TCB0
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 as event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 WO (PA5 on the ATtiny1614)
    TCB0.CCMP = 62500; // 0.2s signal length
    TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_SINGLE_gc; // single shot mode
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // counter starts at both edges(?) / enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm; // enable interrupt
    TCB0.CNT = 62500; // prevents an unwanted, initial signal at PA5
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // enable TCB0, use TCA clock
}

void loop(){}

According to the data sheet, chapter 21.5.3, the timer should only be triggered on a falling (negative) edge when the EDGE bit is set, i.e. when the button is pressed and not when it is released. However, the LED lights up for both events. Perhaps simply an error in the data sheet? If the EDGE bit is not set, the LED does what it should: it only lights up when it is released (rising edge). At least this applies as long as your button does not bounce.

If you comment out line 16, you will see that the LED lights up briefly each time the MCU is reset. Starting the Timer causes it to start counting at 0 and the LED lights up accordingly. If you set the Timer Counter to the value of CCMP, this will not happen.

Adaptations for other tinyAVR MCUs

As already mentioned, several times: the sketches may have to be adapted depending on the ATtiny used. Here are a few examples:

  • The sketch runs unchanged on an ATtiny1604.
  • On an ATtiny402, for example, there is no PORTB. It worked with the following adjustments:
.....    
PORTA.DIRSET = PIN6_bm; // PA6 (= WO0 / ATtiny402) as output
PORTA.DIRCLR = PIN1_bm; // PA1 as input
.....
EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 as event generator
EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 as event user
.....
  • On the tinyAVR MCUs of series 2, major adjustments are necessary (see also Part 1 of the article). With these changes, it worked on an ATtiny3226:
.....
PORTA.DIRSET = PIN5_bm; // PA5 (0,WO0 / ATtiny3226) as output
PORTA.DIRCLR = PIN1_bm; // PA1 as input
.....
EVSYS.CHANNEL0 = EVSYS_CHANNEL0_PORTA_PIN1_gc; // PA1 as event generator
EVSYS.USERTCB0CAPT = EVSYS_USER_CHANNEL0_gc; // TCB0 as event user
.....

This should put you on the right track to adapt the following example sketches yourself. I will limit myself to the ATtiny1614.

Input Capture Time-Out Check Mode

The Time-Out Check Mode is controlled by the Event System. In this mode, an event signal starts the timer at 0 and the next signal stops it. If EDGE is set, the falling edge starts the Timer and the rising edge stops it. If EDGE is not set, the opposite applies. If the Timer Counter reaches CCMP, an interrupt is triggered and the Timer Counter continues to run.

We try this out as follows: a button on a suitable pin (here: PA1) serves as an Event Generator that starts TCB0 in Time-Out Mode. As long as the button is pressed, TCB0 counts up. CCMP and the timer clock are set so that CCMP is reached after one second. If this occurs, an interrupt is triggered and the ISR toggles an LED on another suitable pin (here: PA2). If the button is released before CCMP is reached, there is no interrupt and the board LED does not toggle. The next time the button is pressed, the Timer restarts at zero.

Under default conditions, CCMP is reached quickly. I have therefore slowed down Timer A and used it as the clock source for Timer B. And once again: Changes to the TCA0 and TCB0 settings can have side effects on millis() and Co.

Here is the sketch reduced to the essentials (tested on an ATtiny1614):

#define TCB_TIMEOUT_VALUE (19531) // = 1s with the settings below

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

void setup() {
    PORTA.DIRSET = PIN2_bm; // PA2 as output
    PORTA.DIRCLR = PIN1_bm; // PA1 as input
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm; // enable pull-up at PA1
        
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 is event user

    TCB0.CCMP = TCB_TIMEOUT_VALUE;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm; // slow down Timer A
    TCB0.CTRLB = TCB_CNTMODE_TIMEOUT_gc; // set time-out mode
    TCB0.INTCTRL = TCB_CAPT_bm; // enable capture interrupt
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // start counter on negative edge
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use timer A clock, enable TCB0
}

void loop(){}

With a 20 MHz system clock, a divider of 1024 and a CCMP of 19531, the counter reaches CCMP after 19531 * 1024 / 20000000 = ~1 second. If you keep the button pressed permanently, the counter continues to run and reaches CCMP every 65536 * 1024 / 20000000 = ~3.36 seconds.

To make it easier for you to keep track of which counter value you released the button at, I have an extended version of the sketch here:

#define TCB_TIMEOUT_VALUE (19531) // = 1s with the settings below
volatile bool keyReleased = false;

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

ISR(PORTA_PORT_vect){ // ISR for button release
    PORTA.INTFLAGS = PIN1_bm;
    keyReleased = true;
}

void setup() {
    Serial.begin(115200);
    PORTA.DIRSET = PIN2_bm; // PA2 as output
    PORTA.DIRCLR = PIN1_bm; // PA1 as input
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm | PORT_ISC_RISING_gc; // pull-up / interrupt
        
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 is event use

    TCB0.CCMP = TCB_TIMEOUT_VALUE;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm; // slow down Timer A
    TCB0.CTRLB = TCB_CNTMODE_TIMEOUT_gc; // enable compare/capture output, time-out mode
    TCB0.INTCTRL = TCB_CAPT_bm; // enable capture interrupt
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // start counter on negative edge
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use timer A clock, enable TCB0
}

void loop(){
    if(keyReleased){
        Serial.println(TCB0.CNT);
        keyReleased = false;
    }
}

If the value displayed on the serial monitor is less than 19351, the LED is not toggled.

Input Capture On Event Mode

Now, we come to the four Input Capture Modes that you can use to measure pulse widths and / or frequencies. For all examples, we use a PWM signal with a duty cycle of ~75% at PB1 as the object to be measured, which we generate on an ATtiny1614 with analogWrite(6, 192) (192 / 255 * 100 = ~75.29 %). PB1 is the Event Generator, Timer B is the Event User.

First, let’s look at the Input Capture On Event Mode. In this mode, the counter permanently counts from 0 to 65535, overflows and starts again from the beginning. RDepending on whether you have set the EDGE bit or not, the Timer writes the current Counter value to the CCMP register on a falling or rising edge and triggers an interrupt. We read the CCMP Register in the ISR of the Capture Interrupt. The difference to the last value is the period of the PWM signal in Timer Counter counting steps.

In the event of a Counter Overflow between two interrupts, the difference is negative, and we have to add 65636.

volatile long cnt = 0;
volatile long lastCnt = 0;

ISR(TCB0_INT_vect){
    cnt = TCB0.CCMP - lastCnt; // Read the period
    lastCnt = TCB0.CCMP;
    if(cnt < 0){   // period is neg. when TCB0 was overflowed
        cnt += 65536;
    }
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source 
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0; // Stop TCB0
    TCB0.CTRLB = TCB_CNTMODE_CAPT_gc;  // use input capture on event mode
    TCB0.EVCTRL =  TCB_CAPTEI_bm; // enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm;  // enable capture interrupt
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
    delay(10);
}

void loop(){
    Serial.println(cnt);
    delay(1000);
}

The result was 16320. analogWrite() is controlled via the Timer A clock, which uses the divider 64. Regardless of the duty cycle, a rising edge and a falling edge are generated every 255 counters (not 256!).

A brief digression: analogWrite() is generated by a PWM signal from Timer A in Split Mode. If you check the top value with Serial.println(TCA0.SPLIT.HPER) or Serial.println(TCA0.SPLIT.LPER), the result is 254 (= 255 steps). A analogWrite(x, 255) generates a digitalWrite(x, HIGH). This ensures that a duty cycle of 100 % can be created.

The PWM signal therefore has a frequency of 20 MHz / (64 * 255) = ~1225.5 Hz or a period of 0.000816 seconds. The clock of Timer B corresponds to the system clock. This means that 16320 counting steps correspond to a frequency of 20 MHz / 16320 = ~1225.5 Hz. So it fits perfectly!

Input Capture Frequency Measurement Mode

The Input Capture Frequency Measurement Mode basically does the same as the Input Capture On Event Mode, except that the Counter is reset to 0 after the event signal has been received. This has the advantage that CCMP provides us with the period directly, and we do not have to calculate differences.

The term “frequency measurement” is actually misleading. It should be called “period measurement”.

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_FRQ_gc; // enable input capture frequency mode
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // enable capture event input 
    TCB0.INTCTRL = TCB_CAPT_bm;  // enable capture event input
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
  
}

void loop(){
    Serial.print("Period in TCB0 counts: ");
    Serial.println(TCB0.CCMP + 1);
    float frq = 20000000.0 / ((TCB0.CCMP + 1) * 1.0);
    Serial.print("Frequency [Hz]: ");
    Serial.println(frq);
    delay(1000);
}

In this example sketch, I have integrated the calculation of the frequency into the sketch. It is interesting to note that you have to add 1 to the CCMP value to get the correct result. A CCMP value of 16319 is 16320 steps – that actually makes sense. The only thing that hasn’t really become clear, at least to me, is why you don’t have to add 1 when using the Input Capture On Event Mode.

Output timer_b_input_capture_frq.ino
Output timer_b_input_capture_frq.ino

Input Capture Pulse-Width Measurement Mode

The Input Capture Pulse-Width Measurement Mode allows you to measure the pulse width of signals. In contrast to Frequency Measurement Mode, in which only the rising or falling edge had an effect, in this mode the counter is “zeroed” on one edge and the input capture, i.e. writing the counter reading to CCMP, takes place on the other edge.

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_PW_gc; // enable pulse width measurement mode
    TCB0.EVCTRL = TCB_CAPTEI_bm; // enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm; 
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
    delay(10);
}

void loop(){
    Serial.print("High Period in TCB0 counts: ");
    unsigned int highCnt = (TCB0.CCMP + 1);
    Serial.println(highCnt);
    float pw = (highCnt * 1.0) / 20.0;  // cnt divided by 20 MHz
    Serial.print("Pulse width [µs]: ");
    Serial.println(pw);
    Serial.println();
    delay(1000);
}

As a result, I received:

Output timer_b_input_capture_pw.ino
Output timer_b_input_capture_pw.ino

This corresponds exactly to expectations. The PWM period comprised 16320 counters in 255 steps. 192 steps correspond to 16320 * 192 / 255 = 12288.

If you set the EDGE bit in the Event Control Register (line 12), the sketch evaluates the LOW phase.

Input Capture Frequency and Pulse-Width Measurement Mode

The Input Capture Frequency and Pulse-Width Measurement Mode combines frequency and pulse-width measurement. Of course, not both values can be saved in CCMP. Instead, the mode works as follows (if the EDGE bit is not set):

  • At the first positive edge (pulse begins): The counter is reset and starts.
  • Next negative edge (pulse ends): Input Capture, i.e. the counter reading is written to CCMP.
  • Second positive edge (period ends): Counter stops, an interrupt is triggered.

Since we know the period (from CCMP) and the pulse width (counter status at interrupt), we can calculate the duty cycle. And that’s precisely what the next sketch does:

volatile unsigned int period = 0;

ISR(TCB0_INT_vect){
    period = TCB0.CNT + 1; // Read the period
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_FRQPW_gc;
    TCB0.EVCTRL = TCB_CAPTEI_bm; // TCB_EDGE_bm | TCB_CAPTEI_bm;
    TCB0.INTCTRL = TCB_CAPT_bm;
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
}

void loop(){
    Serial.print("Period in TCB0 counts: ");
    Serial.println(period);
    Serial.print("High Period in TCB0 counts: ");
    Serial.println(TCB0.CCMP + 1);
    float frq = 20000000.0 / (period * 1.0);
    Serial.print("Frequency [Hz]: ");
    Serial.println(frq);
    float dc = (TCB0.CCMP + 1) * 1.0 / (period * 1.0) * 100.0;
    Serial.print("Duty Cycle [%]: ");
    Serial.println(dc);
    Serial.println();
    delay(1000);
}

Here is the result:

Output timer_b_input_capture_frq_and_pw.ino
Output timer_b_input_capture_frq_and_pw.ino

Configurable Custom Logic – CCL Module

The Configurable Custom Logic Module CCL gives the tinyAVR MCUs capabilities that you would otherwise have to use external logic ICs for. But it goes far beyond that.

The CCL module is organized in so-called Look-Up Tables (LUTs). Each LUT has three inputs and one output.

Register of the CCL Module

I will only give a rough overview of this module. Here is the register overview:

Register Summary Module CCL, example ATtiny1614
Register Summary Module CCL, example ATtiny1614

Each LUT has its own TRUTH register. This is where you define which input level combination is true. Example: For LUT0, we define that LUT0-IN2 = HIGH, LUT0-IN1 = LOW and LUT0-IN0 = LOW should be the “true” state. I.e: HIGH/LOW/LOW ⇒ 0b100 = 4. This in turn means that bit no. 4 must be set in TRUTH0 (and not that TRUTH0 = 4!).

Now comes the ingenious part: The inputs do not have to be just I/O pins, but can also be Events, Timers, USARTs or the output of another LUT, for example. So if certain states of different modules in your program require an action, you can save yourself the trouble of querying these states and some if and switch combinations with the CCL. You define the inputs via the Input Select Bitgroups (INSELn[3:0]).

If the “true” state occurs, you can be notified by an interrupt. Alternatively or additionally, you can set the OUTEN bit so that the corresponding output goes from LOW to HIGH when “true”.

You can combine 2 LUTs using the SEQSELn[3:0] bit groups.

Example Sketch for the CCL Module

As a simple example, we check the status of the I/O pins assigned to LUT0. The following applies to the ATtiny1614 (and many others):

  • LUT0-IN2 = PA2
  • LUT0-IN1 = PA1
  • LUT0-IN0 = PA0
  • LUT0-OUT = PA4

Our true condition should be: 2LUT0-IN2 = HIGH, LUT0-IN1 = LOW and LUT0-IN0 = LOW. We pull the inputs to HIGH with the internal pull-up resistors. Pressing the button should bring them to LOW. If the buttons on LUT0-IN1 and LUT0-IN0 are pressed, the true condition is fulfilled and the LUT0-OUT output goes HIGH. This is indicated by an LED. This is the circuit:

Circuit for ccl_basic.ino, example ATtiny1614
Circuit for ccl_basic.ino using ATtiny1614 as an example

And here is the corresponding sketch:

void setup() {
    PORTA.DIRCLR = PIN2_bm | PIN1_bm | PIN0_bm; // PA0 = LUT0-IN0 as input, etc.
    //PORTA.DIRSET = PIN4_bm; // not necessary!     
    PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // PULLUP for PA0 = LUT0-IN0
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm; // PULLUP for PA1 = LUT0-IN1
    PORTA.PIN2CTRL = PORT_PULLUPEN_bm; // PULLUP for PA2 = LUT0-IN2
    CCL.LUT0CTRLB = CCL_INSEL0_IO_gc | CCL_INSEL1_IO_gc; // select I/O as input
    CCL.LUT0CTRLC = CCL_INSEL2_IO_gc; // select I/O as Input
    CCL.TRUTH0 = 0x10; // LUT2-IN2 = HIGH, LUT1-IN2 = LOW, LUT0-IN2 = LOW ==> TRUE
    CCL.LUT0CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm; // OUT Enable / LUT0 enable
    CCL.CTRLA = CCL_ENABLE_bm; // CCL_ENABLE
}

void loop(){}

The status check and the output bypass the CPU. This is extremely resource-efficient.

The megaTinyCore board package has implemented its own library called Logic for the CCL module. Perhaps this is easier for some than register programming. You will also find some example sketches as part of the board package.

The tinyAVR MCUs go to sleep – SLPCTRL Module

Going to sleep – I think this is a good topic for the last chapter!

Register Settings for SLPCTRL

he SLPCTRL module only has the CTRLA register, in which you select the Sleep mode (SMODE[1:0]) and enable the SLPCTRL module using the SEN bit. However, setting the SEN bit alone does not put the microcontroller to sleep. To do this, use sleep_cpu() from avr/sleep.h.

tinyAVR Register: SLPCTRL.CTRLA
Register: SLPCTRL.CTRLA

You can select from the following options for Bit Group Configurations for SMODE[1:0]:

  • IDLE: Idle – the lightest sleep; everything is active except the CPU.
  • STDBY: Stand-by – the most flexible setting.
  • PDOWN: Power Down – deep sleep.

The deeper the sleep, the lower the power consumption and the more limited the wake-up methods. Which peripherals are available in which Sleep Mode can be found in the SLPCTRL section of the data sheet. Stand-by mode is the most flexible. Many modules have a RUNSTBY bit that determines whether the module should be available in standby mode or not.

Example sketch for SLPCTRL

In the following example we send the tinyAVR into power down mode and wake it up after one second using the Periodic Timer Interrupt (PIT).

 #include<avr/sleep.h>

ISR(RTC_PIT_vect){
    RTC.PITINTFLAGS = 1;
}

void setup(){
    Serial.begin(115200);
    RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;
    RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc | RTC_PITEN_bm;
    RTC.PITINTCTRL = RTC_PI_bm;   
    SLPCTRL.CTRLA = SLPCTRL_SMODE_PDOWN_gc | SLPCTRL_SEN_bm;  
}

void loop(){
    Serial.println("Going to sleep...");
    Serial.flush();
    sleep_cpu();
}

Leave a Reply

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