Debugger-based On-Target Testing
Developer Guide

DOTT Concepts

Before reading this section, it is assumed that you have already read the Component and System Test sections. The developer's guide build on the information form these sections and provides additional technical background which shall enable DOTT users to apply and adapt DOTT for their projects.

Target vs. Host

As a general rule, the DOTT framework uses the notion of target firmware/functions/code/... to refer to all code that is executed on the embedded target device. Likewise, host functions/code/tests/... is used for all code executed on the test host. This separation manifests itself throughout the framework where components such as examples are divided into target and host parts (most often by dedicated target and host folders).

Test Execution Flow and Modes of Operation

The typical sequence of steps when executing a test using DOTT is as follows:

  1. The target gets halted.
  2. The target firmware is loaded onto the target device.
  3. The target is reset.
  4. The target starts to execute until it reaches a special location (DOTT test hook) which is typically called form the application's main function. The target is halted at this location.
  5. Control is passed to the implementation of the current test.
  6. In the test, target functions can be called (see halt mode) or target execution can be resumed while providing external stimuli and inspecting target state at defined locations (see free running mode). At the end of the test, the sequence starts over of for the next test.

Which actions are performed before a test is executed (e.g., target reset) are specified for individual tests by means of PyTest fixtures. In the following example, two fixtures are used: target_load and target_reset.

def test_SystickRunning(self, target_load, target_reset):
# ...

Note that in the PyTest configuration file conftest.py in the examples folder, the target_load and target_reset fixtures are re-directed to target_load_flash and target_reset_flash. This means that the target firmware is loaded into FLASH memory and, after reset, execution is performed from FLASH. Likewise, DOTT provides two fixtures called target_load_sram and target_reset_sram for SRAM firmware loading and execution. Additional, project-specific fixtures can easily be added to conftest.py. It is, for example, recommended to implement target access via external interfaces (e.g., I2C) via a fixture and to use this fixture in the tests. With such abstract communication fixtures, communication interface changes (e.g., using SPI instead of I2C) can easily be propagated to all tests without changing the tests themselves.

Halt Mode - Component Testing

For component testing, DOTT is typically used in 'halt mode' where the target is reset, a binary is downloaded to the target, the target is booted up and then halted in a specific location (typically the DOTT 'test hook'). In this state, DOTT passes control to a test implemented in PyTest. The following figure shows the halt mode style test execution.

Free Running Mode - System Testing

The 'free running' test mode starts the same way as a halt mode test by halting the target, downloading a firmware binary to the target and booting the target. It also hits the DOTT 'test hook' where it passes control to the test runner on the host. Instead of halting the target and then calling selected target functions as in halt mode, the target is allowed to run freely. Before resuming target execution, one or more halt points are set up. Once the target is running, stimuli are provided via the target's public communication interfaces (e.g., I2C, SPI). When one of the halt points is hit, the host-side test can inspect the target's internal state and compare it against the expected state (based on the stimuli provided via the external interface).

Creating a new Test Project

The recommended way to set up a new DOTT-based test project is to start with a template project which you then customize to your need. To do so please follow these steps:

  1. Copy the host folder from the 'standard' template from the templates folder as included in the DOTT docu and examples zip from the GitHub releases page to your desired location.
  2. In host/dott.ini customize at least the following entries:
    1. app_load_elf shall point to the location of the ELF-wrapped binary as downloaded to the target
    2. app_symbol_elf shall point to the ELF file which contains all the debug information
    3. device_name shall be the device name as specified in JLinkDevices.xml

Note: A full list of dott.ini file options is provided here.

Preparing Target Firmware for DOTT

To use DOTT in a firmware project the following steps have to be executed.

  • Link the DOTT test-helper (target/build/dott_library.a) to the project. Alternatively you may want to compile and link testhelpers.c with your firmware project.
  • From the firmware's main function call the DOTT test hook:
    int main(void)
    {
    DOTT_test_hook();
    /* rest of main */
    }
  • Build your project and make sure that you create an ELF (axf) file with full debug symbols (-gdwarf-4). Note that the debug symbols are only used by the host and do not affect target binary size.
  • Create a BIN file of your target firmware. To be loaded via GDB into the target's memory the BIN file needs to be wrapped into an ELF file. This can be achieved using the arm-none-eabi-objcopy command which is included, e.g., in th GNU Arm Embedded Toolchain. For details see the Makefiles included with the DOTT example projects.
  • In DOTT_test_hook (testhelpers.c) a variable called dbg_mem_u32 is defined (on stack) which is used by DOTT for on-target memory allocation in halt mode. By default this memory consist of 64 32bit words (256 bytes). This is usually sufficient but can be adjusted. Note that the dott target library needs to be re-compiled when adjusting this memory size.

To simplify the steps outlined above, a basic template project (templates/standard) is included with DOTT. It includes minimal target firmware for a Cortex-M0 device where the DOTT test hook is called from the main. On the host side it includes a test which reads the Systick counter and checks if it properly increments.

Test Execution

Test execution is performed using PyTest as test runner. For basic test execution, open a Python command prompt (and activate the DOTT venv if using a virtual environment). In the command prompt navigate to the folder containing your tests and executed pytest. PyTest will then collect all tests in this folder (and its sub-folders) and execute them. To list all found tests use the –collect-only argument:

> pytest --collect-only
============================= test session starts =============================
collected 10 items
<Module '02_system_testing/host/test_cntr.py'>
<Class 'TestCounters'>
<Function 'test_SystickRunning'>
<Function 'test_SystickRunningLive'>
<Function 'test_TimerRunning'>
<Function 'test_TimerManualIrq'>
<Module '02_system_testing/host/test_i2c_comm.py'>
<Class 'TestI2cCommunication'>
<Function 'test_RegWriteRead'>
<Function 'test_CmdAdd'>
<Function 'test_CmdUnknown'>
<Function 'test_CmdInjectUnknown'>
<Module '02_system_testing/host/test_sysinit.py'>
<Class 'TestSystemInit'>
<Function 'test_sysinit_bss_section'>
<Function 'test_sysinit_data_section'>

To execute only selected tests instead of all tests used the -k argument. The following example executes all tests which have 'Systick' in their name:

> pytest -k Systick

In addition to this basic string-based filtering, PyTest supports custom markers for tests using the @pytest.mark decorator. PyTest allows users to define arbitrary markers and individual tests can carry multiple such markers. For example, the following test has a marker irq_testing:

@pytest.mark.irq_testing
def test_TimerManualIrq(self, target_load, target_reset, live_access):
# ...
# ...

To execute all tests with the irw_testing marker you can now invoke pytesst with the -m argument:

> pytest -m irq_testing

Jenkins Integration

To run DOTT-based tests in a Jenkins environment, activate the DOTT venv. In a Jekinsfile 'bat' section this can be done as follows:

bat '''
call c:\\YOUR_PATH\\dott_venv\Scripts\activate.bat
cd YOUR_TEST_FOLDER
pytest
REM ...
'''

GDB Server Invocation Strategy

DOTT requires a J-LINK GDB server to interact with the target. There are two options to start the GDB server. First, J-Link GDB server is started manually. When doing so you will see a window as shown below. In this cases you need to specify the IP address of your machine using the gdb_server_addr option in dott.ini. Note that this allows you also to configure DOTT to connect to a remote GDB server.

GDB server window for GDB connected to the reference board.

The second option is to use the GDB server 'auto-start' feature of DOTT. If there is no gdb_server_addr option specified in dott.ini (or DOTT Configuration Reference} "conftest.py") then DOTT tries to auto start the J-Link GDB server. If gdb_server_binary is not specified in dott.ini, DOTT assumes to find the J-Link GDB server in its default install location.

Writing Tests

This section describes how to write tests using DOTT. Tests are implemented in PyTest and hence following the PyTest convention that test functions have to start with test_ to be discovered by PyTest:

def test_MyFirstOne(target_load_flash, target_reset_flash):
# ...

In addition, tests can be grouped using classes and modules.

What you can also see in the above example is that the test takes two test fixtures. In contrast to other test framework where you have dedicated setup/teardown methods for controlling your test environment PyTest uses fixtures. DOTT comes with the following built-in fixtures:

  • target_load_sram: Load firmware binary into target SRAM. Loading is performed per test (and not per session).
  • target_load_flash: Load firmware binary into target FLASH. Loading is performed per test session (and not per test).
  • target_load_flash_always: Load the application binary to the target on a per-test basis (vs. target_load_flash which only loads once per test session)
  • target_reset_sram: Reset the target and prepare it to execute from SRAM (at address 0x20000000).
  • target_reset_flash: Reset target and prepare it to execute from FLASH.
  • target_load_symbols_only: Load application symbols but do not perform target download.
  • live_access: Allows to access target memory while the target is running.

Custom fixtures can be easily added in a project-specific conftest.py. For convenience, the conftest.py in the examples aliases target_load_flash to traget_load and target_reset_flash to target_reset. This convention is also used throughout the examples in this document.

Controlling target execution

When a test which uses target_load and target_reset fixtures is entered, the binary has already been loaded to the target, the target was reset and execution was started and finally the target was halted at the location of the DOTT test hook. It must be emphasized that the target is not running when entering a test. Target execution can be continued with cont() and halted again with halt() as shown below:

def test_MyFirstOne(target_load_flash, target_reset_flash):
# target is halted in DOTT test hook
# let target run (continue execution)
dott().target.cont()
# wait some time...
# halt the target again
dott().target.halt()
if dott().target.is_running():
# Oops - somthing went wrong. Target should be halted...

With target_is_running() you can check if the target is currently running or not.

Calling Target Functions

DOTT allows you to call target functions while the target is halted:

def test_MyFirstOne(target_load_flash, target_reset_flash):
# target is halted in DOTT test hook
dott().target.eval('MyTargetFunction(1, 55)')

If a function expects arguments which are not primitive types but pointers, structs, arrays etc. target memory needs to be allocated and filled with appropriate values before calling the function. DOTT supports this type of on-target memory allocation while the target is halted in the DOTT test hook. This is the case after entering the test as long as target execution was not resumed using cont().

def test_MyFirstOne(target_load_flash, target_reset_flash):
# target is halted in DOTT test hook
# allocate a chunk 32 bytes of target memory
ptr = dott().target.mem.alloc(32)
# more conventient: typed allocation; allocate memory for 2 uint32_t elements
ptr = dott().target.mem.alloc_type('uint32_t', cnt=2)
# set values and perform fucntion call
dott().target.eval(f'{ptr}[0] = 9')
dott().target.eval(f'{ptr}[1] = 12')
res = dott().target.eval(f'example_AdditionPtr(&{ptr}[0], &{ptr}[1])')
# or directly set the vlaues as part of memory allocation
p_a = dott().target.mem.alloc_type('uint32_t', val=9)
p_b = dott().target.mem.alloc_type('uint32_t', val=12)
res = dott().target.eval(f'example_AdditionPtr({p_a}, {p_b})')
# continue target execution. IMPORTANT: dott().target.mem.alloc* is not available beyond this point!
dott().target.cont()

Reading an Writing Target Memory

In DOTT there are multiple ways to access target memory. As a general rule, target memory can only be accessed while the target is halted (with the exception of target live access). Using eval() individual (static) global target variables can be read or written as well as local variables which are int the scope where the target is currently halted. Depending on compiler optimization, some variables might no longer be accessible. Some tricks how to deal with such situations are given in the section on compiler optimization.

def test_MyFirstOne(target_load_flash, target_reset_flash):
# read the value of a global variable and write it back incremented by 1
val = dott().target.eval('some_global_var')
dott().target.eval(f'some_global_var = {val + 1}')

An alternative way to read memory is the target.mem interface. In contract to eval, it is designed for bulk memory access and hence should be preferred in larger amounts of memory need to be transferred to/from the target.

def test_MyFirstOne(target_load_flash, target_reset_flash):
# read an uint32_t array with 256 elements using mem.read
addr = dott().target.eval('my_array_var')
val_bytes = dott().target.mem.read(addr, 256 * 4)
val_ints = DottConvert.bytes_to_uint32(val_bytes)
# write elements to the above uint32_t array
elements = [0, 1, 2, 4, 5, 6, 7, 8, 10]
val_bytes = DottConvert.uint32_to_bytes(elements)
dott().target.mem.write(addr, val_bytes)

Improving Target Memory Access Performance

As shown in the previous section, DOTT offers two ways to access target memory: eval() and mem.read() / mem.write(). While eval() is mor convenient since it also allows direct access to, e.g., struct members its performance is relatively low since every eval() translates into a single USB transaction to the debug probe. In contrast to that mem.read/write() offers much better performance since memory access is performed in bulks. The following example shows how eval based access can be replaced with mem.read/write when dealing with a struct. Performance increase for this example is more than 8. The downside is that the target struct has to be replicated on the host using ctypes. If the declaration of the target struct changes, also the ctypes struct on the host needs to be updated. Note that also aspects such as struct packing and byte order need to be taken into account.

import ctypes
# test demonstrating struct filling using eval
def test_eval(self, target_load, target_reset):
p_dat = dott().target.mem.alloc_type('my_add_t')
# eval-based filling of on-target struct data
dott().target.eval(f'{p_dat}->paddA = {0xff}')
dott().target.eval(f'{p_dat}->a = {0xaabbccdd}')
dott().target.eval(f'{p_dat}->paddB = {0x0}')
dott().target.eval(f'{p_dat}->b = {0xdeadbeef}')
dott().target.eval(f'{p_dat}->paddC = {0x0}')
dott().target.eval(f'{p_dat}->sum = {0x60}')
# test demonstrating struct filling using mem.write and ctypes
def test_ctypes(self, target_load, target_reset):
# Arm Compiler struct packing notes:
# https://developer.arm.com/docs/100748/latest/writing-optimized-code/packing-data-structures
# see ctypes _pack_ attribute for configuration details
# replicate struct on host using ctypes
class HOST_my_add_t(ctypes.LittleEndianStructure):
_pack_ = 0
_fields_ = [('paddA', ctypes.c_uint8),
('a', ctypes.c_uint32),
('paddB', ctypes.c_uint8),
('b', ctypes.c_uint32),
('paddC', ctypes.c_uint8),
('sum', ctypes.c_uint32)
]
# ctypes and mem write based filling of on-target struct data
tmp = HOST_my_add_t(paddA=0xff, a=0xaabbccdd, paddB=0, b=0xdeadbeef, paddC=0, sum=0x60)
tmp_bytes = bytes(tmp)
p_dat = dott().target.mem.alloc(len(tmp_bytes))
dott().target.mem.write(p_dat, tmp_bytes)

Live Access to Target Memory

In addition to target memory access while the target is halted, DOTT allows basic access to target memory while the target is running. This can be useful to monitor (and sometimes plot) target variables/state which might be difficult to monitor (e.g., in a system heavily relying on interrupts halting and reading target memory could severely mess up timing).

To make use of live access, add the live_access fixture to the test as shown below. Currently, the live_access feature has a rather limited interface consisting of just two functions namely mem_read_u32 and mem_write_u32.

The following example code demonstrates how to read the SysTick counter using live access and generate a plot using matplotlib which is stored as a png file.

# code snippet form examples/02_system_testing/test_cntr.py
def test_SystickSampleLive(self, target_load, target_reset, live_access):
def sample_mem_addr(mem_addr: int, duration: float, live: TargetLive) -> (List[float], List[int]):
duration_list: List[float] = []
samples_list: List[int] = []
time_start = time.time()
while (time.time() - time_start) < duration:
duration_list.append(time.time() - time_start)
samples_list.append(live.mem_read_32(mem_addr))
return duration_list, samples_list
dott().target.cont()
addr = dott().target.eval('&_tick_cnt')
(host_time, msecs_samples) = sample_mem_addr(addr, 1.0, live_access)
dott().target.halt()
# plot the data samples from the target
pyplot.clf()
pyplot.plot(host_time, msecs_samples)
pyplot.ylabel('systick')
pyplot.xlabel('host runtime')
pyplot.savefig('test_systick_sample_live', dpi=200)
# pyplot.show() # pops up the plot window - usually not wanted in automated tests

Breakpoint Variants and Data Injection

DOTT supports breakpoints to halt target execution when pre-defined locations are reached. More specifically, DOTT currently offers two different breakpoint variants which have specific use cases (and limitations): HaltPoints and InterceptPoints.

HaltPoints

A halt point comes very close to the behavior of a traditional break point. It can be placed at any accessible symbol of the target binary. When the location specified by the halt point is reached, the target is halted. A test can wait for this event by calling the wait_complete() method of the halt point instance which blocks until the halt point is reached.

def test_MyTest(self, target_load, target_reset):
hp = HaltPoint('app_main')
dott().target.cont()
try:
hp.wait_complete(timeout=5)
except TimeoutError:
# main was not reached within specified time - deal with it!
assert(False)
# target is now halted at beginning of main

Note that it usually is a good practice to specify a timeout for wait_complete to avoid that a test gets stuck if a halt point is not reached as expected. A halt point also offers a reached() method which you can override:

def test_MyTest(self, target_load, target_reset):
class MyHp(HaltPoint):
def reached(self):
log.debug('Hello world - halt point reached!')
hp = MyHp('app_main')
dott().target.cont()
hp.wait_complete()

In the reached() method you have full access to all dott().target functions including memory access and target control meaning that you can also continue target execution. However there is one important limitation of a halt point:

WARNING: If a HaltPoint is hit while a target function is executed which was started using

>dott().target.eval(), the execution of this function is interrupted and can not be resumed. In

‍situations where you want to use a breakpoint without altering the target's execution state use an InterceptPoint!

InterceptPoints

InerceptPoints are breakpoints which allow you to access and modify target memory when they are reached without changing the execution state of the target. Specifically, currently ongoing function calls initiated via dott().target.eval() are not interrupted. This feature of InterceptPoints comes with a limitation: In the reached() method of an InterceptPoint no calls to dott().target functions must be made!. Instead, use the eval() and ret() methods of the InterceptPoint class to access target memory or let the target return from its currently executed function. Other functionality such as bulk memory access (functions from dott().target.mem are also not available in InterceptPoints). The following code snippet shows how to use an InterceptPoint:

# code snippet form exampes/01_component_testing/test_example_functions.py
def test_example_AdditionSubcallsExtIntercept(self, target_load, target_reset):
class BpA(InterceptPoint):
def reached(self):
self.ret(10)
class BpB(InterceptPoint):
def reached(self):
self.eval('*b = 89')
self.eval('*b += 10')
val = self.eval('*b')
assert(val == 99)
self.ret(0)
bpa = BpA('example_GetA')
bpb = BpB('example_GetB')
res = dott().target.eval('example_AdditionSubcalls()')
bpa.delete()
bpb.delete()
assert(109 == res)

In the example above, intercept points are create for target methods example_GetA and example_GetB. When the intercept points are reached the values returned by the function are altered and execution continues. Note that the function call to example_AdditionSubcalls performed via eval() is not interrupted as it would be if halt points would have been used.

Breakpoints at Labels

Halt- and InterceptPoints can be placed at every accessible symbol of the target binary and are triggered upon symbol invocation (i.e., function entry). A more versatile approach is to add DOTT_LABELs to you code which can also be used as Halt- and InterceptPoint locations. DOTT_LABELs can be placed anywhere in the code:

#include "testhelpers.h"
void example(void)
{
/* other code here */
DOTT_LABEL("my_location")
/* even more code here */
}

When creating a Halt- or InterceptPoint in the Python test code, the DOTT_LABEL can be specified:

def test_example(self, target_load, target_reset):
hp = HaltPoint(DOTT_LABEL('my_location'))
# ...
dott().target.cont()
hp.wait_complete()
# ...

Dealing with Interrupt-driven Firmware

When implementing system tests for heavily interrupt-driven firmware locations where Halt- and InterceptPoints are placed have to be chosen wisely as evey Halt- and InterceptPoint influences the target's timing behavior. Breakpoints should ideally only be placed after the events of interest have been fully handled by the target and when it is safe to halt the targeted.

If this is not possible or if it is desired to test functionality closely related to interrupts, a different approach can be employed. The basic idea is to disable the generation of interrupts by peripherals and instead control the generation of interrupts via DOTT. This way, the tests no longer have to deal with potentially (too) high interrupt frequency but interrupt generation can be adapted to a speed which can be handled by the tests.

The following example shows a simple test which checks if a timer on the target is incrementing:

def test_TimerRunning(self, target_load, target_reset):
assert (0 == dott().target.eval('_timer_cnt')), 'Timer count shall initially be zero.'
dott().target.cont()
time.sleep(1)
dott().target.halt()
assert (0 < dott().target.eval('_timer_cnt')), 'Timer count should have advanced while target was running'

If one now wants to test specific aspects of the interrupt handling code (e.g., by injecting data using an InterceptPoint) this would substantially change the target's timing behavior and hence, timer interrupts might be missed and results might be incorrect.

The following example shows an approach which overcomes this limitation by (1) disabling the timer and (2) manually generating timer interrupts by setting the timer's pending bit in the ISPR (interrupt set pending register). In addition, an InterceptPoint is set in the timer's interrupt handler which alters the timer count. The before generating the next manual interrupt, the test waits until the InterceptPoint is completed.

# code snippet from examples/02_system_testing/test_cntr.py
def test_TimerManualIrqAdvanced(self, target_load, target_reset, live_access):
APB1ENR = 0x4002101c # APB1 enable register
ISPR = 0xE000E200 # interrupt set pending register
# wait until the target has reached function app_main
hp = HaltPoint('app_main')
dott().target.cont()
hp.wait_complete()
hp.delete()
# Custom InterceptPoint which performs an additional counter increment.
class MyIp(InterceptPoint):
def reached(self):
self.eval('_timer_cnt++')
ip_tmr = MyIp(DOTT_LABEL('TIM7_IRQHandler_End'))
# disable the clock for TIM7 such that it does not fire anymore and reset the timer counter to zero
dott().target.eval(f'*{APB1ENR} &= ~0x20')
dott().target.eval('_timer_cnt = 0')
# let target run and ensure that the timer counter is still zero
dott().target.cont()
time.sleep(1)
dott().target.halt()
timer_cnt = dott().target.eval('_timer_cnt')
assert(0 == timer_cnt), 'Expected timer count to be 0'
# let target run again and via live target access set the TIM7 interrupt pending 4 times (i.e., 'manually'
# trigger the TIM7 interrupt)
dott().target.cont()
for i in range(4):
live_access.mem_write_32(ISPR, [0x000040000])
ip_tmr.wait_complete()
# halt target and check that timer count actually is 8 (interrupt was raised 4 times but our intercept point
# does an additional increment for _timer_cnt for each interrupt and hence the timer count should be 8)
dott().target.halt()
timer_cnt = dott().target.eval('_timer_cnt')
assert(8 == timer_cnt), 'Expected timer count to be 8'

Logging

DOTT comes with its own logger with is using Python's logging facility. To access DOTT's logger import it as follows:

from dottmi.utils import log
def test_LogTesting(self, target_load, target_reset):
log.debug('...')

Compiler Optimization

High compiler optimization levels (e.g., O3 or Oz) and link-time optimization can substantially change the binary's structure versus the code's structure. This can make setting breakpoints or inspecting variable content difficult (if, e.g., some variables are optimized out entirely). To somewhat reduce these problems (usually on the expense of code size), DOTT comes with some macros which might be helpful to tweak the source code to enable/simplify test development.

With the DOTT_NO_OPTIMIZE macro a function can be excluded entirely from optimization:

#include "testhelpers.h"
uint32_t DOTT_NO_OPTIMIZE example(void)
{
/* ... */
}

With the DOTT_NO_INLINE macro a function be prevented from being inlined:

#include "testhelpers.h"
uint32_t DOTT_NO_INLINE example(void)
{
/* ... */
}

With DOTT_VAR_KEEP a variable, which would otherwise be optimized out by the compiler, can be preserved:

#include "testhelpers.h"
uint32_t example(void)
{
/* ... */
DOTT_VAR_KEEP(var_name);
/* ... */
}

Note that it is recommended to use the macros discussed above instead of directly using, .e.g., __attribute__((noinline)). The rationale is that DOTT comes with implementations of these macro for all compilers supported by DOTT.

C++ Scope Resolution

If the target project is a C++ project, some extra care must be taken related to the scope resolution operator (::). When, e.g., calling a method of a static class called Bar in namespace Foo this is done like this:

dott().target.eval("'Foo::Bar::my_function'(...)"

Please note that single quotes are required around namespace, class name and function name.

Customize Target Connect Sequence

If you need to customize the target connection sequence, DOTT provides hooks via the DootHooks class. Via this class you are able to specify callback hooks which are called by DOTT. To date, the following hooks ara available:

  • pre_connect_hook The registered hook callback function is invoked before DOTT attempts to connect to the target. This allows users to, e.g., enable the target's debug channel.

Segger J-Link FLASH cache

The Segger JLINK software caches content of memory regions such as FLASH which it assumes to not change during runtime. However, this assumption might not be correct if, for example, the target uses an on-device bootloader to updated FLASH content via, e.g., I2C. This flash cache can be disabled as follows:

dott().target.cli_exec('monitor exec ExcludeFlashCacheRange 0x08000000-0x09000000')

Re-building the Example Target Firmware and DOTT Library

The example firmware and the DOTT library for the target reference board is provided in binary and source code formats. The binaries enable users to directly run the examples without the need to first set up a build environment and compile the source code for the reference target. All native code comes with Makefiles and Arm Compiler 6 is used to compile the code.

To compile the code on the Windows command line, you need an environment which includes the GNU make, a shell such as busybox and the Arm Compiler 6 command line version. In such an environment, you can now navigate to the folder where you have unpacked the DOTT docu and examples zip archive. Build the examples and the DOTT library by calling make.

DOTT Configuration Reference

DOTT uses two locations for configuration. First, in a folder containing tests there usually is a dott.ini file which is described here. The content of the dott.ini is parsed on startup and stored in an internal data structure called DottConf. The names of the configuration options stored in the DottConf structure match those from the dott.ini. If no dott.ini file can be found, the DottConf is filled with default values.

In conftest.py (PyTest configuration) which usually is also located in the test folder or a parent folder, the content of the DottConf structure can be altered programmatically. Here you can override settings in DottConf or set options based on the host name the tests are executed on. This is quite handy to have one configuration file with different config options depending on if the file is executed on the developer PC or a Jenkins slave. The example and template projects coming with DOTT include conftest.py files for your reference.

DOTT Ini / DottConf Settings

  • app_load_elf shall point to the location of the ELF-wrapped app binary as downloaded to the target
  • app_symbol_elf shall point to the app ELF file which contains all the debug information
  • bl_load_elf shall point to the location of the ELF-wrapped bootloader binary (if any) as downloaded to the target
  • bl_symbol_addr address for the bootloader symbol file in hex (with 0x prefix)
  • bl_symbol_elf shall point to the bootloader ELF file (if any) which contains all the debug information
  • device_endianess endianess of the target device. 'little' (default) or 'big'
  • device_name shall be the device name as specified in JLinkDevices.xml
  • gdb_client_binary GDB client binary. if omitted the default one coming with the DOTT runtime is used
  • gdb_server_addr shall be the IP address where the J-Link GDB server is running. Omit if you want DOTT to auto-start the GDB server locally.
  • gdb_server_binary server binary coming with a J-Link installation. auto-detected if omitted.
  • gdb_server_port port when connecting to a remote GDB server. Using default (2331) if omitted.
  • jlink_interface interface between debug probe and target. SWD (default) or JTAG
  • jlink_serial serial number of the J-Link to use. useful if more than one J-Link is connected
  • jlink_speed interface speed for the J-Link debug probe (consult Segger manual for more information)
  • jlink_server_addr shall be the IP address where the J-Link server is running. Omit if you want DOTT to auto-start the JLINK server locally.
  • jlink_server_port port when connecting to a remote JLINK server. Using default (19020) if omitted.
  • jlink_scrip J-Link script file executed upon connection to the target.
  • jlink_extconf Extra J-Link config options.