What is PyAvrOCD? How do you use it? What are its strong points? Are there alternatives? Why did it take so long to come up with version 1.0.0? And how to pronounce it? These and other questions will be addressed in this blog post.

TL;DR PyAvrOCD can help you debug programs that run on classic AVR chips.

GDB Servers for embedded debugging

In addition to proprietary software development tools for embedded systems, there also exists a large number of open-source development tools. And the symbolic debugger GDB plays a significant role in this context, often only implicitly as the debugging engine in an IDE. For employing GDB in embedded debugging, one needs some software glue between GDB and the debug probe that is attached to the target system: a GDB server, which translates between the protocol the debug probe speaks, often CMSIS-DAP, and GDB’s remote serial protocol (RSP). PyAvrOCD is such a GDB server tailored to AVR MCUs (currently only classic ATtinys and ATmegas). It connects to the EDBG-based Microchip debug probes and the DIY debug probe dw-link.

What does PyAvrOCD stand for and how to pronounce it?

The Py part indicates that this system has been written in Python, which means that it is platform-agnostic. The USB interface module is platform-dependent, though. Avr stands for the AVR MCUs, whereby officially, AVR is not an acronym. However, Internet wisdom (including Wikipedia) has it that AVR stands for Alf and Vegard’s (the first names of the architects of this processor family) RISC processor. Finally, OCD is an acronym for on-chip debugging, which is the capability of debugging on the production chip instead of using in-circuit emulation.

So, how to pronounce it? Since AVR and OCD are both initialisms, the canonical pronunciations would be Pie-Ay-Vee-Ar-Oh-See-Dee. However, you are free to pronounce it as ‘Piaf rocked’, making this piece of software a tribute to the great chanson singer Edith Piaf.

How do you use PyAvrOCD?

Arduinoists can make use of PyAvrOCD by installing a debug-enabled Arduino core. This will take care of downloading and installing PyAvrOCD and the symbolic debugger AVR-GDB. And with that, all of a sudden, the greyed-out debug button on the Arduino IDE window becomes active, and one can start debugging. You may want to check out the quick start guides.

If you are a Pythonist, PyAvrOCD can be simply installed by using pip or pipx:

pip install pyavrocd

All others can download binaries from the Releases page of the GitHub repository and copy the binaries to an appropriate location. Once PyAvrOCD is installed at a place that is in your PATH, you can invoke it like that:

pyavrocd --manage all --device atmega328p 

It will search for a connected debug probe and will then wait for GDB to connect to it. Assuming that you use the CLI interface of GDB, this might look as follows:

 > avr-gdb blink.ino.elf
GNU gdb (GDB) 17.1
Copyright (C) 2026 Free Software Foundation, Inc.
...
(gdb) target remote :2000
Remote debugging using :2000
0x00000000 in __vectors ()
(gdb) monitor debugwire
debugWIRE mode is disabled
(gdb) monitor debugwire enable
*** Please power-cycle the target system ***
Ignoring packet error, continuing...
debugWIRE mode is enabled
(gdb) load
Loading section .text, size 0x596 lma 0x0
Start address 0x00000000, load size 1430
Transfer rate: 1 KB/sec, 1430 bytes/write.
(gdb) break loop
Breakpoint 1 at 0x470: file /Users/.../blink.ino, line 13.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue
...

What are PyAvrOCD’s strong points?

The design goal behind PyAvrOCD was to make its usage as easy and user-friendly as possible. To this end, the following objectives were addressed, which I believe have been achieved to a large extent:

  • cross-platform,
  • easily installable,
  • well documented,
  • configurable fuse management,
  • comprehensible presentation of peripheral registers,
  • safe access to peripheral registers,
  • interrupt-safe and performant single-stepping,
  • minimal flash wear, and
  • robustness (for all supported MCUs and debug probes).

The first three objectives are obvious and have been evidently achieved. So let us look at the remaining ones.

Configurable fuse management

When one wants to debug an AVR MCU, some preparations are often necessary in the form of fuse management. For example, you may need to clear the lock bits, set the fuse for enabling debugging, and clear the fuse that initiates a jump to the bootloader. Most other AVR GDB servers leave that to the user. PyAvrOCD, on the other hand, can be instructed to take care of that. And this can be controlled on a fine-grained level. With the --manage command line option, you can specify which fuses PyAvrOCD shall manage.

Comprehensible presentation of peripheral registers

One of the important tasks in embedded debugging is to track the content of I/O registers and perhaps change them. The Bloom system is very good at that. PyAvrOCD by itself does not provide support for this task. However, with PyAvrOCD, you get SVD specifications, which can be used by IDEs to display meta information and content of I/O registers in the IDE. This information is extracted from the ATDF files provided by Microchip using the atdf2svd utility that has been developed in the Rust ecosystem.

In the Arduino IDE 2, the SVD files are automatically employed to provide a comprehensive view of all I/O registers through the CORTEX PERIPHERAL pane (ignore the CORTEX part in the pane title). When you use another IDE, e.g., VS Code with PlatformIO, you have to take care of specifying the path to the SVD file for the MCU that you are working with.

Safe access to peripheral registers

Reading particular I/O registers in AVR MCUs can have side effects. For example, reading the data register of the USART or SPI peripheral will discard the read value. This is the intended effect when this register is read in a program, but it will distort debugging when the debugger reads the register. Furthermore, such read access may happen without an explicit read command issued by the user. If one uses the PERIPHERAL pane in some IDEs, a whole range of I/O registers is queried. In order to avoid spurious, unintended read access to I/O registers, PyAvrOCD maintains a list of registers, derived from the ATDF specifications of the AVR MCU, for which reading by the debugger is forbidden. Similarly, writing to an I/O register can have unintended side effects. In PyAvrOCD, read and write access to such registers is blocked.

Interrupt-safe and performant single-stepping

When single-stepping an embedded system, an interrupt often distracts the flow of single-stepping through the code. And in this case, one ends up in the interrupt dispatch table without an idea of where to continue. As a user, one would expect that interrupts are transparent to single-stepping, either being served in the background or not served at all. And this is what is implemented in PyAvrOCD as the default behavior.

Another annoying phenomenon when single-stepping is that single-stepping a source line can take ages when the line contains a loop, perhaps implicitly. For example, the macro _delay_ms is expanded into a delay loop. PyAvrOCD handles that using range-stepping. While this helps with the _delay_ms case, there are other possible configurations where executing a line takes forever. The only safe way out is to stop execution, set a breakpoint somewhere else, and then continue.

Unfortunately, stopping the GDB server asynchronously when heavy single-stepping takes place is not easy and most often results in killing the GDB server. PyAvrOCD synchronizes the asynchronous stop signal with the synchronous flow of RSP commands and, by that, achieves a reliable way to stop execution.

Minimal flash wear

Flash memory in AVR MCUs has a limited number of erase/write cycles. On classic chips, 10,000 cycles are guaranteed. While it seems hardly conceivable that one reaches the limit with reprogramming the flash, software breakpoints are also implemented by reprogramming single flash pages. However, it is not only setting and clearing breakpoints that can take a toll. Sometimes, each breakpoint hit induces two reprogramming cycles. This is the case for breakpoints placed at two-word instructions for all GDB servers except PyAvrOCD.

PyAvrOCD minimizes flash wear by using hardware breakpoints as much as possible and by not inducing flashing reprogramming at breakpoint hits. In the case of two-word instructions, it reverts to simulation instead of reprogramming twice.

Robustness

Robustness was achieved by extensive testing. In addition to 500+ unit tests that cover roughly 90% of the source code, there are also integration tests and a large number of end-to-end tests, which simulate user interaction through GDB. These end-to-end tests were then run on almost all classical AVR MCUs using a variety of different debug probes. In doing this, I discovered that there are a number of chip idiosyncrasies that one has to be aware of:

  • ATtiny2313/ATtiny2313A: The pair of ATtiny2313 and ATtiny2313A is very special. They share the same chip signature, but their OCD registers are at different addresses. One has to be aware of that to handle it correctly.
  • ATtiny441, 841, and 1634: For these MCUs, the flashing process is different from the rest of the pack.
  • ATmega48 and ATmega88: These chips appear to be special in that they are not debuggable by most open-source tools. Since they have the same chip signature as their cousins with an A-suffix, it is hard to distinguish them from those and avoid “bricking” them by trying to switch them to debugWIRE mode. In PyAvrOCD, a method to distinguish them has been implemented, however.
  • ATmega88A, ATmega168A, ATmega328, ATmega328P: These chips, or at least some chip revisions, pretend to be of a different chip variety when in debugWIRE mode, e.g., the ATmega328 chip claims to be an ATmega328P. Interestingly, this is also true for a number of JTAG MCUs.
  • ATmega16(A) and ATmega64(A): These chips have non-zero unused bits in their program counter. This does not show when querying the PC directly, but the return addresses on the stack contain such non-zero bits.
  • ATmega329(P) and ATmega3250(P): Similar to the ATmega16/64, these chips have a non-zero unused bit. In contrast to them, though, they show that when the PC value is queried. This means, if the GDB server does not mask the PC, GDB will be completely lost when the program is stopped.

There may be more such idiosyncrasies because I did not test every chip variety in the group of JTAG Mega targets. However, for the biggest problems, the non-zero unused program counter bits, I developed a general solution.

Non-zero unused PC bits are masked out by PyAvrOCD when GDB queries for the value of the PC. Non-zero unused bits in return addresses are masked out by GDB using a patch I supplied. Until the patch makes it into the distribution, I provide a patched GDB version that is shipped with the PyAvrOCD binaries and can be downloaded from the Releases page of my personal avr-gdb repo.

What are alternatives to PyAvrOCD?

PyAvrOCD is not the first GDB server for AVRs. In the AVR context, one now has the choice between fifteen different GDB server implementations. The following table, ordered by the year when development started, gives an overview of the alternatives to PyAvrOCD.

NameStart of
develop-
ment
Supported
platforms
Debug
interfaces
Supported
MCUs
Supported
debug
probes
Comment
atbackend (Microchip Studio 7)1997Windows onlyAllAllAllPropriatary GDB server by Microchip
AVaRICE2001Originally Linux, by now also Windows and Mac (C++, Posix)JTAG, debugWIRE, PDIClassic ATtinys, ATmegas, and XmegasAll, but the latest (5 series)First open source AVR GDB server ever.
dwire-debug2015Cross-platform (C)debugWIREATtiny13, 84, 85, 841, 45, ATmega 168, 328USB-UART bridgeMinimal implementation, only one hardware breakpoint, based on debugWIRE
reverse engineering.
debugwire-gdb-bridge2018Cross-platform (Pascal)debugWIREATtiny 13, 2313, 24, 44, 84, 25, 45, 85, 441, 841 ATmega 48, 88, 168, 328USB-UART bridgeBased on dwire-debug, but more complete.
dwire-gdb2019Linux (C)debugWIREAttiny 84, 85USB-UART bridgeOnly one hardware breakpoint, no flashing.
dwtk2019Cross-platform (Go)debugWIREAll debugWIRE targetsUSB-UART bridgeFull server for all debugWIRE targets.
pyAVRdbg2020Cross-platform (Python)UPDIA few modern partsAll EDBG-based debuggersNo flash programming, based on pymcuprog.
dw-link2021Cross-platform (Arduino firmware)debugWIREAll debugWIRE targetsArduino UNO R3 running GDB server firmwareProvides RSP interface over a serial line.
Bloom2021Linux only (C++, Posix)AllAllAll EDBG-based debuggersBest in terms of chip coverage and I/O register presentation. However, heavy flash wear.
pyavrdebug2022Cross-platform (Python)UPDISome modern partsAll EDBG-based debuggersOnly 2 hardware breakpoints, no flash programming, based on pymcuprog.
gdb-debug-wire-integrated-server2022Cross-platform (Arduino firmware)debugWIREATtiny85, ATmega328PImplemented on ATmegaXU2Provides RSP interface over serial line; in addition a RTT line; can be flashed on a ATmega16U2 on an UNO board.
updi-gdbserver2023Cross-platform (Scheme)UPDISome modern partsUSB-UART bridgeOnly two hardware breakpoints, based on reverse engineering of UPDI protocol.
avr-absurd2024Cross-platform (Python)UPDISome modern partsUSB-UART bridgeOnly 2 hardware breakpoints, no flash programming, based on reverse engineering of UPDI protocol.
PK5-UPDI-GDB-Server2024Cross-platform (Python)UPDIAll modern partsPICkit4 & 5Only two hardware breakpoints, based on reverse engineering of USB protocol to PICkits.

So, why do we need a fifteenth implementation of a GDB server? As should be obvious from this table, some GDB servers run only on one platform, address only one debug interface, or are compatible only with a few MCUs. And a few of them are only proof-of-concept implementations, which makes them interesting from a technical point of view, but not fit to be used in everyday work. Finally, documentation and ease of installation often leave space for improvement.

While some of the design objectives for PyAvrOCD mentioned above have also been achieved in other GDB servers listed in the table, having achieved them all in one system is unique to PyAvrOCD.

On top of that, PyAvrOCD has been integrated into Arduino IDE 2, which was one of the main goals when developing PyAvrOCD. The only other potential candidate for such an integration would have been AVaRICE. However, since I am much more fluent in Python than in C++, I decided to build the GDB server from the ground (that is pymcuprog) up.

Why did it take so long?

It took almost half a year to get from dw-gdbserver, a Python GDB server for debugWIRE targets, to PyAvrOCD. I had hoped to be faster. So, what took so long?

When one compares the directory structure of dw-gdbserver with PyAvrOCD, it’s obvious that some serious restructuring took place. Going through the CHANGELOG notes, one notices a lot of refactoring. Additionally, a large number of features have been added along the way, e.g., the possibility to use monitor commands as command line options. And the adaptation of dw-link to be compatible with PyAvrOCD also took some time.

Big chunks of time were surely taken by writing and executing all the tests. However, I believe that it was very helpful since it minimizes the number of remaining bugs in the GDB server. Connected with that, I had to spend some time chasing bugs or shortcomings in other software, e.g., Arduino’s avr-gcc toolchain, in the FASTLED library, in simavr, and in GDB. And pymcuprog does not appear to be free of errors. Finally, writing the documentation took considerable time as well.

Summary

All in all, I am satisfied with the end result and hope you enjoy this piece of software. I think that the time spent developing it was well spent. With Edith Piaf, I can say: Non, je ne regrette rien. The next step will be to integrate the UPDI interface.

Views: 86