Skip to main content

Command Palette

Search for a command to run...

A Hands-On Introduction to OPC UA: Turning a Raspberry Pi 5 into a PLC

Monitor and Write Variables—and Control an LED—with CODESYS, UaExpert, and GPIO

Updated
24 min readView as Markdown
A Hands-On Introduction to OPC UA: Turning a Raspberry Pi 5 into a PLC
Y
Factory Automation | Manufacturing Engineering | Validation for Medical Device

OPC UA is often described as an “industrial communication standard” or a “secure information model.” Those descriptions are accurate, but they can still feel a little abstract. OPC UA is widely considered an important communication standard for Industry 4.0, its use in manufacturing continues to grow, and it is likely to remain valuable for years to come.

In this article, we will turn a Raspberry Pi 5 into a SoftPLC with CODESYS and connect to it from UaExpert on a Windows PC using OPC UA. By the end, we will use the PLC to light an LED connected to GPIO18 and read a push-button input connected to GPIO23.

What We Will Build

The completed setup will look like this:


1. What Is OPC UA?

1.1 OPC UA Exchanges Industrial Data with Meaning and Context

OPC UA is a standard for exchanging data between devices and software in industrial automation. It is designed to work securely while reducing dependence on a particular vendor or operating system.

For example, imagine that a PLC contains the following variables:

rProcessValue = Current process value
rSetpoint     = Target value
xOutput       = Output state
xAlarmActive  = Current alarm state

Instead of exposing only register numbers, OPC UA can present the data as a meaningful tree like this:

Objects
└─ MachineData
   └─ GVL_Machine
      ├─ rProcessValue
      ├─ rSetpoint
      ├─ xOutput
      └─ xAlarmActive

A client browses this tree to find the variable it needs.

Each variable is represented as a Node. A node contains not only a value, but also information such as:

  • NodeId

  • BrowseName / DisplayName

  • Data type

  • Read and write permissions

  • StatusCode

  • SourceTimestamp

  • ServerTimestamp

This ability to give values both structure and meaning is one of the most important ideas to understand about OPC UA.

1.2 Read, Write, and Subscription

At a minimum, it helps to distinguish between the following basic OPC UA operations.

Read

The client asks for the value at that moment.

Client: What is the current temperature?
Server: It is 42.3.

Write

The client sets a value on a variable for which writing is permitted.

Client: Change the setpoint to 60.0.
Server: The write request has been accepted.

Subscription / MonitoredItem

The client registers a variable to monitor, and the server sends a notification when its value changes.

Client: I want to monitor this variable.
Server: Its value has changed, so here is a notification.

2. What You Need

2.1 Hardware

  • Raspberry Pi 5

  • microSD card or NVMe SSD

  • A stable power supply suitable for the Raspberry Pi 5

  • A Windows 11 PC

  • Ethernet cable, recommended

  • One LED

  • One resistor of approximately 330 Ω

  • One tactile push button

  • One 10 kΩ resistor

  • Breadboard

  • Jumper wires

2.2 Software

  • Raspberry Pi OS Lite, 64-bit

  • CODESYS Development System

  • CODESYS Communication add-on

  • CODESYS Control for Raspberry Pi SL

  • CODESYS Control SL Deploy Tool

  • UaExpert

For a Raspberry Pi 5 running a 64-bit OS, use CODESYS Control for Raspberry Pi 64 SL as the project device.

Without a license, CODESYS Control for Raspberry Pi SL runs without functional limitations for two hours after startup and then stops as part of its demo behavior. Check the licensing terms before using it for long-running or commercial applications.


3. Prepare Raspberry Pi OS

Use Raspberry Pi Imager to install Raspberry Pi OS with settings similar to the following:

Device: Raspberry Pi 5
OS: Raspberry Pi OS Lite (64-bit)
Hostname: Any name you prefer, for example yfpi
Username: Any username you prefer
Password: A strong password
SSH: Enabled
Timezone: Asia/Tokyo

4. Turn the Raspberry Pi 5 into a CODESYS SoftPLC

4.1 Install CODESYS on Windows

Install CODESYS Development System.

Download CODESYS

Next, use CODESYS Installer to add at least the following packages:

CODESYS Control for Raspberry Pi SL
CODESYS Control SL Deploy Tool
CODESYS Communication
CODESYS Security Agent

If Communication Manager does not appear under Application → Add Object, check that the CODESYS Communication add-on is installed. According to the CODESYS workflow, the add-on makes it possible to add a Communication Manager and an OPC UA Server below the Application object.

4.2 Install the CODESYS Runtime on the Pi

In CODESYS, open:

Tools
└─ Deploy Control SL

Connect to the Raspberry Pi over SSH:

IP address: <Pi IP address>
Port: 22
Username: Raspberry Pi username
Password: Raspberry Pi password

Select CODESYS Control for Raspberry Pi SL as the product and install the runtime on the Pi.

At the time this article was written, the latest version used in this environment was 4.21.0.0. As much as possible, keep the Windows package, the runtime installed on the Pi, and the device description used by the project within the same version generation.

4.3 Create a New PLC Project

In CODESYS, select:

File
└─ New Project
   └─ Standard Project

Use the following settings:

Device:
CODESYS Control for Raspberry Pi 64 SL

PLC_PRG language:
Structured Text (ST)

5. Start with a Simulated Machine—No GPIO Yet

If you try to configure GPIO, certificates, and OPC UA all at once, troubleshooting quickly becomes difficult.

We will begin by creating a simulated machine in the PLC program with the following behavior:

  • A Start command begins operation.

  • A Stop command stops operation.

  • The output turns on when the process value is below the setpoint.

  • The output turns off when the process value is above the setpoint.

  • An alarm is raised when the high limit is exceeded.

  • The alarm is latched and must be reset explicitly.

  • Setpoints and commands can be written through OPC UA.

  • Later, xOutput will be mapped to GPIO18.

5.1 Create a GVL

Right-click Application and select:

Add Object
└─ Global Variable List

Name it GVL_Machine.

VAR_GLOBAL
    // Commands operated from an OPC UA client
    xStartCmd          : BOOL := FALSE;
    xStopCmd           : BOOL := FALSE;
    xResetAlarmCmd     : BOOL := FALSE;

    xAutoMode          : BOOL := TRUE;
    xManualOutputCmd   : BOOL := FALSE;

    rSetpoint          : REAL := 50.0;
    rHighLimit         : REAL := 80.0;

    // States published by the PLC
    xRunning           : BOOL := FALSE;
    xOutput            : BOOL := FALSE;
    xAlarmActive       : BOOL := FALSE;
    xAlarmLatched      : BOOL := FALSE;
    xHeartbeat         : BOOL := FALSE;

    rProcessValue      : REAL := 20.0;
    udiCycleCounter    : UDINT := 0;

    // This will later be mapped to GPIO23.
    // The input is assumed to be active-low.
    xPhysicalButtonRaw : BOOL := TRUE;
END_VAR

5.2 Write PLC_PRG

Enter the following code in PLC_PRG:

PROGRAM PLC_PRG
VAR
    xRunLatch      : BOOL := FALSE;

    fbSimTick      : TON;
    fbHeartbeat    : TON;

    fbButtonFilter : TON;
    rtButton       : R_TRIG;
END_VAR


// -----------------------------
// PLC scan counter
// -----------------------------
GVL_Machine.udiCycleCounter :=
    GVL_Machine.udiCycleCounter + 1;


// -----------------------------
// Physical button processing
// The GPIO input is assumed to be active-low.
// -----------------------------
fbButtonFilter(
    IN := NOT GVL_Machine.xPhysicalButtonRaw,
    PT := T#50ms
);

rtButton(CLK := fbButtonFilter.Q);


// -----------------------------
// Start and stop logic
// OPC UA commands are treated as one-shot commands.
// -----------------------------
IF GVL_Machine.xStartCmd OR rtButton.Q THEN
    xRunLatch := TRUE;
    GVL_Machine.xStartCmd := FALSE;
END_IF;

IF GVL_Machine.xStopCmd THEN
    xRunLatch := FALSE;
    GVL_Machine.xStopCmd := FALSE;
END_IF;


// -----------------------------
// Limit the configuration values to valid ranges.
// -----------------------------
IF GVL_Machine.rHighLimit < 20.0 THEN
    GVL_Machine.rHighLimit := 20.0;

ELSIF GVL_Machine.rHighLimit > 100.0 THEN
    GVL_Machine.rHighLimit := 100.0;
END_IF;


IF GVL_Machine.rSetpoint < 5.0 THEN
    GVL_Machine.rSetpoint := 5.0;

ELSIF GVL_Machine.rSetpoint >
      (GVL_Machine.rHighLimit - 5.0) THEN

    GVL_Machine.rSetpoint :=
        GVL_Machine.rHighLimit - 5.0;
END_IF;


// -----------------------------
// High-temperature alarm
// -----------------------------
GVL_Machine.xAlarmActive :=
    GVL_Machine.rProcessValue >=
    GVL_Machine.rHighLimit;

IF GVL_Machine.xAlarmActive THEN
    GVL_Machine.xAlarmLatched := TRUE;
END_IF;


// Require a 5-degree margin before allowing an alarm reset.
IF GVL_Machine.xResetAlarmCmd THEN

    IF GVL_Machine.rProcessValue <=
       (GVL_Machine.rHighLimit - 5.0) THEN

        GVL_Machine.xAlarmLatched := FALSE;
    END_IF;

    GVL_Machine.xResetAlarmCmd := FALSE;
END_IF;


// -----------------------------
// Automatic and manual output control
// -----------------------------
IF xRunLatch AND NOT GVL_Machine.xAlarmLatched THEN

    IF GVL_Machine.xAutoMode THEN

        // Hysteresis control with a ±1.0 band
        IF GVL_Machine.rProcessValue <=
           (GVL_Machine.rSetpoint - 1.0) THEN

            GVL_Machine.xOutput := TRUE;

        ELSIF GVL_Machine.rProcessValue >=
              (GVL_Machine.rSetpoint + 1.0) THEN

            GVL_Machine.xOutput := FALSE;
        END_IF;

    ELSE
        GVL_Machine.xOutput :=
            GVL_Machine.xManualOutputCmd;
    END_IF;

ELSE
    GVL_Machine.xOutput := FALSE;
END_IF;


// -----------------------------
// Simulated process with a 100 ms update interval
// -----------------------------
fbSimTick(
    IN := NOT fbSimTick.Q,
    PT := T#100ms
);

IF fbSimTick.Q THEN

    IF GVL_Machine.xOutput THEN
        // Output ON: the process value rises.
        GVL_Machine.rProcessValue :=
            GVL_Machine.rProcessValue + 0.40;

    ELSE
        // Output OFF: the process value falls.
        GVL_Machine.rProcessValue :=
            GVL_Machine.rProcessValue - 0.10;
    END_IF;

    IF GVL_Machine.rProcessValue > 100.0 THEN
        GVL_Machine.rProcessValue := 100.0;

    ELSIF GVL_Machine.rProcessValue < 0.0 THEN
        GVL_Machine.rProcessValue := 0.0;
    END_IF;
END_IF;


// -----------------------------
// One-second heartbeat
// -----------------------------
fbHeartbeat(
    IN := NOT fbHeartbeat.Q,
    PT := T#1s
);

IF fbHeartbeat.Q THEN
    GVL_Machine.xHeartbeat :=
        NOT GVL_Machine.xHeartbeat;
END_IF;


GVL_Machine.xRunning := xRunLatch;

5.3 Set MainTask to a 20 ms Cycle

Open Task Configuration → MainTask.

Type: Cyclic
Interval: T#20ms

Keep in mind that the following three cycles are separate:

PLC scan cycle:            20 ms
Simulated process update:  100 ms
OPC UA notification/view:  Approximately 250–1000 ms

Reducing the OPC UA update interval does not mean that the control logic itself runs through OPC UA. The PLC executes its control program cyclically inside the runtime, while OPC UA exposes selected variables to external systems.

5.4 Download the Project to the Pi and Run It

Double-click the top-level Device and select the Pi under Communication Settings.

Scan Network
or enter the IP address directly
→ Set Active Path

Then run the following commands:

Build
└─ Build

Online
└─ Login

Debug
└─ Start

Open GVL_Machine online and confirm the following:

udiCycleCounter → Continues increasing
xHeartbeat      → Alternates between TRUE and FALSE about once per second

6. Publish PLC Variables through OPC UA

6.1 Add a Communication Manager

Right-click Application and select:

Add Object
└─ Communication Manager

Right-click the new Communication Manager and select:

Add Object
└─ OPC UA Server

Next, right-click OPC UA Server and select:

Add Object
└─ IEC Symbol Publishing

Name the IEC Symbol Publishing object MachineData.

The final object tree should look like this:

Application
├─ GVL_Machine
├─ PLC_PRG
├─ Task Configuration
└─ Communication Manager
   └─ OPC UA Server
      └─ MachineData
         (IEC Symbol Publishing)

In CODESYS, adding IEC Symbol Publishing below the OPC UA Server in the Communication Manager lets you publish project variables and IEC data types.

6.2 Add Individual Variables in the IEC Symbols Editor

Double-click MachineData and open the IEC Symbols Editor tab.

From Precompile Sets on the left, drag the individual variables you want to publish into the area on the right.

Start with the following variables:

GVL_Machine.xStartCmd
GVL_Machine.xStopCmd
GVL_Machine.xResetAlarmCmd
GVL_Machine.xAutoMode
GVL_Machine.xManualOutputCmd
GVL_Machine.rSetpoint
GVL_Machine.rHighLimit

GVL_Machine.xRunning
GVL_Machine.xOutput
GVL_Machine.xAlarmActive
GVL_Machine.xAlarmLatched
GVL_Machine.xHeartbeat
GVL_Machine.rProcessValue
GVL_Machine.udiCycleCounter

Variables That Clients May Write

xStartCmd
xStopCmd
xResetAlarmCmd
xAutoMode
xManualOutputCmd
rSetpoint
rHighLimit

Set their access rights to:

Read/Write

Read-Only Variables

xRunning
xOutput
xAlarmActive
xAlarmLatched
xHeartbeat
rProcessValue
udiCycleCounter

Set their access rights to:

Read

During the first connection test, if the following option is available, temporarily turning it off can make troubleshooting easier. It helps separate user-management permission problems from symbol-publishing configuration problems.

Use access rights defined in the user management = OFF

After the basic connection works, configure user management and grant only the permissions that are actually needed.

Important troubleshooting tip
In this setup, UaExpert did not show the expected variables when only the GVL name was registered. That is why this article adds each variable individually. Make sure the right-hand side contains one row per variable, rather than a single row named GVL_Machine.

The CODESYS IEC Symbols Editor follows the same basic workflow: drag individual variables from Precompile Sets on the left into the publishing list on the right.

6.3 Refresh and Sync

After adding or changing variables, click Refresh at the top of the IEC Symbols Editor.

If error, warning, or information icons appear, check their tooltips. If Sync is available, run it as well.

The editor refers to the project's precompiled state, so changing variables later can cause the publishing configuration to become inconsistent.[^codesys-symbol-publishing]

6.4 Perform a Full Rebuild and Download

After changing the symbol-publishing configuration, a full download is more reliable than relying only on an online change.

Debug → Stop
Online → Logout
Build → Clean All
Build → Rebuild
Online → Login
Download
Debug → Start

If MachineData does not appear in UaExpert, a full rebuild and download may resolve the problem.


7. Create an OPC UA Server Certificate

7.1 Do Not Confuse It with the Encrypted Communication Certificate

In the CODESYS Security Screen, you may already see a certificate named:

Encrypted Communication

This certificate is mainly used for communication between CODESYS Development System and the runtime on the Pi.

For encrypted OPC UA communication between UaExpert and the Raspberry Pi, you need a separate certificate:

OPC UA Server certificate

Keeping these two certificates separate in your mind is important.

7.2 Check the Device Security Settings

Double-click the top-level Device and open Communication Settings.

From the menu in that view, open:

Device
└─ Security Settings

The main settings are:

Activation = ACTIVATED
CommunicationMode = SECURE_IF_POSSIBLE
UserAuthentication = ENABLED

With SECURE_IF_POSSIBLE, the OPC UA server can temporarily allow an unencrypted connection while no OPC UA Server certificate exists. After a certificate is created, the insecure mode is automatically disabled. Depending on your requirements, you can also use SIGNED_AND_ENCRYPTED or ALL.

To use Anonymous authentication during the initial test, open the following from Communication Settings in the same Device editor:

Device
└─ Change Runtime Security Policy
   └─ Allow anonymous login = ON

Use this only while testing. After confirming that username-based authentication works, turn it back off. CODESYS requires Anonymous access to be allowed explicitly.

If you connect by IP address and want the IP address included in the certificate, configure one of the following before creating the certificate:

CertificateIPAdresses = All

Or specify an address:

CertificateIPAdresses = 192.168.1.14

Depending on the CODESYS version, the setting name may appear with the spelling Adresses.[^codesys-security-settings]

7.3 Create the Certificate in the Security Screen

With the Pi selected as the Active Path, open:

View
└─ Security Screen

On the Devices tab, select the Device itself on the left.

The right side lists services that require certificates, such as:

Encrypted Communication
OPC UA Server
...

Select OPC UA Server and click the certificate creation icon.

In the CODESYS workflow, the certificate created here is created on the controller. Restart the runtime after creating it.[^codesys-certificate]

sudo reboot

Where Is the Certificate Stored?

The server certificate and its private key are held by the CODESYS runtime on the Raspberry Pi.

Raspberry Pi
└─ CODESYS Runtime certificate store
   ├─ OPC UA Server certificate
   ├─ Corresponding private key
   ├─ Trusted Certificates
   └─ Quarantined Certificates

CODESYS Development System acts as a remote management interface for this certificate store.


8. Connect Securely from UaExpert

8.1 What Is UaExpert?

UaExpert is an OPC UA test client provided by Unified Automation.

You can use it to check features such as:

  • OPC UA Server discovery

  • Address Space browsing

  • Variable Read / Write operations

  • Subscriptions and MonitoredItems

  • Trend displays

  • Server diagnostics

  • Certificate information

For this article, install UaExpert on the Windows PC.

8.2 Add the Server

From the menu at the top of UaExpert, select:

Server
└─ Add...

You can also use the + icon on the toolbar.

Enter the Pi's IP address as the Discovery URL:

opc.tcp://<Pi IP address>:4840

For example:

opc.tcp://192.168.1.14:4840

8.3 Choose a Security Policy

The following endpoints appeared in this environment:

Aes256_Sha256_RsaPss - Sign
Aes256_Sha256_RsaPss - Sign & Encrypt

Select:

Aes256_Sha256_RsaPss - Sign & Encrypt

As a general rule, choose a Sign & Encrypt endpoint from the secure endpoints offered by the server.

For the initial troubleshooting step, use:

UserTokenType:
Anonymous

8.4 Trust the Server Certificate in UaExpert

On the first connection, UaExpert will treat the Raspberry Pi's OPC UA Server certificate as untrusted.

Review the certificate details. After confirming that it belongs to your Pi, select:

Trust Server Certificate

For a temporary test, you can instead choose:

Accept the server certificate temporarily for this session

8.5 Trust the UaExpert Certificate in CODESYS

In CODESYS, open:

View
└─ Security Screen
   └─ Devices

Select Quarantined Certificates and look on the right for a certificate similar to:

UaExpert@<Windows PC name>

Check the subject, Application URI, and validity period to confirm that it is your UaExpert certificate.

In versions that do not offer Trust on the right-click menu, drag the certificate row from the right side into Trusted Certificates on the left:

Quarantined Certificates
        ↓ Drag
Trusted Certificates

When using self-signed certificates, the client must trust the server certificate, and the server must trust the client certificate. This establishes mutual application trust for the secure connection.

Return to UaExpert and connect again.

If the connection succeeds, Root → Objects will appear in the Address Space view.


9. Monitor PLC Variables from UaExpert

9.1 Browse the Address Space

After connecting, expand the Address Space in UaExpert.

The exact hierarchy may vary slightly by environment, but in this setup it looked roughly like this:

Root
└─ Objects
   └─ MachineData
      └─ GVL_Machine
         ├─ xStartCmd
         ├─ xRunning
         ├─ xOutput
         ├─ rSetpoint
         └─ rProcessValue

9.2 Drag Variables into Data Access View

Data Access View is empty immediately after you connect.

Drag the following variables from the Address Space into Data Access View:

udiCycleCounter
xHeartbeat
xRunning
xOutput
rProcessValue
rSetpoint
rHighLimit
xAlarmActive
xAlarmLatched

UaExpert creates a Subscription and a MonitoredItem for each variable you drag in, then displays its current value and subsequent changes.[^uaexpert-data]

Check the following:

StatusCode = Good
udiCycleCounter continues increasing
xHeartbeat alternates between TRUE and FALSE

9.3 Write Values from UaExpert

For a writable variable, double-click its Value cell in Data Access View and enter a new value.[^uaexpert-data]

First, set up the following initial state:

xAutoMode = TRUE
rSetpoint = 50.0
rHighLimit = 80.0

If xAlarmLatched = TRUE, clear the alarm first. The reset command will only clear the latch after rProcessValue has fallen to at least 5 degrees below rHighLimit.

Start the simulated machine:

xStartCmd = TRUE

Expected result:

xStartCmd     → Quickly returns to FALSE
xRunning      → TRUE
xOutput       → TRUE
rProcessValue → Begins rising

Because the PLC resets xStartCmd to FALSE immediately after accepting it, UaExpert may not display the TRUE state long enough for you to notice it.

Check whether the command succeeded by looking at the resulting state variable rather than the command variable itself:

Command: xStartCmd
Result:  xRunning

9.4 Change the Setpoint

Write the following value:

rSetpoint = 60.0

Expected result:

rProcessValue moves toward approximately 60
xOutput turns ON at 59 or below
xOutput turns OFF at 61 or above

This is hysteresis control with a ±1.0 band.

9.5 Try the Stop Command

xStopCmd = TRUE

Expected result:

xRunning = FALSE
xOutput  = FALSE

9.6 Try Manual Output Control

xStartCmd = TRUE
xAutoMode = FALSE
xManualOutputCmd = TRUE

Expected result:

xOutput = TRUE

To turn it off:

xManualOutputCmd = FALSE

However, setting the manual command to TRUE will not activate the output in either of the following states:

xRunning = FALSE
or
xAlarmLatched = TRUE

10. Watch the Control Behavior in Trend View

Add a Trend View document in UaExpert.

The exact menu name can vary slightly with the window layout and UaExpert version, but you can usually open it from the menu for adding a new document.

Add rProcessValue, rSetpoint, and xOutput to the trend. This makes it easy to see the process value approach the setpoint and the output switch on and off across the hysteresis band.


11. Connect an LED and Push Button to the Raspberry Pi 5 GPIO

Once the simulated machine and OPC UA connection are working correctly, you can move on to physical GPIO.

11.1 There Are Three Different GPIO Numbering Systems

This was one of the most confusing parts of the setup.

GPIO18 on the Raspberry Pi 5 can be described in at least three different ways:

Numbering system LED connection
BCM GPIO number 18
Physical pin on the 40-pin header 12
Global Sysfs GPIO number in this environment 589

GPIO23 is represented as follows:

Numbering system Button connection
BCM GPIO number 23
Physical pin on the 40-pin header 16
Global Sysfs GPIO number in this environment 594

11.2 Wiring

11.3 Add a GPIO Device in CODESYS

Add a GPIO connector to the Device, then add GPIOs 4 bit below it:

Device
└─ GPIO
   └─ GPIOs 4 bit

It is not a problem if the Vendor is displayed as CODESYS.

GPIOs 4 bit provides four logical channels:

Bit1
Bit2
Bit3
Bit4

The mapping view shows both input and output entries:

Bit1 in
Bit2 in
Bit3 in
Bit4 in

Bit1 out
Bit2 out
Bit3 out
Bit4 out

This does not mean that there are eight GPIO lines. It shows input and output mapping options for each of the four logical channels.


12. Find the Sysfs GPIO Numbers on a Raspberry Pi 5

The GPIO module used in this setup relied on Sysfs.

The Linux GPIO Sysfs API is an older interface, and the Linux kernel documentation recommends migrating to the GPIO Character Device API. However, the CODESYS IoDrvGpioSysfs driver used in this article required global Sysfs GPIO numbers in its Parameters.

12.1 Find the GPIO Chip Base Number

Connect to the Pi over SSH and run:

for chip in /sys/class/gpio/gpiochip*; do
  echo "=== $chip ==="
  echo -n "label: "
  cat "$chip/label"
  echo -n "base:  "
  cat "$chip/base"
  echo -n "ngpio: "
  cat "$chip/ngpio"
done

On the Raspberry Pi 5 used for this article, the output included:

=== /sys/class/gpio/gpiochip571 ===
label: pinctrl-rp1
base:  571
ngpio: 54

On Raspberry Pi 5, the RP1 chip handles external I/O functions including GPIO.

12.2 Convert BCM Numbers to Sysfs Numbers

Use the following formula:

Global Sysfs GPIO number
= pinctrl-rp1 base + BCM GPIO number

In the older global GPIO number space used by Sysfs, a line number is obtained by combining the GPIO chip's base with the line offset.

For this environment:

GPIO18 = 571 + 18 = 589
GPIO23 = 571 + 23 = 594

Do not copy 571 blindly into another environment.
The GPIO chip base can vary. Always check it on the actual device you are using.

12.3 Configure the Parameters

Open the Parameters for GPIOs 4 bit.

Use the following settings for this environment:

Bit1:
GPIO Number = 589
Direction   = OUTPUT

Bit2:
GPIO Number = 594
Direction   = INPUT

Bit3:
Unused

Bit4:
Unused

When I initially entered 18 and 23 directly, the GPIO module did not work correctly. With the Sysfs-based driver on this Raspberry Pi 5 environment, I had to use 589 and 594 instead.

12.4 Configure the I/O Mapping

Use only the mapping entry that matches the direction configured in Parameters.

For the first output test, map:

Bit1 out
└─ GVL_Machine.xHeartbeat

Starting with xHeartbeat instead of xOutput is recommended.

Because xHeartbeat changes state about once per second, it makes it much easier to test the GPIO path independently from the rest of the control logic.

For the input, map:

Bit2 in
└─ GVL_Machine.xPhysicalButtonRaw

Leave the unused rows empty:

Bit1 in      → Empty
Bit2 out     → Empty
Bit3 in/out  → Empty
Bit4 in/out  → Empty

12.5 Perform a Full Rebuild and Download

After changing the GPIO device configuration, run:

Debug → Stop
Online → Logout
Build → Clean All
Build → Rebuild
Online → Login
Download
Debug → Start

In this setup, the configuration initially failed to work even though the values were correct. Rebuilding the project and running it again resolved the issue.

After changing the device configuration or I/O mapping, a full rebuild and download is more reliable than an online change alone.

After the PLC enters RUN, confirm that:

xHeartbeat alternates between TRUE and FALSE
The LED on GPIO18 blinks about once per second

If this works, the following path is operating correctly:

PLC_PRG
  ↓
GVL_Machine.xHeartbeat
  ↓
Bit1 out
  ↓
Sysfs GPIO589
  ↓
BCM GPIO18
  ↓
Physical pin 12
  ↓
LED

After confirming the GPIO output by itself, change the mapping to:

Bit1 out
└─ GVL_Machine.xOutput

The physical LED will now reflect the result of Start commands and setpoint changes made from UaExpert.

12.7 Run the Complete Test

https://youtu.be/iF3GjQXSqsc


13. What This Experiment Teaches Us About OPC UA

13.1 The Address Space Is a Map of Meaningful Machine Data

Being able to browse a structure such as MachineData → GVL_Machine → rProcessValue makes it clear that OPC UA is more than a way to exchange raw bytes.

13.2 A Node Contains More Than a Value

UaExpert displays the value together with information such as the data type, StatusCode, timestamps, and NodeId.

13.3 Subscriptions Make Monitoring More Efficient

In Data Access View and Trend View, we did not repeatedly read each value by hand. Instead, MonitoredItems delivered notifications as values changed.

13.4 SecureChannel and User Authentication Are Separate

In this experiment, certificates established a Sign & Encrypt connection, while the initial test used an Anonymous UserToken.

Certificates
→ Application trust and communication protection

UserToken
→ Authentication of the user accessing the server

13.5 PLC Control and OPC UA Communication Are Separate

The control logic runs in the PLC task on a 20 ms cycle.

OPC UA securely exposes selected control variables to higher-level systems and accepts setpoints and commands from them.

13.6 The Data Path Reaches Real Physical I/O

The final output path in this experiment is:

UaExpert
  ↓ Write
OPC UA Server
  ↓
GVL_Machine.xStartCmd
  ↓
PLC logic
  ↓
GVL_Machine.xOutput
  ↓
CODESYS GPIO Sysfs Driver
  ↓
Sysfs GPIO589
  ↓
BCM GPIO18 / physical pin 12
  ↓
LED

The input path runs in the opposite direction:

Push button
  ↓
BCM GPIO23 / physical pin 16
  ↓
Sysfs GPIO594
  ↓
Bit2 in
  ↓
GVL_Machine.xPhysicalButtonRaw
  ↓
PLC logic
  ↓
OPC UA Subscription
  ↓
UaExpert

Once you reach this point, OPC UA no longer feels like a feature that simply lets a PC view variables inside a PLC. You can see it as a complete way to connect a controller with higher-level systems—including the information model, security, commands, monitoring, and physical I/O.


Conclusion

In this article, we turned a Raspberry Pi 5 into a SoftPLC with CODESYS and connected to it from UaExpert using OPC UA.

Reading the OPC UA specification is useful, but the pieces become much easier to understand when you try the complete workflow yourself: publish variables, establish certificate trust, write a command, and watch a real LED turn on as a result.

That end-to-end experience makes the role of each OPC UA component feel concrete—and gives you a practical foundation for connecting real control equipment to supervisory systems, dashboards, data platforms, and other industrial applications.