[02] - Coding Convention
Code should be easily readable and be able to troubleshooted by someone who is not the writer. These conventions aid in this practice, and adhering to these rules will provide uniformity and clarity across multiple projects.
Naming
Style
There are many different kind of naming conventions, but for general variables in code, we will use Capitalized_Snake_Case.
camelCase is more ergonomic for typing, requiring fewer keystrokes, but sacrifices readability.
One solution, to make Snake_Case easier to type is to:
- Install Windows Power Toys from the Microsoft Store (No IT required)
- Under Keyboard Manager, remap a shortcut to product "_" (I recommend [SHIFT] + [SPACE])
Motor_On_Timer: TON;
Major_Fault_Flag: BOOL;
Total_Loop_Count: UINT;
Intent
Use names which both reveal your intent and removes the need for supplementing your code with comments.
// Bad - Put the UPS in to full capacity mode if the Power Supply 10 is in fault
IF SUP_10.flt THEN
UPS.smode := 1;
END_IF
// Good
IF PowerSupply10.HasFault THEN
UPS.mode := FULL_CAPACITY_MODE;
END_IF
Avoid Abbreviations
Where possible, do not abbreviate. This forces us to think, decode and remember a term. Every time we give our brain something to do it forces it to work harder. Instead favour whole words and phrases so that our mind can stay on auto-pilot when reading our code.
// Bad
Str_Btn: BOOL;
// Good
Start_Button: BOOL;
Hungarian Notation
Hungarian notation is a method of naming variables with prefixes determined by the kind/type of variable.
One variation of Hungarian Notation is practically useless, the other is not.
The practically useless version is called Systems Hungarian, and it is unfortunately, the more common version used today.
bMy_Bit: BOOL;
iCounter: INT;
dwTotal_Cycles: DOUBLEWORLD;
The variable is prefixed by a letter indicating what type of variable it is. i:integer, b:boolean, w:word, etc.
This gives us basically no useful information, and furthermore, is not what the inventor intended the notation be used for
The much more useful version is called Apps Hungarian. It is used to differentiate different kinds of datatypes.
aiCurrent_Box: INT; //ai = array index
rwFirst_Selection: INT; // rw = Row
colFirst_Selection: INT; // col = Column
// All are ints. but differentiated by their kinds
To start with, we will not be using either version of Hungarian Notation. It would be preferable to use verbose names, though this may be changed in the future if a use-case is decided. In either version, readability of the variable name is sacrificed.
Variable Alignment
This style guide is designed to be as simple as possible, therefore the same has been applied to aligning variables. You will see in the example below that a neat layout is formed by using only standard tab indentation and spaces.
Four core benefits are realized,
- It's fast.
- Refactoring using Find / Replace will not destroy the layout if variable names change in length.
- There is no requirement for collaborators to install custom tools to automate alignment.
- It looks nice on all fonts.
Variables must be aligned using the following conventions.
- Begin each line with a tab character (not spaces).
- Use a single space between each section: name, type, and (if present) address or initializer.
- End each line with a semicolon, with no trailing space before it.
VAR
Example_Bool : BOOL := TRUE;
Example_Input AT %I* : INT;
Example_Array : ARRAY [0..n] OF REAL;
Counter : Counter(StartValue := 123, EndValue := 456);
END_VAR
In cases where there are many variables or many similar variables, tabs may be used in place of spaces to align the portions of the declarations in accordance with their name length, though the spacing will be wrong if variable names change
VAR
Test_Bool : BOOL := TRUE;
A_Really_Big_Bool : BOOL := TRUE;
Bit : BOOL := TRUE;
Extra_Long_Massive_Bool : BOOL := TRUE;
Example_Bool : BOOL := TRUE;
END_VAR
Multi-Line Initializers
Classes with multiple initializers may be broken across multiple lines for readability.
- The opening parenthesis must appear immediately after the class name.
- Each parameter appears on its own line, indented with a tab.
- A comma follows each parameter except the last.
- The closing parenthesis and semicolon appear on a new line, aligned with the variable name.
VAR
tcpIpClient1 : TcpIpClient(
ServerAddress := '192.168.0.1',
ServerPort := 123,
ConnectionTimeout := T#5s,
KeepAlive := TRUE
);
tcpIpClient2 : TcpIpClient(
ServerAddress := '192.168.0.2',
ServerPort := 456,
ConnectionTimeout := T#5s,
KeepAlive := TRUE
);
END_VAR
Likewise with general variables, tabs may be used in place of spaces to align the portions of the declarations in accordance with their name length
VAR
tcpIpClient1 : TcpIpClient(
ServerAddress := '192.168.0.1',
ServerPort := 123,
ConnectionTimeout := T#5s,
KeepAlive := TRUE
);
tcpIpClient2 : TcpIpClient(
ServerAddress := '192.168.0.2',
ServerPort := 456,
ConnectionTimeout := T#5s,
KeepAlive := TRUE
);
END_VAR
Comments
Ask yourself 'why do I need a comment?'
If you need them to convey your code's true meaning then you should refactor and rename your variables to make your intent clear.
Do not use comments for auditing purposes, tracking changes, or attributing authorship. This adds extra work to the coders who follow you. Favour source control instead.
Comments must not be used to,
- explain how your code works (Rename / Refactor)
- comment out old code (Delete it and use Source Control)
- add author information (use Source Control)
Comments may be used
- as place holders or for general information.
Remember, no comments are the best form of comments!
Enumeration
Global Enumeration should be kept to a minimum as this will cause tight coupling between objects which use them. Tight coupling is a bad thing. Try instead to replace global enumeration with other objects which can be passed around.
Global enumeration should be used very rarely and for portions of the program that are foundational to its base-level operation and are VERY unlikely to ever change.
Local Enumeration
Enumeration inside of a class is good and will make CASE statements easier to use and extend. Use inline enumeration as shown below. This keeps internal state locked away inside a class and prevents us needing to manage this enumeration in a second file.
Enumeration must be ALL_CAPITALS with underscore word separation.
FUNCTION_BLOCK Cylinder
VAR
Cylinder_State : (RETRACTED, EXTENDING, EXTENDED, RETRACTING, JAMMED_EXTENDING, JAMMED_RETRACTING);
END_VAR
---
CASE Cylinder_State OF
RETRACTED:
//
EXTENDING:
//
//...
END_CASE
Using the instance of the enumeration in the case, instead of using a separate variable, will populate the runtime viewing with state text matching the appropriate strings, rather than an integer.
Constants
Constants must be ALL_CAPITALS with underscore word separation.
Replace "Magic numbers" with constants to assist with readability and understanding.
VAR CONSTANT
MAXIMUM_BUFFER_SIZE_IN_BYTES : UDINT := 200;
END_VAR
As with all symbol names, a well-named constant should convey its purpose and context clearly.
Class
TwinCAT 3 does not have built in Classes. Instead we will use function blocks with the outlined attributes
You must not place code in the FUNCTION_BLOCK body. Use a public method instead, such as .CyclicCall();
You must not allow VAR_INPUT, VAR_OUTPUT and VAR_IN_OUT to exist in a class declaration.
Always use the linkalways and enable_dynamic_creation attributes on function blocks.
{attribute 'linkalways'}
{attribute 'no_explicit_call' := 'This FB is a CLASS and must be accessed using methods or properties'}
{attribute 'enable_dynamic_creation'}
Naming
Always use PascalCase for class names, and name them using nouns or noun phrases. Do not use type prefixes such as fb, FB, T_, etc., as these resemble Hungarian notation and add unnecessary noise. However, an underscore may be used in the name if it helps clarify type specialization or improves overall readability (e.g., AnalogValue_LREAL).
FUNCTION_BLOCK PushButton
Private Variables
Private variables always start with an underscore "_". Private variables which are referenced by a property, share the exact name of that property with the exception of the preceding underscore.
Full Class Example
In this example, the Cylinder has an .Is_Busy property,
{attribute 'linkalways'}
{attribute 'no_explicit_call' := 'This FB is a CLASS and must be accessed using methods or properties'}
{attribute 'enable_dynamic_creation'}
FUNCTION_BLOCK Cylinder EXTENDS ComponentBase IMPLEMENTS I_Move
VAR
_Is_Busy : BOOL;
State : (RETRACTED, EXTENDING, EXTENDED, RETRACTING, JAMMED_EXTENDING, JAMMED_RETRACTING);
Retracted_Sensor : I_Sensor;
Extended_Sensor : I_Sensor;
END_VAR
Methods
Classes may use methods. Methods in a Function Block or Program should be avoided as this is a mix of paradigm which may be confusing for the user.
Methods, like classes should have one reason to exist. They should do one job.
Naming
Always use PascalCase for method names. You should use verb or verb phrase for method names. Do not use prefixes.
Cylinder.Retract();
Persistent_Data.Save();
Methods should not have the word 'and' in them as this is a sure sign that they are doing more than one job.
Arguments
Methods should have as few arguments as possible. Zero arguments is best. One argument is ok. Two arguments is almost too many. Try to pass objects in to methods in order to reduce argument count. Avoid Boolean arguments as this is typically a sign that a method is dual purpose.
// example of a bad method
CSV_Reader.LoadFileAndParseResultsToArrayIfLoggingIsEnabled('file.csv',Results_Array,Logging.IsEnabled);
// refactored to divide the method in to smaller methods with smaller number of arguments.
IF logging.IsEnabled THEN
CSV_Reader.LoadFile('file.csv');
CSV_Reader.ParseToArray(Results_Array);
END_IF
Function Blocks
Using a TC3 function block as a function block requires some adherence to rules to avoid encroaching on the use-case of other objects.
Methods & Actions
Methods and actions may be used in a function block ONLY if they are PRIVATE and used internally, only. Private methods and actions are named in PascalCase and preceded with an "_".
Calling a function block should be done by calling the entire block in-line
// First time calling the function block
Flash_Bit(En:=True, On_Time:=T#250MS);
.
.
.
// Update the function block later in the code when needed
Flash_Bit();
This means that the majority of the code of the function block must be contained in body of the function block.
As with Class Methods, it is usually best to keep the inputs to a function block to a minimum.
Use a structure as an input if there is a frequent group of data that needs to be passed to different FBs
Program Organization Units (POU)
POUs operate very similarly to Function Blocks, unfortunately.
Methods & Actions
Methods may NOT be used in a POU. Actions may ONLY be used in a POU if they are private.
Actions are useful in a POU if there is a section of code that would benefit from being written in a different IEC style. For example calling motor commands with the Function Block Diagram FBD language.