Debugging¶
External Resources¶
- Debugging common application problems with TI-RTOS video presentation on training.ti.com.
Debug Interfaces¶
The CC26x2 platform supports both the cJTAG and JTAG debug interfaces. Debug probes that support cJTAG, like the TI XDS110 and XDS100v3, can work natively with the CC26x2. Other Debug probes that only support JTAG, like the IAR I-Jet and Segger J-Link, need to inject a cJTAG sequence to enable JTAG functionality. The hardware resources included on the devices for debugging are listed as follows. Not all debugging functionality is available in all combinations of debug probe and IDE.
- Flash Patch and Breakpoint Unit (FPB) - 6 instruction comparators, 2 literal comparators
- Data Watchpoint and Trace Unit (DWT) - 5 watchpoints on memory access
- Instrumentation Trace Macrocell (ITM) - 32 x 32 bit stimulus registers
- Trace Port Interface Unit (TPIU) - serialization and time-stamping of DWT and ITM events
XDS110 Debug Probe¶
The CC26x2 LaunchPad have an on-board XDS110, and this is the assumed debug probe for most development.
The XDS110 is the latest entry level debug probe (emulators) for TI embedded processors. Designed to be a complete solution that delivers JTAG and SWD connectivity at a low cost, the XDS110 is the debug probe of choice for entry-level debugging of TI microcontrollers, processors and SimpleLink devices. Also, both Core Processor and System Trace are available for all ARM and DSP devices that support Embedded Trace Buffer (ETB).
Configuring Debugger in CCS¶
If only one debug probe is attached, CCS will automatically select the connected debug probe when a debug session is started. You can start a debug session by clicking the debug icon on the toolbar.
If more than one debug probe is attached when a debug session is started, CCS will prompt you to select a debug probe as seen in Figure 56. CCS will save the selected debug probe in the target configuration for the project.
To set or change the selected debug probe for a project, it is necessary to set the serial number for the probe in the project’s target configuration.
Find The Serial Number¶
To find the serial number for XDS110 debug probe, open a command prompt and run
the command
c:\ti\ccs_7_3_0_00019\ccsv7\ccs_base\common\uscif\xds110\xdsdfu.exe -e
.
This will enumerate all the attached XDS110 debug probes. This should result in
output like the following.
C:\>c:\ti\ccs_7_3_0_00019\ccsv7\ccs_base\common\uscif\xds110\xdsdfu.exe -e
USB Device Firmware Upgrade Utility
Copyright (c) 2008-2015 Texas Instruments Incorporated. All rights reserved.
Scanning USB buses for supported XDS110 devices...
<<<< Device 0 >>>>
VID: 0x0451 PID: 0xbef3
Device Name: XDS110 Embed with CMSIS-DAP
Version: 2.3.0.9
Manufacturer: Texas Instruments
Serial Num: L1100017
Mode: Runtime
<<<< Device 1 >>>>
VID: 0x0451 PID: 0xbef3
Device Name: XDS110 Embed with CMSIS-DAP
Version: 2.3.0.9
Manufacturer: Texas Instruments
Serial Num: L11000EN
Mode: Runtime
Found 2 devices.
C:\>
For XDS100 series debug probes, open a command prompt and run the command
c:\ti\ccs_7_3_0_00019\ccsv7\ccs_base\common\uscif\xds100serial.exe
. This
should result in output like the following.
C:\>c:\ti\ccs_7_3_0_00019\ccsv7\ccs_base\common\uscif\xds100serial.exe
Scanning for XDS100 emulators...
VID/PID Type Serial # Description
0403/a6d1 XDS100v3 06EB12213144 Texas Instruments XDS100v3
C:\>
Configure Serial Number¶
It may be necessary to set or unset the selected debug probe. Use the following steps to do this.
|
|
|
|
|
|
|
Connecting to the XDS Debugger¶
If only one debugger is attached, the IDE uses it automatically when you click the button in CCS or in IAR.
If multiple debuggers are connected, you must choose the individual debugger to use. The following steps detail how to select a debugger in CCS and IAR.
Configuring Debugger in IAR¶
If only one debugger is attached, IAR uses it automatically when you click the button.
If more than one debug probe is connected, use the following steps to have IAR always prompt to select the connection.
- Open the project options (
Project
→Options
) - Go to the Debugger entry.
- Go to Extra options.
- Add the following command line option:
--drv_communication=USB:#select
Breakpoints¶
Comparators in the FPB of the CC26x2 are used to break on an instruction fetch. This can be used to patch a function as it is fetched from instruction memory. Or these comparators can be used to supply a Breakpoint (BKPT) instruction to the CPU. These instructions halt the processors operation, waiting for the debug probe.
Considerations¶
While breakpoints are a useful tool for debugging code online, they have the possibility of altering the execution flow of a piece of code.
Breakpoints and Timing¶
Synchronous RF protocols are timing sensitive. Breakpoints can easily halt the execution long enough to lose network timing and break the link.
To still be able to debug, place breakpoints as close as possible to where the relevant debug information can be read or step through the relevant code segment to debug.
After you hit a breakpoint and read out the necessary debug information, it is recommended that you reset the device and re-establish the connection.
Breakpoints and Optimization¶
When compiler optimizations are enabled, toggling a breakpoint on a line of C code may not result in the expected behavior. Some examples include the following.
- Code is removed or not compiled in
- Toggling a breakpoint in the IDE results in a breakpoint somewhere other than the intended line. Some IDEs disable breakpoints on nonexistent code.
- Code block is part of a common subexpression
- A breakpoint might be trigged from a function or piece of code near the marked line. This might have been due to the compiler reusing sections.
- An if clause is represented by a conditional branch in assembly
- A breakpoint inside an if clause always breaks on the conditional statement, even when the condition is not true.
TI recommends selecting an optimization level as low as possible when debugging. See Optimizations for information on modifying optimization levels.
Breakpoints in CCS¶
Note
CCS reserves one hardware breakpoint for instruction stepping.
To toggle a breakpoint, do any of the following.
- Double-click the area to the left of the line number.
- Press
Ctrl
+Shift
+B
. - Right-click on the line.
A breakpoint set on line 207 looks like the following.
For an overview of the active and inactive breakpoints, click on View
→ Breakpoints
.
To set a conditional break, do as follows.
- Right-click the breakpoint in the overview.
- Choose Properties.
When debugging, Skip Count and Condition can help skip a number of breaks or only break if a variable is a certain value.
Note
Conditional breaks require a debugger response and may halt the processor long enough to break an active RF connection, or otherwise disrupt timing on the debug target.
Breakpoints in IAR¶
Note
IAR reserves one comparator for instruction stepping.
To toggle a breakpoint, do any of the following.
- Single-click the area to the left of the line number.
- Go to the line and press
F9
to toggle breakpoint - Right-click on the line and select Toggle Breakpoint (Code).
A breakpoint looks like this:
For an overview of the active and inactive breakpoints, click
View
→ Breakpoints
.
To set a conditional break, do as follows.
- Right-click the breakpoint in the overview.
- Choose Edit….
When debugging, Skip Count and Condition can help skip a number of breaks or only break if a variable is a certain value.
Note
Conditional breaks require a debugger response and may halt the processor long enough to break an active RF connection, or otherwise disrupt timing on the debug target.
Watching Variables and Registers¶
Debuggers offer several ways of viewing the state of a halted program. Global variables are statically placed during link-time and can end up anywhere in the RAM or Flash of the chip. These variables can be viewed when then target is halted by the debugger through the Watch and Expression windows.
Unless removed due to optimizations, global variables are always available in these views. Local variables or variables that are only valid inside a limited scope are only viewable in that scope. Such variables can also be viewed with the Watch or Expression views, and may also be automatically displayed when breaking or stepping through code.
Considerations¶
Local variables are often placed in CPU registers and not on the stack. These variables also have a limited lifetime even within the scope in which they are valid. Depending on the optimization performed, a variable placed in a register may not have a cohesive view of the current state of the variable. Some possible solutions are:
- Move the variable to global scope, so it remains accessible in RAM.
- Make the variable volatile, so the compiler doesn’t place the value in a register.
- Make a shadow copy of the variable that is global and volatile.
Variables in CCS¶
You can view Global Variables by doing either of the following.
- Select
View
→Expressions
. - Select a variable name in code.
- Right-click and select Add Watch Expression.
- Select
View
→Variables
toauto-variables
that are present at the current location when stepping through code.
Variables in IAR¶
To view Global Variables, do either of the following.
- Right-click on the variable.
- Select Add to Watch: varName.
- Select
View
→Watch
n- Enter the name of the variable.
View –> Locals show the local variables in IAR.
Note
IAR may remove the variable during optimization and inline the usage of the
value. If so, add the __root
directive in front.
Memory Watchpoints¶
As mentioned in Debug Interfaces, the DWT module contains four memory watchpoints that allow breakpoints on memory access. The hardware match functionality looks only at the address. If intended for use on a variable, the variable must be statically allocated.
Note
If a data watchpoint with value match is used, two of the four watchpoints are used.
Watchpoints in CCS¶
- Right-click on a global variable.
- Select
Breakpoint
→Hardware Watchpoint
. - Go to the list of breakpoints (
View
→Breakpoints
). - Right-click and edit the Breakpoint Properties to configure the watchpoint.
This example configuration ensures that if 0x42 is written to the memory location for Characteristic 1 in the Bluetooth low energy simple_peripheral example project the device halts execution.
Watchpoints in IAR¶
- Right-click a variable (global).
- Select
Set Data Breakpoint for 'myVar'
to add it to the active breakpoints. - Go to the list of breakpoints (View –> Breakpoints)
- Choose
Edit...
to set up whether the watchpoint should match on read, write, or any access.
TI-RTOS Object Viewer¶
Debuggers may include the RTOS Object Viewer (ROV) plug-in that provides insight into the current state of TI-RTOS, including task states, stacks, and so forth.
This section discusses some ROV views useful for debugging and profiling. More details can be found in the TI-RTOS User’s Guide, including documentation on how to add log events to application code.
Scanning the BIOS for Errors¶
The BIOS Scan for errors
view goes through the available ROV modules and
reports on errors. This functionality can be a good point to start if anything
has gone wrong. This scan only shows errors related to TI-RTOS modules and only
the errors it can catch.
Viewing the State of Each Task¶
The Task Detailed
view is useful for seeing the state of each task and its
related runtime stack usage. This example shows the state the first time the
user-thread is called. Figure 69. shows the Bluetooth low
energy stack task, represented by its ICall proxy, the Idle task, the
simple_peripheral task and the GAPRole task.
The following list explains the column in Figure 69.
- address
- This column shows the memory location of the
Task_Struct
instance for each task. - priority
- This column shows the TI-RTOS priority for the task.
- mode
- This column shows the current state of the task.
- fxn
- This column shows the name of the entry function of the task.
- arg0, arg1
- These columns show arbitrary values that can be given to entry function of the task. In the image, the ICall_taskEntry is given 0xb001, which is the flash location of the entry function of the RF stack image and 0x20003a30 (the location of bleUserCfg_t user0Cfg, defined in main()).
- stackPeak
- This column shows the maximum run-time stack memory used based on watermark in RAM, where the stacks are prefilled with 0xBE and there is a sentinel word at the end of the run-time stack.
Note
Function calls may push the stack pointer out of the run-time stack, but not actually write to the entire area. A stack peak near stackSize but not exceeding it may indicate stack overflow.
- stackSize
- This column shows the size of the runtime stack, configured when instantiating a task.
- stackBase
- This column shows the logical top of the runtime stack of the task (usage starts at stackBase + stackSize and grows down to this address).
Viewing the System Stack¶
The Hwi Module
view allows profiling of the system stack used during boot
or for main(), Hwi execution, and Swi execution. See
System Stack for more information on the system
stack.
The hwiStackPeak, hwiStackSize, and hwiStackBase can be used to check for system stack overflow.
ROV in CCS¶
To access the ROV while in a debug session in CCS:
- Click the Tools menu.
- Click RTOS Object View (ROV).
Warning
When using autosized heap, the ROV may display errors when accessing modules such as Task and Heap for ROV Classic. In order to use ROV with an autosized heap you can use ROV2, or do the following:
- Apply the following patch to the
function init()
in\kernel\tirtos\packages\ti\sysbios\heaps\package.xs
. The lines highlited below should be added.
function init()
{
/* Add HeapMem's primaryHeap addresses range to ROV's memory sections */
if (xdc.om.$name == "rov") {
var HeapMem = xdc.module('ti.sysbios.heaps.HeapMem');
// if (xdc.om['ti.sysbios.heaps'].HeapMem.$used) {
var Program = xdc.useModule('xdc.rov.Program');
var HeapMemCfg = Program.getModuleConfig(HeapMem.$name);
if (HeapMemCfg.primaryHeapBaseAddr != null) {
var base = Program.getSymbolValue(HeapMemCfg.primaryHeapBaseAddr.substr(1));
var end = Program.getSymbolValue(HeapMemCfg.primaryHeapEndAddr.substr(1));
/* Retrieve the MemoryImage java object. */
var Model = xdc.module("xdc.rov.Model");
var memReader = Model.getMemoryImageInst();
/* retrieve the sections list and add a new section */
var sections = memReader.getSections();
sections.addSection(base, end-base);
}
// }
/* Retrieve the MemoryImage java object. */
var Model = xdc.module("xdc.rov.Model");
var memReader = Model.getMemoryImageInst();
/* retrieve the sections list and add a new section */
var sections = memReader.getSections();
sections.addSection(0x20000000, 0x20005000);
}
}
ROV in IAR¶
To access the ROV while in a debug session in IAR
- Use the TI-RTOS menu on the menu bar.
- Select a subview.
Warning
When using autosized heap, the ROV may display errors when accessing modules such as Task and Heap for ROV Classic. In order to use ROV with an autosized heap, the following steps may be taken:
- Apply the following patch to the
function init()
in\kernel\tirtos\packages\ti\sysbios\heaps\package.xs
. The lines highlited below should be added.
function init()
{
/* Add HeapMem's primaryHeap addresses range to ROV's memory sections */
if (xdc.om.$name == "rov") {
var HeapMem = xdc.module('ti.sysbios.heaps.HeapMem');
// if (xdc.om['ti.sysbios.heaps'].HeapMem.$used) {
var Program = xdc.useModule('xdc.rov.Program');
var HeapMemCfg = Program.getModuleConfig(HeapMem.$name);
if (HeapMemCfg.primaryHeapBaseAddr != null) {
var base = Program.getSymbolValue(HeapMemCfg.primaryHeapBaseAddr.substr(1));
var end = Program.getSymbolValue(HeapMemCfg.primaryHeapEndAddr.substr(1));
/* Retrieve the MemoryImage java object. */
var Model = xdc.module("xdc.rov.Model");
var memReader = Model.getMemoryImageInst();
/* retrieve the sections list and add a new section */
var sections = memReader.getSections();
sections.addSection(base, end-base);
}
// }
/* Retrieve the MemoryImage java object. */
var Model = xdc.module("xdc.rov.Model");
var memReader = Model.getMemoryImageInst();
/* retrieve the sections list and add a new section */
var sections = memReader.getSections();
sections.addSection(0x20000000, 0x20005000);
}
}
Using the Memory Browser¶
Debuggers are able to show a representation of the memory on the CC26x2. In CCS, you can index by address or by symbol name. As an example, consider the stack that was overrun in Figure 69.:
Simple Peripheral Task’s stack. Note BE watermark |
GAPRole Task’s stack. Note it’s completely filled. |
The solution in this case would be to increase the stack size for the failing
task and see what the stack peak really is. The stackPeak
reported is
relying on how many watermark bytes are overwritten, so it can’t know how much
the overrun amounts to.
Because stacks are utilized from higher addresses towards lower addressed (upwards in the picture), stacks that overrun will tend to overwrite data at locations immediately before the stack.
Debugging Common Heap Issues¶
Warning
When using an auto sized heap, ROV Classic may report errors. Please see the TI-RTOS Object Viewer section for a workaround and more information.
As described in Dynamic Memory Allocation, the Heap Manager and its heap are used to allocate messages between the Bluetooth low energy stack task and the application task and as dynamic memory allocations in the tasks, as well as in TI-RTOS.
Profiling functionality is provided for the heap but is not
enabled by default. Therefore, it must be compiled in by
adding HEAPMGR_METRICS
to the defined preprocessor symbols. This
functionality is useful for finding potential sources for
unexplained behavior and to optimize the size of the heap.
When HEAPMGR_METRICS
is defined, the variables and functions listed as
follows become available. Global variables:
- heapmgrBlkMax
- The maximum amount of simultaneous allocated blocks
- heapmgrBlkCnt
- The current amount of allocated blocks
- heapmgrBlkFree
- The current amount of free blocks
- heapmgrMemAlo
- The current total memory allocated in bytes
- heapmgrMemMax
- The maximum amount of simultaneous allocated memory in blocks (this value must not exceed the size of the heap)
- heapmgrMemUB
- The furthest memory location of an allocated block, measured as an offset from the start of the heap
- heapmgrMemFail
- The amount of memory allocation failure (instances
where
ICall_malloc()
has returned NULL)
Furthermore when using a TI-RTOS based heap such as HeapMem or HeapTrack, there is additional debugging capability that can be used.
Functions¶
Note the below functions are enabled only for the legacy OSAL heap, TI-RTOS based heap implementations offer native support for their functionality.
void ICall_heapGetMetrics(u16 *pBlkMax, u16 *pBlkCnt, u16 *pBlkFree, u16 *pMemAlo, u16 *pMemMax, u16 *pMemUb)
- Returns the previously described variables in the pointers passed in as parameters
int heapmgrSanityCheck(void)
- Returns 0 if the heap is ok; otherwise, returns a nonzero (that is, an array access has overwritten a header in the heap)
However, the get stats function is available to all three supported heap configurations.
ICall_getHeapStats(ICall_heapStats_t)
- Returns a pointer to the Heap statics structure.
The heap stats structure, is defined as below:
typedef struct { uint32_t totalSize; uint32_t totalFreeSize; uint32_t largestFreeSize; }ICall_heapStats_t;
Determining the Auto Heap Size¶
The following procedure can be used to view the size of the heap when the auto heap size feature is enabled.
The auto heap size feature takes advantage of linker file symbols to determine the optimal heap size, the user can determine the size of the auto heap via the generated map file using the procedure below:
The size of the heap is the difference between the address of the last item in the .bss section and the start address of the system stack (CSTACK). For example, the
20003f48 heapEnd
20001cc1 heapStart
The size of the heap in this example is defined as:
0x20003f48 - 0x20001cc1 = 0x2287
or 8839 bytes
for the heap.
Note
The above procedure will work for any active heap implementation
Determining the auto heap size is slightly dependent on the heap implementation that is active, see the sections below to see how to determine the size of an auto sized heap at runtime.
OSAL HEAP
- Open the variable watch window and view
HEAPMGR_SIZE
, it will report the heap size.
HeapMem or HeapMem + HeapTrack
- Using ROV, open the
HeapMem.Detailed
view, the heap’s size is reported in the totalSize field. See TI-RTOS Object Viewer for more information on ROV and how to use it.
Programatically Accessing the Heap Configuration¶
The heap configuration variables can be accessed software to determine the active heap configuration and size at runtime. The code snippet below will print out the active heap config and the heap’s size.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <xdc/cfg/global.h> // This is included to access cfg file variables
//...
// Get the HeapSize
ICall_heapStats_t stats;
ICall_getHeapStats(&stats);
if((HEAPMGR_CONFIG & 0x03) == 0x00)
{
Display_print0(dispHandle, 6,0, "Using Heap: OSAL");
}
else if ((HEAPMGR_CONFIG & 0x03) == 0x01)
{
Display_print0(dispHandle, 6,0, "Using Heap: HeapMem");
}
else if((HEAPMGR_CONFIG & 0x03) == 0x02)
{
Display_print0(dispHandle, 6,0, "Using Heap: HeapMem + HeapTrack");
}
Display_print1(dispHandle, 7,0, "Heap Size: %d", stats.totalSize);
|
Troubleshooting Heap Problems¶
Issues with dynamic allocated memory can be notoriously hard to track down and debug; this section aims to give tips on how to debug the most common issues with dynamic memory.
In general, HeapMem and HeapMem + HeapTrack offer more debuggability than the OSAL heap through the ROV tools, but have associated tradeoffs such as speed and overhead. If you suspect that there are heap issues, enable a more verbose heap implementation to help debug and root cause the issue.
- HeapTrack is a module built above the HeapMem module used to debug heaps, it offers the most debugging capability.
- HeapMem offers ROV support and has the ability to detect if the internal structure of the heap has become corrupted.
- OSAL heap offers APIs for logging heap metrics and stats
Reference the Heaps section of the BIOS User’s Guide for more information on the TI-RTOS provided heap implementations.
Writing to already freed memory
Pointers to memory which have already been freed using ICall_free()
should
no longer be used. A common practice is to set pointers to NULL after they have
been freed, and check them for NULL before using them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Allocate Memory
uint32_t *myPtr = ICall_malloc(500);
//..
// Later free the pointer, set it to NULL
ICall_free(myPtr);
myPtr = NULL;
// This check will protect against writing to already freed memory
if( NULL != myPtr)
{
*myPtr = 42;
}
|
When accessing memory that has already been freed, there is a risk that the internal structure of the heap will become corrupted. Let’s assume some code didn’t follow the above convention, and wrote to a free’d pointer.
The figure below shows how the user can use HeapMem to detect heap corruption with ROV, notice the dramatic change in freeSize.
Freeing Already freed Memory
The cause of this bug is the same as the one from the previous section, double frees will corrupt the internal structure of the heap.
1 2 3 4 5 6 7 | // Allocate Memory
uint32_t *myPtr = ICall_malloc(500);
//..
ICall_free(myPtr);
ICall_free(myPtr);
|
Starving the system/Memory Leak
Warning
Asserting during a heap failure may be considered dangerous in production code, however this section seeks to showcase its use in debugging.
If the protocol stack relies on dynamic allocation to pass messages between its internal layers and the application, starving the stack of memory may result in unexpected behavior. This can also negatively affect other application processes that require dynamic memory such as voice streaming.
The stack can be setup to assert when allocations fail by following the steps below:
- Include
hal_assert.c
in the user application project- Define
EXT_HAL_ASSERT
andMEM_ALLOC_ASSERT
- Plug a handler function in
main.c
, seemulti_role
’s main function for an example
The code below will force this condition by mallocing without freeing.
1 2 3 4 5 | uint8_t i = 0;
while(i < 500)
{
ICall_malloc(500);
}
|
This condition can be caused by an application that calls malloc()
during an
operation without a call to free()
later in the code. Thus the code will keep
requiring more memory every time the operation runs without ever freeing any
memory. The above code snippet is an exaggerated example of this.
At the time of a failed allocation, a full call-stack is provided:
Checking the return value of malloc
When allocating memory on the heap using malloc, it is import to check it’s return value. Otherwise, this will often result in dereferncing a null pointer, which will result in an exception.
1 2 3 4 5 6 | uint8_t *myPtr = ICall_malloc(75);
if(NULL == myPtr)
{
// Error handling here
}
|
There are many more tips for debugging heap issues available in this TI-RTOS Debugging Workshop
Optimizations¶
Compiler optimizations are great for saving space or speeding up execution. However, these optimizations can be very difficult to debug around. There are multiple levels at which optimization can be turned on or off.
Project-wide optimization settings are the most general. Sometimes, given the constraints of the device, it is impossible to lower the size optimization level. File-wide optimization settings can be used like project-wide optimizations to turn on or off certain settings. The most granular control is using compiler directives to control optimization at a function level.
Optimizations in CCS¶
Project-Wide Optimizations¶
Open the project optimization settings by going to Project Properties
→ CCS Build
→ ARM Compiler
→ Optimization
Single-File Optimizations¶
Note
Do single-file optimizations with care because this also overrides the project-wide preprocessor symbols.
- Right-click on the file in the Workspace pane.
- Choose Properties.
- Change the optimization level of the file using the same menu in the CCS project-wide optimization menu.
Single-Function Optimizations¶
Warning
Pragmas are very specific to the toolchain, and may lead to non-reusable code. Be careful where you use these.
#pragma FUNCTION_OPTIONS(myFunction, "--opt_level=0")
static void myFunction(int number)
{
// ...
return yourFunction(other_number);
}
#pragma GCC push_options
#pragma GCC optimize ("O0")
static void myFunction(int number)
{
// ...
return yourFunction(other_number);
}
#pragma GCC pop_options
Optimizations in IAR¶
Single-File Optimizations¶
- Right-click on the file in the Workspace pane.
- Choose Options.
- Check Override inherited Settings.
- Choose the optimization level.
Single-Function Optimizations¶
Warning
Pragmas are very specific to the toolchain, and may lead to non-reusable code. Be careful where you use these.
Use #pragma optimize=none before the function definition to deoptimize the entire function, that is, as follows.
#pragma optimize=none
static void myFunction(int number)
{
// ...
return yourFunction(other_number);
}
Loading TI-RTOS in ROM Symbols¶
Some of the TI-RTOS kernel modules are included in ROM, and executed from ROM in order to save Flash space for the application. This can lead to some confusion, when only addresses are shown in the disassembly view and the call-stack view.
All TI-RTOS kernel code in ROM starts with address 0x1001xxxx
. In order to
make sense of the ROM’ed code, you need to include the symbol files in your
debug session.
Import in CCS¶
- While in debug mode, click the drop-down button next to the Load Program icon
- Select
Add Symbols ...
- Select
Browse ...
and find<SDK_INSTALL_DIR>\kernel\tirtos\packages\ti\sysbios\rom\cortexm\cc26xx\r2\golden\CC26xx\rtos_rom.xem3
Import in IAR¶
- In the project options, go to
Debugger
andImages
, then add the image<SDK_INSTALL_DIR>\kernel\tirtos\packages\ti\sysbios\rom\cortexm\cc26xx\r2\golden\CC26xx\rtos_rom.xem3
* Check the box for ‘Debug info only’, and use Offset = 0.
Deciphering CPU Exceptions¶
Several possible exception causes exist. If an exception is caught, an exception handler function can be called. Depending on the project settings, this handler may be a default handler in ROM, which is just an infinite loop or a custom function called from this default handler instead of a loop.
When an exception occurs, the exception may be caught and halted in debug mode immediately, depending on the debugger. If the execution halted manually later through the Break debugger, it is then stopped within the exception handler loop.
Exception Cause¶
With the default setup using TI-RTOS, the exception cause can be found in the
System Control Space register group (CPU_SCS
) in the register CFSR
(Configurable Fault Status Register). The ARM Cortex-M3 User Guide describes
this register. Most exception causes fall into the following three categories.
- Stack overflow or corruption leads to arbitrary code execution.
- Almost any exception is possible.
- A NULL pointer has been dereferenced and written to.
- Typically (IM)PRECISERR exceptions
- A peripheral module (like UART, Timer, and so forth) is accessed
without being powered.
- Typically (IM)PRECISERR exceptions
The CFSR
register is available in View
→ Registers
.
When an access violation occurs, the exception type is IMPRECISERR because writes to flash and peripheral memory regions are mostly buffered writes.
If the CFSR:BFARVALID
flag is set when the exception occurs (typical for
PRECISERR), the BFAR
register in CPU_SCS
can be read out to find which
memory address caused the exception.
If the exception is IMPRECISERR, PRECISERR can be forced by manually disabling
buffered writes. Set [CPU_SCS:ACTRL:DISDEFWBUF
] to 1, by either manually
setting the bit in the register view in the debugger or by including
<hw_cpu_scs.h>
from Driverlib and calling the following.
#include <ti/devices/cc26x0r2/inc/hw_cpu_scs.h>
//..
int main()
{
// Disable write-buffering. Note that this negatively affect performance.
HWREG(CPU_SCS_BASE + CPU_SCS_O_ACTLR) = CPU_SCS_ACTLR_DISDEFWBUF;
// ..
}
Using TI-RTOS and ROV to Parse Exceptions¶
To enable exception decoding in the RTOS Object View (ROV) without using too much memory, use the Minimal exception handler in TI-RTOS. The default choice in the BLE5-Stack projects is to use no exception handler.
To set this up, change the section of the TI-RTOS configuration file that relates to M3Hwi so that it looks like the code below:
//m3Hwi.enableException = true;
m3Hwi.enableException = false;
//m3Hwi.excHandlerFunc = null;
m3Hwi.excHookFunc = "&execHandlerHook";
Then, make a function somewhere with the signature void
(*Hwi_ExceptionHookFuncPtr)(Hwi_ExcContext*);
such as the one below:
#include <ti/sysbios/family/arm/m3/Hwi.h>
// ...
volatile uintptr_t *excPC = 0;
volatile uintptr_t *excCaller = 0;
// ...
void execHandlerHook(Hwi_ExcContext *ctx)
{
excPC = ctx->pc; // Program counter where exception occurred
excCaller = ctx->lr; // Link Register when exception occurred
while(2);
}
Setting m3Hwi.enableException
to false enables the minimal handler, which
fills out the global Hwi_ExcContext
structure that the ROV looks at to show
the decoded exception. By setting up an excHookFunc, the minimal exception
handler will call this function and pass along a pointer to the exception
context for the user to work with. This structure is defined in
<ti/sysbios/family/arm/m3/Hwi.h>
.
When an exception occurs, the device should end up in that infinite loop.
Inspect the ROV
→ Hwi
→ Exception information
.
In this case, a bus fault was forced in the function writeToAddress by dereferencing address 0x0013 and trying to write to it:
void writeToAddress(uintptr_t *addr, int val)
{
*(int *)addr = val;
}
// ..
void taskFxn(...)
{
// ..
writeToAddress( (void*)19, 4 ); // Randomly chosen values
}
The write instruction was placed on line 79 of application.c, as indicated. To get a precise location, the write buffer was disabled as described earlier.
It can be instructive to look at the disassembly view for the locations specified by PC (program counter) and LR (link register). PC is the presumed exception location, and LR is normally the location the failing function should have returned to. As an example, the PC at this exception:
Some forensics is required here. We have from the Hwi decoding in ROV (and from
the exception context in the exception hook) that the program counter was
0x708e
when the exception occurred.
At that location there is a store instruction str r0, [r1]
meaning, store
in R0 the value of what the memory address in R1 points to. The business with
SP
in the figure above is related to optimization being turned off, so all
local variables are stored on the stack, even though in this case R0 and R1
could have been used directly from the caller.
Now we know that the exception occurred because someone called
writeToAddress
with an invalid address.
Thanks to the exception decoder we can easily find the call site by looking at
the call stack, but if the call stack isn’t helpful, we can look at lr
,
which is seen in the exception decoder to be 0x198f
We can see here that R0 and R1 are initialized with constants. This means that some programmer has intentionally called the write function with an address that causes a busfault.
Most often the reason for a bus-fault is that a pointer is not initialized and
a function like writeToAddress
gets the pointer, assumes it’s valid and
dereferences the pointer and writes to the invalid address.
Application exits prematurely¶
When an application is built with TI-RTOS kernel instrumentation enabled, it is
possible that one of its runtime checks might cause the application to terminate
prematurely. Typically when this occurs, the application will halt at a
breakpoint symbol called loader_exit
(in CCS) or __exit
(in IAR).
Various available runtime checks are enabled via the TI-RTOS .cfg
configuration file. To determine what checks are enabled, you can see the
documentation included in the .cfg
file or refer to the TI-RTOS kernal
User Guide directly.
Some applications may contain a pair of Debug and Release build configurations. When such build configurations exist, the Debug configuration takes advantage of utilizing a suite of runtime checks. These runtime check can include but are not limited to:
- Task stack overrun checks
- System stack overrun checks
- Various asserts (Tasks, Swi, Hwi, etc…)
- Loggers (UIA loggers)
- Kernel policy checks (Checking if runtime creation/deletions are permitted)
When an runtime check detects an anomaly, the kernel responds by capturing and
printing the fault condition and halting the system at the exit
or loader_exit
symbol.
loader_exit
in CCS¶
If a fault condition causes an application to exit prematurely in CCS, the
target will halt at the loader_exit
symbol - as shown
in Figure 80.. The cause for the fault condition is typically
displayed in the console window.
If the console window is not already open, CCS will open the console window for you and print the cause for the termination. Alternatively, you can also use ROV to determine the cause for the fault.
__exit
in IAR¶
When an application exits prematurely in IAR, the fault condition is displayed
in the Terminal I/O window. With IAR, the Terminal I/O window does not open
automatically. To the developer, the application will simply halt at __exit
as shown in Figure 82..
In such an event, you will need to manually open the Terminal I/O window under View → Terminal I/O so you can see the cause for the fault. Alternatively, you can also use ROV to determine the cause for the fault.
Debugging Memory Problems¶
This section describes how to debug a situation where the program runs out of memory, either on the heap or on the runtime stack for the individual thread contexts. Exceeding array bounds or dynamically allocating too little memory for a structure corrupts the memory and can cause an exception like INVPC, INVSTATE, IBUSERR to appear in the CFSR register.
Task and System Stack Overflow¶
If an overflow on the runtime stack of the task or the system stack occurs (as found using the ROV plug-in), perform the following steps.
- Note the current size of the runtime stack of each task.
- Increase by a few 100 bytes as described in Initializing a Task and System Stack
- Reduce the runtime stack sizes so that they are larger than their respective stackPeaks to save some memory.
Dynamic Allocation Errors¶
Debugging Common Heap Issues describes how to use the ICall Heap profiling functionality. To check if dynamic allocation errors occurred, do as follows:
- Check whether
heapmgrMemAlo
orheapmgrMemAlo
are close toHEAPMGR_SIZE
- Check memFail to see if allocation failures have occurred.
- Call the sanity check function.
If the heap is sane but there are allocation errors, increase
HEAPMGR_SIZE
and see if the problem persists.
You can set a breakpoint in heapmgr.h in HEAPMGR_MALLOC()
on the
line hdr = NULL;
to find an allocation that is failing.
Check System Flash and RAM Usage With Map File¶
Both application and stack projects produce a map file which can be used to compute the combined flash and RAM system memory usage. Both projects have their own memory space and both map files must be analyzed to determine the total system memory usage. The map file is in the output folder of the respective project in IAR. To compute the total memory usage, do as follows.
- Open the application map file (that is, simple_peripheral_cc2650r2lp_app.map).
Note
At the end of the file, three lines contain a breakdown of memory usage for read-only code, read-only data, and read/write data.
- Add the two values for read-only code and read-only data memory.
Note
This sum is the total flash memory usage for the application project. The read/write data memory is the total RAM usage by the application project.
- Note these values.
- Open the stack map file.
- Compute the same flash and RAM values for the stack project.
- Add the total flash memory value from the application with the total flash usage of the stack to determine the total system flash usage.
- Add the total RAM usage from the application with the stack to get the total system RAM usage.
For CCS, the map file of the respective project gives a summary of flash and RAM usage. To determine the remaining available memory for each project, see Flash and RAM. Due to section placement and alignment requirements, some remaining memory may be unavailable. The map file memory usage is valid only if the project builds and links successfully.
Debugging a Program Exit¶
The program must never exit the main()
function. The usual cause for this is
that some software module has called abort()
.
Both IAR and CCS will halt execution when this happens, and the disassembly and call stack will show some type of __exit symbol.
The BLE Stack will call ICall_abort()
when one of the below happens:
- Calling an ICall function from a stack callback
- Misconfiguring of additional ICall tasks or entities
- Incorrect ICall task registering
Should the call stack not give enough information to deduce the cause of the
abort, a breakpoint can be set in the ICall_abort
function to trace from
where this error is coming.
HAL Assert Handling¶
Asserts can be useful when debugging, to trap undesirable states in
the code. The BLE Stack projects are by default set up to have the global
preprocessor symbol EXT_HAL_ASSERT
enabled, which will try to call an assert
handler that the user application can define.
Catching Stack Asserts in the Application¶
The application has an assert callback to catch asserts in the stack project. The assert callback is registered in main() function of each project:
/* Register Application callback to trap asserts raised in the Stack */
RegisterAssertCback(AssertHandler);
The main.c
file also contains an example AssertHandler
function.
Some generic assert causes that can be returned in the callback
include HAL_ASSERT_CAUSE_TRUE
,
HAL_ASSERT_CAUSE_OUT_OF_MEMORY
, and
HAL_ASSERT_CAUSE_ICALL_ABORT
.
When using a split-image build configuration, it may be possible to get a
HAL_ASSERT_CAUSE_INTERNAL_ERROR
assertion. This typically indicates that
the ICall bleAPITable
dispatch table is missing some functions so it calls
a generic icall_liteErrorFunction
error handler. Typically, a fix for this
is error is to enable a missing predefined compiler option to get correct APIs
into bleAPITable
.
The user can decide how to handle these asserts in the callback. By default, it goes into spinlock for most of the asserts.
The assert can also define a subcause that gives a more specific
reason for the assert. An example of a subcause is
HAL_ASSERT_OUT_OF_HEAP
, which describes the type of memory
causing the assert for HAL_ASSERT_CAUSE_OUT_OF_MEMORY
.
If no application callback is registered, the default assert
callback is called and returns without further action unless
HAL_ASSERT_SPIN
is defined in the application project, which traps
the application in an infinite while loop. In addition, one of the
following can also be defined in the stack project if it is not
caught in the application callback:
HAL_ASSERT_RESET
: Resets the deviceHAL_ASSERT_LIGHTS
: Turn on the hazard lights (to be configured by user)HAL_ASSERT_SPIN
: Spinlock in a while loop indefinitely
Enable these by ensuring that one of the above corresponding symbols are defined in the preprocessor symbols.
See hal_assert.h and hal_assert.c in the stack project for implementation details.