IMPLEMENTING UPDI FROM SCRATCH
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 pin | Function |
|---|---|
| D2 | ACT/status indication |
| D3 | HV/status indication |
| D4 | TARGET_RESET_CTRL |
| D5 | UPDI_RX |
| D6 | UPDI_TX |
| D7 | HV_ENABLE |
| D8 | TARGET_POWER_ENABLE |
| A0 | VTARGET_SENSE |
| A1 | HV_SENSE |
| A4/A5 | Optional IยฒC expansion |
| D10โD13 | Optional SPI expansion |
The target connector 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: