All You Ever Needed To Know About UPDI, And Then Some!

This entry is part 3 of 3 in the series IMPLEMENTING UPDI FROM SCRATCH

Table of Contents

IMPLEMENTING UPDI FROM SCRATCH

All You Ever Needed To Know About UPDI, And Then Some!

All You Ever Needed To Know About UPDI, And Then Some!

All You Ever Needed To Know About UPDI, And Then Some!

All You Ever Needed To Know About UPDI, And Then Some!

All You Ever Needed To Know About UPDI, And Then Some!

All You Ever Needed To Know About UPDI, And Then Some!

Post Stastics

  • This post has 9329 words.
  • Estimated read time is 44.42 minute(s).

Part III — Turning the Arduino Nano into a Real UPDI Programmer

Updated for the finalized NANO UPDI PROGRAMMER Rev. 1.0.0 hardware

Parts I and II got us across the wire.

We now understand enough of UPDI to:

  • electrically connect an Arduino Nano to a target AVR;
  • wake or enable the UPDI interface;
  • generate BREAK;
  • transmit and receive 8E2 characters;
  • generate the 0x55 synchronization character;
  • issue UPDI instructions;
  • access UPDI’s internal Control/Status register space;
  • and begin reading the target’s memory-mapped address space.

That is already a useful communications system.

But it is not yet a programmer.

Programming begins when we cross from:

UPDI transport

into:

NVMCTRL

—the target AVR’s Nonvolatile Memory Controller.

This distinction is critical.

Our Nano does not directly “burn Flash.”

It tells UPDI to perform reads and writes into the target’s address space. Some of those writes go to the NVM controller. The NVM controller, in turn, performs the actual erase and programming operations on:

Flash

EEPROM

Fuses

USERROW

That relationship looks like:

                       Arduino Nano
                            |
                            |
                         UPDI PHY
                            |
                         UPDI DL
                            |
                         UPDI ACC
                            |
                    Target system bus
                            |
           +----------------+----------------+
           |                                 |
           v                                 v
        Memories                          NVMCTRL
                                             |
                    +------------------------+-------------------+
                    |                        |                   |
                    v                        v                   v
                  Flash                   EEPROM              Fuses
                    |
                    +--------------------- USERROW

This part of the project is where we must begin paying particularly close attention to which AVR we are programming.

The UPDI instruction set is broadly common.

The NVM controllers are not.

Our primary teaching target remains:

AVR16DU28-I/SP

and our two reference alternatives remain:

AVR32DU28-I/SP

AVR16DD28-I/SP

The AVR16DU28 and AVR32DU28 use the same DU-family NVM architecture. Microchip’s own pymcuprog source identifies the AVR DU implementation as NVM controller P:4.

The AVR16DD28 uses an earlier NVM-controller architecture. Although many high-level ideas remain similar, addresses, programming details, USERROW organization, and controller behavior must not be assumed identical merely because both devices use UPDI.

This is precisely why we are building a device-aware programmer.

Hardware Baseline Carried Forward from Part II

Part III assumes the finalized Nano programmer hardware developed in Parts I and II.

The Nano pin assignments are:

Nano pinFunction
D2Activity/status indication
D3HV/status indication
D4TARGET_RESET_CTRL
D5UPDI_RX
D6UPDI_TX
D7HV_ENABLE
D8TARGET_POWER_ENABLE
A0VTARGET_SENSE
A1HV_SENSE
A4/A5Optional I²C expansion
D10–D13Optional SPI expansion

The target connector is:

J2-1  TARGET_RESET
J2-2  VTARGET
J2-3  UPDI_DATA
J2-4  GND

The target still sees one UPDI conductor. Inside the programmer, however, UPDI is split into independent transmit and receive paths:

Nano D6 / UPDI_TX
        |
      R8 47k
        |
      Q4 2N3904
  open-collector pull-down
        |
        +-------------------- UPDI_DATA -------------------- target UPDI
        |
      R5 47k
        |
      Q2 2N3904
   receive translator
        |
Nano D5 / UPDI_RX

The Q4 base also has R24 = 47 kΩ to ground so the transmitter defaults OFF while the Nano is resetting. The Q2 collector is pulled to +5 V through R2 = 47 kΩ.

The optional target-side pull-up assist is:

VTARGET
   |
 D1 SS14
   |
 R4 33k  DNP
   |
UPDI_DATA

R4 remains DNP by default. The target’s own UPDI pull-up is the primary source of the idle-HIGH state.

Part III also assumes the completed voltage-monitoring hardware:

VTARGET → 22k / 10k divider → 1k ADC protection → A0
HV      → 33k / 10k divider → 1k ADC protection → A1

Both ADC inputs include Schottky clamping to the Nano’s +5 V rail and 0.1 µF filtering.

The hardware also includes:

  • independent transistor-controlled target RESET on D4;
  • externally supplied, transistor-switched HV controlled by D7;
  • a physical HV route/arm selector;
  • and optional relay-controlled target power on D8.

None of those additions change the UPDI or NVMCTRL programming protocol described in this part. They do, however, change how the firmware names and controls the physical hardware.


121. Start by Identifying the Target

A good programmer should never begin with:

erase

It should begin with:

Who are you?

For our AVR DU target, Microchip places the Signature Row at:

SIGROW base = 0x1080

and the first three bytes contain the device signature. The AVR DU memory map also places:

LOCK 0x1040

FUSE 0x1050

SIGROW 0x1080

BOOTROW 0x1100

USERROW 0x1200

EEPROM 0x1400

NVMCTRL 0x1000

for the devices covered by the AVR DU data sheet.

For our principal targets:

DeviceSignature
AVR16DU281E 94 39
AVR32DU281E 95 40
AVR16DD281E 94 32

Microchip explicitly lists 1E 94 39 for the AVR16DU28 and 1E 95 40 for the AVR32DU28. The AVR16DD28 is 1E 94 32.

Therefore our first useful target-identification routine can simply read:

0x1080

0x1081

0x1082

on the AVR DU.

Conceptually:

struct DeviceSignature

{

uint8\_t b0;

uint8\_t b1;

uint8\_t b2;

};

and:

UpdiError readSignatureDu(DeviceSignature &sig)

{

constexpr uint16\_t SIGROW\_BASE = 0x1080;

UpdiError err;

err = updiLds(SIGROW\_BASE + 0, sig.b0);

if (err != UpdiError::None)

return err;

err = updiLds(SIGROW\_BASE + 1, sig.b1);

if (err != UpdiError::None)

return err;

return updiLds(SIGROW_BASE + 2, sig.b2);

}

The implementation details of updiLds() depend on the direct-address form we finished in Part II.

The important thing here is the sequence.


122. Do Not Hard-Code AVR DU Addresses for AVR DD

The AVR16DD28 presents a perfect demonstration of why a device table matters.

Its memory map includes:

NVMCTRL 0x1000

LOCK 0x1040

FUSE 0x1050

USERROW 0x1080

SIGROW 0x1100

EEPROM 0x1400

rather than the DU arrangement where SIGROW is at 0x1080 and USERROW at 0x1200.

So this code:

constexpr uint16_t SIGROW = 0x1080;

may work perfectly with the AVR16DU28 and fail silently when we plug in an AVR16DD28.

The better design is:

struct DeviceDescriptor

{

const char \*name;

uint8\_t signature[3];

uint32\_t flashSize;

uint16\_t flashPageSize;

uint16\_t eepromSize;

uint16\_t nvmctrlBase;

uint16\_t fuseBase;

uint16\_t sigrowBase;

uint16\_t userrowBase;

uint16\_t bootrowBase;

uint16\_t userrowSize;

uint8\_t nvmVersion;

};

For example:

constexpr DeviceDescriptor AVR16DU28_DESC =

{

“AVR16DU28”,

{0x1E, 0x94, 0x39},

16 * 1024,

512,

256,

0x1000,

0x1050,

0x1080,

0x1200,

0x1100,

512,

4

};

The 4 here represents the P:4 NVM implementation used by AVR DU in Microchip’s serial-UPDI software.

For our AVR32DU28:

constexpr DeviceDescriptor AVR32DU28_DESC =

{

“AVR32DU28”,

{0x1E, 0x95, 0x40},

32 * 1024,

512,

256,

0x1000,

0x1050,

0x1080,

0x1200,

0x1100,

512,

4

};

And the AVR16DD28 descriptor would contain its own memory-map values.


123. Our AVR16DU28 Memory Geometry

Before writing Flash we need to know what Flash actually looks like.

For the AVR16DU28:

Flash size: 16 KB

Flash page: 512 bytes

Number of pages: 32

For the AVR32DU28:

Flash size: 32 KB

Flash page: 512 bytes

Number of pages: 64

Microchip documents both devices with 512-byte Flash pages.

Our AVR16DD28 alternative also uses:

16 KB Flash

512-byte Flash pages

32 pages

so the Flash page geometry happens to match the AVR16DU28 even though the surrounding NVM architecture differs.

Do not confuse:

same Flash page size

with:

same NVM controller

Those are unrelated assumptions.


124. The AVR DU Flash Has Two Addressing Views

Another potential source of confusion is that Flash appears at different addresses depending on how it is accessed.

From the AVR CPU’s code space:

Flash starts at 0x0000

From the CPU data-space view used by LD/ST and UPDI:

Flash is mapped beginning at 0x8000

on the DU family. Microchip explicitly documents this distinction.

So if our Intel HEX file contains:

program byte address = 0x0000

and we are going to write Flash through the data-space UPDI mapping, the effective UPDI address becomes:

0x8000 + 0x0000

=

0x8000

Likewise:

HEX address 0x0200

maps to:

UPDI data-space address 0x8200

for the first mapped 32 KB Flash block.

This distinction belongs in a named conversion function.

Do not scatter + 0x8000 throughout the program.

Use:

uint32_t flashToDataAddress(uint32_t flashAddress)

{

return 0x8000UL + flashAddress;

}

for the simple AVR16DU28/AVR32DU28 range we are presently using.

Later, if we support devices larger than 32 KB, Flash mapping becomes more interesting because only a 32 KB section is visible at 0x8000–0xFFFF at one time. Microchip documents mapping of 32 KB Flash blocks into that region.

Our 16 KB and 32 KB examples avoid that complication nicely.


125. First NVM Test: Read Flash

Before programming anything, read it.

That verifies:

UPDI system-bus access

Flash mapping

address conversion

block reads

REPEAT

pointer increment

host transfer

without modifying the MCU.

At the simplest level:

UpdiError readFlashByte(

const DeviceDescriptor &dev,

uint32\_t flashOffset,

uint8\_t &value)

{

if (flashOffset >= dev.flashSize)

return UpdiError::AddressRange;

uint32\_t address = 0x8000UL + flashOffset;

return updiLds(address, value);

}

But byte-at-a-time reads are slow.

The real implementation should use:

set pointer

REPEAT

LD PTR++

for blocks.

Conceptually:

pointer = 0x8000

REPEAT 255

LD PTR++

then receive:

256 bytes

in one streamed operation.

This is a natural place for the protocol analyzer from Part II.

The capture should show:

SYNCH

REPEAT

SYNCH

LD PTR++

DATA

DATA

DATA

rather than hundreds of independent LDS operations.


126. Entering NVM Programming Mode

Reading an unlocked device can be possible without entering the full NVM programming mode.

Writing safely is another matter.

Microchip’s DU documentation gives a specific programming sequence:

  1. If required, erase/unlock the device.
  2. Send the NVMPROG key with UPDI KEY.
  3. Optionally verify the NVMPROG key status.
  4. issue a system reset through ASI_RESET_REQ.
  5. release the reset.
  6. poll ASI_SYS_STATUS.PROGSTART.
  7. when PROGSTART == 1, begin NVM programming.
  8. perform programming.
  9. reset the system again when finished.
  10. release reset.

The DD family documents the same high-level protected programming entry sequence.

This is important because an active CPU executing code while we change NVM can produce unpredictable behavior.

Programming mode gives us controlled access.


127. The NVMPROG Key

The NVMPROG activation signature is:

0x4E564D50726F6720

It is 64 bits.

Microchip states that the key is written LSB first.

The bytes transmitted therefore correspond to the low-order byte first.

If we represent the 64-bit value numerically:

0x4E 56 4D 50 72 6F 67 20

do not assume that those are transmitted left-to-right as displayed.

The least-significant byte is:

0x20

so the on-wire KEY payload begins with the low byte.

One safe representation is to define the actual transmission byte sequence explicitly:

constexpr uint8_t KEY_NVMPROG[8] =

{

0x20,

0x67,

0x6F,

0x72,

0x50,

0x4D,

0x56,

0x4E

};

Notice that our earlier discussion of endianness is suddenly practical.

The numeric representation and wire representation are not necessarily written in the same human-readable order.


128. Do Not Hide Keys Behind Unexplained Hex

Instead of:

uint8_t key**[]** =

{

0x20, 0x67, 0x6F, 0x72,

0x50, 0x4D, 0x56, 0x4E

};

with no context, write:

// UPDI NVMPROG activation key:

// Numeric signature: 0x4E564D50726F6720

// UPDI KEY payload is transmitted least-significant byte first.

constexpr uint8_t UPDI_KEY_NVMPROG[8] =

{

0x20, 0x67, 0x6F, 0x72,

0x50, 0x4D, 0x56, 0x4E

};

A programmer project is exactly where “magic constants” become dangerous.

Six months after writing the code, neither you nor anyone else should have to wonder where the eight bytes came from.


129. ASI_KEY_STATUS

After transmitting the key we can verify that UPDI decoded it.

The DU documentation gives:

ASI_KEY_STATUS offset = 0x07

and the relevant key-status bits include:

bit 4 = NVMPROG

bit 3 = CHER

for the documented implementation.

So conceptually:

constexpr uint8_t UPDI_ASI_KEY_STATUS = 0x07;

constexpr uint8_t KEY_NVMPROG_bm = 1u << 4;

Then:

bool nvmpKeyAccepted**()**

{

uint8\_t status;

if (updiLdcs(UPDI_ASI_KEY_STATUS, status)

        != UpdiError::None)

return false;

return (status & KEY_NVMPROG_bm) != 0;

}

This is another valuable place to stop and test.

Do not immediately continue into a Flash write.

First demonstrate:

KEY sent

KEY_STATUS.NVMPROG = 1

reliably.


130. System Reset Through UPDI

The finalized hardware also has a physical target RESET driver on Nano D4. That hardware control is separate from the reset request described in this section.

Here we are discussing the UPDI/ASI software reset request sent through ASI_RESET_REQ. To avoid confusing it with the D4-controlled physical RESET pin, the firmware should use distinct names:

updiSystemReset...   = reset request sent through UPDI/ASI
targetResetPin...    = physical RESET pin controlled through Nano D4

The NVM programming-entry sequence below uses the UPDI system reset request unless a device-specific procedure explicitly requires the external RESET pin.

The documented reset-control CS register is:

ASI_RESET_REQ offset = 0x08

and the normal reset signature is:

0x59

while:

0x00

clears the reset condition.

Therefore:

constexpr uint8_t UPDI_ASI_RESET_REQ = 0x08;

constexpr uint8_t UPDI_RESET_APPLY = 0x59;

constexpr uint8_t UPDI_RESET_RELEASE = 0x00;

A helper becomes:

UpdiError updiSystemReset(bool asserted)

{

return updiStcs(

    UPDI\_ASI\_RESET\_REQ,

    asserted

        ? UPDI\_RESET\_APPLY

: UPDI_RESET_RELEASE

);

}

Again, the name is more important than the hex.

This:

targetReset**(true)**;

is far easier to audit than:

updiStcs(0x08, 0x59);


131. PROGSTART

After key activation and reset sequencing we poll:

ASI_SYS_STATUS

At the documented DU address:

offset = 0x0B

and:

bit 3 = PROGSTART

indicates that NVM programming may begin.

So:

constexpr uint8_t UPDI_ASI_SYS_STATUS = 0x0B;

constexpr uint8_t UPDI_PROGSTART_bm = 1u << 3;

and:

UpdiError waitProgrammingMode(uint32_t timeoutMs)

{

uint32\_t start = millis**()**;

while ((uint32_t)(millis() – start) < timeoutMs)

{

    uint8\_t value;

    UpdiError err =

updiLdcs(UPDI_ASI_SYS_STATUS, value);

if (err != UpdiError::None)

return err;

if (value & UPDI_PROGSTART_bm)

return UpdiError::None;

}

return UpdiError::Timeout;

}

Never:

while (!(readStatus**()** & PROGSTART**))** {

}

without a timeout.


132. Complete Enter-Programming Function

Now our high-level operation starts becoming readable:

UpdiError enterProgrammingMode**()**

{

UpdiError err;

// 1. Send NVMPROG key.

err = updiSendKey(

    UPDI\_KEY\_NVMPROG,

sizeof(UPDI_KEY_NVMPROG**))**;

if (err != UpdiError::None)

return err;

// 2. Verify key recognition.

uint8\_t keyStatus;

err = updiLdcs(

    UPDI\_ASI\_KEY\_STATUS,

    keyStatus);

if (err != UpdiError::None)

return err;

if (!(keyStatus & KEY_NVMPROG_bm**))**

return UpdiError::KeyRejected;

// 3. Reset target.

err = targetReset**(true)**;

if (err != UpdiError::None)

return err;

err = targetReset**(false)**;

if (err != UpdiError::None)

return err;

// 4. Wait for programming access.

return waitProgrammingMode(100);

}

This is intentionally verbose.

It reads almost exactly like the datasheet procedure.

That is a good property in programming firmware.

Microchip’s own current tools follow the same broad pattern: send the NVM key, inspect key status, toggle reset, then confirm programming mode.


133. Check Lock State First

ASI_SYS_STATUS also contains:

LOCKSTATUS

at bit 0 for the relevant implementation.

When:

LOCKSTATUS = 1

the device is locked.

When:

LOCKSTATUS = 0

the normal UPDI system-bus access is available.

A locked device behaves very differently.

For the AVR DU family, Microchip states that a locked device denies ordinary UPDI access to Flash, SRAM, EEPROM, SIGROW, fuses, and most I/O memory; specialized operations such as chip erase remain available through key mechanisms unless stronger PDID protection has been deliberately enabled.

So:

bool targetLocked(uint8_t sysStatus)

{

return (sysStatus & 0x01u) != 0;

}

must be checked early.


134. LOCK Is Not the Same as PDID

The AVR DU family introduces a very important distinction.

A normally locked part can ordinarily be recovered using the supported chip-erase procedure.

But AVR DU also supports Program and Debug Interface Disable, or PDID.

If the relevant PDID fuse protection is deliberately activated, Microchip warns that UPDI NVM access and chip erase can be permanently blocked. Firmware updates then require an appropriate bootloader path rather than external UPDI programming.

This deserves a bright warning in our future user interface.

Do not offer:

Enable PDID

as though it were an ordinary harmless checkbox.

It is a security feature with intentionally serious consequences.


135. NVMCTRL on the AVR DU

Our primary AVR16DU28 and AVR32DU28 use a DU-family controller that Microchip’s pymcuprog labels:

P:4

Its base address is:

0x1000

and important offsets include:

CTRLA +0x00

CTRLB +0x01

CTRLC +0x02

INTCTRL +0x04

INTFLAGS +0x05

STATUS +0x06

DATA +0x08

ADDR +0x0C

Microchip’s official data sheet and official pymcuprog source agree on these core offsets.

So our code can define:

constexpr uint16_t NVMCTRL_BASE = 0x1000;

constexpr uint16_t NVM_CTRLA = NVMCTRL_BASE + 0x00;

constexpr uint16_t NVM_STATUS = NVMCTRL_BASE + 0x06;

constexpr uint16_t NVM_DATA = NVMCTRL_BASE + 0x08;

constexpr uint16_t NVM_ADDR = NVMCTRL_BASE + 0x0C;

Again, put these values in a device/NVM module, not in the physical UPDI layer.


136. The AVR DU NVM Commands

For P:4, relevant NVM commands include:

0x00 NOCMD

0x01 NOOP

0x02 FLWR Flash Write Enable

0x08 FLPER Flash Page Erase Enable

0x12 EEWR EEPROM Write Enable

0x13 EEERWR EEPROM Erase-and-Write Enable

0x18 EEBER EEPROM Byte Erase Enable

0x20 CHER Chip Erase

0x30 EECHER EEPROM Erase

The data sheet additionally provides multi-page Flash erase and multi-byte EEPROM erase commands.

We do not need every optimization in version 1.

Our first programmer needs:

NOCMD

FLWR

FLPER

EEERWR

CHER

and good error handling.


137. NVM STATUS

The AVR DU NVM status register is:

NVMCTRL.STATUS = 0x1006

and contains:

bits 6:4 ERROR

bit 1 FBUSY

bit 0 EEBUSY

with defined error codes including:

0 = NONE

1 = INVALIDCMD

2 = WRITEPROTECT

3 = CMDCOLLISION

Microchip notes that a command collision causes subsequent operations to be ignored until the condition is appropriately cleared.

This is exactly why:

write command

delay(10)

assume finished

is a poor programming algorithm.

Read status.


138. Our NVM Wait Routine

A useful implementation is:

enum class NvmError : uint8_t

{

None,

Timeout,

InvalidCommand,

WriteProtect,

CommandCollision,

Unknown

};

Then:

NvmError nvmWaitReady(uint32_t timeoutMs)

{

uint32\_t start = millis**()**;

while ((uint32_t)(millis() – start) < timeoutMs)

{

    uint8\_t status;

if (updiLds(NVM_STATUS, status)

            != UpdiError::None)

        continue;

    uint8\_t error =

(status >> 4) & 0x07;

switch (error)

{

        case 0:

            break;

        case 1:

return NvmError::InvalidCommand;

        case 2:

return NvmError::WriteProtect;

        case 3:

return NvmError::CommandCollision;

        default:

return NvmError::Unknown;

}

    bool flashBusy =

        status **&** (1u **<<** 1);

    bool eepromBusy =

        status **&** (1u **<<** 0);

if (!flashBusy && !eepromBusy)

return NvmError::None;

}

return NvmError::Timeout;

}

Microchip’s own P:4 serial-UPDI implementation follows this same strategy: poll STATUS, check the error field, and wait until both Flash and EEPROM busy flags are clear.


139. Writing NVMCTRL.CTRLA Through UPDI

The normal AVR CPU sees NVMCTRL.CTRLA as protected by Configuration Change Protection.

That is relevant for self-programming firmware running on the AVR CPU.

But the external UPDI programming path is special.

Microchip’s UPDI documentation explicitly permits writing the NVM controller directly after entering NVM programming mode, and Microchip’s own serial-UPDI P:4 implementation writes NVMCTRL.CTRLA directly through UPDI.

This is an excellent example of why we cannot blindly take an ordinary CPU-side NVM code example and assume it is the same as an external programmer.

There are two contexts:

CPU self-programming

and:

UPDI external programming

They interact with the same NVM controller but are not identical programming environments.


140. A Safe NVM Command Helper

For our AVR DU:

UpdiError nvmCommand(uint8_t command)

{

return updiSts(

    NVM\_CTRLA,

    command);

}

A higher-level function should first verify readiness:

NvmError executeNvmCommand(uint8_t command)

{

NvmError ready =

nvmWaitReady(100);

if (ready != NvmError::None)

return ready;

if (nvmCommand(command)

        != UpdiError::None)

return NvmError::Unknown;

return NvmError::None;

}

The complete write operation will then:

wait ready

set command

transfer data

wait ready

clear command

check errors

Microchip’s P:4 implementation explicitly clears the command back to NOCMD after operations.


141. Flash Erase Granularity

On all three of our reference devices:

AVR16DU28

AVR32DU28

AVR16DD28

the normal Flash erase unit is:

512 bytes

one page.

For AVR16DU28:

page 0: 0x0000–0x01FF

page 1: 0x0200–0x03FF

page 2: 0x0400–0x05FF

in Flash/code-relative addresses.

When using the DU data-space Flash mapping:

page 0: 0x8000–0x81FF

page 1: 0x8200–0x83FF

That simple calculation is worth encapsulating.

uint32_t flashPageStart(

uint32\_t address,

uint16\_t pageSize)

{

return address –

(address % pageSize);

}

Or, because 512 is a power of two:

uint32_t page =

address **&** \~0x1FFUL;

But the generic arithmetic version better supports other devices.


142. Do Not Erase a Page Just Because We Are Writing One Byte

Suppose our HEX file contains only:

two changed bytes

inside an existing page.

If we erase the 512-byte page without first preserving all of its other contents, we will turn the remainder into:

0xFF

That might destroy code we intended to keep.

There are therefore two broad programming strategies.

Full-image programming

chip erase

write entire application

verify entire application

This is the easiest and safest first implementation.

Partial page update

read complete 512-byte page

modify desired bytes in RAM/host

erase page

rewrite complete merged page

verify

This preserves untouched data.

Our first programmer should default to full-image programming after chip erase.

Partial page rewriting can come later.


143. Erasing One AVR DU Flash Page

The AVR DU procedure is conceptually:

wait NVM ready

 

NVMCTRL.CTRLA = FLPER (0x08)

 

perform a write to an address in that Flash page

 

erase begins

 

poll FBUSY

 

NVMCTRL.CTRLA = NOCMD

Microchip’s own P:4 implementation performs the page-erase command followed by a dummy write of 0xFF to an address in the target page, then waits for completion and clears the command.

A conceptual Nano implementation:

NvmError eraseFlashPage(uint32_t address)

{

NvmError err =

nvmWaitReady(100);

if (err != NvmError::None)

return err;

if (nvmCommand(0x08)

        != UpdiError::None)

return NvmError::Unknown;

// Trigger erase by touching an address

// within the selected page.

if (updiSts(address, 0xFF)

        != UpdiError::None)

return NvmError::Unknown;

err = nvmWaitReady(100);

nvmCommand(0x00);

return err;

}

This should initially be tested on a device whose Flash contents we are willing to erase.

Do not use your only copy of valuable firmware as the test target.


144. Chip Erase: Two Different Situations

There are two useful chip-erase paths.

Unlocked device

When the device is already unlocked and we are in programming mode, P:4 provides:

NVMCTRL.CTRLA = CHER = 0x20

for erasing Flash and EEPROM, subject to the EESAVE fuse behavior.

Locked device

A locked target cannot give us ordinary system-bus access to NVMCTRL.

Therefore we use the protected Chip Erase KEY sequence through UPDI CS space.

This distinction is very important.


145. The Chip-Erase Key

The documented chip-erase signature is:

0x4E564D4572617365

64 bits, written LSB first.

The actual byte transmission order is therefore the low byte first.

A useful definition is:

constexpr uint8_t UPDI_KEY_CHIPERASE[8] =

{

0x65,

0x73,

0x61,

0x72,

0x45,

0x4D,

0x56,

0x4E

};

Again:

numeric notation

and:

wire order

are different perspectives on the same 64-bit value.


146. Locked-Device Chip-Erase Sequence

Microchip documents the following broad procedure for DU and DD:

  1. Send the CHIPERASE key.
  2. Send the NVMPROG key.
  3. verify both key-status bits.
  4. apply reset through ASI_RESET_REQ.
  5. release reset.
  6. poll LOCKSTATUS.
  7. when LOCKSTATUS == 0, the erase/unlock has completed.
  8. check ERASEFAIL where supported.

Note the interesting detail:

CHIPERASE key

+

NVMPROG key

are both loaded in the documented modern sequence.

That second key helps establish a valid post-erase programming state.


147. Chip Erase Does Not Necessarily Erase Everything

For AVR DU:

USERROW

is specifically intended to survive chip erase.

Microchip documents the AVR DU USERROW as:

512 bytes

and states that it is not affected by chip erase.

The DU family also provides:

BOOTROW = 256 bytes

with its own security/access characteristics.

EEPROM behavior may depend on the EESAVE fuse.

Therefore never define “chip erase” in a user manual merely as:

“Sets every nonvolatile byte to FF.”

That is not generally correct.


148. Flash Writes on P:4 Are Not Traditional Page-Buffer Programming

This is an important AVR DU characteristic.

Microchip’s official P:4 serial-UPDI implementation states:

this NVM version has no page buffer; data is written directly.

For Flash writes it selects:

FLWR = 0x02

then writes Flash data, waits for NVM ready, and returns the command to NOCMD.

That means the conceptual sequence is:

wait ready

CTRLA = FLWR

write target Flash words/data

wait ready

CTRLA = NOCMD

This is different from older AVR descriptions that speak about filling an entire hidden page buffer and then issuing a separate page-write command.

The NVM generation matters.


149. Flash Write Granularity

The DU data sheet specifies:

Flash erase granularity: page

Flash programming granularity: word

with byte access available when using the data-space mapped representation.

Microchip’s official P:4 programmer implementation uses word accesses for Flash, which is a good default for our programmer too.

So our Flash data routines should preferably transfer an even number of bytes and maintain alignment.

For example:

struct FlashWord

{

uint8\_t low;

uint8\_t high;

};

But we should not reinterpret the bytes as native uint16_t values without considering byte order.

The Intel HEX image is fundamentally a stream of bytes.

The target Flash is fundamentally a sequence of bytes containing AVR instruction words.

Preserve the bytes.


150. An AVR DU Flash-Write Function

Conceptually:

NvmError writeFlashBlock(

uint32\_t dataSpaceAddress,

const uint8\_t \*data,

size\_t count)

{

if (count == 0)

return NvmError::None;

NvmError ready =

nvmWaitReady(100);

if (ready != NvmError::None)

return ready;

// Enable Flash writing.

if (nvmCommand(0x02)

        != UpdiError::None)

return NvmError::Unknown;

// Prefer an efficient pointer + REPEAT

// word/block implementation here.

UpdiError err =

updiWriteBlockWords(

        dataSpaceAddress,

        data,

        count);

if (err != UpdiError::None)

{

nvmCommand(0x00);

return NvmError::Unknown;

}

NvmError result =

nvmWaitReady(100);

// Clear the programming command.

nvmCommand(0x00);

return result;

}

The exact updiWriteBlockWords() routine belongs in the UPDI link/instruction layer, not the NVM layer.

That separation remains important.


151. Verification Is Not Optional

After programming, read the bytes back.

Do not assume:

no error flag

means:

correct Flash contents

A complete programming cycle is:

erase

write

read

compare

success

not:

erase

write

hope

Our host should report something like:

Programming Flash…

16384 / 16384 bytes

Verifying…

16384 / 16384 bytes

VERIFY OK

If verification fails:

VERIFY FAILED

Address: 0x03A6

Expected: 0x7C

Read: 0xFC

That information is useful.

“Programming error” is not.


152. Verify Erase Too

Before writing a freshly erased test page, read it.

A correctly erased Flash byte should normally read:

0xFF

So our test firmware can check:

bool pageIsErased(

const uint8\_t \*data,

size\_t count)

{

for (size_t i = 0; i < count; ++i)

{

if (data[i] != 0xFF)

return false;

}

return true;

}

This gives us another intermediate milestone:

erase page

read page

all FF?

before attempting to write program data.


153. EEPROM on the AVR16DU28

Our DU reference devices contain:

256 bytes EEPROM

beginning at:

0x1400

in data space.

The DD alternative also has:

256 bytes

EEPROM base 0x1400

so this particular detail is common across our examples.

EEPROM is byte-oriented rather than Flash-page-oriented.

For P:4 Microchip provides:

EEWR 0x12

EEERWR 0x13

EEBER 0x18

EECHER 0x30

among other erase-block options.


154. EEPROM Erase-and-Write

For a first programmer, EEERWR is particularly convenient.

Microchip’s official P:4 implementation performs:

wait ready

CTRLA = EEERWR

write EEPROM bytes

wait ready

CTRLA = NOCMD

and uses the same mechanism for fuse programming on P:4.

Conceptually:

NvmError writeEepromByte(

uint16\_t offset,

uint8\_t value)

{

if (offset >= 256)

return NvmError::Unknown;

uint16\_t address =

0x1400 + offset;

NvmError ready =

nvmWaitReady(100);

if (ready != NvmError::None)

return ready;

if (nvmCommand(0x13)

        != UpdiError::None)

return NvmError::Unknown;

if (updiSts(address, value)

        != UpdiError::None)

{

nvmCommand(0x00);

return NvmError::Unknown;

}

NvmError result =

nvmWaitReady(100);

nvmCommand(0x00);

return result;

}

Then read it back.

Always.


155. EEPROM Test Before Fuse Test

Our destructive-test progression should be:

SRAM

Flash on expendable image

EEPROM test byte

USERROW

ordinary fuse

dangerous fuse

Why EEPROM before fuses?

Because a mistaken EEPROM value generally corrupts application data.

A mistaken fuse can alter:

clock behavior

reset behavior

EEPROM erase policy

UPDI pin behavior

code protection

security mode

Those consequences are substantially more serious.


156. Fuse Space on AVR DU

For our AVR DU target:

FUSE base = 0x1050

and the data sheet defines fuse offsets including:

+0x00 WDTCFG

+0x01 BODCFG

+0x02 OSCCFG

+0x05 SYSCFG0

+0x06 SYSCFG1

+0x07 CODESIZE

+0x08 BOOTSIZE

Microchip also cautions that reserved bits must be written as zero where specified.

That means a safe fuse-writing program must not invent values.

It should:

read current fuse

decode documented fields

modify requested field

force required reserved-bit values

show old/new value

require confirmation when dangerous

write

reset if required

read back


157. Fuse Safety Classes

I recommend classifying fuse writes.

Class A — ordinary

Changes with relatively modest recovery risk.

Examples may include configuration options that do not affect programming access or security.

Class B — disruptive

Changes that may affect:

startup

BOD

watchdog

clock behavior

and therefore deserve a warning.

Class C — programming-interface related

Anything affecting:

UPDI pin configuration

RESET

should require explicit acknowledgement.

Class D — security/irreversible or difficult-to-recover

Anything involving:

PDID

lock/security configuration

must require a very strong warning and should preferably be inaccessible through the basic hobbyist console.

For the AVR DU PDID feature in particular, Microchip explicitly warns that once the strongest NVM-access-disabled configuration is activated, external UPDI NVM modification—including chip erase—can be disabled.

Do not put that operation behind:

press Y to continue

alone.

Make the user type something explicit such as:

ENABLE-PDID

and still display the consequences.


158. USERROW on AVR DU

The AVR DU USERROW is:

512 bytes

base address 0x1200

and it survives chip erase.

The USERROW is useful for:

board serial number

calibration data

manufacturing data

configuration

hardware revision

which is exactly why Microchip keeps it separate from ordinary application Flash.

On an unlocked AVR DU, USERROW can be accessed through normal system-space operations.

On a locked device, Microchip provides a special USERROW-write key sequence.

Do not casually test locked-user-row programming until ordinary programming is completely reliable.


159. USERROW Differs on AVR DD

Our AVR16DD28 alternative illustrates another device-specific difference.

On AVR DD:

USERROW base = 0x1080

and the USERROW programming page associated with the relevant mechanism is documented as:

32 bytes

rather than the 512-byte DU USERROW architecture.

So:

AVR16DU28 USERROW:

512 B, base 0x1200

AVR16DD28 USERROW:

different organization, base 0x1080

This is precisely the kind of difference our DeviceDescriptor and NVM backend must represent.


160. NVM Backend Architecture

By now our firmware needs a formal abstraction.

Something like:

class NvmBackend

{

public:

virtual NvmError waitReady**()** = 0;

virtual NvmError eraseChip**()** = 0;

virtual NvmError eraseFlashPage(

    uint32\_t address) = 0;

virtual NvmError writeFlash(

    uint32\_t address,

    const uint8\_t \*data,

    size\_t length) = 0;

virtual NvmError writeEeprom(

    uint16\_t address,

    const uint8\_t \*data,

    size\_t length) = 0;

virtual NvmError writeFuse(

    uint16\_t address,

    uint8\_t value) = 0;

};

On an ATmega328P Nano we probably would not use C++ virtual dispatch quite this heavily because Flash and SRAM are limited.

But this expresses the architecture.

A smaller embedded implementation could instead use a function table:

struct NvmOps

{

NvmError (*eraseChip**)()**;

NvmError (*erasePage**)(**uint32_t);

NvmError (*writeFlash**)(**

    uint32\_t,

    const uint8\_t \*,

    size\_t);

};

Then:

device says NVM P:4

 

select nvm_p4_ops

The higher layers no longer care whether the target is DU or DD.


161. Let the Nano Do Low-Level Work; Let the PC Do Big Work

The ATmega328P Nano contains limited resources:

32 KB Flash

2 KB SRAM

Our AVR16DU28 target contains a 512-byte Flash page.

We can buffer a whole page in the Nano’s 2 KB SRAM, but doing so consumes a substantial fraction of available RAM once serial buffers, protocol state, strings, and stack are included.

A better division of labor is:

PC host:

parse files

hold firmware image

merge pages

compare images

display progress

maintain device database

Nano:

perform precise UPDI timing

execute target transactions

move small blocks

report status

This gives us:

smart host

+

small deterministic embedded programmer

which is an excellent architecture.


162. Our Host Protocol

We should now replace free-form development commands with a machine-friendly protocol while keeping the human console available.

A simple textual protocol is perfectly adequate for teaching.

For example:

INFO

SIG

SIB

READ 8000 0100

ENTER

ERASE CHIP

ERASE PAGE 8000

WRITE 8000 40 <hex-data>

VERIFY 8000 40 <hex-data>

LEAVE

Responses:

OK

or:

ERR TIMEOUT

ERR LOCKED

ERR NVM WRITEPROTECT

ERR VERIFY 83A6 7C FC

Text commands are not maximally efficient.

They are extremely easy to debug.

That is the right tradeoff at this stage.


163. A Better Binary Protocol Can Come Later

Once the architecture works, a binary host protocol can provide:

framing

length

command ID

sequence ID

payload

CRC

such as:

SYNC

CMD

SEQ

LENL

LENH

PAYLOAD…

CRC16

But there is little reason to debug:

binary framing

while simultaneously debugging:

UPDI NVM programming

Use the simplest host protocol that allows us to prove the target protocol.


164. Python Host Program

Our first host utility can use:

Python 3.12+

PySerial

A minimal skeleton:

from __future__ import annotations

import serial

class NanoUpdi:

def __init__(

    self,

    port: str,

    baud: int = 115200,

) -> None:

    self.ser = serial.Serial(

        port=port,

        baudrate=baud,

        timeout=2.0,

)

def command(self, text: str) -> str:

    self.ser.write(

(text + “\n”).encode(“ascii”)

)

    response = self.ser.readline**()**

if not response:

        raise TimeoutError(

“Nano programmer did not respond”

)

return response.decode(

“ascii”,

        errors="replace",

).strip**()**

Then:

programmer = NanoUpdi(“/dev/ttyUSB0”)

print(programmer.command(“INFO”))

print(programmer.command(“SIG”))

Eventually:

$ python nanoupdi.py info

Programmer:

NanoUPDI 0.3

Target:

AVR16DU28

Signature: 1E 94 39

Revision: A1

Flash: 16384 bytes

EEPROM: 256 bytes

NVM: P:4


165. Parse Intel HEX on the PC, Not the Nano

Arduino IDE normally creates Intel HEX output when compiling classic AVR firmware.

Intel HEX is a text format where each record begins with:

:

followed by fields representing:

byte count

address

record type

data

checksum

For example:

:10010000214601360121470136007EFE09D2190140

Breaking it apart:

: start

10 byte count = 16

0100 record address

00 record type = data

214601360121470136007EFE09D21901

16 data bytes

40 checksum

The Nano should not need to know any of this.

The PC should parse the HEX file and send binary program bytes to the Nano.


166. Intel HEX Checksum

Intel HEX uses an 8-bit two’s-complement checksum.

The sum of:

length

address high

address low

record type

data bytes

checksum

modulo 256 should equal zero.

A Python parser can verify:

def checksum_valid(record: bytes) -> bool:

return sum(record) & 0xFF == 0

Do not program a HEX record whose checksum fails.

The point of the checksum is to catch file corruption.

Ignoring it would defeat the format’s safety mechanism.


167. Use an Existing HEX Parser or Test Ours Thoroughly

For a production-quality host utility we can use a well-established Intel HEX library.

For educational purposes we may implement a small parser ourselves so that the reader understands the format.

If we do, tests are mandatory.

Test:

data records

EOF

extended segment address

extended linear address

bad checksum

truncated record

invalid hex character

overlapping ranges

out-of-device addresses

A firmware programmer is not a good place for a casual file parser.


168. Address Range Validation

Suppose somebody accidentally loads a HEX file intended for an AVR32DU28 into our identified AVR16DU28.

The file may contain addresses beyond:

0x3FFF

because:

16 KB = 0x4000 bytes

Our host must reject it.

Do not rely on the target to reject a nonsense address.

if highest_program_address >= device.flash_size:

raise ValueError(

“Firmware image exceeds target Flash”

)

Similarly:

AVR16DU28 Flash:

0x0000–0x3FFF

AVR32DU28 Flash:

0x0000–0x7FFF

in image-relative byte addresses.


169. Build a Sparse Memory Image

Intel HEX files are sparse.

They may contain:

data at 0x0000

nothing for 200 bytes

data at 0x0300

That does not necessarily mean we should explicitly program every missing byte.

A useful host representation is:

image: dict[int, int]

where:

address → byte

or page-oriented structures.

For full chip erase + write, absent bytes may remain erased:

0xFF

unless the intended image semantics require otherwise.


170. Page-Oriented Host Processing

Because our AVR DU Flash erase size is:

512 bytes

the host can divide the image into:

page 0

page 1

page 2

Each page buffer begins as:

512 × 0xFF

Then HEX data overlays the appropriate offsets.

Conceptually:

PAGE_SIZE = 512

page = bytearray(

[0xFF] * PAGE_SIZE

)

page[offset] = value

For an AVR16DU28 we have:

32 possible Flash pages

For AVR32DU28:

64 pages


171. Skip Completely Empty Pages

After chip erase:

all Flash = 0xFF

So if a 512-byte host page contains only:

0xFF

there is usually no reason to send it.

That can dramatically reduce programming time for sparse images.

Our host can simply ask:

if all(value == 0xFF for value in page):

continue

Then send only pages containing meaningful data.


172. Programming Flow for the AVR16DU28

A sensible complete programming flow is:

open serial connection

 

wait for Nano READY

 

UPDI connect

 

read SIB

 

read signature

 

identify AVR16DU28

 

measure VTARGET on A0

 

confirm voltage is valid for target/profile

 

check target voltage

 

read lock state

 

enter programming mode

 

chip erase if requested

 

for each used Flash page:

    write block

    ↓

write EEPROM if supplied

 

write approved fuses if supplied

 

read Flash

 

verify

 

read EEPROM

 

verify

 

leave programming mode

 

reset target

Notice what comes before erase:

identify target

Always.


173. Target Voltage Is Already Available to the Programmer

Unlike the earlier draft of this article, the finalized Nano hardware already measures VTARGET.

The signal is:

A0 = VTARGET_SENSE

through a protected divider:

VTARGET
   |
 22 kΩ
   |
   +---- 1 kΩ ---- A0
   |
 10 kΩ
   |
  GND

with a Schottky clamp to the Nano +5 V rail and a 0.1 µF filter capacitor.

The nominal divider ratio is:

[
V_{A0}=V_{TARGET} rac{10}{22+10}=0.3125V_{TARGET}
]

so the firmware can estimate:

[
V_{TARGET}pprox3.2V_{A0}
]

Representative values are:

VTARGETA0 divider node
1.8 V0.563 V
3.3 V1.031 V
5.0 V1.563 V
5.5 V1.719 V

The host should therefore log target voltage before programming:

Target VDD: 5.02 V

and use the device descriptor to decide whether the measured voltage is acceptable for the selected target and requested UPDI speed.

This matters because NVM programming and maximum UPDI bit rate are both subject to target electrical limits.

The external HV rail is measured independently on:

A1 = HV_SENSE

through the 33 kΩ / 10 kΩ protected divider. Part III normally leaves HV disabled, but reporting the measured HV input is still useful because it proves that the safety-monitoring path is operational before Part IV begins using it.

One operational caution remains: because the ADC clamps return to the Nano +5 V rail, the Nano should normally be powered before external VTARGET or HV is applied so that the clamp network cannot unintentionally back-power the Nano rail.

174. Verification Should Produce a Digest Too

Byte-for-byte verification is the authoritative check.

But a checksum or digest is useful for logging.

For example:

FLASH VERIFY: OK

CRC32: 9C37A6B2

Then production records can say:

Board serial: MAR-000123

MCU: AVR16DU28

Firmware: 1.2.4

Flash CRC32: 9C37A6B2

Programmer FW: NanoUPDI 0.3

Date: …

That becomes useful when the project graduates from breadboard experimentation to manufacturing fixtures.


175. The Nano UPDI Sniffer Becomes Especially Valuable Here

Part III finally gives our companion analyzer something substantial to decode.

A programming capture may look like:

KEY NVMPROG

RESET ASSERT

RESET RELEASE

ASI_SYS_STATUS → PROGSTART

NVM STATUS → READY

NVM CTRLA ← FLPER

FLASH[0x8000] ← FF

NVM STATUS → FBUSY

NVM STATUS → READY

NVM CTRLA ← FLWR

FLASH[0x8000…] ← block data

NVM STATUS → READY

NVM CTRLA ← NOCMD

Our analyzer can compare:

what our source intended

with:

what actually occurred

on the wire.

That is extraordinarily useful if programming fails.


176. Compare Against a Known-Good Programmer

The analyzer is even more useful if we capture a known-good programmer.

For example:

PICkit 4

Curiosity Nano debugger

Atmel-ICE

programming the same target.

Then compare:

known-good traffic

     vs

our Nano traffic

Suppose ours fails.

The comparison may reveal:

ours forgot reset

ours sent wrong key order

ours didn’t poll PROGSTART

ours used wrong NVM command

ours wrote byte instead of word

ours did not clear NOCMD

ours addressed 0x0000 instead of 0x8000

This turns reverse engineering into controlled diagnosis.


177. Test Flash Programming with a Tiny Known Program

Do not begin with a 15 KB application.

Create a tiny program in Arduino IDE that produces an unmistakable behavior.

For example:

void setup**()**

{

pinMode(LED_BUILTIN, OUTPUT);

}

void loop**()**

{

digitalWrite(

    LED\_BUILTIN,

    !digitalRead(LED\_BUILTIN)

);

delay(500);

}

The actual target pin/board arrangement may differ, so a bare AVR16DU28 breadboard will need an explicitly selected GPIO and LED.

The idea is:

small image

known behavior

easy verification

Compile it.

Obtain the HEX image.

Program it using our Nano.

Reset.

Observe the LED.

That gives us:

logical verification

+

byte verification

Both are useful.


178. Add a Program-Memory Test Pattern

Before relying on compiler-generated firmware, another useful experiment is to program a deliberately recognizable byte pattern into a sacrificial page.

For example:

00 01 02 03 04 05 …

or:

55 AA 55 AA …

Then read it back.

This helps find:

byte swapping

word swapping

address misalignment

repeat count errors

pointer increments

off-by-one errors

far more easily than random compiler output.


179. A Word-Order Test

Because Flash writes are naturally word-oriented, deliberately write:

Address 0x0000: 12 34

Address 0x0002: 56 78

Address 0x0004: 9A BC

Then verify that the target reads back:

12 34 56 78 9A BC

and not:

34 12 78 56 BC 9A

This isolates exactly the sort of endianness error we discussed in Part II.


180. Boundary Tests Matter

Test:

first byte of Flash

last byte of Flash

first byte of a page

last byte of a page

first word after a page boundary

first EEPROM byte

last EEPROM byte

first USERROW byte

last USERROW byte

Many programming bugs hide at boundaries.

If normal addresses work but:

0x01FF

0x0200

do not, you have discovered a page-boundary bug.


181. Test AVR16DU28 and AVR32DU28

Once the AVR16DU28 works, plug in an AVR32DU28-I/SP.

The NVM backend should remain:

P:4

while the device geometry changes to:

Flash 32 KB

SRAM 4 KB

64 Flash pages

signature 1E 95 40

Microchip documents those differences directly.

If we designed the abstraction well, changing targets should require:

new descriptor

not:

rewrite programmer

That will be an excellent architectural test.


182. Then Test the AVR16DD28

The AVR16DD28 is the more interesting validation target.

It has:

16 KB Flash

2 KB SRAM

256 B EEPROM

512-byte Flash pages

just like the AVR16DU28 in several respects, but its system-memory arrangement differs and its NVM architecture belongs to an earlier generation.

This target answers a critical engineering question:

Did we really separate UPDI from NVMCTRL, or did we accidentally write an AVR16DU28-only programmer?

If changing NVM backends works cleanly, our architecture is sound.


183. Arduino Nano Resource Limits

As the firmware grows, remember that the ATmega328P has only:

2 KB SRAM

Large mistakes include:

uint8_t flashPage[512];

uint8_t verifyPage[512];

uint8_t receiveBuffer[512];

char console[512];

Those four arrays already consume essentially the Nano’s entire SRAM before the stack and globals.

Better:

one 64- or 128-byte transfer buffer

and let Python hold the page.

For example:

PC has 512-byte page

send bytes 0–127

Nano writes them

send bytes 128–255

Nano writes them

send bytes 256–383

Nano writes them

send bytes 384–511

Nano writes them

The exact chunk size can be tuned.


184. Suggested Host Transfer Block

A reasonable starting point is:

64 bytes

Why 64?

It is large enough to reduce host protocol overhead.

It is small enough for:

Nano SRAM

serial buffers

stack

to remain comfortable.

It also divides evenly into:

512-byte Flash page

because:

512 / 64 = 8

That makes progress accounting simple.


185. Host-Side Test Suite

Our Python project should have automated tests for:

Intel HEX parsing

checksum detection

page assembly

address limits

signature lookup

device descriptors

word ordering

program block splitting

verification mismatch reporting

fuse safety classification

Hardware-in-the-loop tests can additionally run against the actual Nano.

For example:

def test_signature_avr16du28(

programmer

):

assert programmer.signature**()** == bytes(

[0x1E, 0x94, 0x39]

)

and:

def test_1000_signature_reads(

programmer

):

for _ in range(1000):

    assert programmer.signature**()** == bytes(

[0x1E, 0x94, 0x39]

)

This is exactly the embedded test-firmware philosophy from Part II, now extended into automated host testing.


186. Tests Before Refactoring

As our firmware becomes more capable, we will inevitably improve its structure.

Before refactoring:

capture passing tests

commit

Then refactor.

Then:

run all tests

That lets us improve readability without accidentally changing programmer behavior.

A useful Git sequence might be:

Implement AVR DU signature reading

Add NVMPROG entry procedure

Add NVM P4 status polling

Add AVR DU Flash page erase

Add AVR DU Flash write and verify

Add EEPROM programming

Add Intel HEX parser

Add Python hardware-in-loop tests

Commit before moving into fuse or HV work.


187. Suggested CLI

Eventually our Python host might support:

nanoupdi info

nanoupdi read flash.bin

nanoupdi erase

nanoupdi program firmware.hex

nanoupdi verify firmware.hex

nanoupdi eeprom-read eeprom.bin

nanoupdi eeprom-write eeprom.bin

nanoupdi fuses

and guarded commands such as:

nanoupdi fuse-write SYSCFG0 0x12

The dangerous operations should be noticeably different.

For example:

nanoupdi security enable-pdid

should produce a major warning rather than behave like an ordinary fuse write.


188. info Should Become Our Most Useful Command

A well-designed info command could report:

NanoUPDI Programmer

——————-

Programmer firmware: 0.3.0

Host software: 0.3.0

UPDI baud: 62500

Target:

Device: AVR16DU28

Signature: 1E 94 39

Silicon revision: 1.0

NVM architecture: P:4

Memory:

Flash: 16384 bytes

Flash page: 512 bytes

EEPROM: 256 bytes

USERROW: 512 bytes

BOOTROW: 256 bytes

State:

Locked: no

Programming mode: no

UPDI: connected

Hardware:

UPDI TX: D6
UPDI RX: D5
Target reset: D4
HV enable: D7
Target power: D8

Target voltage:

VTARGET (A0): 5.01 V
HV sense (A1): 0.00 V

This becomes the starting point for every troubleshooting session.


189. What About AVRDUDE?

Eventually we will probably want:

avrdude

support.

But now we can see why it should come later.

AVRDUDE integration adds another translation layer:

AVRDUDE

|

v

programmer protocol

|

v

Nano firmware

|

v

UPDI

|

v

NVMCTRL

Projects such as jtag2updi historically emulate a programmer protocol that AVRDUDE already understands.

Modern AVRDUDE also contains direct serial-UPDI support code and device-specific NVM handling. Its source provides a useful reference for programming-mode entry and SIB handling.

We should study it.

We should not make it our first debugging layer.


190. Microchip’s pymcuprog Is an Excellent Reference

Microchip’s open-source pymcuprog project is particularly valuable for this part of the article.

Its source explicitly contains:

serialupdi/nvmp4.py

for the AVR DU P:4 controller and defines:

NVMCTRL offsets

Flash write command

Flash page erase command

EEPROM commands

chip erase command

status handling

That gives us a valuable three-way comparison:

Microchip data sheet

|

   \+

Microchip pymcuprog

|

   \+

our Nano implementation

When all three agree, confidence goes up considerably.


191. Do Not Blindly Copy Reference Code

Reference code tells us:

what a working implementation does

The data sheet tells us:

why it is allowed

We want both.

For every important operation:

1. read Microchip datasheet procedure

2. inspect Microchip reference implementation

3. implement our own clearly structured equivalent

4. test with scope/analyzer

5. compare results

That produces understanding instead of cargo-cult programming.


192. Programming Failure Diagnostics

A useful error taxonomy now becomes:

UPDI physical

line stuck

framing

parity

timeout

UPDI protocol

ACK missing

KEY rejected

PROGSTART timeout

security

locked

PDID

write protected

NVM

busy timeout

invalid command

write protect

command collision

verification

data mismatch

host

HEX checksum

image too large

wrong device

These should not all collapse into:

PROGRAMMING FAILED


193. Example Failure Report

A good host might say:

ERROR: Flash verification failed

Target:

AVR16DU28

Signature: 1E 94 39

Flash address:

0x023A

UPDI data-space address:

0x823A

Expected:

0x7E

Read:

0xFE

Page:

1

NVM STATUS:

0x00

Recommendation:

Repeat read before reprogramming.

This immediately suggests:

write/transfer issue

rather than:

NVM controller reported an error

because STATUS says none.

Good diagnostics reduce debugging time dramatically.


194. Production-Style Verification Later

For hobby programming, immediate verify is enough.

For a manufacturing tool we may eventually want:

program

verify

reset

run brief firmware self-test

and perhaps:

write serial number to USERROW

or application EEPROM.

That is one reason USERROW support is worth implementing properly.

The Nano programmer could eventually become part of:

pogo-pin fixture

+

power control

+

UPDI programming

+

functional test

without changing its fundamental protocol engine.


195. Our Programming State Machine

At this point a formal state machine is helpful.

For example:

Disconnected

|

v

Connected

|

v

Identified

|

+---- locked ----> EraseRequired

|

v

ProgrammingMode

|

+----> Erasing

|

+----> Writing

|

+----> Verifying

|

v

Complete

Errors transition to:

Error

where we:

release bus

clear NVM command if possible

preserve diagnostics

avoid accidental follow-up writes

That is safer than allowing arbitrary commands in arbitrary order.


196. Flash-Programming Test Sequence

For the first real AVR16DU28 Flash test I recommend:

1. Read and save complete original Flash.

2. Read and save EEPROM.

3. Read and save all fuses.

4. Confirm signature = 1E 94 39.

5. Enter programming mode.

6. Erase one expendable Flash page

or perform full chip erase if target is sacrificial.

7. Read erased area.

8. Confirm FF.

9. Program known test pattern.

10. Read it.

11. Compare byte-for-byte.

12. Reset target.

13. Repeat experiment 100 times if using

an expendable test device.

14. Record all failures.

This is much more informative than attempting a complete bootable application first.


197. EEPROM Test Sequence

Then:

1. Read EEPROM byte 0.

2. Save original.

3. Write 0x55.

4. Read back.

5. Verify 0x55.

6. Write 0xAA.

7. Read back.

8. Verify 0xAA.

9. Restore original.

10. Verify restored value.

This deliberately exercises contrasting bit patterns.


198. Fuse Test Sequence

Fuse tests should begin with:

read only

Display every fuse and decoded meaning.

For example:

FUSE.SYSCFG0

raw: 0xXX

CRCSRC: …

CRCSEL: …

UPDIPINCFG: …

RSTPINCFG: …

BROWSAVE: …

EESAVE: …

Only after the decoder itself has been verified against Microchip documentation should we permit writes.

A fuse editor that decodes fields incorrectly is worse than no fuse editor.


199. Do Not Guess Fuse Defaults

Fuse defaults vary by family and sometimes device generation.

Our software should load:

documented valid masks

documented defaults

allowed values

danger classification

from the device definition.

The user interface may say:

Current: 0x03

Default: 0x03

but only if that default is actually documented for the identified target.

Do not infer a default merely because another AVR uses it.


200. The AVR16DU28, AVR32DU28, and AVR16DD28 Comparison So Far

At this point our reference devices look roughly like:

PropertyAVR16DU28AVR32DU28AVR16DD28
Signature1E 94 391E 95 401E 94 32
Flash16 KB32 KB16 KB
Flash page512 B512 B512 B
EEPROM256 B256 B256 B
SRAM2 KB4 KB2 KB
NVMCTRL base0x10000x10000x1000
EEPROM base0x14000x14000x1400
SIGROW base0x10800x10800x1100
USERROW base0x12000x12000x1080
DU NVM P:4YesYesNo

The DU values come directly from Microchip’s DU data sheet and P:4 implementation. The DD memory-map and page values come from its own data sheet.

This table is exactly why device-specific programming data belongs in a descriptor.


201. Leave Programming Mode Cleanly

Microchip’s programming procedure concludes by issuing another system reset after programming and releasing it.

So after:

write

verify

we should:

reset assert

reset release

rather than simply disconnecting the UPDI wire and assuming the target will resume cleanly.

A high-level function:

UpdiError leaveProgrammingMode**()**

{

UpdiError err =

targetReset**(true)**;

if (err != UpdiError::None)

return err;

return targetReset**(false)**;

}

Later we may deliberately disable UPDI or change interface configuration.

For now, simple controlled reset is preferable.


202. Save Before Destructive Operations

Our Python host should make backups easy.

For example:

nanoupdi backup target-backup/

which stores:

flash.bin

eeprom.bin

fuses.json

userrow.bin

device.json

before an experiment.

Then:

nanoupdi restore target-backup/

can restore recoverable areas.

This is especially useful during article experimentation.


203. Git the Test Firmware Too

The tiny firmware images we use for programming tests should live in the repository.

For example:

tests/targets/

blink/

gpio\_pattern/

flash\_pattern/

eeprom\_test/

Then an automated integration test can:

build test image

program

verify

reset

observe expected result

This turns the tutorial into a reproducible engineering project.


204. Use the Arduino IDE as the Target Toolchain

Because this article assumes the Arduino IDE as our development environment, our target examples should show how to obtain the compiled program image from that toolchain.

The exact menu labels can change between Arduino IDE releases, but the important workflow is:

create sketch

 

select target/core

 

compile

 

locate/export compiled binary or HEX

 

feed HEX to NanoUPDI host

For an AVR16DU28, Arduino support depends on the installed core/package that provides the DU device definition.

Our programmer itself does not care which compiler produced the bytes.

It cares only that:

image

+

target

+

address range

are compatible.


205. Host Programmer Architecture

Our Python application should now look approximately like:

nanoupdi/

|

+– cli.py

+– serial_link.py

+– programmer.py

+– intelhex.py

+– devices.py

+– memory_image.py

+– verify.py

+– errors.py

|

+– tests/

+-- test\_hex.py

+-- test\_devices.py

+-- test\_pages.py

+-- test\_verify.py

Meanwhile the Nano firmware contains:

firmware/

|

+– NanoUPDI.ino

+– updi_phy.cpp

+– updi_link.cpp

+– updi_instruction.cpp

+– updi_device.cpp

+– nvm_p4.cpp

+– nvm_p2.cpp

+– host_protocol.cpp

The final project remains understandable because each layer has one job.


206. Our End-to-End Architecture Now

We can finally draw the complete programmer using the actual finalized electrical interface:

                   Arduino IDE
                       |
                       |
                  firmware.hex
                       |
                       v

              +------------------+
              | Python Host      |
              |                  |
              | Intel HEX parser |
              | device database  |
              | page manager     |
              | verification     |
              +--------+---------+
                       |
                       | USB serial
                       |
                       v

              +---------------------------+
              | Arduino Nano              |
              |                           |
              | host protocol             |
              | NVM backend               |
              | UPDI command layer        |
              | UPDI link layer           |
              | UPDI PHY                  |
              |                           |
              | D5 = UPDI_RX              |
              | D6 = UPDI_TX              |
              | D4 = TARGET_RESET_CTRL    |
              | D7 = HV_ENABLE            |
              | D8 = TARGET_POWER_ENABLE  |
              | A0 = VTARGET_SENSE        |
              | A1 = HV_SENSE             |
              +-------------+-------------+
                            |
                  +---------+---------+
                  | UPDI transistor   |
                  | translator        |
                  |                   |
                  | Q4 = TX pull-down |
                  | Q2 = RX translator|
                  +---------+---------+
                            |
                            | one-wire UPDI_DATA
                            |
                            v

              +---------------------------+
              | AVR16DU28                 |
              |                           |
              | UPDI                      |
              | system bus                |
              | NVMCTRL P:4               |
              | Flash                     |
              | EEPROM                    |
              | Fuses                     |
              | USERROW                   |
              +---------------------------+

Other target connections:

TARGET_RESET  <---- D4 transistor driver
VTARGET       ----> A0 protected divider
GND           ----- common reference

External HV path, normally disabled in Part III:

bench HV --> switched HV stage --> route/arm selector --> HV_OUT or RESET-HV
                         |
                         +--> A1 protected HV measurement

This is no longer the original D6 → 4.7 kΩ → UPDI teaching circuit.

The target still sees one UPDI conductor, but the Nano now has independent TX and RX GPIOs, voltage translation, target-voltage sensing, HV sensing, reset control, and optional target-power control.

That is the architecture of the actual programmer we are building.

207. What We Have Accomplished in Part III

We began with an Arduino Nano that merely knew how to exchange UPDI bytes.

We now understand how to turn those bytes into NVM programming.

For our primary:

AVR16DU28-I/SP

we established:

Signature:

1E 94 39

Flash:

16 KB

Flash page:

512 bytes

SRAM:

2 KB

EEPROM:

256 bytes @ 0x1400

NVMCTRL:

P:4 @ 0x1000

SIGROW:

0x1080

FUSE:

0x1050

BOOTROW:

0x1100

USERROW:

0x1200

The AVR32DU28 uses the same DU NVM generation but doubles Flash and SRAM, with signature 1E 95 40.

Our AVR16DD28 alternative keeps the same 16 KB Flash and 512-byte page size but demonstrates why memory maps and NVM implementations must remain device-specific.

We implemented the conceptual sequence for:

device identification

NVM key activation

reset sequencing

PROGSTART polling

lock detection

Flash reads

page erase

Flash write

EEPROM write

fuse access

verification

We also established the AVR DU P:4 command model:

FLWR 0x02

FLPER 0x08

EEWR 0x12

EEERWR 0x13

EEBER 0x18

CHER 0x20

EECHER 0x30

and tied those values both to Microchip’s data sheet and its own open-source programming implementation.

Most importantly, our programmer now knows that:

UPDI

NVMCTRL

and:

AVR DU

AVR DD

even though both use UPDI.

That distinction is the difference between a one-device demonstration and a real extensible programmer.


208. Before Part IV: Commit the Working Programmer

This is an excellent place for a development checkpoint.

Before introducing high voltage:

all ordinary UPDI tests should pass

D6 TX / D5 RX translator tests should pass

VTARGET sensing on A0 should be calibrated and checked

HV sensing on A1 should be checked with HV disabled and with a safe bench value

physical target RESET control on D4 should be checked

all NVM programming tests should pass

Flash verify should pass

EEPROM verify should pass

device identification should pass

error paths should be tested

Then commit.

For example:

git commit -m “Implement verified AVR DU UPDI programming”

Tag it if desired:

git tag v0.3.0-normal-updi

Why?

Because Part IV introduces:

external high voltage

switching hardware

RESET sequencing

target power handling

If something breaks after that, we want an unquestionably working ordinary-UPDI baseline.


209. Part IV Preview — High-Voltage UPDI Without Destroying Anything

The next part takes us into one of the most misunderstood areas of AVR programming.

We will add externally supplied high-voltage activation while keeping the Nano electrically isolated from the HV rail.

For our primary:

AVR16DU28-I/SP

and the AVR32DU28/AVR16DD28 examples, we will discuss the modern RESET-pin HV activation mechanism, including approximately:

VHV minimum:

VDD + 2 V

typical:

around 7.5 V

maximum:

8.5 V

minimum pulse:

about 10 µs

valid key window:

about 65 ms

for the appropriate documented devices—not the older casual assumption that every UPDI target wants 12 V.

Then we will separately cover the older approximately:

12 V

shared-UPDI-pin activation mechanism used by appropriate tinyAVR devices.

We will design several hobbyist-friendly switching choices:

simple BJT

2N3904-style arrangement

simple MOSFET

BSS138 / 2N7000 / 2N7002 style

MOSFET + BJT

combined isolation/switching

while obtaining the actual HV rail from a bench power supply.

We will cover:

why D7 controls only the low-voltage HV switching stage

how the D6/D5 transistor UPDI interface isolates Nano GPIO from target-side HV

RESET versus UPDI HV destination

target power sequencing

HV pulse generation

timing

65 ms key window

wrong-voltage protection

manual bench-supply operation

software interlocks

target-voltage sensing on A0

HV sensing on A1

ADC dividers, filtering, and series protection

safe test procedure

locked-target recovery

UPDI-pin recovery

and use the Nano UPDI Sniffer/Analyzer to confirm what happens immediately after the high-voltage event.

Only after that works will we proceed to the final major subject:

using UPDI as an actual on-chip debugger.


Primary Resources Used in Part III

Microchip’s current AVR16DU28 product page provides the current complete AVR DU data sheet and should remain the authoritative starting point for the target device.

Microchip AVR16DU28 product page

The AVR DU data sheet documents the DU memory map, device signatures, Flash geometry, NVMCTRL registers and commands, UPDI keys, programming-mode entry, reset requests, and security behavior.

The AVR16/32DD28/32 data sheet provides the corresponding reference material for our AVR16DD28 alternative.

Microchip’s open-source pymcuprog implementation provides a particularly useful real-world reference for NVM P:4 on AVR DU, including the Flash-write, Flash-page-erase, EEPROM, chip-erase, and NVM-status procedures.

Microchip pymcuprog NVM P:4 source

AVRDUDE’s current serial-UPDI implementation is another valuable independent reference for NVM-key entry, reset sequencing, SIB access, and programming-state checks.

AVRDUDE serial-UPDI source

And our companion analyzer remains:

Nano UPDI Sniffer / Analyzer repository

IMPLEMENTING UPDI FROM SCRATCH

All You Ever Needed To Know About UPDI, And Then Some!

Leave a Reply

Your email address will not be published. Required fields are marked *