Introduction to C++ with Linux CAN

Austin Yang Illini RoboMaster
[[email protected], https://www.illinirobomaster.com]

2025-10-18

Read the pdf version here.

Introduction

Plan

This tutorial will go through 📄can.h and 📄can.cc line-by-line as an introduction to C++. We will cover concepts in:

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

Basic Knowledge

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.

Types

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:

Common C++ 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.

Pointers

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.

Pointer Operators in C++
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 ptr

After 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.

Pointer Syntax and Usage
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.

Understanding our CAN Header File

We will start by examining our header file 📄can.h.

Why header and source files?

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:

  1. 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.

  2. Link: The linker combines all the object files together, resolving references between them to create the final executable.

Include Guards and Headers

/**************************************************************************** 
 *                                                                          * 
 *  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

Comments

/**************************************************************************** 
 *                                                                          * 
 *  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:

This should be self-explanatory.

Include Statements

#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:

Notice how we include both standard C++ libraries like and , as well as Linux system headers for CAN communication like .

Pragma Once

#pragma once

The \#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.

Constants and Namespaces

#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 and Function Pointers


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

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 Declaration

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:

Access Specifiers

The class has two access specifiers, one on line 41 and the other on line 89:

Constructors and Destructors

  CAN(const char *name = "can0");
  ~CAN();

Member Functions

The class declares several member functions:

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.

We will explain the member functions in further detail in .

Member Variables

The class has several member variables: Public variables:

Private variables:

Namespace Closing and a Note


} // namespace CANRAW

This 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.

Understanding our CAN Source File

Now let us examine the implementation file can.cc to see how the functions declared in the header are actually implemented.

Include Statements and Namespace

/**************************************************************************** 
 *                                                                          * 
 *  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.

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?

Constructor Implementation

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);
}

Scope Resolution

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 .

Socket Creation

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:

Remeber the type of s? It was an int. File descriptors, or fd for short, can be represented as integers.

Error Handling

  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 .

String Operations

  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 .

System Calls

  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.)

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.

Struct Initialization

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.

Binding

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;

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.

Atomic Operations

  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.

Destructor Implementation

CAN::~CAN() { this->Close(); }

The destructor just calls the Close() method. The this-> syntax explicitly refers to the current object.

Transmit Method

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.

Struct Field Assignment

  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.

DLC and Utility Function

  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)));
};

Memory Copying

  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.

Writing to Socket

  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.

Receive Method

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.

Reading from Socket

  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 Keyword

  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.

Map Lookup

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).

Iterator Usage

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).

Callback Function

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:

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:

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.

Device Registration Methods

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.

Size Checking

  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.

Pair Creation

  callback_map[can_id] = std::make_pair(callback, args);

This creates a pair of values to store in our map.

Map Erasure

  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.

Thread Management

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:

Lambda Functions

  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.

Thread

  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.

Thread Detachment

.detach() detaches the thread so it runs independently.

Close Method

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.

Using the CAN Class: Examples

Let’s look at how to use the CAN class in practice by examining the example files.

CAN Receive Example

#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.

Object Creation

  CANRAW::CAN *can0 = new CANRAW::CAN("can0");

This creates a new CAN object on the heap using the new operator.

Method Calls

  can0->Receive();

This calls the Receive method using the arrow operator (->) to access methods on a pointer.

Infinite Loop

  while (true) {

This creates an infinite loop. We do this when there is something that must be run continuously.

CAN Send Example

#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.

Receive Thread Example

#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?

Concepts Covered

We breifly went over the following:

Basic Syntax

Object-Oriented Programming

Memory Management

Standard Library

System Programming

Modern C++ Features

Conclusion

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.