Adding Target Drivers

External target API

You can find the public API in target.h, used by the GDB server. Nothing defined in src/target outside of this header should be accessed from outside of src/target/*

Internal target API

There are several internal API headers:

  • target/target_internal.h This header contains structure definitions and convenience functions for use in target support implementations. Specific target implementations fill in the function pointers in these structures for their device specific implementations.

  • target/target_probe.h This header contains declarations for all the *_probe functions so they’re all declared in one central place. This header works in tandem with target/target_probe.c to allow any target support implementation to be switched off as-needed without this breaking the build.

Raw JTAG Devices

Supported JTAG devices are defined in target/jtag_devs.c

The .handler function is called when a device’s ID Code matches the .idcode field with .idmask applied. It is the responsibility of the handler function to instantiate a new target with target_new and fill in the access function pointers.

Raw JTAG access

There is a global structure of function pointers, jtag_proc defined in jtagtap.h, which is automatically initialised to provide access to raw JTAG bus access routines:

typedef struct jtag_proc {
    /* Reset the bus */
    void (*jtagtap_reset)(void);
    /* Step into the next TMS state */
    bool (*jtagtap_next)(bool tms, bool tdi);
    /* Step through a sequence of TMS states (up to 32) */
    void (*jtagtap_tms_seq)(uint32_t tms_states, size_t clock_cycles);
    /* Write a number of bits from data_in to TDI, reading the responses back into data_out from TDO */
    void (*jtagtap_tdi_tdo_seq)(uint8_t *data_out, bool final_tms, const uint8_t *data_in, size_t clock_cycles);
    /* Same as the previous function but without the read-back part */
    void (*jtagtap_tdi_seq)(bool final_tms, const uint8_t *data_in, size_t clock_cycles);
    /* Runs a series of clock cycles on the bus after establishing an initial TMS + TDI state */
    void (*jtagtap_cycle)(bool tms, bool tdi, size_t clock_cycles);
} jtag_proc_s;

There are also some helper macros defined for running certain known sequences onto the bus:

/* Perform a soft reset of the bus */
#define jtagtap_soft_reset() jtag_proc.jtagtap_tms_seq(0x1fU, 6U)
/* From bus idle, clock into the Shift-IR state */
#define jtagtap_shift_ir() jtag_proc.jtagtap_tms_seq(0x03U, 4U)
/* From bus idle, clock into the Shift-DR state */
#define jtagtap_shift_dr() jtag_proc.jtagtap_tms_seq(0x01U, 3U)
/* Return the bus to idle from one of the capture states */
#define jtagtap_return_idle(cycles) jtag_proc.jtagtap_tms_seq(0x01U, (cycles) + 1U)

TAP-layer access

There are higher level access functions defined in target/jtag_scan.h.

These functions provide higher level access to the JTAG IR and DR registers:

/* Write the dev_index'th device's IR to the value in `ir` */
void jtag_dev_write_ir(uint8_t dev_index, uint32_t ir);
/*
 * Write the dev_index'th device's DR with the data sequence pointed to by data_in,
 * Reading the current DR value back in data_out. Either can be NULL to allow only-readout and only-write operations.
 */
void jtag_dev_shift_dr(uint8_t dev_index, uint8_t *data_out, const uint8_t *data_in, size_t clock_cycles);

ARM implementations (ADIv5)

There are a few moving parts to the ADIv5 (ARM Debug Interface v5) implementation. The most important ones are:

  • The debug interface logic itself found in target/adiv5.c.

  • The generic logic for Cortex-M parts which is found in target/cortexm.c. Please note, this presently supports the ARMv6-M and ARMv7-M profiles only.

  • The generic logic for Cortex-A parts which is found in target/cortexa.c. Please note, this presently supports the ARMv7-M profile only.

ADIv5 Coresight identification

adiv5.c implements not just logic for accessing the Debug Port and Access Port components of an ADIv5 Coresight interface over either JTAG or SWD, but also implements the generic identification logic for devices using this specification.

When a device is identified during scan that talks ADIv5, the various Coresight CIDR and PIDR values get read out automatically and decoded. A list of known component class values can be found at the top of adiv5.c and a list of known JEP-106 manufacturer codes (encoded in the PIDR register for each ROM table chunk that must be read) can be found in the target/adiv5.h header.

Once an AP has been identified as belonging to either a Cortex-M or a Cortex-A core, the ADIv5 code dispatches to cortexm_probe or cortexa_probe accordingly.

Cortex-M device handling

Special consideration is made for ARM’s JEP-106 which represents an ARM Cortex device which has not had its ROM tables customised by the device manufacturer. When the Cortex-M support encounters a device like this in cortexm_probe, the ARM part ID retrieved from the ROM tables is used to identify which type of Cortex-M core is being probed, and further part identification is then dispatched on the core type.

In either this case or the normal manufacturer-specific JEP-106 case, we then dispatch to one of a number of _probe routines that are then used to specifically identify the part and, on creating a positive part identification, configure any device-specific behaviour that needs to occur via the target_s structure, and if possible define any known RAM and Flash memory regions via target_add_ram and target_add_flash (the latter typically gets wrapped in a target-specific helper).

Once a probe routine creates a positive identification on a part and configures the target structure and memory regions, it must return true to halt the probing process for the AP. If a probe routine fails to create a positive identification, it must return false as soon as possible.

Flash programming

None of the generic targets are able to provide a generic way to erase or write Flash, as this is implemented differently for each target device. Instead, as part of the writing target support, you must supply suitable Flash erase and write routines. The target layer then interacts with the Flash of your target through these routines, which may even be specific to specific Flash regions depending on how the Flash/NVM controller in the target device works.

Configuration of these routines is achieved by constructing target_flash_s structures, the layout of which is provided below with member documentation. Once the structure has been filled in with the necessary information for the target Flash region, target_add_flash must then be called to register the region against the target.

If your target requires additional data not present in the target_flash_s structure, it is permitted to write a structure that wraps target_flash_s to add the additional members needed. An example of this for the RP2040 support follows:

typedef struct rp_flash {
    /* This member is what gets passed to `target_add_flash` to register the region */
    target_flash_s f;
    /*
     * RP2040 being Flashless can have an arbitrary Flash programming page size based on the attached
     * SPI Flash device, this field stores what that discovered size is for use in write operations
     */
    uint32_t page_size;
    /* Likewise the instruction to issue to the SPI Flash to erase a sector is device-specific */
    uint8_t sector_erase_opcode;
} rp_flash_s;

static void rp_add_flash(target_s *target)
{
    /* Allocate the device-specific structure on the heap (this allocates the target Flash structure too */
    rp_flash_s *flash = calloc(1, sizeof(*flash));
    if (!flash) { /* calloc failed: heap exhaustion */
        DEBUG_WARN("calloc: failed in %s\n", __func__);
        return;
    }

    [...]

    /* Grab a member pointer to the target Flash structure */
    target_flash_s *const target_flash = &flash->f;
    [...]
    /* Register with the target structure */
    target_add_flash(target, target_flash);
    [...]
}

bool rp_probe(target_s *target)
{
    [...]
    /*
     * We can't know the Flash region layout head of time, so we override the target `attach` behaviour
     * and perform RAM and Flash region registration on attach
     */
    target->attach = rp_attach;
    [...]
    return true;
}

static bool rp_attach(target_s *target)
{
    /*
     * RP2040 is a Cortex-M device, so we *must* call the normal Cortex-M attach routine and
     * propagate errors. It is an error to not do this step somewhere in the target-specific attach routine.
     */
    if (!cortexm_attach(target) || !rp_read_rom_func_table(target))
        return false;

    /*
     * Because we are in attach, which can be called multiple times for a device, we *must*
     * free any existing map before rebuilding it. Failure to do so will result in unpredictable behaviour.
     */
    target_mem_map_free(target);
    rp_add_flash(target);
    target_add_ram(target, RP_SRAM_BASE, RP_SRAM_SIZE);

    return true;
}

The generic target Flash structure is defined as follows:

typedef struct target_flash target_flash_s;

typedef bool (*flash_prepare_func)(target_flash_s *flash);
typedef bool (*flash_erase_func)(target_flash_s *flash, target_addr_t addr, size_t len);
typedef bool (*flash_write_func)(target_flash_s *flash, target_addr_t dest, const void *src, size_t len);
typedef bool (*flash_done_func)(target_flash_s *flash);

typedef struct target_flash {
    target *t;                   /* Target this Flash is attached to */
    target_addr_t start;         /* Start address of Flash */
    size_t length;               /* Flash length */
    size_t blocksize;            /* Erase block size */
    size_t writesize;            /* Write operation size, must be <= blocksize/writebufsize */
    size_t writebufsize;         /* Size of write buffer, this is calculated and not set in target code */
    uint8_t erased;              /* Byte erased state */
    uint8_t operation;           /* Current Flash operation (none means it's idle/unprepared) */
    flash_prepare_func prepare;  /* Prepare for flash operations */
    flash_erase_func erase;      /* Erase a range of flash */
    flash_write_func write;      /* Write to flash */
    flash_done_func done;        /* Finish flash operations */
    void *buf;                   /* Buffer for flash operations */
    target_addr_t buf_addr_base; /* Address of block this buffer is for */
    target_addr_t buf_addr_low;  /* Address of lowest byte written */
    target_addr_t buf_addr_high; /* Address of highest byte written */
    target_flash_s *next;        /* Next Flash in the list */
};

Skeleton Driver

Below is a skeleton for adding support for a new target. Please note that it is preferred to forward declare the Flash routines and define them after *_add_flash and *_probe. Functionally it makes no difference, but this improves the navigability of the resulting target support.

/* Declare the license you wish to use here */

#include "general.h"
#include "target.h"
#include "target_internal.h"

static bool skeleton_flash_erase(target_flash_s *flash, target_addr_t addr, size_t length);
static bool skeleton_flash_write(target_flash_s *flash, target_addr_t dest, const void *src, size_t length);

static void skeleton_add_flash(target_s *target)
{
    target_flash_s *flash = calloc(1, sizeof(*flash));
    if (!flash) { /* calloc failed: heap exhaustion */
        DEBUG_WARN("calloc: failed in %s\n", __func__);
        return;
    }

    flash->start = SKELETON_FLASH_BASE;
    flash->length = SKELETON_FLASH_SIZE;
    flash->blocksize = SKELETON_BLOCKSIZE;
    flash->erase = skeleton_flash_erase;
    flash->write = skeleton_flash_write;
    flash->erased = 0xffU;
    target_add_flash(target, flash);
}

bool skeleton_probe(target_s *target)
{
    /* Positively identify the target device somehow */
    if (target_mem_read32(target, SKELETON_DEVID_ADDR) != SKELETON_DEVID)
        return false;

    target->driver = "skeleton partno";
    /* Add RAM mappings */
    target_add_ram(target, SKELETON_RAM_BASE, SKELETON_RAM_SIZE);
    /* Add Flash mappings */
    skeleton_add_flash(target);
    return true;
}

static bool skeleton_flash_erase(target_flash_s *flash, target_addr_t addr, size_t length)
{
    [...]
}

static bool skeleton_flash_write(target_flash_s *flash, target_addr_t dest, const void *src, size_t length)
{
    [...]
}

In addition to this, you must declare your new probe routine in target/target_probe.h, and also define a weak linked stub for it in target/target_probe.c

The existing stubs should serve as a decent example for how to do this.

If you wish your new target support to provide functionality like mass erase, there are members in the target structure such as t->mass_erase specifically for this and should be populated in your probe routine. Similarly, if you wish to add custom commands for your target, you need to build a command_s structure array at the top of your target support implementation and register it in the probe routine with target_add_commands(). An example of how to define this custom command block follows:

const struct command_s stm32f1_cmd_list[] = {
    {"option", stm32f1_cmd_option, "Manipulate option bytes"},
    {NULL, NULL, NULL},
};

An example registration call has this form: target_add_commands(target, stm32f1_cmd_list, target->driver);