Overview
Adafruit QT Py boards are a great way to make very small microcontroller projects that pack a ton of power - and now we have a way for you to turn many QT Py boards into powerful CAN bus devices that are super small!
CAN Bus is a small-scale networking standard, originally designed for cars and, yes, buses, but is now used for many robotics or sensor networks that need better range and addressing than I2C, and don't have the pins or computational ability to talk on Ethernet. CAN is 2 wire differential, which means it's good for long distances and noisy environments.
Messages are sent at about 1Mbps rate - you set the frequency for the bus and then all 'joiners' must match it, and have an address before the packet so that each node can listen in to messages just for it. New nodes can be attached easily because they just need to connect to the two data lines anywhere in the shared net. Each CAN devices sends messages whenever it wants, and thanks to some clever data encoding, can detect if there's a message collision and retransmit later.
If you'd like to connect your QT Py or Xiao to a CAN Bus, the Adafruit CAN Bus BFF will work with any and all boards thanks to the simple SPI interface! The controller used is the MCP25625 - also known as the MCP2515 controller plus MCP2551 transceiver, an extremely popular and well-supported chipset that has drivers in Arduino and CircuitPython and only requires an SPI port and 1 pin for chip-select, plus another for optional IRQ. Use it to send and receive messages in either standard or extended format at up to 1 Mbps.
We've added a few nice extras to this BFF to make it useful in many common CAN scenarios:
- JST PH socket - Plug in one of our JST PH 3-pn cables (not included!) for fast wiring and connection/disconnection. Black wire is ground, Red wire is CAN H, White wire is CAN L.
- 120 ohm termination resistor on board, you can remove the termination easily by cutting the jumper on the top of the board.
- Pre-connected CS pin to A3. There's also jumpers if you want to connect the IRQ, Reset and Silent pins.
We include some header that you can solder to your QT Py. You can also pick up an ItsyBitsy short female header kit to make it removable but compact, you'll just need to trim down the headers to 7 pins long. This board works with any QT Py or Xiao-shaped board that has the SPI port and A3 pin available.
A QT Py and JST PH 3-Pin cable is not included.
Page last edited March 08, 2024
Text editor powered by tinymce.
Pinouts
CAN Bus Controller
The MCP25625 (aka an MCP2515 with built-in transceiver), is an extremely popular and well-supported chipset that has drivers in Arduino and CircuitPython. It only requires a SPI port and one pin for chip-select at a minimum. You can use it to send and receive messages in either standard or extended format at up to 1 Mbps.
The MCP25625 uses the exposed SPI port (SCK, MOSI and MISO).
CAN Bus JST-PH Connector
On the front of the board is the JST-PH connector with the three pins for communicating with the CAN Bus standard. If you use one of our JST-PH 3-pin cables, black wire is ground, red wire is CAN H and white wire is CAN L.
- The left input on the JST-PH connector is common ground shared between the two CAN connections.
- H - the CAN high signal for the CAN Bus standard. It is connected to the middle pin on the JST-PH connector.
- L - the CAN low signal for the CAN Bus standard. It is connected to the right input on the JST-PH connector.
CAN Bus Jumper Pins
- INT - The interrupt pin for the MCP25625. This jumper is open/not connected by default. If you solder the jumper closed, it will connect the interrupt pin to A0.
- STBY - The standby pin for the MCP25625. This jumper is open/not connected by default. If you solder the jumper closed, it will connect the interrupt pin to A1.
- RST - The reset pin for the MCP25625. This jumper is open/not connected by default. If you solder the jumper closed, it will connect the reset pin to A2.
- CS - The chip-select pin for the MCP25625. This jumper is closed by default and connected to pin A3. You can cut this jumper to disconnect CS from A3 and solder a wire from the pad closest to the CS label on the board silk to a different pin.
- Trm - The terminator jumper for the MCP25625. The BFF has a 120 ohm termination resistor connected between H and L. You can disable (remove) the resistor by cutting the Trm jumper, if your bus is already terminated.
Page last edited March 08, 2024
Text editor powered by tinymce.
CircuitPython
It's easy to use the CAN Bus BFF with CircuitPython and the Adafruit_CircuitPython_MCP2515 module. This module allows you to easily write Python code that lets you utilize the MCP25625 CAN bus controller. In the example below, you'll connect an RP2040 CAN Bus Feather to a QT Py plugged into a CAN Bus BFF. The Feather will send CAN Bus messages whenever the seesaw rotary encoder is used and the QT Py will receive and display the messages on a STEMMA OLED.
CircuitPython Microcontroller Wiring
Plug an CAN Bus BFF into your QT Py or Xiao form factor board exactly as shown below. Here's an example of connecting a QT Py RP2040 to the BFF.
Connect the QT Py RP2040 with plug headers into the CAN Bus BFF with socket headers. They should be plugged in with the backs of the boards facing each other.
For more information on soldering socket headers, check out this Learn Guide.
Then, you'll connect the Feather and BFF CAN Bus connections to each other, followed by the STEMMA peripherals to the corresponding boards.
QT Py
-
Board STEMMA 3V to screen VIN (red wire)
-
Board STEMMA GND to screen GND (black wire)
-
Board STEMMA SCL to screen SCL (yellow wire)
- Board STEMMA SDA to screen SDA (blue wire)
CAN Bus
- Feather H terminal block to BFF H JST-PH (red wire)
- Feather middle (ground) terminal block to BFF ground JST-PH (black wire)
- Feather L terminal block to BFF L JST-PH (white wire)
Feather RP2040 CAN Bus
-
Feather STEMMA 3V to rotary encoder VIN (red wire)
-
Feather STEMMA GND to rotary encoder GND (black wire)
-
Feather STEMMA SCL to rotary encoder SCL (yellow wire)
- Feather STEMMA SDA to rotary encoder SDA (blue wire)
CircuitPython Usage
To use with CircuitPython, you need to first install the MCP2515 library, and its dependencies, into the lib folder onto both of your CIRCUITPY drives for this example. Then, load the different example code files onto the corresponding boards; one for the Feather RP2040 CAN Bus and one for the QT Py RP2040 with the CAN Bus BFF.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the necessary libraries and Python files in a zip file. Extract the contents of the zip file, and copy the entire lib folder and can_bus_bff_receiver.py file to your QT Py RP2040 CIRCUITPY drive. Rename can_bus_bff_receiver.py to code.py.
Then, copy the entire lib folder and can_bus_feather_send.py file to your Feather RP2040 CAN Bus CIRCUITPY drive. Rename can_bus_feather_send.py to code.py.
Your QT Py RP2040 and Feather RP2040 CAN Bus CIRCUITPY/lib folders should contain the following folders and files:
- adafruit_bitmap_font/
- adafruit_bus_device/
- adafruit_display_text/
- adafruit_mcp2515/
- adafruit_seesaw/
- adafruit_displayio_ssd1306.mpy
- adafruit_pixelbuf.mpy
Once everything is saved to both CIRCUITPY drives, connect to the serial console to see the data printed out! The Feather will be sending CAN messages to the QT Py via the CAN Bus BFF.
Feather RP2040 CAN Bus Sender
# SPDX-FileCopyrightText: Copyright (c) 2024 ladyada for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import board
from rainbowio import colorwheel
from digitalio import DigitalInOut
from adafruit_mcp2515 import MCP2515 as CAN
from adafruit_mcp2515.canio import Message
from adafruit_seesaw import seesaw, neopixel, rotaryio, digitalio
cs = DigitalInOut(board.CAN_CS)
cs.switch_to_output()
spi = board.SPI()
can_bus = CAN(
spi, cs, loopback=False, silent=False
) # use loopback to test without another device
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
seesaw = seesaw.Seesaw(i2c, addr=0x36)
seesaw_product = (seesaw.get_version() >> 16) & 0xFFFF
print("Found product {}".format(seesaw_product))
if seesaw_product != 4991:
print("Wrong firmware loaded? Expected 4991")
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
button = digitalio.DigitalIO(seesaw, 24)
pixel = neopixel.NeoPixel(seesaw, 6, 1)
pixel.brightness = 0.3
button_held = False
color = 0
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
while True:
with can_bus.listen(timeout=5.0) as listener:
position = -encoder.position
if position != last_position:
if position > last_position:
color += 5
else:
color -= 5
color = (color + 256) % 256 # wrap around to 0-256
pixel.fill(colorwheel(color))
last_position = position
str_pos = str(position)
byte_pos = str_pos.encode()
message = Message(id=0x1234ABCD, data=byte_pos, extended=True)
send_success = can_bus.send(message)
print("Send success:", send_success)
if not button.value and not button_held:
button_held = True
message = Message(id=0x1234ABCD, data=b"pressed", extended=True)
send_success = can_bus.send(message)
print("Send success:", send_success)
if button.value and button_held:
button_held = False
message = Message(id=0x1234ABCD, data=b"released", extended=True)
send_success = can_bus.send(message)
print("Send success:", send_success)
time.sleep(0.1)
When you open the serial console for the Feather, you'll see confirmations that packets have been sent every time you turn the rotary encoder or press the button. As you turn the rotary encoder, the NeoPixels on the encoder and the Feather will advance thru the color wheel.
# SPDX-FileCopyrightText: 2024 ladyada for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import board
import terminalio
import displayio
import i2cdisplaybus
from digitalio import DigitalInOut
from adafruit_mcp2515.canio import Message, RemoteTransmissionRequest
from adafruit_mcp2515 import MCP2515 as CAN
import adafruit_displayio_ssd1306
from adafruit_display_text import label
displayio.release_displays()
i2c = board.STEMMA_I2C()
# STEMMA OLED setup
display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=None)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
cs = DigitalInOut(board.A3)
cs.switch_to_output()
spi = board.SPI()
can_bus = CAN(
spi, cs, loopback=False, silent=False
) # use loopback to test without another device
splash = displayio.Group()
display.root_group = splash
font = terminalio.FONT
main_area = label.Label(
font, text="CAN Receiver", color=0xFFFFFF)
main_area.anchor_point = (0.5, 0.0)
main_area.anchored_position = (display.width / 2, 0)
msg_area = label.Label(
font, text="ID: ", color=0xFFFFFF)
msg_area.anchor_point = (0.0, 0.5)
msg_area.anchored_position = (0, display.height / 2)
val_area = label.Label(
font, text="Val: ", color=0xFFFFFF)
val_area.anchor_point = (0.0, 1.0)
val_area.anchored_position = (0, display.height)
splash.append(main_area)
splash.append(msg_area)
splash.append(val_area)
while True:
with can_bus.listen(timeout=1.0) as listener:
message_count = listener.in_waiting()
for i in range(message_count):
print(message_count, "messages available")
msg = listener.receive()
print("Message from ", hex(msg.id))
msg_area.text = f"ID: {hex(msg.id)}"
if isinstance(msg, Message):
print("message data:", msg.data)
val_area.text = f"Val: {msg.data}"
if isinstance(msg, RemoteTransmissionRequest):
print("RTR length:", msg.length)
time.sleep(0.1)
When you open the serial console for the QT Py RP2040, you'll see the messages coming in via CAN for the status of the encoder position and the push button. You'll also see them on the OLED along with "CAN Receiver" at the top.
Page last edited March 08, 2024
Text editor powered by tinymce.
Arduino
Using the CAN Bus BFF with Arduino involves wiring up the BFF to your Arduino-compatible QT Py or Xiao form factor board, installing the Adafruit_MCP2515 library, and running the provided example code. In the example below, you'll connect a RP2040 CAN Bus Feather to a QT Py plugged into a CAN Bus BFF. The Feather will send CAN Bus messages whenever the seesaw rotary encoder is used and the QT Py will receive and display the messages on a STEMMA OLED.
Wiring
Plug a CAN Bus BFF into your QT Py or Xiao form factor board exactly as shown below. Here's an example of connecting a QT Py RP2040 to the BFF.
Connect the QT Py RP2040 with plug headers into the CAN Bus BFF with socket headers. They should be plugged in with the backs of the boards facing each other.
For more information on soldering socket headers, check out this Learn Guide.
Then, you'll connect the Feather and BFF CAN Bus connections to each other, followed by the STEMMA peripherals to the corresponding boards.
QT Py
-
Board STEMMA 3V to screen VIN (red wire)
-
Board STEMMA GND to screen GND (black wire)
-
Board STEMMA SCL to screen SCL (yellow wire)
- Board STEMMA SDA to screen SDA (blue wire)
CAN Bus
- Feather H terminal block to BFF H JST-PH (red wire)
- Feather middle (ground) terminal block to BFF ground JST-PH (black wire)
- Feather L terminal block to BFF L JST-PH (white wire)
Feather RP2040 CAN Bus
-
Feather STEMMA 3V to rotary encoder VIN (red wire)
-
Feather STEMMA GND to rotary encoder GND (black wire)
-
Feather STEMMA SCL to rotary encoder SCL (yellow wire)
- Feather STEMMA SDA to rotary encoder SDA (blue wire)
Library Installation
You can install the Adafruit MCP2515 library for Arduino using the Library Manager in the Arduino IDE.
Click the Manage Libraries ... menu item, search for Adafruit MCP2515 and select the Adafruit MCP2515 library:
If asked about dependencies for any of the libraries, click "Install all".
If the "Dependencies" window does not come up, then you already have the dependencies installed.
Follow the same process for the Adafruit seesaw Library...
...Adafruit NeoPixel library...
...and the Adafruit SSD1306 library.
// SPDX-FileCopyrightText: 2024 Limor Fried for Adafruit Industries
//
// SPDX-License-Identifier: MIT
#include "Adafruit_seesaw.h"
#include <seesaw_neopixel.h>
#include <Adafruit_MCP2515.h>
#include <Adafruit_NeoPixel.h>
#define CS_PIN PIN_CAN_CS // for Feather RP2040 CAN, change if needed!
Adafruit_MCP2515 mcp(CS_PIN);
#define CAN_BAUDRATE (250000) // must match the other devices!
#define SS_SWITCH 24
#define SS_NEOPIX 6
#define SEESAW_ADDR 0x36
Adafruit_seesaw ss;
seesaw_NeoPixel sspixel = seesaw_NeoPixel(1, SS_NEOPIX, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel pixel(1, 21, NEO_GRB + NEO_KHZ800);
int16_t encoder_position;
bool button_state;
void setup() {
Serial.begin(115200);
delay(500);
while (!Serial) delay(10);
Serial.println("MCP2515 Sender test!");
if (!mcp.begin(CAN_BAUDRATE)) {
Serial.println("Error initializing MCP2515.");
while(1) delay(10);
}
Serial.println("MCP2515 chip found");
Serial.println("Looking for seesaw!");
if (! ss.begin(SEESAW_ADDR) || ! sspixel.begin(SEESAW_ADDR)) {
Serial.println("Couldn't find seesaw on default address");
while(1) delay(10);
}
Serial.println("seesaw started");
uint32_t version = ((ss.getVersion() >> 16) & 0xFFFF);
if (version != 4991){
Serial.print("Wrong firmware loaded? ");
Serial.println(version);
while(1) delay(10);
}
Serial.println("Found Product 4991");
// set not so bright!
sspixel.setBrightness(20);
sspixel.show();
// ditto built in pixel
pinMode(NEOPIXEL_POWER, OUTPUT);
digitalWrite(NEOPIXEL_POWER, HIGH);
pixel.begin();
pixel.setBrightness(20);
pixel.show();
// use a pin for the built in encoder switch
ss.pinMode(SS_SWITCH, INPUT_PULLUP);
// get starting position
encoder_position = ss.getEncoderPosition();
button_state = ss.digitalRead(SS_SWITCH);
}
void loop() {
bool new_button = ss.digitalRead(SS_SWITCH);
if (new_button != button_state) {
Serial.println("Button pressed!");
}
int32_t new_position = ss.getEncoderPosition();
// did we move around or button change?
if ((new_button != button_state) ||
(encoder_position != new_position)) {
encoder_position = new_position; // and save for next round
button_state = new_button;
pixel.setPixelColor(0, Wheel(new_position & 0xFF));
pixel.show();
Serial.print("Sending Position ");
Serial.print(encoder_position);
Serial.print(" & Button ");
Serial.println(button_state);
mcp.beginPacket(0x12);
if (ss.digitalRead(SS_SWITCH)) {
mcp.write(0);
} else {
mcp.write(1);
}
mcp.write(encoder_position >> 8);
mcp.write(encoder_position & 0xFF);
if (mcp.endPacket()) {
Serial.println("Done");
// change the neopixel color on success
sspixel.setPixelColor(0, Wheel(new_position & 0xFF));
} else {
Serial.println("Failure");
// turn off neopixel on failure
sspixel.setPixelColor(0, 0x00);
}
sspixel.show();
}
// don't overwhelm serial port
delay(10);
}
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if (WheelPos < 85) {
return sspixel.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if (WheelPos < 170) {
WheelPos -= 85;
return sspixel.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return sspixel.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
// SPDX-FileCopyrightText: 2024 Limor Fried for Adafruit Industries
//
// SPDX-License-Identifier: MIT
#include <Adafruit_MCP2515.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSans9pt7b.h>
#define CS_PIN A3
#define CAN_BAUDRATE (250000)
Adafruit_MCP2515 mcp(CS_PIN);
Adafruit_SSD1306 oled(128, 64, &Wire1, -1);
void setup() {
Serial.begin(115200);
while(!Serial) delay(10);
Serial.println("MCP2515 OLED Receiver test!");
if (!mcp.begin(CAN_BAUDRATE)) {
Serial.println("Error initializing MCP2515.");
while(1) delay(10);
}
Serial.println("MCP2515 chip found");
if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3D)) {
Serial.println(F("SSD1306 allocation failed"));
while(1) delay(10);
}
oled.display();
delay(1000);
oled.clearDisplay();
oled.display();
oled.setFont(&FreeSans9pt7b);
oled.setTextColor(SSD1306_WHITE);
}
void loop() {
// try to parse packet
int packetSize = mcp.parsePacket();
if (packetSize) {
// received a packet
Serial.print("Received ");
Serial.print("packet with id 0x");
Serial.print(mcp.packetId(), HEX);
Serial.print(" and length ");
Serial.println(packetSize);
// only look for correct length packets
if (packetSize != 3) return;
// wait till all data is ready!
while (mcp.available() != 3) {
delay(1);
}
uint8_t button_state = 0;
int16_t val = 0;
button_state = mcp.read();
val = mcp.read();
val <<= 8;
val |= mcp.read();
Serial.print("Button is ");
if (button_state) Serial.print("pressed");
else Serial.print("released");
Serial.print(" & value is ");
Serial.println(val);
oled.clearDisplay();
oled.setCursor(0, 15);
oled.println("CAN receiver");
oled.print("Val: "); oled.println(val);
oled.print("Button ");
if (button_state) oled.print("DOWN");
else oled.print("UP");
oled.display();
}
}
Upload the Example Feather Code to the Feather RP2040 CAN Bus and then upload the Example CAN Bus BFF Code to the QT Py RP2040. When you open the Serial Monitor for the Feather, you'll see confirmations that packets have been sent every time you turn the rotary encoder or press the button. As you turn the rotary encoder, the NeoPixels on the encoder and the Feather will advance thru the color wheel.
When you open the Serial Monitor for the QT Py RP2040, you'll see the messages coming in via CAN for the status of the encoder position and the push button. You'll also see them on the OLED along with "CAN receiver" at the top.
Page last edited March 08, 2024
Text editor powered by tinymce.
Downloads
Page last edited March 08, 2024
Text editor powered by tinymce.