Filter acceleration signal

frohr

Well-known member
Hi,
I have acceleration signal from adxl and ads1256. I send it to Python and it works well, I mean calculations as gRMS, Peak, mm/s, etc. But I want to calculate gRMS and Peak and mm/s with Teensy 4.0. g values seems ok but velocity mm/s is unstable like 0.5 - 2mm/s when sensor at rest. I need filter acceleration 500-10000Hz and velocity 10-1000Hz and have stable value. Any idea how to do it? Thanks.


My code:
Code:
#include <Arduino.h>
#include <SPI.h>

// PIN assignments
#define CS_PIN    21
#define DRDY_PIN  22
#define RST_PIN   8

// SPI settings
#define SPISPEED 1950000
SPISettings spiSettings(SPISPEED, MSBFIRST, SPI_MODE1);

// ADS1256 registers
#define STATUS_REG   0x00
#define MUX_REG      0x01
#define ADCON_REG    (0x02)
#define DRATE_REG    0x03

// ADS1256 commands
#define WAKEUP_CMD   0x00
#define RDATA_CMD    0x01
#define RREG_CMD     0x10
#define WREG_CMD     0x50
#define SELFCAL_CMD  0xF0
#define SYNC_CMD     0xFC
#define STANDBY_CMD  0xFD
#define RESET_CMD    0xFE

// Register values
#define STATUS_REG_VALUE   (0x01)  // MSB first, Auto-Cal Disabled, Input Buffer Disabled
#define ADCON_REG_VALUE    (0x21)  // Gain 2, Clock Out Freq = fCLKIN
#define DRATE_REG_VALUE    (0xF0)  // 30k SPS
#define MUX_REG_VALUE      (0x01)  // Differential AIN0 AIN1

// Constants
#define VREF                   (2.5f)    // Reference voltage
#define SENSITIVITY_CORRECTION (0.0111f) // Calibrated sensitivity correction
const uint32_t numSamples =  32768;  // 32k samples

float *raw_data;  // Use dynamic allocation to prevent stack overflow

// High-pass filter parameters
float EMA_a_low = 0.00199;  // 10Hz
float EMA_a_high = 0.666;   // 500Hz
float EMA_S_low = 0;
float EMA_S_high = 0;

// DRDY flag
volatile bool dataReady = false;

void setup() {
  Serial.begin(2000000);
  while (!Serial);  // Wait for Serial to initialize

  setupPins();
  resetADC();
  configureADC();

  attachInterrupt(digitalPinToInterrupt(DRDY_PIN), DRDY_Interrupt, FALLING);

  // Allocate memory for raw_data
  raw_data = (float *)malloc(numSamples * sizeof(float));
  if (raw_data == NULL) {
    Serial.println("Memory allocation failed!");
    while (1);  // Halt if memory allocation fails
  }
}

void loop() {
  readData();
  applyHighPassFilter();
  calculateGRMSandPeak();
  calculateVelocityRMS();
}

// Interrupt Service Routine for DRDY
void DRDY_Interrupt() {
  dataReady = true;
}

// Function to wait for DRDY signal with timeout
bool waitDRDY(uint32_t timeout_ms = 1000) {
  uint32_t startTime = millis();
  while (!dataReady) {
    if (millis() - startTime > timeout_ms) {
      return false;  // Timeout occurred
    }
    yield();  // Allow other processes to run
  }
  dataReady = false;
  return true;
}

// Function to read a single sample from the ADC
float READ_ADC() {
  int32_t adc_raw = 0;

  if (!waitDRDY()) {
    Serial.println("DRDY wait timeout!");
    return NAN;  // Return NaN to indicate an error
  }

  digitalWriteFast(CS_PIN, LOW);
  SPI.beginTransaction(spiSettings);

  SPI.transfer(RDATA_CMD);
  delayMicroseconds(5);  // Small delay to ensure ADC is ready

  // Read 24 bits of data
  adc_raw = SPI.transfer(0);
  adc_raw = (adc_raw << 8) | SPI.transfer(0);
  adc_raw = (adc_raw << 8) | SPI.transfer(0);

  SPI.endTransaction();
  digitalWriteFast(CS_PIN, HIGH);

  // Sign extension for 24-bit data
  if (adc_raw & 0x800000) {
    adc_raw |= 0xFF000000;
  }

  // Convert to voltage
  float voltage = ((float)adc_raw * VREF) / (1 << 23);
  voltage /= SENSITIVITY_CORRECTION;

  return voltage;
}

// Function to read multiple samples from the ADC
void readData() {
  for (uint32_t i = 0; i < numSamples; i++) {
    raw_data[i] = READ_ADC();
    if (isnan(raw_data[i])) {
      Serial.println("Error reading ADC data!");
      break;
    }
  }
}

// Function to apply a high-pass filter to the acceleration data
void applyHighPassFilter() {
  for (uint32_t i = 0; i < numSamples; i++) {
    float g = raw_data[i];
    EMA_S_low = (EMA_a_low * g) + ((1 - EMA_a_low) * EMA_S_low);
    EMA_S_high = (EMA_a_high * g) + ((1 - EMA_a_high) * EMA_S_high);
    raw_data[i] = EMA_S_high - EMA_S_low;
  }
}

// Function to calculate gRMS and Peak
void calculateGRMSandPeak() {
  float sum = 0.0;
  float peak = -INFINITY;
  float mean = 0.0;

  // Calculate mean
  for (uint32_t i = 0; i < numSamples; i++) {
    mean += raw_data[i];
  }
  mean /= numSamples;

  // Remove mean and calculate gRMS and Peak
  for (uint32_t i = 0; i < numSamples; i++) {
    raw_data[i] -= mean;
    sum += raw_data[i] * raw_data[i];
    if (fabs(raw_data[i]) > peak) {
      peak = fabs(raw_data[i]);
    }
  }

  float gRMS = sqrt(sum / numSamples);

  Serial.print("gRMS: ");
  Serial.println(gRMS, 5);
  Serial.print("Peak: ");
  Serial.println(peak, 5);
}

// Function to calculate velocity RMS from acceleration data
void calculateVelocityRMS() {
  float *velocity_data = (float *)malloc(numSamples * sizeof(float));
  if (velocity_data == NULL) {
    Serial.println("Memory allocation for velocity_data failed!");
    while (1);  // Halt if memory allocation fails
  }

  float dt = 1.0f / 31470.0f; // Time step based on sampling frequency (SPS_CORRECTION)

  // Remove mean from acceleration data before integration
  float mean_acc = 0.0;
  for (uint32_t i = 0; i < numSamples; i++) {
    mean_acc += raw_data[i];
  }
  mean_acc /= numSamples;

  for (uint32_t i = 0; i < numSamples; i++) {
    raw_data[i] -= mean_acc;
  }

  // Initialize velocity
  velocity_data[0] = 0.0;

  // Integrate raw acceleration to calculate velocity using the basic formula v = v0 + a * dt
  for (uint32_t i = 1; i < numSamples; i++) {
    velocity_data[i] = velocity_data[i - 1] + raw_data[i] * dt * 9.81f; // Convert to m/s
  }

  // Calculate mean of velocity
  float mean_velocity = 0.0;
  for (uint32_t i = 0; i < numSamples; i++) {
    mean_velocity += velocity_data[i];
  }
  mean_velocity /= numSamples;

  // Remove mean and calculate RMS of velocity
  float sum = 0.0;
  for (uint32_t i = 0; i < numSamples; i++) {
    velocity_data[i] -= mean_velocity;
    sum += velocity_data[i] * velocity_data[i];
  }

  float velocityRMS = sqrt(sum / numSamples) * 1000.0f; // Convert to mm/s

  Serial.print("Velocity RMS (mm/s): ");
  Serial.println(velocityRMS, 5);

  // Free allocated memory
  free(velocity_data);
}

// Function to set up pins
void setupPins() {
  pinMode(CS_PIN, OUTPUT);
  digitalWriteFast(CS_PIN, HIGH);
  pinMode(DRDY_PIN, INPUT);
  pinMode(RST_PIN, OUTPUT);
}

// Function to reset the ADC
void resetADC() {
  digitalWriteFast(RST_PIN, LOW);
  delayMicroseconds(10);
  digitalWriteFast(RST_PIN, HIGH);
  delay(5);
}

// Function to configure the ADC
void configureADC() {
  SPI.begin();
  SPI.beginTransaction(spiSettings);

  digitalWriteFast(CS_PIN, LOW);

  // Send RESET command
  SPI.transfer(RESET_CMD);
  delay(5);

  // Write to registers
  writeRegister(STATUS_REG, STATUS_REG_VALUE);
  writeRegister(ADCON_REG, ADCON_REG_VALUE);
  writeRegister(DRATE_REG, DRATE_REG_VALUE);
  writeRegister(MUX_REG, MUX_REG_VALUE);

  // Self-calibration
  SPI.transfer(SELFCAL_CMD);

  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();

  // Wait for calibration to complete
  waitDRDY(1000);
}

// Function to write to a register
void writeRegister(uint8_t address, uint8_t value) {
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(WREG_CMD | (address & 0x0F));
  SPI.transfer(0x00);  // Write to a single register
  SPI.transfer(value);
  digitalWriteFast(CS_PIN, HIGH);
  delayMicroseconds(10);  // Small delay to ensure the command is processed
}
 
If you have a code in Python which works for your needs than have a look at that and see what is different there? From where is all that code coming?

I would have assumed that the accuracy you get from such a adxl sensor board is not sufficient to get a really precise velocity calculation. Therefore I would think in this challenge there is more or less a compromise or some sort of workaround necessary.
 
I use adxl1005 on my custom board. According measurement compared with another analyzers is quite precise. In python I use np.FFT to filter frequency range and np.cumsum and np.hanning. Works well. But do it for Teensy is quite chalenging for me.
 
I think you overestimate the abilities of a MEMS accelerometer. Also velocity is the integral of acceleration, and integration has infinite gain at DC, so you will inevitably get drift and instability over time when measuring velocity with an accelerometer. 0.5 to 2 mm/s seems pretty good in fact (not surprizing as the ADXL1005 is good for a MEMS unit).
 
First, on the hardware:

So what's the circuit diagram in between the ADXL1005 and the ads1256, and the Teensy?
Did you consider the analog filters R1 C1 R2 C2 as in this from the MEMS datasheet? What are the values that you use?

1729679060668.png

Does your ads1256 use the same reference voltage (=VDD from the ADXL1005)? Same extra caps on the power rail? Or is all simply hooked to the same +3V3 Teensy4 power output?
And what about the ground wires? How long are they?

Second, on the software, it looks like you capture an array of samples in a brute force fashion, without consideration for equi-distant sampling. That gets you into trouble when the Teensy CPU gets interrupted for other tasks. Like dealing with USB etc. The time in between ADC samples then will be longer.

What speed does the T4 CPU run at? The faster, the more noisy. The more noise on power rails, the more noise in your acceleration and velocity signals.

The "delayMicroseconds(5);" that you use quite likely does not put the Teensy CPU into sleep or idle mode - so quite likely more noise from CPU into the analog signals...

You wrote that you are only interested in velocity up to 1 kHz and acceleration up to 10 kHz. But when you calculate RMS for these signals, you're doing it on the Raw ADC stream, which runs at ~5 us ish sampling rate, so 200 kHz ish?
For velocity, if you want it up to 1 kHz, then you need to filter out all >1 kHz signal content first. So a low pass filter. I don't see that in your code.
Much better would be to let that ADC sample at say 40 kHz, and have proper analog 20 kHz anti-alias filtering upstream of the ADC (so R1,C1,R2,C2) for that. Exploiting the programmable digital filter inside the ADS1256 is another option maybe.
 
First, on the hardware:

So what's the circuit diagram in between the ADXL1005 and the ads1256, and the Teensy?
Did you consider the analog filters R1 C1 R2 C2 as in this from the MEMS datasheet? What are the values that you use?

View attachment 36110
Does your ads1256 use the same reference voltage (=VDD from the ADXL1005)? Same extra caps on the power rail? Or is all simply hooked to the same +3V3 Teensy4 power output?
And what about the ground wires? How long are they?

Second, on the software, it looks like you capture an array of samples in a brute force fashion, without consideration for equi-distant sampling. That gets you into trouble when the Teensy CPU gets interrupted for other tasks. Like dealing with USB etc. The time in between ADC samples then will be longer.

What speed does the T4 CPU run at? The faster, the more noisy. The more noise on power rails, the more noise in your acceleration and velocity signals.

The "delayMicroseconds(5);" that you use quite likely does not put the Teensy CPU into sleep or idle mode - so quite likely more noise from CPU into the analog signals...

You wrote that you are only interested in velocity up to 1 kHz and acceleration up to 10 kHz. But when you calculate RMS for these signals, you're doing it on the Raw ADC stream, which runs at ~5 us ish sampling rate, so 200 kHz ish?
For velocity, if you want it up to 1 kHz, then you need to filter out all >1 kHz signal content first. So a low pass filter. I don't see that in your code.
Much better would be to let that ADC sample at say 40 kHz, and have proper analog 20 kHz anti-alias filtering upstream of the ADC (so R1,C1,R2,C2) for that. Exploiting the programmable digital filter inside the ADS1256 is another option maybe.
Maybe you can help me with code and show me how to do it? HW seems to be ok for me - pcb made by my friend (professional), noise is quite impossible to measure. I am pretty sure there is problem with software / how I collect data, as you mentioned. But how to improve?
 
Maybe start with printing raw_data and velocity_data samples, that gets you 32768 lines with floating points. As in:

for (uint32_t i = 0; i < numSamples; i++) {
Serial.printf (“%d %1.6f\n”, i, raw_data);
}

Do that immediately after reading all ADC samples.
And after each filtering step.

And finally, just before printing VelocityRMS,

for (uint32_t i = 0; i < numSamples; i++) {
Serial.printf (“%d %1.6f %1.6f\n”, i, raw_data, velocity_data);
}

That in essence is (should be) measuring noise already.
 
I don't know what you mean by "noise is quite impossible to measure" - how have you tried measuring it? The ADXL1005 specifies 75ug / √Hz, ie 0.735mm/s^2 / √Hz, which at 20mV/g sensitivity is 1.5µV/√Hz, which for a 10000Hz bandwidth is 0.15mV rms noise. That should be measurable.
 
I power adxl by 3V. Using vibration calibrator I found sensitivity 0.0111mV/g (to have correct amplitude size). I use clock 8Mhz and according calibrator I set 31450 SPS for calculations (to have correct frequency). I have to ask how he mesasure the noise. Lest forgot any filtering for now. What change do in my code to collect correct data? My main app is not filtering or calculating any data on Teensy. Just collect send via serial usb to PC. Calculations and plots are done in Python. I collect 32768 or 65536 samples and send in chunk size 2048.
 
I will start from scratch - no filtering, no calculations. Important is if basic code is ok and I do no mistakes how I collect data (theoretically assume the hardware is correct).
Do I have correct data and sampling and not increase noise somehow? What you mean?

Code:
#include <Arduino.h>
#include <SPI.h>

// PIN assignments
#define CS_PIN    21
#define DRDY_PIN  22
#define RST_PIN   8

// SPI settings
#define SPISPEED 1950000
SPISettings spiSettings(SPISPEED, MSBFIRST, SPI_MODE1);

// ADS1256 registers
#define STATUS_REG   0x00
#define MUX_REG      0x01
#define ADCON_REG    0x02
#define DRATE_REG    0x03

// ADS1256 commands
#define WAKEUP_CMD   0x00
#define RDATA_CMD    0x01
#define RREG_CMD     0x10
#define WREG_CMD     0x50
#define SELFCAL_CMD  0xF0
#define SYNC_CMD     0xFC
#define STANDBY_CMD  0xFD
#define RESET_CMD    0xFE

// Register values
#define STATUS_REG_VALUE   (0x01)  // MSB first, Auto-Cal Disabled, Input Buffer Disabled
#define ADCON_REG_VALUE    (0x21)  // Gain 2, Clock Out Freq = fCLKIN
#define DRATE_REG_VALUE    (0xF0)  // 30k SPS
#define MUX_REG_VALUE      (0x01)  // Differential AIN0 AIN1

// Constants
#define VREF                   (2.5f)    // Reference voltage
#define SENSITIVITY_CORRECTION (0.0111f) // Calibrated sensitivity correction
#define SPS_CORRECTION         (31470)   // Calibrated frequency correction (8MHz crystal)

// Define maximum number of samples
#define MAX_SAMPLES 65536
float raw_data[MAX_SAMPLES];  // Statically allocated array
uint32_t numSamples = 32768;  // Initial number of samples

bool sendData = false;

// DRDY flag
volatile bool dataReady = false;

void setup() {
  Serial.begin(2000000);
  while (!Serial);  // Wait for Serial to initialize

  setupPins();
  resetADC();
  configureADC();
  attachInterrupt(digitalPinToInterrupt(DRDY_PIN), DRDY_Interrupt, FALLING);
}

void loop() {
  handleSerialCommands();

  if (sendData) {
    sendDataOverSerial();
    sendData = false;
  }
}

// Interrupt Service Routine for DRDY
void DRDY_Interrupt() {
  dataReady = true;
}

// Function to wait for DRDY signal with timeout
bool waitDRDY(uint32_t timeout_ms = 1000) {
  uint32_t startTime = millis();
  while (!dataReady) {
    if (millis() - startTime > timeout_ms) {
      return false;  // Timeout occurred
    }
    yield();  // Allow other processes to run
  }
  dataReady = false;
  return true;
}

// Function to read a single sample from the ADC
float READ_ADC() {
  int32_t adc_raw = 0;

  if (!waitDRDY()) {
    Serial.println("DRDY wait timeout!");
    return NAN;  // Return NaN to indicate an error
  }

  SPI.beginTransaction(spiSettings);
  digitalWriteFast(CS_PIN, LOW);

  SPI.transfer(RDATA_CMD);
  delayMicroseconds(5);  // Small delay to ensure ADC is ready

  // Read 24 bits of data
  adc_raw = SPI.transfer(0);
  adc_raw = (adc_raw << 8) | SPI.transfer(0);
  adc_raw = (adc_raw << 8) | SPI.transfer(0);

  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();

  // Sign extension for 24-bit data
  if (adc_raw & 0x800000) {
    adc_raw |= 0xFF000000;
  }

  // Convert to voltage
  float voltage = ((float)adc_raw * VREF) / (1 << 23);
  g = voltage / SENSITIVITY_CORRECTION;

  return g;
}

// Function to read multiple samples from the ADC
void ReadData() {
  Serial.println("Starting data acquisition...");
  for (uint32_t i = 0; i < numSamples; i++) {
    raw_data[i] = READ_ADC();
    if (isnan(raw_data[i]) || isinf(raw_data[i])) {
      Serial.println("Invalid ADC data encountered! Skipping...");
      raw_data[i] = 0;  // Replace invalid data with zero or another default value
    }
  }
  Serial.println("Data acquisition completed.");
}

// Function to handle incoming serial commands
void handleSerialCommands() {
  if (Serial.available()) {
    char command = Serial.read();

    switch (command) {
      case 'C':
        ReadData();
        sendData = true;
        break;


      case '1':
        numSamples = 32768;
        break;

      case '2':
        numSamples = 65536;
        break;

      default:
        break;
    }
  }
}

// Function to send data over Serial
void sendDataOverSerial() {
  flushSerialBuffers();  // Flush buffers before sending new data

  uint32_t totalBytes = numSamples * sizeof(float);

  // Send data length
  Serial.write((uint8_t *)&totalBytes, sizeof(totalBytes));

  // Send data in chunks
  const uint16_t chunkSize = 2048;  // Adjust chunk size if necessary
  uint32_t bytesSent = 0;
  while (bytesSent < totalBytes) {
    uint16_t bytesToSend = min(chunkSize, totalBytes - bytesSent);

    // Wait if necessary for serial buffer to be ready
    while (Serial.availableForWrite() < bytesToSend) {
      delay(1);
    }

    Serial.write((uint8_t *)(raw_data) + bytesSent, bytesToSend);
    bytesSent += bytesToSend;

    //delay(1);  // Small delay to allow receiver to process data
  }

}

// Function to flush serial buffers
void flushSerialBuffers() {
  // Clear any data in the serial buffers
  while (Serial.available()) {
    Serial.read();  // Read and discard data from the input buffer
  }
  Serial.flush();  // Wait for any outgoing data to complete
}

// Function to set up pins
void setupPins() {
  pinMode(CS_PIN, OUTPUT);
  digitalWriteFast(CS_PIN, HIGH);
  pinMode(DRDY_PIN, INPUT);
  pinMode(RST_PIN, OUTPUT);
  digitalWriteFast(RST_PIN, HIGH);  // Ensure RST_PIN is high
}

// Function to reset the ADC
void resetADC() {
  digitalWriteFast(RST_PIN, LOW);
  delayMicroseconds(10);
  digitalWriteFast(RST_PIN, HIGH);
  delay(5);
}

// Function to configure the ADC
void configureADC() {
  SPI.begin();

  // Send RESET command
  SPI.beginTransaction(spiSettings);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(RESET_CMD);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();

  // Wait for ADC to reset
  delay(10);  // Wait 10ms

  // Wait for DRDY to go low
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("ADC reset timeout!");
  }

  // Write to registers, waiting for DRDY after each
  writeRegister(STATUS_REG, STATUS_REG_VALUE);
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("STATUS_REG write timeout!");
  }

  writeRegister(ADCON_REG, ADCON_REG_VALUE);
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("ADCON_REG write timeout!");
  }

  writeRegister(DRATE_REG, DRATE_REG_VALUE);
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("DRATE_REG write timeout!");
  }

  writeRegister(MUX_REG, MUX_REG_VALUE);
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("MUX_REG write timeout!");
  }

  // Self-calibration
  SPI.beginTransaction(spiSettings);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(SELFCAL_CMD);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();

  // Wait for calibration to complete
  if (!waitForDRDYCompletion(1000)) {
    Serial.println("ADC calibration timeout!");
  }
}

// Function to write to a register
void writeRegister(uint8_t address, uint8_t value) {
  SPI.beginTransaction(spiSettings);
  digitalWriteFast(CS_PIN, LOW);
  SPI.transfer(WREG_CMD | (address & 0x0F));
  SPI.transfer(0x00);  // Write to a single register
  SPI.transfer(value);
  digitalWriteFast(CS_PIN, HIGH);
  SPI.endTransaction();
  // Wait for command to be processed
  delayMicroseconds(10);
}

// Function to wait for DRDY completion with timeout
bool waitForDRDYCompletion(uint32_t timeout_ms) {
  uint32_t startTime = millis();
  while (digitalReadFast(DRDY_PIN) == HIGH) {
    if (millis() - startTime > timeout_ms) {
      return false;  // Timeout occurred
    }
    yield();
  }
  return true;
}
 
Back
Top