Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Building Custom Diagnostic Tools ON the B&R PLC (CP1584)

Overview

One of the most powerful (and underutilized) capabilities of the B&R Automation Runtime (AR) platform is the ability to write custom C/C++ programs that execute directly on the PLC itself. The CP1584 runs Automation Runtime with a built-in GNU C/C++ compiler accessible through Automation Studio. This means you can build diagnostic tools, data loggers, custom monitors, and analysis programs that run in the real-time environment with direct access to IO, system variables, and the file system — all without any external PC connection.

This is critical when you are the sole maintainer of a machine built by a defunct OEM: you need tools that can run autonomously on the PLC, capturing data and diagnosing problems 24/7 without a laptop plugged in.

Why Build Tools ON the PLC?

  • Autonomous monitoring: Capture intermittent faults that happen at 3 AM with no one watching
  • No external dependencies: Works without a PC, OPC-UA client, or Python script running somewhere
  • Deterministic timing: Your diagnostic code runs in the real-time cycle with microsecond precision
  • Direct IO access: Read raw IO data at the bus cycle level, before any software filtering
  • Persistence: Log data to CF card or USB stick for later retrieval
  • Zero-intrusion: Can monitor the running program without modifying it (read-only access to variables)

Programming Languages Available

Automation Studio supports the following languages that execute on the PLC:

LanguageUse CaseNotes
ANSI CLow-level system access, performance-critical codeMost powerful for diagnostics; full POSIX-like API
C++Object-oriented design, complex data structuresRequires C++ options package in AS 3.0+
Structured Text (IEC 61131-3)Quick logic, calling function blocksCan mix freely with C programs
Ladder DiagramRelay logic, simple sequencingLimited for diagnostics
Sequential Function ChartState machine logicUseful for diagnostic state machines

For diagnostic tools, ANSI C is the strongest choice — it provides direct memory access, file system operations, and can call any function block in the system.

See ar-rtos.md for details on the underlying OS and execution-model.md for how tasks are scheduled.

Creating a C Program in Automation Studio

Step 1: Create a C POU

  1. In Automation Studio, right-click ProgramsAdd ObjectProgram
  2. Select ANSI C as the language
  3. Choose Function or Cyclic Cyclic type
  4. Name it (e.g., DiagnosticTool)

Step 2: Understand the C POU Structure

A cyclic C POU has three sections:

/* DiagnosticTool.c - Runs every cycle in its assigned task class */

#include <bur/plc.h>
#include <bur/plctypes.h>
#include <sys_lib.h>
#include <FileIO.h>
#include <ArEventLog.h>

#ifdef _DEFAULT_INCLUDES
    #include <AsDefault.h>
#endif

/* ============================================================
   INIT - Runs once on PLC startup (warm/cold start)
   ============================================================ */
void _INIT DiagnosticToolInit(void)
{
    /* Initialize diagnostic buffers, open log files, etc. */
}

/* ============================================================
   CYCLIC - Runs every cycle (main diagnostic loop)
   ============================================================ */
void _CYCLIC DiagnosticToolCyclic(void)
{
    /* This is your main diagnostic loop.
       It runs at the task class rate you assign.
       Keep it fast - don't block! */
}

/* ============================================================
   EXIT - Runs on PLC stop / program download
   ============================================================ */
void _EXIT DiagnosticToolExit(void)
{
    /* Close log files, flush buffers, cleanup */
}

Step 3: Assign to a Task Class

In the Logical View, assign your C POU to a task class:

  • Task Class 1 (typically 10 ms): For high-speed IO monitoring
  • Task Class 2 (typically 100 ms): For general diagnostics
  • Task Class 8 or background: For slow logging (avoid impacting control)

Critical rule: Your diagnostic code must complete within its cycle time. If it exceeds the watchdog time, the entire PLC will fault. Keep cyclic logic lean — do file I/O and complex processing in lower-priority tasks.

Step 4: Add Required Libraries

In the Configuration ViewLibraries, add the libraries you need:

  • sys_lib — System-level functions (time, string, memory)
  • FileIO — File operations on CF card / USB
  • ArEventLog — Write to the PLC’s event log
  • AsArSdm — SDM diagnostic data
  • AsOpcUac — OPC-UA client functionality
  • AsTcp — TCP/IP client/server
  • AsUdp — UDP communication

Accessing IO Data from C Programs

The safest and simplest way — map IO in the hardware configuration, then access the global variables directly from C:

/* If you have a digital input mapped to variable gDiSensor1 */
extern unsigned short gDiSensor1;
extern unsigned short gDoActuator1;

void _CYCLIC DiagnosticToolCyclic(void)
{
    static unsigned long cycle_count = 0;
    static unsigned long sensor_changes = 0;
    static unsigned short last_state = 0;

    if (gDiSensor1 != last_state) {
        sensor_changes++;
        last_state = gDiSensor1;

        /* Log the edge transition */
        LogSensorEdge(cycle_count, 0, gDiSensor1);
    }
    cycle_count++;
}

Method 2: Direct IO Data Pointers via AsIO Library

For accessing IO without mapped variables — useful when you don’t have the project’s IO mapping:

#include <AsIO.h>

void _CYCLIC ReadDirectIO(void)
{
    /* Enumerate all IO data points and read their values */
    PLCCHECKSUCCESS(AsIOListDP());

    /* After listing, you can iterate through IO data points
       using the AsIO data structures. Each DP has:
       - pVariable: pointer to the data value
       - dwIdent: the IO identifier
       - byDirection: input/output
       - szName: data point name
    */
}

The AsIOFListDP() variant filters to specific IO data points. See memory-map.md for the complete memory mapping scheme and io-card-hardware.md for IO module internals.

Method 3: Direct Memory Access (Advanced)

For the deepest access — reading raw memory addresses where IO is mapped:

#include <sys_lib.h>

/* Access IO via hardware addressing (software addressing must be
   configured in the CPU hardware configuration) */
void _CYCLIC ReadMemoryMappedIO(void)
{
    /* Access a known IO address - format depends on addressing mode
       Software addressing: %IX0.0.1.0 (input, module 0, channel 0)
       Hardware addressing: %IW0 (direct word access)

       In C, use the PV identifier from the variable declaration */
}

Reading System Variables (CPU Health, Timing, etc.)

B&R Automation Runtime exposes numerous system variables that give you insight into the PLC’s health. These are accessible from C as global structures:

Key System Variable Structures

#include <sys_lib.h>

void _CYCLIC ReadSystemHealth(void)
{
    /* CPU temperature (available on CP1584 with hardware monitoring) */
    /* Access via sys_br structure or mapped system variables */

    /* Task cycle time monitoring */
    /* Each task class has cycle time data available through
       the sys_task system variables */

    /* Watchdog status */
    /* Available through system diagnostic variables */

    /* Memory usage */
    /* SRAM/flash usage stats available at runtime */
}

Common System Variables to Monitor

Variable / StructureDescriptionNormal Range
sys_br.bWarmStartWarm start counterIncrement on restart
sys_br.bColdStartCold start counterIncrement on full reboot
Task cycle time varsActual cycle time per task classShould be < configured max
CPU temperatureHardware sensor readingTypically < 70°C
Memory utilizationHeap/stack usageMonitor for leaks
Network interface statsEthernet error countersZero errors ideal
Powerlink node statusEPL bus communication healthAll nodes present
IO module diagnosticsPer-module status bitsAll modules OK

See system-variables.md for the complete list and hardware-monitoring.md for temperature and voltage monitoring details. For CPU specs and physical hardware details, see cp1584-hardware-ref.md.

File System Operations (Logging to CF Card / USB)

Using the FileIO Library

The FileIO library provides function blocks for file operations on the PLC’s file system (CF card, USB stick, or network share):

#include <FileIO.h>

/* FileIO uses function blocks, not direct C calls.
   Declare and call them from your C POU: */

FileCreate    fcCreate;
FileOpen      foOpen;
FileWrite     fwWrite;
FileClose     fclClose;

static int log_file_open = 0;
static unsigned long log_handle = 0;

void _INIT OpenLogFile(void)
{
    fcCreate.pDevice = (UDINT)"/CF_CARD";
    fcCreate.pFileName = (UDINT)"diag_log.csv";
    fcCreate.ulOption = FILE_CREATE_ALWAYS;  /* Overwrite each restart */
    fcCreate.enable = 1;
    FCCreate(&fcCreate);  /* Call the function block */

    if (fcCreate.status == 0) {
        foOpen.pDevice = (UDINT)"/CF_CARD";
        foOpen.pFileName = (UDINT)"diag_log.csv";
        foOpen.ulOption = FILE_OPEN_WRITE;
        foOpen.enable = 1;
        FOOpen(&foOpen);

        if (foOpen.status == 0) {
            log_file_open = 1;
            log_handle = foOpen.ident;

            /* Write CSV header */
            fwWrite.ident = log_handle;
            fwWrite.pData = (UDINT)"Timestamp,Sensor1,Sensor2,Actuator1,CycleTime\n";
            fwWrite.ulLen = 52;
            fwWrite.enable = 1;
            FWWrite(&fwWrite);
        }
    }
}

void _CYCLIC LogDiagnosticData(void)
{
    static unsigned long write_counter = 0;
    static char buffer[256];

    if (log_file_open && (write_counter++ % 100 == 0)) {
        /* Log every 100th cycle to reduce file I/O overhead */
        snprintf(buffer, sizeof(buffer), "%lu,%d,%d,%d,%d\n",
                 cycle_count, gDiSensor1, gDiSensor2,
                 gDoActuator1, current_cycle_time);

        fwWrite.ident = log_handle;
        fwWrite.pData = (UDINT)buffer;
        fwWrite.ulLen = strlen(buffer);
        fwWrite.enable = 1;
        FWWrite(&fwWrite);
    }
}

File System Paths

DevicePathNotes
Internal CF card/CF_CARDDefault storage, limited write cycles
USB port 1/USB0 or /DEVICE0Hot-pluggable, good for log export
USB port 2/USB1 or /DEVICE1If available on CPU
Network shareConfigured mount pointRequires network share setup
RAM disk/RAMFast, volatile — lost on power cycle

Gotchas with File I/O on PLC

  1. CF Card Write Endurance: CompactFlash cards have finite write cycles. Do NOT log at high frequency to the CF card. Use RAM disk for high-speed logging, then periodically copy to CF card or USB.

  2. File Handle Limits: The PLC has a limited number of open file handles (typically 32-64). Always close files you’re not actively using.

  3. Blocking I/O: File writes block the calling task. Keep writes short and infrequent, or use a dedicated low-priority task for all file I/O.

  4. File System Synchronization: The AR file system buffers writes. Data may not be physically written to CF card immediately. Use FileFlush or close/reopen files to force a sync before power cycling.

  5. USB Stick Detection: The PLC may take 1-3 seconds to detect a newly inserted USB stick. Check for device availability before writing.

Using USB Sticks for Data Export

#include <FileIO.h>

void _CYCLIC CheckUSBAndLog(void)
{
    static int usb_available = 0;
    static int file_copied = 0;

    /* Check if USB stick is available by attempting to open a file */
    if (!usb_available) {
        foOpen.pDevice = (UDINT)"/USB0";
        foOpen.pFileName = (UDINT)"test.dat";
        foOpen.ulOption = FILE_OPEN_READ;
        foOpen.enable = 1;
        FOOpen(&foOpen);

        usb_available = (foOpen.status == 0);
        if (usb_available) {
            fclClose.ident = foOpen.ident;
            fclClose.enable = 1;
            FClose(&fclClose);
        }
    }

    /* When USB appears, copy diagnostic logs to it */
    if (usb_available && !file_copied) {
        /* Use FileCopy function block to copy from CF_CARD to USB0 */
        file_copied = 1;
    }
}

Community libraries simplify this:

  • FindUsbStickOnBAndRPlc — Automatically detects USB sticks and mounts them as FileIO devices
  • CSVFileLib — Easy CSV file read/write interface
  • mappCleanUp — Delete old log files by age or size

Writing to the PLC Event Log (ArEventLog)

The ArEventLog library lets you write custom diagnostic messages that appear in the PLC’s built-in Logger (visible in Automation Studio, SDM web interface, and downloadable via FTP).

#include <ArEventLog.h>

ArLogCreate       logCreate;
ArLogWriteMessage logWrite;
ArLogDelete       logDelete;

static unsigned long log_handle = 0;

void _INIT InitEventLogger(void)
{
    logCreate.pName = (UDINT)"MachineDiagnostic";
    logCreate.ulLogLevel = 0x00000001;  /* Level: Info */
    logCreate.enable = 1;
    ArLogCreate(&logCreate);

    if (logCreate.status == 0) {
        log_handle = logCreate.hLog;
    }
}

void _CYCLIC WriteDiagnosticEvents(void)
{
    static unsigned short last_module_ok = 1;

    if (gModuleOk != last_module_ok) {
        logWrite.hLog = log_handle;
        if (gModuleOk) {
            logWrite.ulLogLevel = 0x00000001;  /* Info */
            logWrite.pMessage = (UDINT)"Module recovered - status OK";
        } else {
            logWrite.ulLogLevel = 0x00000010;  /* Error */
            logWrite.pMessage = (UDINT)"Module fault detected!";
        }
        logWrite.enable = 1;
        ArLogWriteMessage(&logWrite);

        last_module_ok = gModuleOk;
    }
}

Log Levels

LevelHex ValueUse Case
Debug0x00Detailed trace info (usually filtered)
Info0x01Normal operational events
Warning0x08Recoverable anomalies
Error0x10Faults requiring attention
Fatal0x20Critical failures

Community Logging Libraries

  • LogThat — Simplified wrapper around ArEventLog with single-call functions
  • UserLog — Synchronous write to user logbooks (avoids async issues)
  • MyDiag — Combines ArEventLog with AsArSdm for comprehensive diagnostic logging

Exposing Diagnostic Data via OPC-UA

If you need external tools (Python scripts, SCADA, dashboards) to access your diagnostic data, expose it through OPC-UA:

#include <AsOpcUac.h>

/* Configure OPC-UA variables in the CPU configuration,
   then reference them in your C code as globals.

   The AsOpcUac library handles the server side automatically
   when OPC-UA is enabled in the hardware configuration.

   Your diagnostic variables just need to be declared as
   global PV (process variable) types, and they appear in
   the OPC-UA namespace. */

Simplified Approach Using Variable Declaration

The easiest way to expose diagnostic data via OPC-UA is to declare global variables with proper attributes. In the Variables tab of your C POU:

/* These globals are automatically exposed via OPC-UA
   if the variable has PV attributes set in the
   OPC-UA server configuration */

GLOBAL DiagData_t gDiagData;

Then in the OPC-UA server configuration (CPU properties → OPC-UA), add these variables to the namespace. External clients can subscribe to changes.

For more details on OPC-UA configuration, see opcua.md.

Exposing Diagnostic Data via Built-in Web Server

The CP1584 has an integrated HTTP server that can serve custom HTML/JS pages. You can build a diagnostic dashboard accessible from any web browser on the network.

Setting Up the Web Server

  1. In Automation Studio, open CPU ConfigurationWeb Server
  2. Enable the web server on the desired port (default 80)
  3. Configure the Web Server Directory (typically on the CF card)
  4. Place your HTML/CSS/JS files in the web directory

HTML + PV_Access.js Example

<!-- /CF_CARD/WebServer/diag.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Machine Diagnostics</title>
    <script src="PV_Access.js"></script>
</head>
<body>
    <h1>CP1584 Live Diagnostics</h1>
    <div>Cycle Count: <span id="cycleCount">--</span></div>
    <div>Sensor 1: <span id="sensor1">--</span></div>
    <div>CPU Temp: <span id="cpuTemp">--</span>°C</div>
    <div>Task 1 Cycle: <span id="task1Cycle">--</span> ms</div>
    <div>Module OK: <span id="moduleOk">--</span></div>

    <script>
        // Connect to PLC PV access
        var pv = new PVAccess();

        // Read variables by name (must be configured in OPC-UA or PV access)
        pv.readVariable("gDiagData.cycleCount", function(value) {
            document.getElementById("cycleCount").textContent = value;
        });

        pv.readVariable("gDiagData.sensor1", function(value) {
            document.getElementById("sensor1").textContent = value;
        });

        // Poll every 1 second
        setInterval(function() {
            pv.readVariable("gDiagData.cycleCount", function(value) {
                document.getElementById("cycleCount").textContent = value;
            });
        }, 1000);
    </script>
</body>
</html>

Community Resources

  • br-plc-as-webserver (GitHub) — Complete demo project for using the PLC as a web server with live data
  • Loupe UX — JavaScript library for web-based HMI on B&R PLCs
  • OMJSON — WebSocket + JSON interface for PLC communication

Network Communication (TCP/UDP)

For custom protocol diagnostics or sending data to external systems:

TCP Client/Server

#include <AsTcp.h>

/* AsTcp library provides TCP client and server function blocks
   Use TCP client to push diagnostic data to a remote server,
   or TCP server to allow external tools to query the PLC */

/* Example: Send diagnostic data to a remote monitoring server */
AsTcpClient tcpClient;

void _CYCLIC SendDiagToServer(void)
{
    static int connected = 0;

    if (!connected) {
        tcpClient.pServerAddr = (UDINT)"192.168.1.100";
        tcpClient.ulServerPort = 9000;
        tcpClient.enable = 1;
        AsTcpClient(&tcpClient);

        if (tcpClient.status == 0) connected = 1;
    }

    if (connected) {
        tcpClient.pData = (UDINT)diag_buffer;
        tcpClient.ulDataLen = buffer_length;
        tcpClient.enable = 1;
        AsTcpClient(&tcpClient);
    }
}

UDP Communication

#include <AsUdp.h>

/* UDP is lower overhead than TCP - good for broadcast diagnostics */

AsUdpSend udpSend;

void _CYCLIC BroadcastDiagStatus(void)
{
    static unsigned long counter = 0;

    if (++counter % 1000 == 0) {  /* Every 1000 cycles */
        snprintf((char*)diag_buffer, sizeof(diag_buffer),
                 "DIAG|%lu|%d|%d|%d",
                 cycle_count, gDiSensor1, gModuleOk, current_temp);

        udpSend.pRemoteAddr = (UDINT)"192.168.1.255";  /* Broadcast */
        udpSend.ulRemotePort = 9876;
        udpSend.pData = (UDINT)diag_buffer;
        udpSend.ulDataLen = strlen((char*)diag_buffer);
        udpSend.enable = 1;
        AsUdpSend(&udpSend);
    }
}

Community Libraries

  • TCPComm — Simplified TCP wrapper around AsTCP
  • UDPComm — Simplified UDP wrapper around AsUDP
  • AsUdp AsTcp demo1 — Official demo project for both libraries
  • paho.mqtt.c-ar — MQTT client for AR (publish diagnostic data to MQTT brokers)

Practical Diagnostic Tool Patterns

Pattern 1: High-Speed IO Edge Detector

Captures every state change on a digital input with microsecond timestamps:

#define MAX_EDGES 10000

typedef struct {
    unsigned long cycle_count;
    unsigned long timestamp_us;
    unsigned short old_state;
    unsigned short new_state;
} EdgeRecord_t;

static EdgeRecord_t edge_buffer[MAX_EDGES];
static unsigned long edge_index = 0;
static unsigned short last_state = 0;

void _CYCLIC EdgeDetectorCyclic(void)
{
    if (gDiSensor1 != last_state) {
        if (edge_index < MAX_EDGES) {
            edge_buffer[edge_index].cycle_count = cycle_count;
            edge_buffer[edge_index].timestamp_us = sys_br.ulCycleTime;  /* System tick */
            edge_buffer[edge_index].old_state = last_state;
            edge_buffer[edge_index].new_state = gDiSensor1;
            edge_index++;
        }
        last_state = gDiSensor1;
    }
}

/* Save to file in a lower-priority task when buffer is full
   or on a periodic basis (every N seconds) */
void _CYCLIC FlushEdgeBuffer(void)
{
    if (edge_index > 0 && (edge_index >= MAX_EDGES || flush_timer_expired)) {
        /* Open file and write edge buffer */
        for (unsigned long i = 0; i < edge_index; i++) {
            snprintf(line_buf, sizeof(line_buf), "%lu,%lu,%d,%d\n",
                     edge_buffer[i].cycle_count,
                     edge_buffer[i].timestamp_us,
                     edge_buffer[i].old_state,
                     edge_buffer[i].new_state);
            /* FileWrite... */
        }
        edge_index = 0;
    }
}

Pattern 2: Analog Signal Quality Monitor

Continuously tracks analog input statistics (min, max, average, noise, dropout detection):

#define ANALOG_SAMPLE_COUNT 1000

typedef struct {
    double min_val;
    double max_val;
    double avg_val;
    double noise_rms;      /* RMS of (value - avg) = noise floor */
    unsigned long dropout_count;  /* Number of times value dropped to zero */
    unsigned long spike_count;    /* Number of sudden large changes */
    double last_rate_of_change;
} AnalogStats_t;

static AnalogStats_t stats;
static double samples[ANALOG_SAMPLE_COUNT];
static unsigned long sample_idx = 0;

void _CYCLIC AnalogMonitorCyclic(void)
{
    double current = (double)gAiTemperature;  /* Read analog input */
    static double prev = 0.0;

    samples[sample_idx++] = current;
    if (sample_idx >= ANALOG_SAMPLE_COUNT) sample_idx = 0;

    /* Simple running statistics */
    if (current == 0.0) stats.dropout_count++;

    double rate = current - prev;
    if (fabs(rate) > (stats.noise_rms * 5 + 1.0)) {
        stats.spike_count++;
    }
    stats.last_rate_of_change = rate;
    prev = current;

    /* Recalculate full stats periodically (every 1000 cycles) */
    if (sample_idx == 0) {
        double sum = 0.0, min_v = 999999.0, max_v = -999999.0;
        for (unsigned long i = 0; i < ANALOG_SAMPLE_COUNT; i++) {
            sum += samples[i];
            if (samples[i] < min_v) min_v = samples[i];
            if (samples[i] > max_v) max_v = samples[i];
        }
        stats.avg_val = sum / ANALOG_SAMPLE_COUNT;
        stats.min_val = min_v;
        stats.max_val = max_v;

        double sq_sum = 0.0;
        for (unsigned long i = 0; i < ANALOG_SAMPLE_COUNT; i++) {
            double diff = samples[i] - stats.avg_val;
            sq_sum += diff * diff;
        }
        stats.noise_rms = sqrt(sq_sum / ANALOG_SAMPLE_COUNT);
    }
}

Pattern 3: Task Cycle Time Watchdog

Monitors all task classes for cycle time violations:

typedef struct {
    char task_name[32];
    unsigned long configured_cycle_us;
    unsigned long actual_cycle_us;
    unsigned long max_cycle_us;
    unsigned long watchdog_violations;
    unsigned long last_violation_cycle;
} TaskMonitor_t;

static TaskMonitor_t task_monitors[8];  /* Up to 8 task classes */

void _CYCLIC TaskCycleMonitor(void)
{
    /* Task cycle time data is available through system variables.
       The exact variable names depend on AR version, but typically:
       - sys_task[0].ulCycleTime (actual cycle time in microseconds)
       - sys_task[0].ulCycleTimeMax (maximum cycle time observed)
       - sys_task[0].ulWatchdogTime (configured watchdog limit)

       Check each task class every cycle. */
    for (int i = 0; i < 8; i++) {
        unsigned long actual = /* sys_task[i].ulCycleTime */;
        unsigned long limit = /* sys_task[i].ulWatchdogTime */;

        if (actual > task_monitors[i].max_cycle_us) {
            task_monitors[i].max_cycle_us = actual;
        }

        if (actual > limit * 80 / 100) {  /* 80% of limit = warning */
            task_monitors[i].watchdog_violations++;
            task_monitors[i].last_violation_cycle = cycle_count;

            /* Log warning */
            LogWriteMessage(LOG_WARN, "Task %d near watchdog: %lu us",
                          i, actual);
        }
    }
}

Pattern 4: Periodic Diagnostic Report Generator

Writes a comprehensive diagnostic snapshot to file every N minutes:

void _CYCLIC DiagnosticReportGenerator(void)
{
    static unsigned long report_timer = 0;
    static const unsigned long REPORT_INTERVAL_CYCLES = 60000; /* ~10 min at 10ms */

    report_timer++;
    if (report_timer < REPORT_INTERVAL_CYCLES) return;
    report_timer = 0;

    char report[2048];
    int offset = 0;

    offset += snprintf(report + offset, sizeof(report) - offset,
        "=== DIAGNOSTIC REPORT ===\n"
        "Timestamp: %lu\n"
        "Cycle Count: %lu\n"
        "Uptime: %lu seconds\n\n",
        GetSystemTime(), cycle_count, uptime_seconds);

    offset += snprintf(report + offset, sizeof(report) - offset,
        "--- Digital Inputs ---\n"
        "Sensor1: %d  Sensor2: %d  Sensor3: %d\n"
        "E-Stop: %d  SafetyOK: %d\n\n",
        gDiSensor1, gDiSensor2, gDiSensor3,
        gDiEStop, gSafetyOK);

    offset += snprintf(report + offset, sizeof(report) - offset,
        "--- Analog Inputs ---\n"
        "Temperature: %.1f °C  (min:%.1f max:%.1f noise:%.3f)\n"
        "Pressure: %.1f bar\n"
        "Level: %.1f mm\n\n",
        gAiTemperature, stats.min_val, stats.max_val, stats.noise_rms,
        (double)gAiPressure, (double)gAiLevel);

    offset += snprintf(report + offset, sizeof(report) - offset,
        "--- IO Module Status ---\n"
        "Module OK: %d  Module ID: %d\n"
        "X2X Bus Status: %d  EPL Status: %d\n\n",
        gModuleOk, gModuleID, gX2XStatus, gEPLStatus);

    offset += snprintf(report + offset, sizeof(report) - offset,
        "--- Task Health ---\n"
        "Task1 cycle: %lu us (max: %lu us)\n"
        "Task2 cycle: %lu us (max: %lu us)\n"
        "Watchdog violations: %lu\n\n",
        task_monitors[0].actual_cycle_us, task_monitors[0].max_cycle_us,
        task_monitors[1].actual_cycle_us, task_monitors[1].max_cycle_us,
        total_watchdog_violations);

    /* Write to file */
    FileWrite(...report, offset...);
}

Building a Complete Diagnostic Task Architecture

Task Class 1 (1 ms or 10 ms) - CONTROL LOGIC + Edge Detection
  ├── Machine control program (existing)
  └── Edge detector (minimal overhead, just captures transitions)

Task Class 2 (10 ms or 100 ms) - MAIN DIAGNOSTICS
  ├── Analog signal monitor
  ├── Task cycle time monitor
  ├── IO module health check
  └── Statistics accumulator

Task Class 4 or 8 (1000 ms) - LOGGING & REPORTING
  ├── File I/O (CSV logging, report generation)
  ├── Event log writing (ArEventLog)
  ├── UDP broadcast of status
  └── USB stick check and data export

Data Flow

IO Hardware → Task 1 (raw capture) → Task 2 (analysis) → Task 8 (storage/output)
                                         ↓
                                    OPC-UA Server (external access)
                                    Web Server (browser access)
                                    UDP Broadcast (network monitoring)

Working Without the Original Project

When you don’t have the original Automation Studio project, building diagnostic tools requires extra steps. See project-reconstruction.md for the full process, but here’s the diagnostic-specific approach:

Step 1: Create a Minimal Project

  1. Install Automation Studio (any version compatible with the AR version on the PLC)
  2. Create a new project with just the CPU (X20CP1584) configured
  3. Set the correct IP address and hardware configuration
  4. Download this minimal project — it won’t run the machine, but it gives you AR access

Step 2: Use Online Browsing

Connect to the running PLC and browse:

  • Online → Variables: See all variables with their current values
  • Online → IO Mapping: See all mapped IO points
  • Online → Task Configuration: See all running task classes and their cycle times
  • SDM (web browser): See complete hardware and software diagnostics

Step 3: Reconstruct IO Mapping

Use brwatch or PVI to read IO values and map them to meaningful names based on:

  • Physical inspection of the machine
  • Wire labeling
  • Behavior observation
  • Cross-referencing with similar B&R machines

Step 4: Add Your Diagnostic C Program

Add your diagnostic C POU to this minimal project. Configure it in a task class. Download with online changes if the machine must keep running.

Step 5: Use OPC-UA or Web Server for Data Access

Since you can’t add your variables to the original program’s OPC-UA namespace easily, expose your diagnostic data through:

  • Your own OPC-UA server instance
  • The built-in web server with PV_Access
  • UDP broadcast
  • Direct file logging to CF card / USB

External Tools That Complement On-PLC Diagnostics

These tools run on a PC but work with data your on-PLC tools produce:

ToolDescriptionSource
brwatchCommand-line variable watch/change/log toolGitHub
SystemDumpViewerViewer for SystemDump.xml files from PLCCommunity
simple data traceRecords PLC variables in high-priority task, saves to CSVCommunity
PVI.pyPython wrapper for B&R PVI APICommunity
systemdump.pyCLI tool to create/load system dumpsCommunity

See pvi-api.md for external PC-based programmatic access and python-diagnostics.md for Python-based diagnostic scripting.

Advanced Techniques

Ring Buffer Data Capture

For capturing high-speed data before a fault (like a flight data recorder’s black box):

#define RING_BUFFER_SIZE 100000
#define TRIGGER_CHANNELS 4

typedef struct {
    unsigned long timestamp;
    unsigned short values[TRIGGER_CHANNELS];
} RingSample_t;

static RingSample_t ring_buffer[RING_BUFFER_SIZE];
static unsigned long ring_write_idx = 0;
static unsigned long ring_full = 0;

void _CYCLIC RingBufferCapture(void)
{
    ring_buffer[ring_write_idx].timestamp = cycle_count;
    ring_buffer[ring_write_idx].values[0] = gDiSensor1;
    ring_buffer[ring_write_idx].values[1] = gDiSensor2;
    ring_buffer[ring_write_idx].values[2] = gDoActuator1;
    ring_buffer[ring_write_idx].values[3] = gModuleStatus;

    ring_write_idx++;
    if (ring_write_idx >= RING_BUFFER_SIZE) {
        ring_write_idx = 0;
        ring_full = 1;
    }
}

/* Trigger: save ring buffer to file when a fault is detected */
void _CYCLIC FaultTrigger(void)
{
    if (gFaultDetected) {
        SaveRingBufferToFile();
        gFaultDetected = 0;  /* Reset trigger */
    }
}

JSON Diagnostic Output (via OMSQL/OMJSON)

The OMJSON community library lets you build JSON strings on the PLC, useful for REST API-like diagnostic output:

/* Using OMSQL/OMJSON to build structured diagnostic output
   that can be consumed by external systems */

void _CYCLIC BuildJsonDiagnostic(void)
{
    /* Build JSON: {"sensor1":true,"temp":23.5,"cycle":1234567,"faults":0} */
    /* OMJSON library provides serialize/deserialize functions */
}

MQTT Diagnostic Publishing

The paho.mqtt.c-ar library (Eclipse Paho MQTT ported to Automation Runtime) lets you publish diagnostic data to an MQTT broker:

/* Using paho.mqtt.c-ar library:
   - Connect to an MQTT broker (local or cloud)
   - Publish diagnostic topics:
     machine/cp1584/digital_inputs
     machine/cp1584/analog_inputs
     machine/cp1584/task_health
     machine/cp1584/io_status
   - Subscribe to commands:
     machine/cp1584/commands/reset_counters
     machine/cp1584/commands/force_output
*/

This is especially useful for IIoT retrofit scenarios.

Safety and Caution

What Can Go Wrong

  1. PLC crash from bad pointers: A NULL pointer dereference or invalid memory access in your C code will cause a page fault. The PLC will stop all task classes and enter service mode. Always validate pointers before accessing them.

  2. Watchdog violation from slow code: If your diagnostic code takes too long in a high-priority task, the hardware watchdog will trigger and reboot the PLC. Profile your code carefully.

  3. CF card wearout from excessive logging: CompactFlash cards have limited write cycles (typically 10,000-100,000 per sector). Log to RAM disk or reduce logging frequency.

  4. Memory exhaustion: Dynamic memory allocation (malloc) on a PLC is dangerous. Memory leaks accumulate and eventually crash the PLC. Use static buffers with fixed sizes.

  5. Interference with control logic: Your diagnostic code runs in the same AR environment as the machine control program. Never modify outputs or control variables from diagnostic code unless absolutely necessary.

Best Practices

  • Read-only access: Design all diagnostic tools as read-only monitors. Never write to outputs from diagnostic code unless it’s a deliberate override with safety interlocks.
  • Static allocation: Avoid malloc()/free(). Use static arrays with known sizes.
  • Error handling: Every function block call should check its status. Never assume success.
  • Graceful degradation: If file I/O fails, continue monitoring in memory. If memory is full, stop capturing but keep the most recent data.
  • Minimal overhead: A diagnostic tool on a production machine should use < 5% of the CPU’s processing capacity.
  • Isolation: Put diagnostic code in its own task class, not mixed into the control task.

Key Findings

  1. ANSI C is the most powerful tool for on-PLC diagnostics — it gives you direct memory access, file system operations, and the ability to call any function block in the system.

  2. Use the layered task architecture — fast capture in high-priority tasks, analysis in mid-priority, file I/O and networking in low-priority background tasks.

  3. File I/O is the biggest risk — CF card wearout and blocking I/O in cyclic tasks are the two most common causes of problems. Always use a dedicated low-priority task for file operations.

  4. The ecosystem is surprisingly rich — community libraries (CSVFileLib, LogThat, UserLog, paho.mqtt.c-ar, TCPComm, OMJSON, Persist, FindUsbStickOnBAndRPlc, PLC-data-trace, mappRemoteShell) fill most gaps that the standard libraries don’t cover.

  5. VS Code can replace Automation Studio for editing — the community B&R Automation Tools extension (github.com/br-automation-com/vscode-brautomationtools) provides IntelliSense for C/C++ programs, build tasks via BR.AS.Build.exe, and PLC transfer via PVITransfer.exe. Combined with the Structured Text VS Code extension, you can develop and deploy diagnostic programs entirely from VS Code. See remanufacturing.md §“Development Tools Update” for the full tool list.

  6. The awesome-B-R community repository was archived in April 2025 — the curated list at github.com/hilch/awesome-B-R is now read-only. Active development continues in individual repositories. Use the archived list as a starting reference, then check individual repos for current status.

  7. You can build a complete diagnostic dashboard using the built-in web server with PV_Access.js — no external software needed, just a web browser.

  8. Ring buffers are your friend for intermittent faults — capture high-speed data continuously and dump to file only when a fault is detected (black box approach).

  9. Without the original project, create a minimal AS project, connect online, browse variables, reconstruct IO mapping, and add your diagnostic program as an additional task. The existing program will continue to run alongside it.

Cross-References