Attiny1604 Help With PWM

I am trying to build a project using an ATtiny1604.

I need to use PWM (to control a servo) and TWI (to talk to a transducer) and Serial (for debug oputput and command entry) but the default pin assignments clash.

Looking at data sheet, there are no alternative pin assignments for TWI SCL or SDA on my device so that prevents my using TCA0 PWM signal output on WO0 and WO1.

I am using Serial.swap(); just before the Serial.begin(); statement so the alternate pins for TxD, RxD (PA1, PA2). Thanks to a solution with using alt pins for Serial by https://forum.arduino.cc/u/hmeijdam

This means, I may use WO2 for the PWM output from TCA0.

(I tried using TCB0 but the pre-scaler only gives you a very limited option and the servo needs a PWM signal with a 50Hz frequency.)

I am struggling to achieve a waveform output on W02 / PB2 physical pin 7.

In the code snippet, I initially have LED and PWR toggle real slow in setup() so I can see them, thus proving I have set up these pins successfully as output pins.

I use toggle LED in loop() just to prove the code is running.

Although I see PWM flash in setup(), when I initialise the TCA0 to generate a fast single slope PWM signal. nothing appears at PB2, physical pin 7.

Please could you suggest how I should change my PWM_0_TCA0_init() routine to successfully set up the PWM signal to PB2, physical pin 7?

I'm not too fussed about the actual pin but I need PA1, PA2 for alt serial and PB1, PB0 for TWI

// ATtiny1604_PWM_Help

// Target ATtiny1604

#include <Arduino.h>
//#include <Wire.h>           //I2C Arduino Library

/* ATtiny1604 / ARDUINO Pins
 *                          _____
 *                  VDD   1|*    |14  GND
 * (nSS)  (AIN4) PA4  0~  2|     |13  10~ PA3 (AIN3)(SCK)(EXTCLK)
 *        (AIN5) PA5  1~  3|     |12  9   PA2 (AIN2)(MISO)
 * (DAC)  (AIN6) PA6  2   4|     |11  8   PA1 (AIN1)(MOSI)
 *        (AIN7) PA7  3   5|     |10  11  PA0 (nRESET/UPDI)
 * (RXD) (TOSC1) PB3  4   6|     |9   7~  PB0 (AIN11)(SCL)
 * (TXD) (TOSC2) PB2  5~  7|_____|8   6~  PB1 (AIN10)(SDA)
*/

/* Alternate pins 
Pin Name  Other     ADC0  AC0   USART0  SPIO  TWI0  TCA0  TCB0  CCL

 1  VDD
 2  PA4             AIN4        XDIR(*) SS          WO4         LUT0-OUT
 3  PA5             AIN5  OUT                       WO5   WO
 4  PA6             AIN6  AINN0
 5  PA7             AIN7  AINP0                                 LUT1-OUT
 6  PB3                         RxD                 WO0(*)
 7  PB2   EVOUT1                TxD                 WO2
 8  PB1             AIN10       XCK           SDA   WO1
 9  PB0             AIN11       XDIR          SCL   WO0
 10 PA0 RESET/UPDI  AIN0                                        LUT0-IN0
 11 PA1             AIN1        TxD(*)  MOSI                    LUT0-IN1
 12 PA2 EVOUT0      AIN2        RxD(*)  MISO                    LUT0-IN2
 13 PA3 EXTCLK      AIN3        XCK(*)  SCK         WO3
 14 GND
*/

#define LED_bp 4   	// PA4 pin 2
#define PWM_bp 2 	// PB2 pin 7

#define Serial_BAUD 19200			

// put function declarations here:
void system_init();
void PWM_0_TCA0_init();

void setup() {
	// put your setup code here, to run once:
	system_init();
	Serial.swap();
	Serial.begin(Serial_BAUD);
	Serial.println(F("\r\nStart"));
	
	
	// prove that LED is configured correctly as output
	PORTA_OUT ^= (1<<LED_bp);
	delay(600);
	PORTA_OUT ^= (1<<LED_bp);
	delay(600);
	
	// prove that PWM is configured correctly as output
	PORTB_OUT ^= (1<<PWM_bp);
	delay(600);
	PORTB_OUT ^= (1<<PWM_bp);
	delay(600);
	
	PWM_0_TCA0_init();	// configure and enable PWM waveform - WO2 / PB2 / PWM_bp remains at 0v
}

void loop() {
	PORTA_OUT ^= (1<<LED_bp);	// to prove loop is running
}

// put function definitions here:

/* Interrupt Service Routines */

/*	Helper routines - to make the code more readable */
// Port A
void disablePortAPullup(uint8_t pin){
	uint8_t *port_pin_ctrl = ((uint8_t *)&PORTA + 0x10 + pin);	// calculate port PINCTRL address for pin
	*port_pin_ctrl &= ~(1 << PORT_PULLUPEN_bp);									// disable pull up
}

// Port B
void disablePortBPullup(uint8_t pin){
	uint8_t *port_pin_ctrl = ((uint8_t *)&PORTB + 0x10 + pin);	// calculate port PINCTRL address for pin
	*port_pin_ctrl &= ~(1 << PORT_PULLUPEN_bp);									// disable pull up
}

/* Initialization routines */
void mcu_init()							/* MCU initialization */
{
	/* On AVR devices all peripherals are enable from power on reset, this
	 * disables all peripherals to save power. Driver shall enable
	 * peripheral if used */

	/* Set all PORT A pins to low power mode and input disabled */
	for (uint8_t i = 0; i < 8; i++) {                                   // PA0 - PA7
		uint8_t *port_pin_ctrl = ((uint8_t *)&PORTA + 0x10 + i);				
		*port_pin_ctrl |= (1 << PORT_PULLUPEN_bp); 										// define pull up 
		*port_pin_ctrl = (*port_pin_ctrl & ~PORT_ISC_gm) | PORT_ISC_INTDISABLE_gc; 	// disable input sensing
	}

    /* Set all PORT B pins to low power mode and input disabled */
	for (uint8_t i = 0; i < 4; i++) {                                   // PB0 - PB3 on ATtiny1604
		uint8_t *port_pin_ctrl = ((uint8_t *)&PORTB + 0x10 + i);				
		*port_pin_ctrl |= (1 << PORT_PULLUPEN_bp); 										// define pull up 
		*port_pin_ctrl = (*port_pin_ctrl & ~PORT_ISC_gm) | PORT_ISC_INTDISABLE_gc; 	// disable input sensing
	}
}

void pin_init(){ 						              // PIN initialization 
	/* See <https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/Ref_DirectPortManipulation.md> */
	
  	// PORT A
	PORTA_DIRSET = (1<<LED_bp);		// set output pins
	disablePortAPullup(LED_bp);		// no need for a pull up on output pin
	PORTB_OUT &= ~(1<<LED_bp);		// initially LOW
  	  	
	// PORT B
	PORTB_DIRSET = (1<<PWM_bp);		// set as output
	disablePortBPullup(1<<PWM_bp);	// no need for a pull up on output pin
	PORTA_OUT &= ~(1<<PWM_bp);		// initially LOW
	
 }

void CPUINT_init(){						            // Interrupt initialisation
	/* Enable interrupts */
	sei();
}

void PWM_0_TCB0_init(){

	TCB0.CCMPL = 0xff; /* PWM Period: 0xff */

	TCB0.CCMPH = 0x64; /* PWM Compare: 0x64 */

	// TCB0.CNT = 0x0; /* Count: 0x0 */

	TCB0.CTRLB = 0 << TCB_ASYNC_bp      /* Asynchronous Enable: disabled */
	             | 1 << TCB_CCMPEN_bp   /* Pin Output Enable: enabled */
	             | 1 << TCB_CCMPINIT_bp /* Pin Initial State: disabled */
	             | TCB_CNTMODE_PWM8_gc; /* 8-bit PWM */

	// TCB0.DBGCTRL = 0 << TCB_DBGRUN_bp; /* Debug Run: disabled */

	// TCB0.EVCTRL = 0 << TCB_CAPTEI_bp /* Event Input Enable: disabled */
	//		 | 0 << TCB_EDGE_bp /* Event Edge: disabled */
	//		 | 0 << TCB_FILTER_bp; /* Input Capture Noise Cancellation Filter: disabled */

	TCB0.INTCTRL = 1 << TCB_CAPT_bp /* Setting: enabled */;

	TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc  /* CLK_PER (No Prescaling) */
	             | 1 << TCB_ENABLE_bp   /* Enable: enabled */
	             | 0 << TCB_RUNSTDBY_bp /* Run Standby: enabled */
	             | 0 << TCB_SYNCUPD_bp; /* Synchronize Update: disabled */
}

void PWM_0_TCA0_init(){

	 //TCA0.SINGLE.CMP0 = 400; /* Setting: 0x0 */

	// TCA0.SINGLE.CMP1 = 0x0; /* Setting: 0x0 */

	TCA0.SINGLE.CMP2 = 0x2ff;  // I think this defines the mark space ratio 

	// TCA0.SINGLE.CNT = 0x0; /* Count: 0x0 */

	TCA0.SINGLE.CTRLB = 0 << TCA_SINGLE_ALUPD_bp            /* Auto Lock Update: disabled */
	                    | 0 << TCA_SINGLE_CMP0EN_bp         /* Setting: disabled */
	                    | 0 << TCA_SINGLE_CMP1EN_bp         /* Setting: disabled */
	                    | 1 << TCA_SINGLE_CMP2EN_bp         /* Setting: enabled for WO2*/
	                    | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; /*  */

	TCA0.SINGLE.CTRLC = 0 << TCA_SINGLE_CMP0OV_bp    /* Setting: disabled */
	                    | 0 << TCA_SINGLE_CMP1OV_bp  /* Setting: disabled */
	                    | 1 << TCA_SINGLE_CMP2OV_bp; /* Setting: enabled */

	// TCA0.SINGLE.DBGCTRL = 0 << TCA_SINGLE_DBGRUN_bp; /* Debug Run: disabled */

	// TCA0.SINGLE.EVCTRL = 0 << TCA_SINGLE_CNTEI_bp /* Count on Event Input: disabled */
	//		 | TCA_SINGLE_EVACT_POSEDGE_gc /* Count on positive edge event */;

	// TCA0.SINGLE.INTCTRL = 0 << TCA_SINGLE_CMP0_bp /* Compare 0 Interrupt: disabled */
	//		 | 0 << TCA_SINGLE_CMP1_bp /* Compare 1 Interrupt: disabled */
	//		 | 0 << TCA_SINGLE_CMP2_bp /* Compare 2 Interrupt: disabled */
	//		 | 0 << TCA_SINGLE_OVF_bp; /* Overflow Interrupt Enable: disabled */

	TCA0.SINGLE.PER = 0xffff; // This helps define the frequency taken with the prescaler TCA_SINGLE_CLKSEL_DIV256_gc

	TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV256_gc /* System Clock */
	                    | 1 << TCA_SINGLE_ENABLE_bp /* Module Enable: enabled */;

}

void system_init(){							/* system initialization */
	mcu_init();
	pin_init();
	CPUINT_init();
	//PWM_0_TCB0_init();					// don't think I can use this as setting the frequency to 50Hz would be a challenge
	//PWM_0_TCA0_init();					// now called at the end of set up after the test pin flashes
}


Is there a particular reason why you do not want to use the servo library that's part of MegatinyCore to control your servo?

This will give you a 50Hz servo sweeper on PB2 driven by TCA0 generated PWM

/* ATtiny1604 / ARDUINO Pins
                            _____
                    VDD   1|*    |14  GND
   (nSS)  (AIN4) PA4  0~  2|     |13  10~ PA3 (AIN3)(SCK)(EXTCLK)
          (AIN5) PA5  1~  3|     |12  9   PA2 (AIN2)(MISO)
   (DAC)  (AIN6) PA6  2   4|     |11  8   PA1 (AIN1)(MOSI)
          (AIN7) PA7  3   5|     |10  11  PA0 (nRESET/UPDI)
   (RXD) (TOSC1) PB3  4   6|     |9   7~  PB0 (AIN11)(SCL)
   (TXD) (TOSC2) PB2  5~  7|_____|8   6~  PB1 (AIN10)(SDA)
*/
void setup() {
  VPORTB_DIR |= PIN2_bm; // PB2 as output
  // Configure timer A
  takeOverTCA0(); // undo the core timer initializations
  PORTMUX_CTRLC |= PORTMUX_TCA02_DEFAULT_gc; //Port Multiplexer TCA0 Output 2 (PB2)
  //PORTMUX_CTRLC |= PORTMUX_TCA02_ALTERNATE_gc; //Port Multiplexer TCA0 Output 2 (PB5, not on 14 pin parts)
  TCA0_SINGLE_CTRLB |= TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // enable WO2 PWM output | select waveform generation mode
  TCA0_SINGLE_PER = F_CPU / 400; // Count up to this value as TOP (max 0xFFFF (65535)) // 50Hz servo pulse frequency
  TCA0_SINGLE_CTRLA |= TCA_SINGLE_CLKSEL_DIV8_gc | TCA_SINGLE_ENABLE_bm;  //Select division factor (1,2,4,8,16,64,256,1024) | enable timer A
}
void loop() {
  uint16_t pos;
  for (pos = 1000; pos <= 2000; pos += 1) { // goes from 1000 - 2000 us pulsewidth
    TCA0_SINGLE_CMP2BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
    delay(3);                       // waits 3ms for the servo to reach the position
  }
  for (pos = 2000; pos >= 1000; pos -= 1) { // goes from 2000 - 1000 us pulsewidth
    TCA0_SINGLE_CMP2BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
    delay(1);                       // waits 1ms for the servo to reach the position
  }
}

You can select any WO output you want by changing which compare you use

eg below will output on the alternate WO0 pin (PB3)

void setup() {
  VPORTB_DIR |= PIN3_bm; // PB3 as output
  // Configure timer A
  takeOverTCA0(); // undo the core timer initializations
  PORTMUX_CTRLC |= PORTMUX_TCA00_ALTERNATE_gc; //Port Multiplexer TCA0 Output 0 (PB3)
  TCA0_SINGLE_CTRLB |= TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // enable WO2 PWM output | select waveform generation mode
  TCA0_SINGLE_PER = F_CPU / 400; // Count up to this value as TOP (max 0xFFFF (65535)) // 50Hz servo pulse frequency
  TCA0_SINGLE_CTRLA |= TCA_SINGLE_CLKSEL_DIV8_gc | TCA_SINGLE_ENABLE_bm;  //Select division factor (1,2,4,8,16,64,256,1024) | enable timer A
}
void loop() {
  uint16_t pos;
  for (pos = 1000; pos <= 2000; pos += 1) { // goes from 1000 - 2000 us pulsewidth
    TCA0_SINGLE_CMP0BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
    delay(3);                       // waits 3ms for the servo to reach the position
  }
  for (pos = 2000; pos >= 1000; pos -= 1) { // goes from 2000 - 1000 us pulsewidth
    TCA0_SINGLE_CMP0BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
    delay(1);                       // waits 1ms for the servo to reach the position
  }
}

Thanks for your replies, I'll certainly give them a go.
In answer to your question as to why I do not want to use the servo library - ignorance mainly but I'll look into this now.
For a longer answer - at the advent of COVID I started building gadget geo caches with batteries, motors and discrete combinational logic. I was looking at a design and needed a servo and was contemplating NE555 timer based methods of generating the required PWM signal when I 'discovered' microcontrollers (OK about 30 years late) . I started with ATtiny13a devices with guidance from Ralph Bacon's excellent 'get you started' YouTube channel - to program these initially I purchased an UNO and so started my exposure to all things Arduino.
I am a newbie C programmer and will run away confused when I take a peek inside the library files.
I am also dyslexic and really struggle using the classic Arduino IDE - I tried several version 2 variants but without too much success. I then hit on Studio 7 (which has a wicked GUI based fuse blower) and armed with a Pololu programmer though I was home dry. The problem I had with Studio 7 was it's inability to deal with Arduino libraries - the one in question at the time was 'SendOnlySoftwareSerial'. I was working with an ATtiny13a after all. I was doing conditional compiles to exclude Arduino code then copy and pasting this into Ardino 1.8 to do a 'production' compile. This was a real pain.
Then I hit on PlatformIO which gave me the ability to write sketches in Arduino form and import all sorts of Arduino libraries. PlatformIO has a raft of support features I find really useful as a dyslexic - the colour coded text, variable scope error detection, and 'intellisense' on register names and bit descriptors - I could not program effectively with out this support.
I like to control the MCU using direct access of the registers so I know what's going on - I fail to grasp the complexities of the Arduino libraries. However I realise that sometimes in doing this, I try to use a resource (timers most often) that is used by a library and the conflict caused is hard to manage.
Things move on and, keen to leverage on the Event System supported on the series 01 eg ATtiny1604, I have grasped how to program these UDPI devices. I use the Studio 7 START project builder to give me a heads up about what registers I should be trying to set up, then I copy these into PlatformIO and proceed as best I can from there.
I am not trying to re-invent the wheel and really appreciate all the work and effort put in by the likes of Spence Konde (aka Dr Azzy) - I guess I'm still a newbie trying to get my 'blinky' to run.
Thanks again for your time and responses.

Should I have known you prefer bare metal programming (like myself) I could have saved the hassle of working my example into a setup() and loop() format.

This is what I started from:
It assumes all registers as per default value from the datasheet.
no need to call takeover TCA0
You will need to prescale the CPU yourself.

/* ATtiny1604 / ARDUINO Pins
                            _____
                    VDD   1|*    |14  GND
   (nSS)  (AIN4) PA4  0~  2|     |13  10~ PA3 (AIN3)(SCK)(EXTCLK)
          (AIN5) PA5  1~  3|     |12  9   PA2 (AIN2)(MISO)
   (DAC)  (AIN6) PA6  2   4|     |11  8   PA1 (AIN1)(MOSI)
          (AIN7) PA7  3   5|     |10  11  PA0 (nRESET/UPDI)
   (RXD) (TOSC1) PB3  4   6|     |9   7~  PB0 (AIN11)(SCL)
   (TXD) (TOSC2) PB2  5~  7|_____|8   6~  PB1 (AIN10)(SDA)
*/
#include <util/delay.h>

int main (void) {
  F_CPU_init ();   // reconfigure CPU clock prescaler
  VPORTB_DIR |= PIN2_bm; // PB2 as output
  // Configure timer A
  TCA0_SINGLE_CTRLB |= TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // enable WO2 PWM output | select waveform generation mode
  TCA0_SINGLE_PER = F_CPU / 400; // Count up to this value as TOP (max 0xFFFF (65535)) // 50Hz servo pulse frequency
  TCA0_SINGLE_CTRLA |= TCA_SINGLE_CLKSEL_DIV8_gc | TCA_SINGLE_ENABLE_bm;  //Select division factor (1,2,4,8,16,64,256,1024) | enable timer A
  while (1) {
    uint16_t pos;
    for (pos = 1000; pos <= 2000; pos += 1) { // goes from 0 degrees to 180 degrees
      // in steps of 1 degree
      TCA0_SINGLE_CMP2BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
      _delay_ms(3);                       // waits 3ms for the servo to reach the position
    }
    for (pos = 2000; pos >= 1000; pos -= 1) { // goes from 180 degrees to 0 degrees
      TCA0_SINGLE_CMP2BUF = (uint64_t) pos * F_CPU / 8000000LL; // output the servopulse
      _delay_ms(1);                       // waits 1ms for the servo to reach the position
    }
  }
}

void F_CPU_init () {
  // reconfigure CPU clock prescaler
#if (F_CPU == 20000000)
  /* No division on clock */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, 0x00);
#elif (F_CPU == 16000000)
  /* No division on clock */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, 0x00);
#elif (F_CPU == 10000000) // 20MHz prescaled by 2
  /* Clock DIV2 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc));
#elif (F_CPU == 8000000) // 16MHz prescaled by 2
  /* Clock DIV2 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc));
#elif (F_CPU == 5000000) // 20MHz prescaled by 4
  /* Clock DIV4 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_4X_gc));
#elif (F_CPU == 4000000) // 16MHz prescaled by 4
  /* Clock DIV4 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_4X_gc));
#elif (F_CPU == 1000000) // 16MHz prescaled by 16
  /* Clock DIV16 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_16X_gc));
#else
#ifndef F_CPU
#error "F_CPU not defined"
#else
#error "F_CPU defined as an unsupported value for untuned internal oscillator"
#endif
#endif
}

If you want to focus on the Attiny peripherals more directly you may want to subscribe to the discussions in Github.

The datasheets and the ioheaders are my friends

In my mind the Arduino world is more like the opposite, to abstract the hardware away from the user via functions and libraries, making it a great entryway.

Thanks
That's exactly what I needed, you got my code to fly. The key break through was that nifty routine takeOverTCA0() to set PeripheralControl and let me control the timer. I had also failed to set the PORTMUX_CTRLC - Doh!

I was also not planning on using the buffered change to TOP but I see the error of my ways now - thank you again.

I'll be poking around in the link to Github and the ioheaders - this was like opening the lid of a box I didn't know was there.

A very minor point - TCAO_SINGLE_CMP2BUF came up as undefined (I must not be including all I should) but accessing the register as TCA0.SINGLE.CMP2BUF worked fine.

Also, keep in mind that the requirements of Servos (1-2ms pulse 50 to 60 times a second) are pretty easy to meet without needing to have the waveform generated solely by a timer. Manipulating the pin in an ISR triggered by a timer will give you a lot more flexibility - and reduce requirements on the timer as well. (an 8bit timer can time 1-2ms, but you need a 16bit timer to fit the whole 1/60 second. (this is how that servo libraries that support 12 servos on an Uno work.)

Thank you - I had previously struggled with this and set a very slow clock to hit 50Hz (managed to brick an ATtiny85 in the process).
I will certainly consider this option.

I suspect the missing define must be a PlatformIO installation problem.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.