Local enumerations in TwinCAT 3

One of the things that have annoyed me ever since I started using TwinCAT is the fact that if you create an enumeration, it will automatically have a global scope. It will be accessible from all functions and function blocks. What’s even worse is that if you create a library project with an enumeration and include that library in another project, the enumeration will be visible there, too. This pollutes the namespace by creating unnecessary types. But no more.

When TwinCAT 3 developers write state machines I often see arbitrary numbers being used to define the different states. Let’s for example assume that we want to write a state machine that opens a file, writes some data into it, and closes the file. The “traditional” way would be something like this:

FUNCTION_BLOCK FB_TextFileWriter
VAR_IN_OUT CONSTANT
    sTextToBeWritten : Tc2_System.T_MaxString;
END_VAR
VAR
    nTextWriteState : INT;
END_VAR

CASE nTextWriteState OF
    
    10 : // Open file
         // Code

    20 : // Write "sTextToBeWritten" to file
         // Code

    30 : // Close file
         // Code

    40 : // Error handling
         // Code
    
END_CASE

I’m not a big fan of comments, at least not the type of comments that clarify what the code is supposed to do. In this case, an enumeration is a better choice. The normal way to use an enumeration in TwinCAT 3 is to first create it as a data unit type (DUT), and then use this DUT.

{attribute 'qualified_only'}
{attribute 'strict'}
TYPE E_TextWriteState :
(
    FILE_OPEN := 0,
    WRITE_TEXT,
    FILE_CLOSE,
    ERROR
);
END_TYPE

The next step is to instantiate the enumeration like eTextWriteState : E_TextWriteState instead of nTextWriteState : INT. The problem with this is that this enumeration will be visible to not only the function block where we want to use it (in this case, FB_TextFileWriter), but it will be visible to every other program organization units (POUs) in our code. Even worse, if we create a library project where this enumeration is included, then this enumeration will be visible to all POUs that reference this library. This pollutes the code with unnecessary types.

In C++ we can simply create local enumerations that have limited scope. For example, if we have a function we can simply declare a local enumeration like in this example:

void WriteTextToFile(const String &s, const String &f) {
    enum {FILE_OPEN, WRITE_TEXT, FILE_CLOSE, ERROR};
    // Code
}

Now we can do something similar in the world of TwinCAT 3 as well by using implicit enumerations. If we take the previous example, we can simply rewrite it like this:

FUNCTION_BLOCK FB_TextFileWriter
VAR_IN_OUT CONSTANT
    sTextToBeWritten : Tc2_System.T_MaxString;
END_VAR
VAR
    eTextWriteState : (OPEN_FILE, WRITE_TO_FILE, CLOSE_FILE, ERROR);
END_VAR

CASE eTextWriteState OF
    
    OPEN_FILE : 
         // Code

    WRITE_TO_FILE :
         // Code

    CLOSE_FILE :
         // Code

    ERROR : 
         // Code
    
END_CASE

Now we don’t need these unnecessary comments as our code is self-documenting and we don’t pollute our code either. If we compile this code and do an online login we can see that the compiler converts these to an implicit internal type.

Playing around with these I have found one limitation. It’s not possible to declare local enumerations inside methods of a function block (not even if you declare them as VAR_INST), which is a shame.

I have not found anything about local enumerations in any official documentation at the Beckhoff website, which is unfortunate as it’s very useful and it’s something that is commonly used in other programming languages. There are no Beckhoff examples that are using this either. I don’t know in which version of TwinCAT local enumerations were introduced, as we don’t have release notes from Beckhoff. This was tested on version 3.1.4024.12 and might be available in versions earlier than this.

This makes me wonder, what other “hidden features” does TwinCAT 3 have?

What do you think of this feature? Will you be using it in your code? Do you know any other non-documented features of TwinCAT 3? Comment below!

  • Share on:

21 Comments, RSS

  1. Saele Beltrani

    Nice feature, it’s working fine also on TC3.1.4022.14! Let’s hope Beckhoff won’t remove it in future versions

  2. Ale_Monta

    Wow, the feature is great: I’ll use it for sure, since it clarifies the intentions of the code.
    The weird thing is that local enums were the “standard” way to use them in TwinCAT2, then they have been “dismissed” with TC3…
    About the limitation you mentioned in declaring them in Methods, that’s a shame just as the fact that you can’t declare VAR_INST variables in methods belonging to PROGRAMS…

    Thank you for the great work you are doing Jakob, keep on going!

    Ale.

  3. Hey Jakob!

    Beat me to it. I also thought about writing about it :D.

    I also discovered this feature relatively recently when I saw someone else’s example code. Later I also saw it in your YouTube series. Then I tried to find any documentation, but couldn’t find any. :shrug:

    It is a very useful feature, but very unfortunate that it is not allowed in methods :(.

  4. Fellowwithlaptop

    Besides the restriction that local enumarations cannot be created in methods, it is also not possible to use the to_String attribute. An error output with the enum text is therefore not possible.
    However, I also suspect that this is a feature of Codesys, but not widely communicated due to its limitation.

    • Good point! I rarely use the TO_STRING function (which was if I remember, was introduced in 4024?), but it’s for sure something that probably many will find limiting! Thanks for the feedback.

      • Bart

        The TO_STRING function called upon an enum defined with {attribute ‘to_string’} option is super powerful. Instead of magic number You get a very descriptive string. I use that in my state loggers to keep track of n last states of the object. With this it is very helpful to commission the block and troubleshoot any erratic behavior. We can also store that state in an alarm history for example. If this feature is not available for internal enums then I’ll stick with the global ones.

  5. Alexander

    Hi Jakob,

    nice read 🙂 as discussed also previously about the GVL i also find it very bad that enumerations are being treated as global since i also prefer to give useful names instead of “magic numbers”. So definetly me and the team will use the local enumerations, good spot!

    cheers

  6. anuj

    Good way to make enumeration in TwinCAT. When i compile the project as a library project then it will give the assignement error. Then i went back to again to use a standard DUT and defined a seperate variable. If anyone face the same problem and has a solution would be happy to hear from you.

    • Dennis Clyne

      @anuj I played around using this feature within a PLC library project and did not have any errors or issues

  7. Tim

    Nice find.

    One thing that has come up is some warnings in the precompiler if there’s two locally declared enumerations which include the same names. For example, two state machines:

    ReadFileState: (Init, Idle, Open, Read, Close, Error, Done);
    WriteFileState: (Init, Idle, Open, CreateDir, Write, Close, Error, Done);

    IF (WriteFileState = Init) THEN
    will generate warnings about comparing one enumeration type with another.

    So it seems like the scope of the individual enumerations is the entire function block. With a globally declared enumeration I would usually use {attribute ‘qualified_only’}, and use it like:
    IF WriteFileState = WRITE_FILE_STATE.Init THEN to avoid confusion, but I don’t know if that’s possible

    • Ben Hutcheson

      I’m guessing this is one of the reasons why it isn’t documented. You need to be very careful if you do use the same name within a couple of local enums that it needs to have the same value.
      So basically if you do this.
      ReadFileState: (Idle, Init, Open, Read, Close, Error, Done);
      WriteFileState: (Init, Idle, Open, CreateDir, Write, Close, Error, Done);

      and then do this
      IF (WriteFileState = Init) THEN

      The Init enum may use the ReadFileState enum and Init will be 1 instead of 0.

      You can specificy the IMPLICIT name for the enum (_IMPLICIT_FUNCTION_BLOCK_ENUM, or something like that) that it is given, but it seems like a hack and the ide flags it as an issue.

  8. Dennis Clyne

    I experimented with this today and it is a neat feature that I think I will include in my programming going forward, some additional observations I noticed (TC 4024.11):

    – I couldn’t find the basic data type of the enum indicated anywhere, but it appears to be an INT, and it doesn’t look like it can be specified to another type like its global sibling can

    – It does not appear to have the ‘strict’ attribute

    – It does allow you to assign values for each label: eTextWriteState : (OPEN_FILE := 1, WRITE_TO_FILE := 2, CLOSE_FILE := 3, ERROR := -1);

    – If you declare two variables with the same enum declaration, TwinCAT recognizes and compiles them as the same:

    eTextWriteState : (OPEN_FILE, WRITE_TO_FILE, CLOSE_FILE, ERROR);
    eTextWriteStateDifferent : (OPEN_FILE, WRITE_TO_FILE, ERROR);
    eLastFiveStates : ARRAY [1..5] OF (OPEN_FILE, WRITE_TO_FILE, CLOSE_FILE, ERROR);

    eLastFiveStates[1] := eTextWriteState; // no warning
    eLastFiveStates[1] := eTextWriteStateDifferent; // causes warning

    I will say that one drawback of using ‘hidden’ features is that there is always the possibility they can be changed or removed with little to no notice.

  9. Nick

    Love the YouTube series Jakob. Fantastic job!

    One thing I have noticed; if i declare an implicit enumeration…
    statusByte: (OM1selected := 2#0000_0001, OM2selected := 2#0000_0010) USINT; //for example

    and
    o_StatusByte: USINT;

    …I cannot then assign “statusByte” to “o_StatusByte”
    o_StatusByte := statusByte;

    I get the error: “cannot convert type….

    Even though both are the same type (USINT). Any ideas anyone?

  10. Andy

    I have used your example and tried it out in my program succesfully.
    that being said I’m still struggling displaying implicit enumerations in the visulization. Is that at all possible and if so what is the trick?

    regards
    Andy

    • Marton

      Hi Andy,

      I don’t know if this is still relevant to you, but what i’ve found is that you have to create a Text list with your desired steps. Create a Text field in your visualisation, then link your new text list to it under “Dynamic texts”. Your “Text index” should be your local enumeration variable.

      Cheers

  11. Martijn

    Dear Jakob,

    Thank you for sharing! I experienced exactly the same limitation!

    However, the local ENUM only seems to be useful for internal usage within a POU, since it seems not to be accessible outside it.

  12. Michel

    This is one handy function.

    One issue worth mentioning; once a local Enumeration variable is declared, you can’t switch to the ‘tabular view’ of the declaration aera any more. Twincat will give a error, crashes and closes (build 4024.35)

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.