FMI logo

The FMI LS Bus Implementers' Guide is a free resource intended to give non-normative recommendations and guidance to implementers of the Functional Mock-up Interface layered standard Network Communication (FMI-LS-BUS). This document is continually revised based on implementer and user feedback and input. All of the content is to be considered non-normative and shall not be considered to supplant any normative statement in the FMI 3.0 standard, the FMI-LS-BUS layered standard, or any other standard or layered standard. Releases and issues can be found on github.com/modelica/fmi-guides.


Copyright © 2021-2024 The Modelica Association Project FMI.

This document is licensed under the Attribution-ShareAlike 4.0 International license. The code is released under the 2-Clause BSD License. The licenses text can be found in the LICENSE.txt file that accompanies this distribution.

Attention is drawn to the possibility that some of the elements of this document may be the subject of patent rights. Modelica Association shall not be held responsible for identifying such patent rights.

1. Introduction

This document provides best practice recommendations to implementers of FMI Layered Standard for Network Communication. The overarching goal of the recommendations is to aid interoperability of implementations and ensure good ease-of-use for the user in employing FMI.

This document is intended for simulation tool vendors planning to support FMI Layered Standard for Network Communication in their products, either for creating or simulating such FMUs, or both. It assumes a technical audience familiar with the specification documents and tries to give further information and hints in areas where either the relevant specification documents have been found sometimes to be less clear than expected or where user expectations are relevant to implementation choices. It also considers errors and bugs encountered in the practical use of existing FMI implementations and guides to avoid common pitfalls.

This document will be maintained and expanded over time as new issues or needs for clarification appear.

2. Common

TODO

3. Physical Signal Abstraction ("high cut")

This chapter contains details on common topics of the physical signal abstraction layer.

4. Network Abstraction ("low cut")

This chapter contains details on common topics of the network abstraction layer.

4.1. CAN, CAN FD, CAN XL

This chapter contains details on CAN, CAN FD, CAN XL specific topics.

4.1.1. Getting Started with CAN, CAN FD and CAN XL

This section shows the exemplary implementation in conjunction with the provided header files of the fmi-ls-bus layered standard.

4.1.1.1. Provided Header Files

Besides the textual specification for FMUs with bus support, the Layered Standard for Network Communication also provides a C API to make the creation of FMUs with bus support as easy and generalized as possible.

  • fmi3LsBus.h provides general macros, types and structures of common Bus Operations. This header file applies to all supported bus types of the layered standard.

  • fmi3LsBusCan.h provides macros, types and structures of Bus Operations explicit for CAN, CAN FD and CAN XL. Primarily, structures are included here that allow the Bus Operations specified by the layered standard to be easily created and used.

  • fmi3LsBusUtil.h provides common utility macros and structures for all supported bus types.

  • fmi3LsBusUtilCan.h provides CAN, CAN FD and CAN XL explicit utility macros.

4.1.1.2. General Aspects

Bus Operations represent protocol units to be transmitted in the environment of the layered standard based on the Layered Standard Bus Protocol. All FMI variables required for the exchange are grouped by a defined Bus Terminal. Bus Operations are created and processed within an FMU. This can be done either in Step Mode or in Event Mode. For a transfer, all Bus Operations are serialized together in a fmi3Binary variable. This serialization can be done using the provided header files or using a custom approach. After the contents of the binary variables have been transferred to another FMU via an FMU importer, they are deserialized again and then processed inside the receiver.

create process bus operations
Figure 1. Create, process and transfer Bus Operations.

The point in time where Bus Operations are transferred between different FMUs is defined by the usage of fmi3Clock variables. For this reason, the exchange of Bus Operations is always carried out within the Event Mode. Details about possible entry points regarding the FMI interface will be provided later in this section. However, it is important to know that due to the resulting decoupling between the generation, processing and transfer of Bus Operations, the need for buffer semantics arises. This semantics is reflected directly in the structure and handling of the header files provided.

4.1.1.3. Buffer Handling

For exchanging Bus Operations between FMUs, variables of type fmi3Binary type are used. For this reason, appropriate variables must first be set up within the implementation that represent the content for this exchange. The buffer variable can be easily initialized in the form of an fmi3UInt8 array of any size. It should be noted that the entire buffer size must of course provide enough space for all created Bus Operations during a fmi3DoStep. To simplify our example, the buffers are declared as global variables within the FMU.

Since describing and reading Bus Operations from a simple array can be quite complicated, the common utility headers provide an fmi3LsBusUtilBufferInfo entity. This abstraction represents a kind of view of the underlying buffer array and allows simplified access using additionally provided functionality.

The following program code shows the definition and initialization of a buffer for transmitting (Tx) and receiving (Rx) Bus Operations in the form of an array. In addition, an fmi3LsBusUtilBufferInfo is created for both buffer variables. Using FMI3_LS_BUS_BUFFER_INFO_INIT, the underlying buffer is coupled to the respective fmi3LsBusUtilBufferInfo instance.

Setting up buffering and fmi3LsBusUtilBufferInfo instance
#include "fmi3PlatformTypes.h"
#include "fmi3LsBusUtil.h"      (1)

fmi3UInt8 TxBufferCan[2048];    (2)
fmi3UInt8 RxBufferCan[2048];
fmi3LsBusUtilBufferInfo TxBufferInfoCan;    (3)
fmi3LsBusUtilBufferInfo RxBufferInfoCan;

fmi3Instance fmi3InstantiateCoSimulation(...) {
    FMI3_LS_BUS_BUFFER_INFO_INIT(&TxBufferInfoCan, TxBufferCan, sizeof(TxBufferCan));    (4)
    FMI3_LS_BUS_BUFFER_INFO_INIT(&RxBufferInfoCan, RxBufferCan, sizeof(RxBufferCan));
}
1 Necessary include of the fmi3LsBusUtil.h header file.
2 Definition and initialization of a fmi3Binary buffer variable.
3 Definition of fmi3LsBusUtilBufferInfo variable instance.
4 Coupling of a Buffer and a fmi3LsBusUtilBufferInfo variable.

The buffer is always treated by the provided header functionalities using FIFO (First In - First Out) semantics. The further usage of the fmi3LsBusUtilBufferInfo variable is discussed later.

Summary
  • The transfer of Bus Operations must typically be decoupled from creation and processing

  • The API provides macros for buffering of Bus Operations in a FIFO manner

4.1.1.4. Creating Bus Operations

The header file fmi3LsBusUtilCan.h offers macros for all Bus Operations specified by the layered standard, which minimize the effort required to create and serialize such an operation. The macros are always provided according to the following syntax: FMI3_LS_BUS_<BusType>_CREATE_OP_<OperationName>. Following these rule, the macro for creating a CAN Transmit operation is FMI3_LS_BUS_CAN_CREATE_OP_CAN_TRANSMIT. A macro for creating an operation also assigns it to a buffer described by an fmi3LsBusUtilBufferInfo instance.

The following program code shows how to first define the payload and the ID that should be used in the CAN Transmit operation. Afterwards, the fmi3LsBusUtilBufferInfo is reset by using FMI3_LS_BUS_BUFFER_INFO_RESET. FMI3_LS_BUS_BUFFER_INFO_RESET sets the internal position of the fmi3LsBusUtilBufferInfo instance to zero, so that it is essentially emptied and written from the beginning. This is necessary to ensure that Bus Operations that have already been transmitted are not transmitted again. FMI3_LS_BUS_CAN_CREATE_OP_CAN_TRANSMIT now creates a new CAN Transmit operation with the associated parameters such as CAN ID and payload and adds them directly to the fmi3LsBusUtilBufferInfo instance. Querying the status of a fmi3LsBusUtilBufferInfo instance allows you to check whether there is still enough space in the underlying buffer. In the last step, FMI3_LS_BUS_BUFFER_LENGTH is used to check whether there are Bus Operations in the respective fmi3LsBusUtilBufferInfo variable that should be transmitted in Event Mode.

Creation of a CAN Transmit operation
#include "Fmi3LsBusUtilCan.h"

fmi3Status fmi3DoStep(..., eventHandlingNeeded, ...) {
    fmi3UInt8 msg[] = "Hey guys";   (1)
    fmi3LsBusCanId msgId = 42;      (2)

    /* Reset read/write positions of the BufferInfo variable */
    FMI3_LS_BUS_BUFFER_INFO_RESET(&TxBufferInfoCan);    (3)

    /* Create a CAN Transmit operation to be send */
    FMI3_LS_BUS_CAN_CREATE_OP_CAN_TRANSMIT(&TxBufferInfoCan, msgId, <options>, sizeof(msg), msg);    (4)

    if(!TxBufferInfoCan.status){   (5)
        /* Error: No free buffer space available */
    }

    ...

    if(FMI3_LS_BUS_BUFFER_LENGTH(&TxBufferInfoCan) > 0){
        *eventHandlingNeeded = fmi3True;    (6)
    }
}
1 Creation of CAN frame payload.
2 Definition of CAN ID of the specified frame.
3 Resetting of fmi3LsBusUtilBufferInfo variable instance.
4 Creation of a CAN Transmit operation and adding it to the specified fmi3LsBusUtilBufferInfo variable.
5 Verify that free buffer space is available.
6 Signal that Event Mode is needed.

According to the same principles, any specified operation can be created using the corresponding macro.

Summary
  • Bus Operations can be created by using the provided FMI3_LS_BUS_<BusType>_CREATE_OP_<OperationName> macros

  • The CREATE_OP macros are creating a Bus Operation and updating the given buffer in a single step

4.1.1.5. Transmitting Bus Operations

Within the layered standard, the connection between the data to be exchanged (TX_Data and Rx_Data) and the time of exchange (Tx_Clock and Rx_Clock) has been well defined. The simplest case represents a triggered Tx_Clock that basically allows to signal events when returning from fmi3DoStep.

The program code below schematically illustrates an implementation. fmi3GetClock is called by the FMU importer in Event Mode after fmi3DoStep has completed or ended prematurely. Within fmi3GetClock, TX_CLOCK_REFERENCE represents the valueReference of the respective Tx_Clock. The usage of the macro FMI3_LS_BUS_BUFFER_IS_EMPTY indicates whether there is data to be transferred in the respective buffer. If this is the case, the corresponding Tx_Clock will tick.

The content of the fmi3LsBusUtilBufferInfo instance is provided to the FMU importer via fmi3GetBinary. The value can be easily passed on to the FMI interface by using the FMI3_LS_BUS_BUFFER_START macro. FMI3_LS_BUS_BUFFER_LENGTH can also be used to set the size of the fmi3Binary variable.

Transmit Bus Operations via triggered clock
fmi3Status fmi3GetClock(fmi3Instance instance,
                         const fmi3ValueReference valueReferences[],
                         size_t nValueReferences,
                         fmi3Clock values[]) {
    ...
    for (size_t i = 0; i < nValueReferences; i++) {
        if (valueReferences[i] == TX_CLOCK_REFERENCE) {
            if(!FMI3_LS_BUS_BUFFER_IS_EMPTY(&TxBufferInfoCan)) {    (1)
                *values[i] = fmi3ClockActive;                       (2)
            }
        }
    }
    ...
}

fmi3Status fmi3GetBinary(fmi3Instance instance,
                          const fmi3ValueReference valueReferences[],
                          size_t nValueReferences,
                          size_t valueSizes[],
                          fmi3Binary values[],
                          size_t nValues) {
    ...
    for (size_t i = 0; i < nValueReferences; i++) {
        if (valueReferences[i] == TX_DATA_REFERENCE) {
            *values[i] = FMI3_LS_BUS_BUFFER_START(&TxBufferInfoCan);        (3)
            *valueSizes[i] = FMI3_LS_BUS_BUFFER_LENGTH(&TxBufferInfoCan);   (4)
        }
    }
    ...
}
1 Verify if Bus Operations exist for transfer.
2 Activate specified Tx_Clock.
3 Get the start memory address of the buffer, by using fmi3LsBusUtilBufferInfo instance.
4 Get the size of the buffer, by using fmi3LsBusUtilBufferInfo instance.

Note that, according to the FMI 3.0 standard, fmi3GetClock only returns fmi3ClockActive once per clock activation.

It should be clear that, depending on the application, the different FMI clock types each offer advantages and disadvantages. See also the corresponding chapter in the layered standard.

Summary
  • The LS-BUS C API provides macros to get the START address and LENGTH of the buffer which can be used in the context of fmi3GetClock and fmi3GetBinary

4.1.1.6. Receiving Bus Operations

The indication whether new operations are pending within the Rx_Data variable is done via the Rx_Clock. This clock ticks as soon as new data is available. The operation-receiving FMU gets the Bus Operations via an fmi3Binary variable. The contents of this variable may then be copied into a buffer described by an fmi3LsBusUtilBufferInfo instance using FMI3_LS_BUS_BUFFER_WRITE.

The code snipped below shows its usage within the fmi3SetClock and fmi3SetBinary functions, which an FMU importer calls when setting the concrete Rx_Data variable.

Receiving Bus Operations
fmi3Clock RxClock;
fmi3UInt8 RxBufferCan[2048];
fmi3LsBusUtilBufferInfo RxBufferInfoCan;

fmi3Status fmi3SetClock(fmi3Instance instance,
                         const fmi3ValueReference valueReference[],
                         size_t nValueReferences,
                         const fmi3clock values[]) {
    ...
    for (size_t i = 0; i < nValueReferences; i++) {
        if (valueReferences[i] == RX_CLOCK_REFERENCE && values[i] == fmi3ClockActive) { (1)
            /* Set an indicator that clock ticked and new Bus Operations arrived */
            RxClock = values[i]; (2)
        }
    }
    ...
}

fmi3Status fmi3SetBinary(fmi3Instance instance,
                          const fmi3ValueReference valueReferences[],
                          size_t nValueReferences,
                          const size_t valueSize,
                          const fmi3Binary value, ...) {
    ...
    for (size_t i = 0; i < nValueReferences; i++) {
        if (valueReferences[i] == RX_DATA_REFERENCE && RxClock == fmi3ClockActive) {
            FMI3_LS_BUS_BUFFER_WRITE(&RxBufferInfoCan, value[i], valueSize[i]); (3)
        }
    }
    ...
}
1 Check if Rx_Clock has ticked.
2 Store the information for global access within other FMI interface functions.
3 Create an fmi3LsBusUtilBufferInfo instance based on received Bus Operations.
Summary
  • The LS-BUS API provides macros to write received binary data into a given buffer

  • The buffer is updated by the FMI3_LS_BUS_BUFFER_WRITE macro

  • The FMI3_LS_BUS_BUFFER_WRITE can be called repeatedly

4.1.1.7. Processing of Bus Operations

The Bus Operations must now be processed on the receiving side. A suitable place for implementation represents fmi3UpdateDiscreteStates. In this case, the FMI3_LS_BUS_READ_NEXT_OPERATION macro can be used to successively deserialize all received Bus Operations into the correct operation structure. After this, they can be further processed.

Processing received Bus Operations.
fmi3Status fmi3UpdateDiscreteStates(...)
{
    fmi3LsBusOperationHeader* hdr;
    ...
    if (fmi3ClockActive == RxClock) {
        /* Processing of received Bus Operations */
        while (FMI3_LS_BUS_READ_NEXT_OPERATION(&RxBufferInfoCan, hdr))    (1)
        {
            switch (hdr->type)                                            (2)
            {
                case FMI3_LS_BUS_CAN_OP_CAN_TRANSMIT:
                    fmi3LsBusCanOperationCanTransmit *receivedTransmitOp   (3)
                        = (fmi3LsBusCanOperationCanTransmit*) hdr;
            ...
            }
        }

        /* Reset clock */
        RxClock = fmi3ClockInactive;

        /* Reset read/write positions */
        FMI3_LS_BUS_BUFFER_INFO_RESET(&RxBufferInfoCan);
    }
    ...
}
1 Read the next operation from the fmi3LsBusUtilBufferInfo instance.
2 Decide which kind of operation needs to be handled.
3 Cast to the specific operation structure.
Summary
  • Received Bus Operations can be processed by using the FMI3_LS_BUS_READ_NEXT_OPERATION macro

  • FMI3_LS_BUS_BUFFER_INFO_RESET allows to reset the fmi3LsBusUtilBufferInfo instance after processing

4.1.2. Demos

The following list contains demos, which illustrate both the Bus Simulation as such and Network FMUs of various designs:

  • CAN Bus Simulation: Represents an exemplary Bus Simulation FMU for CAN. This Bus Simulation can be used in combination with the other Network FMUs listed below.

  • CAN Triggered Output: This demo Network FMU demonstrates sending and receiving multiple CAN Transmit operations using triggered output clocks.

4.1.3. Sequence Diagrams

This section contains sample sequences to clarify the facts in the CAN, CAN FD, CAN XL part.

4.1.3.1. Transmission

Figure 2 illustrates the two possible results of a Transmit operation, whereby the transition from FMU 1 → FMU 2 represents the successful case and FMU 2 → FMU 1 represents the unsuccessful case. For the second transmission, the Bus Simulation injects a transmission error. In step (1), a Transmit operation is delivered to the Bus Simulation. In step (2), the Transmit operation is successfully transferred to FMU 2. In the same step, the Bus Simulation announces the success to FMU 1 via Confirm operation. In step (3), FMU 2 wants to transmit network data to FMU 1: A Transmit operation is delivered from FMU 2 to the Bus Simulation. In step (4), the Bus Simulation intentionally injects a transmission error, which results in a Bus Error operation being sent to both Network FMUs. The Bus Error operation signals FMU 2 that its transmission attempt was not successful. Within this Bus Error operation, the Is Sender argument is set to TRUE for FMU 2, because it initiated the failing Transmit operation. Another Bus Error operation instance is provided by the Bus Simulation to FMU 1. For FMU 1, the Error Flag argument is set to PRIMARY_ERROR_FLAG, which means that FMU 1 detects the specified transmission error.

can transmission acknowledge
Figure 2. Successful and unsuccessful cases of a CAN transmission.

Normally, transmission failures cannot occur during a simulated bus transmission. For advanced testing scenarios, common bus errors are used to inject transmission errors, e.g., by the Bus Simulation FMU.

4.1.3.2. CAN Arbitration without Buffering

Figure 3 shows the realization of a CAN arbitration by using the Arbitration Lost Behavior option DISCARD_AND_NOTIFY within the Configuration operation. At the beginning, FMU 1 and FMU 2 each send network data at the same time. In this situation, an arbitration is necessary to decide which frame should be sent in this case. Both frames are transferred to the Bus Simulation, where the arbitration is performed. In the example given, the two frames with CAN ID = 15 and CAN ID = 16 are analyzed and it is decided that CAN ID = 15 wins the arbitration. The Bus Simulation then calculates the transmission time for the CAN frame with CAN ID = 15. The next time the FMI Event Mode is called for the Bus Simulation, the corresponding CAN frame is transmitted to FMU 2 and FMU 3. For CAN ID 16, FMU 2 is informed via an Arbitration Lost operation that this frame could not be sent. FMU 1 receives a Confirm operation, because the specified frame with CAN ID 15 was successfully transmitted.

can arbitration overview
Figure 3. Arbitration of CAN frames within Bus Simulation.
4.1.3.3. CAN Arbitration with Buffering

Figure 4 shows the realization of a CAN arbitration by using the Arbitration Lost Behavior option BUFFER_AND_RETRANSMIT within the Configuration operation. At the beginning, FMU 1 and FMU 2 each send network data at the same time. In this situation, an arbitration is necessary to decide which frame should be sent in this case. Both frames are transferred to the Bus Simulation, where the arbitration is performed. In the example given, the two frames with CAN ID = 15 and CAN ID = 16 are analyzed and it is decided that CAN ID = 15 wins the arbitration. The Bus Simulation then calculates the transmission time for the CAN frame with CAN ID = 15. The next time the FMI Event Mode is called for the Bus Simulation, the corresponding CAN frame is transmitted to FMU 2 and FMU 3. The Transmit operation of CAN ID 16 is buffered by the Bus Simulation and will be sent within the next time slot. The Bus Simulation does not return an Arbitration Lost operation to FMU 2. FMU 1 gets a Confirm operation, because the specified frame with CAN ID 15 was successfully transmitted.

can arbitration overview with buffer
Figure 4. Arbitration of CAN frames with buffering within Bus Simulation.

4.1.4. Realization of CAN Error Handling

This chapter describes a possible implementation of the CAN error handling within Network FMUs using a rule set based on Bus Error operations. According to the original CAN error confinement rules, each Network FMU provides its own Transmit Error Counter (TEC), Receive Error Counter (REC) and current CAN node state. The values for TEC and REC will be increased and decreased with respect to the Error Code, Is Sender and Error Flag arguments of a Bus Error operation. Based on the values of TEC and REC, the CAN controller moves in the following state machine:

can error state machine
Figure 5. CAN node state machine.

This CAN node state machine and the related TEC and REC values have to be implemented in the Network FMUs. Bus Error operations shall be directly used to maintain the TEC and REC values. The Network FMU shall react on the Bus Error operations that the Bus Simulation provides, based on the following rule set:

  • When an FMU receives a Bus Error operation where the arguments Is Sender = FALSE and Error Flag = SECONDARY_ERROR_FLAG and also Error Code != BROKEN_ERROR_FRAME, REC shall be increased by 1.

  • When an FMU receives a Bus Error operation where the arguments (Is Sender = FALSE and Error Flag = PRIMARY_ERROR_FLAG) or Error Code = BROKEN_ERROR_FRAME, REC shall be increased by 8.

  • When an FMU receives a Bus Error operation where the arguments Is Sender = TRUE or Error Code = BROKEN_ERROR_FRAME, TEC shall be increased by 8. Exception: Status = ERROR_PASSIVE and Error Code = ACK_ERROR.

  • When an FMU provides a Transmit operation and receives a Confirm operation for it, TEC shall be decreased by 1 unless it was already 0.

  • When an FMU receives a Transmit operation, REC shall be decreased by 1, if it was between 1 and 127. If it was 0, it stays 0. If REC was greater than 127, it shall be set to any value between 119 and 127.

A Network FMU communicates its current CAN node state via the Status operation by using the following rule set:

  • After the initialization of a Network FMU, the current CAN node state shall be set to ERROR_ACTIVE and communicated via Status operation to the Bus Simulation.

  • The current CAN node state of a Network FMU shall be set to ERROR_PASSIVE if the value of REC > 127 or TEC > 127 and shall be communicated via Status operation to the Bus Simulation.

  • The current CAN node state of a Network FMU shall be set to ERROR_ACTIVE if the value of REC < 128 and TEC < 128 and shall be communicated via Status operation to the Bus Simulation.

  • The current CAN node state of a Network FMU shall be set to BUS_OFF if the value of TEC > 255 and shall be communicated via Status operation to the Bus Simulation.

  • The BUS_OFF status shall be set back to ERROR_ACTIVE when the Network FMU simulates a controller reset (optional) and has received a total of 128 Transmit or Bus Error operations from the network.

If org.fmi_standard.fmi_ls_bus.Can_BusNotifications is set to false, the Confirm operation cannot be directly used as indicator to set the TEC value and will be incorrect under the rules outlined above. Also Bus Error operations are not available in this scenario, i.e. the values for TEC and REC remain zero. In this case, it is recommended to either implement error handling in a different manner or to disable it completely within the specified Network FMU.

References