HV UPDI - Use same USB port for UPDI programming and debug serial output

I was tinkering with an ATtiny1604 based project with a possible bug. Previously, I had to do a conditional compile to either have my last pin control and LED (on production unit) or generate debug serial output - but not both at once as I had run out of pins.

Not ideal so I went hunting for an alternative.

What if I could use the UPDI pin as a general GPIO pin and have this send serial debug data out.
To achieve this I followed instructions at Tiny UPDI-HV Programmer.

I also found a SendOnlySoftwareSerial library by Nick Gammon that takes the Arduino pin number as the TX pin - on an ATtiny1604, this equates to pin 11. This can be found at Send-only Software Serial.

Here’s a picture of the PlatformIO library setup.
PlatformIOLibrary

Before I could get the ATtiny1604 to send serial data on any pin, I had to tune the clock as per Internal oscillators and Tuning on Tiny AVR devices.

I hooked it all together and it eventually worked but I had to swap out the UPDI programmer with a serial convertor which seemed a faf as I was using the same serial convertor for my UPDI programmer. I also found that I needed to power reset the target device after HV programming to get it to use the UPDI pin as a GPIO pin for the serial data output.

I modified the original HV UPDI concept slightly to include a 12v source (boost circuit) selectable by pressing a push button while starting the UPDI programming sequence and added a MOSFET on the power out to target. This was controlled by an RC and diode link that generated a positive pulse briefly when the DTR signal from the serial board went back to Vcc after programming was complete.

The net effect is I can run a monitor within PlatformIO on the same port as the programming port and the target device will power on reboot after programming and start using the same port (no need to disconnect anything) for serial data output.

OK the PlatformIO switch over time from upload to monitor means I miss the output from setup() but I can live with that or introduce a delay.

Here’s my PlatformIO.ini

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:ATtiny1604]
platform = atmelmegaavr
board = ATtiny1604
framework = arduino
board_build.f_cpu = 16000000L
upload_speed = 115200
upload_port = COM16
monitor_port = COM16
monitor_speed = 115200
upload_flags =
    --tool
    uart
    --device
    ATtiny1604
    --uart
    $UPLOAD_PORT
    --clk
    $UPLOAD_SPEED
upload_command = pymcuprog write --erase $UPLOAD_FLAGS --filename $SOURCE

Here’s my sample code…

// ATtiny1604_Serial_on_UPDI_pin
// written for ATtiny1604
// Peter Charles-Jones
// cSpell:includeRegExp CStyleComment

/* 	MCU clock tuning

	Device was tuned to 16M <https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/Ref_Tuning.md>
	First the device had a bare bones sketch loaded using Arduino IDE and a boot loader burnt at 16M with Millis disabled
	Then the MegaTinyTuner example sketch was loaded into target and run with a 1kHz source connected to PA5 
	Then the boot loader was re-loaded with 16M tuned selected as clock
*/

#include <Arduino.h>
#include <SendOnlySoftwareSerial.h>		// 
#define BAUD 115200						// serial comms baud rate
SendOnlySoftwareSerial mySerial (11);	// set up the serial output stream to PA0 - UPDI pin 
//SendOnlySoftwareSerial mySerial (5);	// set up the serial output stream to PB1 - test on another pin first


// You can initialise like this: RH_ASK driver(2000, 6, 7); which will transmit on digital pin 7 == PB0 == physical pin 9 on Attiny x14 and receive on digital pin 6 == PB1 == physical pin 8 on Attiny x16 Uses Timer B1.

/* 	Pin numbering scheme

	This core uses a simple scheme for assigning the Arduino pin numbers:
	Pins are numbered starting from the the I/O pin closest to Vcc as pin 0
	and proceeding counterclockwise, skipping the (mostly) non-usable UPDI pin.
	The UPDI pin is then assigned to the last pin number (as noted above, it is possible to read the UPDI pin
	(both analog and digital reads work) even if it is not set as GPIO).
	We recommend this as a last resort: the UPDI pin always has its pullup enabled when not set as a GPIO pin,
	and a signal which looks too much like the UPDI enable sequence will cause undesired operation.*/

// ATtiny1604 / ARDUINO
//                          _____
//                  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)
//

/*
  PIN#   DESC         Pin Name  Other/Sp  ADC0      ADC1      PTC       AC0       AC1       AC2       DAC0      USART0    SPI0      TWI0      TCA(PWM)  TCBn      TCD0      CCL
  0      A0 or SS     PA4                 AIN4      AIN0      X0/Y0                                             XDIR      SS                  WO4                 WOA       LUT0-OUT
  1      A1           PA5       VREFA     AIN5      AIN1      X1/Y1     OUT       AINN0                                                       WO5       TCB0 WO   WOB
  2      A2 or DAC    PA6                 AIN6      AIN2      X2/Y2     AINN0     AINP1     AINP0     OUT
  3      A3           PA7                 AIN7      AIN3      X3/Y3     AINP0     AINP0     AINN0                                                                           LUT1-OUT
  4      RX           PB3       TOSC1                                             OUT                           RxD                           *WO0
  5      TX           PB2       TOSC2 /                                                     OUT                 TxD                           WO2
								EVOUT1
  6      SDA          PB1                 AIN10               X4/Y4     AINP2                                   XCK                 SDA       WO1
  7      SCL          PB0                 AIN11               X5/Y5               AINP2     AINP1               XDIR                SCL       WO0
  8      MOSI         PA1                 AIN1                                                                  *TxD      MOSI      *SDA                                    LUT0-IN1
  9      MISO         PA2       EVOUT0    AIN2                                                                  *RxD      MISO      *SCL                                    LUT0-IN2
  10     SCK          PA3       EXTCLK    AIN3                                                                  *XCK      SCK                 WO3       TCB1 WO
  11     UPDI         PA0       RESET/    AIN0                                                                                                                              LUT1-IN0
							    UPDI
	* alternative pin locations
  */

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

void setup(){
	system_init(); 
	mySerial.begin(BAUD);
	mySerial.println(F("\r\n\r\nStarting"));
	}

void loop(){
	mySerial.println(F("Hello"));
	delay(500);
}

/* My subroutines */

/* Interrupt Service Routines */

/* Initialization routines */
void mcu_init(void){
	/* 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 PORTA pins to low power mode and input disabled */
	for (uint8_t i = 0; i < 8; i++)
	{ // PA0 - PA7
		//*((uint8_t *)&PORTA + 0x10 + i) |= (1 << PORT_PULLUPEN_bp);
		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 PORTB pins to low power mode and input disabled */
	for (uint8_t i = 0; i < 4; i++)
	{ // PB0 - PB3 on ATtiny1604
		//*((uint8_t *)&PORTB + 0x10 + i) |= (1 <<  PORT_PULLUPEN_bp);
		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 system_init(){ /* system initialization */
	mcu_init();
}	

A picture of the circuit.

And a photo of the built programmer

I hope this helps anyone stuck for a pin to provide some serial debug output.