Skip to content

Firmware Design Deep-Dive

Overview

The c7x-firmware is a host-DSP compute service for the TI AM67A (J722S) that enables Linux applications running on the ARM A53 to offload ML inference and data processing to the C7x DSP. It communicates over RPMessage IPC and transfers data through shared DDR memory allocated from a DMA heap carveout.

The primary use case is running TVM-compiled neural network models on the C7x DSP. The TVM C static lib backend produces a relocatable C7x ELF module (lib0.out) with model weights embedded in a .rodata.weights section. The firmware's dynamic loader (DLOAD) loads this ELF at runtime, resolves symbols against the firmware's export table, and makes the model's entry point callable via an INFER command from the host.

For build instructions, deployment, CLI usage, and troubleshooting, see Deploying Firmware and Firmware Architecture.


System Architecture

+------------------------------------------------------------------+
|                    Linux/ARM (A53)                                |
|  +------------------------------------------------------------+  |
|  |  c7x_compute CLI / Library                                 |  |
|  |  - Allocates shared DDR via /dev/dma_heap                  |  |
|  |  - Sends commands via RPMessage (/dev/rpmsg*)              |  |
|  |  - Discovers rpmsg_ctrl by device tree address             |  |
|  +------------------------------------------------------------+  |
+------------------------------------------------------------------+
                          |
                ====================== RPMessage (virtio/rpmsg_char)
                          |
+------------------------------------------------------------------+
|                    C7x DSP (7e000000.dsp)                        |
|  +------------------------------------------------------------+  |
|  |  Compute Service (FreeRTOS)                                |  |
|  |  - RPMessage endpoint 20 ("rpmsg_chrdev")                  |  |
|  |  - DLOAD dynamic loader for C7x ELF modules               |  |
|  |  - TVM model manager for weights/constants                 |  |
|  |  - INFER pipeline: resolve entry, call cg_main_dsp         |  |
|  |  - Clean shutdown with remoteproc ACK                      |  |
|  +------------------------------------------------------------+  |
+------------------------------------------------------------------+

Component Roles

ARM Client / Library (arm/): User-space Linux C++14 application that allocates four independent dmabufs from the DMA heap carveout -- staging (480 MB, ELF + weights, fixed at the pool base) and printf_buf (64 KB) at c7x_client_open(), plus input_buf/output_buf (sized from the loaded module's tvm_dsp_io_meta, or via c7x_client_reserve_io()) at c7x_client_dyn_load() -- each with its own fd, /dev/remoteprocN attachment, and independent DMA_BUF_IOCTL_SYNC. See ARM Host Client Internals for the full buffer table and capacity-declaration semantics. The library provides a C API (c7x_compute_client.h with extern "C" linkage) and the CLI wraps it for interactive use. Resource management uses RAII wrappers (raii.h) for file descriptors, mmap regions, and FILE handles.

Compute Service (dsp/src/compute_service.c): FreeRTOS task that blocks on RPMessage_recv(), dispatches messages by type, and sends responses. Handles PING, STATUS, SET_PRINTF_BUF, DYN_LOAD, MODEL_LOAD, INFER (and INFER_LARGE), MODEL_UNLOAD, and DYN_UNLOAD commands. Each INFER/INFER_LARGE request carries the sender's input_dsp_addr/ input_size/result_dsp_addr/result_size -- the current output_buf window is tracked in g_result_dsp_addr/g_result_size since the output-placement code is too deep in the call chain to thread the request through. On DYN_UNLOAD, orchestrates TVM runtime cleanup (register file, model slot, constants) before freeing module memory.

Dynamic Loader (dsp/src/dyn_loader.c + dsp/src/dload/): Wraps TI's DLOAD library to load relocatable C7x ELF modules into DDR at runtime. Provides a symbol export table (119 symbols) so loaded modules can call firmware-provided functions (C library, TVM runtime, math).

TVM Model Manager (dsp/src/tvm_model.c): Manages model weights/constants that are either embedded in the ELF .rodata.weights section or loaded separately via the MODEL_LOAD command. Parses the TVM weights binary format and constructs DLTensor descriptors.

Shared Protocol (common/c7x_compute_protocol.h): Header shared between ARM and DSP defining all message structures, types, status codes, and memory layout constants.


Message Protocol

All communication uses a binary message protocol over RPMessage with a maximum message size of 512 bytes. Every message starts with a 16-byte header:

struct c7x_msg_hdr {
    uint32_t type;      // Message type (C7X_MSG_*)
    uint32_t seq;       // Sequence number for correlation
    uint32_t len;       // Total message length including header
    int32_t  status;    // Response status (0 = success)
};

Response types are 0x1000 | request_type.

Message Types

Type Code Direction Purpose
PING 0x0001 Host -> DSP Connectivity test
GET_STATUS 0x0003 Host -> DSP Get service status
DYN_LOAD 0x0010 Host -> DSP Load ELF module
DYN_UNLOAD 0x0012 Host -> DSP Unload module
MODEL_LOAD 0x0020 Host -> DSP Load weights/constants
INFER 0x0021 Host -> DSP Run inference (descriptors inline, fits 512-byte rpmsg)
MODEL_UNLOAD 0x0022 Host -> DSP Unload model weights
INFER_LARGE 0x0023 Host -> DSP Run inference with too many inputs to fit inline
SET_PRINTF_BUF 0x0030 Host -> DSP Rebind DSP printf to the client's printf_buf dmabuf

INFER Message Structure

Since the per-buffer dmabuf change (protocol v3.0.0), every INFER/INFER_LARGE request carries the sender's input_buf/output_buf window explicitly -- there is no fixed C7X_RESULT_ADDR anymore, since output_buf is a separate, independently-allocated dmabuf whose DSP address can differ per client:

struct c7x_msg_infer {
    struct c7x_msg_hdr hdr;
    uint32_t module_handle;      // From DYN_LOAD response
    uint32_t model_id;           // From MODEL_LOAD response
    uint32_t num_inputs;
    uint32_t flags;               // bits[15:0]=repeat count, bit16=KV_RESIDENT
    uint64_t input_dsp_addr;     // Sender's input_buf base
    uint64_t input_size;         // Sender's input_buf declared capacity
    uint64_t result_dsp_addr;    // Sender's output_buf base
    uint64_t result_size;        // Sender's output_buf declared capacity
    struct c7x_tensor_desc inputs[1]; // Variable-length
};

Each tensor descriptor (80 bytes) contains the DSP virtual address of the data in shared memory, data size, dtype, and shape (up to 6 dimensions). The INFER response includes output tensor descriptors written by the DSP, cycle count, and printf_size indicating the number of bytes of printf output in the shared memory printf buffer.

When too many inputs to fit their descriptors inline in the 512-byte rpmsg buffer (e.g. KV-cache LLMs with 60+ tensors), the host writes the descriptor array into input_buf instead and sends the more compact C7X_MSG_INFER_LARGE, which carries descs_addr/descs_size (where the descriptor array landed in input_buf) alongside the same input_dsp_addr/input_size/result_dsp_addr/result_size fields as c7x_msg_infer.

Wire Format Examples

DYN_LOAD request (32 bytes):

Offset  Field           Value
  0     type            0x00000010  (C7X_MSG_DYN_LOAD)
  4     seq             <n>
  8     len             32
 12     status          0
 16     elf_size        <bytes>
 20     flags           0
 24-31  reserved        0

DYN_LOAD_RESP (68 bytes):

Offset  Field           Value
  0     type            0x00001010  (C7X_MSG_DYN_LOAD_RESP)
  4     seq             <n>  (matches request)
  8     len             68
 12     status          0   (C7X_STATUS_SUCCESS)
 16     module_handle   1   (opaque integer)
 20     text_size       0   (set by DLIF callbacks; currently always reported 0)
 24     data_size       0   (currently always reported 0)
 28     oom_requested   0   (status==ERR_NOMEM only; 0 otherwise)
 32     oom_free        0   (status==ERR_NOMEM only)
 36     oom_total       0   (status==ERR_NOMEM only)
 40     io_input_bytes  <bytes>  (from tvm_dsp_io_meta; 0 if module has none)
 48     io_output_bytes <bytes>
 56     io_num_inputs   <n>
 60     io_num_outputs  <n>
 64     io_flags        0   (bit0 = sizes exact, not just an upper bound)

INFER request (single input = 144 bytes):

Addresses come from whichever input_buf/output_buf this client allocated at c7x_client_dyn_load() time -- there is no fixed constant to show here; <input_buf.dsp_addr> and <output_buf.dsp_addr> stand in for those per-session values.

Offset  Field                Value
  0     type                 0x00000021  (C7X_MSG_INFER)
  4     seq                  <n>
  8     len                  144
 12     status               0
 16     module_handle        1
 20     model_id             0  (0 = use embedded weights)
 24     num_inputs           1
 28     flags                0  (bits[15:0]=repeat count, bit16=KV_RESIDENT)
 32     input_dsp_addr       <input_buf.dsp_addr>
 40     input_size           <input_buf declared capacity>
 48     result_dsp_addr      <output_buf.dsp_addr>
 56     result_size          <output_buf declared capacity>
 64     inputs[0].data_addr  <input_buf.dsp_addr> + data_offset  (DSP virtual)
 72     inputs[0].data_size  <bytes>
 80     inputs[0].ndim       4
 84     inputs[0].dtype_code 2  (kDLFloat)
 88     inputs[0].dtype_bits 32
 92     inputs[0].reserved   0
 96     inputs[0].shape[0]   1
104     inputs[0].shape[1]   3
112     inputs[0].shape[2]   224
120     inputs[0].shape[3]   224
128-143 (remaining shape)    0

INFER_RESP (single output = 148 bytes):

Offset  Field                   Value
  0     type                    0x00001021  (C7X_MSG_INFER_RESP)
  4     seq                     <n>  (matches request)
  8     len                     148
 12     status                  0   (C7X_STATUS_SUCCESS)
 16     return_value            0   (cg_main_dsp return value)
 20     cycles                  <64-bit TSC delta>   (8 bytes)
 28     num_outputs             1
 32     printf_size             0   (or N if -profile-layers was set)
 36     descs_addr              0   (0 = inline; non-zero = out-of-band, in output_buf)
 44     descs_size              0
 48     oom_requested           0   (status==ERR_NOMEM only)
 52     oom_free                0   (status==ERR_NOMEM only)
 56     oom_total               0   (status==ERR_NOMEM only)
 60     result_required         0   (status==ERR_SIZE only: capacity the
                                     overflowing tensor actually needed)
 68     outputs[0].data_addr    <output_buf.dsp_addr>
 76     outputs[0].data_size    <bytes>
 84     outputs[0].ndim         2
 88     outputs[0].dtype_code   2   (kDLFloat)
 92     outputs[0].dtype_bits   32
 96     outputs[0].reserved     0
100     outputs[0].shape[0]     1
108     outputs[0].shape[1]     1000
116-147 (remaining shape)       0

Inference Flow — End-to-End Walk-Through

This section traces the exact code path for a single inference request — from the ARM Linux application calling into the host client library, through the RPMessage IPC boundary, across to the C7x DSP firmware, and back.

Key Source Files

File Side Role
firmware/c7x/arm/include/c7x_compute_client.h ARM Public C API
firmware/c7x/arm/src/c7x_compute_client.cpp ARM Client implementation
firmware/c7x/arm/src/c7x_compute_cli.cpp ARM c7x_compute CLI tool
firmware/c7x/common/c7x_compute_protocol.h both Shared message structs
firmware/c7x/dsp/src/compute_service.c DSP Service loop + handlers
firmware/c7x/dsp/src/dyn_loader.c DSP DLOAD ELF loader
firmware/c7x/dsp/src/tvm_model.c DSP Weights/constants manager

Phase 0: Connection Setup

ARM — c7x_client_open()

Source: firmware/c7x/arm/src/c7x_compute_client.cpp:304

  1. Open RPMessage channel via rpmsg_open(C7X_DEVICE_ADDR, C7X_SERVICE_ENDPOINT, C7X_SERVICE_NAME).
  2. C7X_DEVICE_ADDR = "7e000000.dsp" — stable device-tree address.
  3. C7X_SERVICE_ENDPOINT = 20 — the well-known endpoint announced by firmware.

  4. Protocol version handshake via a PING round-trip, comparing the firmware's major version against C7X_SERVICE_VERSION (currently v3.0.0, the per-buffer dmabuf protocol). A major mismatch aborts open() with an explicit message instead of failing later, deeper, with something that reads like an unrelated bug.

  5. Allocate the staging dmabuf first, covering both the ELF/weights staging region and the KV region (C7X_STAGING_SIZE + C7X_KV_SIZE = 480 MB) in one allocation. The carveout heap is gen_pool_first_fit, so allocating this one first guarantees it lands at the pool's low end -- its DSP address then equals the fixed C7X_STAGING_ADDR, which is what keeps the DSP-side KV logic (still addressed at the fixed C7X_KV_ADDR) working unchanged:

client->dma_heap_fd = open("/dev/dma_heap/carveout_vision_apps_shared-memories", ...);
alloc_dmabuf(client, C7X_STAGING_SIZE + C7X_KV_SIZE, &client->staging);
// client->staging.dsp_addr should now equal C7X_STAGING_ADDR
  1. Allocate the printf dmabuf (64 KB, client->printf_buf) the same way -- its own fd, mmap, and /dev/remoteprocN attachment.

  2. Rebind DSP printf to client->printf_buf via C7X_MSG_SET_PRINTF_BUF, sent once here before any DYN_LOAD. Before this rebind, the DSP's boot-time shm_printf_init() targets a scratch address at the very top of the whole shared carveout (C7X_PRINTF_BUF_BOOT_ADDR) -- nothing else claims that address until the host starts allocating, so it's safe as a placeholder.

input_buf/output_buf are not allocated here -- they're sized from the loaded module's declared I/O metadata at DYN_LOAD time (Phase 1), since that's the earliest point their required capacity is known. Each dmabuf gets its own /dev/remoteprocN attachment (RPROC_IOC_DMA_BUF_ATTACH) translating its physical address to a DSP virtual address via the linear carveout mapping; the underlying rproc_fd for each must stay open for the buffer to remain visible to the DSP.

DSP — compute_service_init()

Source: firmware/c7x/dsp/src/compute_service.c:1553

The firmware was already started via remoteproc. On boot it:

  1. Creates an RPMessage endpoint at C7X_SERVICE_ENDPOINT = 20.
  2. Announces "rpmsg_chrdev" to Linux — creates /dev/rpmsg* character device.
  3. Calls dyn_loader_init() to initialise the DLOAD ELF loader.
  4. Calls tvm_model_init() to initialise the weights/constants manager.
  5. Calls shm_printf_init() to redirect DSP printf to its boot-time scratch address (C7X_PRINTF_BUF_BOOT_ADDR), rebound to the client's real printf_buf once SET_PRINTF_BUF arrives.
  6. Sets gServiceRunning = 1 and enters compute_service_run().

See §IPC Details for RPMessage configuration parameters and shutdown sequence.


Phase 1: Load ELF Module

ARM — c7x_client_dyn_load()

Source: firmware/c7x/arm/src/c7x_compute_client.cpp:612

// 1. Read ELF file into the staging dmabuf (shared DDR, visible to DSP)
stage_file(client, elf_file, &file_size);
sync_input_to_device(client->staging);   // DMA_BUF_SYNC_END|SYNC_WRITE: flush ARM cache

// 2. Build and send IPC message
struct c7x_msg_dyn_load req = {
    .hdr.type = C7X_MSG_DYN_LOAD,
    .hdr.seq  = ++client->seq,
    .hdr.len  = sizeof(req),
    .elf_size = file_size,
};
send_and_recv(client, &req, sizeof(req), &resp, sizeof(resp));
*handle_out = resp.module_handle;

// 3. Record the declared input_buf/output_buf capacity from tvm_dsp_io_meta
//    (0 if the module has none), and grow input_buf/output_buf to at least
//    that size if they aren't already -- the only reallocation this design
//    performs, and only safe because no pointer into either buffer has
//    been handed out yet for this load.
client->io_input_bytes = resp.io_input_bytes;
client->io_output_bytes = resp.io_output_bytes;
ensure_io_capacity(client, resp.io_input_bytes, resp.io_output_bytes);

// 4. Reserve the descriptor region at the front of input_buf, sized from
//    the declared input count (or a small fallback if no io_meta) --
//    tensor data for a normal INFER is staged right after it.
client->input_data_offset = round_up_64(num_inputs_for_descs * sizeof(struct c7x_tensor_desc));

A declaration ensure_io_capacity() can't fully satisfy only warns -- it doesn't fail the load. tvm_dsp_io_meta is an upper bound over calling conventions, not a single call's real requirement (a KV-resident caller diverts its KV outputs to the fixed C7X_KV_ADDR and never touches output_buf for them, so it can need far less than the signature implies). The caller can still declare its real per-call need with c7x_client_reserve_io() before the first CreateInput()/INFER.

DSP — handle_dyn_load()

Source: firmware/c7x/dsp/src/compute_service.c:202

// 1. Load ELF from the staging buffer (fixed at C7X_STAGING_ADDR)
dyn_loader_load(C7X_STAGING_ADDR, req->elf_size, &handle);
//   DLOAD: parse ELF, allocate segments in DDR heap,
//   apply C7x relocations, resolve imported symbols against the
//   119-symbol firmware export table.
//   NOTE: .rodata segments are mapped IN-PLACE from the staging buffer.

// 2. Look up the TVM-generated entry point
dyn_loader_query_symbol(handle, "cg_main_dsp", &sym_addr);
g_cg_main_dsp = (cg_main_dsp_fn)(uintptr_t)sym_addr;

// 3. Check for embedded weights
dyn_loader_query_symbol(handle, "_binary_weights_bin_start", &ws_addr);
dyn_loader_query_symbol(handle, "_binary_weights_bin_size",  &wz_addr);
tvm_model_load_weights(ws_addr, *(uint32_t*)wz_addr, &g_embedded_model_id);

// 4. Check for embedded I/O metadata (tvm_dsp_io_meta) and report its
//    declared input_buf/output_buf capacity in the response. A missing
//    symbol, bad magic, or unsupported version reports zero -- treated
//    as "no metadata" by the host, not an error.
resp->io_input_bytes = resp->io_output_bytes = 0;
// ... dyn_loader_query_symbol(handle, "_binary_tvm_dsp_io_meta_start", ...) ...

// 5. Save pool watermark for workspace reclaim after inference
tvm_dsp_save_infer_watermark();

send_response(C7X_MSG_DYN_LOAD_RESP, ...);

Phase 2: Run Inference

ARM — c7x_client_infer()

Source: firmware/c7x/arm/src/c7x_compute_client.cpp:1055 (thin wrapper around c7x_client_infer_impl() at line 779)

Step A — Stage input tensor data into input_buf, after the descriptor region:

// Zero-copy path: if inputs[i].data already falls within input_buf's
// range (c7x::Module::CreateInput()), skip the memcpy and derive the DSP
// address directly from the pointer offset instead.
data_offset = client->input_data_offset;
for (int i = 0; i < num_inputs; i++) {
    if (/* inputs[i].data is already inside input_buf */) {
        data_addrs[i] = client->input_buf.dsp_addr + (inputs[i].data - input_base);
        continue;
    }
    memcpy(input_base + data_offset, inputs[i].data, inputs[i].data_size);
    data_addrs[i] = client->input_buf.dsp_addr + data_offset;
    data_offset += inputs[i].data_size;
}

KV-resident inputs (data == NULL, used with C7X_INFER_FLAG_KV_RESIDENT) skip staging entirely -- their address is derived from the fixed KV region layout (C7X_KV_ADDR + kv_idx * C7X_KV_TENSOR_SIZE) instead.

Step B — Choose INFER or INFER_LARGE and send:

// Inline INFER fits when the header (64 bytes) + N tensor descriptors
// (80 bytes each) fits the 512-byte rpmsg buffer -- a handful of tensors.
// Larger models (e.g. 60+ KV tensors) use INFER_LARGE instead, which
// stages the descriptor array at the front of input_buf and sends a
// compact fixed-size message referencing it via descs_addr/descs_size.
sync_input_to_device(client->input_buf);

req->hdr.type        = C7X_MSG_INFER;      // or C7X_MSG_INFER_LARGE
req->module_handle   = module_handle;
req->model_id        = model_id;           // 0 → use embedded weights
req->num_inputs      = num_inputs;
req->flags           = flags;              // bits[15:0]=repeat, bit16=KV_RESIDENT
req->input_dsp_addr  = client->input_buf.dsp_addr;
req->input_size      = client->input_buf.size;
req->result_dsp_addr = client->output_buf.dsp_addr;
req->result_size     = client->output_buf.size;
for (int i = 0; i < num_inputs; i++)
    req->inputs[i] = desc_arr[i];   // inline form only
send_and_recv(client, req, req_size, resp, sizeof(resp_buf));

Steps C-D — Back from send_and_recv():

// Sync output_buf regardless of status -- an out-of-band descs_addr from
// a KV-resident call can be present either way.
sync_output_from_device(client->output_buf);

// Output tensor data_addr values are already DSP addresses inside THIS
// client's output_buf, so no offset arithmetic against a shared constant
// is needed -- just translate through output_buf's own mmap base.
for (int i = 0; i < resp->num_outputs; i++) {
    uint64_t offset = td_base[i].data_addr - client->output_buf.dsp_addr;
    outputs[i].data = (uint8_t *)client->output_buf.ptr() + offset;
}

// Read DSP printf output (layer profiles, if -profile-layers was set)
if (resp->printf_size > 0)
    fwrite(printf_buf_ptr, 1, resp->printf_size, stderr);

*cycles = resp->cycles;

DSP — handle_infer()

Source: firmware/c7x/dsp/src/compute_service.c:826

Step A — Record the sender's buffer window, then validate:

// Every output-placement call below is too deep in the call chain to
// thread the request through, so it reads these globals instead.
g_result_dsp_addr = req->result_dsp_addr;
g_result_size     = req->result_size;

if (!host_window_ok(req->input_dsp_addr, req->input_size) ||
    !host_window_ok(req->result_dsp_addr, req->result_size)) {
    resp->hdr.status = C7X_STATUS_ERR_ADDR;   // window outside shared carveout
    goto done;
}

Steps B-D — Resolve entry point, resolve constants, cache-invalidate inputs:

if (g_cg_main_dsp == NULL)
    dyn_loader_query_symbol(req->module_handle, "cg_main_dsp", &sym_addr);

uint32_t eff_model_id = req->model_id;
if (eff_model_id == 0 && g_embedded_model_id != 0)
    eff_model_id = g_embedded_model_id;
tvm_model_get_constants(eff_model_id, &constants, &num_constants);

for (i = 0; i < req->num_inputs; i++) {
    CacheP_inv((void *)(uintptr_t)td->data_addr, td->data_size, CacheP_TYPE_ALL);
    // Build zero-copy NDArray pointing into shared DDR
    ndarrays[i].data = (void *)(uintptr_t)td->data_addr;
    anys[i] = { .type_index = kTVMFFITensor, .v_ptr = &ndarrays[i] };
}

Step E — Call the TVM-generated entry point (optionally in a repeat loop, per flags bits[15:0], for profiling):

shm_printf_reset();
start_cycles = __TSC;

ret = g_cg_main_dsp(input_anys,    // TVMFFIAny[] — input NDArrays
                    num_inputs,
                    constants,      // TVMFFIAny[] — weight NDArrays
                    &output_any);   // TVMFFIAny * — output

end_cycles = __TSC;
resp->cycles = end_cycles - start_cycles;

g_cg_main_dsp is generated by src/target/c_static_lib/. Its signature is:

int cg_main_dsp(TVMFFIAny *inputs, int num_inputs,
                TVMFFIAny *constants, TVMFFIAny *output);

Step F — Extract and stage output tensors into the caller's output_buf (via g_result_dsp_addr/g_result_size):

extract_infer_output(&output_any, resp);
// kTVMFFITensor (single): copy to output_buf + CacheP_wb
// kTVMFFIArray  (multi):  pack consecutively + CacheP_wb; too many to fit
//                         inline writes the descriptor array into
//                         output_buf too (resp->descs_addr != 0)

resp->printf_size = shm_printf_finish();
send_response(C7X_MSG_INFER_RESP, ...);

Phase 3: Unload Module

ARM — c7x_client_dyn_unload()

Source: firmware/c7x/arm/src/c7x_compute_client.cpp:745

req.hdr.type      = C7X_MSG_DYN_UNLOAD;
req.module_handle = handle;
send_and_recv(client, &req, sizeof(req), &resp, sizeof(resp));

input_buf/output_buf are not freed or resized here -- they persist across load/unload cycles (only growing, never shrinking) so a back-to-back reload of a similarly-sized module doesn't pay a reallocation. client->input_data_offset and the capacity lock are reset so the next DYN_LOAD can re-declare them.

DSP — handle_dyn_unload()

The DSP cleanup must follow a strict ordering — see §Dynamic Module Loading §Module Unload Lifecycle for the detailed rationale. The code sequence is:

tidl_bridge_cleanup();                // if the module has this symbol (TIDL): release DMA/IALG/MMA state first
TVMDSPRegFileCleanup();               // drops refs to last inference outputs
TVMDSPRegFileInit(NULL, 0);           // reset reg_file pointer before .bss is freed
tvm_model_unload(g_embedded_model_id);// frees model slot before constants
TVMDSPConstantsCleanup();             // frees constants memory pools
dyn_loader_unload(req->module_handle);// frees .text/.data/.bss/.rodata
tvm_dsp_reset_pools();                // reclaim fragmented DDR heap
g_cg_main_dsp = NULL;

This ordering is unaffected by the per-buffer dmabuf change -- it's about the loaded module's own .bss-resident register file, not about which dmabuf holds inference data.


Phase 4: Disconnect

ARM — c7x_client_close()

RAII destructors clean up all four dmabufs (staging, printf_buf, input_buf, output_buf) and the RPMessage connection, each releasing its own fd, /dev/remoteprocN attachment, and mmap in reverse order:

// Per DmaBuf, for each of staging / printf_buf / input_buf / output_buf:
close(rproc_fd)    // unregisters that buffer's DMA attachment
munmap(ptr, size)
close(dmabuf_fd)   // releases the dmabuf reference

close(dma_heap_fd)
close(rpmsg_fd)

Complete Sequence Diagram

 ARM Linux                 RPMessage IPC            C7x FreeRTOS
 ─────────                 ─────────────            ────────────
 c7x_client_open()
   open RPMsg fd
   PING version handshake
   alloc staging dmabuf (480 MB, fixed at C7X_STAGING_ADDR)
   alloc printf_buf dmabuf (64 KB)
   ──── C7X_MSG_SET_PRINTF_BUF ────────────────────►
                                                    compute_service_init()
                                                      RPMessage_construct(ep=20)
                                                      RPMessage_announce("rpmsg_chrdev")
                                                      dyn_loader_init()
                                                      tvm_model_init()
                                                      shm_printf_init() → boot scratch addr
                                                      → compute_service_run() loop
                                                    handle_set_printf_buf()
                                                      shm_printf_rebind(printf_buf)
   ◄─── C7X_MSG_SET_PRINTF_BUF_RESP ────────────────

 c7x_client_dyn_load("lib0.out")
   fread ELF → staging dmabuf
   DMA_BUF_SYNC_WRITE (flush)
   ──── C7X_MSG_DYN_LOAD ──────────────────────────►
                                                    handle_dyn_load()
                                                      dyn_loader_load(C7X_STAGING_ADDR)
                                                        parse ELF, alloc DDR segments
                                                        apply C7x relocations
                                                        resolve symbols (119-symbol table)
                                                      query_symbol("cg_main_dsp")
                                                      tvm_model_load_weights()
                                                      read tvm_dsp_io_meta (if present)
                                                      tvm_dsp_save_infer_watermark()
   ◄─── C7X_MSG_DYN_LOAD_RESP ─────────────────────
   handle=1, io_input_bytes=N, io_output_bytes=M
   ensure_io_capacity() → alloc/grow input_buf, output_buf

 c7x_client_infer(handle=1, model_id=0, ...)
   memcpy inputs → input_buf[input_data_offset..]
   DMA_BUF_SYNC_WRITE (flush input_buf)
   ──── C7X_MSG_INFER ─────────────────────────────►
        input_dsp_addr=input_buf.dsp_addr, result_dsp_addr=output_buf.dsp_addr
        inputs[0].data_addr = input_buf.dsp_addr + data_offset
                                                    handle_infer()
                                                      g_result_dsp_addr = req->result_dsp_addr
                                                      host_window_ok(input, result)
                                                      resolve g_cg_main_dsp
                                                      get_constants(model_id)
                                                      CacheP_inv(input regions)
                                                      shm_printf_reset()
                                                      start_cycles = __TSC
                                                      g_cg_main_dsp(inputs, N,
                                                                     constants,
                                                                     &output_any)
                                                        ← TVM kernels execute ──►
                                                      end_cycles = __TSC
                                                      extract_infer_output()
                                                        write into output_buf @ g_result_dsp_addr
                                                        CacheP_wb(output data)
                                                      shm_printf_finish()
   ◄─── C7X_MSG_INFER_RESP ────────────────────────
        cycles=N, num_outputs=1
        outputs[0].data_addr = output_buf.dsp_addr + offset
   DMA_BUF_SYNC_READ (invalidate output_buf)
   outputs[0].data = output_buf.ptr() + offset

 c7x_client_dyn_unload(handle=1)
   ──── C7X_MSG_DYN_UNLOAD ────────────────────────►
                                                    handle_dyn_unload()
                                                      tidl_bridge_cleanup() (if present)
                                                      TVMDSPRegFileCleanup()
                                                      tvm_model_unload()
                                                      TVMDSPConstantsCleanup()
                                                      dyn_loader_unload()
                                                      tvm_dsp_reset_pools()
   ◄─── C7X_MSG_DYN_UNLOAD_RESP ───────────────────
   input_buf/output_buf kept (only grow, never freed here)

 c7x_client_close()
   close each of staging/printf_buf/input_buf/output_buf:
     close(rproc_fd)  ← unmaps that DMA buf from DSP
     munmap(ptr)
     close(dmabuf_fd)

CLI One-Liner: c7x_compute run

The c7x_compute run command wraps the full load → infer → unload sequence:

c7x_compute run lib0.out --input input.bin --output output.bin \
                --shape 1,3,224,224 --dtype float32

Internally calls: c7x_client_open → dyn_load → infer → dyn_unload → close, then writes all output tensors to the output file.

Output (stdout, parseable by Python):

{"status":"ok","cycles":12345678,"num_outputs":1,"outputs":[
  {"index":0,"ndim":2,"dtype_code":2,"dtype_bits":32,"data_size":4000,"shape":[1,1000]}
]}


Memory Architecture

Physical DDR Layout

The AM67A has DDR starting at 0x80000000. All addresses below 4 GB are identity-mapped on the DSP unless otherwise noted.

DSP Firmware Regions (reserved by device tree)

Region Physical/Virtual Size Cache Purpose
IPC/DMA 0xAD000000 1 MB Non-cached (MAIR4) Linux IPC area
Resource table 0xAD100000 1 KB Non-cached remoteproc resource table
IPC trace 0xAD100400 ~1023 KB Non-cached DebugP_log trace buffer (trace0)
Boot code 0xAD200000 1 KB Cached (MAIR7) C7x boot vector
Vectors 0xAD400000 16 KB Cached Interrupt vectors
Secure vectors 0xAD600000 16 KB Cached Secure interrupt vectors
Code/Data 0xAD604000 ~34 MB Cached (MAIR7) Firmware .text, .data, .bss
IPC VRing 0xAF800000 8 MB Non-cached (MAIR4) RPMessage virtio rings

Shared Compute Buffer (DMA heap carveout)

Since the per-buffer dmabuf change (protocol v3.0.0), this 512 MB carveout is no longer one dmabuf split at fixed offsets -- it's independently allocated, independently-synced dmabufs, only two of which have a fixed address:

Buffer DSP Virtual Physical Size Cache Purpose
Staging (client->staging) 0xC0000000 (C7X_STAGING_ADDR), fixed 0x900000000 468 MB Cached (MAIR7) ELF modules (DLOAD) + weights (MODEL_LOAD) only -- not inference input tensors
KV cache region 0xDD400000 (C7X_KV_ADDR), fixed 0x91D400000 12 MB Cached (MAIR7) Persistent KV cache across inferences, when C7X_INFER_FLAG_KV_RESIDENT is set. Reserved as part of the same 480 MB client->staging allocation, not a separate dmabuf
Printf buffer (client->printf_buf) Dynamic; boots to 0xDFFF0000 scratch until rebound Dynamic 64 KB Cached (MAIR7) DSP printf output. Its own dmabuf, rebound via C7X_MSG_SET_PRINTF_BUF right after c7x_client_open()
Input buffer (client->input_buf) Dynamic Dynamic Declared from tvm_dsp_io_meta, or via c7x_client_reserve_io() Cached (MAIR7) Inference input tensors + a descriptor region at the front. Grows (never shrinks) across loads
Output buffer (client->output_buf) Dynamic Dynamic Same sizing rule as input_buf Cached (MAIR7) Inference output tensors

client->staging is allocated first (covering both the staging and KV regions, 480 MB in one dmabuf) precisely so the carveout's gen_pool_first_fit allocator packs it at the pool's low end -- that's what keeps C7X_STAGING_ADDR/C7X_KV_ADDR valid as fixed addresses without the host needing to steer the allocator (gen_pool has no such API). printf_buf, then input_buf/output_buf, are allocated afterward and pack into whatever's left above that 480 MB (roughly 32 MB, minus 64 KB for printf_buf).

This carveout is the vision_apps_shared-memories DMA heap, exclusively for host-DSP communication. The host allocates each dmabuf via /dev/dma_heap/carveout_vision_apps_shared-memories, and the DSP MMU maps the whole carveout at 0xC0000000 with write-back cached attributes (MAIR7, Outer Shareable); each dmabuf's DSP virtual address is its physical address translated through that one linear mapping.

(468 + 12 + 32 = 512 MB against common/c7x_compute_protocol.h's C7X_STAGING_SIZE/C7X_KV_SIZE defines and the ~32 MB remaining for printf_buf/input_buf/output_buf. The physical base above [0x900000000] is an 8 GB-board value (e.g. j722s-evm); 4 GB boards like BeagleY-AI use 0x8a0000000 instead, via C7X_SHARED_PHYS_BASE.)

Extended DDR (above 4 GB, MMU-translated)

Region DSP Virtual Physical Size Cache Purpose
Non-cacheable heap 0x100000000 0x880000000 32 MB Non-cached (MAIR4) DMA-accessible allocations
TVM DDR heap 0x102000000 0x882000000 352 MiB Cached (MAIR7) DLOAD segments + TVM workspace

The TVM DDR heap is where DLOAD allocates code and data segments for loaded modules, and where the TVM runtime allocates workspace tensors during inference.

L2 SRAM (C7x-local, not DDR)

Region Address Size Purpose
L2 main 0x7E000000 2 MB Runtime stack, FreeRTOS heap
L2 aux 0x7F000000 240 KB Fast scratch memory
L1 alias 0x7F03C000 16 KB L1 cache as SRAM

Host-Side Allocation

The host allocates each dmabuf independently at runtime from the DMA heap, via the shared alloc_dmabuf() helper (client->staging and client->printf_buf at c7x_client_open(); input_buf/output_buf later, at c7x_client_dyn_load()):

1. open("/dev/dma_heap/carveout_vision_apps_shared-memories")   -- once, shared
2. ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, {len=<this buffer's size>}) -> dma_buf_fd
3. mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, dma_buf_fd, 0) -> userspace ptr
4. open("/dev/remoteprocN")                                     -- own attachment per buffer
5. ioctl(this_buf_rproc_fd, RPROC_IOC_DMA_BUF_ATTACH) -> physical address
6. dsp_addr = phys_addr - C7X_SHARED_PHYS_BASE + C7X_SHARED_BASE

Each buffer's rproc_fd must remain open for the lifetime of that buffer — closing it destroys that buffer's DMA attachment and makes it invisible to the DSP, independent of the other three buffers.

Cache Coherency

All cached shared regions use MAIR7 (Write-Back Read/Write-Allocate) with Outer Shareable for hardware cache coherency between ARM and DSP. The host still performs explicit DMA_BUF_SYNC ioctls: - Before DSP reads: DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE (flush ARM cache) - Before host reads: DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ (invalidate ARM cache)

MMU Configuration

Defined in dsp/configs/c75ss0.syscfg (16 regions). Key MAIR attribute indices:

MAIR Encoding Meaning Used For
MAIR0 0x00 Device-nGnRnE (strongly ordered) Peripherals, CLEC, DRU, L2SRAM
MAIR4 0x29 Normal non-cacheable IPC VRing, DMA buffers
MAIR7 0x3D Write-Back Read/Write-Allocate Code, data, shared buffer, TVM heap

All cached regions (MAIR7) use Outer Shareable for hardware cache coherency.

Memory Budget Example (ResNet-18)

Resource Size Pool Limit
lib0.out ELF (code + 46 MB weights) ~47 MB client->staging 468 MB
DLOAD segments (relocated code+data) ~47 MB TVM DDR heap 352 MiB
Inference workspace (intermediate tensors) ~10-20 MB TVM DDR heap ~305 MiB remaining
Input tensor (1,3,224,224 float32) 0.6 MB client->input_buf Declared from tvm_dsp_io_meta
Output tensor (1,1000 float32) 4 KB client->output_buf Declared from tvm_dsp_io_meta

Memory Address Map Summary

Physical (ARM)           DSP Virtual         Content
─────────────────────    ─────────────────   ────────────────────────────────
0x900000000              0xC0000000          client->staging base (480 MB total,
                                              fixed -- covers both regions below)
  + 0 .. elf_size          ..                ELF bytes (DLOAD in-place rodata)
  + elf_size ..            ..                weights (MODEL_LOAD), if not embedded
0x91D400000              0xDD400000          KV cache region (12 MB, fixed, part of
                                              the same staging dmabuf), when
                                              C7X_INFER_FLAG_KV_RESIDENT
(dynamic, packs above staging)               client->printf_buf (64 KB) -- own
                                              dmabuf; boots to a scratch address at
                                              carveout top (0xDFFF0000) until
                                              SET_PRINTF_BUF rebinds it
(dynamic, packs above the above)             client->input_buf -- own dmabuf, sized
                                              from tvm_dsp_io_meta; descriptor
                                              region at the front, then input
                                              tensors
(dynamic, packs above the above)             client->output_buf -- own dmabuf,
                                              same sizing rule; output tensors
0x882000000              0x102000000         TVM DDR heap (352 MiB)
  + 0 ..                   ..                DLOAD code/data segments
  + segment_end ..         ..                TVM workspace tensors

Unlike the pre-v3.0.0 layout, input_buf/output_buf/printf_buf have no fixed address or size -- each session's actual addresses come back in DYN_LOAD_RESP (declared capacity) and are carried explicitly in every INFER/INFER_LARGE request's input_dsp_addr/result_dsp_addr fields, not derived from a shared constant.


Dynamic Module Loading (DLOAD)

The firmware embeds TI's DLOAD dynamic linker, which loads standard C7x ELF relocatable objects at runtime. This is the mechanism by which TVM-compiled models are deployed without reflashing firmware.

DLOAD Integration

The dyn_loader.c module provides: - DLIF callbacks: Firmware-side implementations of the DLOAD loader interface (DLIF_allocate, DLIF_copy, DLIF_read, etc.) that allocate from the TVM DDR heap via tvm_dsp_alloc - Symbol export table: 119 symbols (C library, TVM runtime, VM builtins, DSP kernels, MMALIB wrappers, math) made available to loaded modules at relocation time - Load/unload API: dyn_loader_load() takes an ELF from the staging buffer (client->staging); dyn_loader_unload() frees all segments

Supported Relocations

DLOAD handles 20 C7x-specific relocation types (defined in dload/C70_DLOAD_REL/c70_reloc.c), covering all relocations produced by the TI CGT C7000 compiler for position-dependent code with external symbol references.

Symbol Export Table

The export table in dyn_loader.c provides these categories to loaded modules:

  • C library: printf (redirected to shm_printf), memcpy, memset, malloc, free, calloc, __c7xabi_cmpd, etc.
  • TVM runtime: TVMBackendAllocWorkspace, TVMBackendFreeWorkspace, TVMFuncCall, TVMArgs_Create, etc.
  • VM builtins (packed): vm_builtin_* functions using TVMArgs
  • VM builtins (direct C++ API): tvm_dsp_* direct-call variants for the C7x C++ API backend
  • Math: expf, logf, sqrtf, powf, floorf, fmaxf, etc.

Module Unload Lifecycle

When the host sends DYN_UNLOAD, handle_dyn_unload() must clean up TVM runtime state before calling dyn_loader_unload(). This ordering is critical because the loaded module's .bss section contains the static register file used by cg_main_dsp, and dyn_loader_unload() frees all ELF segments including .bss.

The cleanup sequence (see §Inference Flow §Phase 3 for the annotated code):

  1. tidl_bridge_cleanup() (if the loaded module exports this symbol) — frees TIDL subgraph instances so TIDL can release DMA channels, IALG memory, and MMA state before the module's code/data sections are freed. Looked up via dyn_loader_query_symbol(); a no-op when the module has no TIDL subgraphs (e.g. MMALIB-only builds).

  2. TVMDSPRegFileCleanup() — Iterates the static register file (in the module's .bss), decrements reference counts on heap-allocated NDArray/storage objects from the last inference, and calls their deleters to free TVM DDR heap memory.

  3. tvm_model_unload(model_id) — Frees the model slot in the g_models[] table (up to MAX_MODELS=4). Must happen before constants cleanup because TVMDSPConstantsCleanup() resets the constants subsystem state that the model slot references.

  4. TVMDSPConstantsCleanup() — Frees all constants memory pools allocated by TVMDSPLoadConstants() during the INFER setup phase, and resets the constants subsystem (g_initialized = 0).

  5. dyn_loader_unload(handle) — Calls tracked_free_all() to free all ELF segments (.text, .data, .bss, .rodata.weights) from the TVM DDR heap.

  6. Clear state — Reset g_loaded_module_handle, g_cg_main_dsp, and g_embedded_model_id to zero.

If steps 2-4 are skipped or performed after step 5: the register file memory is already freed (use-after-free), model slots leak preventing new models from loading after 4 iterations, and constants memory pools leak reducing available TVM DDR heap. Skipping step 1 on a module with TIDL subgraphs leaks DMA channels and IALG memory instead.

Symbol Table Synchronization

When building TVM-compiled modules as DLOAD-loadable ELFs, the module's linker script must import matching symbol names so that DLOAD can resolve them at load time. See src/runtime/ti_dsp/dynmod/ for the linker script templates.


IPC Details

RPMessage Configuration

Parameter Value
Service Name "rpmsg_chrdev" (announced)
Endpoint 20
Max Message Size 512 bytes
VRing Location 0xAF800000

Initialization Sequence

See §Inference Flow §Phase 0 for the annotated compute_service_init() walk-through. The key steps are:

  1. RPMessage_waitForLinuxReady() — polls resource table until Linux initializes virtio vrings
  2. IpcNotify_registerClient(IPC_NOTIFY_CLIENT_ID_RP_MBOX, ...) — register shutdown callback
  3. RPMessage_construct() — create endpoint 20
  4. RPMessage_announce("rpmsg_chrdev") — announce service to Linux
  5. compute_service_run() — blocking message loop

Clean Shutdown

  1. Linux sends IPC_NOTIFY_RP_MBOX_SHUTDOWN via mailbox
  2. ISR callback calls compute_service_stop() which calls RPMessage_unblock
  3. Service loop exits, sends IPC_NOTIFY_RP_MBOX_SHUTDOWN_ACK from task context
  4. Tears down RPMessage endpoint and drivers
  5. Disables interrupts and halts with IDLE instruction

The ACK must be sent before RPMessage_destruct() because that disrupts the virtio transport needed for the mailbox ACK to reach the kernel.

Device Discovery

Both the deploy script and host library discover hardware dynamically by matching the device tree address 7e000000.dsp in sysfs. The host scans /sys/class/rpmsg/rpmsg_ctrlN/device paths for a symlink resolving to this address. This approach is robust across reboots where remoteproc/rpmsg indices may change.


Shared Memory Printf

The DSP's printf output is redirected to a 64 KB region in shared DDR -- its own independent dmabuf (client->printf_buf) since the per-buffer dmabuf change, rebound from its boot-time scratch address via C7X_MSG_SET_PRINTF_BUF once the host connects (see Phase 0 above). This replaces the older approach of writing to the DebugP trace buffer (trace0), which was limited to ~2 KB and could not hold profile output for models with many layers (e.g. CLISTA-DoA produces ~12 KB of profile data for 156 layers).

Architecture

The shm_printf module (dsp/src/shm_printf.c) uses the TI C7000 compiler's add_device() RTS mechanism (Section 7.2.4 of the C7000 Compiler User's Guide) to register a custom I/O device named "shmout" that writes to shared memory. At init time, freopen() redirects stdout through this device, so all standard output functions (printf, fprintf(stdout, ...), fputs, puts) write to the shared buffer.

Two output paths coexist:

  1. Direct path (shm_printf): The DLOAD symbol alias maps the loaded module's printf calls directly to shm_printf(), which does vsnprintf into the buffer. Fast path, avoids FILE* overhead.

  2. Device driver path (SHM_write): The add_device write callback handles any output going through the stdio FILE* machinery (e.g. fprintf(stdout, ...) from runtime code).

Buffer Layout

client->printf_buf is a dedicated 64 KB dmabuf (C7X_PRINTF_BUF_SIZE). Before the host's first SET_PRINTF_BUF, the DSP targets a boot-time scratch address at the very top of the shared carveout (C7X_PRINTF_BUF_BOOT_ADDR = C7X_SHARED_BASE + C7X_SHARED_SIZE - 64 KB = 0xDFFF0000) -- a placeholder nothing else claims that early:

Offset  Size    Field
0       4       magic (0x50524E54 = "PRNT")
4       4       wr_index (bytes written since last reset)
8       4       buf_size (usable text area = 64K - 16)
12      4       reserved
16      ...     text data

Data Flow

No RPMsg is sent per printf call. The flow during inference is:

  1. Before inference: shm_printf_reset() sets wr_index = 0
  2. During inference: DSP printf writes directly to SHM buffer via memcpy
  3. After inference: shm_printf_finish() calls CacheP_wb(), returns byte count
  4. INFER response: resp->printf_size carries the byte count in the single RPMsg
  5. Host reads: printf_buf gets its own sync_output_from_device() call, independent of output_buf's -- only when resp->printf_size > 0, so a normal (non-profiling) inference skips that DMA_BUF_IOCTL_SYNC entirely

Buffer overflow is handled by silent truncation — excess data beyond the 64 KB text area is dropped without error.


Key Design Decisions

Decision Rationale
Four independent dmabufs (staging, printf_buf, input_buf, output_buf) instead of one carveout-wide buffer Each DMA_BUF_IOCTL_SYNC has no offset/length, so sync granularity is allocation granularity -- one dmabuf per purpose means a small inference doesn't pay for syncing the whole carveout, and input_buf/output_buf can be sized (and grown) per model instead of living inside a fixed split
staging allocated first, covering both the staging and KV regions The carveout heap is gen_pool_first_fit; allocating this one first guarantees it lands at the pool's low end, keeping C7X_STAGING_ADDR/C7X_KV_ADDR valid as fixed addresses without needing to steer the allocator
input_dsp_addr/result_dsp_addr carried explicitly in every INFER/INFER_LARGE request input_buf/output_buf have no fixed address once they're independent, per-session dmabufs -- the DSP can't assume a shared constant, so the sender's actual buffer window is part of the wire format
input_buf/output_buf only grow, never shrink or move once capacity-locked Avoids reallocating (and re-validating every outstanding pointer into) a buffer that's already been handed out for CreateInput()/an in-flight INFER
Descriptor region reserved at the front of input_buf, sized from the declared input count DLOAD maps .rodata in-place from staging; placing input data (and, for INFER_LARGE, the descriptor array) in the separate input_buf instead of after the ELF avoids ever touching embedded weights
rproc_fd held open per dmabuf for the lifetime of that buffer Closing a buffer's rproc_fd destroys only that buffer's DMA attachment, making it invisible to the DSP independent of the other three
119 imported symbols resolved at DLOAD time Eliminates per-call FFI overhead; cg_main_dsp calls TVM runtime functions via direct function pointer
__TSC (64-bit hardware TSC) for cycle counting Available on C7x without kernel support; avoids FreeRTOS tick resolution limits; won't wrap at ~4.3s like a 32-bit counter
Explicit DMA_BUF_SYNC ioctls despite MAIR7 Outer Shareable ARM and DSP have separate cache hierarchies; explicit sync is required for correct coherency — hardware coherency at MAIR7 is between ARM cores, not between ARM and DSP

Build System Internals

DSP Firmware

CMake-based cross-compilation using the TI CGT C7000 toolchain. The toolchain file (cmake/toolchain-c7000.cmake) sets paths to the MCU+ SDK, CGT compiler, and SysConfig tool. The linker script (configs/linker_c75_freertos.cmd) defines all memory sections including the DLOAD code/data placement.

SDK Dependencies: - TI MCU+ SDK 11_00_00_06 - TI CGT C7000 5.0.1 LTS - TI SysConfig 1.26.0

Output: dsp/build/c7x_compute.out (~6.9 MB)

Host Application

C++14 CMake build with aarch64-linux-gnu-g++ cross-compilation. Links against pthread. Uses RAII wrappers (UniqueFd, MmapRegion, UniqueFile) for automatic resource cleanup. The build produces a single c7x_compute binary that serves as both CLI tool and library test harness. The public C API header (c7x_compute_client.h) has extern "C" guards and is usable from C code.

Dependencies: - gcc-aarch64-linux-gnu and g++-aarch64-linux-gnu

Output: arm/build/c7x_compute


Relationship to TVM

This firmware is the runtime target for TVM's C static lib backend when compiling for C7x DSP. The workflow is:

  1. TVM compilation (on development host): Compile a neural network model using the TVM C static lib backend, producing lib0.c and weights.bin
  2. C7x ELF build: Compile lib0.c with the TI CGT C7000 compiler into a relocatable ELF (lib0.out) with weights embedded in .rodata.weights
  3. Deployment: Copy lib0.out to the AM67A target
  4. Execution: Use the host CLI or library to load the ELF onto the DSP via DLOAD, run inference, and retrieve results

The DSP-side tests in tests/ti-dsp-runtime/dsp-tests/ automate this full pipeline using pytest, including TVM compilation, C7x ELF building, firmware deployment, and inference verification.


File Organization

c7x-firmware/
+-- README.md                     # Usage and reference documentation
+-- design_doc.md                 # This file
+-- deploy-c7x.sh                # SSH-based firmware deployment to AM67A
+-- common/
|   +-- c7x_compute_protocol.h    # Shared protocol definitions (ARM + DSP)
+-- dsp/
|   +-- build.sh                  # DSP firmware build script
|   +-- CMakeLists.txt
|   +-- cmake/
|   |   +-- toolchain-c7000.cmake # C7x cross-compilation toolchain
|   +-- configs/
|   |   +-- c75ss0.syscfg         # SysConfig (IPC, MMU, 16 regions)
|   |   +-- linker_c75_freertos.cmd
|   +-- src/
|       +-- main.c                # FreeRTOS entry, IPC init, shutdown
|       +-- compute_service.c     # RPMessage handler, message dispatch
|       +-- compute_service.h
|       +-- dyn_loader.c          # DLOAD wrapper, symbol table, DLIF callbacks
|       +-- dyn_loader.h
|       +-- tvm_model.c           # TVM model/constants manager
|       +-- tvm_model.h
|       +-- shm_printf.c          # SHM printf: add_device driver + stdout redirect
|       +-- shm_printf.h
|       +-- dload/                # TI DLOAD dynamic linker source
|           +-- DLOAD/            #   ELF parser, segment loader
|           +-- DLOAD_API/        #   Public API header
|           +-- DLOAD_SYM/        #   Symbol table implementation
|           +-- C70_DLOAD_DYN/    #   C7x dynamic linking
|           +-- C70_DLOAD_REL/    #   C7x relocation handling
+-- arm/
|   +-- build.sh                  # ARM client build and deploy script
|   +-- CMakeLists.txt
|   +-- include/
|   |   +-- c7x_compute_client.h  # Client library API
|   |   +-- c7x_runtime.h         # C++ Module/Function API (DLPack-based)
|   +-- src/
|   |   +-- raii.h                 # RAII wrappers (UniqueFd, MmapRegion, UniqueFile)
|   |   +-- c7x_compute_client.cpp # Client library implementation (C++14)
|   |   +-- c7x_runtime.cc         # c7x::Module C++ wrapper implementation
|   |   +-- rpmsg_wrapper.cpp      # rpmsg_ctrl discovery + endpoint mgmt
|   |   +-- rpmsg_wrapper.h
|   |   +-- c7x_compute_cli.cpp    # CLI tool
|   +-- test/
|       +-- test_c7x_runtime.cpp   # C++ test binary for c7x::Module API
+-- test/
    +-- test_dynmod.sh            # Automated hardware test suite (19 tests)