PLDM (Platform Level Data Model)
OpenSMA implements the PLDM firmware-device (FD) role: it is the
responder that a host-side Update Agent (UA) talks to in order to
inventory and update the MCU’s own firmware (and any attached AP
firmware). PLDM messages arrive as MCTP message type 5 (firmware update)
or type 0 (control & discovery), and are dispatched into an Ada/SPARK
state machine that lives in corepdk/modules/pldm-fd/.
The subsystem splits cleanly into two layers:
| Layer | Language | Path | Responsibility |
|---|---|---|---|
| Task / glue | C++ | src/nv/pldm/ |
RTOS task, queues, event loop, MCTP framing, flash writes |
| Protocol core | Ada/SPARK | corepdk/modules/pldm-fd/ |
Base (T0) commands + firmware-update (T5) state machine |
| Platform wrap | C++ | corepdk/platforms/mcxn236/pldm-fd/ |
send_pldm_msg_to_mctp, pldm_write, version/UUID hooks |
The PLDM task
nv::pldm::Task derives from the common IPC task and owns two receive
queues (a “base” queue and a 4 KB queue for large transfers), a timer,
and an event object — see the class in src/nv/pldm/task.h:60 and the
constructor wiring at src/nv/pldm/task.cpp:183. The task’s work is
driven entirely by event bits declared in
src/nv/pldm/task.h:78 (PldmTaskRxBit, PldmTaskRx4kBit, TimerBit,
BgStartBit, EventRequestBit, ProtocolResetBit, …), each mapped from
the PldmTask enum at src/nv/pldm/task.h:65.
Because the Ada core is written in a synchronous, single-context style,
the C++ layer holds two context records for the life of the task and
passes them into every Ada entry point. Task::main initialises them
once via the exported Ada initialisers ada_pldmfw_context_init and
ada_init_pldm_base_ctx (src/nv/pldm/task.cpp:196) — both declared as
extern "C" at src/nv/pldm/task.cpp:45. The context layout itself
(PldmContextRecord) is the C mirror of the Ada Pldm_Context record
and is defined at corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.h:198.
Inbound path: MCTP → PLDM → Ada dispatch
The MCTP driver forwards any packet whose client is Client::Pldm into
the task by calling nv::pldm::Task::pldm_tx
(src/nv/mctp/driver.cpp:421). pldm_tx
(src/nv/pldm/task.cpp:489) picks the base or 4 KB queue by packet
length, enqueues the raw MCTP packet, and sets the matching event bit.
The event loop then drains the queue and routes by the PLDM message-type
byte. The base type (NV_PLDM_TYPE_MSG_CTRL_AND_DISCOVERY, value 0) goes
to ada_process_base_commands (src/nv/pldm/task.cpp:314); the firmware
-update type (NV_PLDM_TYPE_FIRMWARE_UPDATE, value 5) goes to
ada_pldmfw_state_runner (src/nv/pldm/task.cpp:318). Both type
constants are defined at
corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.h:27. Before dispatch,
get_interface_info (src/nv/pldm/task.cpp:109) peels the MCTP private
- transport header off the buffer and records the source EID / message tag so the response can be routed back to the same UA.
sequenceDiagram
participant UA as Update Agent (host)
participant M as MCTP driver
participant T as pldm::Task loop
participant A as pldm-fd (Ada)
UA->>M: MCTP type 5 (RequestUpdate)
M->>T: pldm_tx() enqueue + set PldmTaskRxBit
T->>T: get_interface_info() strip MCTP header
T->>A: ada_pldmfw_state_runner(context, size)
A->>A: state = IDLE -> route to Idle handler
A-->>M: send_pldm_msg_to_mctp() response
M-->>UA: MCTP response (+ transition to LEARN COMPONENT)
Base (T0) command handling
The base command set is discovery only. Process_Base_Commands
(corepdk/modules/pldm-fd/src/base/pdk-pldm-base-cmd.ads:226, body at
corepdk/modules/pldm-fd/src/base/pdk-pldm-base-cmd.adb:38) switches on
the PLDM command code (corepdk/modules/pldm-fd/src/base/pdk-pldm-base-cmd.adb:54)
and handles SetTID, GetTID, GetPLDMVersion, GetPLDMTypes, and
GetPLDMCommands; anything else returns PLDM_CC_ERR_UNSUPPORTED_CMD.
The advertised capabilities (base + firmware-update versions) are seeded
in Init_Pldm_Base_Ctx.
Firmware-update (T5) state machine
The heart of PLDM-FD is a seven-state machine defined in Ada.
Pldmfw_State_Runner
(corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate-state.ads:89,
exported as ada_pldmfw_state_runner) switches on the current state and
delegates to a per-state handler
(corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate-state.adb:428).
The states themselves are declared as constants in
corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate.ads:76:
| State | Value | Handler | DSP0267 phase |
|---|---|---|---|
IDLE |
0 | Pldmfw_State_Idle_Handler |
inventory + RequestUpdate |
LC |
1 | Pldmfw_State_Lc_Handler |
learn components / PassComponentTable |
RDY_XFER |
2 | Pldmfw_State_Rdy_Xfer_Handler |
UpdateComponent |
DL |
3 | Pldmfw_State_Dl_Handler |
RequestFirmwareData loop |
VERIFY |
4 | Pldmfw_State_Verify_Handler |
VerifyComplete |
APPLY |
5 | Pldmfw_State_Apply_Handler |
ApplyComplete |
ACTIVATE |
6 | Pldmfw_State_Activate_Handler |
ActivateFirmware |
Each incoming command is validated against the current state. For
example, in IDLE only queries and RequestUpdate are accepted; a
PassComponentTable there is rejected with PLDMFW_CC_NOT_IN_UPDATE_MODE
(corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate-state.adb:99).
A successful RequestUpdate calls Pldmfw_Change_State to advance to
LC
(corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate-state.adb:94).
State transitions also arm the FD timers (T1/T2/verify), so a stalled UA
eventually times out — Pldmfw_Change_State sets them per target state
(corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate-state.adb:39)
and the C++ TimerBit drives ada_pldmfw_timeout_handler
(src/nv/pldm/task.cpp:353). The full PLDM command and completion-code
sets are enumerated at
corepdk/modules/pldm-fd/src/fwupdate/pdk-pldm-fwupdate.ads:61 and
:40.
The command codes the FD understands (a subset of DSP0267) include
QueryDeviceIdentifiers (0x01), GetFirmwareParameters (0x02),
RequestUpdate (0x10), PassComponentTable (0x13),
UpdateComponent (0x14), RequestFirmwareData (0x15),
TransferComplete (0x16), VerifyComplete (0x17), ApplyComplete
(0x18), and ActivateFirmware (0x1a).
Writing the image and asking SPDM to authenticate
The Ada core cannot touch flash directly; it calls back into C++ through
the platform wrap. pldm_write
(corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.cpp:188) erases and
programs the downloaded chunks — for the MCU component it writes 256-byte
pages into the inactive slot; for an AP component it routes through the
vRoT metadata/firmware writers. Outbound responses are framed and handed
back to MCTP by send_pldm_msg_to_mctp
(corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.cpp:138, declared at
corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.h:254), which rebuilds
the MCTP header via the composer and calls Driver::mctp_send_from_pldm.
Verification is delegated to the SPDM subsystem. During the
verify phase the FD calls request_authentication
(corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.cpp:796), which for
the MCU invokes nv::spdm::crypto::authenticate_mcu_firmware. SPDM runs
the check asynchronously and posts the result back to the PLDM task: the
Pldm-specialised send_authenticate_mcu_firmware_result
(corepdk/platforms/mcxn236/pldm-fd/src/pldm_wrap.cpp:50) builds a
Request and hands it to Task::to_pldm
(src/nv/pldm/task.cpp:608), which enqueues it and sets EventRequestBit.
The loop then decodes the result (switching on request.type at
src/nv/pldm/task.cpp:462) and calls ada_pldmfw_authenticate_handler
(src/nv/pldm/task.cpp:466), mapping the crypto status onto a PLDM
VerifyComplete completion code.
Interpretation: this indirection keeps all cryptographic work on the SPDM task (which owns the key material and mbedTLS contexts) while the PLDM FD state machine stays purely protocol-focused and non-blocking.
Background copy and protocol reset
Beyond the update flow, the task also drives background copy — the
MCU replicating its freshly written slot — via BgStartBit /
BgEndBit (src/nv/pldm/task.cpp:355), gated by the flash-stored
policy read in is_background_copy_automatic
(src/nv/pldm/task.cpp:138). AP background copy has its own
ApBgStartBit path (src/nv/pldm/task.cpp:418). A ProtocolResetBit
(src/nv/pldm/task.cpp:243) flushes both receive queues and re-inits the
Ada contexts, returning the FD to IDLE. The AP component-id list the FD
reports is built at compile time from the vRoT ApList by
make_component_id_list (src/nv/pldm/common.h:28).
Where to look next
- MCTP framing and the
Client::Pldmforwarding decision: MCTP transport. - The crypto/attestation side of firmware authentication: SPDM and Security & Root-of-Trust.
- Overall task/queue model: Architecture.