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

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:

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/f8531dbb-eedf-47e4-b995-f32dd539e3be.png align="center")

* * *

## 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:

```text
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:

```text
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.

```text
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.

```text
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.

```text
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:

```text
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](https://store.codesys.com/en/)

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

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/3fdb31cb-6dcd-48ad-abc0-f310332f9afa.png align="center")

### 4.2 Install the CODESYS Runtime on the Pi

In CODESYS, open:

```text
Tools
└─ Deploy Control SL
```

Connect to the Raspberry Pi over SSH:

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/27a73b88-c3cf-4f55-a13c-05bc8a6e6c4b.png align="center")

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/c0ff1a3e-ef9b-4b1a-bfda-608ae2fb99be.png align="center")

### 4.3 Create a New PLC Project

In CODESYS, select:

```text
File
└─ New Project
   └─ Standard Project
```

Use the following settings:

```text
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:

```text
Add Object
└─ Global Variable List
```

Name it `GVL_Machine`.

```iecst
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`:

```iecst
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`.

```text
Type: Cyclic
Interval: T#20ms
```

Keep in mind that the following three cycles are separate:

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/b97c9047-a3a0-44e2-be5e-e51f1e0edfd7.png align="center")

### 5.4 Download the Project to the Pi and Run It

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

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

Then run the following commands:

```text
Build
└─ Build

Online
└─ Login

Debug
└─ Start
```

Open `GVL_Machine` online and confirm the following:

```text
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:

```text
Add Object
└─ Communication Manager
```

Right-click the new `Communication Manager` and select:

```text
Add Object
└─ OPC UA Server
```

Next, right-click `OPC UA Server` and select:

```text
Add Object
└─ IEC Symbol Publishing
```

Name the IEC Symbol Publishing object `MachineData`.

The final object tree should look like this:

```text
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:

```text
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

```text
xStartCmd
xStopCmd
xResetAlarmCmd
xAutoMode
xManualOutputCmd
rSetpoint
rHighLimit
```

Set their access rights to:

```text
Read/Write
```

### Read-Only Variables

```text
xRunning
xOutput
xAlarmActive
xAlarmLatched
xHeartbeat
rProcessValue
udiCycleCounter
```

Set their access rights to:

```text
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.

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/cb1be4dd-6210-43c9-90c2-a431d7a72a05.png align="center")

### 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.

```text
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:

```text
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:

```text
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:

```text
Device
└─ Security Settings
```

The main settings are:

```text
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:

```text
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:

```text
CertificateIPAdresses = All
```

Or specify an address:

```text
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:

```text
View
└─ Security Screen
```

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

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

```text
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\]

```bash
sudo reboot
```

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/9d1ed40a-1d42-460b-bb6b-8c5821662c18.png align="center")

### Where Is the Certificate Stored?

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

```text
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

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/575797e9-248f-4a0d-93db-e0c33ac0fbd5.png align="center")

From the menu at the top of UaExpert, select:

```text
Server
└─ Add...
```

You can also use the `+` icon on the toolbar.

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

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

For example:

```text
opc.tcp://192.168.1.14:4840
```

### 8.3 Choose a Security Policy

The following endpoints appeared in this environment:

```text
Aes256_Sha256_RsaPss - Sign
Aes256_Sha256_RsaPss - Sign & Encrypt
```

Select:

```text
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:

```text
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:

```text
Trust Server Certificate
```

For a temporary test, you can instead choose:

```text
Accept the server certificate temporarily for this session
```

### 8.5 Trust the UaExpert Certificate in CODESYS

In CODESYS, open:

```text
View
└─ Security Screen
   └─ Devices
```

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

```text
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:

```text
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:

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

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/99802188-19e9-4246-9d51-1ed2cf254aa7.png align="center")

### 9.2 Drag Variables into Data Access View

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/e7f0b353-a80c-4aec-a275-3756fd0e6797.png align="center")

Data Access View is empty immediately after you connect.

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

```text
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:

```text
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:

```text
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:

```text
xStartCmd = TRUE
```

Expected result:

```text
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:

```text
Command: xStartCmd
Result:  xRunning
```

### 9.4 Change the Setpoint

Write the following value:

```text
rSetpoint = 60.0
```

Expected result:

```text
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

```text
xStopCmd = TRUE
```

Expected result:

```text
xRunning = FALSE
xOutput  = FALSE
```

### 9.6 Try Manual Output Control

```text
xStartCmd = TRUE
xAutoMode = FALSE
xManualOutputCmd = TRUE
```

Expected result:

```text
xOutput = TRUE
```

To turn it off:

```text
xManualOutputCmd = FALSE
```

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

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/18fda164-7233-4149-8a78-9ebf41e29fa3.png align="center")

* * *

## 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

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/6212f157-189a-4096-bec5-13599c662358.png align="center")

### 11.3 Add a GPIO Device in CODESYS

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

```text
Device
└─ GPIO
   └─ GPIOs 4 bit
```

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

`GPIOs 4 bit` provides four logical channels:

```text
Bit1
Bit2
Bit3
Bit4
```

The mapping view shows both input and output entries:

```text
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:

```bash
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:

```text
=== /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:

```text
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:

```text
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:

```text
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.

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/6c61d501-5b2d-4b63-8423-4379e30988bd.png align="center")

### 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:

```text
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:

```text
Bit2 in
└─ GVL_Machine.xPhysicalButtonRaw
```

Leave the unused rows empty:

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

![](https://cdn.hashnode.com/uploads/covers/69ce85bb0ff860b6def2c2ab/0e33ec4d-89e5-4bd5-98e7-7ac8a4288a80.png align="center")

### 12.5 Perform a Full Rebuild and Download

After changing the GPIO device configuration, run:

```text
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.

### 12.6 Confirm That the LED Blinks

After the PLC enters RUN, confirm that:

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

If this works, the following path is operating correctly:

```text
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:

```text
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.

```text
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:

```text
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:

```text
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.
