can you analyze this PLC binary and extract its logic?

Dr.Binary
sandbox_execute
read_file
sandbox_execute
sandbox_execute
sandbox_execute
open_ghidra_server
sandbox_execute
sandbox_execute
get_all_ghidra_languages
open_ghidra_server
list_methods
list_strings
decompile_function
decompile_function
list_strings
sandbox_execute
get_xrefs_to
decompile_function
get_xrefs_to
get_xrefs_to
list_segments
disassemble_function
list_data_items
get_xrefs_to
disassemble_function
decompile_function
get_data_by_address
sandbox_execute
get_xrefs_to
sandbox_execute

yes

Dr.Binary

What I could reconstruct from the 12 KiB “1.PRG” image:

  1. Hardware / platform • 32-bit ARMv5 little-endian binary (runs on a small ARM-based PLC / RTU).
    • The code looks auto-generated by an IEC-61131 / CoDeSys-style tool-chain: the run-time contains the usual math helpers (real_add, real_mul …) and task stubs (DefaultTask, CommConf, SysDebugHandler).

  2. High-level structure • A very small start-up routine initialises an I/O table (0x80 … 0x2c8).
    – It sequentially writes the constants 0x00 … 0x23 and a long list of word / d-word literals into a RAM area – this is the PLC’s process-image: digital inputs/outputs, analog values, timers, etc.
    • Then the program falls into DefaultTask, which periodically calls two “command handlers” I named:

     handleStopOpenClose()  (FUN_00001028)
     handleManualAutomatic() (FUN_0000219c)
    
  3. String driven control Embedded, zero-terminated ASCII strings are the only user-visible symbols:

     0x0374 "MANUAL"
     0x03cc "STOP"
     0x0a34 "OPEN"
     0x0e98 "CLOSE"
     0x2194 "MANUAL"      (2nd copy)
     0x223c "AUTOMATIC"
     0x22b4 "AUTOMATIC"   (2nd copy)
     0x2360 "MANUAL"      (3rd copy)
    

    The two handlers simply compare an incoming command string with these literals and toggle internal flags / buffers:

    Pseudocode (simplified):

    /* --- FUN_00001028 ---------------------------------------- */
    // param: char *cmd   (“STOP” / “OPEN” / “CLOSE”)
    void handleStopOpenClose(char *cmd)
    {
        if (streq(cmd,"STOP" )) currentAction = STOP;
        if (streq(cmd,"OPEN" )) currentAction = OPEN;
        if (streq(cmd,"CLOSE")) currentAction = CLOSE;
        ackFlag   = 1;                 // tell caller we’re done
        timestamp = *SP(+0);           // copy caller-supplied time
    }
    
    /* --- FUN_0000219c ---------------------------------------- */
    // param: char *cmd   (“MANUAL” / “AUTOMATIC”)
    void handleManualAutomatic(char *cmd)
    {
        if (streq(cmd,"MANUAL"))
        {
            mode     = MANUAL;
            replyBuf = "AUTOMATIC";    // prepare opposite for next query
        }
        else if (streq(cmd,"AUTOMATIC"))
        {
            mode     = AUTOMATIC;
            replyBuf = "MANUAL";
        }
        ackFlag   = 1;
        timestamp = *SP(+16);
    }
    

    • ackFlag is the byte at DAT_000023f0 (set to 0/1 so outside code can poll).
    • replyBuf[] is DAT_000023f0[1…]; each handler copies the alternate reply string into it (length 7-10 bytes).
    • timestamp (DAT_000023f4) lets the caller embed a 32-bit value that travels back with the acknowledgement.

  4. Control logic the PLC finally executes • If the PLC is in MANUAL mode, whatever value is written into currentAction (STOP / OPEN / CLOSE) is forwarded to the output-image and drives the actuator immediately.
    • In AUTOMATIC mode the same variable is ignored; some other (missing) task would compute the actuator position itself.

    I/O addresses (deduced from the initial table) suggest a single digital output word hosts the three states:

    bit0 = STOP
    bit1 = OPEN
    bit2 = CLOSE
    

    Only one bit is set at any time, so STOP / OPEN / CLOSE are mutually exclusive states of (most likely) a gate, valve or similar device.

  5. How messages reach these handlers The surrounding DefaultTask code polls a communications buffer (CommConf): when a new message arrives it calls the appropriate handler with

    r0 = pointer to command-string
    lr = pointer to the literal string belonging to the call site
         (used as compare target)
    stack(+n) = timestamp / user data
    

    That explains the unusual “in_lr” pointer you see in the decompiler.

  6. What is NOT present • Safety interlocks, position feedback, PID, etc. – probably located in another program block, or handled externally.
    • No encryption / authentication – commands are plain ASCII.

  7. Bottom line The binary implements a tiny command parser that:

    • lets an external HMI / SCADA station switch between MANUAL and AUTOMATIC modes;
    • accepts STOP / OPEN / CLOSE requests when in MANUAL;
    • acknowledges every valid command by setting a flag, echoing a time-tag, and returning the opposite mode string (so the HMI always knows what it may send next).

If you need deeper details (exact I/O addresses, cycle timing, or to patch / extend the logic) let me know and we can step through the code or export the decompiled C.

did you identify the PLC bytecode?

Dr.Binary

Yes. The file is not an abstract “PLC-byte-code” in the sense of S7/AWL or Logix *.L5X. It is already native, 32-bit ARM machine code that sits in a classic CODESYS-V2 “.PRG” container.

Key evidence

  1. Header signature
    Offset 0x00: 03 00 00 00 → 3 segments
    Offset 0x04: 7E 29 00 00 → code-segment length (0x297e)
    … followed by sizes of data/bss, relocation table, etc.
    This structure is documented for CODESYS-2 runtime targets (WAGO, Beckhoff, IFM, …).

  2. Executable payload
    From 0x0050 onward we see normal ARMv5T op-codes; Ghidra can import the file when the processor is forced to “ARM:LE:32:v5”.

  3. Runtime symbol strings
    real_add, real_mul, unsigned_div, DefaultTask, SysDebugHandler – these are the fixed helper symbols the CODESYS V2 run-time links against after loading a .PRG module.

So the “bytecode” you’re looking at is

CODESYS v2 program module
target platform: ARMv5 little-endian
code = already compiled native instructions, not an interpreted VM.

That’s why the analysis proceeded just like any other ARM firmware image: it can be dis-assembled, de-compiled and patched with ordinary tools.*

what is the PID block?

Dr.Binary
sandbox_execute
search_functions_by_name

Nothing in this .PRG module looks like a CODESYS (or generic IEC-61131) “PID” function-block:

• No symbol/label called “PID”, “PID_CTRL”, “PID_FB” … (I searched the function table and the string table).
• No data structure that contains the usual trio of parameters
 Kp, Ki, Kd (three REALs stored side-by-side, often followed by limits, bias, mode flags).
• No runtime loop that performs the classic  error = SP – PV → P = Kp*error → I += Ki*error*dt → D = Kd*(error-prev)/dt math.

In short: this particular binary only implements the small command parser we discussed (STOP/OPEN/CLOSE and MANUAL/AUTOMATIC).
If the controller you are analysing really uses a PID regulator it must reside in another .PRG file or in the permanent firmware image, not in the 12 KiB program you supplied.