Using CAN within HYPED 2024 code - Hyp-ed/hyped-2024 GitHub Wiki

Overview

There are 2 parts to CAN message handling: sending and receiving. This will be written from the perspective of creating a class for something like a sensor that communicates over CAN.

No matter what we are doing, we should #include can.hpp. This includes the ICan and ICanProcessor interfaces, as well as defining various structs and constants from the Linux can header that are necessary for building on non-Linux systems

Sending

This is accomplished through using a std::shared_ptr<ICan> instance. This is generally passed in to the constructor of the class as we can only have one can instance running on a given bus. This is then stored in a class data member can_.

To send a message we must create an io::CanFrame struct. We will call this object frame, however name it appropriately for your implementation.

We then set frame.can_id to the id of the message we want to send (if we are sending a CAN extended ID then or the desired id with CAN_EFF_FLAG and set frame.can_id to the result).

frame.can_dlc should be set to the number of bytes we are sending. This must be less than or equal to the maximum can frame size of 8 bytes (defined in CAN_MAX_DLEN).

frame.can_data is an array of 8 std::uint8_ts, set each byte in the array as required for your message. The contents of your message will be defined in the manual or datasheet for your CAN compatible sensor/BMS/motor controller etc. If we are sending a message which contains bytes that are set to 0, remember that these values must also be set to 0 as they are not necessarily 0 initialised.

Now that we have a fully formed CAN frame, send it using can_.send(frame);. send() will return a core::Result value, kSuccess if sending succeeds and kFailure if sending fails.

Receiving

As CAN has arbitration, messages with lower IDs get priority for transmission. This means that we cannot guarantee, for example, if we send a message that asks a sensor to respond, that the next message received is the reply to that request. Because of this, CAN frame receiving is not handled on a per class basis. Instead, can_.receive() will be called in one location, and receive() will then call relevant std::shared_ptr<ICanProcessor>s.

When we want our class to receive a message, we must first implement ICanProcessor. We have 2 options here:

  1. For a small sensor which has little logic needed to deal with a received message, the sensor class can implement the interface
  2. For more complex objects it may be preferable to use a ObjectCanProcessor class as well as an Object class.

In our case, we have a simple sensor so we will use option 1, however the process will be similar either way.

When we construct our sensor, we should call can_.addProcessor(id, processor) for each ID we want to receive messages containing that ID.

For option 1, we will use a static create() function, where once our shared_ptr to our class is created we then pass it in. For option 2, on construction of our Object class we pass in or instantiate our ObjectCanProcessor and pass that as the processor.

This means that every time a message is received with ID, the can implementation will call ObjectCanProcessor->processMessage(frame). This should then handle the message as appropriate.

processMessage returns a core::Result value representing successful or failed message processing.

If you are receiving a value that is needed elsewhere in the code, the best way to handle this is that when processMessage gets a frame containing a value, the value is then stored in a data member which has a getter.

Example implementation

sensor.hpp

#pragma once

#include <core/logger.hpp>
#include <core/types.hpp>
#include <io/can.hpp>

namespace hyped::somewhere {

class Sensor: public io::ICanProcessor{
 public:
  static std::optional<std::shared_ptr<Sensor>> create(core::ILogger &logger, const std::shared_ptr<io::Can> can);
  Sensor(core::ILogger &logger, const std::shared_ptr<io::Can> can)
  core::Result configureSensor();
  core::Result processMessage(const io::CanFrame &frame);
  std::uint8_t getValue();

 private:
  std::shared_ptr<io::Can> can_;
  std::uint8_t value_;
}

}  // namespace hyped::somewhere

sensor.cpp

#include "sensor.hpp"

namespace hyped::somewhere {

Sensor::create(core::ILogger &logger, const std::shared_ptr<io::Can> can)
{
  std::optional<std::shared_ptr<Sensor>> optional_sensor = std::make_shared<Sensor>(logger, can);
  if (!optional_sensor) {
    logger.log(core::LogLevel::kFatal, "Failed to create sensor");
    return std::nullopt;
  }
  std::shared_ptr<Sensor> sensor = *optional_sensor;
  can.addProcessor(1, sensor);
  return sensor;
}

Sensor::Sensor(core::ILogger &logger, const std::shared_ptr<io::Can> can)
    : logger_(logger),
      can_(can)
{
}

Sensor::configureSensor()
{
  io::CanFrame frame;
  frame.can_id = 0x123;
  frame.can_dlc = 8;
  frame.data[0] = 12;
  frame.data[1] = 34;
  frame.data[2] = 56;
  frame.data[3] = 78;
  frame.data[4] = 0;
  frame.data[5] = 0;
  frame.data[6] = 0;
  frame.data[7] = 0;
  core::Result result = can_.send(frame);
  if (result == core::Result::kFailure) {
    logger.log(core::LogLevel::kFatal, "Failed to configure sensor");
  }
  return result;
}
Sensor::processMessage(const io::CanFrame &frame)
{
  value_ = frame.data[0];
  return core::Result::kSuccess;
}

Sensor::getValue()
{
  return value_;
}

}  // namespace hyped::somewhere

For an example of a separate processor and sensor look at how can_processor and controller are implemented.

⚠️ **GitHub.com Fallback** ⚠️