One of the challenges when working with hardware verification frameworks like CocoTB and PyUVM is setting up a proper debugging environment. In this post, I’ll share my recent experience setting up remote debugging for CocoTB tests using VSCode, which has significantly improved my development workflow.
The Problem
As I’ve been experimenting with PyUVM and CocoTB for hardware verification, I’ve frequently needed to dive deep into the library’s internals to understand how things work. When you’re learning these frameworks, you often need to step through the library code itself, not just your own test code. Print statements can only get you so far. I needed a better way to:
- Inspect variable values during test execution
- Set breakpoints at critical points in my test code
- Step through the execution of PyUVM internals to understand the library better
- Debug issues by examining what’s happening “under the hood”
I deliberately set up my environment with tests running on a Raspberry Pi (the closest linux machine I had lying around) while editing code on my Mac to simulate a common industry setup. In many professional hardware verification environments, engineers connect to remote Linux machines (often via VNC or SSH) rather than running simulations locally. I wanted to replicate this setup to ensure my debugging solution would work in a professional environment.
The Solution: Remote Debugging with debugpy
Python’s debugpy
module provides a clean way to add debugging capabilities to your code, allowing you to connect VSCode as a remote debugger client. Here’s how I set it up:
Step 1: Add debugpy to your CocoTB test
First, I added these crucial lines at the beginning of my test file:
# At the top of test_*.py
import debugpy
debugpy.listen(("0.0.0.0", 5678))
debugpy.wait_for_client()
The code above:
- Imports the debugpy module
- Sets up a listener on all network interfaces (0.0.0.0) and port 5678
- Waits for a debugger client to connect before proceeding
Note: For local debugging on your own machine, you can use “localhost” instead of “0.0.0.0” as the host.
Step 2: Create a Test Runner
I’m trying to move from Makefiles to test_runner scripts in order to check support for regression-like environment, so I created a separate test runner script to launch my simulation:
import os
from pathlib import Path
from cocotb.runner import get_runner
def test_counter_runner():
# Use Icarus Verilog as the simulator
sim = os.getenv("SIM", "icarus")
# Get the project path
proj_path = Path(__file__).resolve().parent
# Create the simulation build directory
sim_build_dir = proj_path / "sim_build"
sim_build_dir.mkdir(exist_ok=True)
# Source files
sources = [
proj_path / "hdl" / "counter.v",
]
hdl_toplevel = "counter"
test_module = "test_counter"
# Get the runner
runner = get_runner(sim)
# Optional: Set waveform dumping format
os.environ["COCOTB_DUMP_WAVE_FORMAT"] = "fst"
os.environ["IVERILOG_DUMPER"] = "fst"
# Build the simulation
runner.build(
sources=sources,
hdl_toplevel=hdl_toplevel,
build_dir=sim_build_dir,
waves=True,
)
# Run the test
runner.test(
hdl_toplevel=hdl_toplevel,
test_module=test_module,
plusargs=["-fst"],
build_dir=sim_build_dir,
)
if __name__ == "__main__":
test_counter_runner()
Step 3: Configure VSCode for Remote Debugging
In VSCode, I configured a launch configuration in .vscode/launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/path/to/project/on/raspberry/pi"
}
]
}
]
}
Important: Update the
remoteRoot
to match the path on your remote machine (Raspberry Pi in my case). If debugging locally, you can use the same path for both localRoot and remoteRoot.

Step 4: Workflow
My debugging workflow now looks like this:
- Set breakpoints in my test code using VSCode
- Run the test runner script on the Raspberry Pi:
python test_runner.py
- Wait for the message indicating that debugpy is waiting for a client
- In VSCode, press the green play button in the Run & Debug panel
- The code will execute until it hits a breakpoint, where I can inspect variables, step through code, etc.
Yes, I like automation and this click, wait, click is going to drive me nuts when debugging a bug. I haven’t found a good automated solution for this yet because VSCode has some limitations on cascading tasks as part of Run & Debug flow. I’ll see to it again. This is merely a work-in-progress.
Results
This setup has dramatically improved my PyUVM development experience:
- I can now inspect complex PyUVM objects at runtime
- Breakpoints allow me to pause execution at critical points
- I can step through the test sequence and observe the DUT’s responses
- The remote debugging capability means I can run tests on more powerful hardware while still having a great debugging experience
Next Steps
This is just the beginning of my journey with CocoTB and PyUVM debugging. Next, I plan to:
- Explore conditional breakpoints for more complex scenarios
- Set up debugging for multiple test files
- Integrate debugging with continuous integration workflows
Stay tuned for more updates as I continue to refine this setup!
If you’re working with hardware verification in Python, I highly recommend giving this setup a try.