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.
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.
#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
|
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
.
#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
|
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.
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
|
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.
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
|
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.
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
|
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.
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.
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.
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:
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 argumentsIs Sender = FALSE
andError Flag = SECONDARY_ERROR_FLAG
and alsoError Code != BROKEN_ERROR_FRAME
, REC shall be increased by 1. -
When an FMU receives a
Bus Error
operation where the arguments (Is Sender = FALSE
andError Flag = PRIMARY_ERROR_FLAG
) orError Code = BROKEN_ERROR_FRAME
, REC shall be increased by 8. -
When an FMU receives a
Bus Error
operation where the argumentsIs Sender = TRUE
orError Code = BROKEN_ERROR_FRAME
, TEC shall be increased by 8. Exception:Status = ERROR_PASSIVE
andError Code = ACK_ERROR
. -
When an FMU provides a
Transmit
operation and receives aConfirm
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 viaStatus
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 viaStatus
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 viaStatus
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 viaStatus
operation to the Bus Simulation. -
The
BUS_OFF
status shall be set back toERROR_ACTIVE
when the Network FMU simulates a controller reset (optional) and has received a total of 128Transmit
orBus 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
-
[RFC2119] Bradner, S.: Key words for use in RFCs to Indicate Requirement Levels. RFC 2119, March 1997. https://www.ietf.org/rfc/rfc2119.txt
-
[SEMVER200] Preston-Werner, T.: Semantic Versioning 2.0.0. https://semver.org/spec/v2.0.0.html