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

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

Table of Contents

IMPLEMENTING UPDI FROM SCRATCH

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

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

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

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

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

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

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

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

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

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

Post Stastics

  • This post has 10176 words.
  • Estimated read time is 48.46 minute(s).

Part V โ€” Using UPDI as an On-Chip Debugger

Updated for the finalized NANO UPDI PROGRAMMER Rev. 1.0.0 hardware

So far, we have used only half of UPDIโ€™s name.

We have built the:

Unified Program and Debug Interface

into a functioning programmer.

Now we turn to the second half:

Unified Program and Debug Interface

This is where the project becomes substantially more ambitious.

Programming can be reduced to a comparatively straightforward sequence:

identify target

  โ†“

enter NVM programming mode

  โ†“

erase

  โ†“

write

  โ†“

verify

  โ†“

reset

Debugging is different.

A debugger must interact continuously with a running CPU.

It must be able to:

  • stop execution;
  • determine why execution stopped;
  • read the Program Counter;
  • inspect SRAM and peripheral registers;
  • inspect CPU registers;
  • resume execution;
  • single-step instructions;
  • establish breakpoints;
  • remove breakpoints;
  • perhaps temporarily modify Flash;
  • keep track of which breakpoints are hardware and which are software;
  • translate machine addresses into source-code locations;
  • and communicate all of this to a host debugger such as GDB.

Microchip publicly documents that modern UPDI AVR devices provide these broad OCD capabilities. For newer AVR E/D-class devices, the documented feature set includes two hardware breakpoints, change-of-flow and software breakpoints, runtime PC/SP/SREG observation, and register-file read/write while the CPU is stopped. Broader UPDI OCD documentation also lists memory-mapped NVM/RAM/I/O access, program flow control, and nonintrusive runtime monitoring.

But there is an important complication:

The low-level OCD protocol is not documented publicly to the same degree as the ordinary UPDI programming protocol.

That changes our implementation strategy.

For programming, we could build from the public device data sheets almost byte for byte.

For debugging, we need to combine:

officially documented OCD capabilities

+

public debugger/tool protocols

+

open-source implementations

+

careful protocol observation

+

repeatable experiments

This makes Part V as much an exercise in protocol engineering as firmware development.

Final Hardware Baseline Used in Part V

Part V assumes the finalized programmer hardware developed in Parts Iโ€“IV.

The Nano assignments remain:

Nano pinFunction
D2ACT/status indication
D3HV/status indication
D4TARGET_RESET_CTRL
D5UPDI_RX
D6UPDI_TX
D7HV_ENABLE
D8TARGET_POWER_ENABLE
A0VTARGET_SENSE
A1HV_SENSE
A4/A5Optional IยฒC expansion
D10โ€“D13Optional SPI expansion

The target connector remains:

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

The target still sees one physical UPDI conductor, but inside the programmer the data path is split into separate 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

Q4 has R24 = 47 kฮฉ from base to ground so the transmitter defaults OFF during Nano reset. Q2’s collector is pulled to +5 V through R2 = 47 kฮฉ.

The optional pull-up-assist branch remains:

VTARGET
   |
 D1 SS14
   |
 R4 33k  DNP
   |
UPDI_DATA

R4 is DNP by default.

Part V also inherits the completed support hardware:

  • independent target RESET control on D4 through Q6;
  • externally supplied HV switched by the Q1 2N3906 / Q3 2N3904 stage under D7 control;
  • physical J4 HV route/arm selection;
  • optional relay-controlled target power on D8;
  • VTARGET measurement on A0 through the 22 kฮฉ / 10 kฮฉ protected divider;
  • HV measurement on A1 through the 33 kฮฉ / 10 kฮฉ protected divider.

These additions do not change the debugger protocol stack. They do change the physical-layer implementation and make the Nano a more suitable development/debug bridge than the original one-resistor teaching circuit.


305. Three Protocols, Not One

Before continuing, separate three interfaces that are easily confused.

          GDB / IDE
              |
              | GDB Remote Serial Protocol
              |
              v
       PC-side debug server
              |
              | our host protocol
              |
              v
         Arduino Nano
              |
              | UPDI + OCD operations
              |
              v
          Target AVR

These are three different protocol layers.

Layer 1 โ€” GDB ↔ Debug Server

GDB knows nothing about UPDI.

It speaks its own Remote Serial Protocol, or RSP.

Layer 2 โ€” Debug Server ↔ Nano

We get to design this protocol.

It might initially be simple commands such as:

HALT

RUN

STEP

PC

READ 2000 20

BREAKSET 012C

and eventually become a framed binary protocol.

Layer 3 โ€” Nano ↔ AVR

This remains our one-wire UPDI connection, but we now invoke the targetโ€™s On-Chip Debug system rather than merely its NVM programming facilities.

Keeping these boundaries separate will make the debugger much easier to develop.


306. What the AVR OCD Actually Provides

Microchipโ€™s current documentation for modern UPDI-based devices advertises capabilities including:

memory-mapped NVM access

RAM access

I/O register access

program flow control

CPU halt / stop

CPU run

CPU reset

two hardware breakpoints

software breakpoints

change-of-flow break conditions

runtime PC observation

runtime SP observation

runtime SREG observation

CPU register-file access while stopped

and related monitoring functionality.

That is quite a sophisticated debug system for an 8-bit MCU.

It means our one-wire connection can conceptually expose:

UPDI

|

+—— Flash

|

+—— SRAM

|

+—— peripherals

|

+—— CPU registers

|

+—— program counter

|

+—— stack pointer

|

+—— status register

|

+—— execution control

|

+—— breakpoint comparators

All without permanently dedicating a conventional JTAG port.


307. Why Debugging Is Different from Reading Memory While the CPU Runs

We already know how to perform something like:

LDS 0x2000

through UPDI.

That lets us inspect a memory location.

But suppose the CPU is running code that modifies that location at the same moment.

We may see:

first read: 0x21

second read: 0x22

third read: 0x23

That is useful for monitoring.

It is not necessarily debugging.

Traditional debugging requires the ability to freeze execution:

CPU running

|

v

HALT

|

v

CPU state frozen

|

+---- inspect registers

+---- inspect SRAM

+---- inspect stack

+---- inspect peripherals

+---- examine PC

Then:

RUN

allows execution to continue.

The OCD hardware provides this program-flow-control machinery.


308. Why โ€œStopโ€ Must Preserve CPU State

A debugger cannot implement halt by simply resetting the target.

Reset changes:

PC

SREG

stack

peripheral state

possibly SRAM-visible behavior

and destroys the state we wanted to investigate.

Consider a program that crashes only after:

17 minutes

of operation.

If our debugger responds:

STOP requested

    โ†“

RESET target

we have erased the evidence.

A proper OCD stop operation freezes or redirects execution in a way that preserves the state needed for inspection.

That is what makes an on-chip debugger fundamentally different from a programmer.


309. Public Documentation Ends Before the Most Interesting Part

Microchipโ€™s device documentation does a good job of explaining:

what OCD can do

but does not publicly expose every low-level OCD control register and sequence needed to reproduce a Microchip debugger from first principles.

This is important to state explicitly.

We should not pretend that the complete debugger protocol is formally documented just because the phrase:

two hardware breakpoints

appears in the datasheet.

Community reverse-engineering work has documented parts of the hidden OCD interface, including an OCD activation key reportedly represented numerically as:

0x4F43442020202020

which corresponds to an ASCII-like OCD signature padded with spaces. This information is community-derived rather than something we should treat as a formally guaranteed public programming interface.

This distinction will shape our project.


310. Do Not Build the Entire Debugger Around an Undocumented Constant

If we experiment with a community-derived OCD key, isolate it.

Bad:

uint8_t whatever**[]** = {

0x20, 0x20, 0x20, 0x20,

0x20, 0x44, 0x43, 0x4F

};

buried inside a function.

Better:

// Community-reported UPDI OCD activation key.

// Not treated as a stable public Microchip API.

// Verify against observed known-good debugger traffic.

constexpr uint8_t UPDI_KEY_OCD**[8]** =

{

0x20,

0x20,

0x20,

0x20,

0x20,

0x44,

0x43,

0x4F

};

and accompany the device backend with:

source

validation status

tested families

known limitations

This is especially important when dealing with undocumented interfaces.


311. The SIB Already Hints at Debug Generations

Recall from Part III that the UPDI System Information Block contains fields describing aspects of the target implementation.

Current AVRDUDE code explicitly parses strings representing:

device family

NVM interface

debug interface

PDI/UPDI oscillator

and stores a debug-interface version separately from the NVM-interface version.

That is a valuable clue.

The target effectively tells us:

NVM implementation: one generation

DEBUG implementation: another generation

So our device model should not contain just:

uint8_t nvmVersion;

It should contain:

uint8_t nvmVersion;

uint8_t debugVersion;

or, better, parse the SIB dynamically.


312. A Debug Descriptor

We can extend our target information:

struct DebugDescriptor

{

uint8\_t debugVersion;

uint8\_t hardwareBreakpoints;

bool supportsSoftwareBreakpoints;

bool supportsFlowBreak;

bool supportsRegisterFile;

bool supportsRuntimePc;

bool supportsRuntimeSp;

bool supportsRuntimeSreg;

};

For our modern target profile we expect capabilities broadly corresponding to:

hardware breakpoints: 2

software breakpoints: yes

runtime PC: yes

runtime SP: yes

runtime SREG: yes

register-file access stopped: yes

because those capabilities are documented for newer AVR E/D OCD implementations.

The exact low-level command implementation remains a separate matter.


313. Why Some Old Documents Say One Hardware Breakpoint

If you research UPDI deeply enough, you may find older Microchip debugger documentation stating that UPDI devices provide:

one hardware breakpoint

while newer Microchip documentation says:

two hardware breakpoints

Both documents exist.

For example, an older EDBG-based tools protocol guide describes UPDI targets in terms of one hardware breakpoint, while current OCD documentation for newer devices states two.

This is exactly why we should avoid saying:

โ€œUPDI always has exactly two hardware breakpoints.โ€

A better statement is:

The number and capabilities of OCD breakpoint resources depend on the target/OCD generation; the modern AVR D/E-class devices discussed here document two hardware breakpoints.

Our device database should therefore contain the count.


314. What Is a Hardware Breakpoint?

A hardware breakpoint uses a comparator inside the OCD.

Conceptually:

CPU fetch address

|

  +---- comparator ---- expected address

|

           match?

|

              v

          STOP CPU

Suppose we want execution to stop at:

0x0320

The debugger configures a comparator with that address.

The application Flash remains unchanged.

When execution reaches that address:

PC = 0x0320

the OCD stops the CPU.

That is ideal.

The disadvantage is that there are only a few hardware breakpoint comparators.

For our modern examples:

two

is the documented expectation.


315. Software Breakpoints

A software breakpoint solves the resource problem differently.

Instead of configuring a comparator, the debugger temporarily changes the program.

Conceptually:

original instruction:

0x0320: ADD R16,R17

becomes:

0x0320: BREAK

When execution reaches that instruction, the CPU enters the debug stop condition.

The debugger must remember the original instruction so it can later restore it.

Conceptually:

breakpoint table

address original instruction

——————————–

0x0320 ADD R16,R17

0x08A4 RJMP …

0x0C10 LDS …

Microchip tool documentation explicitly notes that software breakpoints may be inserted into Flash when additional breakpoints are required.


316. โ€œUnlimited Breakpointsโ€ Does Not Mean Unlimited Hardware Comparators

Microchip sometimes describes UPDI OCD as supporting:

unlimited user program breakpoints

while also saying:

two hardware breakpoints

There is no contradiction.

It generally means:

two breakpoint comparator resources

+

software BREAK instructions inserted as needed

provide effectively many user-visible code breakpoints.

That is an important distinction.


317. Software Breakpoints Wear Flash

Software breakpoints modify Flash.

Flash has a finite erase/write endurance.

Repeatedly inserting and removing software breakpoints can therefore consume programming cycles.

Current independent UPDI debugger software explicitly warns about this; for example, avr-absurd makes software breakpoints optional and notes their Flash-wear implications.

For our debugger:

hardware breakpoints

should be preferred whenever available.

Software breakpoints should be:

optional

clearly indicated

wear-aware

and restored carefully.


318. Software Breakpoints Are More Complicated Than โ€œWrite BREAKโ€

Consider this program:

0x0100: instruction A

0x0102: instruction B

0x0104: instruction C

If we replace instruction B with BREAK, when the CPU stops we eventually need to execute the original B.

A typical debugger strategy is conceptually:

hit BREAK

โ†“

halt

โ†“

restore original instruction B

โ†“

single-step B

โ†“

reinsert BREAK

โ†“

continue

Otherwise, continuing immediately would hit the same breakpoint again forever.

This is why breakpoint management belongs largely on the PC side.

The Nano should execute primitive operations.

The host should maintain breakpoint policy.


319. Breakpoint Table Belongs on the PC

The ATmega328P has only:

2 KB SRAM

Our PC has vastly more memory.

So maintain something like:

@dataclass

class Breakpoint:

address: int

kind: str

original: bytes **|** None

enabled: bool

on the host.

Then:

GDB asks for breakpoint

   โ†“

host selects hardware/software method

   โ†“

Nano performs low-level target operation

This keeps our Nano firmware small.


320. Hardware Versus Software Breakpoint Policy

A simple policy might be:

Breakpoint 1

hardware

Breakpoint 2

hardware

Breakpoint 3+

software

But we can do better.

Prefer hardware for:

temporary stepping breakpoints

high-frequency code

Flash-sensitive applications

breakpoints changed frequently

Use software for:

long-lived low-frequency breakpoint

when hardware resources are exhausted

The host can move hardware breakpoint resources dynamically.


321. The BREAK Instruction

Modern AVR instruction sets include a dedicated debug BREAK operation.

Software breakpoints work because the debugger can substitute that opcode into program memory.

The exact instruction representation and address alignment must come from the AVR instruction-set documentation for the target architecture.

Do not guess.

And do not attempt:

one-byte breakpoint patch

if the instruction encoding occupies a word.

Our hostโ€™s disassembly/address model must understand AVR code alignment.


322. AVR Program Addresses Can Be Confusing

AVR documentation, debugger protocols, ELF files, and NVM interfaces do not always describe addresses in the same units.

You may encounter:

byte addresses

word addresses

data-space addresses

Flash-relative addresses

For example:

Flash byte address: 0x0200

instruction word address: 0x0100

UPDI data mapping: 0x8200

can describe the same underlying location in different contexts.

Older Microchip debugger-protocol documentation explicitly distinguishes breakpoint addresses using word-address semantics in some commands.

So our host needs named conversion functions.

Never scatter:

address // 2

through debugger code without explaining why.


323. Define Address Spaces Explicitly

A clean Python model might contain:

@dataclass**(**frozen=True)

class CodeAddress:

byte\_address: int

@dataclass**(**frozen=True)

class DataAddress:

address: int

@dataclass**(**frozen=True)

class WordAddress:

word\_address: int

This may seem overly formal.

It prevents bugs such as setting a breakpoint at:

0x0200

when the OCD expected:

0x0100

because one side meant words and the other bytes.


324. What Does Single-Step Mean?

A normal:

STEP

operation should execute one machine instruction and stop again.

Conceptually:

PC = instruction A

|

  STEP

|

   v

execute A

|

   v

stop before B

Microchip documentation for modern UPDI OCD explicitly lists step/program-flow features for applicable implementations.

At source level, however:

one source line

may compile into:

many AVR instructions

So:

machine step

and:

source-line step

are different operations.

GDB manages the higher-level semantics.

Our target backend needs only the machine-level primitives.


325. Step Into, Step Over, and Step Out

These sound like three separate CPU features.

Usually they are mostly debugger algorithms.

Step Into

Execute the next instruction/source operation normally.

If it is a function call:

enter the function

Step Over

If the current instruction calls a function:

set temporary breakpoint after call

continue

rather than stepping through the called function.

Step Out

Determine the return address from program/stack state and:

set temporary breakpoint at caller

continue

Thus a relatively small target primitive set can support sophisticated debugger behavior.


326. Read the Program Counter

One of the most important debug operations is:

Where is the CPU?

Microchip explicitly documents runtime readout of:

PC

SP

SREG

for profiling/debug support on modern devices.

Our low-level interface should therefore eventually expose:

DebugError debugReadPc**(uint32_t &pc)**;

DebugError debugReadSp**(uint16_t &sp)**;

DebugError debugReadSreg**(uint8_t &sreg)**;

The Nano does not need to know which source line corresponds to the PC.

It returns an address.

The PC host resolves:

PC address

โ†“

ELF/DWARF debugging information

โ†“

filename

function

source line


327. Why the ELF File Matters

When Arduino compiles our sketch, the final machine image is not the only useful artifact.

The build also produces an:

ELF

file.

Unlike a plain HEX image, an ELF file can contain:

machine code

symbols

section layout

debug information

source mappings

variable locations

function names

The programmer mostly cared about:

HEX

or binary bytes.

The debugger cares deeply about:

ELF + DWARF

debugging information.

This is why GDB can turn:

PC = 0x073A

into:

loop**()** at Blink.ino:17

assuming the build retained useful debug symbols.


328. Compile with Debug Information

For useful source-level debugging, the target build should preserve debug information.

That usually means compiling with appropriate:

-g

options and selecting an optimization level compatible with the debugging goals.

Optimization matters because the compiler may:

remove variables

reorder code

inline functions

merge operations

eliminate entire branches

Then the debugger may appear to:

jump around strangely

at source level even though machine execution is correct.

This is not a UPDI problem.

It is a consequence of debugging optimized code.


329. Arduino IDE Is Still Our Development Environment

We are continuing to assume Arduino IDE as the source-development tool.

Arduino IDE 2 supports debugging infrastructure when the selected board platform provides appropriate debug configuration, and Arduino CLI includes a debug command that launches an interactive GDB session for supported board/programmer combinations.

Our homemade AVR16DU28/Nano combination will not automatically become an officially supported Arduino IDE debugger merely because the hardware works.

We will first use:

Arduino IDE

|

+---- compile sketch

|

+---- produce ELF

then separately run:

our GDB server

and:

avr-gdb sketch.elf

This decouples UPDI development from Arduino IDE integration.


330. Arduino CLI Can Help Locate and Preserve the Build

Arduino CLI is the engine behind much of the Arduino tooling and supports explicit build directories and debugger configuration.

A useful workflow is:

Arduino IDE for editing

    โ†“

Arduino CLI compile with known build path

    โ†“

ELF remains easy to locate

    โ†“

launch our debug server

    โ†“

avr-gdb ELF

Eventually we may add an Arduino platform debug definition so IDE 2 can launch our server automatically.

That is integration work, not OCD work.

Do it last.


331. Registers While Stopped

Modern Microchip OCD documentation says the register file is readable and writable while the CPU is stopped.

For AVR, that means registers such as:

R0

R1

R31

can potentially be inspected.

This is extremely valuable.

Suppose we stop inside:

result = a + b;

GDB may need to know:

R18 = …

R19 = …

in order to reconstruct local variables.

Our debug backend therefore eventually needs:

debugReadRegister**(index)**;

debugWriteRegister**(index, value)**;

provided the target/debug generation supports it.


332. Writing Registers Is Powerfulโ€”and Dangerous

If we halt and modify:

R24

then resume, we have changed program execution.

That is useful.

It lets the developer test:

What happens if this function returned 0x42?

without recompiling.

Likewise, modifying:

SREG

SP

PC

can have profound effects.

The host should distinguish:

ordinary variable write

from:

CPU control-state write

and avoid accidental register modification.


333. SRAM Access Is Straightforward by Comparison

The best part of our architecture is that memory access is already familiar.

We wrote:

READ

WRITE

operations in Parts II and III.

While the target is halted, the debugger can reuse that machinery to inspect:

global variables

stack

buffers

structures

Suppose:

volatile uint16_t counter;

resides at:

0x3E20

GDB requests two bytes.

Host translates that to:

READ_MEM 0x3E20 2

Nano performs UPDI load operations.

Target returns:

34 12

GDB interprets:

0x1234

according to AVR little-endian representation.

Our work on endianness now pays off again.


334. Peripheral Register Inspection

Because AVR peripherals are memory mapped, we can inspect:

PORT

TCA

TCB

USART

SPI

TWI

ADC

USB

registers through the same broad address-space mechanism. Microchip lists I/O access as part of the UPDI OCD feature set.

This is extremely useful.

A developer can halt and ask:

Why isn’t USART0 transmitting?

then inspect:

USART0.CTRLA

USART0.CTRLB

USART0.STATUS

USART0.BAUD

directly.


335. But Some Peripheral Registers Have Side Effects

Reading a register is not always passive.

Some MCU registers:

clear flags when read

latch related values

advance FIFOs

clear interrupt state

require ordered accesses

Therefore a debuggerโ€™s generic:

read every peripheral register continuously

can change target behavior.

This is true of virtually all microcontroller debuggers, not just UPDI.

A serious debugger needs device metadata describing:

safe to read

read has side effects

write-protected

write-one-to-clear

where possible.


336. Nonintrusive Runtime Monitoring

One of the more interesting documented UPDI OCD capabilities is:

nonintrusive runtime monitoring

including PC/SP/SREG-style observations and status monitoring without ordinary system-register access.

This suggests possibilities beyond conventional break-debug cycles.

For example:

sample PC repeatedly

while the application runs.

Then produce:

function A 48%

function B 29%

idle 17%

ISR 6%

as a crude statistical profiler.

This can provide useful runtime insight without repeatedly stopping the target.


337. Statistical Profiling

Suppose we sample PC:

10,000 times

while the CPU runs.

We obtain:

0x020A 3200 samples

0x0320 1800 samples

0x06A8 900 samples

Using ELF symbols:

0x020A โ†’ calculate_filter**()**

0x0320 โ†’ service_usb**()**

0x06A8 โ†’ loop**()**

Now we can estimate where the program spends its time.

This is not cycle-accurate tracing.

But it can be surprisingly useful.

The targetโ€™s documented runtime PC observation capability is what makes such a tool possible.


338. The Nano Is Not the Best Place to Perform Symbol Resolution

The Nano should report:

PC = 0x020A

The PC should determine:

calculate_filter**()**

filter.cpp:148

Symbol lookup can involve:

ELF parsing

DWARF parsing

demangling C++ names

source paths

line-number tables

That is far beyond what we want in 2 KB of Nano SRAM.

Again:

Nano = precise target bridge

PC = intelligence

is the right architecture.


339. Halting the Target on Demand

One required debugger feature is:

user presses pause

or in GDB:

Ctrl-C

while the target runs.

The GDB server must translate that into an OCD stop request.

The target stops.

Then the server reports a GDB stop reason.

Conceptually:

GDB

|

| Ctrl-C

v

Python GDB server

|

| DEBUG_STOP

v

Nano

|

| OCD stop primitive

v

AVR CPU halted

|

v

read PC / reason

|

v

report stop to GDB

GDBโ€™s remote protocol explicitly supports interrupting a running target over a remote connection.


340. Stop Reasons Matter

A debugger needs to know not merely:

CPU stopped

but ideally:

why?

Possibilities include:

manual stop

hardware breakpoint

software breakpoint

single-step completed

reset

fault-like condition

This becomes important for GDB.

If GDB set a breakpoint and receives a stop event, it must know whether execution actually reached that breakpoint.

Our OCD backend therefore needs a conceptual result such as:

enum class DebugStopReason

{

Unknown,

Manual,

HardwareBreakpoint,

SoftwareBreakpoint,

Step,

Reset

};

Even if the first implementation reports:

Unknown

for several cases, the architecture should allow refinement.


341. GDB Remote Serial Protocol

GNU GDB already solves the difficult user-facing parts of debugging.

It understands:

symbols

source lines

stack frames

breakpoints

register display

variables

disassembly

expression evaluation

We should not rewrite all of that.

Instead, implement a GDB Remote Serial Protocol server.

GDBโ€™s documentation describes RSP as the normal communication mechanism between GDB and remote target stubs or gdbserver, over serial or TCP.

Our Python program becomes the remote target server.


342. Why Put the GDB Server in Python?

We could theoretically implement GDB RSP directly inside the Nano.

That would require the ATmega328P to manage:

UPDI timing

OCD state

GDB packet framing

checksums

register packets

memory requests

breakpoints

target descriptions

host serial

inside:

32 KB Flash

2 KB SRAM

It is possible to build small GDB stubs.

It is not the sensible architecture here.

Python gives us:

memory

file handling

ELF tools

logging

network sockets

tests

easy iteration

while the Nano remains a small deterministic hardware adapter.


343. The GDB Server Architecture

Our system now becomes:

                    AVR-GDB
                      |
                      | TCP localhost:3333
                      | GDB RSP
                      |
                      v
              +------------------+
              | Python Debug     |
              | Server           |
              |                  |
              | GDB RSP          |
              | ELF awareness    |
              | breakpoints      |
              | target model     |
              +--------+---------+
                       |
                       | USB serial
                       |
                       v
              +---------------------------+
              | Arduino Nano              |
              |                           |
              | debug primitives          |
              | UPDI command 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 |
                   | interface       |
                   | Q4 TX / Q2 RX   |
                   +--------+--------+
                            |
                            | one-wire UPDI_DATA
                            |
                            v
              +------------------+
              | AVR16DU28        |
              |                  |
              | UPDI             |
              | OCD              |
              | CPU              |
              +------------------+

The separate RESET/HV and target-power paths remain available when required by the selected device/debug-entry procedure, but they are not part of every ordinary debug transaction.

This division of responsibility is both practical and teachable.

344. Minimum GDB Feature Set

We do not need to implement every RSP command.

A minimal useful server needs primitives corresponding roughly to:

connect

read registers

write registers

read memory

write memory

continue

single step

set breakpoint

clear breakpoint

report stop reason

GDBโ€™s current remote protocol negotiates supported features, so a stub can truthfully expose only what it implements.

That means we do not have to fake capabilities.


345. Useful RSP Packets

Conceptually, GDB may issue requests such as:

?

meaning:

why did the target stop?

and commands broadly associated with:

g read registers

G write registers

m read memory

M write memory

c continue

s single step

Z0 insert software breakpoint

z0 remove software breakpoint

Z1 insert hardware breakpoint

z1 remove hardware breakpoint

The current GDB documentation explicitly identifies Z0 and Z1 as the standard software- and hardware-breakpoint packet classes.

The Python server translates these into our own programmer/debugger operations.


346. Example Translation

GDB says conceptually:

m3e20,10

meaning:

read 0x10 bytes beginning at address 0x3E20

Our server translates:

Nano command:

READ 3E20 10

Nano returns:

12 00 A4 36 …

Python converts this to the hex representation expected by GDB.

The Nano does not know GDB exists.

That is deliberate.


347. Continue

When GDB sends:

c

the server should:

ensure breakpoints correctly installed

  โ†“

tell Nano to resume CPU

  โ†“

wait for target stop notification/poll state

  โ†“

read stop reason and PC

  โ†“

send stop reply to GDB

The challenge lies primarily in the OCD primitives:

RUN

STOP DETECTION

STOP REASON

not in the GDB packet itself.


348. Polling Versus Asynchronous Stop Detection

There are two broad ways for the server to know the target stopped.

Polling

Repeatedly ask:

is CPU stopped?

For example:

every 1โ€“10 ms

This is simple.

Asynchronous notification

If the debugger interface can signal a stop condition without polling, use that mechanism.

Microchip documentation mentions detection/signaling of CPU Break/Stop conditions as an OCD feature.

But until we understand the low-level signaling fully, polling is a perfectly reasonable first implementation.


349. Do Not Poll Too Aggressively

If we ask:

STOPPED?

thousands of times per second, we may:

flood UPDI

consume USB serial bandwidth

increase host CPU usage

disturb timing

A starting interval such as:

2โ€“10 ms

would provide an interactive debugger experience while remaining manageable.

Then measure.

As always:

test

do not guess


350. Read Registers After Halt

Once the stop condition is confirmed:

read PC

read SP

read SREG

read general registers

Then send GDB whatever register layout the AVR GDB target expects.

The ordering is architecture-specific.

Do not invent it.

Use the target description expected by the AVR GDB build and test with known register values.

This is a perfect place for automated unit tests.


351. A Register Test Firmware

Create a target test function:

void register_test**()**

{

volatile uint8\_t a = 0x12;

volatile uint8\_t b = 0x34;

volatile uint16\_t c = 0x5678;

asm volatile**(**"nop"**)**;

asm volatile**(**"nop"**)**;

asm volatile**(**"nop"**)**;

}

Set a breakpoint near the NOPs.

Then inspect:

registers

stack

SRAM locations

and compare them with:

compiler disassembly

ELF symbols

expected values

This is far more informative than debugging a large Arduino sketch immediately.


352. Disassembly Is Your Friend

During debugger bring-up, use:

avr-objdump

or the corresponding AVR toolchain disassembler on the ELF.

You want to know:

source statement

 โ†“

actual AVR instructions

 โ†“

addresses

For example:

00000120 <register_test>:

0120: …

0122: …

0124: nop

0126: nop

Then if the debugger reports:

PC = 0x0124

we can verify it independently.


353. Breakpoint Test 1 โ€” Hardware Only

Do not begin with software breakpoints.

Set one hardware breakpoint at a known address.

Run.

Verify:

CPU stops

PC corresponds to breakpoint

registers readable

memory readable

continue works

Then repeat:

1000 times

if practical.

Only once one hardware breakpoint is completely reliable should we add the second.


354. Breakpoint Test 2 โ€” Two Hardware Breakpoints

Create a loop containing two identifiable points:

for (;;)

{

point_a**()**;

point_b**()**;

}

Set hardware breakpoints at both.

Verify alternating stops:

A

B

A

B

A

B

This tests:

both comparators

stop-reason handling

resume

PC retrieval

for the targetโ€™s documented two-breakpoint implementation.


355. Software Breakpoint Test

Only after hardware breakpoints work:

1. Read original Flash instruction.

2. Save it on the PC.

3. Insert BREAK instruction.

4. Verify Flash.

5. Run target.

6. Confirm stop at expected location.

7. Restore original instruction.

8. Verify restoration.

9. Single-step original instruction.

10. Reinsert breakpoint if still enabled.

Every step should be logged.

Software-breakpoint bugs can corrupt program Flash.


356. Program Flash Modification Must Reuse Our Part III Code

Do not write a second Flash programmer inside the debugger.

We already built:

erase

write

verify

in Part III.

Use the same tested code.

This is one of the rewards of good architecture.

Our software-breakpoint manager calls:

programmer.patch_flash_instruction**()**

which ultimately uses the same NVM backend proven earlier.


357. Flash-Wear Reduction

Because software breakpoints modify Flash, we should avoid unnecessary writes.

If a breakpoint is already installed:

do not rewrite it

If several breakpoints lie in one page:

merge changes

erase page once

rewrite page once

rather than erasing repeatedly.

A mature debugger can also cache original pages.

Independent open-source UPDI debug projects have likewise treated software-breakpoint wear as a real design concern.


358. Instruction Injection

Some advanced debug approaches can execute selected instructions while the CPU is under debug control rather than repeatedly altering Flash.

Current community UPDI debugging software reports use of instruction-injection techniques to reduce software-breakpoint wear.

This is an advanced feature.

Our first debugger does not need it.

But it belongs on the roadmap because it can improve:

stepping

software breakpoint handling

register access

Flash endurance

once the underlying OCD mechanism is understood.


359. A Real Open-Source Proof That UPDI + GDB Is Feasible

This project is not merely theoretical.

Current open-source software exists that exposes UPDI targets through a GDB server using serial-UPDI hardware.

For example, avr-absurd documents:

instruction-level stepping

two hardware breakpoints

optional software breakpoints

GDB Remote Serial Protocol

over a SerialUPDI programmer, with GDB connecting via a TCP port.

That is useful evidence for our architecture:

SerialUPDI interface

   \+

PC-side GDB server

   \=

practical UPDI debugging

Our Nano simply replaces the generic serial-UPDI electrical adapter with our own programmable hardware bridge.


360. Why We Should Study Existing Open-Source Debuggers

Not to copy blindly.

To answer questions such as:

How is OCD mode entered?

How is CPU stop requested?

How are breakpoints represented?

How is single-step triggered?

How are CPU registers transferred?

How are stop reasons detected?

Then compare those observations with:

Microchip documentation

known-good debugger traffic

our Nano sniffer captures

The combination is much stronger than any one source.


361. The Nano UPDI Sniffer Becomes a Research Instrument

The sniffer project introduced in Part II is especially important here.

Repository:

github.com/Monotoba/Nano_UPDI_Sniffer/

For ordinary programming we already know most of the protocol.

For debugging, the analyzer can help discover the parts that Microchipโ€™s public device documentation leaves opaque.

The experiment becomes:

Microchip debugger

|

  v

AVR16DU28

AND

Nano UPDI Sniffer watching the line

Then perform one debugger operation at a time.


362. Capture Only One Operation at a Time

Do not begin by recording an entire ten-minute IDE debug session.

That gives:

thousands of bytes

many operations

uncertain boundaries

Instead:

Experiment 1

Connect debugger.

Do nothing.

Capture.

Experiment 2

Connect and halt.

Capture difference.

Experiment 3

Read one SRAM byte.

Capture difference.

Experiment 4

Read PC.

Capture difference.

Experiment 5

Set one hardware breakpoint.

Capture difference.

Experiment 6

Single-step once.

Capture difference.

This is differential protocol analysis.

It is far easier to understand.


363. Annotate Captures

For each capture record:

target

firmware

tool

tool version

operation

UPDI baud

target VDD

timestamp

For example:

Target: AVR16DU28-I/SP

Tool: Atmel-ICE

Operation: single step

Firmware ELF: debug-test-3

Target VDD: 5.02 V

Capture: 20260823-step-001

Then decoded packets can be compared months later.


364. A Debug Research Notebook

Create:

docs/research/ocd/

with files such as:

001-connect.md

002-halt.md

003-read-pc.md

004-run.md

005-step.md

006-hw-breakpoint.md

Each file should contain:

hypothesis

experiment

capture

decoded transactions

conclusion

confidence

open questions

This is how undocumented protocol work remains reproducible rather than becoming folklore.


365. Confidence Levels Matter

Not all protocol knowledge has equal authority.

Mark findings:

DOCUMENTED

directly stated by Microchip

CONFIRMED

repeated observation on multiple captures

INFERRED

behavior strongly suggested but not formally confirmed

SPECULATIVE

hypothesis awaiting experiment

For example:

Two hardware breakpoints:

DOCUMENTED

Exact OCD register X purpose:

CONFIRMED or INFERRED

Meaning of unknown bit 5:

SPECULATIVE

That discipline is particularly important in an article intended to help engineers.


366. GDB Server First Without Real OCD

We can build much of the Python GDB server before the OCD reverse engineering is finished.

Create a simulated target backend.

For example:

class FakeTarget:

def __init__(self):

    self.memory = bytearray**(**65536**)**

    self.registers = bytearray**(**32**)**

    self.pc = 0

    self.running = **False**

Then test:

GDB connects

GDB reads memory

GDB reads registers

GDB sets breakpoint

GDB continues

fake target reports stop

This isolates GDB RSP work from UPDI work.


367. Backend Interface

Define:

class DebugTarget:

def halt**(self)** –> None:

    ...

def run**(self)** –> None:

    ...

def step**(self)** –> None:

    ...

def read_memory**(**

    self,

    address: int,

    length: int,

)> bytes:

    ...

def write_memory**(**

    self,

    address: int,

    data: bytes,

)> None:

    ...

def read_registers**(self)** –> bytes:

    ...

def set_hw_breakpoint**(**

    self,

    address: int,

)> None:

    ...

Then implementations can be:

FakeTarget

NanoUpdiTarget

future PICkitTarget

future AtmelIceTarget

The GDB server remains unchanged.


368. Nano Debug Command Set

Our Nano host protocol can grow to:

DBG ENTER

DBG LEAVE

DBG HALT

DBG RUN

DBG STEP

DBG STATUS

DBG PC

DBG SP

DBG SREG

DBG REG READ <n>

DBG REG WRITE <n> <value>

DBG BREAK SET <n> <addr>

DBG BREAK CLEAR <n>

and reuse:

READ

WRITE

PROGRAM

VERIFY

from the programmer interface.

This is much easier to test manually than a binary debugger protocol.


369. Example Manual Session

Eventually:

NanoUPDI**>** dbg enter

OK DEBUG MODE

NanoUPDI**>** dbg halt

OK STOPPED

NanoUPDI**>** dbg pc

PC 0000032A

NanoUPDI**>** dbg sp

SP 3FFF

NanoUPDI**>** dbg sreg

SREG 80

NanoUPDI**>** dbg reg read 24

R24 42

NanoUPDI**>** dbg run

OK RUNNING

If this works from Serial Monitor, connecting GDB becomes much less mysterious.


370. Debug Mode Should Have Its Own State

Extend our state machine:

Disconnected

|

Connected

|

Identified

|

 +-------- ProgrammingMode

|

 +-------- DebugMode

|

             +---- Running

|

             +---- Stopped

Do not permit:

Flash erase

while the target is actively running in DebugMode unless a carefully designed operation requires it.

State transitions should be explicit.


371. Programming and Debugging Share Infrastructure

Our work was not duplicated.

Both modes use:

UPDI physical layer
(D6 TX / D5 RX through the Q4/Q2 translator)

UPDI framing

SYNCH

BREAK

LDS

STS

LD

ST

KEY

memory access

device identification

target voltage

HV activation

Then they diverge:

             UPDI
               |
        +------+------+
        |             |
        v             v
      NVMCTRL         OCD
        |             |
 programming       debugging

This is why the interface is called Unified.


372. Debug Entry May Also Need HV

If a target’s normal UPDI access has been reconfigured and its device supports HV override, debugging may require the same activation hardware we built in Part IV.

For our finalized programmer that means:

modern RESET-HV target:
    external bench HV
        โ†“
    Q1/Q3 switch
        โ†“
    R6 680 ฮฉ
        โ†“
    J4 route/arm
        โ†“
    TARGET_RESET

older shared-pin target:
    external bench HV
        โ†“
    Q1/Q3 switch
        โ†“
    R6 680 ฮฉ
        โ†“
    J4 HV_OUT
        โ†“
    deliberately wired shared UPDI/HV node

The broad sequence becomes:

HV activation if required
       โ†“
establish UPDI through D6/D5 transistor interface
       โ†“
activate OCD
       โ†“
halt/run/debug

Microchip debugger documentation notes that shared-UPDI devices may require HV activation on UPDI or RESET depending on the device.

So our HV work was not programmer-only infrastructure.

373. Debugging a Locked Device

Security restrictions matter.

A locked device is deliberately not supposed to reveal protected application information merely because a debugger asks nicely.

The OCD and lock mechanisms cooperate to restrict access.

Microchipโ€™s documentation advertises only selected monitoring functionality on locked targets, such as access to CRC status, rather than unrestricted program inspection.

Our debugger must respect the targetโ€™s security state.

It should report:

TARGET LOCKED

DEBUG ACCESS RESTRICTED

rather than repeatedly attempting prohibited reads.


374. PDID Again

The stronger PDID-style protections discussed in Part IV can disable external programming/debug access by design.

A debugger cannot treat HV as a universal bypass.

Security features that deliberately disable the external interface are intended to remain effective against external tools.

That is a target policy, not a programmer failure.


375. Stack Inspection

Once we can read:

SP

and SRAM, we can inspect the active stack.

Conceptually:

SP โ†’ current stack location

Then GDB uses:

debug information

calling convention

saved return addresses

frame rules

to reconstruct:

function C

called by function B

called by function A

This gives the familiar:

backtrace

command.

Again, the Nano merely reads bytes.

The PC does the difficult symbolic interpretation.


376. Local Variables

A variable in C++ source may be:

int temperature;

but at the current optimization level it might live in:

R18:R19

or:

SRAM relative to SP

or:

nowhere

because the optimizer proved it unnecessary.

DWARF debugging information tells GDB how to locate it.

This is why our GDB server should not try to implement:

print temperature

itself.

Its job is to expose raw machine state.


377. Interrupts and Single Stepping

Interrupts complicate stepping.

Suppose:

STEP

executes one instruction but an interrupt becomes pending during that operation.

Should the debugger:

enter ISR?

That depends on OCD behavior and debugger policy.

Likewise, step-over may require managing interrupts and temporary breakpoints carefully.

These edge cases should be tested only after basic stepping works.


378. Debugging Sleep Modes

Modern AVR devices often spend substantial time in:

IDLE

STANDBY

POWER-DOWN

Microchip documents monitoring of sleep status through UPDI/OCD facilities on applicable devices.

A debugger should be able to report:

TARGET SLEEPING

rather than incorrectly declaring communication failure.

This becomes particularly important in low-power applications.


379. Debugging ISRs

One of the most useful tests is:

break inside timer ISR

because it exercises:

interrupt flow

stack

SREG

return address

breakpoint behavior

A test program could increment a counter from TCA.

Set a breakpoint in the ISR.

Verify:

PC at ISR

interrupt flag state

SP changed as expected

SREG I-bit state

counter value

This gives us an excellent end-to-end debugger qualification test.


380. Change-of-Flow Break Features

Microchip describes change-of-flow and interrupt-related breakpoint capabilities for newer AVR E/D OCD.

These can support more sophisticated debugging than simple address comparators.

Potential events include code flow transitions such as:

branch

call

return

interrupt

depending on implementation.

These features should be regarded as advanced phase-two OCD work.

They are not required for our first GDB-capable debugger.


381. Watchpoints Are Not Automatically Guaranteed

Do not assume that because GDB has:

watch

rwatch

awatch

commands, the AVR UPDI OCD necessarily exposes general hardware data watchpoints.

GDB RSP defines packets for write/read/access watchpoints, but the target stub advertises only those it actually supports.

Our first server should therefore say:

hardware breakpoint: supported

software breakpoint: supported

data watchpoint: unsupported

unless experimentation proves otherwise for a particular OCD generation.

Honest capability negotiation is better than pretending.


382. Our First Debugger Does Not Need Every Feature

Version 0.1 can be useful with only:

halt

run

single-step

read memory

write SRAM

read PC

read SP

read SREG

read registers

two hardware breakpoints

That is already enough to debug an enormous number of embedded problems.

Then add:

software breakpoints

source-level stepping

profiling

advanced flow breakpoints

incrementally.


383. Suggested Development Milestones

A disciplined sequence is:

Milestone D1

Activate OCD reliably.

Milestone D2

Detect running/stopped state.

Milestone D3

Stop CPU on demand.

Milestone D4

Read PC while stopped.

Milestone D5

Resume execution.

Milestone D6

Single-step.

Milestone D7

Read SP/SREG.

Milestone D8

Read general register file.

Milestone D9

Set one hardware breakpoint.

Milestone D10

Use two hardware breakpoints.

Milestone D11

Build Python target backend.

Milestone D12

Connect avr-gdb.

Milestone D13

Read memory from GDB.

Milestone D14

Continue/stop through GDB.

Milestone D15

GDB hardware breakpoints.

Milestone D16

Software breakpoints.

Milestone D17

Arduino IDE integration.

Each milestone should have tests.


383.1. Hardware Sanity Check Before OCD Entry

Before the first OCD experiment in a session, the firmware should verify the same physical conditions used by the programmer:

UPDI_DATA is not stuck LOW
VTARGET is within the device profile
HV is disabled unless explicitly required
TARGET_RESET is released
target power state is known

If the selected target requires HV debug entry, the host should also verify:

measured HV on A1
J4 route/arm selection
allowed HV range

before permitting the pulse.

This keeps debugger bring-up from accidentally becoming an electrical debugging problem.


384. The Most Important OCD Test: Halt โ†’ Read โ†’ Run

Before breakpoints, GDB, source lines, or fancy UI, prove:

CPU running

 โ†“

HALT

 โ†“

PC readable

 โ†“

SRAM readable

 โ†“

RUN

 โ†“

application continues

Repeat:

1000 times

If this primitive is unreliable, nothing above it will be reliable.


385. Test That Halt Does Not Reset

Use a target program containing:

volatile uint32_t counter = 0;

void loop**()**

{

++counter;

}

Let it run.

Halt.

Read counter.

Resume.

Halt again.

Expected:

counter increased

If the value returns to zero after every halt:

we are resetting

not debugging.

This simple experiment detects a fundamental implementation mistake.


386. Timing Tests

Measure:

halt latency

resume latency

single-step latency

memory-read bandwidth

register-read latency

For example:

HALT request โ†’ STOPPED:

2.1 ms

read full register state:

4.7 ms

STEP:

2.8 ms

These measurements tell us whether the Nano/host architecture is responsive enough for interactive use.


387. The Nano May Become the Bottleneck

Our original:

62.5 kbaud

educational UPDI rate was perfect for understanding the protocol.

A debugger exchanges many small operations.

Interactive responsiveness may improve considerably at:

225 kbaud

or faster if the Nano implementation can sustain it reliably.

This is where the qualification work from Part II pays off.

We increase speed only after correctness is proven.


388. PC-to-Nano Baud Also Matters

Suppose:

UPDI = 225 kbaud

but:

PC ↔ Nano = 115200

Then the USB-serial link may become the bottleneck.

A Nanoโ€™s hardware UART can operate at higher rates such as:

230400

460800

500000

depending on USB bridge support and clock error.

Again:

test actual hardware

rather than assuming every cloneโ€™s USB bridge behaves identically.


389. Binary Host Protocol Becomes Worthwhile Here

During programming, text commands were acceptable.

Debugger interaction involves many tiny transactions.

A framed binary protocol now becomes more attractive.

For example:

SOF

VERSION

COMMAND

SEQUENCE

LENGTH

PAYLOAD

CRC16

Commands:

0x20 DBG_ENTER

0x21 DBG_LEAVE

0x22 DBG_HALT

0x23 DBG_RUN

0x24 DBG_STEP

0x25 DBG_STATUS

0x26 DBG_READ_REGS

0x27 DBG_WRITE_REG

0x28 DBG_SET_HWBP

0x29 DBG_CLR_HWBP

Do not replace the human-readable console.

Keep both:

diagnostic console

binary production protocol

if resources allow.


390. Sequence Numbers Help Debugging the Debugger

Suppose Python sends:

SEQ 104: READ_REGS

and the Nano responds:

SEQ 104: OK …

If packets get delayed or corrupted, the host can tell which response belongs to which request.

This becomes increasingly valuable when GDB is generating many requests quickly.


391. Timeouts at Every Layer

Now we have three communications layers.

Each needs timeouts.

GDB ↔ Python

Python ↔ Nano

Nano ↔ target

A target timeout should not cause Python to wait forever.

Likewise, a dead Nano should not freeze the GDB server indefinitely.

Report the correct failure layer:

TARGET TIMEOUT

PROGRAMMER TIMEOUT

GDB CLIENT DISCONNECTED

These mean different things.


392. What Happens If the USB Cable Is Unplugged While Target Is Halted?

This is an important real-world case.

If:

target halted

and:

debugger disappears

should the target:

remain stopped forever?

For a laboratory board, maybe.

For a control system, probably not.

A mature debugger can implement a disconnect policy:

leave target halted

OR

resume target

OR

reset target

selected by configuration.

The safest choice depends on the application.


393. Breakpoints Must Be Removed on Clean Exit

A hardware breakpoint naturally disappears when the debug hardware resets.

A software breakpoint may remain physically programmed into Flash.

Therefore a debugger must carefully track:

every Flash patch it has made

and restore all original instructions before exiting.

On unexpected PC-side termination, recovery becomes harder.

A breakpoint journal file may help.


394. Breakpoint Journal

Store:

{

“target_signature”: “1E9439”,

“firmware_digest”: “…”,

“breakpoints”: [

{

“address”: 800,

“original”: “0C94”,

“replacement”: “…”

}

]

}

on the PC.

If the debugger crashes, the next session can detect:

Flash contains debug patch

and offer restoration.

Do not blindly restore unless the firmware digest matches.

Otherwise we might overwrite legitimate new firmware.


395. Firmware Digest Protects Against Stale Debug State

Suppose:

breakpoint journal belongs to firmware A

but the user has since programmed:

firmware B

Restoring Aโ€™s โ€œoriginal instructionโ€ into B would corrupt B.

Therefore every debug session should compute something like:

ELF/Flash CRC

and associate breakpoint metadata with that image.

This is another place our Part III verification infrastructure is reusable.


396. GDB Does the Source-Level Work

Once the basic RSP server works, a session might resemble:

$ avr-gdb blink.elf

(gdb) target remote :3333

Remote debugging using :3333

(gdb) break loop

Breakpoint 1 at …

(gdb) continue

Breakpoint 1, loop**()** at Blink.ino:17

(gdb) print counter

(gdb) next

(gdb) info registers

(gdb) continue

Our server translates those high-level requests into low-level OCD operations.

The source line display comes from the ELF.

Not from the Nano.


397. Arduino IDE Integration Comes Last

Once:

avr-gdb

+

our server

+

Nano

+

AVR16DU28

works reliably, Arduino IDE integration becomes mostly configuration and process-launch work.

Arduinoโ€™s platform specification supports debug properties and external debug tooling when a board platform defines them appropriately.

We could eventually provide a custom board/core configuration that tells Arduino tooling:

debug executable = generated ELF

debug server = NanoUPDI GDB server

debug port = 3333

or the equivalent configuration required by the platform.

But the debugger should work from the command line first.


398. Why Command-Line First Is Better

If the Arduino IDE says:

Debug session failed

we do not know whether the problem is:

Arduino IDE

platform configuration

GDB startup

Python server

Nano serial

UPDI

OCD

target

If:

avr-gdb

already works from a terminal, IDE integration becomes a much narrower problem.

Layered validation wins again.


399. Using Our Analyzer Against Atmel-ICE or MPLAB SNAP

Microchip debuggers such as Atmel-ICE and SNAP support UPDI debugging on applicable AVR devices.

They give us a known-good reference implementation.

Our research fixture can be:

                   +---- Microchip debugger
                   |
Target UPDI --------+
                   |
                   +---- Nano UPDI Sniffer

The sniffer must be passive enough not to disturb the line.

Then capture:

connect

halt

run

step

breakpoint

read register

one operation at a time.


400. Do Not Drive the Bus from the Sniffer

A protocol sniffer should be:

receive only

unless deliberately acting as an active test instrument.

It should not:

pull UPDI

inject ACK

alter pull-up

because then we cannot be sure whether the observed debugger behavior is natural.

The measurement instrument must avoid becoming part of the experiment.


401. High-Impedance Input Matters

UPDI is a one-wire interface with finite pull-up strength and timing requirements.

A poorly designed analyzer input can:

add capacitance

slow rise time

change logic thresholds

and cause failures that disappear when the analyzer is removed.

That is why a buffer may be valuable.

Community UPDI-debug research has also noted the weakness of some UPDI I/O behavior and the usefulness of a buffered sniffing arrangement.


402. Protocol Decoder Enhancements for Debugging

Our Nano UPDI Snifferโ€™s decoder should add:

OCD key candidate

OCD status transactions

halt/run operation signatures

breakpoint-register writes

PC reads

SP reads

SREG reads

register-file transactions

as those meanings become confirmed.

Unknown values should remain visible:

UNKNOWN CS WRITE:

address 0x??

value 0x??

rather than being discarded.

Todayโ€™s unknown packet may be tomorrowโ€™s breakthrough.


403. Compare Captures Across Devices

Repeat experiments on:

AVR16DU28

AVR32DU28

AVR16DD28

Then ask:

Which OCD transactions are identical?

Which depend on DEBUG version?

Which depend on family?

Which depend on memory size?

This lets us discover the proper abstraction boundaries.

It is the debugger equivalent of our NVM-generation work in Part III.


404. Do Not Assume AVR DD and DU Debug Identically

The same lesson keeps returning.

Both:

AVR16DD28

AVR16DU28

use UPDI.

Both advertise modern OCD features.

That does not prove every internal OCD register or command is identical.

Use:

SIB debug version

and observed behavior to select a backend.

Potentially:

ocd_v1.cpp

ocd_v2.cpp

just as we created NVM backends.


405. Debug Backends

Conceptually:

struct DebugOps

{

DebugError (*enter**)()**;

DebugError (*leave**)()**;

DebugError (*halt**)()**;

DebugError (*run**)()**;

DebugError (*step**)()**;

DebugError (*readPc**)(uint32_t *)**;

DebugError (*readSp**)(uint16_t *)**;

DebugError (*readSreg**)(uint8_t *)**;

DebugError (*setHwBreakpoint**)(**

    uint8\_t slot,

    uint32\_t address**)**;

DebugError (*clearHwBreakpoint**)(**

    uint8\_t slot**)**;

};

Then:

SIB DEBUG version

  โ†“

select appropriate DebugOps

This keeps speculative generation-specific code out of the generic UPDI layer.


406. Testing an Undocumented Interface Requires More Discipline, Not Less

A common mistake in reverse engineering is:

it worked once

therefore:

we understand it

No.

Repeat experiments across:

cold power-on

warm reset

different code

different addresses

different targets

different debugger sessions

and compare results.

For each inferred field:

change one variable

observe one effect

This is basic experimental design.


407. Example Breakpoint Reverse-Engineering Experiment

Suppose the known-good debugger writes:

unknown register A = 0x34

unknown register B = 0x12

unknown register C = 0x01

when setting a breakpoint at:

0x1234

Repeat with breakpoint:

0x5678

and observe:

A = 0x78

B = 0x56

C = 0x01

Now we have strong evidence:

A/B = breakpoint address, little endian

while:

C = enable/type field

remains an inference.

That is how we should decode the OCD interface.


408. Single-Step Reverse Engineering

Capture:

HALT

then:

STEP

Compare packets.

If only one previously unseen write changes, test it repeatedly.

Then examine:

PC before

PC after

If exactly one instruction executes each time, we have strong confirmation of the step command.

Do not infer step behavior merely because the IDE source line changed.

Source-level stepping can involve temporary breakpoints and multiple machine instructions.


409. The PC May Advance Differently for Different Instructions

AVR instructions are not all the same length.

Some occupy:

1 word

while others occupy:

2 words

So after a machine step:

PC_new – PC_old

may not always be the same number of bytes.

Our tests should include:

NOP

RJMP

CALL

LDS

STS

or representative instruction lengths.

This also validates our PC address-unit interpretation.


410. Interrupt Test for Step

Disable interrupts initially.

Once normal stepping works:

enable timer interrupt

and repeat.

This reveals how OCD handles:

pending interrupt

step

ISR entry

and whether our GDB server needs special policy.


411. Reset as a Debug Operation

Microchip documentation lists reset among program-flow-control operations.

A debugger reset operation commonly wants:

reset target

then halt at reset vector

rather than:

reset and let application run away

So we may need distinct operations:

RESET_RUN

RESET_HALT

even though both ultimately use target reset machinery.


412. โ€œRun to Cursorโ€ Is Just Another Temporary Breakpoint

IDE feature:

Run to Cursor

sounds sophisticated.

The backend algorithm is usually approximately:

set temporary breakpoint at cursor address

continue

stop

remove temporary breakpoint

Again, sophisticated UI does not necessarily require sophisticated target hardware.


413. Profiling and Debugging Can Share the Same OCD

A future host application could support:

Debugger mode

Profiler mode

Monitor mode

using the same Nano hardware.

Debugger

Stop and inspect.

Profiler

Sample PC while running.

Monitor

Periodically read selected memory/register status.

This turns the project into a broader AVR development instrument.


414. Runtime Monitoring Must Be Labeled as Potentially Intrusive

Even if UPDI/OCD supports nonintrusive PC monitoring, arbitrary memory reads may still interact with the target bus or peripheral side effects.

So distinguish:

OCD nonintrusive monitor

from:

periodic system-bus register read

The latter may affect timing or peripheral state.

Do not market every live-value display as โ€œzero impact.โ€


415. The Host GUI Can Come Later

Eventually, our Python host could have:

source window

register view

memory view

breakpoint list

UPDI trace

target voltage

programmer state

perhaps using PySide6.

But GDB already gives us a mature debugging core.

A GUI should initially be a client of:

GDB / GDB-MI

or our own backend rather than duplicating debugger semantics.

For now:

command line

is the correct engineering environment.


416. Testing the GDB RSP Server

Unit-test packets.

For example:

def test_memory_read_packet**()**:

...

Test:

valid checksum

bad checksum

unsupported packet

memory range

register encoding

breakpoint insertion

stop reply

Ctrl-C

disconnect

The GDB layer should be testable without any AVR attached.


417. Hardware-in-the-Loop Debug Tests

Then add real target tests.

Examples:

connect โ†’ halted

run โ†’ running

halt โ†’ stopped

step NOP โ†’ PC advances

set HW breakpoint โ†’ stop at expected PC

clear breakpoint โ†’ no stop

read register โ†’ expected value

write register โ†’ changed behavior

Run these through:

Python โ†’ Nano โ†’ AVR

automatically.


418. GDB-Level Integration Tests

Finally automate:

launch GDB

connect

load symbols

set breakpoint

continue

query PC

inspect variable

step

detach

A test harness can control GDB through:

MI

or command files.

This gives us the entire path:

GDB

โ†“

RSP

โ†“

Python

โ†“

Nano

โ†“

UPDI

โ†“

OCD

โ†“

CPU

under automated test.


419. Commit Every Confirmed OCD Primitive Separately

Because we are working partly from experimental evidence, commit history becomes especially valuable.

For example:

Document observed OCD activation sequence

Implement confirmed OCD entry

Add reliable target halt

Add target run operation

Add confirmed PC read

Add single-step primitive

Add first hardware breakpoint

Add second hardware breakpoint

Add GDB memory packets

Add GDB register packets

Do not combine:

OCD reverse engineering + GDB server + GUI

into one giant commit.

That would make failures extremely difficult to isolate.


420. Keep Raw Captures in the Repositoryโ€”or Document How to Reproduce Them

Protocol captures can become large.

If storing them is reasonable:

captures/

with metadata.

Otherwise store:

capture script

tool settings

SHA256

analysis notes

and preserve the raw files in release artifacts.

The articleโ€™s claims about undocumented behavior should be reproducible.


421. What Our Nano Can Realistically Do

The Nano is fully capable of remaining the electrical/timing bridge.

Its strengths are:

predictable GPIO

precise timing

simple firmware

low cost

wide availability

Its weaknesses are:

limited RAM

limited Flash

no native USB

no modern hardware one-wire UART

Those weaknesses matter much less when the PC performs:

GDB

symbols

breakpoint policy

ELF parsing

logging

UI

The architecture is therefore viable.


422. The Final Hardware Already Includes the Useful Debugging Enhancements

Earlier drafts treated several improvements as future options.

The finalized programmer now already contains the most useful ones:

D6/D5 transistor-buffered UPDI
VTARGET sensing on A0
HV sensing on A1
physical target RESET control on D4
external HV switching on D7
optional target-power switching on D8
physical HV route/arm selection

These features improve:

  • multi-voltage target support;
  • electrical isolation;
  • signal observation;
  • HV recovery;
  • controlled reset;
  • repeatable power-cycle testing.

The remaining future hardware improvements are mainly convenience or performance upgrades, such as:

  • a faster MCU;
  • native USB;
  • automated programmable HV generation;
  • more precise voltage references;
  • stronger dedicated buffering for long cables;
  • or galvanic isolation where a specialized application requires it.

The core debugging architecture does not depend on those future enhancements.

423. A Future Better MCU Could Replace the Nano

Once the project is proven, a modern MCU with:

native USB

multiple USARTs

open-drain modes

more RAM

faster clock

multiple voltage domains

could implement the same architecture much more elegantly.

But the Nano has done something more important.

It has forced us to understand:

every layer

instead of hiding the protocol behind specialized hardware.

That is why it remains the right teaching platform.


424. What We Know and What We Do Not Yet Know

At this point we can divide knowledge cleanly.

Publicly documented and well grounded

Modern UPDI OCD provides capabilities including:

memory access

program-flow control

hardware breakpoints

software breakpoints

PC/SP/SREG observation

register access while stopped

for appropriate devices.

Publicly demonstrated by open-source projects

GDB debugging over serial-UPDI-class hardware is practical, including stepping and breakpoint support.

Community reverse-engineered

Specific low-level OCD activation details and undocumented command/register behaviors.

Still requiring our own validation

Exact implementation details for:

AVR16DU28-I/SP

AVR32DU28-I/SP

AVR16DD28-I/SP

across their specific SIB debug-interface versions.

That is the correct level of confidence to present.


425. A Practical Version-1 Debugger Goal

Our first working debugger should promise only:

AVR16DU28-I/SP

5 V target

normal transistor-translated UPDI or validated RESET-HV activation

halt

run

reset-halt

single machine-step

read PC

read SP

read SREG

read register file

read/write SRAM

read I/O

two hardware code breakpoints

GDB Remote Serial Protocol

command-line avr-gdb workflow

That is already an excellent hobbyist/debug engineering instrument.

Software breakpoints can be version 1.1.

Advanced flow breakpoints can be later.

Arduino IDE integration can follow after the command-line debugger is proven.


426. Proposed Debugger Directory Layout

Our project can now grow into:

nano-updi/
|
+-- firmware/
|   |
|   +-- updi_phy.cpp
|   +-- updi_link.cpp
|   +-- updi_instruction.cpp
|   +-- nvm_p4.cpp
|   +-- debug_backend.cpp
|   +-- debug_vX.cpp
|   +-- voltage_sense.cpp
|   +-- target_reset.cpp
|   +-- target_power.cpp
|   +-- hv_control.cpp
|   +-- host_protocol.cpp
|
+-- host/
|   |
|   +-- nanoupdi/
|       |
|       +-- serial_link.py
|       +-- programmer.py
|       +-- debugger.py
|       +-- gdb_rsp.py
|       +-- breakpoints.py
|       +-- devices.py
|       +-- elf.py
|       +-- errors.py
|
+-- tests/
|
+-- captures/
|
+-- docs/
    |
    +-- research/
        |
        +-- ocd/

The UPDI PHY module owns the hardware-specific D6/D5 transistor interface.

The debugger backend should never manipulate PORTD directly.

Likewise, the GDB server should know nothing about Q2, Q4, D7, or the ADC divider ratios.

Those details remain below clean abstraction boundaries.

Now our little programmer has become a development platform.

427. Full Debugging Data Flow

When the user types:

(gdb) break loop

the complete operation becomes:

GDB

|

| Z1 or Z0 breakpoint request

v

Python RSP server

|

| choose HW or SW breakpoint

v

Nano command protocol

|

| DBG BREAK SET

v

Nano UPDI/OCD backend

|

| target-specific OCD transaction

v

AVR OCD breakpoint resource

When the CPU reaches it:

AVR CPU

|

| breakpoint condition

v

OCD STOP

|

| observed/polled over UPDI

v

Nano

|

| STOPPED + PC

v

Python

|

| GDB stop response

v

GDB

|

| ELF/DWARF lookup

v

Blink.ino:17

That is the complete debugger stack.


428. Debugging Is Not Magic Either

This is the central lesson.

When an IDE displays:

Breakpoint 1, loop**()** at sketch.ino:27

it can look as though the microcontroller somehow understands:

C++

source files

line numbers

variables

It does not.

The target understands:

machine instructions

addresses

registers

memory

stop/run control

The debugger on the PC combines those raw values with:

ELF

DWARF

symbols

compiler knowledge

and presents the illusion of source-level execution.

Once the layers are separated, debugger architecture becomes far less mysterious.


429. Part V Review

We began with a programmer.

We now have the architecture of a debugger.

The target-side AVR OCD gives us documented capabilities such as:

program-flow control

memory-mapped inspection

two hardware breakpoints on our modern target class

software breakpoints

PC/SP/SREG observation

register-file access while stopped

for applicable modern UPDI AVRs.

But we also discovered an important limitation:

Microchipโ€™s public documentation does not expose every low-level OCD detail as completely as it exposes normal NVM programming.

Therefore our debugger implementation must combine official documentation with:

known-good debugger captures

community research

open-source implementations

our Nano UPDI Sniffer

controlled experiments

and clearly distinguish:

documented

confirmed

inferred

speculative

information.

We designed the three-layer architecture while retaining the finalized hardware PHY:

Nano D6 / UPDI_TX -> Q4 -> UPDI_DATA -> Q2 -> Nano D5 / UPDI_RX

Above that physical layer:

GDB

|

| RSP

v

Python debug server

|

| Nano command protocol

v

Arduino Nano

|

| UPDI/OCD

v

AVR16DU28

We deliberately assigned:

timing and electrical control

to the Nano,

while assigning:

ELF

symbols

DWARF

breakpoint policy

GDB protocol

logging

to the PC.

We examined:

halt

run

step

reset

PC

SP

SREG

register file

SRAM

peripherals

hardware breakpoints

software breakpoints

Flash wear

breakpoint restoration

profiling

GDB RSP

Arduino IDE integration

and laid out an incremental path to implementing each one.


430. The Most Important Conclusion of Part V

Our target-side development connection remains remarkably small:

TARGET_RESET
VTARGET
UPDI_DATA
GND

and ordinary debugging normally uses only the one UPDI data conductor plus ground and target-voltage reference.

Inside the programmer, however, that one target wire is implemented much more carefully than the original teaching circuit:

D6 = UPDI_TX through Q4 open-collector driver
D5 = UPDI_RX through Q2 receive translator

with independent support for:

D4 = target RESET control
D7 = externally supplied HV switching
D8 = optional target-power control
A0 = VTARGET measurement
A1 = HV measurement

Through that small target connection we can potentially:

  • identify the MCU;
  • erase it;
  • program it;
  • verify it;
  • configure it;
  • recover UPDI access;
  • halt its CPU;
  • resume its CPU;
  • single-step instructions;
  • inspect memory;
  • inspect peripherals;
  • read CPU state;
  • set hardware breakpoints;
  • set software breakpoints;
  • profile execution.

That is a remarkable amount of functionality for one data conductor.

And now we understand why.

The wire itself is simple.

The intelligence exists in the stack built on top of it.

431. Where We Go Next

We now have all of the major technical pieces needed for the final installment.

Part VI will consolidate the entire series.

We will revisit the design from the perspective of someone who now wants to actually build and use the tool.

It will include:

complete system architecture

final Nano pin assignment

final D6/D5 transistor UPDI interface

final four-pin target connector

VTARGET and HV sensing

D4 target-reset control

Q1/Q3 external-HV switching

J4 HV route/arm selection

D8 optional target-power control

modern RESET-HV workflow

older shared-pin 12 V workflow

firmware module structure

host module structure

complete command set

programming workflow

debugging workflow

test procedures

logic-analyzer procedures

Nano UPDI Sniffer integration

failure diagnosis

common mistakes

security considerations

recommended device database structure

Arduino IDE workflow

Python/PySerial workflow

GDB workflow

Git/test strategy

We will also include a substantial troubleshooting section covering symptoms such as:

UPDI line stuck LOW

UPDI line stuck HIGH

SYNCH gets no response

parity failures

works slowly but not at 225 kbaud

device identifies but will not enter programming mode

KEY rejected

NVM busy never clears

Flash programs but verify fails

wrong signature

locked device

HV pulse fails

HV works once but not after reset

GDB connects but register reads are wrong

breakpoint address is doubled or halved

single-step advances to unexpected address

software breakpoint corrupts Flash

debugger stops but PC is nonsense

Finally, we will assemble a resource directory containing the primary Microchip documentation, Arduino tooling documentation, AVRDUDE, Microchipโ€™s open-source programming tools, GDB documentation, useful UPDI open-source projects, and our Nano UPDI Sniffer/Analyzer project.

That final installment will also clearly separate:

what we know from published specifications

what we verified experimentally

what is implementation-specific

what remains reverse-engineered

so that the finished article remains useful not only to hobbyists but also to engineers who need to know the provenance and confidence of technical claims.

At that point we will have gone from:

โ€œWhat is UPDI?โ€

to:

โ€œWe understand the stack well enough to build, test, program, recover, analyze, and debug through it.โ€

IMPLEMENTING UPDI FROM SCRATCH

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

Leave a Reply

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