DOCA Core objects and lifecycle
S3·E2It worked yesterday, now it returns BAD_STATE · Dell lab, Round Rock, second afternoon of bring-up
Builds on: Development environments
Before you read: what do you already know?
3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.
After this lesson you can
- Distinguish doca_devinfo, doca_dev and doca_dev_rep and state which side of the PCIe bus opens each.
- Trace the create, configure, start, use, stop, destroy sequence through doca_mmap, doca_buf_inventory, doca_ctx and doca_pe as executed by dma_common.c and common.c.
- Analyze a doca_ctx state transition (Idle, Starting, Running, Stopping) and predict which API calls are legal in each state.
- Map a returned doca_error_t such as INVALID_VALUE, BAD_STATE, NOT_SUPPORTED or IN_PROGRESS to the layer that produced it and the correct next action.
Episode 2 — It worked yesterday, now it returns BAD_STATE
The card is fine. That is the part nobody in the room believes yet. The night-shift operator has already labeled the tray SUSPECT? with the label maker he brings to every bring-up, and the archive’s network lead has his notebook open at a fresh page, waiting for somebody to show him a counter that proves anything. On the shared screen a developer from the platform team scrolls a diff from last night’s refactor — tidier, fewer lines, one configuration call moved a few lines down — and her DMA program that ran clean yesterday now fails on startup. Her manager wants to know whether the card is bad, because a swap costs a week out of six and the PoC report does not move.
DOCA is built so that this is a lifecycle question, not a hardware one. Every core object follows the same flow — create the instance, configure it, start it — and after start it “adheres to zero allocations and can be used safely in the data path”.[1] A context makes that flow enforceable through four states: in Idle every configuration API is enabled, in Running “Task allocation/submission enabled” while “All configuration APIs are disabled”, and Stopping cleans in-flight tasks.[1] A configuration call arriving after start is therefore refused with an error rather than quietly corrupting a data path that promised no allocations.
The samples make the legal order concrete, and they decode every failure with doca_error_get_descr.[2][3]
Before you suspect the silicon, read the state machine.
You take the screen share and start where DOCA starts.
1Devices: devinfo, dev, representor
The Core guide describes device handling as querying device information and capabilities, then opening the selected device. On the BlueField it “Gets local BlueField devices” and “Gets representors list (representing host local devices)”; on the host it gets local devices and their representors, which matter for ConnectX or a DPU in NIC mode.[1] The entities are doca_devinfo, doca_devinfo_rep, doca_dev and doca_dev_rep.[1]
The samples wrap this in common.c. open_doca_device_with_pci_and_callback calls doca_devinfo_create_list(&dev_list, &nb_devs), loops with doca_devinfo_is_equal_pci_addr(dev_list[i], pci_addr, &is_addr_equal), skips any device whose capability callback does not return DOCA_SUCCESS, opens the first match with doca_dev_open(dev_list[i], retval), frees the list with doca_devinfo_destroy_list, and otherwise logs “Matching device not found” and returns DOCA_ERROR_NOT_FOUND.[2] The DMA code passes dma_task_is_supported, which is nothing more than doca_dma_cap_task_memcpy_is_supported(devinfo); the capability is checked on the doca_devinfo before any open.[3] After the open, capabilities are read from doca_dev_as_devinfo(dev), for example doca_dma_cap_task_memcpy_get_max_buf_list_len(doca_dev_as_devinfo(state->dev), &max_buf_list_len).[3]
Representors follow the same shape one level down. open_doca_device_rep_with_pci(local, filter, pci_addr, &rep) calls doca_devinfo_rep_create_list(local, filter, &rep_dev_list, &nb_rdevs), matches with doca_devinfo_rep_is_equal_pci_addr, opens with doca_dev_rep_open, and cleans up with doca_devinfo_rep_destroy_list.[2] The Comch glue on the DPU side uses it as open_doca_device_rep_with_pci(cfg->dev, DOCA_DEVINFO_REP_FILTER_NET, rep_pci_addr, &cfg->dev_rep) and then creates the server with doca_comch_server_create(cfg->dev, cfg->dev_rep, server_name, &cfg->server); teardown closes the representor with doca_dev_rep_close before doca_dev_close.[8]
2Memory: mmap and buffers
The Core guide gives one flow for every core object: “Create the object instance (e.g., doca_mmap_create). Configure the instance (e.g., doca_mmap_set_memory_range). Start the instance (e.g., doca_mmap_start).” After start “it adheres to zero allocations and can be used safely in the data path”, and it “must be stopped and destroyed (doca_mmap_stop, doca_mmap_destroy)”; some objects can go create, configure, start, stop, configure, start.[1] Note a naming drift: the guide’s prose says doca_mmap_set_memory_range, but the function the 3.5.0 samples call is doca_mmap_set_memrange.[4]
In code the configuration is doca_mmap_create(&state->src_mmap) and doca_mmap_add_dev(state->src_mmap, state->dev) inside create_core_objects, then later doca_mmap_set_memrange(mmap, buffer, length) followed by doca_mmap_start(mmap) in register_memory_range_and_start_mmap.[2][4] When the memory must be reachable from the other side of the PCIe bus, permissions come first: dma_copy_host calls doca_mmap_set_permissions(state.src_mmap, DOCA_ACCESS_FLAG_PCI_READ_ONLY), then doca_mmap_set_memrange, doca_mmap_start, and doca_mmap_export_pci(state.src_mmap, state.dev, &export_desc, &export_desc_len).[5] The guide warns that “If appropriate access has not been provided, the export fails” and that the exported descriptor “contains sensitive information”.[1] The guide’s page prints an enum spelled DOCA_ACCESS_LOCAL_READ_ONLY, DOCA_ACCESS_LOCAL_READ_WRITE, DOCA_ACCESS_RDMA_READ, DOCA_ACCESS_RDMA_WRITE, DOCA_ACCESS_RDMA_ATOMIC, DOCA_ACCESS_DPU_READ_ONLY and DOCA_ACCESS_DPU_READ_WRITE; the 3.5.0 samples compile with the DOCA_ACCESS_FLAG_ names, so trust the sample when the two disagree.[1][5]
A doca_buf is a view onto mapped memory with three regions: headroom “starting from the buffer’s address up to the buffer’s data address”, dataroom “starting from the buffer’s data address with a length indicated by the buffer’s data length”, and tailroom “starting from the end of the dataroom to the end of the buffer”; the tailroom is free writing space for libraries, and the buffer “does not own nor manage the data it references”.[1] Buffers come from an inventory: doca_buf_inventory_create(max_bufs, &inv) then doca_buf_inventory_start(inv); allocate_doca_buf_list takes each one with doca_buf_inventory_buf_get_by_addr(inv, mmap, addr, len, &buf), marks source data with doca_buf_set_data(buf, addr, len), chains with doca_buf_chain_list(head, next), and releases with doca_buf_dec_refcount(buf, NULL), where releasing the head releases every chained buffer.[2] Once handed to a task, “ownership of the buffer moves to the library until that task is complete”.[1]
3Context state machine and the progress engine
A doca_ctx has four states. Idle: “0 in-flight tasks”, all configuration APIs enabled right after doca_<T>_create. Starting: “mandatory for CTXs where transition to running state is conditioned by one or more async op completions/external events”, the guide’s example being a client connecting to a comm channel. Running: “Task allocation/submission enabled (disabled in all other states)” and “All configuration APIs are disabled”. Stopping: “Clean all in-flight tasks that may not complete in near future”.[1] An internal error in Starting or Running causes an involuntary transition to Stopping.[1]
The samples name them DOCA_CTX_STATE_IDLE, DOCA_CTX_STATE_STARTING, DOCA_CTX_STATE_RUNNING and DOCA_CTX_STATE_STOPPING, and observe them through a callback registered with doca_ctx_set_state_changed_cb: dma_state_changed_callback(const union doca_data user_data, struct doca_ctx *ctx, enum doca_ctx_states prev_state, enum doca_ctx_states next_state).[3] Because DMA has no asynchronous start, that callback logs “DMA context entered into starting state. Unexpected transition” if it ever sees Starting.[3] Creation is doca_dma_create(state->dev, &resources->dma_ctx), the generic handle is doca_dma_as_ctx(resources->dma_ctx), and configuration includes doca_dma_cap_get_max_num_tasks, doca_dma_task_memcpy_set_conf(dma_ctx, completed_cb, error_cb, num_tasks) and doca_ctx_set_user_data.[3]
The progress engine is created with doca_pe_create(&state->pe) in create_core_objects; the context is attached with doca_pe_connect_ctx(state->pe, state->ctx) and only then started with doca_ctx_start(state->ctx).[2][4] A PE “can be connected to multiple contexts”, of the same or different types, so one thread can wait on all of them.[1] Stopping is asynchronous: request_stop_ctx calls doca_ctx_stop, and on DOCA_ERROR_IN_PROGRESS loops doca_pe_progress(pe) and doca_ctx_get_state(ctx, &st) until DOCA_CTX_STATE_IDLE.[2] The PE README adds the destruction rule: “all contexts must be destroyed before the PE”.[6]
struct doca_devinfo **dev_list; uint32_t nb_devs; doca_devinfo_create_list(&dev_list, &nb_devs);
Why: Lists every DOCA-capable function (PF/SF) this process can see. Nothing is opened yet.
FAE note: Same call on host and on the Arm side; on BlueField the list also includes SFs, which RDMA on BlueField requires.
4doca_error_t: what each family means
Every DOCA API returns doca_error_t. The Core guide lists DOCA_SUCCESS, DOCA_ERROR_UNKNOWN, DOCA_ERROR_NOT_PERMITTED, DOCA_ERROR_IN_USE, DOCA_ERROR_NOT_SUPPORTED, DOCA_ERROR_AGAIN, DOCA_ERROR_INVALID_VALUE, DOCA_ERROR_NO_MEMORY, DOCA_ERROR_INITIALIZATION, DOCA_ERROR_TIME_OUT, DOCA_ERROR_SHUTDOWN, DOCA_ERROR_CONNECTION_RESET, DOCA_ERROR_CONNECTION_ABORTED, DOCA_ERROR_CONNECTION_INPROGRESS, DOCA_ERROR_NOT_CONNECTED, DOCA_ERROR_NO_LOCK, DOCA_ERROR_NOT_FOUND, DOCA_ERROR_IO_FAILED, DOCA_ERROR_BAD_STATE, DOCA_ERROR_UNSUPPORTED_VERSION, DOCA_ERROR_OPERATING_SYSTEM, DOCA_ERROR_DRIVER, DOCA_ERROR_UNEXPECTED, DOCA_ERROR_ALREADY_EXIST, DOCA_ERROR_FULL, DOCA_ERROR_EMPTY, DOCA_ERROR_IN_PROGRESS and DOCA_ERROR_TOO_BIG.[1] The same headers serve BlueField and host, “but specific API calls may return DOCA_ERROR_NOT_SUPPORTED if the API is not implemented for that processor”.[1]
The samples decode with doca_error_get_descr(result) in every log line and keep the first failure through teardown with DOCA_ERROR_PROPAGATE(result, tmp_result).[3][4] The debugging notes turn the enum into a diagnosis map, reproduced here with the sample evidence:
| Code | Layer | Sample evidence and action |
|---|---|---|
DOCA_ERROR_INVALID_VALUE |
program argument | memory_ranges_overlap returns it when buffers overlap; fix the caller.[4] |
DOCA_ERROR_BAD_STATE |
call order | a configuration call in Running or a task in Idle; fix the lifecycle.[7] |
DOCA_ERROR_NOT_SUPPORTED |
capability | doca_ctx_profiler_set_max_nranges returns it without NVTX; the sample warns and continues.[4] |
DOCA_ERROR_NOT_FOUND |
device | open_doca_device_with_pci when no device matches; check the address.[2] |
DOCA_ERROR_IN_PROGRESS |
asynchronous stop | normal from doca_ctx_stop; progress until Idle.[2] |
DOCA_ERROR_AGAIN |
backpressure | comch_utils_send returns it when no send task can be allocated, “telling the application to progress and retry”; progress the PE and retry — this is the one family a retry loop is meant for.[8] |
DOCA_ERROR_DRIVER |
below DOCA | kernel or firmware; do not retry in the program.[7] |
The rule from the notes: never retry-loop INVALID_VALUE, BAD_STATE, NOT_SUPPORTED, INITIALIZATION or DRIVER, and retry TIME_OUT only with a documented bound.[7]
5The canonical order, as executed by dma_common.c and common.c
Read together, allocate_dma_resources and dma_local_copy execute the lifecycle in this order. Open the device with a capability filter: open_doca_device_with_pci(pcie_addr, &dma_task_is_supported, &state->dev).[3] Query limits: doca_dma_cap_task_memcpy_get_max_buf_list_len, and reject requests above them with DOCA_ERROR_INVALID_VALUE.[3] Create the core objects: two mmaps with doca_mmap_create and doca_mmap_add_dev, an inventory with doca_buf_inventory_create and doca_buf_inventory_start, and the PE with doca_pe_create.[2] Create and configure the context: doca_dma_create, doca_dma_as_ctx, doca_ctx_set_state_changed_cb, doca_dma_cap_get_max_num_tasks, doca_dma_task_memcpy_set_conf, doca_ctx_set_user_data.[3] Attach and start: doca_pe_connect_ctx, doca_ctx_start.[4] Finish the mmaps: doca_mmap_set_memrange and doca_mmap_start for destination then source.[4] Use: buffers from the inventory, doca_dma_task_memcpy_alloc_init, doca_task_submit, and a doca_pe_progress loop.[4] Tear down: release buffers, doca_ctx_stop, doca_dma_destroy, then destroy_core_objects, which destroys the PE, the inventory, both mmaps and closes the device.[2][3] The README summarises the same run as locate device, initialize core structures, populate the mmap, allocate inventory elements, initialize the task, submit, handle completion, check the result, destroy.[9]
The calls, in execution order, with the object and state each one touches:
/* 1. device: filtered open */
open_doca_device_with_pci(pcie_addr, &dma_task_is_supported, &state->dev);
doca_dma_cap_task_memcpy_get_max_buf_list_len(doca_dev_as_devinfo(state->dev), &max_buf_list_len);
/* 2. core objects (create_core_objects) */
doca_mmap_create(&state->src_mmap); doca_mmap_add_dev(state->src_mmap, state->dev);
doca_mmap_create(&state->dst_mmap); doca_mmap_add_dev(state->dst_mmap, state->dev);
doca_buf_inventory_create(max_bufs, &state->buf_inv); doca_buf_inventory_start(state->buf_inv);
doca_pe_create(&state->pe);
/* 3. context: create + configure (Idle) */
doca_dma_create(state->dev, &resources->dma_ctx);
state->ctx = doca_dma_as_ctx(resources->dma_ctx);
doca_ctx_set_state_changed_cb(state->ctx, dma_state_changed_callback);
doca_dma_cap_get_max_num_tasks(resources->dma_ctx, &max_tasks_num);
doca_dma_task_memcpy_set_conf(resources->dma_ctx, dma_memcpy_completed_callback, dma_memcpy_error_callback, num_tasks);
doca_ctx_set_user_data(state->ctx, ctx_user_data);
/* 4. attach + start (Idle -> Running) */
doca_pe_connect_ctx(state->pe, state->ctx);
doca_ctx_start(state->ctx);
/* 5. mmaps: configure + start */
doca_mmap_set_memrange(state->dst_mmap, dst_buffer, total_length); doca_mmap_start(state->dst_mmap);
doca_mmap_set_memrange(state->src_mmap, src_buffer, total_length); doca_mmap_start(state->src_mmap);
/* 6. use (Running) */
doca_buf_inventory_buf_get_by_addr(state->buf_inv, mmap, addr, len, &buf); doca_buf_set_data(buf, addr, len); /* source only */
doca_dma_task_memcpy_alloc_init(resources->dma_ctx, src_buf, dst_buf, task_user_data, &task);
doca_task_submit(doca_dma_task_memcpy_as_task(task));
while (resources->run_pe_progress) if (doca_pe_progress(state->pe) == 0) nanosleep(&ts, &ts);
/* 7. teardown (Running -> Stopping -> Idle, then destroy) */
doca_buf_dec_refcount(buf, NULL);
doca_ctx_stop(state->ctx); /* completion callback already called it when the last task finished */
doca_dma_destroy(resources->dma_ctx);
destroy_core_objects(state); /* pe -> buf_inv -> dst_mmap -> src_mmap -> dev */Reasoning: steps 1 to 3 happen while everything is Idle, so every configuration call is legal. Step 4 moves the context to Running; from here only tasks are legal on the context. Step 5 is legal because the mmaps are separate objects still in their own configure phase. In step 7 the completion callback calls doca_ctx_stop when num_remaining_tasks reaches zero, the state callback sees DOCA_CTX_STATE_IDLE and clears run_pe_progress, and the main function’s own doca_ctx_stop returns harmlessly.
Fill the blanks, then check against the worked pane.
- Device:
open_doca_device_with_pci(pcie_addr, &____, &state->dev)— the filter is the DMA capability probe. - Core objects: for each mmap
doca_mmap_createthen____; inventorydoca_buf_inventory_createthen____; thendoca_pe_create. - Context:
doca_dma_create,____to get the generic handle,doca_ctx_set_state_changed_cb,doca_dma_task_memcpy_set_conf(dma_ctx, completed_cb, error_cb, ____). - Attach and start:
____thendoca_ctx_start. The context is now in state____. - mmaps:
____thendoca_mmap_start. - Use:
doca_dma_task_memcpy_alloc_init,____, loop ondoca_pe_progressuntil the state callback reports____. - Teardown: buffers released with
____, thendoca_dma_destroy, thendestroy_core_objects, which destroys the____first and closes the device last.
Write the equivalent ordered call list for the host-side Comch client in applications/common/comch_utils.c, using only these calls: doca_pe_create, open_doca_device_with_pci, doca_comch_cap_get_max_msg_size, doca_comch_cap_client_is_supported, doca_comch_client_create, doca_comch_client_set_max_msg_size, doca_comch_client_as_ctx, doca_pe_connect_ctx, doca_comch_client_task_send_set_conf, doca_comch_client_event_msg_recv_register, doca_ctx_set_user_data, doca_ctx_start, doca_ctx_get_state, doca_comch_client_get_connection.
Acceptance criteria: every configuration call precedes doca_ctx_start; both capability queries precede doca_comch_client_create; after doca_ctx_start you show a doca_pe_progress loop that waits for doca_ctx_get_state to report DOCA_CTX_STATE_RUNNING before doca_comch_client_get_connection; and you name the state the client passes through that a DMA context never enters, with the reason.
Episode 2 — One call, moved back
Her doca_dma_task_memcpy_set_conf had landed after doca_ctx_start, so it ran against a context already in Running, where configuration APIs are disabled.[1] Moved back above the start, the program runs first try. The operator peels SUSPECT? off the tray without comment. No retry loop would have helped, because retrying an illegal call cannot change the state that makes it illegal.[7]
What you say to her manager: “Nothing is wrong with the hardware. Send me the exact doca_error_t name and the call that returned it, and this class of problem is a five-minute read.”
It runs for an hour before the network lead looks up from the notebook with the next counter: one Arm core, pinned at a hundred percent, whether or not anything is arriving.
Lab
Pre-flight (read-only): on the BlueField /opt/mellanox/doca/tools/doca_caps --list-devs and record the PF address; /opt/mellanox/doca/tools/doca_caps --version on both host and Arm must match.
- On the Arm side, in the devel container from lesson 3.1, run the unmodified sample with SDK logs at DEBUG:
/tmp/build-dma/doca_dma_local_copy -p <pf> --sdk-log-level 60 -l 60— expectDMA context is running,DMA task was completed successfully,DMA context entered into stopping state. Any inflight tasks will be flushed,DMA context has been stopped,Sample finished successfully. If the device is not found, recheck the PF address. - Run with four tasks:
-nt 4— expect four completion lines and fourSuccess, DMA memory task N copied and verified as correctlines. If you seeNumber of tasks [N] exceed the memcpy task max_tasks_num capability, the value is above whatdoca_dma_cap_get_max_num_tasksreported; lower it. - Apply the ordering injection from the no-hardware lab (
set_confafterdoca_ctx_start), rebuild, run — record the exactdoca_error_ttext printed bydoca_error_get_descrand which log line printed it. Compare with your prediction. - Revert the source (
git checkout -- samples/doca_dma), rebuild, rerun step 1 — expect the original output. This is the rollback; no device or firmware state was touched at any step.
- In the
devel-3.5.0-hostcontainer with the 3.5.0 samples cloned (lesson 3.1), rungrep -n "doca_" samples/doca_dma/dma_common.c | grep -v LOG | sed -n '1,80p'— expect the create, set_conf, connect and destroy calls in the order listed in segment 5. If a call appears out of the order you predicted, write down which object it belongs to. grep -n "doca_ctx_\|doca_pe_" samples/common.c— expectdoca_pe_create,doca_ctx_stop,doca_pe_progress,doca_ctx_get_state,doca_pe_destroy. Locaterequest_stop_ctxand confirm theDOCA_ERROR_IN_PROGRESSbranch.- Open the DocaCoreStepper above in inject mode. Move
doca_ctx_startahead ofdoca_pe_connect_ctx, and separately movedoca_dma_task_memcpy_set_confafterdoca_ctx_start. For each, write thedoca_error_tyou expect and why, before revealing the answer. - Apply the second injection for real: in
dma_common.c, move thedoca_dma_task_memcpy_set_confblock to just afterdoca_ctx_startindma_local_copy_sample.c(passresources.dma_ctx), rebuild withninja -C /tmp/build-dma— expect a clean build. You cannot run it here; carry the prediction to the hardware lab, then revert withgit checkout -- samples/doca_dma. grep -n "DOCA_ACCESS" samples/doca_dma/dma_copy_host/dma_copy_host_sample.c applications/secure_channel/secure_channel_core.c— expectDOCA_ACCESS_FLAG_PCI_READ_ONLYandDOCA_ACCESS_FLAG_PCI_READ_WRITE. Note the spelling against the Core guide page.
Retrieval check
9 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a developer at a Dell customer, in five sentences, why every DOCA object goes through create, configure, start, use, stop and destroy, and what goes wrong when the order is broken.
Sources
Facts in this lesson were checked against DOCA 3.5.0 Core programming guide and doca-samples tag 3.5.0, 2026-09-06. Dates are when each page was fetched.
- DOCA Core programming guide · fetched 2026-09-06 · DOCA 3.5.0
- samples/common.c (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
- samples/doca_dma/dma_common.c (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
- dma_local_copy_sample.c (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
- dma_copy_host_sample.c (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
- samples/doca_common/README.md (progress engine samples) · fetched 2026-09-06 · DOCA 3.5.0
- NVIDIA/skills doca-debug CAPABILITIES.md · fetched 2026-09-06 · DOCA 3.5.0
- applications/common/comch_utils.c (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
- samples/doca_dma/README.md (tag 3.5.0) · fetched 2026-09-06 · DOCA 3.5.0
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.