Need for Speed
Imagine you are planning your next EV conversion, OBDII CAN project, or working on a complex simulation or gateway project at work. Looking around, you find a wealth of information on Arduino UNO’s and associated SPI to CAN shields. This solution looks easy, lots of related example code, 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 CAN buses in no time. As your project scope grows over weeks you continue to put layers on your code and something starts to happen….the other devices on the bus are updating very slowly and the UNO itself seems to only spuriously respond to messages. It seems your open source venture has reached the limits of a bottleneck, and you’ve got to find another route.
Going Native
Many open-source CAN / CANFD solutions are missing are 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 UNO’s, Mega’s etc. If you are lucky, you mayfind an open source solution that has ONE CAN port built in. SPI shields carry significant overhead as the host processor (UNO) has to read and write over SPI, serially sending and receiving one bit at a time, creating a bottleneck in performance. Native CAN controllers are simply reading and writing to registers and memory locations that exist in their own memory space, reading and writing all bits in parallel (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/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 are with mixed baud rates or differing protocols need to be integrated I.E. I need my BMS to talk to this inverter, or I need my transmission control to talk to my engine control, or I need contents of these CAN FD messages re-transmitted on a CAN bus.
Speed Test
An experiment was conducted using the CANFDuino and the Arduino UNO and SparkFun SPI CAN shield. The 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 of the CANFDuino and an UNO+MCP shield by receiving a single message (0x100 sent by a PCAN), modifying the message contents, and then transmitting the message. In the case of the CANFDuino it is receiving on CAN0 and transmitting on CAN1, the UNO is using only CAN0 and transmitting a different ID. A 500K baud rate was used for this example and only a single message was tested. The oscilloscope traces used for timing, wiring setup, and code snippets 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 during which we pull up digital output 3, the third trace is the execution time of the UNO while we pull up digital output 3 and the bottom trace is CAN bus 1 where the CANFDuino is transmitting the modified message. The 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 Results
The traces and graph above show that the CANFDuino performed the operation 22x faster than the UNO+SPI shield performing the whole operation in 16uS vs 365uS. The receive code alone took the UNO 200uS to execute. This test used only a single message, the majority of CAN and CANFD projects in practice will involve reception and transmission of many messages on the bus. When using an SPI to CAN device the time your processor spends just receiving messages on a busy bus will be quite long leaving less time for important tasks such as computations, writing to SD cards, driving outputs, reading inputs, handling UART data, and communicating to the PC. While CAN to SPI shields may be a very quick solution to “get up and running”, it may be a better choice to use a solution with native controllers to leave room for your project to scale up in complexity.
Disclaimer: This test was performed on libraries in their “as-is” state found on GitHub, with the existing code examples found for each board. There are definitely not optimized for speed, both libraries have room for improvement (filtering, interrupts) however the idea here is to illustrate the significant differences between an external peripheral and native peripherial.