Manage Two Arduinos with Ease Using PlatformIO

Sometimes, we need to test out communication protocols like UART and I2C on two Arduino boards. Based on my experience, the testing experience on the Arduino IDE is far from optimal. First, you have to create a separate instance of the IDE in order to open two serial ports simultaneously. Second, ports are occasionally deselected by “magic” and you have to reselect them from the tools->port dropdown. Third, the IDE lacks useful features like autocomplete, goto definition, and flexible pane layout. Despite these shortcomings, I do not intend to blame the Arduino IDE because it’s designed to be beginner-friendly. However, when it comes to more complicated applications, we can bear some added complexity in exchange for flexible configurations. In this case, the Platformio IDE is the best tool. It leverages the familiar & well-crafted Visual Studio Code IDE while providing flexible configurations through a simple platformio.ini file.


  • Arduino Nano 33 BLE Sense
  • Arduino Nano 33 BLE Sense (or any other Arduino board that works at 3.3V)
  • 2 x mini breadboard or 1 x long breadboard
  • 3 x jumper wires
  • Familiarity with Visual Studio Code
  • Basic command-line skills


We are going to show how you can take advantage of Platformio by demonstrating a UART example between two Arduino Nano 33 BLE boards. In this example, we will power both the Arduino boards through the computer. Then, we will use the Serial Monitor to send some commands from the transmitter board through UART to the receiver board. Depending on the commands received, the receiver board will turn ON or OFF its built-in orange LED. You can browse through this official Arduino tutorial to learn more about the example. Here’s the schematic of what we are building:

Schematic of our UART example


Go to Visual Studio Code, and create a new Platformio project.

Create a new project

In the project wizard, select Arduino Nano 33 BLE as the board and Arduino as the framework.

Project Wizard

Under the src directory, create two files named receiver.cpp and transmitter.cpp.

Create source files

Here’s the code for receiver.cpp:

Here’s the code for transmitter.cpp:

The two source files above are adopted from the Arduino tutorial with an added bugfix on the transmitter side and useful debug messages on the receiver side.


After you select the board and framework in the Project Wizard, Platformio generates a platformio.ini file to match your selections. However, the configuration does not work out-of-the-box for projects with multiple main files, i.e. receiver.cpp and transmitter.cpp. If you build the project, you will receive an error like the one below:

Linking .pio\build\nano33ble\firmware.elf
.pio\build\nano33ble\src\transmitter.cpp.o: multiple definition of `setup';
.pio\build\nano33ble\src\receiver.cpp.o: first defined here
.pio\build\nano33ble\src\transmitter.cpp.o: multiple definition of `loop';
.pio\build\nano33ble\src\receiver.cpp.o: first defined here
collect2.exe: error: ld returned 1 exit status

The error is long and nasty, but it conveys a very simple message: Platformio doesn’t know which main file to use during the build because they both contain a setup function and a loop function. Essentially, Platformio is confused about which version of the functions to pick. This is not a bug though if we analyze the default platformio.ini generated by Platformio:

Each project may have multiple configuration environments defining the available project tasks for building, programming, debugging, unit testing, device monitoring, library dependencies, etc.¹ By default, Platformio generated the [env:nano33ble] configuration environment for us. To accommodate the two main files, we need to extract the common configurations into the common environment called [env] and make two more environments specific to each main file. Then during testing, we can build and upload those two main files separately.

Before we proceed to add those environments, we need to understand how Platformio includes/excludes source files from build. Unlike IDEs like Eclipse where you right-click on a filename to exclude it from build, Platformio offers a more powerful and generic src_filter property in the platformio.ini file. To include every file under the src/ directory, use +<*>, where * is a wildcard that matches all file names. To exclude a particular file from build, use -<filename.cpp>.² Note that all paths are relative to the src/ directory, not the project root. You can change the default by specifying the base path in the src_dir property.³

Now, we can create the two new environments named [env:transmitter] and [env:receiver].

In the common environment [env], we include all source files using the most generic src_filter. Then in [env:transmitter] environment, we exclude the receiver.cpp. Similarly, we exclude the transmitter.cpp in the [env:receiver] environment. Note that ${env.src_filter} just refers to the src_filter property of the [env] environment.

Before we start building and testing the example, we can save ourselves some hassle by configuring the USB port for each environment. The ports have different names on different operating systems but the steps to obtain them are the same. First, connect your two Arduinos to the computer through USB. Second, open a terminal (Command Prompt on Windows, Terminal on Mac & Linux) and type:

pio device list

This will list all available devices connected to the computer.⁴ On my Windows computer, I get this output:

> pio device list
Hardware ID: USB VID:PID=1234:5678 SER=F8EF8889D0211923 LOCATION=1-3.2:x.0
Description: USB 串行设备 (COM12)
Hardware ID: USB VID:PID=1234:5678 SER=DAB991012486A110 LOCATION=1-3.1:x.0
Description: USB 串行设备 (COM11)

The important bits are the underlined names: COM11 and COM12. On Mac & Linux, you will get different names. We choose COM11 as the transmitter port and COM12 as the receiver port. This decision is arbitrary as either board can be the transmitter/receiver.

It’s time to update our platformio.ini with port configurations:

The upload_port property specifies the port to upload the build and the monitor_port property specifies the port to monitor during execution.

Build & Upload

Open a new terminal in VS Code by going to Terminal->New Terminal.

If your default terminal does not support Platformio (like WSL terminal on Windows), create a new terminal by clicking on the dropdown arrow next to the little + icon.

In the terminal, build and upload the transmitter and receiver environments by typing:

pio run

You will see build logs similar to the one below:


Open port monitors from the left side:

First, starts monitoring the transmitter:

Second, starts monitoring the receiver:

Third, drag the transmitter monitor to the left of the receiver monitor:

Now, you get a nice split monitor with the transmitter on the left and the receiver on the right:


Focus your mouse on the the left transmitter monitor and type the number 1 on your keyboard. You should see the message LEDS ON immediately printed on both monitors. Take a look at your boards, you should see both built-in orange LEDs light up.

Next, type the number 0 in the left transmitter terminal. You should see the message LEDS OFF immediately printed on both monitors. Take a look at your boards, you should see both built-in orange LEDs light off.


You have successfully learned how to manage multiple Arduinos using the powerful Platformio IDE.

This article is inspired by the discussion between @jcw and @flaghacker on this Platformio community thread.

[1] Section [Env] — PlatformIO Latest Documentation. Accessed 12 Aug. 2021.

[2] Build Options: src-filter — PlatformIO Latest Documentation. Accessed 12 Aug. 2021.

[3] Section [Platformio]: src_dir — PlatformIO Latest Documentation. Accessed 12 Aug. 2021.

[4] Pio Device List — PlatformIO Latest Documentation. Accessed 12 Aug. 2021.

❤️ Open Source, Web Dev, programming languages, and Hanzi 漢字