Arduino and Visual Studio Code: larger projects with PlatformIO

How to program an Arduino in Visual Studio Code with PlatformIO: installing, the project and platformio.ini, main.cpp, libraries and several files.

Arduino Info
A laptop with a program in an editor with a dark side bar, connected by USB cables to two blue microcontroller boards, with a few wires beside them

What are Visual Studio Code and PlatformIO?

Visual Studio Code, VS Code for short, is a free program editor from Microsoft. PlatformIO is an extension for VS Code that turns it into a development environment for microcontrollers: it compiles the program, uploads it to the board, has a Serial Monitor and looks after boards and libraries.

This page is for the Arduino workshop and follows on from the Arduino IDE page, so it assumes you can already upload a program to the board and use the Serial Monitor. We use VS Code with PlatformIO when a program grows and several people work on it, as in the pinball machine workshops.

Arduino IDE or VS Code?

Arduino IDEVS Code with PlatformIO
To start withthe simplestneeds more setting up
The programone .ino file, others as tabsmany .cpp and .h files in folders
Help while writingbasiccompletes words, underlines errors as you type, jumps to definitions
Librariesinstalled for the whole computerwritten into the project, with their version, so everyone in the team has the same ones
BoardsArduino and many othersArduino, ESP32 and hundreds of others
Serial Plotteryesnot built in
Computerworks on slower ones tooneeds more memory and space

The Arduino IDE and VS Code aren’t rivals: for trying something out quickly the Arduino IDE is still handier, and for a larger project VS Code.

Installing

  1. Download VS Code from the official site, code.visualstudio.com. For Windows there is also a version that doesn’t need administrator rights (User Installer).
  2. In VS Code open Extensions (the icon with four squares on the left bar, Ctrl+Shift+X), search for PlatformIO IDE and click Install.
  3. Wait: PlatformIO installs its core (PlatformIO Core) by itself. This takes a few minutes and needs the internet. When it has finished, it asks you to restart VS Code.
  4. The first time you compile a project for the Uno, PlatformIO also downloads the compiler and the board support. This also takes a few minutes.

The driver for the board’s USB chip is the same as for the Arduino IDE, so see Connecting the board if the port doesn’t appear.

Watch out for special letters in the path. PlatformIO doesn’t support paths with letters such as č, ć, đ, š or ž, or other letters outside the English alphabet. So save projects in a folder without them, for example C:\projects\pinball, not in C:\Users\Ivančić\Documents.

For the leader: prepare the computers before the workshop

  • Install VS Code and PlatformIO on every computer and compile a test project for the Uno once, so that everything it needs is downloaded. On a school network this can take a long time or fail.
  • VS Code and PlatformIO send usage data (telemetry). In VS Code you switch it off in the settings (Ctrl+,): search for telemetry and set telemetry.telemetryLevel to off. In PlatformIO type pio settings set enable_telemetry No in the terminal.
  • VS Code offers to sign in with a Microsoft or GitHub account, for example for the AI assistant Copilot. On the workshop computers don't sign in, and don't ask the participants to.
  • Make a folder for the projects with no special letters in its path.

A new project

  1. Click the PlatformIO icon on the left bar (a head with two antennae), then PIO Home → Open. The house on the status bar at the bottom opens it too.
    PIO Home in VS Code: on the left the Home, Projects, Inspect, Libraries, Boards, Platforms and Devices menu, in the middle the PlatformIO logo, and on the right under Quick Access the New Project, Import Arduino Project, Open Project and Project Examples buttons
    On the start page, under Quick Access, are the buttons for a new project, for importing a program from the Arduino IDE and for opening a project. On the left are Projects (your projects), Libraries, Boards and Devices, a list of the connected boards with their ports.
  2. Click New Project. Type a name, without spaces or special letters, search for Arduino Uno in the Board field, leave Arduino as the Framework, choose a folder and click Finish.
  3. PlatformIO makes the project folder:
The folder of a new PlatformIO project in VS Code: the folders .pio, include, lib, src with the file main.cpp, and test, and the file platformio.ini, marked 1 to 6
  1. platformio.ini: the project’s settings, for example the board, the Serial Monitor speed and the libraries.
  2. src/main.cpp: the program, with the setup() and loop() functions.
  3. include: .h files used by several parts of the program.
  4. lib: the project’s own libraries.
  5. test: for automatic tests. We don’t use it in the workshop.
  6. .pio: what PlatformIO makes when it compiles. Don’t change it and don’t send it to others. If you delete it, PlatformIO makes it again.

The platformio.ini file

A new project for the Uno has settings like these. Add the monitor_speed line yourself:

[env:uno]
platform = atmelavr
board = uno
framework = arduino
monitor_speed = 9600
  • [env:uno] is an environment called uno: a set of settings for one board.
  • platform = atmelavr is the family of microcontrollers, AVR, that the Uno’s ATmega328P belongs to.
  • board = uno is the board.
  • framework = arduino means we program with Arduino commands, as in the Arduino IDE.
  • monitor_speed = 9600 is the speed of the Serial Monitor. It must be the same as in Serial.begin().

Later we add libraries (lib_deps) and the port (upload_port) here. A line that starts with ; is a comment.

main.cpp instead of .ino

In PlatformIO the program is an ordinary C++ file. Before compiling, the Arduino IDE adds two things to an .ino file by itself, and here we write them ourselves:

  1. #include <Arduino.h> at the top. Without it the compiler doesn’t know pinMode(), digitalWrite() and the other Arduino commands.
  2. A declaration of a function you use before it is written: the first line of the function, with a ; at the end.
#include <Arduino.h>

void flash(unsigned int duration);  // declaration: the function is written further down

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  flash(500);
}

void flash(unsigned int duration) {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(duration);
  digitalWrite(LED_BUILTIN, LOW);
  delay(duration);
}

Without the declaration the compiler reports error: 'flash' was not declared in this scope, because it only sees the function below loop(). Next to the error it may also suggest a suggested alternative that has nothing to do with our program: the compiler’s suggestions aren’t always right. Instead of a declaration, you can also write the function above the place where you use it.

Compiling, uploading and the Serial Monitor

A simplified VS Code window with PlatformIO. On the left is the bar with the Explorer (1), Extensions (2) and PlatformIO (3) icons, next to it the project folder, in the middle the editor with the program (4), below it the terminal (5), and on the status bar at the bottom the Build (6), Upload (7) and Serial Monitor (8) buttons
  1. Explorer (Ctrl+Shift+E): the project’s folders and files.
  2. Extensions: extensions for VS Code. This is where you install PlatformIO.
  3. PlatformIO: PIO Home and Project Tasks, a list of all the project’s commands (Build, Upload, Monitor, Clean…).
  4. Editor: this is where you write the program.
  5. Terminal: messages from compiling and uploading, and the Serial Monitor.
  6. Build (the tick, Ctrl+Alt+B): compiles the program.
  7. Upload (the arrow, Ctrl+Alt+U): compiles the program and uploads it to the board.
  8. Serial Monitor (the plug, Ctrl+Alt+S): opens the Serial Monitor in the terminal.

On the status bar there are also the house (PIO Home), the bin (Clean, which deletes what has been compiled, so the next build starts from scratch) and PlatformIO’s terminal. In newer versions the Build and Upload buttons are also at the top right, above the editor. If the buttons look different in a newer version, you can also find every command in the command palette: Ctrl+Shift+P, then type, for example, PlatformIO: Upload.

  • PlatformIO usually finds the port by itself. When several boards are connected, like the four Unos in the pinball machine, write the port into platformio.ini: upload_port = COM5 and monitor_port = COM5.
  • The Serial Monitor runs in the terminal. You close it with Ctrl+C. If uploading fails because the port is busy, close the monitor and try again.

The Serial Monitor in the VS Code terminal: it says Terminal on COM1 | 9600 8-N-1, a list of filters and on the last line Quit: Ctrl+C, Menu: Ctrl+T, Help: Ctrl+T followed by Ctrl+H
When the monitor opens, its first lines give the port and the speed, here COM1 and 9600, and the last line how to close it (Ctrl+C) and open its menu (Ctrl+T). The messages from the board appear below that.

Libraries

  1. Open PIO Home → Libraries and search for a library, for example RTClib for a clock module.
  2. Click it, then Add to Project, choose your project and click Add.

PlatformIO writes the library into platformio.ini:

lib_deps =
    adafruit/RTClib @ ^2.1.4

This means: the RTClib library by adafruit, version 2.1.4 or newer, but not 3.0 or later, because that one might work differently. PlatformIO downloads it into the .pio folder when you save platformio.ini, or at the latest the first time it compiles. Because the library is written into the project, everyone in the team gets the same version, and nothing has to be installed by hand on a new computer. You save the project’s own libraries in the lib folder, each in its own folder.

VS Code with the platformio.ini file of a project for an ESP32-S3 board open, in which lib_deps is the address of the LCDGraph library on GitHub. The output below says Library Manager: LCDGraph@1.2.0 has been installed and Project has been successfully updated
You can also add a library that isn’t in PlatformIO’s list by the address of its repository on GitHub, as in the picture, in a project for an ESP32-S3 board. When you save platformio.ini, PlatformIO downloads it and reports has been installed in the Output. A library added this way has no fixed version, so for a team project prefer a library from the list.

A program in several files

When a program grows, we split it into parts: in the pinball machine, for example, the lights, the coils and the score. Each part has two files:

  • the .h (header) says what the part offers: a list of functions.
  • the .cpp says how it does it: the functions themselves.

src/lights.h:

#pragma once
#include <Arduino.h>

// Lights on pins 5, 6 and 7
void lightsBegin();
void lightsFlash(byte pin, unsigned int duration);

src/lights.cpp:

#include "lights.h"

const byte LIGHTS[] = {5, 6, 7};

void lightsBegin() {
  for (byte pin : LIGHTS) {
    pinMode(pin, OUTPUT);
  }
}

void lightsFlash(byte pin, unsigned int duration) {
  digitalWrite(pin, HIGH);
  delay(duration);
  digitalWrite(pin, LOW);
}

src/main.cpp:

#include <Arduino.h>
#include "lights.h"

void setup() {
  lightsBegin();
}

void loop() {
  lightsFlash(5, 200);
  lightsFlash(6, 200);
  lightsFlash(7, 200);
}
  • #pragma once at the top of the .h file makes sure it is only included once, even when several files include it.
  • We include our own file with quotes, #include "lights.h", and a library with angle brackets, #include <Arduino.h>.
  • The list of pins LIGHTS is only in lights.cpp, so the other parts of the program don’t see it and can’t change it by accident.

When everyone in the team works on their own pair of files, they get in each other’s way less, and the program is easier to follow.

Several boards in one project

The pinball machine has four Unos, and each has its own program. They can all be in one project: we write the shared settings in the [env] section, and for each board we add its own environment, which compiles only its folder in src:

; settings shared by all the boards
[env]
platform = atmelavr
board = uno
framework = arduino
monitor_speed = 9600

; the Uno with the game rules: compiles only src/game
[env:game]
build_src_filter = +<game/>

; the Uno that drives the coils: compiles only src/coils
[env:coils]
build_src_filter = +<coils/>

In Project Tasks each environment has its own Build and Upload commands. Also add each environment’s upload_port, so the program goes to the right Uno.

Help from the editor

  • Word completion: as you type, VS Code offers the names of functions and variables. Tab or Enter accepts the suggestion.
  • Errors as you type: a red wavy line under an error, and a list of all errors in the Problems panel (Ctrl+Shift+M).
  • F12 (Go to Definition) jumps to where a function or variable is written, even in another file.
  • F2 (Rename Symbol) changes the name of a function or variable everywhere in the project at once.
  • Shift+Alt+F (Format Document) lines up the indents neatly.
  • Ctrl+Shift+F searches for a word in all the project’s files.

VS Code with a program that is missing a semicolon on line 17, after delay(trajanje): the end of the line has a red wavy underline, the Problems tab shows 2, and the terminal says src/main.cpp:17:18: error: expected ';' before '}' token and FAILED
In the picture a ; is missing on line 17. VS Code underlines it as you type, the Problems tab counts the errors, and Build reports the error and FAILED in the terminal. The project in the picture is for an ESP32 board, whose newer compiler reports line 17 itself. The compiler for the Uno reports the same error on the next line, as in the Arduino IDE.

Sometimes VS Code underlines something that compiles fine, for example right after you make a project. The real test is Build. If the lines don’t go away even after a successful build, run PlatformIO: Rebuild IntelliSense Index from the command palette.

When something doesn’t work

  • Compile errors are the same as in the Arduino IDE, because the compiler is the same too. See the troubleshooting tips.
  • was not declared in this scope after moving a program from the Arduino IDE: #include <Arduino.h> or a function declaration is missing.
  • The port is busy: close the Serial Monitor (Ctrl+C in the terminal) and upload again.
  • The program went to the wrong board: check the environment and upload_port.
  • Installing or compiling reports strange errors about paths: check whether the project’s path has special letters in it.

Moving a program from the Arduino IDE

  • In PIO Home → Projects click Import Arduino Project and choose the folder of the .ino program. PlatformIO makes a project from it.
  • Or by hand: make a new project, copy the program into src/main.cpp, add #include <Arduino.h> and the function declarations at the top, and add the libraries to lib_deps.

What did we learn?

  • PlatformIO is an extension for VS Code that compiles and uploads programs for microcontrollers and looks after boards and libraries.
  • The project’s settings, the board, the speed and the libraries with their versions, are written in platformio.ini, so everyone in the team works with the same ones.
  • In main.cpp we write #include <Arduino.h> and the function declarations ourselves, which the Arduino IDE does by itself.
  • We split a larger program into parts, each with a .h and a .cpp file.
  • We save projects in a folder with no special letters in its path.

Short quiz

1. What is PlatformIO?
An extension for Visual Studio Code that compiles programs for microcontrollers, uploads them to the board, has a Serial Monitor and looks after boards and libraries.
2. Why is it useful for a team that the libraries are written in platformio.ini?
Everyone in the team, on every computer, gets the same libraries in the same version, and PlatformIO downloads them by itself. It can’t happen that the program works for one person but not for another because of a different library version.
3. A program compiles in the Arduino IDE, but in PlatformIO it reports 'digitalWrite' was not declared in this scope. What is missing?
#include <Arduino.h> at the top of main.cpp. The Arduino IDE adds it to an .ino file by itself.
4. Why mustn't you save a project in the folder C:\Users\Šime\Documents?
The path has the letter š in it. PlatformIO doesn’t support paths with letters such as č, ć, đ, š and ž, so compiling or installing can report strange errors.
5. What do we write in a .h file, and what in a .cpp file?
In the .h a list of what the part of the program offers, the function declarations. In the .cpp the functions themselves, how it is done.
6. Four Unos are connected to the computer. How do you make sure the program goes to the right one?
I write each board’s upload_port into its environment in platformio.ini and upload from that board’s environment.