Welcome to our new forum
All users of the legacy CODESYS Forums, please create a new account at account.codesys.com. But make sure to use the same E-Mail address as in the old Forum. Then your posts will be matched. Close

Advice on OOP hardware I/O code structure

sturmghost
2023-04-03
2023-04-18
  • sturmghost - 2023-04-03

    Im trying to find my way to map hardware I/O with object oriented programming (OOP) in mind.

    I found several approaches and discussions about this topic but all what I found lacks a clear example - or at least an example I understand. :-)

    Discussions

    1: https://forge.codesys.com/forge/talk/Engineering/thread/49f235f440/
    2: https://stackoverflow.com/questions/67434017/best-variable-structure-for-initialization-of-several-motors-in-codesys

    Codesys OOP talks on YouTube (exzellent talks and information, although the key part about mapping hardware I/O is not shown in the necessary detail...):

    1: https://www.youtube.com/watch?v=2ReDGcCL9w8
    2: https://www.youtube.com/watch?v=M_qzxc_h20o
    3: https://www.youtube.com/watch?v=NFXxEEyQkbs

    Im new to PLC programming so I want to start right away with OOP instead the traditional way. I think OOP is the supperior way in the future - maybe not today but soon. I have programming background in VB.NET, VBA, Python, Arduino and MatLab and the concept of OOP is not new to me.

    What I want to do

    Say my PLC comes with 13 digital inputs and 10 digital outputs.
    The hardware address for the digital inputs starts at %IX0.0...%IX0.7, %IX1.0, ... up to %IX1.5
    The hardware address for the digital outputs starts at %QX0.0...%QX0.7, %QX1.0, ... up to %QX1.1

    Say I have 10 motors which are controlled with my PLC. Each motor is an instance ob my motor function block 'MyMotor'. MyMotor includes a member variable layed out as a struct which contains all the parameters belonging to each motor called 'MyMotorParameters_UDT', like descriptions, wire tags, states, etc. It should also contain a variable 'StartMotor' and 'FaultDetected' which are tied to the hardware I/O. For example 'StartMotor' is mapped to %QX0.0, 'FaultDetected' is mapped to %IX0.0.

    But how can I do this, allowing the 'MyMotor' FB to be reusable?

     
  • ojz0r - 2023-04-03

    This concept of "OOP" has been around for decades in PLC programming and is a standard way of working when you have more than one identical objects to control.
    When you create a POU chose typ FB and then you will be able to createinstances of this function block in your program.

     
    • hermsen

      hermsen - 2023-04-05

      Creating instances from FB's is tecchically not even OOP as this part of procedural programming. Procedural programming is a 4th gen language like C or PASCAL orIEC 61131-3 2nd edition Structured Text
      OOP is introduced in 5th generation programming languages like C++, Delphi or IEC 61131-3 3rd edition Structured Text.

       

      Last edit: hermsen 2023-04-05
  • sturmghost - 2023-04-03

    Well, I know how to instantiate a FB. This wasn't my question. I'm questioning a way of mapping my hardware I/O to project variables without violating the "heart" of OOP (hardcode the addressee into the FB).

     
  • ojz0r - 2023-04-03

    I miss understood the question.
    How do you imagine the FBs being reusable if you have hard coded global variables in them? Somehow you need to pass the variables to the FB.
    I suppose you could pass pointers as inputs to the blocks and modify the dereferenced pointer inside the function block but i cant see the gain in that.

     
    • hermsen

      hermsen - 2023-04-05

      VAR_IN_OUT is technically a pointer so yes it is absolutely the way to go

       
  • sturmghost - 2023-04-03

    Well I don't want to hardcode them. Thats the whole point of my question...

     
    πŸ‘
    1
  • ojz0r - 2023-04-04

    Use VAR_OUTPUT for pins on the right side or VAR_INOUT for pins on the left side. i cant see any other way.

     
  • alexgooi

    alexgooi - 2023-04-04

    Dear sturmghost,

    What you can do is split the program up into 2 sections core (dynamic) and config (Static). In the core you can create the classes/objects and in the config you can couple the data from the objects to the I/O

    like so:
    PumpMotor.Pump_Manager();
    %QX0.0 := PumpMotor.Start;
    PumpMotor.Fault := %IX0.0;

    What I have found the most workable is declare every public variable as an VAR_INPUT and every private variable as an VAR (In this way you can also write to the output from another source other than the object itself).

     

    Last edit: alexgooi 2023-04-04
  • hermsen

    hermsen - 2023-04-04

    Hi, if you use OOP, please refrain from using IEC hardware adresses! Write code entirely symbolic.

    So, in general, avoid hardcoded hardware adressing at all time.

     
  • hermsen

    hermsen - 2023-04-04
    PumpMotor(); //Calling the main FB body which on the inside calls a private method Pump-Manager, which probably manages the internal handling of the pump
    OPump1Start := PumpMotor.Start;
    PumpMotor.FeedbackOn := IPump1Feedback;
    PumpMotor.Fault := IPump1Fault;
    

    instead of using properties, like above
    I'd choose to go with methods but that is a matter of taste I guess.

     

    Last edit: hermsen 2023-04-04
    • sturmghost - 2023-04-04

      I miss the part where you send out the infromations to the physical I/O where and how this is done?

       
  • hermsen

    hermsen - 2023-04-05

    In my example I assume that you are known with how you can access IO.
    There are several ways possible to access IO fully symbolic. Please read up on thisi. the manuals fro CODESYS.

    1) define symbols on each channel in the IO list. These symbols are now directly accessible in your code. This is what I used in my example.
    2) A more elegant way is to pre-define an IO Struct DUT type. This DUT type should now be defined in a funtion block or Program as VAR_IN_OUT. After This you should declare this FB as an instance somewhere in your code. Lastly you now can reference this instance path in the IO list. When you done this properly,the pull path of the IO channel may look something like this in the IO List;

    Application.PLC_PRG.Pump1Instance.IO.iFeedBackOn

    3) Since CODESYS 3.5.18.0 you can access IO symbolically directly in you code.
    The idea behind this new way is to use Hardware and IO channel names implicitly in you code and therefore also avoid % adressing. To enabl this you should enable Symbolic acces to the hardware in any runtime which is compatible (i.e.greater then 3.5.18.0)

    I hope you now have more hints on how to achieve accessing IO channels. Be sure to read up on these topics, as they will make you code easier to readand manintain and a joy to write.

     
  • sturmghost - 2023-04-05

    @h-hermsen

    Thank you for your detailed answer. I'm fully aware about the full path mapping capabilities but as I read your text I thought you mean a different technique. So 1) is clear to me.

    2) Can you show me a short example of this IO struct DUT type?

    3) An example would be great. Unfortunately our IDE incorporates Codesys 3.5.16.x, so thats a topic for the next update but is nice to know anyway.

     
    • hermsen

      hermsen - 2023-04-05

      The concept is really quite simple as the followings steps will explain it

      1) Define a Struct of your choice,
      2) Define an FB of your choice,
      3) Instanciate the struct from 1 into a VAR_IN_OUT of 2
      4) Instanciate the FB somewhere in a program
      5) In your hardware you can now refer to the VAR_IN_OUT from 4

      i.e. Application.PLC_PRG.MyFBinstanceOfStep4.iSomeInputAsBool;

       

      Last edit: hermsen 2023-04-05
  • sturmghost - 2023-04-05

    With 5) you mean using the I/O mapping tab and mapping the full path name to a hardware channel/address?

    Im doing this right now but using the AT command for mapping the full Input or Output module, not just a single channel.

    So I have an IO Struct, which contains for example

    Value : Bool
    Unit : String(80);

    And another struct for the module, with:

    Channel : Array[0..13] Of IO_DUT
    Input At %IW0 : Word;

    Im instancing the module struct as a Var_in_out in a program. Then I convert the word into single bits and write them to the right channel. The other way around is done with the output module. With that I didn't touched the I/O mapping tab.

    In order to avoid the at command I need to define

    Input_1 : Bool;
    Input_2 : Bool;
    ...

    and map each variable to one input channel via the I/O mapping tab, right? Or do you have a different procedure in mind?

     

    Last edit: sturmghost 2023-04-05
  • hermsen

    hermsen - 2023-04-06

    I'd prefer programming individual signals over BYTE or WORD/ DWORD assigned boolean channels. Usually individual channel assignment makes your code read way easier both at the hardware IO mapping and in the FB or PRG itself. The slight cost of declaring more channels usually adds tremendous added value for well readable code. The adagio in modern days is readability over compact programming. PS read up on PLCOPen programming style guidelines, they forbid you from using of direct adressing.

     
  • fajean - 2023-04-11

    I am the guy from the Stackoverflow answer. Let me attempt to give you a few more breadcrumbs.

    You are likely to want to achieve any combination of these goals.

    • Make your control logic independent from your hardware
    • Avoid hard-coded hardware addressing.
    • Allow several instances of whichever device you control (e.g. motor)
    • Make your code portable (for me, this means CODESYS and TwinCAT).

    I have found only one sane way to check all these boxes. This way involves not trying to achieve all these goals within the same function block. I have roughly four layers (that each correspond to separate libraries/projects):

    • Function blocks that represent actual hardware devices (such as individual I/O cards). They define inputs/outputs as incomplete addresses (%I or %Q). The incomplete addresses can be coded against even though they are only to be mapped in the final project that uses the library.
    • Function blocks that represent the control logic. They have their own input/output structures, which are normal variables. They use no I/O addresses, and no hardware device function blocks.
    • "Electrical design" function blocks, that inherit from control logic function blocks, but also declare hardware function blocks as well as methods that copy the hardware inputs to control inputs, and control outputs to hardware outputs. Each function block matches a specific electric drawing. Only in this function block is there a relationship between the hardware and the control logic, which makes sense because the electrical design is not hardware-independent (the coupling happens at the "right place").
    • Projects, that use these (reusable) hardware-dependent function blocks, and map the incomplete addresses.

    You can see how each layer has a precise role to play in abstracting away different things, which allows different kinds of reuse (control logic reuse, hardware device reuse, full subsystem reuse) to happen independently.

    We can have several hardware implementations (electrical designs) for the same control logic. We can have inputs and outputs that have different formats in the control logic vs. hardware (think analog scaling, reading/writing ENUM values as bit groups, etc.).

    I/O function blocks can be as simple as this :

    FUNCTION_BLOCK BECKHOFF_EL1002
    VAR_INPUT
      in AT %I* : IO_2CH1_BIT;
    END_VAR
    

    The IO_2CH1_BIT structure is just this :

    TYPE IO_2CH1_BIT :
    STRUCT
      ch1 : BIT;
      ch2 : BIT;
    END_STRUCT
    END_TYPE
    

    Per-project mapping is super-easy, because you are mapping an I/O card's memory to a function block that represents the same I/O card (and can have the same instance name as the I/O card in your project tree). The relationship between hardware and control logic variables is implemented at a reusable layer, so we only have to test and debug once for each hardware design. Any technician with a rough understanding of the IDE can create a project and map the I/O without any knowledge of the control logic.

    In other words, we achieve hardware-independence of the control logic, control-logic independence of the project, while retaining reusable, programmatic use of hardware-dependent features neither the project nor the control logic know anything about.

     
    πŸ‘
    1

    Last edit: fajean 2023-04-11

Log in to post a comment.