Qt CAN Bus API extensions

The latest Qt 6.5 release introduced a lot of new features. You can read about all of them in the recent blog post. This blog post will give an overview of the improvements that we have made to the Qt CAN Bus module.

The Brief History

The Qt CAN Bus module has always provided APIs for high-level manipulation with CAN bus: 

  • QCanBusDevice represents a CAN device. This class can be used to read and write CAN frames. 
  • QCanBusFrame represents an actual CAN frame. 

The CAN bus frame consists of a FrameId and a Payload. In the existing API, the FrameId is represented as an unsigned integer, and the payload is just a QByteArray holding some raw bytes. 

In practice, higher-level protocols are applied on top of CAN bus frames. This overcomes the generic definition of arbitrary payloads and FrameIds, which replaces it with unique identifiers for bus devices, message and signal types which may occur, and provides type information for the values within signals.

Before Qt 6.5, the users had to provide their own implementation for extracting these values. With Qt 6.5, we introduced a set of APIs to simplify this process.

New APIs to Parse CAN Frames

The new APIs provide a way to describe the top-level protocol. Later on, these rules can be used to decode an incoming CAN frame, as well as to encode data into a CAN frame before sending it to a device. 

Let’s have a closer look at the APIs: 

  • QCanUniqueIdDescription defines the rules for storing and extracting a unique identifier within a CAN frame. 
  • QCanSignalDescription defines the rules for storing and extracting individual signal values within a CAN frame. 
  • QCanMessageDescription defines a CAN message. A message contains a unique identifier and multiple signals. 
  • QCanFrameProcessor can be used to decode an incoming CAN frame or encode the provided values into a CAN frame to be sent to the receiver. 

The QCanFrameProcessor class uses the descriptions provided by other new classes. It provides two main methods for encoding and decoding the frames: 

Let’s develop a small example to demonstrate the new APIs in action.

Demo Time

Let’s consider a protocol with the following format: 

  • A unique identifier is encoded into the first 11 bits of the FrameId. 
  • A payload contains 8 bytes. 
  • The first two bytes represent a signal called “Signal 0”. This signal contains an unsigned integer number. 
  • The next 4 bytes represent a signal called “Signal 1”. This signal contains a signed integer number. 
  • The bytes are in Little-Endian format. 

This format can be visualized with the following image.

payload

Let’s see how we can describe this protocol in terms of the new API.

Unique Identifier

Let’s start with the unique identifier.

QCanUniqueIdDescription uid; 
uid.setSource(QtCanBus::DataSource::FrameId); 
uid.setEndian(QSysInfo::Endian::LittleEndian); 
uid.setStartBit(0); 
uid.setBitLength(11); 

We define the source (FrameId), the endian (Little-Endian), the start bit and the bit length of the unique identifier.

The source defines a part of the CAN frame which will be used to extract the value. It can be either a FrameId, or a Payload.

Signal and Message Descriptions

Next, let’s define the signal descriptions.

QCanSignalDescription s0;
s0.setName(u"signal 0"_s);
s0.setDataSource(QtCanBus::DataSource::Payload);
s0.setDataEndian(QSysInfo::Endian::LittleEndian);
s0.setDataFormat(QtCanBus::DataFormat::UnsignedInteger);
s0.setStartBit(0);
s0.setBitLength(16);

QCanSignalDescription s1;
s1.setName(u"signal 1"_s);
s1.setDataSource(QtCanBus::DataSource::Payload);
s1.setDataEndian(QSysInfo::Endian::LittleEndian);
s1.setDataFormat(QtCanBus::DataFormat::SignedInteger);
s1.setStartBit(16);
s1.setBitLength(32);

For both signals, we define the signal name, the source (Payload), the endian (Little-Endian), the data type, the start bit, and the bit length. Note that the bit numbering is continuous for the whole payload. Bit 0 represents the first bit of Byte 0, and Bit 63 represents the last bit of Byte 7. The signal name must be unique within a message description. The signal names are used to provide meaningful results when parsing the frame, and also to identify the proper rules for encoding the values into a newly generated frame. The QCanSignalDescription class allows you to specify more parameters. Please refer to the documentation for the full list. 

Once the signal description is specified, we can define the message description.

QCanMessageDescription msg; 
msg.setName(u"example message"_s); 
msg.setSize(8); 
msg.setUniqueId(QtCanBus::UniqueId{0x123}); 
msg.setSignalDescriptions({s0, s1}); 

For the message description, we specify the payload size, the list of signal descriptions contained in this message, and a unique identifier. The unique identifier will be used to select a proper message description when decoding the incoming CAN frame. Like with signal descriptions, the QCanMessageDescription class allows you to specify more parameters, so make sure to check the documentation. 

Frame Processor

Once we have created the description for a unique identifier and for the message, we can create an instance of QCanFrameProcessor.

QCanFrameProcessor processor;
processor.setUniqueIdDescription(uid);
processor.setMessageDescriptions({msg});

The frame processor is initialized with the previously-generated unique id description and a list of message descriptions. In our case, the list contains only one element.

Processing CAN Frames

This section describes how the above message description can be used to parse incoming and encode new frames.

For simplicity, let's create a CAN frame manually.

QCanBusFrame frame(0x123, QByteArray::fromHex("ABCD123456780000"));

In practice, such frames will be received from a QCanBusDevice. Note that the frame has a unique identifier which matches the unique identifier of the message description which was created earlier.

To parse this frame, simply call a parseFrame() method:

QCanFrameProcessor::ParseResult result = processor.parseFrame(frame);
qDebug() << Qt::hex << Qt::showbase << Qt::uppercasedigits
         << "Unique ID:" << result.uniqueId << Qt::endl
         << "Values:" << result.signalValues;

This method returns a ParseResult struct which contains a unique identifier and a map holding signal names and signal values. The output of the qDebug() call is shown below.

Unique ID: 0x123 
Values: QMap(("signal 0", QVariant(qulonglong, 0xCDAB))("signal 1", QVariant(qlonglong, 0x78563412)))

To generate a frame, we need to call the prepareFrame() method, and pass a unique identifier and a map of signal names and signal values as parameters. The signal names must match those of the signal descriptions. For this example, we will re-use the values returned by the parseFrame() method.

QCanBusFrame generated = processor.prepareFrame(result.uniqueId,
                                                result.signalValues);
qDebug() << Qt::hex << Qt::showbase << Qt::uppercasedigits
         << generated.frameId() << generated.payload().toHex().toUpper();

The generated frame should be similar to the initial one. And that's what we see in the qDebug() output.

0x123 "ABCD123456780000"

Automatically Extracting Bus Information

The example in the previous section shows how to manually specify CAN message descriptions. This approach is rather verbose and error-prone.

Can we do better?

Luckily, there are already some well-known standards for describing CAN bus parameters. One such standard is DBC. It is a text-based format that is widely used in various industries.

In Qt 6.5, we have introduced a QCanDbcFileParser class. This class parses an input DBC file, and automatically generates the message descriptions. The DBC format also contains well-defined requirements for unique identifier, so the class also has a static method to generate the unique identifier description.

This example can illustrate the typical pattern for using this class.

QCanDbcFileParser dbcParser;
if (dbcParser.parse("path/to/file.dbc")) {
    QCanFrameProcessor processor;
    processor.setUniqueIdDescription(QCanDbcFileParser::uniqueIdDescription());
    processor.setMessageDescriptions(dbcParser.messageDescriptions());
    // Do the actual processing
} else {
    // Failed to extract data from DBC file
    qDebug() << "Got error:" << dbcParser.error();
    qDebug() << "Error details:" << dbcParser.errorString();
}

If the DBC file parsing is successful, we can use the generated message and unique identifier descriptions to create a frame processor and start processing. If the parsing fails, the API provides some convenient methods to handle the errors.

Conclusion

Thanks for reading that far. The new APIs are released as Technical Preview, so your feedback is extremely valuable to us. If you encounter any bugs or have any suggestions for improving the API, please submit your reports in our bugtracker under the SerialBus: CAN Bus component.


Blog Topics:

Comments