Skip to content

Detailed Description

Port Reference Table

In case you want to address the services of the MIO04 via IP:Port, you can use the following table to find the correct port number:

Port Function
10000 COM1
10001 COM2
10002 CAN

Serial Interfaces

The MIO04 has two identical serial interfaces, labelled COM1 and COM2.

Features

  • RS232 or RS485 full-duplex or RS485 half-duplex
  • Virtual TTY support using RFC2217
  • Appears as a standard TTY device on Linux hosts
  • Baud rates up to 460800 baud
  • Galvanic isolation between the COM ports and other interfaces
  • Hardware flow control lines available in RS232 mode

Connection

COM port connector on MIO04:

COM port connector

Pin functionality as viewed from MIO04:

Pin Symbol Description
1 RS485_TX+ RS485 positive transmit line
2 RS232_TXD RS232 transmit line
3 RS232_RXD RS232 receive line
4 RS485_RX+ RS485 positive receive line
5 GND Ground (isolated from other interfaces)
6 RS485_TX- RS485 negative transmit line
7 RS232_CTS RS232 clear to send
8 RS232_RTS RS232 request to send
9 RS485_RX- RS485 negative receive line

Typical Connection Examples

COM port connection

Info

Whether you use RS232 or RS485 is defined solely by the hardware wiring. Hardware flow control and half-/full-duplex operation must be configured in software.

Info

For RS485/RS422 half-duplex operation, connect the COM port RX pins with the corresponding TX pins externally.

Info

In RS485/RS422 mode, add termination resistors to the end of the line. The termination must be 120 ohms at each end of the cable.

Serial Function Block

The io4edge TTYnvt function exposes the serial interface of the MIO04 over the network using RFC2217. Clients can connect via TCP and obtain a virtual TTY that behaves like a local serial port.

The module provides multiple serial ports. Refer to the service table above for the service suffixes and TCP ports.

You can use the ttynvt function block utilities from the io4edge SDK or any RFC2217-capable client. Typical workflow:

  1. Discover the service via mDNS or take the static service name.
  2. Connect with an RFC2217 client (for example socat or pyserial).
  3. Configure baud rate, parity, stop bits, hardware handshake according to your peripherals. Hardware handshake is supported.

Example using socat to create a local pseudo terminal:

socat -d -d pty,link=/tmp/com1.tty,raw tcp:<module-ip>:10000

Afterwards use /tmp/com1.tty like a local serial device from your applications.

CAN Bus Interface

The MIO04 has one CAN bus interface labelled CAN.

Features

  • ISO 11898 CANBus Interface, up to 1MBit/s
  • Usable for direct I/O or as data logger with multiple data streams.
  • SocketCAN Support
  • Standard and Extended Frame Support
  • RTR Frame Support
  • Optional Listen Only Mode
  • One Acceptance Mask/Filter

Connection

Connection is provided via a 9-pin D-Sub plug:

Pin Symbol Description
1 - Not connected
2 CAN_L CAN signal (dominant low)
3 GND_ISO CAN ground
4 - Not connected
5 SHIELD Shield
6 GND_ISO CAN ground
7 CAN_H CAN signal (dominant high)
8 - Not connected
9 - Not connected

Using the io4edge API to access CAN Function

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

First, install the io4edge client library.

Want to have a quick look to the examples? See our Github repository.

Connect to the CAN Function

To access the CAN Function, create a Client and save it to the variable c. Pass as address either a service address or an ip address with port. Example:

  • As a service address: MIO04-1-can
  • As an IP/Port: e.g. 192.168.201.1:10002

We need this client variable for all further access methods.

import (
  "fmt"
  "os"
  "time"
  "github.com/ci4rail/io4edge-client-go/canl2"
  fspb "github.com/ci4rail/io4edge_api/canL2/go/canL2/v1alpha1"
)

func main() {
  const timeout = 0 // use default timeout

  c, err := canl2.NewClientFromUniversalAddress("MIO04-1-can", timeout)
  if err != nil {
    log.Fatalf("Failed to create canl2 client: %v\n", err)
  }
}

To access the CAN Function, create a Client and save it to the variable can_client. Pass as address either a service address or an ip address with port. Examples:

  • As a service address: MIO04-1-can
  • As an IP/Port: 192.168.201.1:10000

We need this client variable for all further access methods.

import io4edge_client.canl2 as canl2
import io4edge_client.functionblock as fb

def main():
  can_client = canl2.Client("MIO04-1-can")

Bus Configuration

There are two ways to configure the CAN function:

  • Using a persistent parameter that is stored in the flash of the MIO04, as described here.
  • Temporarily, via the io4edge CANL2 API, as shown below
Temporary Bus Configuration

When applying a configuration via the API, the configuration is only active until the next restart of the device. The configuration is not stored in flash. When the device is restarted, it will apply the persistent configuration stored in flash, or - if no persistent configuration is available - will keep the CAN controller disabled.

Bus Configuration can be set via UploadConfiguration.

  err = c.UploadConfiguration(
    canl2.WithBitRate(125000),
    canl2.WithSamplePoint(0.625),
    canl2.WithSJW(1),
    canl2.WithListenOnly(false),
  )

Bus Configuration can be set via upload_configuration.

Note: The sample point is given as a thousandth of a percent, e.g. 625 for 62.5%.

    can_client.upload_configuration(
        canl2.Pb.ConfigurationSet(
            baud=125000,
            samplePoint=625,
            sjw=1,
            listenOnly=False,
        )
    )

Receiving CAN Data

To receive data from the CANbus, the API provides functions to start a Stream.

In the stream the firmware generates Buckets, where each Bucket contains a number of Samples. Each sample contains:

  • A timestamp of the sample
  • The CAN frame (may be missing in case of bus state changes or error events)
  • The CAN Bus state (Ok, error passive or bus off)
  • Error events (currently: receive buffer overruns)

For efficiency, multiple samples are gathered are sent as one Bucket to the host.

Without any parameters, the stream receives all CAN frames:

// start stream
err = c.StartStream()
Missing parameters to ´StartStream` will take default values:

  • Filter off (let all CAN frames pass through)
  • Maximum samples per bucket: 25
  • Buffered Samples: 50
  • Keep Alive Interval: 1000ms
  • Low Latency Mode: off

  for {
    // read next bucket from stream
    sd, err := c.ReadStream(time.Second * 5)

    if err != nil {
      log.Printf("ReadStreamData failed: %v\n", err)
    } else {
      samples := sd.FSData.Samples
      fmt.Printf("got stream data with %d samples\n", len(samples))

      for _, s := range samples {
        fmt.Printf("  %s\n", dumpSample(s))
      }
    }
  }

func dumpSample(sample *fspb.Sample) string {
  var s string

  s = fmt.Sprintf("@%010d us: ", sample.Timestamp)
  if sample.IsDataFrame {
    f := sample.Frame
    s += "ID:"
    if f.ExtendedFrameFormat {
      s += fmt.Sprintf("%08x", f.MessageId)
    } else {
      s += fmt.Sprintf("%03x", f.MessageId)
    }
    if f.RemoteFrame {
      s += " R"
    }
    s += " DATA:"
    for _, b := range f.Data {
      s += fmt.Sprintf("%02x ", b)
    }
    s += " "
  }
  s += "ERROR:" + sample.Error.String()
  s += " STATE:" + sample.ControllerState.String()

  return s
}

    # start stream, accept all frames
    stream_start = canl2.Pb.StreamControlStart(
        acceptanceCode=0, acceptanceMask=0
    )

    can_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=False,
        ),
    )

    while True:
        try:
            generic_stream_data, stream_data = can_client.read_stream(timeout=3)
        except TimeoutError:
            print("Timeout while reading stream")
            continue

        print(
            "Received %d samples, seq=%d" % (len(stream_data.samples), generic_stream_data.sequence)
        )

        for sample in stream_data.samples:
            print(sample_to_str(sample))


def sample_to_str(sample):
    ret_val = "%10d us: " % sample.timestamp
    if sample.isDataFrame:
        frame = sample.frame
        ret_val += "ID:"
        if frame.extendedFrameFormat:
            ret_val += "%08X" % frame.messageId
        else:
            ret_val += "%03X" % frame.messageId
        if frame.remoteFrame:
            ret_val += " R"
        ret_val += " DATA:"
        for i in range(len(frame.data)):
            ret_val += "%02X " % frame.data[i]
        ret_val += " "

    ret_val += "ERROR: " + canl2.Pb._ERROREVENT.values_by_number[sample.error].name
    ret_val += (
        " STATE: "
        + canl2.Pb._CONTROLLERSTATE.values_by_number[sample.controllerState].name
    )
    return ret_val

Warning

At the moment, timestamps are expressed in microseconds relative to the start of the MIO04. Future client libraries will map the time to the host's time domain.

Controlling the Stream

The stream behavior can be fine-tuned to the application needs. If you do not specify any parameters, the default values are used.

  • The BucketSamples parameter (default: 25) defines the number of samples per bucket. If the bucket contains BucketSamples, it is sent to the client.
  • The KeepAliveInterval parameter (default: 1000) defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.
  • The BufferedSamples parameter (default: 50) defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, BufferedSamples should be at least two times the BucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

  • If you want low latency on the received data, you can enable the "low latency" mode by using LowLatencyMode (default: false). In this mode, samples are sent as soon as possible after they have been received. This means that the buckets contain 1..BufferedSamples samples.

  // configure stream to send the bucket at least once a second
  // configure the maximum samples per bucket to 25
  // configure low latency mode
  // configure the buffered samples to 200
  err = c.StartStream(
      canl2.WithFBStreamOption(functionblock.WithKeepaliveInterval(1000)),
      canl2.WithFBStreamOption(functionblock.WithBucketSamples(25)),
      canl2.WithFBStreamOption(functionblock.WithLowLatencyMode(true))
      canl2.WithFBStreamOption(functionblock.WithBufferedSamples(200)),
  )
If you don't want to receive all CAN identifiers, you can specify an acceptance code and mask that is applied to each received frame. The filter algorithm is pass_filter = (code & mask) == (received_frame_id & mask). The same filter is applied to extended frames and standard frames.

  // apply a filter. Frames with an identifier of 0x1xx pass the filter, other frames are filtered out
  code := 0x100
  mask := 0x700
  err = c.StartStream(
    canl2.WithFilter(code, mask),
  )

The stream behavior can be fine-tuned to the application needs:

  • The bucketSamples parameter defines the number of samples per bucket. If the bucket contains bucketSamples, it is sent to the client.

  • The keepAliveInterval parameter defines the maximum time in ms between two buckets. If the bucket is not full, it is sent after the configured interval.

  • The bufferedSamples parameter defines the number of samples that can be buffered in the device. If the buffer is full, the oldest samples are overwritten. As a rule of thumb, bufferedSamples should be at least two times the bucketSamples. Select a higher number if your reception process is slow to avoid buffer overruns.

  • If you want low latency on the received data, you can enable the "low latency" mode by setting low_latency_mode to True. In this mode, samples are sent as soon as possible after they have been received. This means that the buckets contain 1..bufferedSamples samples.

If you don't want to receive all CAN identifiers, you can specify an acceptance code and mask that is applied to each received frame. The filter algorithm is pass_filter = (code & mask) == (received_frame_id & mask). The same filter is applied to extended frames and standard frames.

    # apply a filter. Frames with an identifier of 0x1xx pass the filter, other frames are filtered out
    stream_start = canl2.Pb.StreamControlStart(
        acceptanceCode=0x100, acceptanceMask=0x700
    )

    can_client.start_stream(
        stream_start,
        fb.Pb.StreamControlStart(
            bucketSamples=100,
            keepaliveInterval=1000,
            bufferedSamples=200,
            low_latency_mode=args.lowlatency,
        ),
    )
Error Indications and Bus State

The samples in the stream contain also error events and the current bus state.

Error events can be:

  • ErrorEvent_CAN_NO_ERROR - no event
  • ErrorEvent_CAN_RX_QUEUE_FULL - either the CAN controller dropped a frame or the stream buffer was full

Each sample contains also the bus state. When the bus state changes, a sample without a CAN frame may be generated. Furthermore, client method GetCtrlState may be used to query the current status.

Bus States can be:

  • ControllerState_CAN_OK - CAN controller is "Error Active"
  • ControllerState_CAN_ERROR_PASSIVE - CAN controller is "Error Passive"
  • ControllerState_CAN_BUS_OFF - CAN controller is bus off

Error events can be:

  • canl2.Pb.ErrorEvent.CAN_NO_ERROR - no event
  • canl2.Pb.ErrorEvent.CAN_RX_QUEUE_FULL - either the CAN controller dropped a frame or the stream buffer was full

Each sample contains also the bus state. When the bus state changes, a sample without a CAN frame may be generated. Furthermore, client method ctrl_state may be used to query the current status.

Bus States can be:

  • canl2.Pb.ControllerState.CAN_OK - CAN controller is "Error Active"
  • canl2.Pb.ControllerState.CAN_ERROR_PASSIVE - CAN controller is "Error Passive"
  • canl2.Pb.ControllerState.CAN_BUS_OFF - CAN controller is bus off

Sending CAN Data

To send CAN data, prepare a batch of frames to be sent and call SendFrames.

    // prepare batch of 10 frames
    frames := []*fspb.Frame{}

    for j := 0; j < 10; j++ {
      f := &fspb.Frame{
        MessageId:           uint32(0x100),
        Data:                []byte{},
        ExtendedFrameFormat: false,
        RemoteFrame:         false,
      }
      len := j % 8
      for k := 0; k < len; k++ {
        f.Data = append(f.Data, byte(j))
      }
      frames = append(frames, f)
    }
    // send frames at once
    err = c.SendFrames(frames)

    if err != nil {
      log.Printf("Send failed: %v\n", err)
    }
If you want a high send throughput, it is important not to call SendFrames with only a single frame. If you do so, overhead of the transmission to the io4edge will reduce your send bandwidth.

The maximum number of frames you can send with one batch is 31.

You can't send frames and SendFrames will return an error in the following scenarios (status codes for go can be found here)

To send CAN data, prepare a batch of frames to be sent and call send_frames.

  frames = []
  for msg in range(10):
      frames.append(
          canl2.Pb.Frame(
              messageId=0x100,
              data=bytes([msg for _ in range(msg % 8)]),
              extendedFrameFormat=False,
              remoteFrame=False,
          )
      )
  can_client.send_frames(frames)

If you want a high send throughput, it is important not to call send_frames with only a single frame. If you do so, overhead of the transmission to the io4edge will reduce your send bandwidth.

The maximum number of frames you can send with one batch is 31.

You can't send frames and send_frames will return an error in the following scenarios)

Condition Error Code
No CANbus Configuration applied UNSPECIFIC_ERROR
Configured for listen only mode UNSPECIFIC_ERROR
Firmware Update in progress TEMPORARILY_UNAVAILABLE
Transmit buffer full TEMPORARILY_UNAVAILABLE
CANBus State is BUS OFF HW_FAULT

In case the firmware's transmit buffer is full, the firmware will send none of the frames and return TEMPORARILY_UNAVAILABLE error. Therefore, you can retry later with the same set of frames.

Bus Off Handling

When the CAN controller detects serious communication problems, it enters "Bus off" state. In this state, the CAN controller cannot communicate anymore with the bus.

When bus off state is entered, The firmware waits 3 seconds and then resets the CAN controller.

Multiple Clients

It is possible to have multiple clients active at the same time. For example: One client sends data, a second client receiving a stream with a specific filter and a another client receiving a stream with a different filter.

Using SocketCAN

In Linux, SocketCAN is the default framework to access the CANBus from applications.

The MIO04 can be integrated into SocketCAN using the socketcan-io4edge gateway:

MIO04 product view

Note

When using SocketCAN, you must configure the CAN Controller persistently as shown here.

In Ci4Rail Linux Images, the socketcan-io4edge gateway is started automatically by socketcan-io4edge-runner which detects available io4edge devices with CAN support and start an instance of the socketcan-io4edge gateway, if the corresponding virtual can instance exists. For an example, see here.