In stock
View Purchasing OptionsProject update 4 of 14
Hi everyone,
It’s true there are several CAN and CANFD shields on the markets with several supporting libraries. Many of them are designed for use with popular open-source platforms like Arduino UNO and ATMega. So why not just use one of those?
For this week’s update, I decided to write about one of the primary motivations for using an open-source board that is purpose built for CAN projects as opposed to a "stacked-shield SPI-based" approach: the need for speed. {canfduino-u02-p01-execution-time-1 | link}
Imagine you are planning your next EV conversion, working on an OBDII CAN project, or developing on a complex simulation or gateway project at work. Looking around, you find a wealth of information on Arduino UNOs and associated SPI-to-CAN shields. The solution looks easy, you find plenty of related sample code, there are lots of vendors, and you’ve worked with Arduino’s a ton already. With the UNO+shield order placed, you are coding and integrating and working with CAN buses in no time. Over the next few weeks, as your project scope grows, you continue adding layers to your code and then…something starts to happen. The other devices on the bus begin updating very slowly, and the UNO itself starts responding with spurious messages. It seems your open source venture is stuck in a bottleneck until you find a more reliable way to get where you’re going.
Many open-source CAN/CANFD solutions are missing microcontrollers that have the bus hardware built-in (native CAN controller). This is why there are a good number of SPI CAN/CANFD shield options on the market for UNOs, Megas, etc. If you are lucky, you may find an open source solution that has one CAN port built in. SPI shields carry significant overhead as the host processor (such as the UNO) has to read and write over SPI, sending and receiving one bit at a time in serial, which creates a performance bottleneck. Native CAN controllers read and write to registers and memory locations that exist in their own memory space and do so in parallel, meaning 8, 16, or 32 bits at a time!
The constraints of CAN SPI shields are usually reached as the complexity of a project grows. Examples include multi-node systems where you want to receive one or more messages, make a computation or modification, and then re-transmit a corresponding message to a node on the same bus or a different bus. CAN gateway projects are a common example where multiple buses with mixed baud rates or differing protocols need to be integrated. Examples might include making your BMS to talk to your inverter, making your transmission control talk to your engine control, or re-transmitting the contents of some CAN FD messages on a CAN bus.
We conducted an experiment using a CANFDuino and an Arduino UNO with a SparkFun SPI CAN shield. CANFDunio uses the SAMC218GA processor with two native CAN/CANFD controllers; the SPI shield uses the MCP2515 SPI-to-CAN chip. The test was designed to illustrate the difference in execution times between CANFDuino and an UNO+MCP shield by having each board receive a single message (0x100 sent by a PCAN), modify the message contents, and then transmit the message. In the case of CANFDuino, that message is receiving on CAN0 and transmitting on CAN1. The UNO is using only CAN0 and transmitting a different ID. We used a 500 K baud rate for this example and tested only a single message. The wiring setup and oscilloscope traces used for timing are shown below.
The top trace represents the actual CAN bus traffic on CAN bus 0. The second trace is the execution time of the CANFDuino while we pull up digital output 3. The third trace is the execution time of the UNO while we pull up digital output 3. The bottom trace is CAN bus 1, where the CANFDuino is transmitting the modified message. Code snippets for both platforms are shown below.
//CANFDuino Rx, Modify, Tx
void loop()
{
//just sit here and look for messages, fire our new function below (callback) when registered ID recieved
CanPort0.RxMsgs();
digitalWrite(3, LOW);
}
/**
* This is a overridden implementaiton of the base class member that is called by the RXMsgs() funciton in the CAN class everytime a matching frame ID is recieved
* @param R *R - pointer to RX Frame
*
* @return - a flag to accept or reject this CAN frame
*/
bool RxTx::CallbackRx(RX_QUEUE_FRAME *R)
{
bool retVal = false;
UINT8 i;
if (R)
{
digitalWrite(3, HIGH);
//we already know that the ID matches, now copy all contents, modify and re-transmit on CAN1
tx.id = R->rxMsgInfo.id;
tx.len = R->rxMsgInfo.data_len;
//copy all data bytes except byte 0
for(i=1;i<tx.len;i++)
{
tx.data[i] - R->data[i];
}
//lastly modify byte 0, re-transmit on can port 1
tx.data[0] = 0x80;
CanPort1.TxMsg(&tx);
}
return(retVal);
}
//UNO Rx,modify Tx
void loop(){
tCAN message;
digitalWrite(3, LOW);
if (mcp2515_check_message())
{
//scope trace high to capture time to rx from SPI
digitalWrite(3, HIGH);
if (mcp2515_get_message(&message))
{
//scope trace low
digitalWrite(3, LOW);
if(message.id == 0x100) //uncomment when you want to filter
{
//modify byte 0 and byte 7
message.data[0] = 0x55;
message.id=0x200;
//now transmit back, scope trace high to time SPI Tx
digitalWrite(3, HIGH);
mcp2515_send_message(&message);
}
}
}
}
The traces and graph above show that the CANFDuino performed the operation 22 times faster than the UNO+SPI shield, with the entire operation taking 16 uS as opposed to 365 uS. The receive code alone took the UNO 200 uS to execute. And this test used only a single message! In practice, most CAN and CANFD projects will involve reception and transmission of many messages on the bus. When using an SPI-to-CAN device, your processor will spend a long time just receiving messages on a busy bus, leaving it less time for important tasks like computation, writing to SD cards, driving outputs, reading inputs, handling UART data, and communicating with the PC. While a CAN-to-SPI shield might be a quick solution to "get it up and running," you’re probably better off using a solution with native controllers that leave room for your project to scale up in complexity.
Disclaimer: This test was performed using libraries as I found them on GitHub, with the sample code available for each board and no performance optimizations. Both libraries have room for improvement (filtering, interrupts, etc.), but the point of this experiment is simply to demonstrate the significant differences between an external peripheral and a native peripheral.