IMPLEMENTING UPDI FROM SCRATCH
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
0x55synchronization 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 pin | Function |
|---|---|
| D2 | Activity/status indication |
| D3 | HV/status indication |
| D4 | TARGET_RESET_CTRL |
| D5 | UPDI_RX |
| D6 | UPDI_TX |
| D7 | HV_ENABLE |
| D8 | TARGET_POWER_ENABLE |
| A0 | VTARGET_SENSE |
| A1 | HV_SENSE |
| A4/A5 | Optional I²C expansion |
| D10–D13 | Optional 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 | |
| AVR16DU28 | 1E 94 39 |
| AVR32DU28 | 1E 95 40 |
| AVR16DD28 | 1E 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:
- If required, erase/unlock the device.
- Send the NVMPROG key with UPDI
KEY. - Optionally verify the NVMPROG key status.
- issue a system reset through
ASI_RESET_REQ. - release the reset.
- poll
ASI_SYS_STATUS.PROGSTART. - when
PROGSTART == 1, begin NVM programming. - perform programming.
- reset the system again when finished.
- 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:
- Send the CHIPERASE key.
- Send the NVMPROG key.
- verify both key-status bits.
- apply reset through
ASI_RESET_REQ. - release reset.
- poll
LOCKSTATUS. - when
LOCKSTATUS == 0, the erase/unlock has completed. - check
ERASEFAILwhere 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:
| VTARGET | A0 divider node |
| 1.8 V | 0.563 V |
| 3.3 V | 1.031 V |
| 5.0 V | 1.563 V |
| 5.5 V | 1.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 | |||
| Signature | 1E 94 39 | 1E 95 40 | 1E 94 32 |
| Flash | 16 KB | 32 KB | 16 KB |
| Flash page | 512 B | 512 B | 512 B |
| EEPROM | 256 B | 256 B | 256 B |
| SRAM | 2 KB | 4 KB | 2 KB |
| NVMCTRL base | 0x1000 | 0x1000 | 0x1000 |
| EEPROM base | 0x1400 | 0x1400 | 0x1400 |
| SIGROW base | 0x1080 | 0x1080 | 0x1100 |
| USERROW base | 0x1200 | 0x1200 | 0x1080 |
| DU NVM P:4 | Yes | Yes | No |
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.
And our companion analyzer remains: