2025-10-18
This tutorial will go through 📄can.h and
📄can.cc
line-by-line as an introduction to C++. We will cover concepts in:
Basic C++ syntax and structure
Object-oriented programming concepts
Libraries and the standard library
GNU+Linux and UNIX
You can find the files covered at https://github.com/illini-robomaster/irm_jetson/blob/main/src/include/board/can.h and the source file code at https://github.com/illini-robomaster/irm_jetson/blob/main/src/include/board/can.cc You can find the examples at https://github.com/illini-robomaster/irm_jetson/blob/main/src/examples/can_recieve.cc https://github.com/illini-robomaster/irm_jetson/blob/main/src/examples/can_send.cc https://github.com/illini-robomaster/irm_jetson/blob/main/src/examples/motor_m3508.cc
I will assume the most basic knowledge of programming. I will take time in the introduction to go over types and pointers. If you are familiar, please skip this subsection and proceed directly to . Do note that these two introductions are partially written by AI.
In C++, a type is a classification that specifies what kind of value a variable can hold and what operations can be performed on it. C++ has several fundamental types:
| Type | Description | Size (typical) |
|---|---|---|
bool |
Boolean value | 1 byte |
char |
Character | 1 byte |
int |
Integer | 4 bytes |
float |
Single-precision floating point | 4 bytes |
double |
Double-precision floating point | 8 bytes |
void |
No type | N/A |
C++ also allows for type modifiers like signed,
unsigned,
short,
and long
to modify the range of values a type can hold. Additionally, C++
supports compound types like arrays, pointers, and references. In our
code, we will use the more verbose types like uint8\_t
etc. This is common in embedded programming.
A pointer is a variable that stores the memory address of another variable. Think of it as a label that points to a location in memory where data is stored. Pointers are fundamental to C++ and enable features like dynamic memory allocation and efficient array handling.
| Operator | Symbol | Purpose |
|---|---|---|
| Address-of | \& |
Gets the memory address of a variable |
| Dereference | * |
Accesses the value at the memory address |
| Member access | -> |
Accesses members of an object through a pointer |
Here’s a simple example:
int x = 5; // Declare an integer variable
int *ptr; // Declare a pointer to an integer
ptr = &x; // Store the address of x in ptrAfter this code executes, ptr
contains the memory address of x.
To access the value stored at that address (i.e., the value of x),
you would dereference the pointer using *ptr,
which would give you 5.
| Concept | Syntax | Explanation |
|---|---|---|
| Pointer declaration | int *ptr |
Declares a pointer to an integer |
| Address assignment | ptr = \&x |
Assigns the address of x
to ptr |
| Dereferencing | *ptr = 10 |
Sets the value at the address stored in
ptr
to 10 |
| Pointer to pointer | int **ptr2 |
A pointer to a pointer to an integer |
Pointers are especially important in C++ for memory management, creating dynamic data structures, and interfacing with system-level code.
We will start by examining our header file 📄can.h.
In C++, we typically split our code into header files (.h)
and source files (.cc
or .cpp)
for two main reasons: Convenience: Header files act as
an interface for our code. They tell other programmers (and the
compiler) what functions and classes are available, what parameters they
take, and what they return. This makes it easier for others (humans or
your IDE) to work with your code. Compilation: When you
\#include
a file, you are essentially pasting its contents into place. You don’t
want the implementation there. The compilation process works in two main
stages:
Compile: Each .cc
file is compiled independently into an object file (.o),
checking what functions and classes exist/their definitions against the
header files (.h).
Preprocessor directives (code starting with \#)
are run before compilation.
Link: The linker combines all the object files together, resolving references between them to create the final executable.
/****************************************************************************
* *
* Copyright (C) 2025 RoboMaster. *
* Illini RoboMaster @ University of Illinois at Urbana-Champaign *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
****************************************************************************/
#include <atomic>
#include <iostream>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <map>
#include <net/if.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#pragma once/****************************************************************************
* *
* Copyright (C) 2025 RoboMaster. *
* Illini RoboMaster @ University of Illinois at Urbana-Champaign *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
****************************************************************************/The file begins with a large comment block that contains licensing information. This is standard practice.
In C++, comments can be written in two ways:
/* comment */
for multi-line comments
// comment
for single-line comments
This should be self-explanatory.
#include <atomic>
#include <iostream>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <map>
#include <net/if.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>The \#include
directive tells the compiler to include the contents of other files. You
may notice there are two different types:
\#include <filename>
for system headers (standard library and system libraries)
\#include "filename"
for “local” headers (i.e. our own files)
Notice how we include both standard C++ libraries like and , as well as Linux system headers for CAN communication like .
#pragma onceThe \#pragma once
directive is a header guard that tells the compiler to not
include the same file multiple times. This is important because
including the same file multiple times can lead to errors.
#define MAX_CAN_DEVICES 12
namespace CANRAW {Here we define a constant MAX\_CAN\_DEVICES
using the preprocessor directive \#define.
Constants defined this way in C++ are replaced by their values
before compilation. Here we meet a namespace.
Namespaces are used to group related code and prevent naming
conflicts. Everything within the CANRAW
namespace can be accessed using the scope resolution operator ::,
i.e. CANRAW::function\_name.
typedef void (*can_rx_callback_t)(const uint8_t data[], void *args);We meet typedef.
C++ is a typed language. If you do not know what a type or a
type signature is, please consult a search engine. A typedef
creates a new type from existing ones.
We typedef
a pointer to such a function, hence
void:
The function returns nothing.
(*can\_rx\_callback\_t):
This is the name of our alias, with the asterisk indicating it is a
pointer. We wrap it in parenthesis so it’s a pointer to a function
(void (*func))
and not a function that returns a void pointer ((void *)func).
(const uint8\_t data[], void *args):
These are the arguments the function takes. const uint8\_t data[]
is an array ([]
after a variable name means an array) of constant (const,
cannot be changed) unsigned 8-bit integers (uint8\_t,
\_t
indicates it is a type). The void pointer void *args
means that it will accept a pointer to args
of any type.
Therefore the type can\_rx\_callback\_t
indicates a pointer to a function which takes a constant array of bytes
and a pointer to anything, and returns nothing.
class CAN {
public:
CAN(const char *name = "can0");
~CAN();
/**
* @brief Transmits a CAN message
* @param can_id The CAN ID to transmit to
* @param dat Pointer to the data to transmit
* @param len Length of the data in bytes
*/
void Transmit(canid_t can_id, uint8_t *dat, int len);
/**
* @brief Receives a single CAN message
* @note This is a blocking call
*/
void Receive();
/**
* @brief Closes the CAN socket and cleans up resources
*/
void Close();
/**
* @brief Registers a callback for a specific CAN ID
* @param can_id The CAN ID to register for
* @param callback The callback function to invoke when message received
* @param args Optional arguments to pass to the callback
* @return 0 on success, -1 on failure
*/
int RegisterCanDevice(canid_t can_id, can_rx_callback_t callback,
void *args = nullptr);
/**
* @brief Deregisters a callback for a specific CAN ID
* @param can_id The CAN ID to deregister
* @return 0 on success, -1 if CAN ID not found
*/
int DeregisterCanDevice(canid_t can_id);
struct can_frame frx;
/**
* @brief Starts a thread to continuously receive CAN messages
* @param stop_flag Pointer to atomic bool to control thread execution
* @param interval_us Time between receive attempts in microseconds
*/
std::atomic<bool> *StartReceiveThread(int interval_us = 10);
std::atomic<bool> *stop_flag_;
private:
int s;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame ftx;
std::map<canid_t, std::pair<can_rx_callback_t, void *>> callback_map;
std::atomic<bool> *receive_thread_present_;
};This is the declaration of our main CAN
class. Let’s examine its components:
The class has two access specifiers, one on line 41 and the other on line 89:
public:
Members that can be accessed from outside the class
private:
Members that can only be accessed from within the class
CAN(const char *name = "can0");
~CAN();The first line is the constructor. This is a special
function with the same name as the class that gets called when we create
an object of this class. The name = "can0"
indicates the default value for name
is “can0”.
The second line is the destructor. This is called when the object is destroyed and is used for cleanup.
The class declares several member functions:
Transmit
- For sending CAN messages.
Receive
- For receiving CAN messages.
Close
- For closing the CAN connection.
RegisterCanDevice
- For registering callbacks.
DeregisterCanDevice
- For removing callbacks.
StartReceiveThread
- For starting a background thread.
Let us take a look at Transmit:
/**
* @brief Transmits a CAN message
* @param can_id The CAN ID to transmit to
* @param dat Pointer to the data to transmit
* @param len Length of the data in bytes
*/
void Transmit(canid_t can_id, uint8_t *dat, int len);This function returns nothing (void)
and takes three arguments.
canid:
canid\_t
comes from . It is just an alias for a 32-bit unsigned integer.
*dat:
A pointer to a byte array.
len:
The length of the byte array.
We will explain the member functions in further detail in .
The class has several member variables: Public variables:
struct can\_frame frx;,
on line 79: The definition of can\_frame
can be found in . The prefix struct
indicates that can\_frame
it is a struct. frx
is short for “frame receive”. We will be storing the CAN data we receive
into this structure.
std::atomic<bool> *stop\_flag\_;,
on line 87: A pointer to an
atomic boolean. You can think of an atomic variable as one that
is thread safe. This flag stops the receive thread when set to true. I
will elaborate on this in .
Private variables:
int s;,
on line 90: An integer to store the
socket file descriptor. I will elaborate on this in .
struct sockaddr\_can addr,
on line 91: sockaddr\_can
is a struct
defined in . It is used in socket configuration. I will elaborate on
this in .
struct ifreq ifr;,
on line 92: ifreq
is a struct
defined in . It is used in socket configuration. I will elaborate on
this in .
struct can\_frame ftx;,
on line 93: This shares the same type
as frx.
ftx
is short for “frame transmit”; we will be storing the data we want to
send out into this structure.
std::map<canid\_t, std::pair<can\_rx\_callback\_t, void *>> callback\_map;,
on line 94: Something to help us make
callbacks. I will elaborate on this in .
std::atomic<bool> *receive\_thread\_present\_,
on line 95: Another atomic boolean,
this one to make sure only one receive thread is running at a
time.
} // namespace CANRAWThis closes the namespace we opened earlier. Notice the comment. If
our file is littered with lots of \s,
it is good to include these comments to remind ourselves what ends
where. Here is a comment from me. If there is anything basic you do not
understand (or if you basically do not understand anything), take the
time to search it up or ask an LLM. However, there is no need to fully
understand the networking/operating system bits.
Now let us examine the implementation file can.cc
to see how the functions declared in the header are actually
implemented.
/****************************************************************************
* *
* Copyright (C) 2025 RoboMaster. *
* Illini RoboMaster @ University of Illinois at Urbana-Champaign *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
****************************************************************************/
#include "can.h"
#include "utils.h"
#include <chrono>
#include <cstring>
#include <thread>
namespace CANRAW {Notice how we include our own header file with quotes "can.h"
rather than angle brackets (remember why?). contains basic functionality
that I had to re-implement because of the outdated version of the the
board uses.
: Provides tools to work with time. We use it for sleeping.
: Provides tools to work with strings in memory. This is the C++ version of C’s library.
: Multithreading. You can think of this as running multiple pieces of code at once.
We then re-enter the CANRAW
namespace. If we did not, we would have to prefix everything we defined
in 📄can.h
with CANRAW::.
Exersise: why?
CAN::CAN(const char *name) {
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s < 1) {
std::cerr << "Error while opening socket" << std::endl;
}
strcpy(ifr.ifr_name, name);
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
std::cerr << "Error while binding address" << std::endl;
}
stop_flag_->store(false);
receive_thread_present_->store(false);
}CAN::CAN(const char *name) {The CAN::
before the constructor name indicates that this function belongs to the
CAN
class within the CANRAW
namespace. const char *name
takes a single argument .
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);This creates a socket. Let us look at the declaration of this function in :
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;Now we know know what this means:
PF\_CAN:
Protocol family for CAN.
SOCK\_RAW:
Raw socket type.
CAN\_RAW:
CAN raw protocol.
Remeber the type of s?
It was an int.
File descriptors, or fd for short, can be represented as integers.
if (s < 1) {We check if the socket was created successfully by checking if s < 1.
If there’s an error, we print a message to std::cerr
(standard error output). Do you know why? Hint: read the code block in
.
strcpy(ifr.ifr_name, name);This copies the interface name to the ifr
structure. This is a C-style string operation. strcpy
was provided by .
ioctl(s, SIOCGIFINDEX, &ifr);This is a system call. ioctl
is provided by . This performs the I/O control operation specified by
REQUEST on FD. One argument may follow; its presence and type depend on
REQUEST. (Taken directly from a comment in 📄/usr/include/sys/ioctl.h.)
s:
The file descriptor of our CAN socket.
SIOCGIFINDEX:
Retrieve the interface index of the interface into ifr\_ifindex.
(Taken directly from .)
\&ifr:
A reference to a struct ifreq
that will be populated with the interface information; specifically, the
SIOCGIFINDEX
request informs the kernel to fill in ifr.ifr\_ifindex
with the index of the network interface whose name is specified in ifr.ifr\_name.
In human language, we are telling our computer to find the interface
index of s
and stick it into the ifr\_ifindex
field of a ifreq
structure that we pass by reference.
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;After retrieving the interface index via ioctl,
we initialize the sockaddr\_can
structure addr,
which is used to bind the socket to a specific CAN interface.
addr.can\_family = AF\_CAN;:
Sets the address family to CAN. This tells the kernel we
are working with CAN sockets.
addr.can\_ifindex = ifr.ifr\_ifindex;:
Assigns the interface index we obtained earlier from the ifreq
structure. Populating a struct
with configuration data before passing it to a system call is a common
pattern in UNIX network programming. The bind
function (called next) uses this fully initialized addr
to associate our raw CAN socket with the desired hardware interface
(e.g., can0).
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
std::cerr << "Error while binding address" << std::endl;
}The bind
system call associates the socket file descriptor s
with a specific local address. In this case, the CAN interface we’ve
configured in the addr
structure. Here is the docstring above bind
in :
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
__THROW;s:
The socket file descriptor returned by socket().
(struct sockaddr *)\&addr:
A pointer to the sockaddr\_can
structure we initialized earlier. We cast it to sockaddr*
because bind
is a generic function that works with any address family. It doesn’t
know about sockaddr\_can
specifically, so we must cast to its parent type.
sizeof(addr):
The size of the address structure, so the kernel knows how much memory
to read from the pointer.
If bind
fails (returns < 0),
we print an error to std::cerr,
indicating the socket could not be bound to the specified interface.
After binding, our socket is now ready to send and receive CAN frames on the specified interface.
stop_flag_->store(false);
receive_thread_present_->store(false);We use ->store
to set the atomic boolean values. Here we initialize the stop flag and
the receive flag to false.
CAN::~CAN() { this->Close(); }The destructor just calls the Close()
method. The this->
syntax explicitly refers to the current object.
void CAN::Transmit(canid_t can_id, uint8_t *dat, int len) {
ftx.can_id = can_id;
ftx.can_dlc = clip(len, 0, CAN_MAX_DLEN);
memcpy(ftx.data, dat, sizeof(uint8_t) * ftx.can_dlc);
if (write(s, &ftx, sizeof(struct can_frame)) != sizeof(struct can_frame)) {
std::cerr << "Error while sending CAN frame" << std::endl;
}
}Let us go through Transmit.
ftx.can_id = can_id;The first step in transmitting a CAN message is assigning the target
can\_id
to the can\_id
field of the ftx.
This field identifies which device or functional unit on the CAN bus
should receive the message.
ftx.can_dlc = clip(len, 0, CAN_MAX_DLEN);Here we assign the Data Length Code (can\_dlc)
using a helper function clip.
Since CAN frames are limited to 8 bytes
of data (defined by CAN\_MAX\_DLEN),
this function ensures len
is clamped within valid bounds to prevent buffer overruns and malformed
frames. We use clip
from our own because we are on a very old version of where std::clamp
is not available.
template <typename T> T clip(T value, T min, T max) {
return value < min ? min : (value > max ? max : value);
}Read through and see if you can understand.
/**
* struct can_frame - Classical CAN frame structure (aka CAN 2.0B)
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
* @len: CAN frame payload length in byte (0 .. 8)
* @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8)
* @__pad: padding
* @__res0: reserved / padding
* @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length
* len8_dlc contains values from 9 .. 15 when the payload length is
* 8 bytes but the DLC value (see ISO 11898-1) is greater then 8.
* CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver.
* @data: CAN frame payload (up to 8 byte)
*/
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
union {
/* CAN frame payload length in byte (0 .. CAN_MAX_DLEN)
* was previously named can_dlc so we need to carry that
* name for legacy support
*/
__u8 len;
__u8 can_dlc; /* deprecated */
} __attribute__((packed)); /* disable padding added in some ABIs */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
}; memcpy(ftx.data, dat, sizeof(uint8_t) * ftx.can_dlc);We copy the user-provided data buffer dat
into the data
array of the can\_frame
structure. memcpy
(from ) performs a low-level byte-by-byte copy which is efficient and
safe for fixed-size buffers. Note that we use ftx.can\_dlc
(not len)
to determine how many bytes to copy, ensuring we never exceed the legal
payload size even if the caller passed a longer len.
if (write(s, &ftx, sizeof(struct can_frame)) != sizeof(struct can_frame)) {
std::cerr << "Error while sending CAN frame" << std::endl;
}Finally we send the fully prepared can\_frame
through the socket file descriptor s
using the POSIX write
system call. The kernel interprets this as a request to transmit a raw
CAN message over the bound interface. write
returns the amount of bytes it wrote. If this is less than expected (or
−1), it indicates an error.
void CAN::Receive() {
if (read(s, &frx, sizeof(struct can_frame)) != sizeof(struct can_frame)) {
std::cerr << "Error while receiving CAN frame" << std::endl;
return;
}
auto it = callback_map.find(frx.can_id);
if (it != callback_map.end()) {
it->second.first(frx.data, it->second.second);
}
}This method receives and distributes CAN frames.
if (read(s, &frx, sizeof(struct can_frame)) != sizeof(struct can_frame)) {This reads a CAN frame from the socket. Note how this is like reading from a file. Again, these are the same. Can you guess what this does based on ?
auto it = callback_map.find(frx.can_id);We use the auto
keyword to automatically deduce the type of the iterator. This is a
modern C++ feature that makes code more readable. callback\_map
is a map. I will elaborate on it here and the next few subsubsections.
Recall its signature and the signature of can\_rx\_callback\_t:
std::map<canid\_t, std::pair<can\_rx\_callback\_t, void *>> callback\_map;
typedef void (*can\_rx\_callback\_t)(const uint8\_t data[], void *args);
Using auto
for the iterator type avoids having to write out the full type:
std::map<canid_t, std::pair<can_rx_callback_t, void *>>::iterator it =
callback_map.find(frx.can_id);which is a mess.
We associate a CAN identifier (canid\_t)
with a callback function and an optional context pointer (void *).
When a CAN message is received, the system looks up the corresponding
can\_id
in callback\_map
with the find find
method for maps (which returns an iterator).
We check if the iterator is valid (it != callback\_map.end())
and then call the callback function with it->second.first(frx.data, it->second.second).
Recall that callback\_map
is a std::map
with the following type signature:
std::map<canid_t, std::pair<can_rx_callback_t, void *>> callback_map;This means each element in the map is a key-value pair where:
The key is of type canid\_t
(the CAN identifier).
The value is of type std::pair<can\_rx\_callback\_t, void *>,
which itself contains two elements:
first:
A function pointer of type can\_rx\_callback\_t.
This is the callback function to invoke.
second:
A void *.
This is an optional user-provided argument (context) to pass to the
callback.
When we call callback\_map.find(frx.can\_id),
we get an iterator it
pointing to the found entry (or callback\_map.end()
if not found). Assuming the lookup succeeds (it != callback\_map.end()),
then:
it->second
accesses std::pair<can\_rx\_callback\_t, void *>,
the value part of the kv pair.
it->second.first
accesses the first element of that pair, the pointer to the function to
be called.
it->second.second
accesses the second element of that pair, the pointer to the context
(void *)
to be passed to the callback.
Therefore, the full expression:
it->second.first(frx.data, it->second.second);In English, this means: “If we have a callback registered for this CAN ID, call that function now and give it the received data along with any user-specified context.” This mechanism allows different parts of the system to register interest in specific CAN IDs and respond automatically when messages arrive.
int CAN::RegisterCanDevice(canid_t can_id, can_rx_callback_t callback,
void *args) {
if (callback_map.size() >= MAX_CAN_DEVICES) {
std::cerr << "Maximum number of CAN devices reached" << std::endl;
return -1;
}
callback_map[can_id] = std::make_pair(callback, args);
return 0;
}
int CAN::DeregisterCanDevice(canid_t can_id) {
auto it = callback_map.find(can_id);
if (it != callback_map.end()) {
callback_map.erase(it);
return 0;
}
std::cerr << "Can ID " << can_id << " not registered" << std::endl;
return -1;
}These methods are for managing the callback map.
if (callback_map.size() >= MAX_CAN_DEVICES) {
std::cerr << "Maximum number of CAN devices reached" << std::endl;
return -1;
}We check if we’ve reached the maximum number of devices before adding a new one.
callback_map[can_id] = std::make_pair(callback, args);This creates a pair of values to store in our map.
auto it = callback_map.find(can_id);
if (it != callback_map.end()) {
callback_map.erase(it);
return 0;
}This attempts to find and remove an entry from the map.
std::atomic<bool> *CAN::StartReceiveThread(int interval_us) {
if (receive_thread_present_->load()) {
std::cerr << "Error: Receive thread already running" << std::endl;
return nullptr;
}
stop_flag_->store(false);
receive_thread_present_->store(true);
std::thread([this, interval_us]() {
while (!stop_flag_->load()) {
this->Receive();
std::this_thread::sleep_for(std::chrono::microseconds(interval_us));
}
receive_thread_present_->store(false);
}).detach();
return stop_flag_;
}This method starts a background thread for receiving CAN messages:
std::thread([this, interval_us]() The [this, interval\_us]() \{ ... \
syntax creates a lambda function (anonymous function). The variables in
the square brackets (this
and interval\_us)
mean that those variables are available inside the scope of the lambda
function.
std::thread([this, interval_us]() {
while (!stop_flag_->load()) {
this->Receive();
std::this_thread::sleep_for(std::chrono::microseconds(interval_us));
}
receive_thread_present_->store(false);
}).detach();We first call the Receive
function, then we call sleep\_for.
This pauses the thread for a specified amount of time. Notice our while
loop checks for !stop\_flag\_->load().
When the stop flag is set, the thread stops.
.detach()
detaches the thread so it runs independently.
void CAN::Close() {
// Signal receive thread to stop
stop_flag_->store(true);
receive_thread_present_->store(false);
// Close socket
close(s);
}This method cleans up resources by stopping the receive thread and closing the socket.
Let’s look at how to use the CAN class in practice by examining the example files.
#include "board/can.h"
#include "stdint.h"
#include "stdio.h"
#include <unistd.h>
void receive(CANRAW::CAN *can0) {
// CANRAW::CAN *can0 = new CANRAW::CAN("can0");
// read(, &can0->frx, sizeof(struct can_frame));
can0->Receive();
for (int i = 0; i < 8; i++) {
printf("%02x ", (int)can0->frx.data[i]);
}
printf("\n");
}
int main() {
CANRAW::CAN *can0 = new CANRAW::CAN("can0");
printf("Start can receive test\n");
while (true) {
receive(can0);
}
return 0;
}This is 📄src/examples/can\_receive.cc.
CANRAW::CAN *can0 = new CANRAW::CAN("can0");This creates a new CAN object on the heap using the new
operator.
can0->Receive();This calls the Receive
method using the arrow operator (->)
to access methods on a pointer.
while (true) {This creates an infinite loop. We do this when there is something that must be run continuously.
#include "board/can.h"
void sendtest() {
int i;
int len = 8;
uint8_t dat[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00};
CANRAW::CAN *can0 = new CANRAW::CAN("can0");
for (i = 0; i < 10; i++) {
can0->Transmit(0x200, dat, len);
}
can0->Close();
}
int main() {
std::cout << "Start can send test" << std::endl;
sendtest();
std::cout << "End can send test" << std::endl;
return 0;
}This is 📄src/examples/can\_send.cc.
Try to figure this one out for yourself.
#include "board/can.h"
#include "motor/motor.h"
#include <unistd.h>
int main() {
CANRAW::CAN *can = new CANRAW::CAN("can0");
control::MotorCANBase *motor = new control::Motor3508(can, 0x207);
control::MotorCANBase *motors[] = {motor};
std::atomic<bool> *can_stop = can->StartReceiveThread();
if (can_stop == nullptr) {
std::cerr << "Error: Could not start CAN receive thread" << std::endl;
return 1;
}
while (true) {
motor->SetOutput(400);
control::MotorCANBase::TransmitOutput(motors, 1);
motor->PrintData();
usleep(100);
}
return 0;
}This is 📄src/examples/motor\_m3508.cc.
Take note of lines 26, 27 and 30.
Can you figure out what they do?
We breifly went over the following:
Comments (/* */
and //)
Include statements (\#include)
Preprocessor directives (\#define,
\#pragma once)
Header and implementation files
Classes and objects
Access specifiers (public,
private)
Constructors and destructors
Member functions and variables
Namespaces
Pointers and the *
and ->
operators
Destructors
Containers (std::map)
Utility classes (std::pair)
Thread support (std::thread,
std::atomic)
Time functions (std::chrono)
Linux socket API
System calls (socket,
bind,
ioctl)
File descriptor operations (read,
write,
close)
The auto
keyword
Lambda functions
This is the first tutorial, going over C++ and the Linux usage of CAN. There will be a shorter second part, introducing the actual structure of a CAN packet and how we structure data, i.e. communicating with multiple devices with a single packet. If there are any concepts I have failed to cover, or you do not completely understand, search it up and ask an LLM.