Annotating a TwinCAT3 project

Pytmc is capable of generating most of the DB file but some settings require human direction. Developers set this configuration by adding an attribute pragma to TwinCAT3 variables when they’re declared. These pragmas can be appended to variables in project files and library files.

Data and Record Types

TwinCAT data types and their corresponding record types are as follows:

Data type

Lower bound

Upper bound

Memory space

Record Type

Scalar DTYP

Waveform DTYP

BOOL

0

1

8 bit

bi, bo

asynInt32

asynInt8ArrayIn, asynInt8ArrayOut

BYTE

0

255

8 bit

longin, longout

asynInt32

asynInt8ArrayIn, asynInt8ArrayOut

SINT

-128

127

8 bit

longin, longout

asynInt32

asynInt8ArrayIn, asynInt8ArrayOut

USINT

0

255

8 bit

longin, longout

asynInt32

asynInt8ArrayIn, asynInt8ArrayOut

WORD

0

65535

16 bit

longin, longout

asynInt32

asynInt16ArrayIn, asynInt16ArrayOut

INT

-32768

32767

16 bit

longin, longout

asynInt32

asynInt16ArrayIn, asynInt16ArrayOut

UINT

0

65535

16 bit

longin, longout

asynInt32

asynInt16ArrayIn, asynInt16ArrayOut

ENUM

0

4294967295

32 bit

longin, longout

asynInt32

asynInt16ArrayIn, asynInt16ArrayOut

DWORD

0

4294967295

32 bit

longin, longout

asynInt32

asynInt32ArrayIn, asynInt32ArrayOut

DINT

-2147483648

2147483647

32 bit

longin, longout

asynInt32

asynInt32ArrayIn, asynInt32ArrayOut

UDINT

0

4294967295

32 bit

longin, longout

asynInt32

asynInt32ArrayIn, asynInt32ArrayOut

LWORD

0

2**64-1

64 bit

N/A

N/A

N/A

LINT

-2**63

2**63-1

64 bit

N/A

N/A

N/A

ULINT

0

2**64-1

64 bit

N/A

N/A

N/A

REAL

-3.4E+38

3.4E+38

32 bit

ai, ao

asynFloat64

asynFloat32ArrayIn, AsynFloat32ArrayOut

LREAL

-1.797693134862316e+308

1.797693134862358e+308

64 bit

ai, ao

asynFloat64

asynFloat64ArrayIn, AsynFloat64ArrayOut

STRING

Varies

waveform

asynFloat64

asynInt8ArrayIn, asynInt8ArrayOut

Lines marked as N/A are not supported by pytmc.

Pragma syntax

At a minimum, developers must specify a PV. Specifying an IO direction for each field is recommended but not required. This would look like the following:

{attribute 'pytmc' := '
    pv: TEST:MAIN:SCALE
    io: i
'}
scale : LREAL := 0.0;

The {attribute 'pytmc' := ' and '} specify the beginning and end of the pragma that pytmc will recognize. The middle two lines specify the configuration for this variable.

Pytmc uses a custom system of configuration where newlines and white space in a line is important. All lines begin with a title and the title ends before the colon. All parts thereafter are the ‘tag’ or the configuration state for this setting. Some title types such as field can have multiple settings for a single PV.

A pragma could have multiple fields specified. For example, an ai record TEST:MAIN:SCALE would be generated from the following, with a slope of 2.0 and an offset of 1.0, updating only at a rate of once per second:

{attribute 'pytmc' := '
    pv: TEST:MAIN:SCALE
    io: i
    field: AOFF 1.0
    field: ASLO 2.0
'}
scale : LREAL := 0.0;

Declaring top level variables

This is an example of the simplest configuration a developer can provide to instantiate a variable.

{attribute 'pytmc' := '
    pv: TEST:MAIN:SCALE
    io: i
'}
scale : LREAL := 0.0;

The developer must specify the PV name (pv: line). All other fields are optional. We recommend that the user specify the direction of the data (io: line) however.

Pytmc needs no additional information but users have the option to override default settings manually. For information on all the pragma fields, consult the Pragma fields section.

Declaring encapsulated variables

Variables declared inside of data structures can be processed by pytmc so long as each level of encapsulation, all the way down to the first level, is marked for pytmc.

The instantiation of encapsulating data types only needs the pv: line. The instantiation of a function block could resemble the following:

{attribute 'pytmc' := '
    pv: TEST:MAIN:COUNTER_B
'}
counter_b : counter;

A variable declared within the counter function block could resemble the following:

{attribute 'pytmc' := '
    pv: VALUE
    io: i
'}
value_d : DINT;

When combined, the PV specified at the instantiation of the user-defined data type will be appended to the beginning of the PV for all data types defined within. Each step further into a data structure can add an additional section to the PV. In the example above the final PV will be TEST:MAIN:COUNTER_B:VALUE. The colons are automatically included.

This can be recursively applied to data types containing data types.

Information other than the PV name name can be specified at the datatype instantiation if you wish to make generalizations about the variables contained inside. These generalizations are overridden if the same field is specified either on a contained datatype or variable.

For example the following code block will assign a field: of DESC test to all the variables and datatypes that it contains unless they specify their own setting for DESC.

{attribute 'pytmc' := '
    pv: BASE
    field: DESC test
'}
counter_b : counter;
{attribute 'pytmc' := '
    pv: VALUE_F_R
    field: DESC test
    io: i
'}
value_d : DINT;

Declaring bidirectional PVs

In instances where a single TwinCAT variable should be able to be both written and read, multiple PVs can be specified. This allows multiple EPICS records to be tied to a single TwinCAT variable.

{attribute 'pytmc' := '
    pv: TEST:MAIN:ULIMIT
    io: io
'}
upper_limit : DINT := 5000;

In this case, two records will be generated: TEST:MAIN:ULIMIT and TEST:MAIN:ULIMIT_RBV.

Arrays

By default, structures with a pragma will generate PVs for each array index, including all encapsulated sub-elements that also have an associated pragma.

Depending on the number of elements in the array, the PV name will be zero-padded to aid in future expansion. Reminding ourselves that array bounds are inclusive in TwinCAT, the following pragma:

{attribute 'pytmc' := '
    pv: MY:ARRAY
'}
myStructure : ARRAY [0..5] of DUT_MyStructure

would generate these prefixes:

MY:ARRAY:00:
MY:ARRAY:01:
MY:ARRAY:02:
MY:ARRAY:03:
MY:ARRAY:04:
MY:ARRAY:05:

The formatting of this may be customized, but it is not recommended to do so in general. Adding expand: :%.3d would extend the zero-padding to 3 digits, regardless of the number of array elements.

It is also possible to select individual elements or a range of elements from a large array by way of the array pragma.

To include only the first 2 elements (0 and 1) of this large 101 element array, the following pragma could be used:

{attribute 'pytmc' := '
    pv: MY:ARRAY
    array: 0..1
'}
myStructure : ARRAY [0..100] of DUT_MyStructure

The array pragma is flexible, allowing for the following:

Array Pragma

Elements Selected

0, 1, 2

0, 1, 2

0..2

0, 1, 2

99..

99, 100

..5

0, 1, 2, 3, 4, 5

..5, 99

0, 1, 2, 3, 4, 5, 99

Pragma fields

Format: {field}: [value]

A pragma key or field (before the :) and the value after the : are used to generate records in EPICS.

pv

This constructs the PV name that will represent this variable in EPICS. It is the only mandatory configuration line. This line can be used on specific variables as well as the instantiations of data types.

Note

$ may not be used in pragmas due to some TwinCAT limitations as of version 4024. An alternative character @ may be used in its place for pv names. This can also be customized using the macro_character pragma key.

io

This is a field that defaults to ‘io’. Specify the whether the IOC can only read or also write the value. Values being sent from the PLC to the IOC should be marked as input with input (or equivalently i, ro) and values being sent to the PLC from the IOC should be marked output (or equivalently o, rw).

Note

The following are valid for input-only (read-only) pragmas: input, i, and ro.

The following are valid for input-output (read-write) pragmas: output, io, rw, and o.

Update rate

Format: update: {rate}{s|Hz} [{poll|notify}]

Example: 1s, 1s poll, 2Hz notify

By default, any given PLC variable will be polled at a rate of T=1s (1Hz). Other poll rates planned to be available by default may be selected on a per-record (*) basis: T=.5s (2Hz), T=1s (1Hz), T=2s (.5hz), T=10s (.1Hz), and T=50s (.02Hz), or as configured by the IOC startup script.

By default, any given PLC variable will be bundled together to be polled at a fixed rate. This is the recommend means of using the ADS IOC. Using one of the default polling rates is the only supported method currently, though these might be configurable in the future in the IOC startup script. New poll rates cannot be created in the TwinCAT code.

To customize the polling rate, specify the desired rate in either seconds or hertz in an update pragma key. For example:

update: 1Hz
update: 1s
update: 0.5s
update: 2Hz

The keyword poll can also be used to explicitly mark it for polling:

update: 1s poll

For faster rates, an ADS concept of notifications, can be used. These are conceptually similar to callback-on-change in Python or camonitor in the context of EPICS.

Use the notify keyword in the update setting to enable this:

update: 10Hz notify
update: 0.1s notify

Note

Adding too many of these notifications can significantly slow down a PLC, even when specified at slow rates. As such, notify should be used sparingly.

(*) This is on the wishlist for ads-ioc. As of December 2019, all polled records will be processed at a rate of 1 Hz/the IOC-configured poll rate.

Scale and Offset

Integer and floating point values may have an EPICS-side scaling applied.

Example:

scale: 3.0
offset: 1.0

Values will be scaled according to the following:

readback_value = raw_value * scale + offset
setpoint_value = (user_value - offset) / scale

Note

If either scale or offset are applied to an integer symbol, the generated EPICS record type will no longer be a “long” integer input/output record but rather change to an analog input/output record.

Keep this in mind if using advanced “field” directives in your pragmas.

If unspecified, scale will be assumed to be 1.0 and offset 0.0.

Archiver settings

Format: archive: {rate}{s|Hz} [{scan|monitor}]

Example: 1s, 1s scan, 2Hz monitor

Using the database-generating tool pytmc db along with the pragma key archive will automatically generate archiver appliance cron-job compatible .archive files (i.e., those in $IOC_DATA/$IOC/archive/*.archive). The cron job will read these files and automatically configure the archiver to archive the listed PVs.

Without an archive pragma key, the default setting is 1s scan. This means that your PVs will be archived at a rate of once per second, using the scan method.

For more information on the two methods, see the EPICS Archiver Appliance documentation.

Note

If the update frequency is slower than the specified archive frequency, the archive frequency will be reduced.

Note

Large arrays will not be archived, regardless of pragma settings.

Note

Additional fields can be specified for archiving through the archive_fields key, which is a space-delimited list of field names.

Example: archive_fields: DESC PREC

Record fields

This specifies additional field(s) to be set on the generated EPICS record(s). Multiple field lines are allowed. These lines determine the PV’s behaviors such as alarm limits and scanning frequency.

The format is as follows:

field: FIELD_NAME field value

This would correspond to a field in the record being generated as follows:

record(ai, "my_record") {
   ...
   field(FIELD_NAME, "field value")
}

SCAN

While the SCAN field is special in EPICS to specify the rate at which records should be updated, pytmc requires that such configuration be done through the update pragma key (see Update rate).

Autosave

Autosave fields for individual EPICS records are configured by default with pytmc. It is possible to customize this behavior with additional pragmas, optionally specifying different fields for input or output records.

Pass 0 indicates restoring information prior to record initialization on IOC initialization, whereas pass 1 indicates restoring information after record initialization. Pass 0 is generally safe and does not cause record processing, whereas pass 1 is just as if one were to caput to the record after starting the IOC. When in doubt, use pass 0 and/or ask an EPICS expert.

To apply to either input or output records, pragma keys autosave_pass0 or autosave_pass1 can be used.

To only apply to input records, pragma keys autosave_input_pass0 autosave_input_pass1 can be used.

To only apply to output records, pragma keys autosave_output_pass0 autosave_output_pass1 can be used.

For example, a pragma like the following:

autosave_pass0: VAL DESC

Would result in both input and output records having these fields marked for autosaving:

record(ai, "my:record_RBV") {
   ...
   info(autosaveFields_pass0, "VAL DESC")
}

record(ao, "my:record") {
   ...
   info(autosaveFields_pass0, "VAL DESC")
}

Sub-Items

There is a special syntax to pragma a member of a structure differently depending on the instance.

Given this example function block:

FUNCTION_BLOCK FB_Test
VAR
    {attribute 'pytmc' := 'pv: variable1'}
    variable1 : LREAL := 0.0;

    {attribute 'pytmc' := 'pv: variable2'}
    variable2 : LREAL := 0.0;
END_VAR
END_FUNCTION_BLOCK

Adding these pragmas to two instances:

{attribute 'pytmc' := '
    pv: TEST:A
    variable1.io: i
    variable2.io: i
'}
fbTestA : FB_Test;

{attribute 'pytmc' := '
    pv: TEST:B
    variable1.io: io
    variable2.io: io
'}
fbTestB : FB_Test;

The above would result in two different io settings for fbTestA.variable1 and fbTestB.variable1.

There are some limitations on this preliminary feature. You may not add or change the following fields for now:

pv
link
field

The primary use case is expected to be array, to limit the number of array instances generated with hard-coded upper bounds on encapsulated structures.