A Shallow Deep Dive into the CAN Protocol - ECE-180D-WS-2023/Knowledge-Base-Wiki GitHub Wiki

A Shallow Deep Dive Into the CAN Protocol

What if I told you that there exists a simple low-cost technology that you can utilize in your projects to land that much coveted summer internship?

Join me on this journey of discovering what this protocol is about and, to give a little incentive, I guide you on a simple application where we create a reflex game between two people using this technology.

You CAN do this!

What is CAN

Controller Area Network or CAN is a network technology primarily used in embedded systems that provide fast communication amongst microcontrollers while being cost-effective. Notably, a network can function as a series of nodes and operate without a host computer [1].

Ok, but what does that mean!? It means that there isn't a central computer delegating tasks and controlling how messages are sent or received. Like some cryptocurrencies, a CAN bus is a decentralized network that is able to send messages efficiently across its network. With the way the CAN is implemented, industries such as automotive, medical, and defense did not have to spend most of their funding on network implementation. The automotive industry created the protocol to reduce wiring across the multiple instruments in a car. In the medical field, CAN is used for real-time control between Class III medical devices (devices that sustain or support life) and peripheral. [6,7]. Finally, one of my coworkers was telling me that they used CAN for satellites in Boeing.

Some characteristics of CAN [2]:

  • preserving foods
  • needs only two wires to communicate
  • operates at data rates up to 1 Mb/s
  • supports a max of 8 Bytes per message frame
  • supports message priority
  • supports ID lengths of of 11-bits and 29-bits

Some other industries of where CAN is used:

  • manufacturing
  • railroads
  • elevators and escalators (where I use it for my job 💅)

Similar technologies to CAN [4]:

  • electric can-opener
  • ethernet w/ TCP/IP (expensive 👎)
  • FPD-link

Digging Deeper into 🥫

Hardware Principles

As mentioned earlier, a network using CAN only needs two wires to operate:

image [1]

This significantly reduces the wires needed to connect the network, and is why it was and still is popular in the automobile industry. The motivation behind the CAN protocol was to reduce costs by reducing the use of copper wire:

image [2]

We can break down a node into three parts:

image [1]

  1. Microprocessor

    The microprocessor is responsible for decoding messages received and decides what messages to transmit.

  2. CAN controller

    The controller is responsible for receiving and storing messages until a full message frame is received to process.

  3. Transceiver

    The tranceiver converts the data from the bus to a format that the CAN controller can understand. It also converts the message from the controller to a format the bus accepts.

[1]

Software Principles

Well that's cool and all, but how CAN I use this!? I gotchu!

There are many abstractions and API's created for the user to be able to use the CAN protocol. In my job, I am able to use CAN on python, C++, and C to interact and send signals to the elevator system.

Really, all we need to focus on as users are:

  1. Message ID's

    There exists two different standards for the protocol: Standard CAN and Extended CAN.

    Standard CAN has a message ID of 11 bits, therefore, we can have 211 or 2048 different identifiers from 0 to 2047. Extended CAN has a message ID of 29 bits, therefore, we can have 229 or 536(ish) million identifiers from 0 to 536870911 [3].

    Pretty crazy huh? And as programmers we can even design how we want to use these messages. Do we potentially want 536 million different nodes? Do we want to have 536 million different types of messages we can send? The skies the limit or really 229, but you get what I mean.

  2. The actual message

    Regardless of whether you choose standard or extended, the protocol still give us 8 bytes to send our message in.

    And again, as programmers we have the ability to design how we want to encode our data:

    1. we can use one-hot encoding where only one of the 64 bits (8 bytes * 8 bits/byte) are set to a high
    2. we can use the whole range of the 64 bits with either binary or hexadecimal
    3. we can even send strings as longs as we bound it to 8 characters since a character will take up 1 byte each
  3. What speed we want to send things through the bus

    Before using a the CAN bus we must first initialize and set all nodes to read and write data at a certain speed. Remember that we are bounded by 1 Megabit per second, but something else that we should consider is the length of the transmission cable we are going to use. The higher the data rate, the shorter we can send data.

     A good rule of thumb to follow is that 1Mb/s can reach 40 meters while 125kb/s can reach 500 meters.
    

Ethernet and CAN - For Thoroughness

As an aside, I believe its important to compare similar technologies to get a better sense when one is advantageous over the other in context.

Both ethernet and CAN are packetized systems with bidirectional sending of information.

Where they differ are in the speeds in which you can send data. Ethernet is good for mid-bandwidth (12.5 to 112.5 MB/s) applications, meaning it can transport over a 100 times faster [4]. However, Ethernet nodes cost per node is higher than CAN - CAN is the effective cheaper option when it comes to applications that don't need a lot of bandwidth.

A Simple Walkthrough: CAN we make a 🎮

Have you ever imagined what its like to be a cowboy at a stand-off? Well, no need to keep imagining because we're going to use CAN to simulate one!

Parts we are going to need:

  • 1 breadboard
  • 2 buttons or dip-switches
  • 2 4.7 kOhm resistors
  • 2 USB-C cables
  • 2 Feather M4 CAN microcontrollers [8]
  • 10 jumper wires

Configuration of board:

IMG_0826

Before we begin, we have to initialize our environment. Follow the steps in this and this link to properly set up your Arduino environment.

Some things to look out for:

  • the positive side of the resistor is connect to 'RX' of the Feather board
  • make sure the same terminal are connected on the board (CAN_H to CAN_H, CAN_L to CAN_L, etc.)
  • we are using GPIO pins with max voltage at 3.3v - make sure to use the 3.3v pin from the Feather board

Player 1 code:

#include <CAN.h>
#include <Adafruit_NeoPixel.h>

const long HOST_ID = 0x000;
const long PLAYERS_ID = 0x001;
const long CAN_BPS = 250000;
const int MESSAGE = 1;
const int WAIT_TIME = 4;
const int GPIO_INPUT = 0;
bool play = false;
bool in = true;

Adafruit_NeoPixel strip(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

void setup() {
    Serial.begin(9600);

    strip.begin();
    strip.setBrightness(50);

    pinMode(GPIO_INPUT, INPUT); // set the correct pin as input

    if (!CAN.begin(CAN_BPS)) {
        Serial.println("Starting CAN failed");
        while (true);
    }

    delay(5000); // delay by 5 seconds so that other nodes are initialized
}

void loop() {
    if (play) {
        if (digitalRead(GPIO_INPUT) && in) { // send data to other nodes
            CAN.beginPacket(PLAYERS_ID);
            CAN.write(MESSAGE);
            CAN.endPacket();
            in = false;
        }

        int packetSize = CAN.parsePacket();
        if (packetSize) { // received a message
            if (CAN.packetId() == PLAYERS_ID) { // received a message from another player
                int payload = 0;
                while (CAN.available()) {
                    payload = CAN.read();
                }

                if (payload) { // another player unalived me
                    strip.setPixelColor(0, strip.Color(255, 0, 0)); // set LED to red
                    strip.show();
                    Serial.print("Winner - Player ");
                    Serial.println(payload);
                }

                play = false;
                in = true;
                delay(3000);

                strip.setPixelColor(0, strip.Color(0, 0, 0)); // turn off LED to continue game
                strip.show();

                CAN.beginPacket(HOST_ID); // send a reset to all players
                CAN.write(MESSAGE);
                CAN.endPacket();
            }
            if (CAN.packetId() == HOST_ID) { // another node asked for a reset
                int payload = 0;
                while (CAN.available()) {
                    payload = CAN.read();
                }
                strip.setPixelColor(0, strip.Color(0, 0, 0)); // set LED to off
                strip.show();
                play = false;
                in = true;
            }
        }
    }
    else { // player's are not available to play yet
        Serial.println("Get Ready!");
        for (int i = 0; i <= WAIT_TIME; i++) { // flash yellow sequence to alert players that game is about to begin
            if (i % 2)
                strip.setPixelColor(0, strip.Color(255, 255, 0)); // set LED to yellow for standby
            else
                strip.setPixelColor(0, strip.Color(0, 0, 0));

            strip.show();
            delay(1000);
        }

        // send begin to other nodes
        CAN.beginPacket(HOST_ID);
        CAN.write(MESSAGE);
        CAN.endPacket();

        play = true;
        Serial.println("Go!");
        strip.setPixelColor(0, strip.Color(0, 255, 0)); // set LED to green to alert players game has started
        strip.show();
    }
}

This code has a sentinel 'play' value. If we are not currently in play mode, player 1 initiates the game by flashing a yellow LED, then turns the LED to green, and send sends the go signal to other nodes in the network. In the 'play' block, the code checks for an input from the user and if it detects a press/switch, we send a CAN message to all other nodes. On the other hand, if another player sends us a message first, we are the ones that are shot and we switch the LED from green to red. We wait a few seconds, reset the LED, and send a reset signal to all other nodes.

Player 2 code:

#include <CAN.h>
#include <Adafruit_NeoPixel.h>

const long HOST_ID = 0x000;
const long PLAYERS_ID = 0x001;
const long CAN_BPS = 250000;
const int MESSAGE = 2;
const int WAIT_TIME = 3;
const int GPIO_INPUT = 0;
bool play = false;
bool in = true;

Adafruit_NeoPixel strip(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

void setup() {
    Serial.begin(9600);

    strip.begin();
    strip.setBrightness(50);

    pinMode(GPIO_INPUT, INPUT);

    if (!CAN.begin(CAN_BPS)) {
        Serial.println("Starting CAN failed");
        while (true);
    }
}

void loop() {
    Serial.print("Play Mode: ");
    Serial.println(play);
    
    if (play) {
        if (digitalRead(GPIO_INPUT) && in) {
            CAN.beginPacket(PLAYERS_ID);
            CAN.write(MESSAGE);
            CAN.endPacket();
            in = false;
        }

        int packetSize = CAN.parsePacket();
        if (packetSize) {
            if (CAN.packetId() == PLAYERS_ID) {
                int payload = 0;
                while (CAN.available()) {
                    payload = CAN.read();
                }

                if (payload) {
                    strip.setPixelColor(0, strip.Color(255, 0, 0)); // set LED to red
                    strip.show();
                    Serial.print("Winner - Player ");
                    Serial.println(payload);
                }

                play = false;
                in = true;
                delay(3000);

                strip.setPixelColor(0, strip.Color(0, 0, 0));
                strip.show();
                
                CAN.beginPacket(HOST_ID);
                CAN.write(MESSAGE);
                CAN.endPacket();
            }
            if (CAN.packetId() == HOST_ID) {
                int payload = 0;
                while (CAN.available()) {
                    payload = CAN.read();
                }
                strip.setPixelColor(0, strip.Color(0, 0, 0)); // set LED to off
                strip.show();
                play = false;
                in = true;
            }
        }
    }
    else { // player's are not available to play yet
        int packetSize = CAN.parsePacket();
        Serial.println("Waiting for GO signal from host");
        if (packetSize && CAN.packetId() == HOST_ID) {
            Serial.println("Recived GO signal from host");
            int payload = 0;
            while (CAN.available()) {
                payload = CAN.read();
            }

            strip.setPixelColor(0, strip.Color(0, 255, 0)); // set LED to green
            strip.show();
            play = true;
        }
    }
}

The code for both nodes is essentially the same. The only difference is that player 1 is the node that initiates the game, hence the difference in code in the last 'else' statements.

Known bugs:

  • sometimes the CAN bus may not send the reset signal, when that happens you should press the RESET button of the culprit FEATHER board - the one that remains lit up
  • plug in player 2 before powering player 1

Photos of the game working: IMG_0828 Player 1 node blinking yellow telling all players to get ready

IMG_0829 All nodes are lit green to tell players that they should make a move

IMG_0830 Player 1 unalived player 2

Sealing this CAN Post

Controller Area Network is an easy and cheap solution to any type of application that needs linked communication. In this post, I have given a brief overview of what CAN is, it's components, and how we can use it.

It is a technology that is used in many of the things we interact with and now you know how to use it!

Links

  1. Wikipedia
  2. CopperHillTech
  3. TexasInstruments-1
  4. TexasInstruments-2
  5. Adafruit
  6. SterlingMedicalDevices
  7. FDA
  8. Adafruit
⚠️ **GitHub.com Fallback** ⚠️