IMPLEMENTING UPDI FROM SCRATCH
Post Stastics
- This post has 8032 words.
- Estimated read time is 38.25 minute(s).
Part II — Building UPDI from Scratch with an Arduino Nano
In Part I we took UPDI apart conceptually.
We established that UPDI is not merely “serial programming on one wire.” It is a layered system consisting of:
- an electrical physical interface;
- UART-like framing;
- synchronization and BREAK mechanisms;
- a compact UPDI instruction set;
- memory-mapped access to the target;
- protected activation mechanisms;
- target-specific nonvolatile-memory controllers;
- and an on-chip debugging system.
Now we are going to begin making it work.
Rather than immediately downloading somebody else’s programmer firmware and treating the Arduino Nano as a mysterious appliance, we will construct the essential portions ourselves.
Our goals in this part are to:
- select a convenient UPDI target;
- build the finalized Nano UPDI electrical interface;
- understand how one external UPDI wire becomes separate TX and RX paths inside the programmer;
- support target voltages below the Nano’s fixed 5 V logic level;
- generate UPDI signaling;
- transmit correctly formatted 8E2 serial frames;
- receive frames from the target;
- generate BREAK;
- perform ordinary UPDI activation;
- send SYNCH;
- construct actual UPDI instructions;
- read UPDI’s own status registers;
- reach the target’s memory-mapped address space;
- measure VTARGET and the external HV rail;
- develop a repeatable test methodology;
- and introduce a companion UPDI protocol analyzer.
The purpose is not to outperform Microchip’s professional tools.
Nor are we trying to outperform mature open-source projects.
We are trying to understand the interface deeply enough that:
source code
=
timing diagrams
=
oscilloscope traces
=
actual UPDI transactions
all tell the same story.
45. First, a Correction About 225 kbaud
Many descriptions of UPDI leave the impression that it operates at:
225000 baud
as though that were a fixed UPDI baud rate.
It is not.
UPDI performs automatic baud synchronization from the:
0x55
SYNCH character transmitted at the beginning of each new instruction.
The target measures that incoming pattern and uses it to derive serial timing for the associated instruction and response.
There is therefore no ordinary initial UART baud-rate configuration transaction that the programmer must perform before the first communication.
For implementations using the traditional UPDI clock selections, representative maximum rates are approximately:
| UPDI clock | Representative maximum |
|---|---|
| 4 MHz | 225 kbaud |
| 8 MHz | 450 kbaud |
| 16 MHz | 0.9 Mbaud |
| 32 MHz on applicable newer devices | about 1.8 Mbaud |
The exact supported maximum depends upon:
- target family;
- target voltage;
- UPDI clock configuration;
- operating conditions;
- and the device’s own electrical specification.
Our Arduino Nano does not need to begin anywhere near those limits.
A polished programmer benefits from speed.
An educational programmer benefits from visibility.
We can therefore start around:
62,500 baud
At 62,500 baud:
[ T_{BIT} = \frac{1}{62500}=16\ \mu s ]
Our ATmega328P-based Nano runs at:
16 MHz
so:
[ 16,MHz\times16,\mu s=256 ]
CPU clocks are available during each bit.
That gives us enormous timing headroom.
At 225 kbaud:
[ T_{BIT}\approx4.444\ \mu s ]
and:
[ 16,MHz\times4.444,\mu s\approx71.1 ]
CPU clocks are available per bit.
That is still practical for carefully written AVR firmware, but considerably less forgiving while learning and debugging the protocol.
Our development sequence will therefore remain:
slow and observable
↓
proven reliable
↓
faster
↓
qualified
↓
optimized
rather than:
maximum speed
↓
mysterious intermittent failures
46. Choosing Our Experimental Target
For the primary target in this article we use:
AVR16DU28-I/SP
This is particularly convenient for a breadboard-based UPDI tutorial because the /SP version is supplied in a:
28-pin SPDIP package
That means:
- no breakout PCB is required;
- no SMD soldering is required;
- individual pins are readily accessible;
- a logic analyzer or oscilloscope probe can be attached easily;
- and the device can be replaced without reworking a board.
The AVR16DU28 provides approximately:
- 16 KB Flash;
- 2 KB SRAM;
- 256 bytes EEPROM;
- operation up to 24 MHz;
- USB Full-Speed device capability;
- SPI;
- TWI/I²C;
- USARTs;
- ADC and other modern AVR peripherals;
- and UPDI programming/debugging.
Its nominal operating supply range is:
1.8 V to 5.5 V
That makes it especially useful for testing the voltage-translation circuitry we have now added to our programmer.
Useful reference links
Microchip AVR16DU28:
DigiKey and Mouser can be searched using the exact ordering code:
AVR16DU28-I/SP
LCSC inventory varies, particularly for the through-hole package, so current availability should be checked rather than assumed from an old listing.
Datasheet
Our primary device document is Microchip’s current AVR DU family complete data sheet available from the AVR16DU28 product page.
Whenever this article and an old locally saved copy of a data sheet disagree:
use the current Microchip device documentation.
That is especially important for:
- absolute maximum ratings;
- HV activation;
- fuses;
- NVM behavior;
- UPDI timing;
- and silicon revisions.
46.1 Other Good UPDI Targets
The AVR16DU28-I/SP is not mandatory.
Two particularly useful alternatives are:
AVR32DU28-I/SP
This is the larger-memory sibling of our primary device.
It provides approximately:
32 KB Flash 4 KB SRAM 256 B EEPROM 24 MHz maximum CPU clock 28-pin SPDIP 1.8–5.5 V supply
and belongs to the same AVR DU family.
It is perhaps the easiest substitute for the AVR16DU28.
AVR16DD28-I/SP
This is another excellent breadboard target.
It provides approximately:
16 KB Flash 2 KB SRAM 256 B EEPROM 24 MHz maximum CPU clock 28-pin SPDIP 1.8–5.5 V supply
but belongs to the AVR DD family.
That makes it particularly educational because it lets us compare two modern UPDI families while keeping many gross characteristics similar.
The DD family also introduces features such as:
MVIO — Multi-Voltage I/O
which reinforce an important lesson:
Modern AVRs may operate in several voltage domains, so a serious programmer should not assume that its own 5 V logic level is automatically appropriate for the target.
46.2 Our Reference-Target Table
| Parameter | AVR16DU28-I/SP | AVR32DU28-I/SP | AVR16DD28-I/SP |
|---|---|---|---|
| Family | AVR DU | AVR DU | AVR DD |
| Package | 28-SPDIP | 28-SPDIP | 28-SPDIP |
| Flash | 16 KB | 32 KB | 16 KB |
| SRAM | 2 KB | 4 KB | 2 KB |
| EEPROM | 256 B | 256 B | 256 B |
| Maximum CPU clock | 24 MHz | 24 MHz | 24 MHz |
| Supply range | 1.8–5.5 V | 1.8–5.5 V | 1.8–5.5 V |
| 5 V compatible | Yes | Yes | Yes |
| UPDI | Yes | Yes | Yes |
The AVR16DD28 is deliberately included as a second-family reference.
We do not want to teach:
“The AVR16DU28 does this, therefore every UPDI AVR must do exactly this.”
That is one of the easiest ways to write a programmer that works beautifully on three devices and fails completely on the fourth.
47. From the Simplest UPDI Connection to Our Final Interface
The simplest same-voltage Arduino UPDI experiment can be built with approximately:
Nano D6 ---- 4.7 kΩ ---- target UPDI
when:
Nano VCC ≈ 5 V Target VDD ≈ 5 V
That configuration is historically useful and helps explain how projects such as jtag2updi can achieve UPDI with very little hardware.
But it is not the electrical interface we will use for the finalized article programmer.
Our final design is intended to be more useful.
It separates internal transmit and receive functions and electrically isolates the Nano’s 5 V GPIOs from the target UPDI voltage domain.
Externally:
TARGET | +---- one UPDI conductor
Internally:
Nano D6 = UPDI_TX Nano D5 = UPDI_RX
The two paths meet only at the target-side UPDI node.
47.1 The Final Target Connector
Our target connector contains four signals:
1 TARGET_RESET 2 VTARGET 3 UPDI 4 GND
Conceptually:
NANO PROGRAMMER TARGET AVR TARGET_RESET ---------------------- RESET VTARGET --------------------------- VDD UPDI ------------------------------ UPDI GND ------------------------------- GND
For ordinary UPDI communication, RESET is not necessarily required.
We expose it because it is useful for:
- controlled reset;
- debugging;
- automated testing;
- HV recovery on applicable modern devices.
48. Why We Split TX and RX
A single GPIO can implement half-duplex UPDI by switching between:
OUTPUT
and:
INPUT / high impedance
states.
That works.
But our Nano is fixed at approximately:
5 V logic
while our target might operate at:
1.8 V 3.3 V 5.0 V 5.5 V
and an older shared-pin UPDI device may briefly receive approximately:
12 V
during HV activation.
A bare Nano GPIO is therefore not the ideal universal electrical endpoint.
Our final architecture uses:
D6 → TX transistor → target UPDI target UPDI → RX transistor → D5
This gives us:
- target-voltage-domain isolation;
- 5 V Nano protection;
- easier lower-VDD support;
- old-style UPDI-HV tolerance;
- permanent RX availability;
- and much cleaner firmware direction handling.
49. The UPDI Transmit Stage
The TX path uses a 2N3904 as an open-collector pull-down.
Conceptually:
Nano D6 / UPDI_TX
|
47 kΩ
|
base
QTX
2N3904
|
collector
|
+---------------- TARGET UPDI
|
emitter
|
GND
A base-to-ground resistor keeps QTX OFF when the Nano is resetting and D6 is high impedance.
In the finalized design this biasing is deliberately weak so that it does not waste useful drive current.
Logical operation
When:
D6 = HIGH
QTX turns ON.
Therefore:
TARGET UPDI = LOW
When:
D6 = LOW
QTX turns OFF.
Therefore:
TARGET UPDI = released
The Nano never actively drives target UPDI HIGH.
That is one of the major advantages of this circuit.
The target’s own voltage domain establishes the HIGH level.
50. The UPDI Receive Stage
The RX path also uses a 2N3904.
Conceptually:
TARGET UPDI
|
47 kΩ
|
base
QRX
2N3904
|
emitter
|
GND
QRX collector
|
+---------------- Nano D5 / UPDI_RX
|
47 kΩ
|
+5 V
When target UPDI becomes HIGH, QRX receives base current and pulls:
Nano D5 LOW
When target UPDI becomes LOW, QRX turns OFF and the Nano-side pull-up raises:
Nano D5 HIGH
Thus:
| Target UPDI | Nano UPDI_RX |
|---|---|
| LOW | HIGH |
| HIGH | LOW |
The receive path is therefore:
inverted.
50.1 TX Is Inverted Too
The transmit stage is also logically inverted:
Nano UPDI_TX | QTX | Target UPDI |
|---|---|---|
| LOW | OFF | Released/HIGH |
| HIGH | ON | LOW |
This might look inconvenient at first.
In practice it is easy to hide behind a physical-layer abstraction.
For example:
inline void updiDriveLow()
{
// HIGH turns the NPN TX transistor on.
PORTD |= _BV(PD6);
}
inline void updiRelease()
{
// LOW turns the NPN TX transistor off.
PORTD &= ~_BV(PD6);
}
inline bool updiRead()
{
// RX transistor inverts target logic.
return !(PIND & _BV(PD5));
}
The rest of the program should never need to know that the transistors invert the signals.
It should think only in terms of:
drive low release read line
51. Why the New Interface Supports Lower Target Voltages Better
Suppose the target is powered from:
1.8 V
A bare 5 V Nano output could exceed the target’s permitted input level.
Our TX circuit does not have that problem.
The Nano only controls whether QTX is:
ON
or:
OFF.
When QTX is OFF, the target-side UPDI HIGH level is determined by the target itself.
Thus:
VTARGET = 1.8 V → UPDI HIGH ≈ target's 1.8 V domain VTARGET = 3.3 V → UPDI HIGH ≈ target's 3.3 V domain VTARGET = 5.0 V → UPDI HIGH ≈ target's 5 V domain
The Nano never imposes its own +5 V HIGH directly on the target.
52. Why the RX Interface Can Work at 1.8 V
Consider:
VTARGET ≈ 1.8 V RBASE = 47 kΩ
and approximate a BJT base-emitter drop around:
0.7 V
Then receiver base current is approximately:
[ I_B=\frac{1.8-0.7}{47k} ]
or about:
[ 23\ \mu A ]
The Nano RX collector pull-up is also:
47 kΩ
so QRX only needs to sink approximately:
[ \frac{5}{47k}\approx106\ \mu A ]
That requires an effective transistor current gain of less than:
5
to establish a strong LOW.
That is intentionally conservative.
The circuit therefore has good DC margin at low target voltage.
Dynamic timing still must be measured.
That distinction matters.
53. The Optional Target-Side Pull-Up
The target normally provides the UPDI idle-HIGH behavior itself.
Our final schematic nevertheless includes an optional pull-up-assist branch:
VTARGET | Schottky diode | 33 kΩ DNP | TARGET UPDI
The resistor is:
DNP — Do Not Populate
by default.
It exists because the actual rise time depends on:
- target internal pull-up strength;
- wiring capacitance;
- cable length;
- transistor capacitance;
- baud rate;
- target VDD.
If hardware testing shows that the target’s internal pull-up is sufficient:
leave R4 unpopulated.
That is our preferred outcome.
If testing shows that additional pull-up assistance significantly improves the waveform:
populate R4.
The circuit gives us that option without requiring a PCB change.
53.1 Why There Is a Diode in the Optional Pull-Up Branch
An older shared-pin UPDI device may intentionally receive approximately:
12 V
on the UPDI conductor during HV activation.
Without isolation, an external target pull-up would create:
12 V UPDI
|
pull-up resistor
|
VTARGET rail
which could backfeed the target supply.
The series Schottky diode is oriented so the optional branch can provide normal pull-up assistance from VTARGET toward UPDI while substantially blocking reverse current from the HV-charged UPDI node back into VTARGET.
This is another reason the optional pull-up is not simply a resistor from VTARGET to UPDI.
54. 12 V Tolerance of the UPDI Front End
The revised transistor interface also improves protection during old-style 12 V UPDI activation.
When approximately 12 V is deliberately applied to TARGET UPDI:
TX transistor
The TX 2N3904 is OFF and sees the elevated voltage at its collector.
A properly selected 2N3904 has collector-emitter voltage capability comfortably above this operating point.
RX transistor
The 12 V reaches the receiver base only through:
47 kΩ
so approximate base current is:
[ I_B=\frac{12-0.7}{47k} ]
or about:
[ 0.24\ mA ]
The Nano’s RX GPIO remains on the transistor’s collector side in the 5 V domain.
Nano TX
The TX GPIO reaches only the base of the TX transistor through its own resistor.
It is not directly connected to target UPDI.
Thus the Nano does not directly see the target-side 12 V.
That is a substantial improvement over the original one-resistor interface.
55. Target Operating Voltage
Our primary target:
AVR16DU28-I/SP
operates over approximately:
1.8–5.5 V
as do our selected:
AVR32DU28-I/SP AVR16DD28-I/SP
examples.
The finalized electrical interface is designed to accommodate target operation over this range.
But we must separate:
designed for
from:
qualified at.
We will experimentally verify operation at representative values:
1.8 V 3.3 V 5.0 V
and at several UPDI bit rates.
Until we have those results, the correct statement is:
The interface is designed for low-voltage target support.
not:
Guaranteed 1.8 V operation at every UPDI speed.
That guarantee belongs to the test results, not the schematic.
55.1 Datasheet First, Tutorial Second
Whenever this article gives a value that could damage hardware if wrong, the device data sheet remains authoritative.
That includes:
- target VDD limits;
- absolute maximum input voltages;
- UPDI speed;
- HV activation voltage;
- RESET limits;
- pulse width;
- target current;
- clock restrictions;
- NVM programming rules.
The article explains the system.
It does not replace target documentation.
56. UPDI Enable Behavior
The UPDI input contains internal pull-up behavior when the interface is enabled.
A normal enable operation begins when the debugger/programmer asserts the UPDI line LOW for the required interval and then releases it.
A useful conceptual sequence is:
programmer pulls LOW
↓
target detects activation
↓
target initializes UPDI clock/interface
↓
target may temporarily hold line LOW
↓
target releases line
↓
UPDI becomes idle HIGH
Representative modern-device timing includes an initial LOW pulse greater than approximately:
200 ns
followed by a target initialization/release interval that can extend from hundreds of microseconds into several milliseconds, depending on implementation.
The exact values always belong to the target documentation.
57. A Practical Ordinary-UPDI Activation Sequence
Conceptually:
1. Ensure TX transistor is OFF. 2. Verify the target line is not stuck LOW. 3. Turn TX transistor ON long enough to create the required enable LOW pulse. 4. Turn TX transistor OFF. 5. Observe UPDI_RX. 6. Wait for the target to release the line HIGH. 7. Send 0x55 SYNCH. 8. Send the first instruction.
Notice something important.
Because RX is always active, our Nano can observe the target UPDI state without changing D6 to an input.
That is one of the practical advantages of the new architecture.
58. Enable Is Not BREAK
Both involve pulling target UPDI LOW.
Their purposes differ.
Enable
Conceptually:
Wake or activate the UPDI physical interface.
BREAK
Conceptually:
Reset the UPDI communications state machine.
Our firmware should therefore retain separate operations:
updiEnable(); updiBreak();
rather than treating every long LOW pulse as the same operation.
59. The Programmer Still Has Two Serial Links
Our Nano participates in two serial conversations.
The first is:
PC | | USB serial v Arduino Nano
The second is:
Arduino Nano | | D6 TX / D5 RX internally v UPDI translator | | one target conductor v Target AVR
These are completely different interfaces.
The host link may use:
115200 baud 8N1
while UPDI initially uses:
62,500 baud 8E2 SYNCH BREAK ACK half-duplex protocol semantics
Their baud rates are independent.
60. Why Not Use the Nano Hardware UART for UPDI?
The Nano’s ATmega328P has a conventional USART.
However, that USART is normally involved in:
PC ↔ USB serial bridge ↔ Nano
communication.
We want that hardware UART available for our diagnostic and host protocol.
UPDI itself also requires unusual control over:
- BREAK timing;
- synchronization;
- half-duplex ownership;
- inverted external TX/RX behavior;
- timing experiments.
So the useful architecture becomes:
hardware UART:
PC communications
D6 / PD6:
UPDI_TX timing engine
D5 / PD5:
UPDI_RX sampling
The target still sees only one physical UPDI line.
61. Final Nano Pin Assignment
Our working article pin assignment is now:
| Nano pin | Function |
|---|---|
| D2 | Activity/status indication |
| D3 | HV-enable/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 critical change from the original article draft is:
OLD: D6 = bidirectional UPDI FINAL: D6 = UPDI_TX D5 = UPDI_RX
Externally, however:
UPDI remains one wire.
62. UPDI Uses 8E2
An ordinary UPDI DATA frame contains:
1 start bit 8 data bits 1 even parity bit 2 stop bits
or:
8E2
So:
START D0 D1 D2 D3 D4 D5 D6 D7 PARITY STOP STOP
contains:
12 bit periods
At 62,500 baud:
1 bit = 16 µs
and a full frame takes:
192 µs
At 225 kbaud:
1 bit ≈ 4.444 µs
and a full 12-bit frame takes approximately:
53.3 µs
63. Even Parity
UPDI uses even parity.
A simple parity function is:
uint8_t evenParity(uint8_t value)
{
uint8_t parity = 0;
for (uint8_t i = 0; i < 8; ++i) {
parity ^= (value >> i) & 1u;
}
return parity;
}
Examples:
0x00 ones = 0 parity = 0 0x01 ones = 1 parity = 1 0x03 ones = 2 parity = 0 0xFF ones = 8 parity = 0
64. Direct Port Access with Separate TX and RX
Arduino digitalWrite() is convenient.
It is not ideal for a carefully timed software UART.
On the ATmega328P:
Arduino D6 = PD6 = UPDI_TX Arduino D5 = PD5 = UPDI_RX
Define:
#define UPDI_TX_BIT _BV(PD6) #define UPDI_RX_BIT _BV(PD5)
Configure TX as an output:
static inline void updiTxInit()
{
DDRD |= UPDI_TX_BIT;
// LOW keeps the external NPN transmitter OFF.
PORTD &= ~UPDI_TX_BIT;
}
Drive target UPDI LOW:
static inline void updiDriveLow()
{
// HIGH turns QTX on.
PORTD |= UPDI_TX_BIT;
}
Release target UPDI:
static inline void updiRelease()
{
// LOW turns QTX off.
PORTD &= ~UPDI_TX_BIT;
}
Read target UPDI:
static inline bool updiRead()
{
// External receiver transistor is inverting.
return !(PIND & UPDI_RX_BIT);
}
This is much simpler than repeatedly switching one GPIO between input and output modes.
65. We No Longer Need GPIO Direction Turnaround
The original one-pin design required:
D6 output during transmit
↓
D6 input during receive
The final hardware does not.
D6 remains:
TX output
and D5 remains:
RX input.
The target bus still changes ownership.
But the Nano’s internal GPIO directions do not.
Therefore:
updiSendByte(command); // TX transistor is released by send routine. updiReceiveByte(response);
does not require a DDR change between operations.
That removes an entire class of potential direction-switching mistakes.
66. Guard Time Still Matters
Separating TX and RX does not eliminate half-duplex timing.
The target and programmer still share one physical conductor.
The programmer must:
finish its last stop bit
↓
release TX transistor
↓
allow target to acquire the line
↓
sample response
UPDI guard-time mechanisms help prevent both ends from attempting to own the line simultaneously.
During development:
do not optimize guard time.
Use:
- moderate baud rate;
- normal ACK handling;
- generous timing;
- clear instrumentation.
Then shorten delays only after the protocol is reliable.
67. Sending an UPDI Byte with the New Hardware
Because the transmitter is open collector:
logical target LOW = Nano TX HIGH logical target HIGH = Nano TX LOW / released
A teaching implementation might resemble:
void updiSendByte(uint8_t value)
{
noInterrupts();
uint8_t parity = 0;
// Start bit: target line LOW.
updiDriveLow();
delayBit();
// Eight data bits, LSB first.
for (uint8_t i = 0; i < 8; ++i) {
bool bit = value & 0x01;
if (bit)
updiRelease(); // target logical HIGH
else
updiDriveLow(); // target logical LOW
parity ^= bit;
delayBit();
value >>= 1;
}
// Even parity.
if (parity)
updiRelease();
else
updiDriveLow();
delayBit();
// Two stop bits = released/HIGH.
updiRelease();
delayBit();
delayBit();
interrupts();
}
The important conceptual change is:
we do not drive target HIGH.
We release the transistor and allow the target-side UPDI node to rise.
68. Why Interrupts Matter
Suppose our intended bit periods are:
16 µs 16 µs 16 µs 16 µs
but a timer interrupt occurs halfway through:
16 µs 16 µs 31 µs 16 µs
We no longer have a valid asynchronous frame.
Timing-critical UPDI bit transactions should therefore execute without unrelated interrupt jitter.
But interrupts should only be disabled for the short timing-critical region.
Good:
disable send frame enable process disable receive frame enable
Bad:
disable erase chip program Flash verify entire device enable
69. Timer1 Gives Us Better Timing
At:
F_CPU = 16 MHz Timer1 prescaler = 8
Timer1 runs at:
2 MHz
so:
1 timer tick = 0.5 µs
At 62.5 kbaud:
bit period = 16 µs
which is:
32 Timer1 ticks.
That is extremely convenient.
static inline void waitTicks(uint16_t ticks)
{
uint16_t start = TCNT1;
while ((uint16_t)(TCNT1 - start) < ticks) {
}
}
Then:
constexpr uint16_t UPDI_BIT_TICKS = 32;
gives us deterministic timing.
70. Receiving Is Harder
When transmitting, we determine when each bit begins.
When receiving, the target decides.
The receiver must:
wait for falling START edge
↓
move toward center of D0
↓
sample D0
↓
sample D1
↓
...
↓
sample D7
↓
sample parity
↓
verify STOP
↓
verify STOP
At 62.5 kbaud:
bit time = 16 µs
so the center of D0 occurs roughly:
1.5 bit periods
after the initial falling edge:
[ 1.5\times16\ \mu s=24\ \mu s ]
The external RX transistor already presents us with a safe Nano-level input.
updiRead() reverses the inversion so the higher-level code sees the actual target logic state.
71. Conceptual Receiver
bool updiReceiveByte(uint8_t &value)
{
// TX must already be released.
updiRelease();
if (!waitForLow())
return false;
noInterrupts();
// Move to center of D0.
delayMicroseconds(24);
uint8_t data = 0;
uint8_t parity = 0;
for (uint8_t i = 0; i < 8; ++i) {
bool bit = updiRead();
if (bit)
data |= (1u << i);
parity ^= bit;
delayMicroseconds(16);
}
bool receivedParity = updiRead();
delayMicroseconds(16);
bool stop1 = updiRead();
delayMicroseconds(16);
bool stop2 = updiRead();
interrupts();
if (receivedParity != parity)
return false;
if (!stop1 || !stop2)
return false;
value = data;
return true;
}
Again, this code explains the architecture.
The production implementation should use deterministic timer-based sampling.
72. Never Wait Forever
Embedded communications code must assume that the target can disappear.
This is dangerous:
while (updiRead()) {
}
Possible causes of an infinite wait include:
- target not connected;
- target unpowered;
- wrong target pin;
- broken wire;
- UPDI unavailable;
- failed translator;
- damaged target.
Instead:
bool waitForLow(uint32_t timeoutUs)
{
uint32_t start = micros();
while (updiRead()) {
if ((uint32_t)(micros() - start) >= timeoutUs)
return false;
}
return true;
}
and similarly:
bool waitForHigh(uint32_t timeoutUs);
Every externally dependent operation should have a timeout.
73. Error Reporting
Instead of returning only:
true false
use meaningful diagnostic states.
enum class UpdiError : uint8_t
{
None,
Timeout,
StartBit,
Parity,
StopBit,
BadAck,
LineStuckLow,
LineStuckHigh,
NotInitialized,
Protocol,
UnsupportedDevice,
VoltageOutOfRange
};
Then:
UPDI ERROR: parity failure
means something.
Compare that with:
ERROR
which tells us very little.
74. Our First Oscilloscope Test
Do not connect the target yet.
Have the Nano repeatedly transmit:
0x55
and observe two points if possible:
Nano D6 / UPDI_TX
and:
TARGET UPDI node
They should be inverted relative to one another because QTX is an NPN pull-down.
At:
62,500 baud
verify:
- 16 µs bit period;
- 8 data bits;
- LSB first;
- even parity;
- 2 stop bits.
A simple loop:
void loop()
{
updiSendByte(0x55);
delay(10);
}
The 0x55 alternating pattern is especially easy to recognize.
75. Test More Than 0x55
Also transmit:
0x00 0xFF 0x01 0x80 0x55 0xAA
These reveal different bugs.
For example:
0x01
versus:
0x80
quickly exposes accidental MSB-first transmission.
Also probe both:
D6
and:
UPDI_DATA
so inversion is not mistaken for a firmware error.
76. Loopback and Echo Testing
The new hardware gives us a useful test that the original one-pin implementation did not provide as conveniently.
Because RX remains active while TX operates, the Nano can observe the UPDI line while transmitting.
When TX commands a target LOW:
D6 HIGH → QTX ON → target UPDI LOW → QRX OFF → D5 HIGH
When TX releases the line:
D6 LOW → QTX OFF → target UPDI HIGH → QRX ON → D5 LOW
Firmware can therefore compare:
expected target state
with:
observed RX state.
This can help detect:
- stuck lines;
- incorrect wiring;
- target contention;
- missing pull-up;
- transistor faults.
It is not a replacement for an oscilloscope, but it is a useful diagnostic feature.
77. BREAK
BREAK is a sufficiently long LOW condition.
With our hardware:
BREAK = keep QTX ON.
For recovery from an unknown communication state, we intentionally use a long BREAK.
Representative traditional UPDI recovery durations are approximately:
4 MHz UPDI clock → about 24.6 ms 8 MHz → about 12.3 ms 16 MHz → about 6.15 ms
A deliberately conservative development implementation is therefore:
void updiBreak()
{
updiDriveLow();
delay(25);
updiRelease();
}
The important difference from the old direct-GPIO circuit is purely electrical:
updiDriveLow()
now means:
turn TX transistor ON.
78. Why Double BREAK?
Suppose the target is halfway through an operation.
The first abnormal BREAK may terminate the current operation.
The second arrives after the interface is receptive to a proper communications reset.
Thus:
BREAK 1:
terminate confused/current state
BREAK 2:
reset the now-receptive protocol engine
is generally more robust than a single attempt when the existing state is unknown.
79. SYNCH
Every new UPDI instruction begins with:
0x55
Define:
constexpr uint8_t UPDI_SYNCH = 0x55;
and give the action a name:
void updiSynch()
{
updiSendByte(UPDI_SYNCH);
}
Then:
updiSynch(); updiSendByte(instruction);
is much more understandable than scattering unexplained 0x55 values throughout the program.
80. The Instruction Families
The principal instruction groups are:
000 LDS 001 LD 010 STS 011 ST 100 LDCS 101 REPEAT 110 STCS 111 KEY
Convenient broad opcode bases are:
constexpr uint8_t UPDI_LDS = 0x00; constexpr uint8_t UPDI_LD = 0x20; constexpr uint8_t UPDI_STS = 0x40; constexpr uint8_t UPDI_ST = 0x60; constexpr uint8_t UPDI_LDCS = 0x80; constexpr uint8_t UPDI_REPEAT = 0xA0; constexpr uint8_t UPDI_STCS = 0xC0; constexpr uint8_t UPDI_KEY = 0xE0;
The remaining bits encode instruction-specific details.
Do not treat values such as:
0x80 0x82 0xA0 0xC3
as magic.
Decode them.
Name them.
Document them.
81. LDCS — Our First Real UPDI Instruction
LDCS reads UPDI’s internal control/status register space.
This makes it an excellent first genuine command because it does not require us to understand:
- target SRAM;
- Flash;
- NVMCTRL;
- EEPROM;
- fuses.
A simplified constructor:
uint8_t makeLdcs(uint8_t address)
{
return UPDI_LDCS | (address & 0x0F);
}
Then:
SYNCH LDCS response
gives us a real request/response transaction.
82. Why LDCS Is Such a Valuable Milestone
If repeated LDCS reads work, we have already verified:
- Nano TX GPIO;
- TX transistor;
- target UPDI node;
- target power;
- RX transistor;
- Nano RX GPIO;
- idle-state behavior;
- software transmit timing;
- start bit;
- LSB-first order;
- even parity;
- two stop bits;
- SYNCH;
- instruction encoding;
- turnaround;
- target transmission;
- receive timing;
- parity checking;
- stop-bit checking.
That is why the first serious test should be small and nondestructive.
83. STCS
STCS writes a byte into UPDI’s control/status space.
Conceptually:
void updiStcs(uint8_t address, uint8_t value)
{
updiSynch();
updiSendByte(
UPDI_STCS | (address & 0x0F)
);
updiSendByte(value);
}
Not every UPDI write uses identical response semantics.
Do not assume:
every write → ACK
without checking the definition of the specific instruction.
84. ACK
For operations requiring acknowledgement, the conventional ACK byte is:
0x40
Define:
constexpr uint8_t UPDI_ACK = 0x40;
Then:
UpdiError expectAck()
{
uint8_t value;
UpdiError err = updiReceiveByte(value);
if (err != UpdiError::None)
return err;
if (value != UPDI_ACK)
return UpdiError::BadAck;
return UpdiError::None;
}
The important point is:
Some byte arrived
does not mean:
The operation succeeded.
85. LDS and STS
Once UPDI internal-register access works reliably, we enter the target’s memory-mapped system space.
LDS means:
Load Direct from address
or approximately:
value = memory[address]
STS means:
Store Direct to address
or approximately:
memory[address] = value
This is where UPDI becomes enormously powerful.
Because the AVR system is heavily memory mapped, UPDI can reach:
- SRAM;
- peripheral registers;
- NVMCTRL;
- device information;
- debugging resources.
86. LD and ST
LD and ST use UPDI’s internal pointer.
Instead of repeatedly sending:
address data address data address data
we can establish a pointer and perform:
LD PTR++
or:
ST PTR++
for sequential transfers.
This becomes critical when reading or programming pages.
87. REPEAT
REPEAT loads the UPDI repeat counter.
An important detail is that a repeat value:
n
causes the following operation to occur approximately:
n + 1
times.
So for:
64 transfers
the repeat field is:
63
not 64.
That is exactly the kind of off-by-one rule that deserves an automated unit test.
88. KEY
The KEY instruction provides access to protected UPDI functionality.
Among the important 64-bit signatures are keys associated with:
- NVM programming;
- chip erase;
- USERROW operations.
Representative keys include:
CHIP ERASE 0x4E564D4572617365 NVMPROG 0x4E564D50726F6720
These are not passwords or encryption keys.
They are protocol activation signatures.
Part III will use them in the actual NVM programming procedure.
89. Device-Specific “Magic Values” Must Be Identified
From this point onward every unexplained numeric constant should make us suspicious.
There are two broad classes.
Protocol constants
Examples:
SYNCH = 0x55 ACK = 0x40 KEY opcode family = 0xE0
These belong to UPDI itself.
Device-dependent values
Examples:
- Flash size;
- Flash page size;
- NVM controller generation;
- memory-map addresses;
- maximum UPDI speed;
- signature bytes;
- fuse layout;
- HV activation voltage;
- HV destination pin.
These belong to the target.
Our implementation will explicitly separate the two.
90. Device-Dependent Timing
A useful baseline for modern devices is:
ordinary enable LOW pulse:
> about 200 ns
target initialization/release:
hundreds of microseconds to milliseconds
safe initial baud:
≤ 225 kbaud for appropriate implementations
Higher UPDI rates may become available with:
- higher target VDD;
- different UPDI clock selection;
- appropriate device support.
Our article still begins at:
62.5 kbaud
because understanding comes first.
91. HV Values for Our Reference Devices
This is where our chosen targets differ dramatically from the old shorthand:
“UPDI HV means 12 V.”
For our modern AVR DU/DD examples using separate RESET-based HV activation, representative documented behavior is approximately:
HV destination:
RESET
VHV minimum:
VDD + 2 V
VHV typical:
7.5 V
VHV maximum:
8.5 V
minimum pulse:
about 10 µs
valid-key window:
about 65 ms
subject always to the exact target’s documentation.
Our final hardware therefore supports a separate HV output path rather than assuming high voltage always belongs on RESET or always belongs on UPDI.
That distinction lets the same experimental programmer explore:
modern RESET-HV activation
and:
older 12 V shared-UPDI activation
without electrically treating them as the same thing.
92. VTARGET Sensing
The finalized hardware measures target voltage on:
A0 = VTARGET_SENSE
using a protected divider.
Our design uses approximately:
22 kΩ top 10 kΩ bottom
so:
[ V_{ADC}=V_{TARGET}\frac{10}{22+10} ]
or:
[ V_{ADC}=0.3125V_{TARGET} ]
Examples:
| VTARGET | ADC voltage |
|---|---|
| 1.8 V | 0.563 V |
| 3.3 V | 1.031 V |
| 5.0 V | 1.563 V |
| 5.5 V | 1.719 V |
| 12 V | 3.75 V |
| 15 V | 4.69 V |
The relatively large range is intentional.
VTARGET sense is not merely a precision measurement of normal 1.8–5.5 V operation.
It is also a protection/diagnostic channel.
A series resistor and Schottky clamp protect the ADC input, and a small capacitor provides filtering.
93. HV Sensing
The external HV rail is measured independently on:
A1 = HV_SENSE
using approximately:
33 kΩ top 10 kΩ bottom
giving:
[ V_{ADC}=V_{HV}\frac{10}{43} ]
or approximately:
[ V_{ADC}=0.2326V_{HV} ]
Representative values are:
| HV | ADC |
|---|---|
| 7.5 V | 1.74 V |
| 8.5 V | 1.98 V |
| 12 V | 2.79 V |
| 18 V | 4.19 V |
| 20 V | 4.65 V |
| ~21.5 V | ~5.0 V |
This gives us comfortable headroom around our intended:
7–8.5 V
and:
12 V
activation classes.
The programmer can therefore verify that the bench supply is reasonably set before enabling HV.
94. Target Reset Control
We also dedicate:
D4 = TARGET_RESET_CTRL
to a small NPN transistor that can pull target RESET LOW.
This allows ordinary reset control independently of the HV system.
Conceptually:
Nano D4 | base resistor | NPN | target RESET
The transistor defaults OFF while the Nano resets.
This matters because:
ordinary reset
and:
high-voltage activation
are not the same operation even when a particular target happens to use the RESET pin for both.
95. Target Power Control
We reserve:
D8 = TARGET_POWER_ENABLE
for optional target-power switching.
The article reference schematic includes an optional relay driver.
The initial programming experiments do not require it to be populated.
But it becomes useful later for:
- target power cycling;
- repeatable startup tests;
- HV recovery of a pin otherwise driven by application firmware;
- automated test fixtures.
That capability is reserved now so that later firmware does not require a pin reassignment.
96. A Human-Readable Diagnostic Console
Before attempting AVRDUDE compatibility, build a simple command console.
For example:
NanoUPDI> help Commands: wake break status sib signature vtarget vhv peek <address> poke <address> <value> reset info
Initially omit:
erase writeflash writefuse
Read first.
Write later.
97. Arduino IDE Serial Monitor
The simplest host console is already available through Arduino IDE.
void setup()
{
Serial.begin(115200);
updiTxInit();
Serial.println();
Serial.println(F("NanoUPDI experimental console"));
Serial.println(F("READY"));
}
The Nano’s hardware UART remains dedicated to the host connection while D5/D6 perform UPDI.
98. Other Serial-Terminal Options
Arduino IDE is convenient but not mandatory.
Useful alternatives include:
- PuTTY;
- Tera Term;
- CoolTerm;
- minicom;
- picocom;
- screen;
- GtkTerm.
On Linux:
picocom -b 115200 /dev/ttyUSB0
may be convenient.
The particular terminal is not important.
The observable command interface is.
99. Python and PySerial
Python lets us move from:
“Try it a few times.”
to:
“Perform 10,000 trials and count exactly what fails.”
For example:
import serial
port = serial.Serial(
"/dev/ttyUSB0",
115200,
timeout=1
)
port.write(b"status\n")
print(
port.readline().decode(errors="replace")
)
Then automate:
10,000 status reads 10,000 memory reads multiple target voltages multiple baud rates
and collect:
successes timeouts parity errors framing errors bad ACKs
That is engineering evidence.
100. The Nano’s Limitations Are Not Dead Ends
The classic Nano lacks:
- native USB;
- a specialized modern single-wire USART;
- multi-voltage GPIO;
- onboard HV generation;
- programmable target power;
- native target-voltage measurement.
But these are limitations of the bare Nano board.
Our final reference hardware adds:
two NPN transistors for UPDI translation target reset transistor HV switching transistors VTARGET divider HV divider ADC protection optional target-power relay
The Nano remains useful because inexpensive external circuitry supplies the capabilities its MCU lacks.
This is a broader embedded-system lesson:
A microcontroller’s integrated peripherals do not define the limits of the complete system.
101. Device Support Should Be Data Driven
Avoid:
if (device == AVR16DU28) {
...
}
else if (device == AVR32DU28) {
...
}
else if (device == AVR16DD28) {
...
}
scattered throughout the firmware.
Instead use something like:
struct DeviceDescriptor
{
uint32_t signature;
uint32_t flashSize;
uint16_t flashPageSize;
uint16_t eepromSize;
uint8_t nvmVersion;
uint8_t debugVersion;
uint32_t minVdd_mV;
uint32_t maxVdd_mV;
uint32_t maxUpdiBaud;
bool hvSupported;
uint16_t hvTypical_mV;
};
Then:
signature ↓ device descriptor ↓ memory geometry NVM generation voltage limits UPDI limits HV behavior
all come from one auditable source.
102. Keep the Physical Layer Separate
The implementation should have clear boundaries.
updi_phy.cpp
UPDI_TX control
UPDI_RX sampling
transistor inversion
raw line state
bit timing
8E2 send
8E2 receive
BREAK
enable/wake
updi_link.cpp
SYNCH
ACK
retries
recovery
guard timing
updi_instruction.cpp
LDS
STS
LD
ST
LDCS
STCS
REPEAT
KEY
voltage_sense.cpp
VTARGET
HV
calibration
range validation
updi_device.cpp
signatures
device descriptors
updi_nvm.cpp
NVM programming algorithms
hv_control.cpp
switching
route policy
interlocks
timing
Nothing in:
updi_phy.cpp
should need to know what Flash memory is.
Likewise:
updi_nvm.cpp
should never directly manipulate PORTD.
103. A First Serious Target Test
Once the transmitter and receiver have passed their oscilloscope tests:
release UPDI
↓
verify HIGH
↓
ordinary UPDI activation
↓
wait for target release
↓
send SYNCH
↓
issue harmless LDCS
↓
receive byte
↓
verify parity
↓
verify stop bits
↓
print result
Repeat it.
Then repeat it again.
Do not proceed merely because:
it worked once.
104. A Protocol Trace Mode
During development we want output such as:
[TX] 55 [TX] 80 [RX] 30
or:
[UPDI] SYNCH [UPDI] LDCS 0 [UPDI] RX 0x30
A compile-time switch can enable it:
#define UPDI_TRACE 1
But never print inside a timing-sensitive bit loop.
Record the event.
Print it afterward.
105. Testing Firmware Is an Embedded-Development Instrument
The diagnostic firmware is not disposable scaffolding.
It represents a valuable engineering habit:
Turn assumptions into measurements.
Suppose we believe:
the target releases UPDI after X µs
or:
the 47 kΩ RX network works at 1.8 V
or:
the optional pull-up is unnecessary at 225 kbaud.
Do not merely reason about it indefinitely.
Write an experiment.
Measure it.
Examples:
Test 1
Does D6 produce exactly the expected bit timing?
Test 2
Does target UPDI have clean rise/fall edges after the TX transistor?
Test 3
Does D5 decode the target correctly at 1.8 V?
Test 4
How long does the target hold UPDI LOW during enable?
Test 5
Does LDCS work 10,000 times?
Test 6
Does it still work with R4 DNP at 225 kbaud?
Test 7
Does populating the optional 33 kΩ pull-up improve edge timing?
This is real embedded engineering.
106. Baud-Rate and Voltage Qualification
The new translator means our test matrix should include both:
baud
and:
VTARGET.
For example:
| VTARGET | 62.5k | 115.2k | 125k | 225k |
|---|---|---|---|---|
| 1.8 V | test | test | test | test |
| 3.3 V | test | test | test | test |
| 5.0 V | test | test | test | test |
At each combination:
10,000 status reads 10,000 memory reads count failures
Also record:
- optional pull-up populated or DNP;
- target model;
- cable length;
- board/breadboard construction;
- temperature if relevant.
This will tell us exactly what the electrical interface can honestly claim.
107. Target Voltage Can Affect Maximum UPDI Speed
Maximum recommended UPDI speed can depend on:
- target voltage;
- temperature;
- target generation;
- UPDI clock.
Representative newer-device values may step through approximately:
225 kbaud 450 kbaud 0.9 Mbaud 1.8 Mbaud
as operating conditions and UPDI clock permit.
The ATmega328P Nano itself will also become a practical limiting factor.
Our first AVR16DU28 experiment therefore remains:
62.5 kbaud
even at 5 V.
Correctness first.
108. Moving from LDCS to LDS
Once internal UPDI register reads are completely reliable, begin accessing ordinary target memory.
Conceptually:
LDS
|
+---- system address
|
+---- registers
+---- SRAM
+---- identification data
+---- NVM-related regions
A useful high-level API might eventually be:
UpdiError updiLoadDirect(
uint32_t address,
uint8_t *destination,
size_t count
);
The low-level code chooses the correct encoding.
109. Byte Order — Endianness
Whenever a multi-byte number crosses a protocol boundary, ask:
In what byte order?
Suppose:
0x1234
must be sent least-significant byte first.
The wire order becomes:
0x34 0x12
not:
0x12 0x34.
The same idea applies to:
- memory storage;
- addresses;
- NVM values;
- debug state;
- host protocols.
A simple test:
void testAddressEncoding()
{
uint8_t bytes[2];
encodeAddress16(0x1234, bytes);
assert(bytes[0] == 0x34);
assert(bytes[1] == 0x12);
}
can eliminate an entire category of difficult bugs.
110. STS — Write Something Harmless First
After reading target memory successfully, we can implement writes.
But the first write should not be:
erase Flash
and certainly not:
modify fuse.
Instead use SRAM where appropriate:
read SRAM byte save original write test value read back verify restore original verify again
This tests:
- STS encoding;
- ACK handling;
- target write path;
- subsequent LDS;
without modifying nonvolatile memory.
111. Pointer-Based LD/ST Comes Next
Once direct access works:
LDS STS
move to:
LD ST
using the UPDI internal pointer.
Then:
LD PTR++ ST PTR++
Finally:
REPEAT + pointer post-increment.
This staged progression makes failures easier to isolate.
112. Then REPEAT
A good development sequence is:
single LDS
↓
single LD
↓
LD PTR++
↓
REPEAT + LD PTR++
↓
block read
If:
single LD works
but:
REPEAT block fails
then the basic physical and instruction layers are already largely vindicated.
The failure is much easier to localize.
113. Then Read the SIB
The System Information Block provides implementation information relevant to the target’s programming/debug architecture.
During development preserve both:
raw SIB bytes
and:
parsed interpretation.
Do not throw the raw information away merely because our parser believes it understands it.
New device families have a habit of making old assumptions visible.
114. Intermediate Firmware API
By this stage we want something closer to:
// Physical layer
void updiTxInit();
void updiDriveLow();
void updiRelease();
bool updiRead();
UpdiError updiEnable();
void updiBreak();
void updiDoubleBreak();
UpdiError updiSendByte(uint8_t value);
UpdiError updiReceiveByte(uint8_t &value);
// Link layer
void updiSynch();
UpdiError updiExpectAck();
// Instruction layer
UpdiError updiLdcs(
uint8_t reg,
uint8_t &value
);
UpdiError updiStcs(
uint8_t reg,
uint8_t value
);
UpdiError updiLds(...);
UpdiError updiSts(...);
UpdiError updiLd(...);
UpdiError updiSt(...);
UpdiError updiRepeat(
uint8_t count
);
UpdiError updiSendKey(...);
UpdiError updiReadSib(...);
// Hardware support
uint16_t readVtargetMillivolts();
uint16_t readHvMillivolts();
void targetResetAssert();
void targetResetRelease();
void targetPowerEnable(bool enabled);
void hvEnable(bool enabled);
Still absent:
chip erase Flash write fuse write.
Those belong to the NVM layer and come later.
115. The Architectural Boundary
At this point:
+-------------------------------------+
| UPDI instructions |
| LDS STS LD ST LDCS STCS REPEAT KEY |
+-------------------------------------+
|
+-------------------------------------+
| UPDI link |
| SYNCH ACK BREAK recovery |
+-------------------------------------+
|
+-------------------------------------+
| Physical layer |
| D6 TX + D5 RX + transistor bridge |
| target-side one-wire 8E2 |
+-------------------------------------+
|
v
========== TARGET SYSTEM BUS ==========
|
v
+-------------------------------------+
| NVM CONTROLLER |
| |
| PART III |
+-------------------------------------+
|
v
Flash / EEPROM / fuses
UPDI lets us communicate with the target.
The target’s NVM controller determines how we actually program it.
116. Corrected Complete Reference Wiring
The target-side interface should now be understood as:
ARDUINO NANO
D6 / PD6 UPDI_TX
|
47k
|
base
QTX 2N3904
emitter → GND
collector
|
+-------------------------------- TARGET UPDI
|
+---- 47k ---- base QRX
QRX 2N3904
emitter → GND
collector
|
+------------ D5 / PD5 UPDI_RX
|
47k
|
+5V
OPTIONAL PULL-UP ASSIST:
VTARGET
|
Schottky diode
|
33k DNP
|
TARGET UPDI
The target connector additionally provides:
TARGET_RESET VTARGET GND
The programmer is therefore not simply:
D6 → 4.7k → UPDI
anymore.
That simple circuit remains useful historically and pedagogically, but our final hardware has evolved beyond it.
116.1 Reference Connection Table
| Programmer signal | Target connection | Purpose |
|---|---|---|
TARGET_RESET | RESET | Normal reset / applicable HV-reset use |
VTARGET | VDD | Voltage sense/reference |
UPDI_DATA | UPDI | One-wire UPDI |
| GND | GND | Common reference |
Internal Nano mapping:
| Nano | Function |
|---|---|
| D4 | TARGET_RESET_CTRL |
| D5 | UPDI_RX |
| D6 | UPDI_TX |
| D7 | HV_ENABLE |
| D8 | TARGET_POWER_ENABLE |
| A0 | VTARGET_SENSE |
| A1 | HV_SENSE |
116.2 Target Decoupling
Target decoupling remains separate from the UPDI circuitry.
Use at least:
VDD ----||---- GND
100 nF
close to the AVR power pins.
On a breadboard, an additional:
1–10 µF
bulk capacitor is useful.
The capacitors are target-power components.
They are not part of UPDI signaling.
117. Suggested Programming Header
A convenient development connector is:
1 RESET 2 VTARGET 3 GND 4 UPDI
Our schematic may use a different physical ordering depending upon connector layout, but the logical four-signal set is:
RESET VDD/VTARGET GND UPDI
For a simple non-HV experiment, only:
UPDI VDD GND
may be needed.
118. Nano Autoreset
Classic Nano boards commonly reset when the host opens the serial port because the USB-UART circuit manipulates DTR.
This is convenient during sketch upload.
It can be surprising during programmer use.
Instead of modifying the Nano immediately, our host protocol can expect:
host opens port
↓
Nano resets
↓
UPDI TX defaults safe/OFF
↓
HV defaults OFF
↓
Nano initializes
↓
Nano sends READY
↓
host proceeds
This makes safe hardware default states particularly important.
119. Startup Banner
A useful updated startup report might be:
NanoUPDI Experimental Programmer 0.3 ------------------------------------- Host baud: 115200 UPDI baud: 62500 UPDI TX: D6 / PD6 UPDI RX: D5 / PD5 Reset control: D4 HV enable: D7 Target power: D8 VTARGET sense: A0 HV sense: A1 Target profile: AVR16DU28-I/SP Target VDD: 5.02 V HV supply: 0.00 V Optional UPDI pull-up: DNP Trace mode: enabled READY
Now there is no ambiguity about which hardware version the firmware expects.
120. Keep Measurements with the Code
When testing a configuration, record:
firmware Git commit target MCU target VDD UPDI baud UPDI pull-up DNP/populated wiring/cable length test count failure count
For example:
Firmware: 8c17a3e Target: AVR16DU28-I/SP VDD: 3.302 V UPDI baud: 125000 R4 assist: DNP Cable: 10 cm Reads: 10000 Timeouts: 0 Parity errors: 0 Frame errors: 0 Bad ACK: 0
Now:
“It seems reliable.”
becomes:
“Ten thousand transactions completed with zero observed failures under a documented configuration.”
121. A Companion Nano UPDI Sniffer / Analyzer
We can go farther than merely observing UPDI_DATA on an oscilloscope.
For this series we can use a separate companion project:
Nano UPDI Sniffer / Analyzer
The intended project provides:
- Nano firmware;
- host software;
- wiring documentation;
- schematics;
- PCB design;
- protocol-decoding capabilities.
The analyzer is a separate instrument from the programmer.
The programmer says:
Perform this operation.
The analyzer says:
Tell me what actually happened.
That difference becomes extremely useful when our implementation disagrees with a known-good tool.
121.1 Why a Sniffer Is So Valuable
A protocol analyzer gives us a third perspective.
We already have:
source code
and:
electrical waveform.
The analyzer adds:
decoded protocol meaning.
Thus:
SOURCE CODE
|
v
"send LDCS"
WIRE
|
v
55 82
ANALYZER
|
+-- SYNCH
+-- LDCS
TARGET
|
v
30
Theory, source, waveform, and target response can now be compared directly.
121.2 Sniffer Versus Programmer
When our programmer misbehaves, compare its traffic against:
- PICkit;
- Atmel-ICE;
- another known-good programmer.
A differential capture may reveal:
- missing BREAK;
- extra BREAK;
- different guard timing;
- unexpected ACK handling;
- different KEY ordering;
- additional reset sequences;
- different NVM commands.
That is far more productive than guessing.
121.3 A Higher-Level Decoder
The analyzer can evolve through layers:
Layer 1:
raw transitions
Layer 2:
8E2 bytes
Layer 3:
SYNCH / ACK / BREAK
Layer 4:
LDS / STS / LD / ST
LDCS / STCS
REPEAT / KEY
Layer 5:
register names
Layer 6:
NVM operations
Layer 7:
semantic summaries
Eventually:
KEY NVMPROG RESET ASSERT RESET RELEASE PROGRAMMING MODE ENTERED SET PTR = 0x008000 REPEAT 63 WRITE 64 BYTES NVM WRITE WAIT BUSY READ BACK VERIFY OK
becomes possible.
122. Test Firmware as a Scientific Method
The combination of:
- small Arduino test programs;
- Serial Monitor;
- terminal emulator;
- Python/PySerial;
- oscilloscope;
- logic analyzer;
- Nano UPDI Sniffer;
gives us an unusually capable hobbyist laboratory.
The methodology is:
form belief
↓
construct smallest useful test
↓
run repeatedly
↓
measure
↓
compare with documentation
↓
revise understanding
Not:
“I think the peripheral probably behaves this way.”
But:
“The data sheet says this. The test produced this. The scope shows this. Ten thousand trials produced zero exceptions.”
That is engineering.
123. Git and Tests
Even this Nano project deserves version control.
Begin:
git init
Then make meaningful commits:
Initialize Nano UPDI firmware Add UPDI TX transistor control Add inverted UPDI RX support Add timer-based 8E2 transmitter Add timer-based receiver Add enable and double-BREAK recovery Implement LDCS and STCS Add VTARGET measurement Add HV measurement Add automated serial reliability tests Add AVR16DU28 descriptor Add analyzer integration notes
Deterministic functions should have independent tests:
- parity;
- instruction encoding;
- address serialization;
- repeat counts;
- target lookup;
- timeout arithmetic;
- endian conversion;
- voltage-scaling math.
Tests become increasingly important once Part III begins modifying nonvolatile memory.
124. Review of Part II
We now have a concrete target:
AVR16DU28-I/SP
Its:
- 28-pin SPDIP package;
- 1.8–5.5 V supply range;
- modern AVR architecture;
- UPDI support;
make it particularly suitable for a breadboard tutorial.
We also identified:
AVR32DU28-I/SP AVR16DD28-I/SP
as useful alternatives.
More importantly, we have moved beyond the original:
D6 → 4.7k → UPDI
teaching circuit.
Our finalized programmer uses:
D6 = UPDI_TX D5 = UPDI_RX
and two NPN transistors to combine those internal paths into:
one target UPDI conductor.
That gives us:
- open-collector target drive;
- target-referenced HIGH levels;
- lower-VDD compatibility by design;
- Nano-domain RX logic;
- improved tolerance of old 12 V-on-UPDI activation;
- permanent receive visibility;
- easier TX/RX firmware.
We also added:
D4 = TARGET_RESET_CTRL D7 = HV_ENABLE D8 = TARGET_POWER_ENABLE A0 = VTARGET_SENSE A1 = HV_SENSE
and established a protected, optional/DNP target-side pull-up assist.
We established the development environment:
Arduino Nano Arduino IDE Serial Monitor terminal emulator Python + PySerial oscilloscope logic analyzer Nano UPDI Sniffer
and treated all of them as legitimate embedded-development instruments.
We implemented the conceptual foundation for:
8E2 TX 8E2 RX parity timeouts enable BREAK double BREAK SYNCH ACK LDCS STCS LDS STS LD ST REPEAT KEY
We introduced:
endianness
and recognized that byte ordering must be explicit whenever a multi-byte value crosses a protocol boundary.
Most importantly, we established the development methodology:
assumption
↓
small test
↓
measurement
↓
automated repetition
↓
evidence
↓
implementation
That methodology becomes even more important in the next part because we are about to cross the boundary between:
talking to an AVR
and:
modifying an AVR.
125. Where We Go Next
Part III will turn the Nano from a communications experiment into an actual AVR programmer.
We will begin with the AVR16DU28-I/SP as our primary target and explicitly note differences for the AVR32DU28-I/SP and AVR16DD28-I/SP where they matter.
We will cover:
Device identification
- SIB;
- signature;
- revision;
- descriptor lookup.
The AVR memory map
Including:
- Flash;
- SRAM;
- EEPROM;
- FUSE;
- USERROW;
- signature row;
- peripherals;
- NVMCTRL.
Entering NVM programming mode
We will trace:
UPDI established
↓
NVMPROG KEY
↓
required reset sequence
↓
read programming status
↓
confirm programming mode
NVM-controller generations
We will distinguish:
UPDI transport protocol
from:
target NVM programming protocol.
This is one of the central architectural lessons of the entire series.
Flash reading
Our first NVM operation will remain nondestructive.
Chip erase
Then:
erase request status polling completion error handling
Flash programming
Including:
- Flash geometry;
- target-specific NVM commands;
- pointer setup;
- REPEAT;
- erase/write procedure;
- busy/error polling;
- readback verification.
EEPROM
EEPROM gets its own programming procedure rather than being treated as miniature Flash.
Fuses
Fuse writes will be classified according to risk.
Dangerous configuration changes require explicit safeguards.
USERROW
We will examine:
- where it lives;
- whether chip erase affects it;
- its programming requirements.
Intel HEX
We will decode records such as:
:10010000214601360121470136007EFE09D2190140
and show how host software converts a sparse image into programmer operations.
Host software
We will progress from:
Arduino Serial Monitor
to:
Python + PySerial
and build a host programmer capable of:
identify read backup erase program verify
Verification
Every destructive operation will be followed by readback verification.
Analyzer comparison
Finally, we will capture our programming sequence and compare:
data-sheet procedure
=
our source code
=
wire traffic
Only after ordinary programming is reliable will Part IV enable the externally supplied HV recovery hardware, including the critical distinction between:
modern RESET-HV
and:
older approximately 12 V shared-UPDI activation.