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.
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;
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 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 Type | PAUSE | Why |
|---|---|---|
| High beam | T#0ms | Must respond instantly |
| Reverse | T#250ms | 250ms delay is acceptable |
| Ignition | T#250ms or passive CAN | Not time-critical |
| Position lights | T#500ms | Rarely changes |
| VIN / diagnostic | T#5s | Read 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
| Parameter | Type | Description |
|---|---|---|
ACTIVE | POINTER TO BOOL | Enable flag - pointer to ENABLE_XXX variable |
TX_ENABLE | POINTER TO BOOL | Global TX enable - usually @SIGNAL_TANDNING or @SIGNAL_SYSTEM_ON |
OUT_DATA | ARRAY OF BYTE | Buffer where response data is stored |
OUT_DATA_LEN | INT | How many bytes to capture from response |
PAUSE | TIME | Delay between requests (T#0ms = fastest) |
INIT_PAUSE | TIME | Delay before first request after init |
MSGSHIFT | BYTE | 0 = standard, 1 = BMW (ECU ID in first byte) |
EXTPAUSE | BOOL | Use extended flow control (for Dongle v1) |
EXTPAUSE_TIME | BYTE | STmin value in ms (typically 0x01) |
LEN_OFFSET | INT | Length adjustment (0 for most, -3 for some VAG) |
FD | BOOL | CAN-FD mode |
BRS | BOOL | Bit rate switch (CAN-FD) |
TIMEOUT | BOOL | Output: TRUE when ECU is not responding |
Quick Reference: Signal Byte Index
| Data byte | MODULE_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.