Arduino Leonardo, VirtualBox, and Windows

I realize this post may be useful to about three other people in the world, but sharing is caring.

What I wanted to do

On my Windows 11 machine I wanted to be able to write and upload firmware to an Arduino Pro Micro (Leonardo) using PlatformIO.

The standard way to accomplish this is to install PlatformIO in VSCode for Windows and you’re done. However, I’m a bit of long time *nix user and really like my PlatformIO + Git workflow in that environment.

WSL sounds like a great option for this case, but unfortunately from what I’ve read, serial communication doesn’t play too nicely without a bunch of hoops to jump through.

To keep things (somewhat) simple I installed a Linux VM through VirtualBox and added a USB Device Filter for the attached Arduino Leonardo so it would be forwarded and accessible by PlatformIO. Using VSCode as my editor would still be possible thanks to its awesome remote features.

The Problem

At this point I thought I’d be done, but after running a quick upload test I kept facing the follow error:

Looking for upload port…
Use manually specified: /dev/ttyACM0
Forcing reset using 1200bps open/close on port /dev/ttyACM0
Waiting for the new upload port…
Error: Couldn't find a board on the selected port. Check that you have the correct port selected. If it is correct, try pressing the board's reset button after initiating the upload.
*** [upload] Explicit exit, status 1
============================================== [FAILED] Took 7.34 seconds ==============================================

PlatformIO would successfully send the reset command to the board, but it would never come back in time to get the new firmware.

Reading up on how the Leonardo works tells us what’s really happening under the hood:

Uploading Code to the Leonardo, Leonardo ETH and Micro

In general, you upload code to the Leonardo or Micro as you would with the Uno or other Arduino boards. Click the upload button in the Arduino IDE and your sketch will be automatically uploaded onto the board and then started. This works more or less the same way as with the Uno: the Arduino software initiates a reset of the board, launching the bootloader – which is responsible for receiving, storing, and starting the new sketch.

However, because the serial port is virtual, it disappears when the board resets, the Arduino software uses a different strategy for timing the upload than with the Uno and other boards. In particular, after initiating the auto-reset of the Leonardo, Leonardo ETH or Micro (using the serial port selected in the Tools > Serial Port menu), the Arduino software waits for a new virtual (CDC) serial / COM port to appear – one that it assumes represents the bootloader. It then performs the upload on this newly-appeared port.

Source – Arduino docs

The Solution

After initiating a reset from PlatformIO and monitoring Device Manager in Windows, I spotted the new “bootloader” device that appeared on a different port. This device was not being forwarded to VirtualBox, so PlatformIO couldn’t see it and gave up looking for it!

Digging down into the device’s properties in Device Manager showed me the two most important pieces I needed to know – the Vendor ID and Product ID, which are 2341 and 8036 respectively.

Ultimately I ended up with two filters in VirtualBox:

After adding the bootloader filter everything works great!

A couple optional but potentially helpful steps I took were installing the Arduino IDE to get drivers, as well as the VirtualBox Extension Pack to get USB 3.0 support. Though we won’t need USB 3.0’s bandwidth, it seems like the controller is better overall.


Looking for upload port...
Use manually specified: /dev/ttyACM0
Forcing reset using 1200bps open/close on port /dev/ttyACM0
Waiting for the new upload port...
Uploading .pio/build/tx/firmware.hex

Connecting to programmer: .
Found programmer: Id = "CATERIN"; type = S
    Software Version = 1.0; No Hardware Version given.
Programmer supports auto addr increment.
Programmer supports buffered memory access with buffersize=128 bytes.

Programmer supports the following devices:
    Device code: 0x44

avrdude: AVR device initialized and ready to accept instructions

The same can apply for other boards

Though this example is for the Leonardo, the same can apply for other boards that reset into a special bootloader mode to get new firmware. For the Adafruit Feather M0 I had to do something similar:

  1. Filter for the board in the regular mode with Vendor ID of 239a and (in my case) Product ID of 800b.
  2. Filter for the bootloader mode with a Vendor ID of 239a and Product ID of 0015.

In both of these cases you can make things simpler by just filtering on the Vendor ID. This should work fine unless you have devices that you don’t want to be used in VirtualBox.