Best way to manage shared data between CAN, EMA filtering and SD logging with interrupts?

MOCmaniac

Member
Hello everyone,

As part of my thesis on electric bikes, I’m using a Teensy 4.1 (and arduino IDE on windows) to collect and process data from a large brushless motor controlled by an ODrive Pro. Communication with the ODrive and other components is done via CAN, and this setup cannot be changed.

I log 15 float values along with a timestamp to an SD card at 1 kHz, using CSV format. I know binary logging would be more efficient, but I’ve already achieved up to 20 kHz in CSV without issues, so I currently have enough performance margin. In parallel, I apply an Exponential Moving Average (EMA) filter to each of the 15 values, each with its own cutoff frequency.

So far, everything is working as expected: CAN communication, logging, filtering, and general data handling are functional. However, the timing between consecutive log entries or filter applications is not as regular as I would like. The average loop execution time is around 300 to 600 µs, but there is noticeable jitter in the actual timing of logs and filter updates.

To improve timing consistency, I recently moved the filtering to a timer interrupt. This makes the filter application more regular, but introduces a new problem: accessing and modifying shared data between the main loop, CAN reception, and the timer interrupt. I am currently disabling interrupts whenever I copy or update shared values to avoid race conditions, but this approach quickly becomes complex and hard to maintain, especially as the codebase grows.

I used an array of pointers to the float variables I wanted to log and filter (dataPtr). I also have a array with the headers, with the cut-off frequency and associated alphas. This made it easy to loop through the data for both operations. To improve structure and flexibility, I recently thought about creating a struct for each variable, like this

C++:
struct dataStruct {
  const char* header; // Used in the CSV
  float rawvalue; // Usually updated by the CAN receive function
  float filteredValue; // Calculated with the EMA
  unsigned long cutOffFreq;
  float alpha;
};

All the variables would be stored in an array of these structs, which I can iterate over for filtering and logging.

This setup is clean in theory, but I'm still facing the core issue: how to manage shared access to these values across interrupts and the main loop, without having to disable and enable interrupts constantly or having data duplicates everywhere. I also plan to move the logger into a timer interrupt, which will add further pressure on data access safety.

I’m looking for advice or examples of best practices to manage shared float variables between the main loop, CAN handlers, and multiple interrupts, in a way that remains efficient, safe, and maintainable. I suspect there are better approaches or patterns for handling this kind of situation that I’m not aware of. You can find the code linked to this problem in the attached files.

There are probably other things I could improve in the code, but for now it works. Once I have a solution to this problem, I can start improving the code, and any advice would be welcome for this part as well!

Thanks in advance for your help,
Arnaud
 

Attachments

  • CAN.ino
    7.5 KB · Views: 85
  • Filter.ino
    1.2 KB · Views: 83
  • Logger.ino
    5.8 KB · Views: 82
  • Main.ino
    26.8 KB · Views: 85
I log 15 float values along with a timestamp to an SD card at 1 kHz, using CSV format. I know binary logging would be more efficient, but I’ve already achieved up to 20 kHz in CSV without issues, so I currently have enough performance margin. In parallel, I apply an Exponential Moving Average (EMA) filter to each of the 15 values, each with its own cutoff frequency.

So far, everything is working as expected: CAN communication, logging, filtering, and general data handling are functional. However, the timing between consecutive log entries or filter applications is not as regular as I would like. The average loop execution time is around 300 to 600 µs, but there is noticeable jitter in the actual timing of logs and filter updates.

To improve timing consistency, I recently moved the filtering to a timer interrupt. This makes the filter application more regular, but introduces a new problem: accessing and modifying shared data between the main loop, CAN reception, and the timer interrupt. I am currently disabling interrupts whenever I copy or update shared values to avoid race conditions, but this approach quickly becomes complex and hard to maintain, especially as the codebase grows.

Isn't it the timing of the data collection that matters? As long as you are reading data via CAN every 1 ms as precisely as possible, and you are completing the filtering and logging within the 1-ms data cycle, jitter in the filtering and logging will have no effect on the values written to the log file. I don't see any point in executing the filters in a timer ISR, or any need to share data between ISR and loop(). It seems like you could do this entirely in loop()
  • wait for 1-ms boundary
  • read
  • filter
  • log
 
Indeed, I may have overthought this. I should check that this is not a problem because I am receiving CAN messages randomly. (I set a frequency at which I would like to receive them on the odrive and it tries to stick to it but it is not very regular). I could then simply use a flag to tell me if my loop() is running in less than 1ms. If it is not the case I will have to find a solution otherwise everything is good I suppose
 
Indeed, I may have overthought this. I should check that this is not a problem because I am receiving CAN messages randomly. (I set a frequency at which I would like to receive them on the odrive and it tries to stick to it but it is not very regular). I could then simply use a flag to tell me if my loop() is running in less than 1ms. If it is not the case I will have to find a solution otherwise everything is good I suppose
I wanted to mention that the way you’re doing the SD logging is great. Did you start from the TeensySDIOLogger example? I always point to that as best practice for logging to SD.
 
I wanted to mention that the way you’re doing the SD logging is great. Did you start from the TeensySDIOLogger example? I always point to that as best practice for logging to SD.
Yes exactly ! I spent a few days gathering informations on the best practices to log on a SD card, especially at high rates and finally based my code on this example and it's working like a charm !
 
Yes exactly ! I spent a few days gathering informations on the best practices to log on a SD card, especially at high rates and finally based my code on this example and it's working like a charm !
Yes, I’ve done a lot of benchmarking on this, and as long as your data rate is well below the max of about 20 MB/s, calls to write() return in less than 5 us, which is fantastic. A number that’s easy to remember is 1 MB/s with only 1% of CPU. My applications have always been much lower data rate than that.
 
Yes, I’ve done a lot of benchmarking on this, and as long as your data rate is well below the max of about 20 MB/s, calls to write() return in less than 5 us, which is fantastic. A number that’s easy to remember is 1 MB/s with only 1% of CPU. My applications have always been much lower data rate than that.
I don't remember reading these values in the examples but it's really interesting to keep in mind ! 1MB/s would give approximately a sampling rate of 6.5kHz with the timestamp (%10lu) and the 15 floats (%09.4f) which is more than enough for me.

I'm also wondering if it is possible to make my setupLogging() and finishLogging() function any faster ? With my debug I see that it usually takes 20 to 30ms to start and 10ms to stop the log.
 
Last edited:
I don't remember reading these values in the examples but it's really interesting to keep in mind ! 1MB/s would give approximately a sampling rate of 6.5kHz with the timestamp (%10lu) and the 15 floats (%09.4f) which is more than enough for me.
Those are my own measurements made in programs based on the same logic from TeensySDIOLogger.
 
Those are my own measurements made in programs based on the same logic from TeensySDIOLogger.
I edited my message exactly when you answered to the original one.

I'm also wondering if it is possible to make my setupLogging() and finishLogging() function any faster ? With my debug I see that it usually takes 20 to 30ms to start and 10ms to stop the log. It freezes my filter and the communication and it would be nice if I could improve that. Perhaps splitting the start/stop functions in multiple steps ?
 
preAllocate() takes a while to run. I don't know of any way to reduce it, so I think you just have to accept that there is a short period of preparation before logging begins.
 
Back
Top