TG
Back to projects
Library v1.0.0 Open SourcePersonal Project CANLibrarySignals

CANMessageSignal

An Arduino library I created to remove the pain of manually handling bits, endianness and CAN Bus frames.

CANMessageSignal is an Arduino library I created to take away the pain of manually managing bits, bytes, endianness, scaling and offsets in application code.

The goal is simple: define the CAN messages and signals once, then write firmware using real engineering values instead of constantly packing and unpacking raw CAN frames. That means the application code can work with values such as RPM, vehicle speed, coolant temperature, warning lamps or steering angle while the library takes care of where those values belong in the CAN payload.

Why I Built It

When you are working with a couple of signals, managing byte manipulation and CAN frames is fine. But once the project grows and you are dealing with dozens of signals and messages, that approach becomes fragile very quickly.

Once a project grows into a CAN translator, gateway, emulator or test tool, manually managing every byte, bit position, scale factor and offset quickly becomes repetitive and difficult to maintain.

Endianness is another thing that quickly becomes painful. Some vehicles use big-endian, others use little-endian, and different MCU architectures can have different native byte ordering. What started as a simple CAN message soon turns into code that’s manually packing bytes and shifting bits all over the application. We’ve all been there when something starts simple, but then we end up writing code like this in multiple places:

data.byte[0] = (RPM >> 8) & 0xFF; // Extract the most significant byte (upper 8 bits)
data.byte[1] = RPM & 0xFF; // Extract the least significant byte (lower 8 bits)

celLight ? (WarningLights |= (1 << 1)) : (WarningLights &= ~(1 << 1));
CoolantLight ? (WarningLights |= (1 << 4)) : (WarningLights &= ~(1 << 4));
BatteryLight ? (WarningLights |= (1 << 6)) : (WarningLights &= ~(1 << 6));
OilLight ? (WarningLights |= (1 << 7)) : (WarningLights &= ~(1 << 7));

I wanted the application code to say what it means. Set rpm to 3500, set speedo to 70, turn celOn on, set the oil pressure state to Ok, then let the library take care of how those values map into the CAN payload.

That is especially important for MicroCAN-FD as it has to be super easy to use.

The library takes care of packing those values into the correct bits and bytes, applying scaling and offsets, handling endianness and generating the finished CAN frame. So in application code, all we have to do is:

rpm.setSignalValue(3500);
speed.setSignalValue(70);
oilGauge.setSignalValue("Ok");
checkEngineLamp.setSignalValue(true);

That keeps the application code much easier to read, easier to debug and much easier to reuse across multiple projects.

What it supports

  • Classic CAN 2.0 and CAN FD message definitions.
  • Standard and extended identifiers.
  • Defining each signal once, DBC-style including start bit, length, endianness, scaling, offset, units, minimum, maximum and default values.
  • Message and Signal metadata such as name, default fill value, comments and description.
  • Classic CAN and CAN FD frame handling, including CAN FD DLC codes where DLC 9-15 map to 12, 16, 20, 24, 32, 48 and 64 byte payloads.
  • Little-endian and big-endian signal packing using the normal DBC bit numbering conventions.
  • Byte-aligned signal helpers for values that naturally fill byte boundaries.
  • Single-bit helpers using either an absolute bit index or a byte plus bit position.
  • Active-low or inverted bit signals, which are common for warning lamps and status flags.
  • Enum maps, so firmware can set a signal using a label rather than a raw number.
  • Constant fields for fixed, reserved or required values, these cannot be overwritten after they’ve been set, important for critical data.
  • Automatic validation of overlapping signals, duplicate names and invalid definitions.
  • Receive-side decoding into signal objects, including decoded physical value, raw value, enum label and last update time.
  • Single shot and periodic transmission helpers.
  • Optional signal override values, which are useful for overriding, testing and debugging over USB/serial without losing the sketch’s normal default value.

Typical Sketch Shape

Defining messages

Messages and signals are defined once and then reused throughout the application.

A typical message definition looks like this:

CanMessage engine201({
	.name = "Engine201",
	.idType = STANDARD,
	.id = 0x201,
	.dlc = 8,
	.defaultFill = 0xFF
});

This is very similar to what you’d see in a DBC file.

A signal is fully defined like this:

CanSignal batteryLight({
	.name = "BatteryLight",
	.dataType = UNSIGNED,
	.startBit = 54,  //byte6 bit 6
	.bitLength = 1,
	.endianness = BIG_ENDIAN,
	.factor = 1,
	.offset = 0,
	.unit = "NA",
	.comment = "Red Battery Light",
	.min = 0,
	.max = 1,
	.defaultValue = 0,
	.signalRole = NORMAL_SIGNAL,
	.multiplexor = nullptr,
	.multiplexValue = 0,
	.enumMap = nullptr,
	.overrideCapable = true });

Or, if your signals naturally sit on full byte boundaries:

CanByteSignal rpm({
	.name = "EngineRPM",
	.dataType = UNSIGNED,
	.startByte = 0,
	.byteLength = 2,
	.endianness = BIG_ENDIAN,
	.factor = 0.26,
	.offset = 0.0,
	.unit = "rpm",
	.comment = "Engine speed",
	.min = 0,
	.max = 10000,
	.defaultValue = 0,
	.overrideCapable = true
});

CanByteBitSignal celOn({
	.name = "CheckEngineLight",
	.byte = 5,  //byte5 bit 6
	.bit = 6
});

CanBitSignal celFlashing({
	.name = "FlashingEngineLight",
	.bit = 47
});

You can even work with enums so you can work with text values rather than remembering bit fields when writing the main code:

EnumMap oilMap({
  { 0x00, "Low" },
  { 0x01, "Ok" },
  { 0x02, "Fault" },
});


CanSignal oilGauge({
.name = "OilGauge",
	.dataType = UNSIGNED,
	.startBit = 33, //byte 4
	.bitLength = 2,
	.endianness = BIG_ENDIAN,
	...
	.enumMap = &oilMap });

You then can add your signals to your messages:

engine201.addSignal(rpm);

And the library then handles and validates everything such as duplicate signals, overlapping bytes, endianness, etc.

Those message and signal definitions can be kept in a separate header file so they can be shared between different sketches, or just kept out of the main sketch. Once defined, the application code is as simple as things like this:

rpm.setSignalValue(3500);
speed.setSignalValue(70);

You can then transmit on either channel by:

channel1.sendIfDue(engine201, 50); // transmit every 50ms

There is also a bus type guard. If a sketch marks the channel as Classic CAN only, the library rejects accidental CAN FD frames before they get to the driver. That matters when the same codebase is used across Classic CAN and CAN FD projects.

That is the whole point of the library. The application works with engineering values rather than worrying about which byte or bit they belong to.

Receiving messages

The same message definitions can also be used to decode incoming CAN frames.

Instead of manually extracting bits from every received message, the library updates the signal objects automatically, allowing the application to work with decoded values regardless of whether the data originated locally or arrived from the CAN Bus.

There are three receive styles shown in the examples: manual polling, database lookup and callback-driven receive.

Polling

For small projects, the simplest approach is to receive frames as they arrive and try to decode them against the messages you care about.

You create your message and signal and add the signal to the message like you usually would, as described above:

CanMessage engine201({
	.name = "Engine201",
	.idType = STANDARD,
	.id = 0x201,
	.dlc = 8,
	.defaultFill = 0xFF,
	.comment = "From ECM, contains RPM"
});

CanByteSignal rpm({
	.name = "EngineRPM",
	.dataType = UNSIGNED,
	.startByte = 0,
	.byteLength = 2,
	...
});

engine201.addSignal(rpm);

Then just feed the received CAN frame into the message you want to decode:

engine201.decode(frame);

So your void loop in your sketch can be as simple as this:

CANFDMessage frame;
	while (can0.receiveFD0(frame)) {
		if (engine201.decode(frame)) {
			Serial.print("RPM = ");
			Serial.println(rpm.signalValue());
		}
	}

The library then automatically extracts all of the signal values from the received CAN frame so you can use them in your sketch, for example ‘Serial.println(rpm.signalValue());’.

Database lookup

The library includes a database lookup for when you are working with dozens of signals, CanMessageDatabase holds the messages you care about and tries to match incoming frames. If a frame is unknown, decode simply returns false; that is normal on a busy network. If a frame matches a known message but is malformed, that is treated as an error.

In practice, the receive side can stay very small. Add the messages you care about to the database, feed incoming frames into it, and then read the updated signal values.

So you create your database like this, giving it a suitable name and number of messages:

CanMessageDatabase<2> myDatabase;

Add your signals to your messages and then add the messages to the database:

engine201.addSignal(rpm);
engine420.addSignal(celOn);

myDatabase.addMessage(engine201);
myDatabase.addMessage(engine420);

Then similar to the polling option above, just feed the library with the CAN frame and database, and it will decode all of your signals in all of the messages contained in the database:

myDatabase.decode(frame);

So despite having many messages and signals, your main void loop can become as simple as:

CANFDMessage frame;

while (can0.receiveFD0(frame)) {
	if (myDatabase.decode(frame)) {
		Serial.print("RPM = ");
		Serial.print(rpm.signalValue());

		Serial.print("  Engine Light = ");
		Serial.println(celOn.signalValue() != 0.0 ? "ON" : "OFF");
	}
}

The database searches the registered messages, decodes the matching one and updates the attached signal objects. If a frame ID is unknown, that is treated as a normal not-decoded result rather than an error.

One intentional detail: the database is not channel-aware. If the same CAN ID can mean different things on two physical buses, the clean approach is to use a separate database per channel.

Callback-driven receive

Callback/dispatch is useful when you want the CAN driver filter to route known IDs directly to small handler functions. This keeps loop() very small, avoids manual “if this ID, decode that message” checks, and scales nicely when several known messages each have their own handler.

For callback-driven firmware, ACANFD_SAME still owns the receive path and filter dispatch. CANMessageSignal is used inside the callback to decode the message and update the signals.

ACANFD_SAME::StandardFilters standardFilters;
standardFilters.addSingle( 0x201, ACANFD_SAME_FilterAction::FIFO0, myCallBackFunction );
can0.beginFD(settings, standardFilters);

This sets a callback, and myCallBackFunction (named whatever you like) is run every time we receive ID 0x201 on can0.

This is useful when you have a CAN Bus with heavy traffic but only care about a handful of messages and want to act on them.

static void myCallBackFunction(const CANFDMessage& frame) {
  if (engine201.decode(frame)) {
    // Do a thing when RPM is over a certain value
	// Enable something when speed is under a certain value
  }
}

This is useful when the firmware only cares about specific CAN IDs and you want the receive code to be driven from the CAN filter callbacks rather than manually checking every frame in the main loop.

That is the sort of code I wanted to end up with. The CAN frame can arrive as raw bytes, but the rest of the application gets to deal with rpm.signalValue() and celOn.signalValue() instead of knowing which bytes they came from. That means the same definitions can be reused by transmitters, receivers, gateways, emulators, analysers and test equipment.

DBC-style definitions

CANMessageSignal follows the parts of the DBC model that make firmware easier to write.

Messages define their name, ID type, ID and default value, signals define their bit position, length, endianness, scaling, offset, units and engineering limits. The application works entirely with engineering values while the library converts them to and from the raw integers stored in the CAN frame.

For example, with a factor of 0.26, setting an RPM signal to 3500 means the library works out the raw payload value and packs the correct bits into the message. It also handles the awkward part that most people only want to debug once: Intel / little-endian signals count up from the least significant bit, while Motorola / big-endian signals are defined from the most significant bit and walk differently through the payload.

Something like translation becomes as simple as:

RPMrx8.setSignalValue(RPM350Z.signalValue());

The above would then take the RPM value as sent by a Nissan 350Z engine ECU and convert it into what a Mazda RX8 instrument cluster wants to see, handling all scaling, bit packing, IDs, etc.

The definitions are written directly in C++ rather than imported from DBC files. That keeps everything self-contained inside the firmware and avoids introducing additional tooling into Arduino projects. I am currently developing a web app and in the future this will include functionality that will bidirectionally convert between DBC libraries and C++ header files.

Setup and use

CANMessageSignal deliberately leaves the CAN controller setup to ACANFD_SAME. A sketch still configures the CAN peripheral, bit timing, filters, FIFOs and transceiver pins in the normal way, this gives us full control of the CAN controllers without reinventing the wheel in this library.

A typical setup has two parts:

  1. Set up the CAN controller with ACANFD_SAME.
  2. Set up the messages, signals and channels with CANMessageSignal.

RAM sizes, CAN controller, filters etc are set up using ACANFD_SAME:

#define CAN0_MESSAGE_RAM_SIZE (1728)
#define CAN1_MESSAGE_RAM_SIZE (0)

#include <Arduino.h>
#include <ACANFD_SAME.h>
#include "CANMessageSignal.h"
#include "DeviceSignals.h"

void setup() {
	ACANFD_SAME_Settings settings(
		ACANFD_SAME_Settings::CLOCK_48MHz,
		500UL * 1000UL,
		DataBitRateFactor::x1
	);

	settings.mModuleMode = ACANFD_SAME_Settings::NORMAL_FD;

	can0.beginFD(settings);
}

For a Classic CAN project, the call to beginFD() can look a little odd at first, but that is just the ACANFD_SAME API. The channel guard myChannel0.setBusType(CAN_CLASSIC); in CANMessageSignal is what stops the library from accidentally sending CAN FD frames on a Classic CAN Bus.

CANMessageSignal then sits above that driver. The signal definitions are normally registered once during setup:

using namespace CANMessageSignal;

//This gives each CAN controller a friendly name. 
CanChannel myChannel0(can0);
CanChannel myChannel1(can1);

setDeviceSignals();

myChannel0.setBusType(CAN_CLASSIC);
myChannel1.setBusType(CAN_FD);

RX-8 Examples

Most of the supplied examples are based on the Mazda RX-8 instrument cluster, as I’ve fully reverse engineered the RX-8 CAN Bus protocol in a previous project. If you get hold of a Mazda RX-8 series 1 instrument cluster and connect it to the MicroCAN-FD or Adafruit Feather M4 CAN Express and upload the transmit example from this library, you will have full control of the instrument cluster.

All examples define real CAN messages for engine speed, vehicle speed, coolant temperature, oil pressure, warning lamps, cruise control indicators, steering warnings, ABS / DSC / traction control lamps and other dashboard functions.

There are examples covering both transmission and reception, including periodic messages, RPM and speed sweeps, callback-driven reception and polling.

There are also receive examples that decode frames and print them over serial as either whole messages or live signal values. Both polling and callback styles are shown, because both are useful depending on the firmware structure.

Because both the transmitter and receiver use the same message definitions, the exact same signal descriptions can be shared between multiple projects.

Where it fits in my projects

CANMessageSignal is the higher-level layer and sits above ACANFD_SAME.

  • ACANFD_SAME handles the SAME5x CAN hardware, bit timing, message RAM, filters, FIFOs, callbacks and transmit queues.
  • CANMessageSignal handles message definitions, signal packing, signal decoding, enum labels, constants and periodic sends.
  • MicroCAN-FD is an ultra-small device that is designed to be super easy to use, it uses the two libraries together so firmware can be written as a CAN translator, emulator, analyser or development tool without constantly re-writing low-level payload handling.

Separating the hardware driver from the message layer makes both libraries reusable across different projects while keeping the application code clean and easy to understand.

Current state

The library is released as open source and is currently at v1.0.0.

It supports everything described above for both Classic CAN and CAN FD. It is used throughout my own projects, including MicroCAN-FD, where it forms the message and signal layer for translators, gateways, analysers and test equipment. I am regularly using it now in most of my CAN Bus projects, even if it doesn’t require two channels or translation, I love being able to set up all the messages and signals in a separate header file and having the main code super easy to write.

The library is complete and ready to use, it supports Adafruit Feather M4 CAN, MicroCAN-FD and any other SAME5x board supported by the ACANFD_SAME library. Currently, multiplexing fields are present in the signal metadata, to future proof the API. Multiplexed packing and decoding are not implemented in v1.0.0, if there is enough community interest I will implement it. Multiplexed signals can be implemented using enums and some extra logic if needed, which I have done in the past, I’ll add an example of this to the examples soon, star or watch the GitHub repository to keep updated.

For me, the biggest benefit of this library isn’t that it saves a few lines of code. It’s that I can define CAN Bus messages and signals using a DBC style format. In most cases I can just copy directly from a DBC file and wrap it in the appropriate C++ definition, when everything has been defined, I can stop thinking about bits and bytes altogether and write firmware using the values the application actually cares about.