> ## Content Index
> Fetch the complete content index at: https://www.smallstepsystems.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Speaking CAN: Request and Response
- URL: https://www.smallstepsystems.com/speaking-can-request-and-response/
- Published: 2019-05-24T16:13:58.000Z
- Updated: 2026-01-25T13:59:33.000Z
- Author: Burkhard Stubert
- Tags: System Architecture, Embedded Linux

Controller Area Networks (CANs, for short) are used for the communication between electronic control units (ECUs) in cars, trucks, tractors, harvesters, construction machines, packaging lines and e-bikes. Qt provides the [Qt CAN Bus API](https://doc.qt.io/qt-5/qtcanbus-backends.html?ref=smallstepsystems.com) as part of its QtSerialBus module. Qt CAN Bus provides a clean and simple API to connect to a CAN bus, to send and receive CAN frames over the CAN bus.

When we build an HMI application to control a machine, we need a lot more functionality than the Qt CAN Bus API should offer. The HMI application must not send CAN frames too fast to avoid buffer overflows in the CAN controller. When the HMI application receives, say, more than 200 CAN frames per second and tries to display the changes, it will freeze. The HMI application must be able to recover from CAN bus errors. We do not want to write the code for serialising and deserialising thousands of CAN messages manually, but want to generate this code automatically.

I'll show solutions to these problems in a loose series of blog posts called *Speaking CAN*. In this first post, I'll explain how an HMI application running on a display computer retrieves the value of a parameter from an ECU. The terminal sends a request to the ECU over CAN and receives a response from the ECU over CAN.

## Setup

The next diagram shows the hardware needed for CAN communication.

![Setup for CAN communication](https://storage.ghost.io/c/c7/ee/c7ee1cf5-b86f-491b-aab7-20ad081598e1/content/images/2025/11/speaking-can-setup.png)

Hardware setup for CAN communication between **terminal* app and **ecu* app

In the red corner, we have a *display computer* (a.k.a. terminal), which runs the *terminal* app on a custom Linux system and on an i.MX6 single-board computer (SBC) like the [BD-SL-i.MX6](https://boundarydevices.com/product/bd-sl-i-mx6/?ref=smallstepsystems.com). We can replace the i.MX6 by any ARM Cortex-A SoC as long as the SoC has a CAN connector.

In the blue corner, we have a standard PC, which runs the *ecu* app on a standard Linux like Ubuntu 16.04 LTS or newer and on an Intel processor. The standard PC can be any laptop or desktop PC running Linux natively or in a virtual machine. The *ecu* app simulates the communication part of a real ECU.

We plug one end of a CAN cable with termination resistors (e.g. Peak-System's [PCAN-Cable 2](https://www.peak-system.com/PCAN-Cable-1-2.272.0.html?&L=1&ref=smallstepsystems.com)) into the display computer and the other end into the CAN-USB adapter (e.g. Peak-System's [PCAN-USB FD adapter](https://www.peak-system.com/PCAN-USB-FD.365.0.html?&L=1&ref=smallstepsystems.com)). The USB end of the adapter goes into the PC.

We could run the *terminal* app on a PC as well. Then, we would need a second CAN-USB adapter and would connect the two adapters with a CAN cable. On the PC, we could use Windows instead of Linux. We would then have to install the Windows driver for the CAN variant (e.g. PCAN) used. Linux desktop kernels come with SocketCAN drivers preinstalled.

If we use [*systemd*](https://en.wikipedia.org/wiki/Systemd?ref=smallstepsystems.com) to control the Linux start-up on the display computer or on the PC, we must provide a service file for each CAN bus. The service file *can0.service* to configure and start the network interface for CAN bus *can0* looks as follows.

```
[Unit]
Description=can0

[Service]
ExecStartPre=/sbin/ip link set can0 type can bitrate 250000 restart-ms 100
ExecStart=/sbin/ifconfig can0 up txqueuelen 10
Restart=on-failure
RestartSec=1

[Install]
WantedBy=multi-user.target
```

The command *ExecStartPre* sets the type of the network interface *can0* to *can*, sets the bitrate to 250,000 bits per second and restarts the CAN controller 100ms after a bus-off error occurred. We must install the *iproute2* package for the *ip* command to work. The command *ExecStart* sets the network interface *can0* up and running and sets the length of the write queue to 10 CAN frames.

I use a small buffer size on purpose, because I want to demonstrate a buffer overflow in one of the next posts. In real-life applications, we must find a buffer size that works fine for most usage scenarios without an overflow. I have found 64 or 128 to be good buffer sizes.

Of course, we could manually enter the commands for *ExecStartPre* and *ExecStart* in a Linux terminal to bring up the network interfaces. However, we would have to repeat the commands after every reboot.

If our system uses *init.d* for system startup, we add the following lines to the configuration file */etc/network/interfaces*.

```
auto can0
iface can0 inet manual
    pre-up /sbin/ip link set $IFACE type can bitrate 250000 restart-ms 100
    up /sbin/ifconfig $IFACE up txqueuelen 10
    down /sbin/ifconfig $IFACE down
```

Ubuntu 16.04 LTS uses this method as well.

## Connecting to the CAN bus

The *main* function in *terminal/main.cpp* creates the main model *TerminalModel*. The constructor of class *TerminalModel* connects to CAN bus *can0* using the *socketcan* plugin of Qt CAN bus. If an error occurs, the constructor emits the error text *errorStr* with the Qt signal *logMessage*. It delays the signal emission with *Qt::QueuedConnection* until the event loop is up and running. Otherwise, the signal would get lost.

```
// TerminalModel constructor    
auto errorStr = QString{};
m_can0.reset(CanBus::setUp(QStringLiteral("socketcan"),
                           QStringLiteral("can0"), errorStr));
if (!errorStr.isEmpty()) {
    QMetaObject::invokeMethod(
        this,
        [this, errorStr]() { emit logMessage(errorStr); },
        Qt::QueuedConnection);
}

```

The third line calls the function *CanBus::setup* (see the files *ecucan/canbus.h* and *ecucan/canbus.cpp*), which does the heavy lifting.

```
QCanBusDevice *setUp(const QString &plugin, 
                     const QString &interface, 
                     QString &errorStr)
{
    auto device = QCanBus::instance()->createDevice
                      (plugin, interface, &errorStr);
    if (device != nullptr && !device->connectDevice()) {
        errorStr = device->errorString();
    }
    return device;
}

```

The function *QCanBus::createDevice* creates a *QCanBusDevice* for the given CAN *plugin* (here: *socketcan*) and for the given network *interface* (here: *can0*). If the plugin name does not exist, the function returns *nullptr*. If the network interface isn't yet up and running, the function returns a non-null pointer to a *QCanBusDevice* and returns an error message through *&errorStr*.

When *device* is non-null, the *setup* function tries to connect to the CAN bus device. Connecting to a device is nothing else but opening a CAN socket. If the connection fails, *errorStr* is set to the error message. The *setup* function returns the newly created *QCanBusDevice* to the *TerminalModel* constructor. As the created *QCanBusDevice* may be null, all subsequent functions must be able to deal with *m\_can0* possibly being null.

When the *main* function exits, it will call the *TerminalModel* destructor. The destructor calls the function *CanBus::tearDown*.

```
CanBus::tearDown(m_can0.get());
```

The *tearDown* function disconnects the *QCanBusDevice* stored in *m\_can0*, if the CAN bus device is connected. Disconnecting an unconnected device yields an error and doesn't close the device properly.

```
void tearDown(QCanBusDevice *device)
{
    if (device != nullptr && 
        device->state() == QCanBusDevice::ConnectedState) {
        device->disconnectDevice();
    }
}
```

The *ecu* app does exactly the same - only in the class *EcuModel* instead of *TerminalModel*.

## A Full Round Trip

The next figure shows the *terminal* app on the left and the *ecu* app on the right. When we press the button *Tx 10* in the *terminal*, the *terminal* sends 10 requests to read the values of the parameters with IDs from 1 to 10 and displays each request in its GUI. Let us look at the round trip for requesting the value of parameter 3.

- **Step 1**. The *terminal* sends the request for parameter 3 via CAN bus *can0* to the *ecu* and displays *Trm/Send: Read(0x0003, 0x00000000)* in its GUI. The value in the request is irrelevant and hence set to 0.
- **Step 2**. The *ecu* app receives the request and displays *Ecu/Send: Read(0x0003, 0x00000000)* in its GUI.
- **Step 3**. The *ecu* looks up the value of parameter 3 as 0x0306b1d3, sends the response via CAN bus *can0* to the *terminal* and displays *Ecu/Send: Read(0x0003, 0x0306b1d3)*.
- **Step 4**. The *terminal* receives the response and displays *Trm/Recv: Read(0x0003, 0x0306b1d3)*.

![](https://storage.ghost.io/c/c7/ee/c7ee1cf5-b86f-491b-aab7-20ad081598e1/content/images/2025/11/speaking-can-trm-ecu-1.png)

The terminal (left) sends 10 read-parameter requests. The ECU (right) fills in the parameter values and sends back a response.

### Step 1 - Trm/Send: Read(0x0003, 0x00000000)

Let us now look under the hood of the GUI. When we press the *Tx 10* button, the QML engine calls the button's event handler:

```
onReleased: gTerminal.simulateTxBufferOverflow(10)
```

The global property *gTerminal* is bound to the C++ object *TerminalModel*, which is created in the *main* function of *terminal*. The *onReleased* handler calls the function *TerminalModel::simulateTxBufferOverflow*.

```
void TerminalModel::simulateTxBufferOverflow(int count)
{
    for (quint16 i = 1; i <= count; ++i) {
        m_a2Proxy->sendReadParameter(i);
    }
}
```

The for-loop calls the function *EcuProxy::sendReadParameter* with the parameter IDs from 1 to 10, as *count* is 10.

```
void EcuProxy::sendReadParameter(quint16 pid, quint32 value)
{
    emitReadParameterMessage(QStringLiteral("Trm/Send"), 
                             pid, value);
    canBus()->writeFrame(
        QCanBusFrame(0x18ef0201U, encodeReadParameter(pid, value))
    );
}
```

The function *sendReadParameter* has two arguments *pid* (parameter ID) and *value*, where *value* is default-initialised to 0U. So, the call for parameter ID 3 becomes *sendReadParameter(3U, 0U)*, which explains the log message *Trm/Send: Read(0x0003, 0x00000000)*.

The function *QCanBusDevice::writeFrame* takes the *QCanBusFrame* created in place and writes it to CAN bus *can0*. A CAN frame consists of a frame ID (here: 0x18ef0201U) and a payload (here: *encodeReadParameter(pid, value)*).

The frame ID 0x18ef0201U conforms to the [SAE J1939 standard](https://en.wikipedia.org/wiki/SAE%5FJ1939?ref=smallstepsystems.com), which is widely used in agricultural, construction and commercial vehicles. It denotes a low-priority command request (0x18ef) sent from the device with CAN ID 0x01 (the *terminal*) to the device with the CAN ID 0x02 (the *ecu*). Although the frame ID seems to define a peer-to-peer message, all CAN devices will see the message. CAN messages are always broadcasted on the CAN bus.

The payload of the CAN frame is a byte array. The array size ranges from 0 to 8 bytes. The J1939 standard uses a fixed size of 8 bytes. The function *encodeReadParameter* serialises the ID 1 for the read-parameter command, the *pid* and the *value* of a parameter into an array of 8 bytes.

```
QByteArray EcuBase::encodeReadParameter(quint16 pid, quint32 value)
{
    QByteArray payload(8, 0x00);
    qToLittleEndian(quint8(1), payload.data());
    qToLittleEndian(pid, payload.data() + 1);
    qToLittleEndian(value, payload.data() + 3);
    return payload;
}
```

The function initialises *payload* with eight zeros. CAN expects all numbers to be transferred in little-endian format. We use the Qt function *qToLittleEndian* to convert the command ID 1, the *pid* and the *value* into little-endian format and to store them at position 0, at positions 1 and 2, and at positions 3 to 6, respectively, in the *payload* array.

The function *sendReadParameter* creates the CAN frame

18 ef 02 01 # 01 03 00 00 00 00 00 00

from the frame ID and the payload returned by *encodeReadParamter* and writes the frame on the CAN bus *can0*.

### Step 2 - Ecu/Recv: Read(0x0003, 0x00000000)

The *ecu* app runs on a different device, which is connected to the *terminal* device with a CAN cable. It listens to incoming CAN frames in classic Qt fashion with a signal-slot connection .

```
// EcuBase constructor in ecucan/ecubase.cpp
connect(m_canBus.get(), &QCanBusDevice::framesReceived,
        this, &EcuBase::onFramesReceived);

```

The slot *EcuBase::onFramesReceived* is called whenever a CAN frame arrives on the CAN bus.

```
void EcuBase::onFramesReceived()
{
    ...
    auto count = canBus()->framesAvailable();
    for (qint64 i = count; i > 0; --i) {
        auto frame = canBus()->readFrame();
        if (isReadParameter(frame)) {
            receiveReadParameter(frame);
        }
    }
}
```

The for-loop iterates over all CAN frames currently available. It reads a frame and removes the frame from its internal queue. If the frame is a read-parameter frame, it calls the function *Ecu::receiveReadParameter* to deserialise the CAN frame.

```
void Ecu::receiveReadParameter(const QCanBusFrame &frame)
{
    quint16 pid = 0U;
    quint32 value = 0U;
    std::tie(pid, value) = decodeReadParameter(frame);
    emitReadParameterMessage(QStringLiteral("Ecu/Recv"),
                             pid, value);
    sendReadParameter(pid, QRandomGenerator::global()->generate());
}
```

The function *decodeReadParameter* performs the inverse conversion of *encodeReadParameter* from Step 1\. It converts the payload bytes 1 and 2 from little-endian format into the integer *pid* and the bytes 3-6 into the integer *value*.

```
std::tuple<quint16, quint32> EcuBase::decodeReadParameter(const QCanBusFrame &frame)
{
    const auto &payload = frame.payload();
    quint16 pid = qFromLittleEndian<qint16>(payload.data() + 1);
    quint32 value = qFromLittleEndian<qint32>(payload.data() + 3);
    return std::make_tuple(pid, value);
}
```

The function *qFromLittleEndian* works only with *qint16*, *qint32* or *qint64*. We force the conversion to *quint16* for *pid* and *quint32* for *value* by declaring *pid* and *value* explicitly as *quint16* and *quint32* instead of using *auto*.

The function *decodeReadParameter* returns *pid* and *value* to the calling function *receiveReadParameter*. The latter function displays the log message *Ecu/Recv: Read(0x0003, 0x00000000)* and calls the function *Ecu::sendReadParameter* with the extracted *pid* and a random number as the *value*. A real ECU would read the value of the parameter from EEPROM or RAM.

### Step 3 - Ecu/Send: Read(0x0003, 0x0306b1d3)

The function *Ecu::sendReadParameter* sends the response to a read-parameter request back to the *terminal* device. It first displays the message *Ecu/Send: Read(0x0003, 0x0306b1d3)* in its log window. It then creates the response frame and writes the frame to the CAN bus *can0*.

```
void Ecu::sendReadParameter(quint16 pid, quint32 value)
{
    emitReadParameterMessage(QStringLiteral("Ecu/Send"), pid, value);
    canBus()->writeFrame(QCanBusFrame(0x18ef0102U, 
                         encodeReadParameter(pid, value)));
}
```

The frame ID is nearly the same as on the *terminal* side except that the target ID is now 0x01 for *terminal* and the source ID is now 0x02 for *ecu*. The function *encodeReadParameter* serialises *pid* and *value* into a byte array as described in Step 1 above. The *ecu* devices sends the response frame

18 ef 01 02 # 01 03 00 d3 b1 06 03 00

over CAN bus *can0* to the *terminal* device.

### Step 4: Trm/Recv: Read(0x0003, 0x0306b1d3)

When the *terminal* app receives the response frame from the *ecu* app, it calls the slot *EcuBase::onFramesReceived*. This is the same slot the *ecu* app calls in Step 2\. This time, however, the slot is called on an *EcuProxy* object instead of an *Ecu* object. Hence, the slot calls the function *EcuProxy::receiveReadParameter* to handle the response.

```
void EcuProxy::receiveReadParameter(const QCanBusFrame &frame)
{
    quint16 pid = 0U;
    quint32 value = 0U;
    std::tie(pid, value) = decodeReadParameter(frame);
    emitReadParameterMessage(QStringLiteral("Trm/Recv"), pid, value);
}
```

This function is nearly the same as the function *Ecu::receiveReadParameter* of Step 2 - except that it need not prepare a response. The round trip for reading a parameter value ends here with displaying the message *Trm/Recv: Read(0x0003, 0x0306b1d3)* in the log window of the *terminal*.

## Getting the Example Code

The example code is available on GitHub. You get it with the following commands.

```
$ git clone https://github.com/bstubert/embeddeduse.git
$ cd embeddeduse
$ git checkout can-comm-1
$ cd BlogPosts/CanComm
```

*CanComm* is a normal CMake-based Qt project.