Why CODESYS persistent variables reset on a single-board PLC

8 min read · Published November 4, 2026

On a flash-only single-board PLC, CODESYS persistent variables come back at their initial values because nothing writes them to storage. Declare them in a persistent variable list, restore them with AppRestoreRetainsFromFile on the first scan, and store them with AppStoreRetainsInFile on a trigger from the CmpApp library. A clean shutdown then keeps the latest values; a hard power cut returns the last stored ones.

On a single-board PLC, CODESYS persistent variables reset to their declared initial values after a restart because nothing on the controller has written them to storage. The hardware has volatile RAM and flash, no battery-backed retain memory, and this runtime writes the file only when the program asks for it. The fix is to ask: restore with AppRestoreRetainsFromFile from the CmpApp library once on the first scan, and store with AppStoreRetainsInFile from a trigger. After that, a clean shutdown brings back the latest values and a hard power cut brings back whatever the last store wrote. Below are the reset symptoms with the check that tells them apart, the calls in place, the shutdown and download rules, and a worked sizing of the store interval against the data a power cut can take.

Why the values reset on flash-only hardware

A remanent variable needs non-volatile storage to survive a restart. On controllers with battery-backed SRAM or NVRAM, the runtime keeps retain and persistent data in that memory and the subject ends there. A single-board PLC has volatile RAM plus an SD card or eMMC, so the runtime has to serialize the data into a file on that media, and the serialization runs only on defined events: an orderly shutdown, or a library call the program makes. On the runtime at CODESYS v3.5.17.30 for this hardware, no automatic event performs the write, so every restart finds no saved data and the application starts at the initial values you declared.

Two CmpApp functions supply the missing events. AppStoreRetainsInFile takes an application pointer and a file path and writes the persistent data of the whole application; the CmpApp documentation lists its return type as RTS_IEC_RESULT. AppRestoreRetainsFromFile reads the same file back. Neither call takes a variable list, so one call covers every persistent variable in the application and you cannot pick a subset.

Symptoms and what each one points to

Match your case to one row before changing code, because each row has a different fix.

Symptom Cause Decisive check
Persistent variables return at their declared initial values after every restart Nothing writes the retain file, or nothing reads it back at start Look for the file at the path the store call passed, /home/pi/retains.ret in this article
Changed values come back after a clean shutdown, older values come back after the supply is pulled The store runs only in a shutdown path, so a hard cut skips it Change a value, pull power, restart, compare against the last stored value
The store code has no effect and no file appears No task calls the POU, so it never executes Attach the POU in the task configuration and watch a counter inside it in online mode
Values reset right after a download The download used Update boot application, which builds a fresh boot application and reinitializes persistent data Repeat with a plain download or an online change and compare
Values survive some reboots with no store code at all Newer runtime builds handle warm restarts; build 4.12.0.0 of the SL runtime for this hardware kept values across a network-triggered reboot with no file present, and still reset on a hardware reset in the same test Test your exact build with both a network reboot and a hardware reset

If a value comes back changed while the line is still running, that is a different failure: the case where an HMI overwrites PLC retained variables at startup.

Add the store and restore calls

  1. Declare the values in a persistent variable list, or with the PERSISTENT attribute, the same way you would on any other target.
  2. Add the CmpApp library in the Library Manager of the application.
  3. Find the application by name. The string you pass to AppFindApplicationByName has to match the application name in the device tree, 'Application' by default. A name that does not match brings the pointer back as 0, and the guard skips both calls with no message.
  4. Restore under the first-scan condition, before any process logic reads the values.
  5. Store from a trigger of your choice: an operator command, a value-change detection, a timer, or a shutdown request from your supervisory logic. Keep the rate low, because every store rewrites the file on flash.
  6. Call the POU from a program that a task executes. A POU no task calls does nothing, and that alone produces the no-file symptom.
  7. Download to the controller and start the application.
PROGRAM Persist
VAR
    application_ptr : POINTER TO CmpApp.APPLICATION; // documented CmpApp handle
    res             : DINT;                          // call result
    first_scan      : BOOL := TRUE;                  // example variable, clears itself
    store_now       : BOOL := FALSE;                 // example variable, set by your trigger
END_VAR

IF first_scan THEN
    application_ptr := CmpApp.AppFindApplicationByName('Application', ADR(res));
    IF application_ptr <> 0 THEN
        AppRestoreRetainsFromFile(application_ptr, '/home/pi/retains.ret');
    END_IF
    first_scan := FALSE;
END_IF

// process logic runs here, between the restore and the store

IF store_now THEN
    application_ptr := CmpApp.AppFindApplicationByName('Application', ADR(res));
    IF application_ptr <> 0 THEN
        AppStoreRetainsInFile(application_ptr, '/home/pi/retains.ret');
    END_IF
    store_now := FALSE;
END_IF

first_scan and store_now are example variables; the CmpApp names are the documented ones. Keep the conditions in place. A restore that runs every scan overwrites the live values with the file contents and discards every change the program makes, so the restore belongs under the first-scan condition and the store under its trigger. Both calls return an RTS_IEC_RESULT; check the code during commissioning so a failed write becomes an alarm instead of silent data loss.

Checks before you trust it

Run three checks in order.

  1. Trigger one store and check the file exists on the controller filesystem at the path you passed. No file means the POU is not running or the pointer came back 0.
  2. Store a known value, for example 47 in a persistent counter, shut the operating system down cleanly, and power back up. The counter must read 47. That result shows the store, the file and the restore all work.
  3. Change the counter again, pull the supply with no shutdown, and power up. The counter comes back at the last stored number, and that is the correct behavior on this hardware. The gap between stores is your exposure window, and the example below sizes it.

Shutdown and power cuts

With the calls in place the rule is simple. A clean shutdown gives the runtime time to write, so the latest values come back. Pulling the supply skips every write, so the values come back from the last store, whenever that was. The file never captures the moment of the cut unless a store happened just before it.

Three designs follow from that rule:

  • Values a hard cut must keep: store on change or on a short timer, and accept the flash writes. The example below sizes the timer.
  • Values on a line where operators shut down cleanly every time: keep the store in the shutdown path and the flash writes stay rare.
  • Sites where people pull the plug: give them a shutdown command in the HMI or a button wired to an operating-system shutdown, so the plug pull becomes the rare case.

A power-fail input that starts a controlled shutdown covers both cases: the store runs while the supply is still up. The hold-up time a given controller manages is a figure from its own documentation.

A shutdown routine protects data. It is no substitute for the machine's emergency stop circuit or for lockout during maintenance.

Example: sizing the store interval for a parts counter

The inputs here are example values; keep the arithmetic and substitute your own takt. A packaging line counts finished cartons into one persistent variable, one count per carton, at a takt of 8 s. The store trigger is a timer set to 10 minutes.

  • Worst case lost in a hard power cut: the timer writes every 600 s and the line makes one carton every 8 s, so a cut can discard 600 ÷ 8 = 75 cartons of counting.
  • Write load: six stores an hour (60 ÷ 10), so 144 stores a day while the application runs.

If recounting 75 cartons after every hard cut is too much, tighten the trigger to 1 minute:

  • Worst case lost: 60 ÷ 8 = 7.5, so seven or eight cartons depending on where the last count lands after a store.
  • Write load: 60 stores an hour, 1,440 a day.

Storing on every count costs one file write every 8 s, 450 an hour, 10,800 a day, which is heavy flash traffic for one number. Pick the slowest trigger whose worst-case loss the shift can absorb, and write that number into the shift instructions so a cut is measured against it. If the counter only needs a history for reporting and the live value is expendable, log PLC variables to a database from CODESYS and let the database keep the record. The count logic is one pulse per carton; if the carton sensor can hold its pulse across several scans, the scan-timing rules in PLC up down counter: scan timing and direction logic decide whether each carton counts once.

Downloads and the .ret file

Treat every download as a data-loss event. Selecting Update boot application during a download builds a fresh boot application and reinitializes the persistent data to the declared initial values, so values loaded that way fail to hold in the running application. The sequence that keeps the data: run the store trigger, download, start, and let the first-scan restore reload the file. Keep a copy of the .ret file off the controller before commissioning changes.

The file is a binary image of the persistent data and carries no variable names. In a project with a single string variable, a hex dump shows a 24-byte block ahead of the data with no application handle and no variable reference in it, and no documented meaning for those bytes, so treat the layout as opaque. That is also why a prepared file transfers to a new device only when the device runs the identical application: the file depends on the memory layout of that exact build, and any program change can move the data.

The Persistence Manager add-on is the other route. It needs the Application Composer Single License, and it runs only on runtime builds that list it as compatible, so check the compatibility statement for your runtime before planning around it.

Which store trigger to pick

Recommendation: On a flash-only single-board PLC running CODESYS 3.5, declare the values in a persistent variable list, restore them with AppRestoreRetainsFromFile under a first-scan condition, and store them with AppStoreRetainsInFile on a 1-minute timer when a hard power cut must cost little, or on a shutdown-only store when operators shut down cleanly every time and flash writes are the concern. Check that the file exists after the first store and check both return codes before you trust the setup. If the runtime is build 4.12.0.0 of the SL variant for this hardware, test a restart with no code first, because that build already keeps values across warm restarts.

Questions

What is the difference between VAR RETAIN and a persistent variable list in CODESYS?

VAR RETAIN keeps a value across a warm restart and, on controllers with battery-backed memory, across a power loss. A persistent variable list keeps values across an application download and a cold restart. On a flash-only single-board PLC neither kind survives unless the data reaches non-volatile storage, and the CmpApp calls in this article are that write. Use the persistent list for values a download must not destroy, and VAR RETAIN where the target hardware keeps retain memory powered.

Can I copy a prepared .ret file to another controller?

Only when the other controller runs the identical application build. The file is a binary image of the persistent data with no variable names and no application handle in it, so it depends on the memory layout of that exact program. Rebuild the file after any program change, and keep the copy off the controller before a commissioning download, because Update boot application reinitializes persistent data to the declared initial values.

How often can AppStoreRetainsInFile run on an SD card?

Every call rewrites the whole file on the flash media. At a 1-minute trigger that is 1,440 writes a day; storing on every carton at an 8 s takt is 10,800 a day. Flash cells take a finite number of rewrites at the same location, so pick the slowest trigger whose worst-case data loss the process accepts, and put a value that must be stored on every event on industrial flash media.

Planning information, not legal or engineering advice. Rules differ by place: check codes and permits with your local authority and your licensed trades.

Keep reading

Build your signAsk for a quote