For the latest information on how COVID-19 is impacting our business, please see our updates page.

How to Use Visual Studio Code to Debug Your NetBurner Applications

AdobeStock_253257169
Share on facebook
Share on reddit
Share on twitter
Share on linkedin
Share on email
Share on print

In our previous article, we reviewed how to configure Visual Studio Code to compile, load, and be more productive in our NetBurner projects. These features are helpful functionalities when coding, but what if we have a bug? What tools can we use to make our code bug-free?

In this article, we’ll talk about how to configure Visual Studio Code to use its debugging features with GDB. In the end, we’ll give some valuable tips when debugging.

IoT Dev Kit Featured in this Article

Netburner ARM Cortex M7 embedded Development Kit for IoT product development and industrial automation.

Or, learn more about NetBurner IoT.

Visual Studio Code

Assumptions and Requirements

This article assumes the following:

Introduction

The NetBurner team works hard so that developers have easy-to-use development tools and a plethora of examples that allow us to focus on programming our applications instead of spending time configuring embedded systems. In addition, they also provide us with tools to uncover clues about bugs, such as TaskScan, SmartTrap, and Addr2line. However, to exterminate those bugs, we must find them; that is why it’s essential to know the benefits that a debugger can offer us.

So, grab your Proton Pack, because after this article you’ll be a “Bug Buster”.

A Brief Introduction to GDB

The GNU Project Debugger (GDB) is a tool created in 1986 by one of the most influential advocates for free code, Richard Stallman. It’s developed for Unix-like environments and allows us to analyze what happens inside programs during their execution.

GDB is one of the most widely used debuggers for programs developed in languages such as Assembler, Fortran, Pascal, Rust, C, and C++, among others. It allows us to do things like pausing the program’s execution under certain circumstances, displaying the state of local and global variables, following the path of functions in the stack, simulating execution conditions, etc.

Like most software created for UNIX environments, it runs out of the command terminal. In addition, it has a remote operating mode that allows us to run our program on a different system than the one running GDB. This feature is essential for us, as it is ideal for debugging embedded systems.

In this article, we won’t go into too much detail about the commands and how GDB works. However, if you are interested, we recommend that you do a little research on the Official GDB Website. There is also a lot of free information on YouTube. For now, you can learn the basics with the GDB Song (to the tune of Do-Ri-Me, from the Sound of Music).

GDB with NetBurner Command-Line tools

The ARM team provides a tool that allows us to use GDB in the 32-bit architecture of our Cortex-M microcontrollers, the GNU Arm Embedded Toolchain. While this tool doesn’t have all the native functionalities of GDB, we can still take advantage of it with the help of our IDEs.

Fortunately, the NNDK 3.x.x software installation already includes the binaries needed to run the ARM debugging tools. These can be found at \nburn\gcc\bin.

Before running GDB, it’s necessary to load the compilation with the debug symbols into the NetBurner module. For this, you will need to build the debug version of the application with the command, make -j loaddebug. This will also load the application on your module, using the environment variable DEVIP to figure out what module it should be loaded on.

To run the debugger from the command line, run the following commands:

arm-eabi-gdb -se ./obj/debug/<debug_file>.elf

target remote <module_ipaddress>:2159

The first command starts the GDB session using the .elf file to load the debug symbols. The second points the GDB session at the NetBurner module and tries to initiate the remote debugging session. Figure 1 shows a basic example of how to use GDB on the command line. The green lines indicate the commands that are used.

 

GDB session from the command line
Figure 1: Example of Using GDB

In the next section, we’ll talk about how to setup Visual Studio Code to use GDB commands almost automatically. We won’t directly use the command line for debugging, so don’t feel overwhelmed.

Debugging C/C++ With Visual Studio Code

Preparing Visual Studio Code for Debugging

Before we go any further, we need an example application to be able to test the debugger. I have created an example just for this purpose, which you can access from NetBurner’s GitHub account. It has the basic configuration of the makefile and the Visual Studio Code files to compile and load the project in the NetBurner module. Figure 2 shows the contents of the sample folder.

Visual Studio Code Project Folders
Figure 2: Example Project Content
  • .vscode: Contains the Visual Studio Code configuration files to be able to execute the tasks of compiling, loading, compiling with debug symbols, etc. (More information in the article How to Use Visual Studio Code with Your NetBurner Projects). It also contains a file that will serve as a debugger configuration template.
  • html: Basic HTML files
  • src: Contains the main.cpp file
  • makefile: File that contains the compilation rules when using the NetBurner command-line tools (More information in article Building Applications with NetBurner Command Line Tools).
  • ReadMe: File with a short description of the project’s functionality.

First, open the project in VS Code. There’re two ways to do it:

  1. Right-click at the project’s root and choose Open with Code, as shown in figure 3.
  2. In the upper menu, select the option File> Open Folder…, as shown in Figure 4. Choose the example folder in the popup that appears.

The contents of the example folder will appear on the left side, as shown in Figure 5.

VS Code Create Folder
Figure 5: VS Code with Opened Folder.

Before we can start debugging, we need to load the program into our NetBurner module. To do this, click on the tasks.json file is in the .vscode folder. Look for the Build Debug & Load Project label, and change the value of DEVIP with the IP of your NetBurner module, as shown in Figure 6. Save the changes with the <CTRL> + <S> shortcut.

Visual Studio Code DEVIP
Figure 6: Change DEVIP in Build Debug & Load Project

Click on Terminal> Run Task … and choose the Build Debug & Load Project option, shown in Figures 6 and 7. This will load the program into the NetBurner module.

Figure 6: Run Task
Figure 7: Build Debug Proejct Option

If the application builds without any errors and the module is connected correctly, we’ll see the confirmation with a status of 200 in the terminal. The result is shown in Figure 8. This indicates that the NetBurner module is ready to debug using GDB.

Figure 8: Status 200 After Loading

It should also be noted that at the end of the compilation, the folder called obj will be generated automatically. Within obj, a folder named debug will be generate that contains a file called DBgdbdemo.elf. This file is essential since it’s the one that we’ll use to load the symbols into the debugger. The file name will change depending on the value of the makefile’s NAME tag. The result is shown in Figure 9.

Figure 9: Debug ELF file in obj Folder

Debugger Configuration: lauch.json File

Figure 10: Open launch.json File

It’s time to configure the VS Code Debugger. To do this, click on the folder .vscode and open the file called launch.json, as shown in Figure 10.

Some modifications must be made to adapt it to your development environment; inside the file, look for the "program" element and change the value of the <debug_elf> template to the name of the ELF file generated during compilation. For us that is DBgdbdemo.elf.

Figure 11: Change debug_elf in the "program" Element

In the "setupCommands" element, you must make two modifications:

  1. In the "file" option, replace <full_path> to the path where the project is located and replace <debug_elf> with the name of the ELF file (same that the "program" element).
  2. In the "target" option, change the value of the <nb_ipaddress> template to the IP of your NetBurner module.
Figure 12: Elements to Edit in "setupCommands" Element

After the modifications, it’s necessary to save the changes in the file. Figure 13 shows an example of how it looks in my environment.

Figure 13: Example of launch.json File

Now that we have the configuration ready to continue, open main.cpp, located in the “src” folder. Figure 14 shows how to do it.

Figure 14: Open main.cpp File

To run the debugger, click the option on the left menu Run and Debug and then the Start Debugging option, as shown in Figure 15. It can also be run with it the <F5> shortcut.

Figure 15: Run Debug Session

If everything was configured correctly and the NetBurner module is running the program to be debugged, a new perspective will be displayed, as shown in Figure 16.

Figure 17: Debugger Perspective

However, if there was any configuration or connection error, an error window will be displayed, as shown in Figure 18. In that case, check that the launch.json file has the correct values.

Figure 18: Debugger Error

Once the VS Code files are set up, they can be used as a template for other projects.

With all of this configuration complete, it’s finally time for us to get into some actual debugging. The parts of the debugger and its functions are described below.

Visual Studio Code Debug Toolbar

The debug toolbar allows you to navigate through the code while it’s executing. It’s located at the top of the editor and has the following functions (from left to right):

Figure 19: Debug Toolbar
  • Allows you to drag the toolbar horizontally in the editor.
  • Continue/Pause execution (<F5> shortcut).
  • Step Over (<F10> shortcut).
  • Step Into (<F11> shortcut).
  • Step Out (<SHIFT>+<F11> shortcut).
  • Restart (<CTRL>+<SHIFT>+<F5> shortcut).
  • Stop (<SHIFT>+<F5> shortcut).

Breakpoints with Visual Studio Code

Breakpoints allow you to stop the execution of the program at specific points and under certain circumstances. In VS Code, there are the following types of breakpoints:

Simple Breakpoint

It stops the execution of the program when it reaches the desired line of code. To add a simple breakpoint, move the mouse to the left of the line number that you want to stop at and click. A red dot will appear when a new breakpoint is added. When the program reaches the line to which we set the breakpoint, VS Code will mark the stop line and activate all the functions of the Debug toolbar.

In the lower right window of VS Code, we have the option to edit, add, and delete breakpoints in our program, as shown in Figure 20.

Figure 20: Breakpoint Functionalities

Conditional Breakpoint

Unlike the simple breakpoint, the conditional breakpoint only stops program execution when a specified condition is met. It’s helpful for when we have loops or when the value of a variable is known before the program crashes.

To add it, the procedure is like the simple breakpoint, only that when the red dot appears, you must right-click and choose the Add Conditional Breakpoint option. Then specify the condition that must be met.

Logpoint

This is a peculiar type of breakpoint because, unlike the previous ones, it doesn’t stop the program’s execution; instead, it only prints in the Debug Console window every time the program reaches the chosen line. It’s helpful because, in printing, you can add program variables using curly braces (“{}”).

To add it, right-click on the red point of the breakpoint and choose the Add Logpoint option.

Variables

In the variables section, you can see the values of local variables and microcontroller registers at the time a breakpoint is hit, as shown in Figure 21.

Figure 21: Variable Section

You can also position the cursor on a variable and be able to see its current value.

Variable values can be modified by double-clicking on them or right-clicking and selecting the Set Value option.

Watch

In the Watch section, you can add variables to display so that you can monitor their value when stopping with a breakpoint. One of the essential utilities of this section is that you can declare variables in different numerical bases (hexadecimal, binary, etc.). To add a variable, just click on the Add Expression option; a text box will appear where you write the variable’s name to add, as shown in Figure 22.

Figure 22: Add Watch Variable

To choose the numerical base, the variable is added as follows:

<variable_name>,<base_code>

Some codes are listed in the following table, and Figure 23 shows an example.

Code Base
b Binary
x, h Hexadecmial
d decimal
c Character
o Octal
s Character STring
Figure 23: Variables With Some Numerical Codes

Call Stack

In the Call Stack section, we’ll see the stack of RTOS tasks. It’s useful when we want to know the sequence of steps to execute some process or function.  Figure 24 shows the example of the stack that is made when calling addAndSquare().

Figure 24: Call Stack of addAndSquare Function

Debug Console

As seen in the Breakpoints section, the debugger console shows the result of GDB execution. In this terminal, you can execute GDB commands in case the VS Code tools are not enough for you. To perform a command, it’s necessary to put the instruction -exec before the GDB command. Figure 25 shows the example of running the backtrace command.

Figure 25: Debug Console Command Example

Additional Information

Personal Tips

  • Try to remove all warnings from your program. These could be a nest of bugs in the future.
  • Many developers use the famous “printf debug” (I include myself). In my personal experience, this is helpful when the program is small. However, when there are many tasks running and a lot of interactions between them, the use of “print debug” is not the best idea. There have been times when my program crashes, and the cause is the “printf” itself. I recommend that you use it as little as possible and replace it with the help of the debugger’s Logpoints.
  • Keep in mind that compiling the code with debug symbols increases the memory usage of the microcontroller. If you have a huge system or little memory, it may be that using the debugger is not an option. One way around this is to divide your program into modules that can be tested independently. This allows you to more easily find errors through unit testing, and can also help avoid issues with resource limitations.
  • Try to use the “Delta Debugging” technique. This uses the divide and conquer strategy to try and reduce the input causing the problem. The less code you need to replicate the problem, the easier it will be to find the error; it’s like catching an insect by covering it with a glass.
  • Try to use as little dynamic memory as possible.
  • I recommend adding some kind of internal logging system in your application in case the bug appears when the system is already in production (The following article may be helpful: Real-Time Data Logging for Embedded Systems and IoT – Tutorial)

Useful Links and References

Troubleshooting

  • NNDK version 3.3.2 and lower has a problem with the display of local variables and call stack on the MODM7AE70. To solve this, open /arch/cortex-m7/make/archinc.mk and remove -fomit-frame-pointer from both CPPFLAGS and DBCPPFLAGS.
  • The C / C ++ Visual Studio Code extension version 1.6.0 has a bug with LogPoints. This bug makes the LogPoints work like a simple Breakpoint. We have reported it (https://github.com/microsoft/vscode-cpptools/issues/8065 ) so that the creators could fix it in a future version. In case you require LogPoints functionality, we recommended installing version 1.5.1 of the extension.

Wrapping Up

Hopefully, this information will help all those developers who seek to make their code bug-free. If you have any other tips or tricks on debugging, we would be delighted to hear from you! Leave us your comments below.

If you have any other questions, concerns, or suggestions, please contact us directly by email. Also, remember that we have a team of “bug busters” that can help you find those complex problems. NetBurner Support.

About the Author

Jordan Garcia. IoT and industrial protocols engineer at Racom Microelectronics.

Dog lover, bookworm, and sweet tooth. Fan of embedded systems, back-end and front-end development, and a constant joy in learning new things. Unique ability: to finding bugs in systems

Share this post

Share on facebook
Share on twitter
Share on linkedin
Share on reddit
Share on email

Subscribe to our Newsletter

Get monthly updates from our Learn Blog with the latest in IoT and Embedded technology news, trends, tutorial and best practices. Or just opt in for product change notifications.

Leave a Reply

Your email address will not be published. Required fields are marked *

Click to access the login or register cheese