Basic Hi-CAN Usage¶
In this tutorial, you will learn the basics of transmitting and receiving data with Hi-CAN.
Requirements¶
Before starting this tutorial, follow the Setup here.
Receiving Data¶
Note
The program written in this section of the tutorial can be run with:
nix run .#pkgs.examples.hi-can.basic-rx
If you’ve read through the main CAN document, you’ll know that we need a CanInterface instance to work with the bus and receive a CAN frame, represented with a Packet containing the actual data.
Since these examples are all intended to run on Linux, we need to use the hi-can-raw implementation to get our CAN interface:
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
using namespace hi_can;
using std::cout, std::cin, std::endl;
int main()
{
std::string interface_id = "vcan0";
RawCanInterface can_interface(interface_id);
cout << "Opened CAN interface: " << interface_id << endl;
return 0;
}
If the bus in the interface_id exists, great!
You just created your first CAN interface, and the program exited after printing out the message.
However, if it doesn’t, you might get an error that looks something like this:
terminate called after throwing an instance of 'std::runtime_error'
what(): Failed to get interface index for "can0": No such device
[1] 39092 abort ./hi_can_basic_error
This is because our program currently has no error handling, and therefore crashed trying to connect to the bus.
Following the software standards, hi-can uses exceptions for (unusual) error handling!
Either the calls succeed, they are expected to fail and the return type reflects that, or they fail and throw an exception.
Therefore, we need to wrap code that can potentially fail in a try/catch block:
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
using namespace hi_can;
using std::cout, std::cin, std::endl;
int main()
{
std::string interface_id = "vcan0";
try
{
RawCanInterface can_interface(interface_id);
cout << "Opened CAN interface: " << interface_id << endl;
}
catch (const std::exception& e)
{
cout << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
This time, the error is caught before it can crash the program, and it cleanly prints out the error message:
Error: Failed to get interface index for "can0": No such device
To make our lives easier, let’s also make the program use the CAN bus name we give it as an argument, if provided:
int main(int argc, const char** argv)
{
std::string interface_id = "vcan0";
if (argc > 1)
interface_id = argv[1];
// ...
However, just having a connection to the bus is no use if we don’t use it!
Error
If you’re getting an that looks like:
Error: Failed to create CAN socket: Address family not supported by protocol
This usually means the CAN bus you’re trying to open does not exist.
In these tutorials, that probably means you’re trying to use vcan0 but haven’t run the setup script as described here.
Now we need to actually receive data. First, a function to process received frames:
Which, in this case, just prints out all the information about the frame. Next, to actually receive data.
The CanInterface::receive() function has two modes, blocking, and non-blocking.
In blocking mode, it will wait indefinitely for a frame to arrive, and immediately return a frame once it’s received.
If a frame has already arrived and is buffered internally, it won’t block at all, since it can just return that frame.
Alternatively, in non-blocking mode, it will return a buffered frame if possible, or std::nullopt otherwise.
In this case, we will use it in blocking mode, so we can guarantee that it returns a frame.
// ...
void rx_callback(const Packet&);
int main(int argc, const char** argv)
{
// ...
cout << "Waiting to receive frame..." << endl;
const auto frame = can_interface.receive(true);
rx_callback(*frame);
// ...
}
Tip
To send data on the bus, use cansend(1).
For example, on vcan0: cansend vcan0 12345678#1122334455667788
This is the most basic way to receive data from the bus.
However, it is also the worst.
The receive() function only handles a single frame at a time, so if you want to receive more than one you need to call it multiple times.
Additionally, you need to deal with error handling if you’re using it in non-blocking mode and it doesn’t receive a frame.
The preferred way to receive data is instead to set the interface’s receive callback.
This is a function that gets called every time the interface processes an incoming frame:
// ...
can_interface.set_receive_callback(print_frame_data);
cout << "Waiting to receive frame..." << endl;
can_interface.receive_all(true);
// ...
Using this then allows us to use the CanInterface::receive_all() function instead of receive().
receive_all() can’t return frames, so you must set a receive callback to use it, but it is much more convenient for actual use, since if there are multiple buffered frames, it will process all of them at once.
Putting it all together:
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
using namespace hi_can;
using std::cout, std::cin, std::endl;
void print_frame_data(const Packet&);
int main(int argc, const char** argv)
{
std::string interface_id = "vcan0";
if (argc > 1)
interface_id = argv[1];
try
{
RawCanInterface can_interface(interface_id);
cout << "Opened CAN interface: " << interface_id << endl;
can_interface.set_receive_callback(print_frame_data);
cout << "Waiting to receive frame..." << endl;
can_interface.receive_all(true);
}
catch (const std::exception& e)
{
cout << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
void print_frame_data(const Packet& frame)
{
// pull out the data we want
const auto& address = frame.get_address();
const auto& data = frame.get_data();
// print out the address info
cout << std::format("Address: {:#10x}\tExtended: {}\tRTR: {}\tError: {}\t",
address.address,
address.is_extended,
address.is_rtr,
address.is_error)
<< endl;
// and data
cout << "Data length: " << data.size() << " bytes" << endl;
if (data.size() > 0 && !address.is_rtr)
{
cout << "Data: ";
for (const auto byte : data)
cout << std::format("{:#04x} ", byte);
cout << endl;
}
}
This program will open vcan0 by default, or any bus you specify, wait to receive a CAN frame, print out the frame, and then exit.
Tip
To receive from any bus, use any instead of a bus name:
nix run .#pkgs.examples.hi-can.basic-rx -- any
If you do this, you will not be able to transmit frames, however.
Transmitting Data¶
Note
The program written in this section of the tutorial can be run with:
nix run .#pkgs.examples.hi-can.basic-tx
Like before, we will need a valid CanInterface instance to transmit data, but this time we will also need to create a Packet containing the data to actually transmit.
However, setting up the interface is exactly the same as before:
#include <hi_can_raw.hpp>
#include <iostream>
#include <string>
#include <vector>
using namespace hi_can;
using std::cout, std::cin, std::endl;
int main(int argc, const char** argv)
{
std::string interface_id = "vcan0";
if (argc > 1)
interface_id = argv[1];
try
{
RawCanInterface can_interface(interface_id);
cout << "Opened CAN interface: " << interface_id << endl;
addressing::standard_address_t txAddress(0x1F, 0, 0, 0, 0);
const std::vector<uint8_t> txData{1, 2, 3, 4, 5, 6, 7, 8};
Packet txPacket(addressing::flagged_address_t(txAddress), txData);
cout << "Transmitting frame with ID "
<< std::format("{:#04x}",
static_cast<addressing::raw_address_t>(txAddress))
<< endl;
can_interface.transmit(txPacket);
}
catch (const std::exception& e)
{
cout << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
To build a Packet, we first need an address for it.
In this case, we’re using the addressing::standard_address_t, which constructs the frame ID from system, subsystem, device, group, and parameter IDs.
Next is data—although it is optional.
Since most of the time data needs to be serialised, returning a std::vector<uint8_t> data buffer, we will directly create a buffer too.
However, the Packet needs a addressing::flagged_address_t, not just an ordinary address.
Fortunately, it can be directly constructed from the standard address.
Tip
To view CAN bus data, use candump(1).
You probably want to receive from any bus, so run: candump any.
That’s all there is to it!
Actually sending data is fairly simple—the trick is implementing the whole Hi-CAN standard, particularly address construction and interval transmission.
Fortunately, that’s what the PacketManager class is for, and the next tutorial teaches you how to use it!
Completed Code¶
All source code examples can be found under /software/native/examples.