cmicrocontrolleravratmelstudioattiny

Why can't I change the PWM output pin in ATTINY1616?


I have started to dive into MCU programming and have been working with some basic codes. I first did it with Arduino but now I'm trying with an ATTINY1616 MCU and I'm hardstuck in how to create a PWM effect that dims a LED on and off. I thought I had a working code but for some reason it will work with one and only one pin. I have read the datasheet and it confirms there there are more pins available for PWM and find no command or statement that I could be missing to use other pins.

The code will compile regardless of the pin that I select as output but in the MCU, PB0 is the only one that will work. Other pins won’t do anything, don’t give any output and the software (Microship Studio) doesn’t show any errors. I have even swapped the MCU for new ones.

I’m not very good at reading datasheets but I really don’t see any commands that specifically allow some pins to be PWM (https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny1614-16-17-DataSheet-DS40002204A.pdf, specifically section 20 of TCA). My theory is that since PB0 is shown as being WO0 for TCA0, this pin is like the default for the TCA function and I need a command to indicate that I want to use another but I can’t find anything that works.

#define F_CPU 20000000
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdint.h>



void PWM_init() {
    // Configure Timer/Counter 0 (TC0) for PWM generation

    // Set TC0 waveform generation mode to Fast PWM
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_SINGLESLOPE_gc | TCA_SINGLE_CMP0EN_bm;

    // Set TC0 period (TOP value) to 8-bit resolution
    TCA0.SINGLE.PER = 0xFF;

    // Set TC0 PWM output on PB0 (LED pin)  
    PORTB.DIRSET = PIN0_bm;
    //PORTA.DIRSET = PIN4_bm; <------Pin PA4. It won't work even if PB0 is removed

    TCA0.SINGLE.CTRLA = TCA_SINGLE_ENABLE_bm;
}

void PWM_control() {
    // Define the number of steps and the delay between each step
    const uint16_t numSteps = 256;
    const uint16_t delayMs = 10;

    // Fade in
    for (uint16_t step = 0; step < numSteps; step++) {
        // Set ADC value based on the step
        uint16_t adcValue = step;

        // Scale the ADC value to PWM range (0-255)
        uint8_t pwmValue = adcValue >> 2; // Right shift by 2 (divide by 4)

        // Update PWM duty cycle
        TCA0.SINGLE.CMP0 = pwmValue;

        // Delay between each step
        _delay_ms(delayMs);
    }
    // Fade out
    for (uint16_t step = 256; step > 0; step--) {
        // Set ADC value based on the step
        uint16_t adcValue = step;

        // Scale the ADC value to PWM range (0-255)
        uint8_t pwmValue = adcValue >> 2; // Right shift by 2 (divide by 4)

        // Update PWM duty cycle
        TCA0.SINGLE.CMP0 = pwmValue;

        // Delay between each step
        _delay_ms(delayMs);
    }
}

int main() {
    // Initialize PWM
    PWM_init();
    CPU_CCP = 0xD8; //enable write to protected register;
    CLKCTRL_MCLKCTRLB = 0; // No prescaling to periphery, therefore 20MHz frequency;

    while (1) {
        // Control PWM
        PWM_control();

        _delay_ms(10);
    }

    return 0;
}

Have tried:

E: Changes made for solution in PWM set up

    TCA0.SPLIT.CTRLA = TCA_SINGLE_ENABLE_bp;
    //SPLIT MODE INIT
    TCA0.SPLIT.CTRLD = TCA_SINGLE_SPLITM_bm;
    TCA0.SPLIT.HPER = 0xFF;
    TCA0.SPLIT.CTRLA = TCA_SINGLE_ENABLE_bm;
    TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_HCMP1EN_bm | TCA_SPLIT_HCMP2EN_bm;

In the control, I just changed TCA0.SINGLE.CMP0 for TCA0.SPLIT.HCMP1 for pin PA4.


Solution

  • Yes, each PWM peripheral is associated with its own specific pins, as noted in the datasheet. Peripherals in the ATtiny are multiplexed with regular general-purpose input-output (GPIO) pins, meaning that if you're not using the peripheral you can use its pin as a GPIO. The reverse is not true: you cannot simply connect any peripheral to any pin as, fundamentally, those wires do not exist. Every (non-power) pin is a GPIO because this is a very useful functionality to have, but connecting single every peripheral to every single pin would require a very complex network of intertwined wires on the silicon die!

    You can find a table summarizing the multiplexing in section 5.1 of the datasheet (page 18). You are using TCA0.WO0 which is multiplexed with PB0. I draw your attention to note 3, though: there are alternate pin positions which can be selected through the port multiplexer. Specifically, you can also access TCA0.WO0 through PB3. You're also not limited to WO0; provided you set up the timer peripheral properly, you can start a free-running PWM on any pin listed under the TCA0 heading.

    Fundamentally, PWM is implemented in a memory-mapped peripheral, outside of the main program loop. Specifically, it's a timer peripheral with a set of compare registers: the timer constantly counts, and once the count is equal to the contents of the compare register, something happens. In the case of PWM, a pin goes low. (It goes high when the counter rolls over to zero.)

    To initialize the PWM in the way you want to use it, you write to various memory address (hence the term "memory mapped"). You are doing this in your PWM_init() function. Once you say "go" (by setting the CTRLA.ENABLE bit), the peripheral runs by itself, forever.

    To see in more detail how the peripheral works, you need to go to Chapter 20 in the datasheet. From that section, I see that this line of code:

     TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_SINGLESLOPE_gc | TCA_SINGLE_CMP0EN_bm;
    

    is writing values into the CTRLB register (see page 190). You have toggled the CMP0EN flag, enabling the compare function on the pin corresponding to the output WO0. As such, the PWM takes over the function of PB0 and uses it as its WO0 instead. (You are also setting the PWM into 'single slope' mode; see page 182 of the datasheet for details.)

    In your PWM_control() function, you then write into the CMP0 register:

            // Update PWM duty cycle
            TCA0.SINGLE.CMP0 = pwmValue;
    

    which allows you to set the duty cycle. Fundamentally, your PWM output is toggling high whenever the timer rolls over to zero, and back low again once the timer equals the contents of the compare register. Here is the relevant timing diagram from the datasheet (page 183):

    Timing diagram of count and compare registers.

    If instead you want to see the PWM output on a different pin, e.g. PB1, you would first set PB1 as an output, and then set the CMP1EN enable bit in the CTRLB register. (Presumably your IDE has TCA_SINGLE_CMP1EN_bm defined, but it may not; if so, simply edit that line appropriately.) You could also read the section on the port multiplexer (chapter 15) which gives you access to a register to enable the alternate output for WO0, which would put the PWM signal on PB3.

    Note, however, that while your desired pin PA4 can produce a PWM signal, it can only do so in split mode which requires a different setup for your PWM peripheral. You could make this work but you'll need to dig into Chapter 20 to figure out how to set up the PWM peripheral in split mode.

    In summary, you can fairly easily change the PWM to output on pins PB1 or PB2: all you have to do is use their matching CMPn registers, and write the appropriate CMPnEN bit in CTRL2. With a bit more complexity, you can write to the PORTMUX.CTRLC register, and route the PWM to PB3, PB4, or PB5. With even more effort, you can enable split mode and get PWM on PA3, PA4 or PA5, or by doing split mode AND writing to the port multiplexer, you could get PWM on PC3, PC4 and PC5 (but the latter two only if you're using the VQFN package that has these pins).

    You could also try using the TCB or TCD peripherals, which also generate waveforms; with appropriate code, this may allow you get PWM working on PC0 or PC1.

    It is impossible to put PWM on any other pin.