Debugging, in the context of embedded development, is the process of identifying, analyzing and resolving issues related to software and hardware. Debugging is an iterative process where the developer analyzes the code or hardware and makes small adjustments before they test their application again. Common problems you might face during development are syntax errors, logical errors, memory leaks, timing issues, and hardware-related issues.
The nRF Connect SDK has multiple tools that can help when debugging your application. In this chapter, we will go through some of the most useful debugging tools and how to use them.
To be able to debug your nRF Connect SDK application, you must configure it correctly before building.
In the build configuration menu, under Optimization level, select the option Optimize for debugging. This option enables additional extension-specific debugging features after the application is built.
When this optimization is set, the following Kconfigs are enabled in the build:
CONFIG_DEBUG_OPTIMIZATIONS
– Limits the optimizations done by the compiler to only those that do not impact debugging.CONFIG_DEBUG_THREAD_INFO
– Adds additional information to the thread object so the debugger can discover the threads.Other Kconfigs that can be useful when debugging:
CONFIG_I2C_DUMP_MESSAGES
– Enables the possibility to log all I2C messages. See Debugging I2C communication for more information.CONFIG_ASSERT
– This enables the __ASSERT()
macro in the kernel code. If an assertion fails, the policy for what to do is controlled by the implementation of the assert_post_action()
function, which by default will trigger a fatal error.It’s worth taking into consideration that enabling optimization for debugging will affect the size of the code, and this can cause errors. If this is a problem, enabling only what is needed from the default optimization for debugging might be used; for example CONFIG_THREAD_NAME
or CONFIG_DEBUG
. There are also additional Kconfigs for different network stacks.
Outputs are created at CMake configuration time and are not always regenerated when one of their inputs changes. As a first step in getting the build compiled, it is recommended to delete the build folder and do a pristine build manually.
A breakpoint is a place in the code where you want the instruction to pause the execution of the application when the selected point is reached. This allows you to examine the program state and variable values or look at the call stack at that point of execution. The breakpoints are normally set at the line where you would like the application to halt its execution.
In VS Code, one can set conditional breakpoints, so the breakpoint occurs when reaching a specific expression or function. It is also possible to print a log statement instead of stopping at the breakpoint. To change the type of breakpoint, either right-click on an existing breakpoint and select Edit breakpoint or right-click on the line number and add a desired type of breakpoint.
The debugger can run in two modes: halt mode and monitor mode.
In halt mode, the debugger halts the CPU when a debug request occurs, while in monitor mode, the debugger lets the CPU debug parts of an application while crucial functions continue. Monitor mode is useful for timing-critical applications like Bluetooth Low Energy or PWM, where halting the entire application will affect the timing-critical aspects. The CPU takes debug interrupts, running a monitor code for J-Link communication and user-defined functions. The Logpoint can be used together with monitor mode to provide debug information without crashing the application.
To enable monitor mode for the application, do the following:
CONFIG_CORTEX_M_DEBUG_MONITOR_HOOK
and CONFIG_SEGGER_DEBUGMON
.-exec monitor exec SetMonModeDebug=1
in the debug console to enable monitor mode.The Variables view shows variables in the current scope and organizes them into local, global, and static sections. The variables are only updated when the application is halted.
The Watch View shows the output of variables you have selected to watch.
To start watching a variable:
When debugging starts, the debugger shows a memory map of peripherals from your device’s SVD file and displays the values of their registers and fields. You can use this information to monitor and troubleshoot your hardware behavior.
The call stack lists all of the available threads and indicates which ones are running. It also includes the call stack for each thread that is running. The call stack is a data structure that keeps track of function calls within an application. A stack is a kernel object that implements a Last-In-First-Out (LIFO) queue. This allows threads and ISRs to add and remove a limited number of integer data values. More about stacks can be found here.
The Thread Viewer shows you information about the specific threads in the application. Their state uses Zephyr’s thread states and is only updated when the debugger is stopped. While the device is running, the View is frozen. Any state in the table is stale until execution is stopped again.
The Memory Explorer shows the memory content on your device. You can review different regions of memory data in its Flash and RAM tabs. More information about what you can do in this view can be found here.