MIO04 Detailed Description
Please note the following restrictions with MIO04 Revision 00:
- Do not connect USB Service interface when you want to use COM Port RTS, CTS lines, or when using half-duplex mode!
- Wifi performance is not ideal and will be improved in coming revisons
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
- Baudrates up to 460800 Baud
- Galvanic Isolation between the COM port and other ports
Connection
COM port connector on MIO04:
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
Important Notes:
Whether to use RS232 or RS485 is solely defined by hardware connection. It is not defined by software configuration. However, hardware flowcontrol and half/full-duplex operation must be configured in software!
For RS485/RS422 half-duplex operation, you must externally connect the COM ports Rx pins with the corresponding Tx pin
In RS485/RS422 mode, please add termination resistors to the end of the line. The termination must be 120R at each end of the cable.
Operating Principle of IP Based COM-Port
The COM port on the MIO04 is exposed to the network as a ttynvt
(network virtual terminal) service. For the COM port, a TCP-Server, implementing the RFC2217 protocol is active on the MIO04.
RFC2217 extends the telnet protocol by adding COM port configuration commands, flow control and more. In addition to RFC2217 standard, we have added a proprietary extension for MIO04 to control half duplex operation. (See TNS_CTL_ENABLE_RS485
and TNS_CTL_DISABLE_RS485
in the telnet header file)
WARNING RFC2217 is a protocol without any security measures. Please use it only in trusted, closed networks!
To access the COM port from the host, you can
- Use the ttynvt program to create a virtual
/dev/tty
device for each COM port. - Use pyserial’s RFC2217 support
Using ttynvt
An instance of ttynvt
must be started for each virtual RFC2217 COM port. ttynvt
creates a device entry in /dev
, e.g. /dev/ttyMIO04-1-com1
. Your application can then use this device as any other tty
device in the system.
Ci4Rail Linux Image
If you are using a Linux Image from Ci4Rail, ttynvt
instances are automatically started for each ttynvt COM port in the network via ttynvt-runner. ttynvt-runner
is started as a systemd
service.
Linux Images without ttynvt support
If ttynvt isn’t integrated in your Linux Image, follow this section.
Compile ttynvt
Requirements on linux host:
- kernel must support FUSE (
FUSE_FS=y
) libpthread
andlibfuse
must be installed in the root filesystem.- git, autoconf, make, gcc installed
Build:
$ git clone https://gitlab.com/ci4rail/ttynvt.git
$ cd ttynvt
$ autoreconf -vif
$ ./configure
$ make
Start ttynvt
Start ttynvt as root:
$ ttynvt -f -E -M <major-number> -m <minor-number> -S <device-ip-address>:<port-number> -n <tty-devicename>
Parameters:
Parameter | Description |
---|---|
major number | The major number identifies the driver associated with the device. Select for the ttynvt driver a number that is not yet in use in your system |
minor number | Provide a new minor number for each device. Select a number between 1 and 255 |
device-ip-address | The IP address of your MIO04 |
port-number | The port number in your MIO04 associated with the COM port |
tty-devicename | The name to create for the device. E.g. ttyMIO04-1-com1 |
To find the IP address and Port, ensure avahi
and avahi-utils
are installed on host and run avahi-browse
. Example:
$ avahi-browse -rt _ttynvt._tcp
+ enp5s0 IPv4 MIO04-1-com1 _ttynvt._tcp local
+ enp5s0 IPv4 MIO04-1-com2 _ttynvt._tcp local
= enp5s0 IPv4 MIO04-1-com1 _ttynvt._tcp local
hostname = [MIO04-1.local]
address = [192.168.24.89]
port = [10000]
txt = ["funcclass=ttynvt" "security=no" "auxport=not_avail-0" "auxschema=not_avail"]
= enp5s0 IPv4 MIO04-1-com2 _ttynvt._tcp local
hostname = [MIO04-1.local]
address = [192.168.24.89]
port = [10001]
txt = ["funcclass=ttynvt" "security=no" "auxport=not_avail-0" "auxschema=not_avail"]
Autostart of ttynvt
Instead of starting ttynvt
manually, you can use ttynvt-runner. ttynvt-runner
starts ttynvt
automatically for each virtual COM port in the network.
Using Half Duplex Mode with ttynvt
Half duplex mode must be used with the RS485/RS422 signals on the connector. In half duplex mode, two or more participants share the Rx/Tx lines. Only one participant is allowed to drive the Tx data.
To enable half duplex mode, the application must call iotcl TIOCSRS485
on the tty device provided by ttynvt
. In python, this can be done like this:
import serial
import serial.rs485
ser = serial.Serial(
port="/dev/MIO04-1-com1",
baudrate = 115200,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1,
rtscts = False
)
# Set half duplex operation
ser.rs485_mode = serial.rs485.RS485Settings()
ser.write(bytes("Hello World", "utf8"))
data = ser.read(10)
print("data: {}".format(data))
NOTE: In half duplex mode, the data that is sent to the line is NOT echoed back to the application, because the receiver is disabled while sending.
Using pyserial with RFC2217
pyserial
provides a way to communicate directly with RFC2217 compliant servers, this way, ttynvt
is not required.
The following example opens COM1 of the MIO04 with device id MIO04-1
(port 10000 is the port for COM1
), sets the baudrate to 19200 and sends and receives some characters:
import serial
ser = serial.serial_for_url("rfc2217://MIO04-1.local:10000?ign_set_control")
ser.baudrate = 19200
ser.write(bytes("Hello World", "utf8"))
data = ser.read(10)
print("data: {}".format(data))
NOTE pyserial with RFC2217 does not support the proprietary extension to set the COM port into half duplex mode. Therefore, half-duplex mode is not supported.
Considerations when running with Handshaking enabled
The COM port supports hardware (CTS/RTS) and software (XON/XOFF) handshaking for flow control. A receiver may stop the sender by de-asserting CTS or sending an XOFF character.
However, be aware about the following behaviour: When the MIO04 is the sender and the connected receiver forces the transmission to stop, the MIO04 may hang until the transmission is re-enabled again by the receiver. Even if you stop the transmitting process, the firmware will not accept any new connection request until its transmit buffer gets empty.
If you run into such a situation and the receiver will never re-enable the transmission (for example because you have selected hardware handshake but you haven’t connected CTS), you must restart the MIO04, for example using the io4edge-cli
.
CANBus Interface
The MIO04 has one CANBus interfaces, 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
- Fixed Listen Only Mode
- One Acceptance Mask/Filter
Connection
Connection is done via 9-pin DSub 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
NOTE: At the moment, timestamps are expressed in micro seconds 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 containsBucketSamples
, 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 theBucketSamples
. 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 contain1..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 containsbucketSamples
, 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 thebucketSamples
. 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
toTrue
. In this mode, samples are sent as soon as possible after they have been received. This means that the buckets contain1..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 eventErrorEvent_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 eventcanl2.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 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:
NOTE: When using SocketCAN, you must configure the CAN Controller persistently as shown here MIO04, as described 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.