[TUT][SOFT] AVR 0/1-series Setting up TCA0 for split-mode PWM

Go To Last Post
8 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

In this example, I will show how to setup the timer/counter type A to generate PWM waveforms and output them to I/O pins. I used a tiny1616 here, but the process is similar for other chips of the AVR 0/1 series.

 

The first step is to setup the system clock, in this case I will use a 10x divider. Given the default oscillator frequency of 20MHz (set by a fuse), this will result in a system clock of 2MHz.

Note that the clock registers are protected, so to write them, the _PROTECTED_WRITE macro found in the <avr/xmega.h> header file should be used.

_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_10X_gc | CLKCTRL_PEN_bm);

 

Now to setup Timer A itself. In split mode, timer A, which by default is a 16 bit timer, behaves as 2 independent 8 bit timers, H and L, each with 3 output channels.

Each timer can have a different period, but in this example I will set the same for both, and instead show how a different phase can be set for each timer.

// Configure timer A0 to generate waveforms
// In split mode, the type A timer can be used as 2x 8 bit timers, "high" (H) and "low" (L)
// Each timer has 3 channels, here we will use Ch0 from both timers
TCA0.SINGLE.CTRLD = TCA_SINGLE_SPLITM_bm;				// enable split mode
TCA0.SPLIT.HPER = 99;							// set 100 counts period on H
TCA0.SPLIT.LPER = 99;							// and L timers
TCA0.SPLIT.HCMP0 = 50;							// set 50% duty cycle on H timer Ch0
TCA0.SPLIT.LCMP0 = 25;							// set 25% duty cycle on L timer Ch0
TCA0.SPLIT.HCNT = 75;							// phase = 90 deg. on H timer
TCA0.SPLIT.LCNT = 100;							// phase = 0 deg. on L timer
TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_LCMP0EN_bm;		// enable PWM outputs on Ch0 of H and L timers
TCA0.SPLIT.CTRLA = TCA_SPLIT_ENABLE_bm;					// start timer

The period is set by loading HPER/LPER with (period-1), so in this case the period will be 100 timer cycles. Since the timer is using the system clock, the resulting waveforms will have a frequency of 2MHz/100 = 20kHz.

 

In split mode, the timers always count from top to bottom then wrap around. The output of a timer channel is LOW while xCNT >= xCMPn (n is the channel), HIGH otherwise.

The duty cycle is chosen by setting xCMPn. I chose a period of 100 so that the values of xCMPn translate directly to percentage duty cycles.

So, HCMP0 = 50 means that channel 0 of timer H will have 50% duty cycle and TCA0.SPLIT.LCMP0 = 25 means channel 0 of timer L will have 25% duty cycle.

 

The phase can be chosen by setting initial values on the count registers, while the timer is stopped.

Setting LCNT = 100 means the phase for timer L will be zero degrees. A value of 200 or 0 would give the same result (i.e., a multiple of the period).

Setting HCNT = 75 means the phase for timer H will be leading timer L by 90º, because the timer is counting down. Timer H will reach zero and wraparound 25 timer cycles (i.e. 1/4 period or 90º) before timer L.

 

CTRLB is used to enable timer outputs on their respective ports. L timer channels are present in WO0 to WO2, while H channels are sent to WO3 to WO5. Here we enable only the channels zero of both timers (WO0 and WO3).

Finally, CTRLA enables the timer.

 

However, this procedure is not enough to actually get the PWM outputs. The last step is to set the ports corresponding to WO0 and WO3 as outputs.

Looking at the "Multiplexing and considerations" chapter of the tiny1616 datasheet, we can see that the ports are B0 for WO0 and A3 for WO3.

This is the code to enable them:

PORTB.DIRSET = 1 << 0;
PORTA.DIRSET = 1 << 3;

 

Putting it all together, this is the complete code:

#include <avr/io.h>

inline void clock_init(void);
inline void timer_init(void);

int main(void) {
    clock_init();
    timer_init();
    while (1);
}

/*
    Set 10x divider for main clock. This results in a 2 MHz clock.
 */
void clock_init(void) {
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_10X_gc | CLKCTRL_PEN_bm);
}

/*
    This function uses timer type A to generate 2 waves, out of phase by 90 deg.
    The resulting frequency for these settings is 20kHz (main clock /100)
 */
void timer_init(void) {
    // Configure timer A0 to generate waveforms
    // In split mode, the type A timer can be used as 2x 8 bit timers, "high" (H) and "low" (L)
    // Each timer has 3 channels, here we will use Ch0 from both timers
    TCA0.SINGLE.CTRLD = TCA_SINGLE_SPLITM_bm;                           // enable split mode
    TCA0.SPLIT.HPER = 99;                                               // set 100 counts period on H
    TCA0.SPLIT.LPER = 99;                                               // and L timers
    TCA0.SPLIT.HCMP0 = 50;                                              // set 50% duty cycle on H timer Ch0
    TCA0.SPLIT.LCMP0 = 25;                                              // set 25% duty cycle on L timer Ch0
    TCA0.SPLIT.HCNT = 75;                                               // phase = 90 deg. on H timer
    TCA0.SPLIT.LCNT = 100;                                              // phase = 0 deg. on L timer
    TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_LCMP0EN_bm;     // enable PWM outputs on Ch0 of H and L timers
    TCA0.SPLIT.CTRLA = TCA_SPLIT_ENABLE_bm;                             // start timer
    // Set PWM pins as outputs; PB0 = L timer Ch0 (WO0), PA3 = H timer Ch0 (WO3)
    PORTB.DIRSET = 1 << 0;
    PORTA.DIRSET = 1 << 3;
}

 

These are the resulting PWM waves (top - timer H, bottom - timer L)

 

 

I recommend changing the period, duty and phase to see the effects on the generated waveforms.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Very useful - thanks.  The new ATTINYs are great value for money but you have to forget years of ATTINY85 coding to use them.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi El Tangas

 

If you want a topic for your next example, how about the whole putting the processor to sleep to save power and waking it up again with the timers and RTC.  There are several glaring errors in the datasheets (e.g. table 11.2 saying TCB works in Sleep mode and 21.3.6 saying it doesn't - which I believe is correct as the processor clock is usually off) so an example showing what works best would help a lot of people I expect.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

El Tangas, Great work! Thanks a lot.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Yeah, that's a good suggestion. I noticed that TCB works in sleep mode if the oscillator is also working, but sleep modes are a more complex subject. It needs more testing.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have used split mode to generate 6 PWMs at the same time on the attiny1614.

 

You can't set the period independently (there are only two timers in split mode) - but you can set the duty cycle independently, because there are three 16-bit comparators, which work as 6x 8-bit comparators in split mode. Job done, 6 PWM duty-cycle outputs.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

markxr wrote:

I have used split mode to generate 6 PWMs at the same time on the attiny1614.

 

You can't set the period independently (there are only two timers in split mode) - but you can set the duty cycle independently, because there are three 16-bit comparators, which work as 6x 8-bit comparators in split mode. Job done, 6 PWM duty-cycle outputs.

 

Very good, would you like to contribute to this thread and post some code.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Ok,

 

Generating 6x independent PWM duty cycles on the attiny1614:

 

1. Set up timer-counter A in the split mode:

 

uint8_t period_val = 200;
        // Enable split mode
        TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm;
        // Enable all comparators in split mode
        TCA0.SPLIT.CTRLB =
                TCA_SPLIT_LCMP0EN_bm |
                TCA_SPLIT_LCMP1EN_bm |
                TCA_SPLIT_LCMP2EN_bm |
                TCA_SPLIT_HCMP0EN_bm |
                TCA_SPLIT_HCMP1EN_bm |
                TCA_SPLIT_HCMP2EN_bm;
        // Set the period - use the same period for both halves.
        TCA0.SPLIT.HPER = period_val;
        TCA0.SPLIT.LPER = period_val;

2. Enable the pins for output - I am using PB0,1,2 and PA3,4,5 - which is the default; most of them can't be changed (I think there is maybe 1 alternative)

 

        PORTB.DIRSET = 1 << 0;
        PORTB.DIRSET = 1 << 1;
        PORTB.DIRSET = 1 << 2;
        PORTA.DIRSET = 1 << 3;
        PORTA.DIRSET = 1 << 4;
        PORTA.DIRSET = 1 << 5;

3. Set the clock divider and start the timer (default cpu clock is 20Mhz, default perhipheral clock is 20/6 =~ 3.33mhz, I'm using the defaults)

 

    // Divide by 64, 3.333mhz / 64 =~ 52khz
    // 52khz / period_val =~ 260hz = PWM frequency.
        TCA0.SPLIT.CTRLA =
                TCA_SPLIT_CLKSEL_DIV64_gc | // divide sys. clock by 64
                TCA_SPLIT_ENABLE_bm;

        // To set the PWM duty, we can now write to
        // TCA0.HCMP0, .HCMP1, .HCMP2, .LCMP0, .LCMP1, .LCMP2
        // these would have values of 0.. period_val
        // here are sample values
        // TCA0.SPLIT.LCMP0 = 80; // WO0 / PB0
        // TCA0.SPLIT.LCMP1 = 100; // WO1 / PB1
        // TCA0.SPLIT.LCMP2 = 120; // WO2 / PB2
        // TCA0.SPLIT.HCMP0 = 20; // WO3 / pin PA3
        // TCA0.SPLIT.HCMP1 = 40; // WO4 / PA4
        // TCA0.SPLIT.HCMP2 = 60; // W05 / PA5

 

I am generating a PWM of approx 260hz, the duty values can be set between 0 and 200, which means we can adjust them by 0.5% duty resolution.

 

The 6 pins all operate at the same frequency, but their duty can be controlled individually.

 

I'm not completely sure what happens if you set the duty part way through a pulse, I suppose the output is a bit inconsistent for one pulse. I don't think it matters for my application (controlling motors)

 

 

Last Edited: Thu. Mar 21, 2019 - 02:57 PM