Open-CMSIS-Pack  Version 1.7.12
Delivery Mechanism for Software Packs
Implement debug sequences

Most Cortex-M devices rely on Arm Debug Interface (ADI) that specifies standard interface for accessing debug functionality on the processor. However due to implementation-specific variations it can still be challenging for debug tool vendors to provide reliable debugging experience for complex devices. Device vendors can use debug access sequences to customize the debugger behavior for a particular device.

Section Usage of debug access sequences provides working example flows that can be implemented in a debugger. By overwriting the predefined debug sequences it is possible to customize the debugger operation for a specific device. The syntax and available functions are described in details in Writing debug access sequences. This chapter explains the common cases that can be covered using debug sequences:

The examples provided are quite generic demonstrating the concept to follow when addressing a specific scenario. However actual implementation shall always take device specific behavior into account.

Enable device-specific debug configurations

The debug configuration options available in the debug IDEs mostly cover quite generic scenarios applicable to a wide set of devices and architectures, for example whether to use reset for debug connection and what type of reset, whether to stop after debug connection or not and so on. Configure debug access describes how to use debug descriptions to specify the configuration options available for the device and how to pre-select the default values.

But often there is a need to provide developers with some debug configuration options that are very device-specific. This can vary from simple SWO pin and clock source selection for tracing to more complex bootloader configuration or secure debug provisioning and multi-core system debug.

The debugvars element allows to define custom global debug access variables. Their values can also be made configurable via a project-specific debug configuration file (*.dbgconf). It is recommended to implement this file with Configuration Wizard annotations to enable simple graphical configuration view. Predefined debug access sequences can be overwritten where needed and use the custom debug variables. If a user-defined global access variable is not specified in the *.dbgconf file, then the value provided in the variable definition in the pdsc file is applied.

Documentation for the debugvars provides an example for trace SWO pin selection via a *.dbgconf file. Below is also an example that uses a custom global debug variable Dbg_CR for specifying whether the program shall stop after bootloader execution or not:

Use of debugvars in a pdsc file:

...
<debugvars configfile="Debug/LPC84x.dbgconf">>
__var Dbg_CR = 0x00000000; // DBG_CR, with default value 0x00000000
</debugvars>
...
// ResetCatchSet Sequence LPC84x
<sequence name="ResetCatchSet">
... // initial setup
<control if="Dbg_CR == 0x00000000" info="Stop after bootloader disabled">
<block>
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value | 0x00000001)); // Enable Reset Vector Catch in DEMCR
</block>
</control>
<control if="Dbg_CR == 0x00000001" info="Stop after bootloader enabled">
<block>
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value &amp; (~0x00000001))); // Disable Reset Vector Catch in DEMCR
</block>
</control>
...
</sequence>
...

*.dbgconf file: source code

// <<< Use Configuration Wizard in Context Menu >>>
// <h>Debug Configuration
// <o.0> StopAfterBootloader <i> Stop after Bootloader
// </h>
Dbg_CR = 0x00000001;
// <<< end of configuration section >>>

*.dbgconf file: Configuration Wizard view

In the same way custom debug variables can be used to provide configuration for device-specific debug registers that then can be programmed via debug sequences.

Ignore access errors

In some cases the debug access errors need to be ignored to support device-specific implemenation. For that the predefined debug access sequence can be overwritten by duplicating the original code with the error handling disabled in required places using predefined debug access variable __errorcontrol.

Below is an example for an NXP IMXRT1051 family:

<sequence name="ResetSystem">
<block>
// System Control Space (SCS) offset as defined in Armv6-M/Armv7-M.
__var SCS_Addr = 0xE000E000;
__var AIRCR_Addr = SCS_Addr + 0xD0C;
__var DHCSR_Addr = SCS_Addr + 0xDF0;
__errorcontrol = 0x1; // Skip errors, write to AIRCR.SYSRESETREQ may not be able to finish with OK response
// Execute SYSRESETREQ via AIRCR
Write32(AIRCR_Addr, 0x05FA0004);
__errorcontrol = 0x0; // Honor errors again
DAP_Delay(20000); // Delay of 20ms to let reset finish. Otherwise access to DHCSR can fail with too fast debug units.
</block>
//Reset Recovery: Wait for DHCSR.S_RESET_ST bit to clear on read
<control while="(Read32(DHCSR_Addr) &amp; 0x02000000)" timeout="500000"/>
</sequence>

In this implementation the standard system reset via AIRCR temporarily disables the DAP resulting in an access error. That could cause a debugger to disconnect. To overcome this the error handling is disabled before register write (__errorcontrol = 0x1;) and then enabled after it again. Additionally a delay is introduced (DAP_Delay(20000);) to allow reset to complete. The rest of the code is same as in the default ResetSystem implementation.

Configure trace

A common case that requires use of debug access sequences is trace configuration. Predefined debug access sequences have two trace-related sequences: TraceStart and TraceStop that are being called when trace is enabled in the project. The TraceStart sequence is executed at the end of the initial debug connection to the target and after device reset while TraceStop is executed at the beginning of debug disconnect.

By default these sequences are empty and often need to be implemented in the .pdsc file to support device-specific behavior, for example to differentiate configuration for 1-pin SWO trace and 5-pin ETM trace (TPIU).

For example:

<sequence name="TraceStart">
<block>
// obtain project trace configuration from global variable __traceout
__var traceSWO = (__traceout &amp; 0x1) != 0;
__var traceTPIU = (__traceout &amp; 0x2) != 0;
</block>
<control if="traceSWO">
<block>
Sequence("EnableTraceSWO");
</block>
</control>
<control if="traceTPIU">
<block>
Sequence("EnableTraceTPIU");
</block>
</control>
</sequence>

Note that the code above uses following features allowed in debug access sequences:

  • read access to a predefined global debug access variable __traceout.
  • implements the pre-defined debug sequence TraceStart
  • calls custom debug sequences EnableTraceSWO ,EnableTraceTPIU

Implementation of custom debug access sequences traceEnableSWO and traceEnableTPIU is a means to better structure the sequence implementations. Their content is highly vendor and device-specific. Common functionality of such sequences is to trace on the device, configure trace clock and assign trace pin(s). But the complexity of the code varies significantly depending on the device functionalities.

Below is a simple example of EnableTraceSWO for Microchip SAMS70 family, that also demonstrates the use of a user-defined global debug access variable (TracePCK3) configurable via a debug configuration file SAMx7.dbgconf. See Enable device-specific debug configurations for additional information about custom global debug variables and *.dbgconf file.

...
<family Dfamily="SAMV70" Dvendor="Microchip:3">
<debugvars configfile="samv70/keil/debug/SAMx7.dbgconf" version="1.0.0">
// Debug Access Variables
__var TracePCK3 = 0x00000000; // Trace Clock Source Selection and Prescaler
</debugvars>
<sequence name="EnableTraceSWO">
<block>
Write32(0x400E06E4, 0x504D4300); // Disable PMC write protection
Write32(0x400E064C, TracePCK3); // Select clock source and prescaler for PCK3
Write32(0x400E0600, (1 &lt;&lt; 11)); // Enable PCK3
</block>
</sequence>
...

Some devices can require that trace clock is enabled already at DebugDeviceUnlock sequence to ensure that access to global trace components is available when reading the ROM table and processor features. In such cases corresponding functionality needs to be moved from TraceStart to DebugDeviceUnlock sequence and check if trace is enabled via the __traceout variable.

Implement reset for debug access

This section explains reset debug sequences for systems with a single CPU. Multi-core specifics are covered in Handle debug in multi-core systems.

Reset is an important part of debug operation and is used to bring the device into a known state from which debug connection can be reliably established. Reset also allows users to debug their code from the very beginning. In the typical case when user initiates a debug session the debugger connects to the device, and resets the processor to ensure its fresh start, and then stops the CPU before user application is started.

Sometimes it is needed to connect to a running target ("hot debug") without any resets performed when establishing a debug connection. Since there is no resets this is out of scope for the current section.

The figure below shows an example reset flow in a debugger (copied from Usage of debug access sequences):

CPU halt and ResetCatchSet

In the flow shown above the debugger first decides whether to halt the processor after the reset or not. This decision depends on the project configuration but also on when and how the reset is requested (automatically by debugger during or after debug connect, or manually by user through IDE, etc.).

If processor halt after reset is needed then ResetCatchSet sequence is executed before performing the reset operation. Default implementation of ResetCatchSet enables and configures Cortex-M Reset Vector Catch functionality so that the core is stopped right after reset thus allowing users to debug the program from the very start. In some cases ResetCatchSet needs to be overwritten, for example for Support bootloader operation.

Reset types

There are 3 predefined reset types and a custom reset type that debugger chooses from when performing a reset. The choice depends on the project configuration and defaultResetSequence value. Corresponding reset debug sequence is executed to perform required reset type.

The reset types are listed below with details described in the referenced documentation.

  • ResetHardware is a system-wide reset without debug domain executed via the dedicated debugger reset line, e.g. nRST.
  • ResetSystem is a software-triggered system-wide reset that preserves established debug connection.
  • ResetProcessor is a software-triggered local reset for a processor only.
  • CustomResetName sequence is used when a user-defined debug sequence is assigned to the defaultResetSequence attribute. This can be implemented when very special reset type is needed that cannot be performed by modifying predefined reset types.

CPU halt and ResetCatchClear

After reset is performed and the processor is halted (on the breakpoint enabled in ResetCatchSet) the ResetCatchClear sequence is executed. The default implementation may need to be overwritten to support bootloader as explaine in Support bootloader operation.

Support bootloader operation

Systems with built-in ROM bootloader often require special handling to ensure that debug is correctly started from the user application.

In particular the reset flow described in Implement reset for debug access most likely needs special adjustments for bootloader operation. After device reset the bootloader gets executed first. The debugger needs to take that into account and stop the processor with a breakpoint just before the application is started. For some devices this is also essential because debug can be disabled during bootloader execution for asset protection purposes.

The default implementation of ResetCatchSet sequence halts the core right after reset. This however would be before the bootloader is started and hence may be not relevant for application development or even not possible to debug if bootloader code is not available.

To overcome this problem the ResetCatchSet sequence needs to be overwritten in the .pdsc file of the Device Family Pack (DFP). In constrast to the default implementation the Reset Vector Catch shall be disabled allowing uninterrupted bootloader execution after reset. To halt the core before the application starts the sequence additionally sets a breakpoint at the Reset Vector, where the execution jumps to after bootloader is finished.

Example 1: ResetCatchSet

The code below gives an example for an Armv8-M system with the vector table placed at address 0x00000000:

<sequence name="ResetCatchSet">
<block>
__var DHCSR_Addr = 0xE000EDF0;
__var DEMCR_Addr = 0xE000EDFC;
__var FP_CTRL_Addr = 0xE0002000;
__var FP_COMP0_Addr = 0xE0002008;
__var FPB_KEY = 0x00000002;
__var FPB_ENABLE = 0x00000001;
__var value = 0;
__var resetVect = 0x00000000;
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value &amp; ~0x00000001)); // Disable Reset Vector Catch
resetVect = Read32(0x00000004); // Read Reset Vector
Write32(FP_COMP0_Addr, (resetVect | FPB_ENABLE)); // Set BP0 to Reset Vector (ARMv8M)
Write32(FP_CTRL_Addr, (FPB_KEY | FPB_ENABLE)); // Enable FPB
</block>
<block>
Read32(DHCSR_Addr); // Read DHCSR to clear potentially set DHCSR.S_RESET_ST bit
</block>
</sequence>

After reset is performed and the processor is halted (on the breakpoint enabled in ResetCatchSet) the ResetCatchClear sequence is executed. There in addition to the default functionality we need to clear the breakpoint introduced in the customized ResetCatchSet sequence.

Example 1: ResetCatchClear

Below is a ResetCatchClear function for an Armv8-M core that corresponds to the ResetCatchSet sequence shown in Example 1: ResetCatchSet:

<sequence name="ResetCatchClear">
<block>
__var DEMCR_Addr = 0xE000EDFC;
__var FP_CTRL_Addr = 0xE0002000;
__var FP_COMP0_Addr = 0xE0002008;
__var FPB_KEY = 0x00000002;
__var value = 0;
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value &amp; ~0x00000001)); // Disable Reset Vector Catch in DEMCR
Write32(FP_COMP0_Addr, 0x00000000); // Clear BP0
Write32(FP_CTRL_Addr, FPB_KEY ); // Disable FPB
</block>
</sequence>

Example 2: ResetCatchSet

In some cases the ResetCatchSet sequence shall behave differently depending on where the obtained Reset Vector is located. Such differentiation can be introduced using XML <control> element. For example Cortex-M0/M0+/M1/M3/M4 cores have a FBP/BPU limitations that doesn't allow to set an FPB breakpoint for code memory above 0x20000000. For systems that have firmware located above this address (mostly in large external flash) the debugger can just rely on the Reset Vector Catch to stop right after reset and can't jump to the reset vector. Here is corresponding debug sequence:

<sequence name="ResetCatchSet">
<block>
__var DHCSR_Addr = 0xE000EDF0;
__var DEMCR_Addr = 0xE000EDFC;
__var FPB_BKPT_H = 0x80000000;
__var FPB_BKPT_L = 0x40000000;
__var FPB_COMP_M = 0x1FFFFFFC;
__var FPB_KEY = 0x00000002;
__var FPB_ENABLE = 0x00000001;
__var value = 0;
__var resetVect = 0x00000000;
// Run over Bootloader
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value &amp; ~0x00000001)); // Disable Reset Vector Catch
Write32(0x40000000, 0x00000002); // Map Flash to Vectors
resetVect = Read32 (0x00000004); // Read Reset Vector
</block>
<control if="resetVect &lt; 0x20000000" info="Set and enable breakpoint">
<block>
//determine if instruction is at upper or lower half-word in an aligned 4-byte block
value = ((resetVect &amp; 0x02) ? FPB_BKPT_H : FPB_BKPT_L) | (resetVect &amp; FPB_COMP_M) | FPB_ENABLE ;
Write32(0xE0002008, value); // Set BP0 to Reset Vector
value = FPB_KEY | FPB_ENABLE;
Write32(0xe0002000, value); // Enable FPB
</block>
</control>
<control if="resetVect &gt;= 0x20000000" info="Enable reset vector catch">
<block>
// Enable Reset Vector Catch in DEMCR
value = Read32(DEMCR_Addr);
Write32(DEMCR_Addr, (value | 0x00000001));
</block>
</control>
<block>
Read32(DHCSR_Addr); // Read DHCSR to clear potentially set DHCSR.S_RESET_ST bit
</block>
</sequence>

Example 2: ResetCatchClear

The ResetCatchClear sequence from Example 1 can also be used with the Example 2: ResetCatchSet as there's no special handling additionally required.

Other modifications

Additionally the reset behavior can be made configurable per project via custom global debug access variables and a *.dbgconf file. See Enable device-specific debug configurations for additional details.

In some cases also the reset sequences (ResetSystem, ResetProcessor, ResetHardware) need to be adjusted to ensure proper bootloader handling. For example for debug authentication or bootloader configuration purposes. The actual implementation is very device and use case specific.

Handle debug in multi-core systems

To correctly debug multicore systems, first of all the debug connection shall be correctly specified using debug. See Specify CPU debug connection for description and examples.

To achieve correct debug operation on a multi-core system often modification of the predefined debug sequences are required. The actual implementation very much depends on the particular system architecture.

The Usage of debug access sequences provides example flows for debugger operation. These flows shall be analyzed for particular system and different implementations may be required for each available core.

Recommendations described in previous sections such as error-handling, trace configuration, bootloader support can be applied for individual cores in the multi-core system as well. Using the Pname identifier in the sequence element it is possible to specify the debug access sequence for a particular core.

The most multi-core Cortex-M systems have their CPUs intended for running different applications and not for load balancing. For simplicity we consider further such an assymmetric (AMP) dual-core system. In this system the CPUs can have either equal roles or master-slave dependancy. The roles can also be either predefined or configurable.

Sections below explain additional use-cases specific for multi-core systems:

Reset sequences

Multi-core devices often have quite unique reset systems that a debugger shall use correctly when connecting to a target and during debug operation. For that the default reset debug sequences (see Implement reset for debug access) need to be overwritten or require processor-specific implementations. Below is an overview for different reset types:

  • ResetHardware is a hardware-triggered system-wide reset and should not be differentiated per individual core. However its default implementation may need to be overwritten in order to take the system configuration into account (master-slave, etc.).
  • ResetSystem is a software-triggered system-wide reset. It is assumed to be applied to the whole system and shouldn't be core-specific. But same as with ResetHardware it may require different implementation, for example to ensure correct reset in master-slave systems.
  • ResetProcessor is a software-triggered local reset for the specified CPU (or if required CPU subsystem). It needs to be differentiated for each core and is done by overwriting predefined ResetProcessor sequence for each CPU. Custom debug access sequences can be used to simplify code structure as shown in the example below:
<sequences>
//-- Begin: ResetProcessor Sequence for Cortex-M4
<sequence name="ResetProcessor" Pname="CM4">
<block>
Sequence("ResetProcessor_CM4");
</block>
</sequence>
//-- Begin: ResetProcessor Sequence for Cortex-M0
<sequence name="ResetProcessor" Pname="CM0plus">
<block>
Sequence("ResetProcessor_CM0plus");
</block>
</sequence>
...
</sequences>

In the example above the reset functionality itself is implemented in the user-defined (custom) debug sequences ResetProcessor_CM0plus and ResetProcessor_CM4.

The same Pname identifier shall be used in sequence element as defined in the corresponding processor element ('CM0plus' or 'CM4' in this example).

Following the same concept the ResetCatchSet and ResetCatchClear sequences may need to be overwritten for individual cores, as reset vectors for different cores are located in different areas and hence the breakpoint for halt after reset shall be set differently. The approach is very similar to the one described in Support bootloader operation.

Debug sequences for different use cases

When debugging an application running on a processor in a multi-core system, it is often required to have special control over the processors in the system. For example in a master-slave system it may be desired to debug only the application on the slave. For that debugger needs to ensure that the slave is running independent from the master. Debug-related sequence DebugCoreStart can be used for that. Below is an example for NXP LPC4300 family, with ReleaseM0OnConnect is a configuration parameter specified via *.dbgconf as explained in Enable device-specific debug configurations.

<sequence name="DebugCoreStart" Pname="Cortex-M0">
<block>
// Default implementation
// Enable Core Debug via DHCSR
Write32(0xE000EDF0, 0xA05F0001);
</block>
<control if="ReleaseM0OnConnect">
<block>
// Release M0 from reset
Write32(0x40053104, 0x00000000); // RESET_CTRL1: Clear M0APP_RST (Bit 24)
</block>
</control>
</sequence>