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:
| Language | Use Case | Notes |
|---|---|---|
| ANSI C | Low-level system access, performance-critical code | Most powerful for diagnostics; full POSIX-like API |
| C++ | Object-oriented design, complex data structures | Requires C++ options package in AS 3.0+ |
| Structured Text (IEC 61131-3) | Quick logic, calling function blocks | Can mix freely with C programs |
| Ladder Diagram | Relay logic, simple sequencing | Limited for diagnostics |
| Sequential Function Chart | State machine logic | Useful 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
- In Automation Studio, right-click Programs → Add Object → Program
- Select ANSI C as the language
- Choose Function or Cyclic Cyclic type
- 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 View → Libraries, add the libraries you need:
sys_lib— System-level functions (time, string, memory)FileIO— File operations on CF card / USBArEventLog— Write to the PLC’s event logAsArSdm— SDM diagnostic dataAsOpcUac— OPC-UA client functionalityAsTcp— TCP/IP client/serverAsUdp— UDP communication
Accessing IO Data from C Programs
Method 1: Through Mapped Variables (Recommended)
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 / Structure | Description | Normal Range |
|---|---|---|
sys_br.bWarmStart | Warm start counter | Increment on restart |
sys_br.bColdStart | Cold start counter | Increment on full reboot |
| Task cycle time vars | Actual cycle time per task class | Should be < configured max |
| CPU temperature | Hardware sensor reading | Typically < 70°C |
| Memory utilization | Heap/stack usage | Monitor for leaks |
| Network interface stats | Ethernet error counters | Zero errors ideal |
| Powerlink node status | EPL bus communication health | All nodes present |
| IO module diagnostics | Per-module status bits | All 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
| Device | Path | Notes |
|---|---|---|
| Internal CF card | /CF_CARD | Default storage, limited write cycles |
| USB port 1 | /USB0 or /DEVICE0 | Hot-pluggable, good for log export |
| USB port 2 | /USB1 or /DEVICE1 | If available on CPU |
| Network share | Configured mount point | Requires network share setup |
| RAM disk | /RAM | Fast, volatile — lost on power cycle |
Gotchas with File I/O on PLC
-
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.
-
File Handle Limits: The PLC has a limited number of open file handles (typically 32-64). Always close files you’re not actively using.
-
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.
-
File System Synchronization: The AR file system buffers writes. Data may not be physically written to CF card immediately. Use
FileFlushor close/reopen files to force a sync before power cycling. -
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 devicesCSVFileLib— Easy CSV file read/write interfacemappCleanUp— 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
| Level | Hex Value | Use Case |
|---|---|---|
| Debug | 0x00 | Detailed trace info (usually filtered) |
| Info | 0x01 | Normal operational events |
| Warning | 0x08 | Recoverable anomalies |
| Error | 0x10 | Faults requiring attention |
| Fatal | 0x20 | Critical 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
- In Automation Studio, open CPU Configuration → Web Server
- Enable the web server on the desired port (default 80)
- Configure the Web Server Directory (typically on the CF card)
- 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
Recommended Task Layout
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
- Install Automation Studio (any version compatible with the AR version on the PLC)
- Create a new project with just the CPU (X20CP1584) configured
- Set the correct IP address and hardware configuration
- 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:
| Tool | Description | Source |
|---|---|---|
| brwatch | Command-line variable watch/change/log tool | GitHub |
| SystemDumpViewer | Viewer for SystemDump.xml files from PLC | Community |
| simple data trace | Records PLC variables in high-priority task, saves to CSV | Community |
| PVI.py | Python wrapper for B&R PVI API | Community |
| systemdump.py | CLI tool to create/load system dumps | Community |
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
-
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.
-
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.
-
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.
-
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.
-
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
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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).
-
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
- ar-rtos.md — Automation Runtime OS internals
- execution-model.md — Task scheduling and priorities
- memory-map.md — CPU memory layout and addressing
- io-card-hardware.md — IO module hardware details
- system-variables.md — Complete system variable listing
- pvi-api.md — External programmatic PLC access (PVI)
- opcua.md — OPC-UA server configuration
- python-diagnostics.md — Python-based external diagnostic scripts
- iiot-retrofit.md — MQTT and modern monitoring retrofits
- project-reconstruction.md — Rebuilding a project from an unknown PLC
- cf-card-boot.md — CF card file system layout
- hardware-monitoring.md — Temperature and voltage monitoring