FlexCAN_T4 Receive timing.

samm_flynn

Active member
Hi everyone,

First time posting here. I apologize in advance if I have broken any rules unknowingly.

I’m using a Teensy 4.1 with two CAN transceivers for a project.
CAN0 TX is connected to pin 22, RX to pin 23,
while CAN1 TX is on pin 1 and RX on pin 0.
Both are set to 500 kbps using the FlexCAN_T4 library.
C-like:
#include <FlexCAN_T4.h>
#include <motctrl_prot.h>
#include <cstring>
#include <IntervalTimer.h>
FlexCAN_T4<CAN1, RX_SIZE_8, TX_SIZE_8> Can0;
FlexCAN_T4<CAN2, RX_SIZE_8, TX_SIZE_8> Can1;
IntervalTimer sendTimer;
volatile uint32_t can0WriteTime = 0;
volatile uint32_t can1WriteTime = 0;
void canSniff0(const CAN_message_t &msg) {
  uint32_t delaySinceCan1Write = micros() - can1WriteTime;
  Serial.print("CAN0 received after CAN1.write: ");
  Serial.print(delaySinceCan1Write);
  Serial.println(" us");
}
void canSniff1(const CAN_message_t &msg) {
  uint32_t delaySinceCan0Write = micros() - can0WriteTime;
  Serial.print("CAN1 received after CAN0.write: ");
  Serial.print(delaySinceCan0Write);
  Serial.println(" us");
  uint32_t start = micros();
  CAN_message_t reply;
  reply.id = 0x0;
  reply.len = 8;
  uint8_t buf[8] = {0};
  MCReqStartMotor(buf);
  memcpy(reply.buf, buf, sizeof(buf));
  Can1.write(reply);
  uint32_t duration = micros() - start;
  Serial.print("CAN1 write duration: ");
  Serial.print(duration);
  Serial.println(" us");
  can1WriteTime = start;
}
void sendCANMessages() {
  uint32_t start = micros();
  CAN_message_t msg;
  msg.id = 0x1;
  msg.len = 8;
  uint8_t buf[8] = {0};
  MCReqStopMotor(buf);
  memcpy(msg.buf, buf, sizeof(buf));
  Can0.write(msg);
  uint32_t duration = micros() - start;
  Serial.print("CAN0 write duration: ");
  Serial.print(duration);
  Serial.println(" us");
  can0WriteTime = start;
}
void setup() {
  Serial.begin(115200);
  delay(1000);
  Can0.begin();
  Can0.setBaudRate(500000);
  Can0.setMaxMB(16);
  Can0.enableFIFO();
  Can0.enableFIFOInterrupt();
  Can0.onReceive(canSniff0);
  Can1.begin();
  Can1.setBaudRate(500000);
  Can1.setMaxMB(16);
  Can1.enableFIFO();
  Can1.enableFIFOInterrupt();
  Can1.onReceive(canSniff1);
  sendTimer.begin(sendCANMessages, 1000*1000);// 1s = 1 millon microseconds
}
void loop() {
  Can0.events();
  Can1.events();
}
Every second, CAN0 sends an 8-byte message. When CAN1 receives it, it triggers the canSniff1 function, which immediately sends a response back to CAN0. When CAN0 receives this reply, it logs the timing information. The goal is to measure how long it takes for messages to be received and processed.

The write times are really good, around 5 microseconds, but the reception time is much slower than I expected, around 250 ± 50 microseconds. My actual motor driver responds within 50 microseconds, and I confirmed that using an oscilloscope. I also tested an MCP2515, which took around 350 microseconds to transmit but showed receive times between 70 and 100 microseconds.

My main question , Is this normal receive timing for the FlexCAN_T4 library, or am I doing something wrong(my money is on I am doing something wrong)? Is there a way to optimize this and reduce the receive latency? I also don’t fully understand how FIFO vs. mailboxes impact performance in this library. My setup is copied from a GitHub example.

Here is an how I intend to use the code -
C-like:
#include <FlexCAN_T4.h>
#include <math.h>
#include <motctrl_prot.h>
#include <IntervalTimer.h>

const int LED_PIN = 13;
const uint32_t CAN_ID = 0x01;  // Motor controller CAN ID

const int update_interval_us = 1000;  // 1 ms update rate


IntervalTimer motor_timer;

volatile bool armed = false;
volatile bool send_flag = false;
volatile int8_t temp = 0;
volatile float theta = 0;
volatile float omega = 0;
volatile float torque = 0;
volatile int canmsg_count = 0;
volatile float cmd_vel = 0.0;  // Command velocity (rad/s)


String inputString = "";
bool commandReceived = false;

FlexCAN_T4<CAN2, RX_SIZE_8, TX_SIZE_8> Can0;

volatile uint32_t t_send = 0;
volatile uint32_t t_recv = 0;
volatile uint8_t lastCmdType = 0;

void canRxHandler(const CAN_message_t &msg);
void sendCmd();
void processCommand(String command);
void startMotor();
void stopMotor();
void setVelocity(float radPerSec);

typedef enum
{
  MOTCTRL_CMD_START_MOTOR = 0x91,
  MOTCTRL_CMD_STOP_MOTOR = 0x92,
  MOTCTRL_CMD_TORQUE_CONTROL = 0x93,
  MOTCTRL_CMD_SPEED_CONTROL = 0x94,
  MOTCTRL_CMD_POSITION_CONTROL = 0x95,
  MOTCTRL_CMD_STOP_CONTROL = 0x97,
} MOTCTRL_CMD;

void setup()
{
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);

  Serial.begin(115200);
  while (!Serial)
  {
    delay(1000);
  }
  Serial.println("<Ready>");
  digitalWrite(LED_PIN, LOW);

  Can0.begin();
  Can0.setBaudRate(500000);
  Can0.enableFIFO();
  Can0.enableFIFOInterrupt();
  Can0.onReceive(canRxHandler);
  Serial.println("CAN Initialization Complete.");
  delay(1000);
  stopMotor();

  motor_timer.begin(sendCmd, update_interval_us);
}

void loop()
{
  Can0.events();
}

void serialEvent()
{
  while (Serial.available() > 0)
  {
    char receivedChar = Serial.read();
    if (receivedChar == '\n')
    {
      processCommand(inputString);
      inputString = "";
    }
    else
    {
      inputString += receivedChar;
    }
  }
}

void processCommand(String command)
{
  command.trim();

  if (command.equalsIgnoreCase("arm"))
  {
    Serial.println("arm");
    startMotor();
    commandReceived = true;
  }
  else if (command.equalsIgnoreCase("stop"))
  {
    Serial.println("stop");
    stopMotor();
    commandReceived = true;
  }
  else if (command.startsWith("vel:"))
  {
    String floatPart = command.substring(4);
    cmd_vel = floatPart.toFloat();
    Serial.print("CMD_VEL : ");
    Serial.println(cmd_vel, 6);
    commandReceived = true;
  }
  else
  {
    Serial.println("Invalid command. Please enter 'arm', 'stop', or 'vel:<value>'.");
  }
}

void startMotor()
{
  t_send = micros();

  send_flag = true;
  uint8_t buf[8];
  MCReqStartMotor(buf);
  lastCmdType = MOTCTRL_CMD_START_MOTOR;

  CAN_message_t msg;
  msg.id = CAN_ID;
  msg.len = MOTCTRL_FRAME_SIZE;
  memcpy(msg.buf, buf, MOTCTRL_FRAME_SIZE);
  Can0.write(msg);

  Serial.println("<Start Request>");
  Serial.print("Start-> DT:");
  Serial.println(micros() - t_send);
}

void stopMotor()
{
  t_send = micros();

  send_flag = false;
  uint8_t buf[8];
  MCReqStopMotor(buf);
  lastCmdType = MOTCTRL_CMD_STOP_MOTOR;

  CAN_message_t msg;
  msg.id = CAN_ID;
  msg.len = MOTCTRL_FRAME_SIZE;
  memcpy(msg.buf, buf, MOTCTRL_FRAME_SIZE);
  Can0.write(msg);

  Serial.println("<Stop Request>");
  Serial.print("Stop-> DT:");
  Serial.println(micros() - t_send);
}

void setVelocity(float radPerSec)
{
  t_send = micros();

  uint8_t CMD_buf[8];
  MCReqSpeedControl(CMD_buf, radPerSec * (30.0 / M_PI), 0);
  lastCmdType = MOTCTRL_CMD_SPEED_CONTROL;

  CAN_message_t msg;
  msg.id = CAN_ID;
  msg.len = MOTCTRL_FRAME_SIZE;
  memcpy(msg.buf, CMD_buf, MOTCTRL_FRAME_SIZE);
  Can0.write(msg);

  Serial.print("CMD_VEL-> DT:");
  Serial.println(micros() - t_send);
}

void canRxHandler(const CAN_message_t &msg)
{
  canmsg_count++;
  uint8_t buf[8];
  memcpy(buf, msg.buf, msg.len);

  if (buf[1] == MOTCTRL_RES_SUCCESS)
  {
    switch (buf[0])
    {
      case MOTCTRL_CMD_SPEED_CONTROL:
        {
          float p, s, t;
          int8_t tmp;
          MCResSpeedControl(buf, &tmp, &p, &s, &t);
          theta = p;
          omega = s;
          torque = t;
          temp = tmp;

          if (canmsg_count % 10 == 0)
          {
            Serial.print(" ID: 0x0");
            Serial.print(msg.id, HEX);
            Serial.print(". ω : ");
            Serial.println(fabs(omega), 6);

            if (t_send != 0 && lastCmdType == MOTCTRL_CMD_SPEED_CONTROL)
            {
              t_recv = micros();
              uint32_t latency = t_recv - t_send;
              Serial.print("Round-trip latency for speed control: ");
              Serial.print(latency);
              Serial.println(" us");
              t_send = 0;
            }
          }
          break;
        }
      case MOTCTRL_CMD_START_MOTOR:
        {
          armed = true;
          digitalWrite(LED_PIN, HIGH);
          Serial.print(" ID: 0x0");
          Serial.print(msg.id, HEX);
          Serial.println(" . <ENABLED>");

          if (t_send != 0 && lastCmdType == MOTCTRL_CMD_START_MOTOR)
          {
            t_recv = micros();
            uint32_t latency = t_recv - t_send;
            Serial.print("Round-trip latency for start motor: ");
            Serial.print(latency);
            Serial.println(" us");
            t_send = 0;
          }
          break;
        }
      case MOTCTRL_CMD_STOP_MOTOR:
        {
          armed = false;
          digitalWrite(LED_PIN, LOW);
          Serial.print(" ID: 0x0");
          Serial.print(msg.id, HEX);
          Serial.println(" . <DISABLED>");

          if (t_send != 0 && lastCmdType == MOTCTRL_CMD_STOP_MOTOR)
          {
            t_recv = micros();
            uint32_t latency = t_recv - t_send;
            Serial.print("Round-trip latency for stop motor: ");
            Serial.print(latency);
            Serial.println(" us");
            t_send = 0;
          }
          break;
        }
      default:
        Serial.println("UNKNOWN_CMD");
        break;
    }
  }
  else
  {
    Serial.println("FAIL");
  }
}

void sendCmd()
{
  if (send_flag && armed)
  {
    setVelocity(cmd_vel);
  }
}


Any advice on optimizing CAN receive times or improving the way I handle messages would be greatly appreciated. Also, if anyone has insights into the best way to set up FIFO or mailboxes for low-latency CAN reception, I’d love to learn more. Thanks in advance.
Please feel free to ask for more clarification if required.
 
The standard CAN "interrupt" logic follows the Arduino model, it's called as user level code when you call the .events() method. This is good in that it means it's harder to create code that locks up and avoids the gotchas that come with true interrupt level code but bad if you want best possible timing.

in <FlexCAN_T4.h> there are 3 functions defined:

extern void ext_output1(const CAN_message_t &msg);
extern void ext_output2(const CAN_message_t &msg);
extern void ext_output3(const CAN_message_t &msg);

These are called by the underlying CAN driver as part if it's handling of the CAN hardware interrupt.
They default to doing nothing but only have a weak binding which means if you define these functions in your code it will override the default and call your function.

Or the TL: DR version:
add the code
Code:
volatile uint32_t CAN_RxTime;
void ext_output1(const CAN_message_t &msg) {
CAN_RxTime  = micros();
}

and you'll get the actual Rx time of the message stored in a variable you can read in the rest of your code.

You could do more in that function but I wouldn't put all of your current canRxHandler code in the interrupt, the code contains printf commands, and calling printf from inside an interrupt context is generally a bad idea.
 
@AndyA,

Many thanks for answering my question. Really appreciate it.

Follow up question,

If you are using two can transceivers, what happens then?
from the docs i see that -
"
In the background, the library has 3 weak functions used for interrupt driven frames, that can be used by other libraries to gather data for their own functionality.An example of this is TeensyCAN, which uses one of the 3 weak functions, leaving 2 available for other libraries, if needed.

C-like:
extern void ext_output1(const CAN_message_t &msg); // Interrupt data output, not filtered, for external libraries, CAN2.0
extern void ext_output2(const CAN_message_t &msg);
extern void ext_output3(const CAN_message_t &msg); // TeensyCAN uses this one.
"
since I am planning to drive two separate can bus on the same teensy.
 
@AndyA,

Many thanks for answering my question. Really appreciate it.

Follow up question,

If you are using two can transceivers, what happens then?
from the docs i see that -
"
In the background, the library has 3 weak functions used for interrupt driven frames, that can be used by other libraries to gather data for their own functionality.An example of this is TeensyCAN, which uses one of the 3 weak functions, leaving 2 available for other libraries, if needed.

C-like:
extern void ext_output1(const CAN_message_t &msg); // Interrupt data output, not filtered, for external libraries, CAN2.0
extern void ext_output2(const CAN_message_t &msg);
extern void ext_output3(const CAN_message_t &msg); // TeensyCAN uses this one.
"
since I am planning to drive two separate can bus on the same teensy.
I just noticed about the bus field in CAN_message_t struct
C-like:
typedef struct CAN_message_t {
  uint32_t id = 0;          // can identifier
  uint16_t timestamp = 0;   // FlexCAN time when message arrived
  uint8_t idhit = 0; // filter that id came from
  struct {
    bool extended = 0; // identifier is extended (29-bit)
    bool remote = 0;  // remote transmission request packet type
    bool overrun = 0; // message overrun
    bool reserved = 0;
  } flags;
  uint8_t len = 8;      // length of data
  uint8_t buf[8] = { 0 };       // data
  int8_t mb = 0;       // used to identify mailbox reception
  uint8_t bus = 0;      // used to identify where the message came from when events() is used.
  bool seq = 0;         // sequential frames
} CAN_message_t;

I will try that
 
Just dropping in to say: CAN bus messages take time to send and receive. This sounds obvious but I'm just reminding you that transmission takes time. At the wire it takes 2 microseconds to send each bit and a CAN message with 8 data bytes could be upwards of 112 bits on the wire. 112 * 2 gets pretty close to a quarter of a millisecond. So, be sure you're allowing for this. It takes 200 some microseconds to send and that long to receive too (though they nearly directly overlap). So, the latency is expected to be around 200-250us. This is normal. Thus if you measure from the time you start to send to the time you receive a message on the other channel, it should be 250us or so.
 
Just dropping in to say: CAN bus messages take time to send and receive.
And if you look close enough you will see differences in time for two packets of the same length. CAN performs bit stuffing, if there are more than a certain number of 0's (5?) in a row then the hardware inserts an extra 1 into the packet. This means packet transmission time will depend a little on the ID and data being transmitted.
 
thanks everyone, I measured the times with a scope and you @CollinK and @AndyA you were right, considering bit stuffing the timing is actually spot on, I tried with a multiple baudrates.
Thanks to @AndyA for showing me how to use the ext_output1 function. My main concern was If I had wrote the code in a sub optimal way, it looks like, this is as good as it gets. I need to play with the baudrates if I want faster timing.
I am just beyond thankful for getting help on this matter.
Many thanks to you both. God Bless and have a great day!
 
If you need faster data over CAN your options are somewhat limited. Higher baud rates are an obvious quick win. For lots of data you can use CAN FD. If you only have a very small amount of data then depending on the environment you can sometimes use the ID being sent as a means of conveying information and so reduce or even completely skip the data portion of the packet.
 
Back
Top