🔍
v1.3.8

DID_EXT Function Block

DID_EXT is the function block for reading vehicle ECU data via UDS (Unified Diagnostic Services). It sends ReadDataByIdentifier (service 0x22) requests and handles the response automatically - including multi-frame (ISO-TP), flow control, and timeouts.

Important: One Library Per ECU

DID_EXT reads from one ECU (one CAN TX/RX pair). If your recipe needs to read from multiple ECUs, you create one library per ECU. Each library can read multiple DID numbers from that ECU with individual polling timers.

DID Module Library Structure

A DID module is always written as a separate library that gets linked to your recipe. It follows a fixed structure with 5 sections. Here is the complete pattern, based on a real production recipe (BMW G20, recipe 1439):

Section 1: ECU Configuration (VAR_CONSTANT)

Define which ECU to talk to and how many DIDs to read:

VAR_CONSTANT
    // CAN addresses for this ECU
    MODULE_CAN_SEND_ID  : UDINT   := 0x6F1;    // TX: where to send requests
    MODULE_CAN_RECV_ID  : UDINT   := 0x640;    // RX: where responses come from
    MODULE_CAN_EXT_ID   : BOOL    := FALSE;    // FALSE = 11-bit, TRUE = 29-bit CAN ID
    MODULE_ECU_ID       : BYTE    := MODULE_CAN_RECV_ID BAND 0xFF;  // ECU identifier byte
    MODULE_MSG_SHIFT    : BYTE    := 1;        // BMW: 1 (ECU ID in first byte), others: 0

    // How many DID requests this library sends
    TOTALMSG : BYTE := 1;

    // How many bytes to capture from each DID response
    MODULE_0_DATA_LEN : INT := 19;   // DID 0xD542 returns 19 bytes
END_VAR;
About MODULE_MSG_SHIFT

BMW uses a single TX address (0x6F1) for all ECUs, with the ECU ID as the first byte in the payload. Set MODULE_MSG_SHIFT := 1 to strip this extra byte. For all other manufacturers, set it to 0.

Section 2: Data Buffers and Payload (VAR)

Declare the data buffers and UDS request payloads:

VAR
    // Data buffer for each DID response
    MODULE_0_DATA : ARRAY[0..MODULE_0_DATA_LEN - 1] OF BYTE;

    // UDS request payloads (one row per DID)
    // BMW format: [ECU_ID, PCI, 0x22, DID_H, DID_L, 0, 0, 0]
    PAYLOAD : ARRAY[0..TOTALMSG - 1, 0..7] OF BYTE :=
        [
            0x40, 0x03, 0x22, 0xD5, 0x42, 0x00, 0x00, 0x00  // DID 0xD542
        ];

    // Priority order for round-robin polling
    ORDER      : ARRAY[0..0] OF BYTE := [0];
    TOTALORDER : BYTE := 1;
END_VAR;
BMW vs Standard Payload Format

BMW payloads start with the ECU ID byte. Standard payloads start directly with PCI:

// BMW:      [ECU_ID, PCI,  0x22, DID_H, DID_L, 0, 0, 0]
// Example:  [0x40,   0x03, 0x22, 0xD5,  0x42,  0, 0, 0]

// Standard: [PCI,  0x22, DID_H, DID_L, 0, 0, 0, 0]
// Example:  [0x03, 0x22, 0xD5,  0x42,  0, 0, 0, 0]

Section 3: Output Signals (VAR_SIGNAL)

Declare the signals that your recipe will read, plus enable flags:

VAR_SIGNAL
    // Signals extracted from ECU response (available to recipe via VAR_SIGNAL)
    SIGNAL_HELLJUS   : BYTE;
    SIGNAL_HALVLJUS  : BYTE;
    SIGNAL_BACKLJUS  : BYTE;
    SIGNAL_BROMSLJUS : BYTE;

    // Enable flag: recipe sets this to control when to poll
    ENABLE_LJUS : BOOL;
END_VAR;

VAR_SIGNAL makes these variables global - your main recipe can read SIGNAL_HELLJUS directly, and set ENABLE_LJUS to control when this DID should be polled. See Library Scrambling for why this must be VAR_SIGNAL and not VAR.

Section 4: Initialization (runs once)

Configure all DID_EXT modules and the CAN receiver. This runs only once at startup:

VAR
    // DID_EXT instances (one per DID number)
    MODULES  : ARRAY[0..TOTALMSG - 1] OF DID_EXT;

    // Shared CAN receiver for all modules in this library
    CANRECV  : CAN_RX;
    RECVDATA : ARRAY[0..7] OF BYTE;
    INIT     : BOOL;
    RES      : INT;
    PRIO     : BYTE;
    COUNTER  : BYTE;
END_VAR;

IF NOT INIT THEN
    // Common settings for all modules (same ECU)
    FOR i : BYTE := 0 TO TOTALMSG - 1 DO
        MODULES[i].TX_ENABLE := @SIGNAL_TANDNING;   // Only send when ignition is on
        MODULES[i].ACTIVE    := MODULES[i].TX_ENABLE;
        MODULES[i].NEW_DATA  := @CANRECV.AVAILABLE;  // Check this CAN receiver
        MODULES[i].IN_DATA   := RECVDATA;             // Read from this buffer
        MODULES[i].PAYLOAD   := @PAYLOAD[i, 0];       // UDS request for this DID
        MODULES[i].MSGSHIFT  := MODULE_MSG_SHIFT;
        MODULES[i].ECUID     := MODULE_ECU_ID;
        MODULES[i].PRIO_VAL  := i;
        MODULES[i].PRIO_P    := @PRIO;
        MODULES[i].TX_ID     := MODULE_CAN_SEND_ID;
        MODULES[i].TX_EXT    := MODULE_CAN_EXT_ID;
        MODULES[i].RESULT    := @RES;
    END_FOR;

    // Per-module settings (each DID can have different timing/buffer)
    MODULES[0](
        ACTIVE       := @ENABLE_LJUS,            // Enable flag from recipe
        OUT_DATA     := MODULE_0_DATA,            // Where to store response
        OUT_DATA_LEN := MODULE_0_DATA_LEN,        // How many bytes to capture
        LEN_OFFSET   := 0,
        INIT_PAUSE   := T#0ms,                   // No initial delay
        PAUSE        := T#0ms,                    // Poll as fast as possible
        EXTPAUSE     := HW.HARDWARE_TYPE = XBB_DONGLE,  // Slower flow control on Dongle v1
        EXTPAUSE_TIME := 0x01
    );

    // Initialize the shared CAN receiver for this ECU
    CANRECV(ENABLE := TRUE, EXT := MODULE_CAN_EXT_ID,
            ID := MODULE_CAN_RECV_ID, DATA := RECVDATA);
    INIT := TRUE;
END_IF;

Section 5: Main Loop + Signal Extraction

Process incoming CAN responses and extract signal values:

// Process all pending CAN responses (may be multiple per cycle)
DO
    RES := 0;
    FOR i : INT := 0 TO TOTALMSG - 1 DO
        MODULES[i]();                    // Let each module check for its response
        IF RES > 0 THEN
            PRIO := ORDER[COUNTER];      // Round-robin priority
            COUNTER := COUNTER + 1;
            IF COUNTER > TOTALORDER - 1 THEN COUNTER := 0; END_IF;
            RES := 0;
        END_IF;
    END_FOR;

    CANRECV();                           // Get next CAN message from queue
WHILE CANRECV.AVAILABLE > 0 END_WHILE;

// Extract signals from the response data
// First data byte is always at index [3] (after 0x62 + DID_H + DID_L)
SIGNAL_HELLJUS   := MODULE_0_DATA[8].0;    // Byte 8, bit 0
SIGNAL_HALVLJUS  := MODULE_0_DATA[6].0;    // Byte 6, bit 0
SIGNAL_BACKLJUS  := MODULE_0_DATA[18].0;   // Byte 18, bit 0
SIGNAL_BROMSLJUS := MODULE_0_DATA[15].0;   // Byte 15, bit 0

Byte Mapping (MODULE_DATA)

After DID_EXT processes a UDS response, it strips protocol bytes (PCI, sequence bytes) and stores the result in MODULE_X_DATA:

// MODULE_X_DATA layout after DID_EXT processing:
//
// Index:  [0]    [1]    [2]    [3]    [4]    [5]    [6]   ...
// Data:  0x62   DID_H  DID_L  DATA0  DATA1  DATA2  DATA3  ...
//         |      |      |      |
//         |      +------+      +---- Actual signal data starts here
//         +-- Positive response (always 0x62)
//
// FORMULA: Data byte N -> MODULE_X_DATA[N + 3]

Multi-Frame Responses

For responses larger than 7 bytes, DID_EXT handles multi-frame (ISO-TP) automatically. Sequence bytes (0x21, 0x22, 0x23...) are stripped - the data in MODULE_X_DATA is continuous:

// Raw CAN frames on the bus:
// Frame 1: [0x10][len][0x62][DID_H][DID_L][D0][D1][D2]
// Frame 2: [0x21][D3][D4][D5][D6][D7][D8][D9]    <- 0x21 STRIPPED
// Frame 3: [0x22][D10][D11][D12]...               <- 0x22 STRIPPED
//
// MODULE_X_DATA (continuous, no gaps):
// [0x62][DID_H][DID_L][D0][D1][D2][D3][D4][D5][D6][D7][D8][D9][D10]...

The formula MODULE_X_DATA[N + 3] always works regardless of frame count.

Reading Multiple DIDs from One ECU

A single library can read multiple DIDs from the same ECU. Add more rows to the PAYLOAD array, increase TOTALMSG, and add data buffers for each DID:

VAR_CONSTANT
    TOTALMSG          : BYTE := 3;      // Three DIDs from this ECU
    MODULE_0_DATA_LEN : INT := 19;      // DID 0xD542: light status
    MODULE_1_DATA_LEN : INT := 8;       // DID 0x421B: ignition
    MODULE_2_DATA_LEN : INT := 6;       // DID 0xF190: VIN (first bytes)
END_VAR;

VAR
    MODULE_0_DATA : ARRAY[0..MODULE_0_DATA_LEN - 1] OF BYTE;
    MODULE_1_DATA : ARRAY[0..MODULE_1_DATA_LEN - 1] OF BYTE;
    MODULE_2_DATA : ARRAY[0..MODULE_2_DATA_LEN - 1] OF BYTE;

    PAYLOAD : ARRAY[0..TOTALMSG - 1, 0..7] OF BYTE := [
        0x03, 0x22, 0xD5, 0x42, 0x00, 0x00, 0x00, 0x00,   // DID 0xD542
        0x03, 0x22, 0x42, 0x1B, 0x00, 0x00, 0x00, 0x00,   // DID 0x421B
        0x03, 0x22, 0xF1, 0x90, 0x00, 0x00, 0x00, 0x00    // DID 0xF190
    ];

    ORDER      : ARRAY[0..2] OF BYTE := [0, 1, 2];
    TOTALORDER : BYTE := 3;
END_VAR;

Then configure each module with individual timing in the INIT block:

    // Fast polling for lights (time-critical)
    MODULES[0](ACTIVE := @ENABLE_LJUS, OUT_DATA := MODULE_0_DATA,
        OUT_DATA_LEN := MODULE_0_DATA_LEN, PAUSE := T#0ms, ...);

    // Slower polling for ignition (not time-critical)
    MODULES[1](ACTIVE := @ENABLE_TANDNING, OUT_DATA := MODULE_1_DATA,
        OUT_DATA_LEN := MODULE_1_DATA_LEN, PAUSE := T#250ms, ...);

    // Very slow polling for VIN (read once)
    MODULES[2](ACTIVE := @ENABLE_VIN, OUT_DATA := MODULE_2_DATA,
        OUT_DATA_LEN := MODULE_2_DATA_LEN, PAUSE := T#5s, ...);

PAUSE: Bandwidth Optimization

The PAUSE parameter controls how often each DID is polled. Use differentiated polling to optimize CAN bus load:

Signal TypePAUSEWhy
High beamT#0msMust respond instantly
ReverseT#250ms250ms delay is acceptable
IgnitionT#250ms or passive CANNot time-critical
Position lightsT#500msRarely changes
VIN / diagnosticT#5sRead occasionally

Timeout and State Preservation

Each DID_EXT module has a built-in TIMEOUT output that goes TRUE when the ECU stops responding (after ~2 seconds). When timeout occurs, the MODULE_X_DATA array is zeroed automatically.

You can use TIMEOUT to preserve signal states when the ECU is temporarily unavailable. For example, keep gear position at its last known value instead of resetting to zero:

// Only update gear signal when ECU is responding
IF NOT MODULES[1].TIMEOUT THEN
    SIGNAL_VXL_P := MODULE_1_DATA[3] = 0x06;
    SIGNAL_VXL_R := MODULE_1_DATA[3] = 0x0E;
    SIGNAL_VXL_D := MODULE_1_DATA[3] = 0x1E;
END_IF;
// When TIMEOUT is TRUE, signals keep their last value
// (useful for gear position - keeps "P" until ECU responds again)

ENABLE Flags and OUTPUT.USED

The ACTIVE pointer controls whether a DID is polled. In your recipe, you set the enable flag to only read DIDs when their outputs are actually connected:

// In your main recipe:
// Only poll the light DID when HIGHBEAM output is connected to a PowerUnit
// or when the app is connected (for monitoring)
ENABLE_LJUS := HIGHBEAM.USED OR HW.HOST_CONNECTED;

This saves CAN bandwidth - if no PowerUnit output is wired to HIGHBEAM, there is no point in polling the light ECU every 10ms.

Key Parameters Reference

ParameterTypeDescription
ACTIVEPOINTER TO BOOLEnable flag - pointer to ENABLE_XXX variable
TX_ENABLEPOINTER TO BOOLGlobal TX enable - usually @SIGNAL_TANDNING or @SIGNAL_SYSTEM_ON
OUT_DATAARRAY OF BYTEBuffer where response data is stored
OUT_DATA_LENINTHow many bytes to capture from response
PAUSETIMEDelay between requests (T#0ms = fastest)
INIT_PAUSETIMEDelay before first request after init
MSGSHIFTBYTE0 = standard, 1 = BMW (ECU ID in first byte)
EXTPAUSEBOOLUse extended flow control (for Dongle v1)
EXTPAUSE_TIMEBYTESTmin value in ms (typically 0x01)
LEN_OFFSETINTLength adjustment (0 for most, -3 for some VAG)
FDBOOLCAN-FD mode
BRSBOOLBit rate switch (CAN-FD)
TIMEOUTBOOLOutput: TRUE when ECU is not responding

Quick Reference: Signal Byte Index

Data byteMODULE_DATA index
Positive response (0x62)[0]
DID high byte[1]
DID low byte[2]
First data byte[3]
Second data byte[4]
Nth data byte[N + 3]

This mapping is the same for all manufacturers. MSGSHIFT handles BMW’s extra ECU ID byte internally.