Thursday, January 4, 2024

How to program an UPDI AVR microcontroller using avrdude and USB to serial programmer

After buying some ATtiny402 microcontrollers I've noticed that newer AVR models from Microchip are now using the UPDI interface for programming. Previously the programming was done using SPI or UART if a bootloader was present.

How to program an UPDI AVR microcontroller using avrdude and USB to serial programmer
 

Contents

 

What is UPDI and how do I use it?

UPDI stands for Unified Program and Debug Interface and is proprietary to Microchip. In many ways is similar to 1 wire UART. The main advantage is that it can be used for programming and also for debugging. Now there is no need for SPI, bootloader, debugWire... It's all Unified in one pin and one interface.

On certain devices such as ATtiny, the UPDI and Reset are on the same pin. In each case, the UPDI pin can also be used as a GPIO pin. When UPDI and Reset share the same pin, the functionality can be selected using the specific fuse. By default the fuse is set to select UPDI as a pin function. If you change the fuse and enable the Reset then you will need a 12V programmer to be able to program the microcontroller.

To prevent false triggering when the line is idle, it is recommended to have a pull-up resistor of at least 10k on the UPDI pin. Although some say that it works without problems even without a pull-up resistor so if you are using the UPDI pin also as a GPIO pin, you might consider not placing the resistor.

Source: https://microchip.my.site.com/s/article/AVR---Hardware-Design-Considerations-for-UPDI-pin.

Can i manually reset the microcontroller when UPDI is shared with RESET? 

When UPDI and RESET share the same pin, the external RESET function is disabled so you can not place a button to manually reset the microcontroller. It is possible to enable the RESET by changing the appropriate fuse, but then you would need a 12V programmer to program the micro. But is a manual reset that important? An alternative would be to cycle the power to the microcontroller that would also cause a power on reset. 

Tip: newer AVR devices can trigger a reset from software.

What hardware do i need for programming using UPDI interface?

There are many official programmers from Microchip for AVR devices such as the well known Atmel ICE. However a simple serial USB to UART converter can also be used. The benefit of using a programmer such as Atmel ICE is that it can also be used for debugging together with Microchip Studio where you can even use it as a logic analyzer.

How to use a regular USB to serial adapter with UPDI

Simply connect RX to UPDI pin and TX to UPDI pin but this time through a 1k resistor to isolate TX and RX and also limit the current from TX through the UPDI pin when it drives its pin low. This works for programming but if you need to use UART with the same USB to serial UART adapter, another circuit must be used.

Connecting USB to serial module to an UPDI device

Programming UPDI and UART using single USB to serial adapter

 
Programming UPDI and UART using single USB to serial

It is possible to program the UPDI device and use the UART with the same USB to UART converter with just two resistors and two Schottky diodes to condition the signals and limit the current. The diodes must have a low voltage drop for this to wok so small signal Schottky types are used. In the above schematic I have used BAT54A which packs two diodes back-to-back that fits perfect for this purpose, but individual diodes can also be used. Example of other diodes that can be used: BAT85, BAT54, BAT54A, BAT54C, SD103A, SD103B.

USB_TX to UPDI: By default UPDI pin idles HIGH using an internal pull-up resistor. USB_TX is connected to UPDI pin through a 1k resistor to limit the current through the UPDI pin when it sends a 0 by pulling its pin low.

UPDI to USB_RX: After USB_TX starts UPDI programming mode, the microcontroller will use the pin to send data to USB_RX through diode 1 (pin 1). Since the USB_RX has a pull-up resistor it idles HIGH so even though the diode will block current from UPDI to USB_RX it doesn't matter since it can pull the pin LOW to send a 0.

MCU_RX to USB_TX: The microcontroller can receive serial data from USB_TX through resistors R1 and R2. R2 is used to limit the current through the microcontroller pin in case the pin is set as output high and UPDI pin is low.

MCU_TX to USB_RX: MCU_TX connects to USB_RX through diode 2 (pin 2) with Cathode connected to MCU pin so that the diode will block current flowing from microcontroller to the UPDI pin. Diode 1 is also used to block the MCU_TX from pulling low the UPDI pin.

Tip: depending on the diodes or the firmware on the microcontroller is it possible to lock out the programmer which is usually due to the MCU_TX pin overlapping data on the USB_RX. I recommend connecting the MCU_RX to diode 2 through a jumper, and in case the programming fails, remove the jumper and modify the software which can affect the programmer.

Important software considerations

UART TX 

Depending how the UART is used, it can interfere with programming mode by talking on USB_RX line over UPDI. If your application is sending data over UART, make sure there is at least 100ms between transmissions so that UPDI can have a clear line. After programming mode is enabled the CPU will be halted during programming so UART will be stopped.

UART RX 

Another consideration is when receiving data. Since USB_TX and UPDI is connected to MCU_RX, the microcontroller will also receive data while programmer tries to enter UPDI mode. If your application sends data after it receives it, it will also block programming mode. This can easily be prevented by responding back only if there is no UART error. When the programmer issues a UPDI BREAK, it holds the line LOW for a long duration. To the UART peripheral, this looks like a badly malformed character and triggers a frame error.

pymcuprog and UART TX issue 

Note that at the moment of writing, pymcuprog doesn't work if the microcontroller sends data over UART during programming. It will just hang forever at "Connecting to SerialUPDI". Avrdude doesn't have this issue. I noticed this on Linux, so on Windows this issue might not exist.

This is because when avrdude starts up, it opens /dev/tty* and immediately forces a purge/flush command on the operating system's serial input buffer. It essentially tells Linux: "Delete any garbage data that came in before I opened this port." It then sends the UPDI break pulse down USB_TX and reads the response.

pymcuprog is built on top of pyserial. When it initializes, it walks through a multi-step sequence to detect the programmer type and read system information blocks. If your microcontroller is actively flooding the USB_RX queue (even once per second), Python attempts to read or parse the initial characters hitting the serial buffer before it executes its main UPDI handshake. Because the incoming application text doesn't look like valid UPDI communication protocol frames, Python's internal packet decoder gets stuck in an infinite parsing loop or blocks indefinitely while waiting for an expected frame delimiter. 

To overcome this issues the MCU should only transmit data over UART after it receives a specific request string from your PC's terminal software.

Improving programming speed

Linux

On Linux, more specifically CachyOS I managed to decrease the programming time from 12 seconds to 1 second by modifying the latency timer from 16ms to 1ms. Even if you use a higher baud rate for programming, the operating system still has a large delay between USB packets by default. To test the difference, run this in a terminal (CTRL+ALT+T):

echo 1 | sudo tee /sys/class/tty/ttyUSB0/device/latency_timer

Replace ttyUSB0 accordingly. This command is not permanent and it will reset after reconnecting the device. To make it permanent we need a udev rule.

First create the rule file. The name doesn't matter.

sudo nano /etc/udev/rules.d/custom_devboard_atmel.rules

In my case I have two programmers: one FT232 and one using FT230, so I have two rules in one file but they can be separated in individual files.

# Target the TTY subsystem instead of raw USB
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", SYMLINK+="ttyUSB_UART", TAG+="uaccess"

# FTDI Module (FT232R) with automated 1ms low-latency rule
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="ttyUSB_FTDI", RUN+="/bin/sh -c 'echo 1 > /sys/class/tty/%k/device/latency_timer'", TAG+="uaccess"
 

Another benefit of udev rules is that you can have symlinks with a custom name so you don't have to check on what port the device is after plug in. Instead it can be referenced by the custom symlink. To know which device each rule targets, the system is using the idVendor and idProduct attributes.

To find product and vendor IDs, unplug the usb cable then plug it back in, then in another terminal (CTRL+ALT+T) use dmesg -T to see kernel logs with human readable timestamps. It should look something like this:

usb 1-9: new full-speed USB device number 35 using xhci_hcd
usb 1-9: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00usb 1-9: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-9: Product: FT232R USB UART
usb 1-9: Manufacturer: FTDI
usb 1-9: FTDI USB Serial Device converter now attached to ttyUSB0

Finally press CTRL+S to save the udev rule file then ALT+F4 to close it. To confirm file creation, use:

ls /etc/udev/rules.d

You now should see the custom file. Last steps is to reload the rules then unplug and plug the device:

sudo udevadm control --reload-rules && sudo udevadm trigger

Verify the link now points to a TTY device:

ls -l /dev/ttyUSB_UART

It should show something like: /dev/ttyUSB_UART -> ttyUSB0 

Windows

Probably there is an equivalent setting to lower the latency timer on Windows somewhere in Devices. I can't remember since I am no longer in speaking terms with Windows

What are the chances of UART data accidentally triggering UPDI mode?

The short answer is: mathematically zero. Before the UPDI hardware will listen to a single byte, it expects a "BREAK" character. In serial terms, a BREAK is not data, it means the line must be held completely, solidly LOW for at least 24.5 consecutive bit periods. At 115200 baud, one bit period is 8.68us. The line must be held low for at least 212us. Standard UART characters can only have a maximum of 9 consecutive low bits (1 start bit + 8 data bits of all zeros). After that, the mandatory Stop Bit forces the line back HIGH. Therefore, normal UART transmissions can never hold the line low long enough to trigger a UPDI break due to the stop bit.

After the break character is detected, the line must immediately return HIGH and remain idle for an exact internal guard-time window before the next sequence starts.

If the break sequence succeeds, the UPDI peripheral wakes up but remains locked. It expects the very next transmission to be an exact, hardcoded 8-byte (64-bit) security key known as the NVMPROG key. Your UART data stream would have to randomly output these exact 8 specific hexadecimal bytes back-to-back, perfectly timed: 0x20, 0x30, 0x34, 0x35, 0x38, 0x4D, 0x56, 0x4E (which translates to the ASCII string "NVMProg " in reverse).

What software do I need?

At the moment there are two options that I know of and tried: pymcuprog and avrdude.

pymcuprog is a command line Python utility and is made by Microchip. It is a bit slower than avrdude since it's using Python as opposed to C and with current version programming doesn't work if microcontroller sends UART data during programming and TX and RX are connected to programmer TX and RX.

avrdude is another popular option and is my favorite since from my experience is faster than pymcuprog and also doesn't require python to be installed.

Programming an UPDI device using avrdude, Microchip Studio and a USB to serial module

UPDI programming can be done in a command terminal by invoking avrdude with the proper arguments. In Microchip Studio the compiled file can be uploaded to the microcontroller by using avrdude with a click of a button. To do that you need to add an external tool as a programmer. If you need to know how to do that, i have a tutorial here.

The command is similar to how you would add a usbtiny programmer with a few small changes.

avrdude -c serialupdi -p t402 -P COM3 -b 115200 -U flash:w:$(ProjectDir)Debug\$(TargetName).hex:i
  • -c: here programming interface is set to serialupdi
  • -p: this argument specifies the programmed device. In my case is ATtiny402 which is defined as t402. To see a full list of microcontrollers use avrdude -p ?
  • -P: serial port number. In my case is COM3. On Linux it would be the custom symlink name in the udev file: /dev/ttyUSB_FTDI.
  • -b: baud rate. Usually 115200. UPDI will detect and respond with the same baud rate.
  • -U: perform a memory operation.
  • flash: the memory type is flash. Other values can be eeprom, lfuse, hfuse.
  • w: this field specifies what operation to perform and in this case is write. In could be 'r' for read only.
  • $(ProjectDir)Debug\$(TargetName).hex: file name. The variables that compose the filename are specific to Microchip Studio 7 and not avrdude.
  • i: file format field. Usually Intel Hex (i).

More avrdude arguments

pymcuprog

To install pymcuprog you could use pip but you will get the warning that can break system packages so you would need a custom Python environment. A better alternative is to use pipx.

pipx install pymcuprog

Example of programming command. Replace path to HEX file, device type, programmer port.

time pymcuprog write -d attiny402 -t uart -u /dev/ttyUSB_FTDI -b 115200 -f /attiny402.X/dist/default/production/attiny402.X.production.hex

Since pymcuprog doesn't display a progress bar and elapsed time like avrdude, I used time in front of pymcuprog which is a Linux utility.

Bonus tips

  • During programming, FLASH and EEPROM memories will be erased. To preserve EEPROM memory during re-flashing, set the appropriate fuse bit.
  • To generate code for your AVR microcontroller using interactive GUI use start.atmel.com.
  • Although by default the UPDI pin cannot be used as a GPIO, it can be used as an input pin, to connect a button for example because it has an internal pull-up resistor active when UPDI functionality is enabled. All you have to do is place a button and check the pin state. When pin is low, the button is pressed.
  • When having more than one project in one solution (more than one microcontroller type), select the main.c file before uploading the code to ensure the proper hex file will be used.

Power optimization of floating pins

If an unused microcontroller pin is left physically disconnected (floating), environmental noise will cause the pin voltage to float somewhere between High and Low. This forces the digital logic gate into an unstable state, constantly burning power. Setting unused or disconnected pins to PORT_ISC_INPUT_DISABLE_gc completely isolates the logic from the pin, bringing current consumption down. This should also be used for analog pins such as ADC or AC.

Example: disable digital input buffer for pin 0 port A:

PORTA.PIN0CTRL = PORT_ISC_INPUT_DISABLE_gc

If you have many pins you can use a for loop to iterate through the pins of a port via pointer arithmetic:

// Turn off all 8 pins on PORTA
for (uint8_t i = 0; i < 8; i++) {
    (&PORTA.PIN0CTRL)[i] = PORT_ISC_INPUT_DISABLE_gc;
}

// Re-enable a specific pin you actually intend to use digitally later
PORTA.PIN2CTRL = PORT_ISC_INTDISABLE_gc; // Re-enables the digital buffer (no interrupts)

Highlights of the new AVR architecture

If you are transitioning from "classic" AVRs (like the ATmega328P) to modern AVRs (like ATtiny402, or the newer AVR DA/DB/DD families), the architectural shift is massive. Microchip completely rebuilt the peripheral layout, memory mapping, and hardware interconnects.

Unified Memory Mapping

In classic AVRs, memory was fragmented. You had to use specific assembly instructions (IN/OUT vs. LDS/STS) depending on whether you were touching internal registers or I/O space.

The new architecture maps absolutely everything into a single, continuous 16-bit linear address space:

  •     0x0000: Virtual Ports (for fast single-cycle bit flipping).
  •     0x0020: General Purpose CPU Registers (R0 to R31).
  •     0x0040: Peripherals and Control Registers (GPIO, UART, Timers, SPI).
  •     0x1400: Internal EEPROM.
  •     0x3800: User Signature Rows / Fuses.
  •     0x4000+ Internal SRAM.
  •     0x8000+ Flash Memory (maps natively into data space so you can read strings/tables directly without using macro commands like pgm_read_byte).

Core Independent Peripherals (CIPs)

The single biggest feature of modern AVRs is that their peripherals are intelligent. In classic chips, if an ADC finished a reading, it had to wake up the CPU via an interrupt, wasting clock cycles and power.

Modern AVRs use Core Independent Peripherals that execute tasks completely on their own without the CPU's intervention.

The Event System: This is an internal hardware routing switchboard. You can wire a pin or a timer directly to another peripheral. For instance, you can configure Timer A to trigger an ADC sample every 10ms entirely in hardware. The CPU can remain asleep in low-power Standby mode while this happens.

Configurable Custom Logic (CCL): The chips contain a mini internal FPGA consisting of Look-Up Tables (LUTs). You can combine raw hardware pins or peripheral outputs using logic gates (AND, OR, XOR, latches) completely in hardware.

Rewritten I/O Structure: PORT and VPORT

Hardware port manipulation got a substantial structural upgrade:

PORTx Registers: Every I/O port now has structured sub-registers. For example, instead of manually read-modifying a register to flip a bit, you have dedicated target destinations:

  • PORTA.OUTSET (writing a 1 sets pins HIGH).
  • PORTA.OUTCLR (writing a 1 sets pins LOW).
  • PORTA.OUTTGL (writing a 1 toggles the pin state instantly).

VPORTx (Virtual Ports): Because the highly structured PORT registers sit deeper in the unified memory space, it takes 2 clock cycles to modify them. To optimize for speed, Microchip mirrored the core I/O registers into the low-address "Virtual Port" space (VPORTA, VPORTB). Writing to a VPORT compiles down to single-cycle execution instructions.

Advanced Hardware Clocking

Classic AVRs required a bunch of external fuse bits to switch from internal RC oscillators to external crystals, and changing clock speeds dynamically was clunky.

Modern AVRs feature a highly accurate, factory-calibrated internal oscillator (typically running natively at 16 MHz, 20 MHz, or 24 MHz depending on the specific series).

Prescalers and system clock choices are completely software-controlled. You can shift the chip’s operational speed or change clock sources mid-execution by writing to protected registers using Microchip's configuration macros (like _PROTECTED_WRITE).

Single-Pin Debugging (UPDI)

The old 6-pin SPI programming interface (MISO, MOSI, SCK, RESET) has been entirely deprecated. Modern chips use the Unified Program and Debug Interface (UPDI).

It uses just one single physical wire to handle both flash programming and full interactive on-chip debugging.

It uses a bi-directional, half-duplex UART-based physical link layer, operating independently of the main CPU clock to allow direct memory access even while the core microcontroller is locked or halted.

Fuses configuration

While the fuse bits can be written in hex format using avrdude they can also be configured and stored alongside application firmware using FUSES structure provided by avr-gcc. If you are using XC8 compiler from Microchip there is also the #pragma config option which shouldn't be used with avr-gcc since that might not be compatible.

Example of using FUSES structure

#include <avr/io.h>
#include <avr/fuse.h>

FUSES = {
    .WDTCFG  = PERIOD_OFF_gc,                          // Watchdog turned off
    .BODCFG  = ACTIVE_DIS_gc | LVL_BODLEVEL0_gc,       // BOD Disabled completely
    .OSCCFG  = FREQSEL_20MHZ_gc,                       // 20MHz internal base profile
    .SYSCFG0 = CRCSRC_NOCRC_gc | RSTPINCFG_UPDI_gc,    // UPDI pin functionality enabled
    .SYSCFG1 = SUT_8MS_gc,                             // 8 millisecond startup delay
    .APPEND  = 0x00,                                   // Default/Disabled
    .BOOTEND = 0x00                                    // Default/Disabled
};

The FUSES structure can be placed after includes. Details about each fuse bit can be found in the specific datasheet of your microcontroller under Memories -> Configuration and user fuses.

Caution: even if you need to change one value, such as set internal oscillator to 16MHz instead of 20MHz, you need to configure ALL fuses because when you instantiate the FUSES block struct, any element you omit from the list will automatically get initialized by the compiler to a value of 0x00 that can have unwanted effects. For example, leaving .SYSCFG0 blank means it defaults to 0x00. On an ATtiny402, 0x00 inside SYSCFG0 sets the CRC memory scanner to check the whole flash continuously on boot and forces the programming pin into a physical hardwired Reset pin mode instead of a UPDI pin mode. If you write a 0x00 to that fuse, you will immediately lose standard 5V UPDI communication and lock yourself out of the chip until you hook up a 12V high-voltage programmer! 

The LOCKBIT is the one major exception to this rule. In avr-libc, the lock bits are intentionally defined in a completely separate memory structure from the main configuration fuses. The regular FUSES struct handles the 7 core bytes (WDTCFG through BOOTEND). The lock bit sits in its own primitive macro register space.

If you don't explicitly instantiate a separate LOCKBITS macro block, the compiler leaves the device's lock bit space entirely untouched. When you flash the .hex file, your programmer will skip the lock memory space, and the device will safely retain its default factory setting: 0xC5 (Unlocked Mode). 

Note: do not set LOCKBIT unless you need it for the intended purpose. The lock bit (LOCKBIT) is a security fuse designed for final mass-production commercial products. Its purpose is IP Protection (Intellectual Property Protection). When you activate the lock bit, it instructs the internal hardware state machine to completely block any external reader from pulling your compiled firmware out of the flash memory. It shuts down the ability of UPDI to read the Flash, EEPROM, or Signature rows.

If you write a 0x00 to the lock bit (or explicitly set it to a locked configuration macro like LB_NOLOCK_gc or LB_RWLOCK_gc depending on your header definitions), you will immediately encounter these blocks:

  • You will no longer be able to verify your code or read back variables.
  • Your programming tools (avrdude or pymcuprog) will throw immediate "Access Denied" or "Device is Locked" validation errors when you try to update your application code. 

If you accidentally activate the lock bit, the chip is not completely destroyed (bricked), but it does require a hard reset by executing a Chip Erase (-e in avrdude) command that will wipe the entire flash and EEPROM memory.

How to write the fuses

With my programming setup, avr-gcc and MPLAB X IDE, the fuses are saved together with the code in the elf file so you can write to microcontroller using same command for programming but specifying the fuses memory instead of flash.

Example: 

avrdude -c serialupdi -p t402 -P /dev/ttyUSB_FTDI -b 115200 -U fuses:w:/path/to/attiny402.X/dist/default/production/attiny402.X.production.elf:e

Clock settings

On the modern tinyAVR 0-series (like the ATtiny402), the clock configuration is controlled by the CLKCTRL peripheral block. By default ATtiny402 is using internal 20MHz RC oscillator with a prescaler of 6 which means the F_CPU is: 20 / 6 = 3333333UL. The decision of which base speed to choose (16 or 20MHz) is controlled by the FREQSEL (Frequency Select) bits inside the FUSE.OSCCFG fuse byte. If you run the microcontroller at 3.3V you might want to select a lower CPU frequency using the fuse bits.

How to read and print the clock settings

You can inspect the MCLKCTRLA (Main Clock Control A) and MCLKCTRLB (Main Clock Control B) registers to extract the clock source and prescaler value.

Here is how to decode those values in your main.c file and print them over UART:

void print_clock_settings() {
    // Read the Clock Source (CLKSEL bits in MCLKCTRLA)
    uint8_t clksel = CLKCTRL.MCLKCTRLA & CLKCTRL_CLKSEL_gm;
    
    if (clksel == CLKCTRL_CLKSEL_OSC20M_gc) {
        UART_sendString(&uart0, "Clock Source: 16/20 MHz Internal Oscillator\r\n");
    } else if (clksel == CLKCTRL_CLKSEL_OSCULP32K_gc) {
        UART_sendString(&uart0, "Clock Source: 32.768 kHz Ultra Low-Power Internal Oscillator\r\n");
    // Comment out if not available } else if (clksel == CLKCTRL_CLKSEL_XOSC32K_gc) {
         UART_sendString(&uart0, "Clock Source: 32.768 kHz External Crystal\r\n");
    } else if (clksel == CLKCTRL_CLKSEL_EXTCLK_gc) {
        UART_sendString(&uart0, "Clock Source: External Clock Signal\r\n");
    }

    // Check if the Prescaler is Enabled (PEN bit in MCLKCTRLB)
    if (CLKCTRL.MCLKCTRLB & CLKCTRL_PEN_bm) {
        // Read the Prescaler Division Factor (PDIV bits)
        uint8_t pdiv = CLKCTRL.MCLKCTRLB & CLKCTRL_PDIV_gm;
        
        UART_sendString(&uart0, "Prescaler: ENABLED, Division Factor: ");
        
        switch(pdiv) {
            case CLKCTRL_PDIV_2X_gc:  UART_sendString(&uart0, "2\r\n");  break;
            case CLKCTRL_PDIV_4X_gc:  UART_sendString(&uart0, "4\r\n");  break;
            case CLKCTRL_PDIV_8X_gc:  UART_sendString(&uart0, "8\r\n");  break;
            case CLKCTRL_PDIV_16X_gc: UART_sendString(&uart0, "16\r\n"); break; // 20MHz / 16 = 1.25MHz
            case CLKCTRL_PDIV_32X_gc: UART_sendString(&uart0, "32\r\n"); break;
            case CLKCTRL_PDIV_64X_gc: UART_sendString(&uart0, "64\r\n"); break;
            case CLKCTRL_PDIV_6X_gc:  UART_sendString(&uart0, "6\r\n");  break; // 20MHz / 6 = 3.333333MHz
            case CLKCTRL_PDIV_10X_gc: UART_sendString(&uart0, "10\r\n"); break;
            default:                  UART_sendString(&uart0, "Unknown\r\n"); break;
        }
    } else {
        UART_sendString(&uart0, "Prescaler: DISABLED (Direct 1:1)\r\n");
    }
}

How to Modify the Clock Source and Prescaler

Because accidentally corrupting the clock settings can lock or crash the CPU, the CLKCTRL registers are protected by a hardware Configuration Change Protection (CCP) mechanism. You cannot write to them directly via normal assignments like CLKCTRL.MCLKCTRLA =....

Instead, avr-gcc provides a dedicated macro called _PROTECTED_WRITE() (found inside <avr/cpufunc.h>), which handles unlocking the system register safely for you.

Example A: Changing the Prescaler (e.g., changing speed dynamically)

If you wanted to remove the default division factor of 6 and make the chip run at its full 20 MHz output speed, you would disable the prescaler by writing a 0 to MCLKCTRLB using the protected write macro:

#include <avr/cpufunc.h> // Required for _PROTECTED_WRITE

void set_clock_to_20MHz() {
    // Disable the main clock prescaler (Bit 0 = 0)
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, 0x00); 
    
    // Note: If you do this, your F_CPU definition would need to change to 20000000UL
    // otherwise delay functions and UART baud generation calculations will warp.
}

Example B: Changing the Clock Source (e.g., switching to the Ultra Low-Power 32kHz OSC)

If you want to enter an ultra-deep power-saving profile, you can shift the system clock source over to the 32.768 kHz oscillator:

void set_clock_to_32kHz() {
    // Set clock source to internal 32kHz Ultra Low Power oscillator
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSCULP32K_gc);
    
    // Disable prescaler to ensure full 32.768kHz output
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, 0x00);
}

Configuring pins on UPDI microcontrollers

With newer AVR devices, configuring the pins as inputs, outputs, high or low, can be a bit confusing because there are more ways to do it. If you need to learn more about this, below are some useful resources. 

To lears about port manipulation I recommend reading "AVR1000b Getting Started with Writing C-Code for AVR" page 28 chapter "4.4 PORT Example". For portability and code readability it is recommended to use the definitions provided by the <avr/io.h> which are presented in the manual. You will need to get familiar with bit masks, bit groups and bit group configurations masks. To better understand them you need to know about bit wise operations and navigate to a specific definition to see its value and think how that affects the register. I have a tutorial for complete beginners about bit wise operations here: https://www.programming-electronics-diy.xyz/2020/09/program-any-avr-microcontroller-using.html#led_blinking.

Port manipulation

There are two main methods of configuring pins. Using SET, CLR or VPORTS (virtual ports).

Method 1: Using VPORT (Single-Bit Operations)

Best for fast, single-pin modifications.

#include <avr/io.h>

// 1. Configure Pin Direction
VPORTA.DIR |= PIN1_bm;   // Set Pin 1 as OUTPUT
VPORTA.DIR &= ~PIN2_bm;  // Set Pin 2 as INPUT

// 2. Enable Internal Pull-Up Resistor
// Note: Pull-ups are always configured via the main PORTx.PINnCTRL register
PORTA.PIN2CTRL = PORT_PULLUPEN_bm; 

// 3. Drive Outputs
VPORTA.OUT |= PIN1_bm;   // Drive Pin 1 HIGH
VPORTA.OUT &= ~PIN1_bm;  // Drive Pin 1 LOW

// 4. Toggle an Output
VPORTA.IN = PIN1_bm;     // Writing a '1' to IN toggles the OUT bit instantly!

// 5. Read an Input Pin
if (VPORTA.IN & PIN2_bm) {
    // Code executes if Pin 2 is HIGH
}

Method 2: Using PORT accelerator registers (multi-bit mask operations)

Best for modifying multiple pins simultaneously without affecting the rest of the port.

#include <avr/io.h>

// Create a bitmask for multiple pins (e.g., Pins 4, 5, and 6)
#define MY_PINS_bm (PIN4_bm | PIN5_bm | PIN6_bm)

// 1. Configure Pin Direction
PORTC.DIRSET = MY_PINS_bm;  // Set Pins 4, 5, and 6 as OUTPUT (Others left unchanged)
PORTC.DIRCLR = MY_PINS_bm;  // Set Pins 4, 5, and 6 as INPUT  (Others left unchanged)

// 2. Enable Pull-ups for Multiple Pins
// (Must be done individually per pin control register)
PORTC.PIN4CTRL = PORT_PULLUPEN_bm;
PORTC.PIN5CTRL = PORT_PULLUPEN_bm;

// 3. Drive Outputs
PORTC.OUTSET = MY_PINS_bm;  // Drive Pins 4, 5, and 6 HIGH simultaneously
PORTC.OUTCLR = MY_PINS_bm;  // Drive Pins 4, 5, and 6 LOW simultaneously

// 4. Toggle Multiple Outputs
PORTC.OUTTGL = MY_PINS_bm;  // Toggle Pins 4, 5, and 6 at the exact same microsecond

// 5. Read Multiple Pins at Once
uint8_t port_snapshot = PORTC.IN; 
// Now check state or isolate masks out of 'port_snapshot'

Something that wasn't specified in the manual or I couldn't find, is why PORTC.DIRSET / DIRCLR doesn't affect the rest of the bits.

When you write PORTC.DIRSET = PIN6_bm, you are performing a direct hardware-masked strobe write, not a traditional Read-Modify-Write (RMW).

In traditional registers (like classic DDRC or the new VPORTC.DIR), when you write a byte, whatever bits are 0 will force those physical pins to 0, and whatever bits are 1 will force those pins to 1.

However, DIRSET, DIRCLR, OUTSET, and OUTCLR are dedicated hardware accelerator registers. The internal silicon logic inside the microcontroller treats them with special rules:

  • For SET registers (DIRSET / OUTSET): Writing a 1 to a bit instructs the hardware to turn that specific pin into an output (or set it HIGH). Writing a 0 to a bit means "ignore this pin / do nothing."
  • For CLR registers (DIRCLR / OUTCLR): Writing a 1 to a bit instructs the hardware to turn that specific pin into an input (or pull it LOW). Writing a 0 to a bit means "ignore this pin / do nothing."

Because writing a 0 translates to a hardware command to "do nothing," the CPU doesn't need to know what the other pins are currently doing. It can use a bitmask onto the register, and only the target bit will be modified.

Which method is faster and more code efficient

Surprisingly, the VPORT method compiles into smaller, faster code for single-bit bitwise operations, even though the C-code looks like it is performing a slower Read-Modify-Write (|= or &= ~).

The PORTC.DIRSET Assembly

Because the standard PORT registers sit deeper in the unified memory map (outside the reach of the CPU's direct-bit-manipulation instructions), the compiler has to load the data using standard data pointer instructions:

LDI  R24, 0x40            ; Load bitmask (PIN6_bm) into register
STS  0x0421, R24        ; Store direct to PORTC.DIRSET address (Takes 2 cycles)

Total Execution Time: 2 to 3 clock cycles.
Code Size: 4 bytes. 

The VPORTC.DIR Assembly 

The VPORT (Virtual Port) registers are explicitly mirrored into the ultra-low memory addresses (0x0000 to 0x001F). The AVR CPU has specialized, native 1-cycle assembly instructions specifically designed for this address range: SBI (Set Bit in I/O) and CBI (Clear Bit in I/O).

Even though VPORTC.DIR |= PIN6_bm  implies reading, modifying, and writing back, the compiler optimizes this completely. It sees that you are only manipulating a single bit in the low I/O space and compiles it into a single atomic instruction: 

SBI  0x000D, 6          ; Set Bit 6 in VPORTC.DIR directly (Takes 1 cycle)

Total Execution Time: 1 clock cycle.
Code Size: 2 bytes.

Rule of thumb

Use VPORT when manipulating a single pin at a time (e.g., flipping an LED, checking a button) inside performance-critical loops like ISRs or fast bit-banging routines. It generates the smallest and fastest SBI, CBI, SBIS, and SBIC assembly instructions.

Use standard PORT (SET/CLR) registers when manipulating multiple arbitrary pins simultaneously using a dynamic variable runtime mask, or when working with generic driver libraries where the exact port address might be calculated dynamically at runtime.

Bitmask and bit position

A trap to watch out for is when to use bitmask or bit position. Bitmask defines ends with _bm while bit position ends with _bp.

A bitmask could be something like PIN6_bm which is defined as (1<<6) that is 64 in decimal or 0b01000000 in binary. This is useful to set the pin of a port but if used when a pin number is expected, it will cause thinks not to work and can be hard to debug. If for example you need number 6 use the PIN6_bp bit position which is defined as 6. You could just use the number 6 instead of the define but using a define makes the code much more easier to read.

    No comments:

    Post a Comment