Error Handling

Rule #6:

Implement an user friendly Error Handling (Required)

  • Return only error codes which are documented within your library.

  • Never simply return the original error code from sub libraries.

  • Map it to your own error domain.

Note

Each function block should have some outputs for error signaling. Please refer the chapter Behaviour Model and Interface Design

The error handling should be designed in a way that only error codes are returned, which are documented in the affected library. It is very convenient but not recommended simply to return untreated error codes from sublibraries. This would result in a bad user experience. It is recommended to map foreign error codes to the error range of the affected library.

With CODESYS 3.5.3.0 the tool generates a new warning message when the compiler detects an assignment of one ENUM type to another. See the example described in the following. In this example we take a closer look to the interaction between the Memory Block Manager library (MBM) and the Function Block Factory (FBF). Each library defines its own ERROR Enum data type.

The ERROR Enum of the library “Function Block Factory” (FBF)

{attribute 'qualified_only'}
TYPE ERROR : (
    NO_ERROR := 0, // The defined operation was executed successfully
    TIMEOUT := 1, // The specified operation time was exceeded
    INVALID_PARAM := 10, // One or more function parameters have no valid value
    NO_MEMORY := 20, // The extension of memory pool is not possible
    (*...*)
END_TYPE

The ERROR Enum of the library “Memory Block Manager” (MBM)

{attribute 'qualified_only'}
TYPE ERROR : (
    NO_ERROR := 0, // The defined operation was executed successfully
    NO_MEMORY := 10 // The memory pool has no further capacity
    HANDLE_INVALID := 20, // The object was not created properly or has been already released
    WRONG_ALIGNMENT := 30, // The structure description aligns not properly to the block specification
    (*...*)
END_TYPE
  • Two libraries are isolated with a namespace (in this example FBF and MBM).

  • Each ERROR Enum declaration should respect two predefined error codes.

    • NO_ERROR ⇒ 0 (Zero)

    • TIME_OUT ⇒ 1 (One)

  • If the TIME_OUT error code has no usage in a specific domain the value should not reused for an other error code.

  • Any error code need a short description about the background of its error condition.

  • A Enum data types should be isolated from other Enum data types with its own namespace ({attribute 'qualified_only'}). FBF.ERROR.NO_MEMORY has a completely different meaning as MBM.ERROR.NO_MEMORY.

In the FBF library (Version 3.5.1.0) we can find the following code:

// :return: hInst
METHOD prvAllocInstMem : CAA.HANDLE
VAR_OUTPUT
    eError: ERROR;
END_VAR
(*...*)
prvAllocInstMem := MBM._PoolGetBlock(_hPool, ADR(eError)); // see note (1)
(*...*)
eError := MBM._PoolExtendH(_hPool, ctInstCount); // see note (2)
  1. Try to write a value of type MBM.ERROR to a memory location of type FBF.ERROR => not recommended

  2. Assign a value of type MBM.ERROR to a variable of type FBF.ERROR => Compiler warning

This code breaks rule #6 and cannot be translated without a compiler warning in CODESYS 3.5.3.0. In the current document we recommend two possible solutions:

  1. Summarize all ERROR values from the sublibrary in one ERROR value of the affected library

  2. Map some ERROR values of the sublibrary to the related values of the affected library

In the following see an example for solution 1:

// :return: hInst
METHOD prvAllocInstMem : CAA.HANDLE
VAR_OUTPUT
    eError: ERROR;
END_VAR
VAR
    (*...*)
    iError : INT;
END_VAR
(*...*)
prvAllocInstMem := MBM._PoolGetBlock(_hPool, ADR(iError)); // see note (3)
eError := SEL(iError=0, ERROR.NO_MORE_MEMORY, ERROR.NO_ERROR); // see note (4)
(*...*)
iError := MBM._PoolExtendH(_hPool, ctInstCount); // see note (5)
eError := SEL(iError=0, ERROR.NO_MORE_MEMORY, ERROR.NO_ERROR); // see note (4)
  1. Try to write a value of type MBM.ERROR to a memory location of type INT => ok

  2. Summarize all MBM.ERROR values to one FBF.ERROR value

  3. Assign a value of type MBM.ERROR to a variable of type INT => ok.

This new code assumes that all errors from the MBM library will be treated as a “no more memory available” situation.

Now see an example for solution 2:

Sometimes the two concerned ENUM data types of two libraries are in a special relationship. Some elements of the first ENUM type have the same integer values as the related elements of the second ENUM data type. In this case, using a simple cast operation is a possible solution to avoid the warning message.

eError := TO_INT(MBM._PoolExtendH(_hPool, ctInstCount));

This example assumes that the base data type of eError is INT. In CODESYS it is possible to define a different base type for a specific ENUM type. Please adapt the example in the right way. For example:

{attribute 'qualified_only'}
TYPE FILE_ENCODING :
(
    UNKNOWN,    // The encoding is not known
    UTF8,       // The Unicode encoding is UTF-8
    UTF16BE,    // The Unicode encoding is UTF-16 big endian
    UTF16LE     // The Unicode encoding is UTF-16 little endian
) : DINT;
END_TYPE

{attribute 'qualified_only'}
TYPE ENCODING :
(
    UNKNOWN,    // The encoding is not known
    UTF8,       // The Unicode encoding is UTF-8
    UTF16BE,    // The Unicode encoding is UTF-16 big endian
    UTF16LE     // The Unicode encoding is UTF-16 little endian
) : USINT;
END_TYPE

VAR
    eFileEncoding : FILE_ENCODING;
    eEncoding : ENCODING;
END_VAR

eEncoding := TO_USINT(eFileEncoding);

Working together with sub libraries brings up the need for mapping the different error domains to the one local domain. The next example demonstrates the possible design of an error code mapping function. It handles the error codes (from CS.ERROR and CO.ERROR) of two sub libraries and tries to map these to the one local Error Enum (CANOPEN_KERNEL_ERROR) (All enum data types in this example have the base type INT).

FUNCTION MapError : CANOPEN_KERNEL_ERROR
VAR_INPUT
    eError : INT;
END_VAR

MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_UNKNOWN_ERROR;
IF eError = CS.ERROR.NO_ERROR THEN
    MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_NO_ERROR;
ELSIF eError > CS.ERROR.FIRST_ERROR AND eError < CS.ERROR.LAST_ERROR THEN
    CASE eError OF
        CS.ERROR.TIME_OUT           : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_TIMEOUT;
        CS.ERROR.REQUEST_ERROR      : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_REQUEST_ERROR;
        CS.ERROR.WRONG_PARAMETER    : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_WRONG_PARAMETER;
        CS.ERROR.NODEID_UNKNOWN     : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_NODEID_UNKNOWN;
        CS.ERROR.SDOCHANNEL_UNKNOWN : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_SDOCHANNEL_UNKNOWN;
    ELSE
        MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_OTHER_ERROR;
    END_CASE
ELSIF eError > CO.ERROR.FIRST_ERROR AND eError < CO.ERROR.LAST_ERROR THEN
    CASE eError OF
        CO.ERROR.TIME_OUT       : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_TIMEOUT;
        CO.ERROR.NO_MORE_MEMORY : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_NO_MORE_MEMORY;
        CO.ERROR.WRONG_PARAMETER: MapError := CANOPEN_KERNEL_ERROR.CANOPEN_WRONG_PARAMETER;
        CO.ERROR.NODEID_UNKNOWN : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_NODEID_UNKNOWN;
        CO.ERROR.NETID_UNKNOWN  : MapError := CANOPEN_KERNEL_ERROR.CANOPEN_NETID_UNKNOWN;
    ELSE
        MapError := CANOPEN_KERNEL_ERROR.CANOPEN_KERNEL_OTHER_ERROR;
    END_CASE
END_IF

This design assumes CS.ERROR.NO_ERROR has the same value as CO.ERROR.NO_ERROR and the rest of the value range of CS.ERROR and CO.ERROR is disjunct.