Introduction
This is part two of a three part series of articles on creating and debugging programs in Visual Studio Code. For part one which deals with setting up the VS Code to build Arduino programs, see here. For part three which deals with improving the debugger by using custom bootloder see here.
One thing I should make clear first – this article is about debugging Arduino Uno, Nano, Mega (and possibly other boards based on the AVR microcontrollers). There is a ‘Debugging Arduino Code’ chapter in the documentation for the VS Code Arduino extensions which can be a bit misleading. The debugging they are talking about is only available to a few boards (e.g., Arduino M0 Pro) which include hardware debugger interface on the board. There is no support for debugging the popular AVR-based boards like Uno, Nano or Mega in the Arduino extensions. These boards don’t have support for debugging and in general, it is not possible to debug the code in these boards without buying an external debug probe. In practice, there is a way, which I want to describe in this article. For more information on debugging options for Arduino, please see my older article here on Code Project.
Background
Some time ago, I wrote an article about software debugger for Arduino Uno which works with the GNU debugger – GDB. The article is about using the debugger in the Eclipse IDE. It works fine, but it requires quite a bit of work setting up the environment. Recently, I found out that Visual Studio Code IDE could be used instead of eclipse to make things easier.
So in this article, I present a simple tutorial to make the debugging available in VS Code. We will use an Arduino library called avr-debugger
which I created to make it easier to use the debugger.
Step 1 – Install the avr-debugger Library
The library is attached to this article. Just extract the avr-debugger folder into your Documents/Arduino/libraries folder. You should now have Documents/Arduino/libraries/avr-debugger folder.
Alternatively, you can download (or clone) the repository from github – https://github.com/jdolinay/avr_debug. In this repository, find the folder from avr_debug/arduino/library and copy it into the Documents/Arduino/libraries.
Restart VS Code if it is now running so that the new library is loaded.
Step 2 – Add the Library to Your Program
In VS Code, open Command Palette (F1) and type Arduino, then select Arduino: Library manager.
Type avr-debugger
into the Filter your search… box.
The avr-debugger
library should appear in the list.
Click the Include Library button.
This will add 2 include lines into your program.
#include <app_api.h>
#include <avr8-stub.h>
Add call to function debug_init
to your setup function. See the picture below step 3.
Verify (build) the program and note the size. In my case, it is 4852 bytes.
Step 3: Turn Off Optimizations
The program is now built with optimizations, which means the compiler which translates your code into the native language of the microcontroller can make changes in the organization of the code to make it smaller. Of course, the compiler makes sure the program works the way you wrote it; it just makes it more efficient. So optimizations are good in general; they let you put more code into small memory of the microcontroller. But they are not so good for debugging because we want to be able to step through the code the way we wrote it and see it in the editor. So it’s a good idea to turn off the optimizations when you debug your program.
To Turn Off the Optimizations
- In your favorite file manager, go to the avr-debugger folder and locate file platform.local.txt.
- Copy this file to the folder where your Arduino IDE is installed into sub-folder hardware/arduino/avr.
For example, on my system, the platform.local.txt file should be placed in c:\Program Files.(x86)\Arduino\hardware\arduino\avr\. You may need admin privileges to copy the file into Program Files.
- Restart VS Code.
- In File manager, also delete the contents of the build subfolder in your program folder – Documents/Arduino/vscode/test/build.
- Verify the program again and note the size. It should be much larger than before. In my case, 7896 bytes now with optimizations off. If the program is not about this size, there is something wrong. Try deleting the build folder again and restarting VS Code again and double-check that the location of the platform.local.txt file is correct.
Two notes about this:
- This step is optional - if you don’t use the platform.local.txt file, you will still be able to debug your programs; it just sometimes may do strange things like jumping to another line than expected.
- The platform.local.txt file is the only way I found to define compiler options for Arduino. It affects all your Arduino programs including those built in Arduino IDE. When you are done debugging, you can delete or rename this file to get back to normal, optimized programs. I wish it was possible to set the build options per project but it is not; the platform.local.txt file must be in the above mentioned folder to work.
Step 4: Create launch.json File – Launch Configuration
To be able to debug the program in VS Code, you need to create launch configuration which tells VS Code how to start your program.
Click the debug button in the Action bar on the left-hand size – it’s the icon with the bug.
At the top left, you will see a green “play” button and a gear wheel next to it with Configure or fix launch.json tooltip shown when you hover your mouse there.
Click this gear wheel button.
A list with options will appear at the top.
From this list, select the C++ (GDB/LLDB).
Important: Don’t select Arduino; this will not work for us. It works only for the boards with integrated debug interface.
A launch.json file will appear in the .vscode subfolder of your folder and it will also be opened in the editor.
Change the following options in the launch.json file (see the image below for final result):
program
- set to "${workspaceFolder}/build/app.ino.elf".
miDebuggerPath
– set the path to avr-gdb.exe in your Arduino IDE installation, for example: "c:\\Program Files (x86)\\Arduino\\hardware\\tools\\avr\\bin\\avr-gdb.exe".
Add this line under the miDebuggerPath
line – but change the COM port number to your port – see the bottom right of the status bar for the current COM port you are using to communicate with your Arduino:
"miDebuggerServerAddress": "\\\\.\\COM3",
In the setupCommands
section, replace the default block with “Enable pretty printing” with this block:
"description": "Remote debug enable",
"text": "-gdb-set debug remote 1",
"ignoreFailures": false
Here is the resulting launch.json:
Now we are ready to start debugging.
Start Debugging
Upload your program to your Arduino. You may want to switch to the app.ino file to be able to use the Upload button in the top-right of the window.
Click to the left margin on the line of digitalWrite(13, HIGH);
in the loop()
function. This will insert breakpoint at this line and the program should stop there. You will see a red dot.
Click the Start Debugging (green Play) button at the top left.
If everything goes well, after few seconds, you should see buttons to control the program at the top, the status bar should turn orange and the line with the breakpoint should be highlighted. See the picture.
Click the Step Over button in the toolbar above, or press F10 to execute one line of the program. The LED on the Arduino board should now be on.
Step through the program by repeatedly clicking the Step Over button and watch the LED blink.
After stepping over the last delay()
, you will find yourself in the main.cpp file. To get back to your loop()
, click Step Into (F11) on the loop()
line.
You can also let the program run by clicking the Continue button or pressing F5. It will stop again on the breakpoint.
Play with the program, remove and insert breakpoints and step through the code.
When you are ready, press the Stop red square button to stop debugging the program.
Working with Variables
Let’s add some variables to the program and see what we can do with them in the debugger.
Change the code as follows:
int globalVar;
void setup()
{
debug_init();
pinMode(13, OUTPUT);
}
void loop()
{
int localVar = 5;
globalVar++;
localVar++;
digitalWrite(13, HIGH);
delay(100);
digitalWrite(13, LOW);
delay(1000);
localVar++;
}
We’ve created two variables – one is global, visible in the whole program and one is local, defined inside loop. The local variable is visible only in the loop function and it exists only as long as the loop function is executed. When loop finishes, the variables is removed and then re-created when loop is executed again.
Verify and upload the program again.
Important note: You need to upload the program every time you make some changes in the code. It is not uploaded automatically when you click the Start debugging button.
Click the Start debugging button.
After the program stops at the breakpoint in loop (I hope you still have it there), look at the Variables view on the left-hand side. There is the local variable localVar
with value 6
.
The global variable globalVar
is not automatically shown. You need to add it to the Watch view. Just move your mouse to the Watch window and click the plus (+) button which appears. Then type globalVar
into the ‘Expression to watch’ box which appears.
Here is how it all looks:
Step through the program and see how the values change. The value of globalVar
should still grow while the localVar
is reset to 5
every time we enter the loop function.
You can also set the value of the localVar
– just click the value, enter new number and press Enter key.
Unfortunately, it’s not possible to change the value of the globalVar
in the Watch view. But you can still change the value by executing gbd
command.
To change the value of the global variable:
Click Debug console below the code of your program. There should be a line with command prompt at the bottom.
Enter the following command there:
-exec set var globalVar = 10
You will not see the change in the Watch window until you do a step in the program to force refresh of the view, but the variable value is already set to 10
.
This is not very comfortable but you can use the up/down arrows on your keyboard to quickly select previous commands - no need to type it again and again.
Well, and that’s all. This is the end of the tutorial. Enjoy your debugging!
Points of Interest
Note that there are some interesting options you can set for the debugger. We've used the basic configuration, which I call 'RAM breakpoints'. The advantage of this configuration is that you don’t need to do anything with the Arduino board, just add library to your program. The disadvantage is that the program runs at much slower speed when debugged.
To overcome this disadvantage, you can switch to ‘Flash breakpoints’. But using flash breakpoints requires special bootloader in your Arduino. I currently have this bootloader for Arduino Uno (ATmega328). You can find it in the bootloader subfolder of the library and burn it into your Arduino. Then open the avr8-stub.h file and change AVR8_BREAKPOINT_MODE
to 0
from 1
. For detailed instructions please see Part 3 of this article.
You can also look at the documentation for the debugger, which can be found in the repository at github – https://github.com/jdolinay/avr_debug. Look into the doc sub-folder.
Limitations of this Debugger Solution
There are 4 important limitations of using this debugger:
- Serial communication (hardware serial) cannot be used together with the debugger – no Serial.something functions in your program.
If you are used to using Serial.print
for debugging, you don’t need to do this when you have a debugger – just put breakpoint at the place where you want to check what is happening in your program. If your program needs to communicate over the serial line for normal work, this may not be possible with the debugger. You can try to use software serial for the communication (not for the debugger, it requires the hardware serial port), but there may be a problem with timing – see point 3 below. Or use conditional compilation to debug the program without using serial line and enable the serial line once you no longer need the debugger.
- One pin must be reserved for the debugger – this is pin 2 on Uno by default, can be changed to pin 3 in the avr8-stub.h file. You cannot use this pin in your program.
- The program runs at slow speed when any breakpoint is enabled (and there is always at least one enabled in VS Code, see Common problems below). Some time-sensitive operations may not work correctly when debugging the program.
- It is not possible to stop the program in interrupt service routines, or in general, it is not possible to step through the program when interrupts are disabled – as it happens in the Arduino
micros()
function. If you find yourself stepping over cli();
command in the code, expect that something unexpected may happen.
These limitations are explained in detail in the documentation of the complete debugger project (of which the Arduino library is a part) here on github: https://github.com/jdolinay/avr_debug. Look into the doc sub-folder.
Common Problems
The VS Code debugger always places breakpoint into main()
function and so far, I didn’t find any way to stop it from doing this. The program never stops at this breakpoint because we actually connect to a running program which is already running in the main function but while this breakpoint is present, the program runs at a lower speed than normal - see point 3 above in the Limitations sections. You can remove the breakpoint by executing the following command in the Debug Console after the program stops in the debugger:
-exec clear main
The debugging solution described here was tested on Windows 10 with Arduino Uno, Nano and Mega and with Nano on Windows 7. It should also work on Linux and Mac. On Win 7 and older, you may need to use a “proxy” program instead of direct serial connection. Please see this article on Code Project or the documentation for the avr_debug
project on github for more information.
If there is some problem connecting to the debugger, you can enable logging the communication by adding the following block into the setupCommands
section in launch.json.
{
"description": "Log file enable",
"text": "-gdb-set remotelogfile gdb_logfile.txt",
"ignoreFailures": false
}
You will then find gdb_logfile.txt file in your program folder and you can examine this file to see what went wrong.
If there is some strange behavior when you step through the program, like jumping over several lines, this is most likely because the code generated by the compiler is different from the code you wrote. See the talk about optimizations in the Step 3 section above. This sometimes happens even if the optimizations are off. To examine the problem, it is useful to see the code generated by the compiler.
To do this, run the following command from the command line in the build sub-folder of your project:
"c:\Program Files (x86)\Arduino\hardware\tools\avr\bin\avr-objdump.exe" -S app.ino.elf > list.txt
Substitute your path to Arduino IDE if it is different.
This command will produce list.txt file showing your code together with the resulting assembly code generated by the compiler. You can search this file for the <main>
or <loop>
symbols to see how your program translates to the actual operations of the processor.
The next step
In the Part 3 of this article you can see how to replace the bootloader in your Arduino to be able to debug programs without slowing them down.
History
- 2019-09-11: Updated zip file with fixed bootloader.
- 2019-07-19: Added links to Part 3 article.
- 2019-06-26: First version