Reading 2 analog inputs at 1kHz: interrupts/timed loop?

Hello!

I'm trying to read 2 analog inputs at 1kHz, and send them (+ the time between reads) over serial to a python script. I got it working, but can't get the time to correspond to 1 ms. Board has a SAMD51 chip.

I tried with a timed loop, which didn't give me the expected results. Then I tried with the SAMDTimer library example, by sampling with an interrupt, and then send the data over serial, which didn't work either. Can someone point me in the right direction?

What works, but at a 2ms (commented out) or 5-10ms at the moment:

unsigned long t0 = 0;
long loopTime = 1000;   // microseconds = 1ms loop

float tmp1, tmp2 = 0;   // variables at conversion
float val1, val2 = 0;   // variables after filtering

float timer = 0;

unsigned long sampleTimer = 0;
unsigned long sampleInterval = 1000; //100 us = 100Hz rate

void setup() {
  Serial.begin(115200);     // baudrate
  analogReadResolution(12); // ADC resolution
}

void loop() {
  //  unsigned long tnow;
  //  do
  //    tnow = micros();
  //  while ( (tnow - t0) < 1000 );  // waits until time has come
  //  t0 += 1000;
  //  tmp1 = analogRead(A0) * 3.3 / 4095.0;     // convert reading into volts
  //  tmp2 = analogRead(A1) * 3.3 / 4095.0;
  //  val1 = 0.9875 * val1 + 0.01249 * tmp1;    // filter variables
  //  val2 = 0.9875 * val2 + 0.01249 * tmp2;
  //  sendToPC(&val1, &val2, &tnow);


  unsigned long currMicros = micros();
  if (currMicros - sampleTimer >= sampleInterval) // is it time for a sample?
  {
    sampleTimer = currMicros;
    timer = (float)sampleTimer;
    tmp1 = analogRead(A0) * 3.3 / 4095.0;     // convert reading into volts
    tmp2 = analogRead(A1) * 3.3 / 4095.0;
    val1 = 0.9875 * val1 + 0.01249 * tmp1;    // filter variables
    val2 = 0.9875 * val2 + 0.01249 * tmp2;
    sendToPC(&val1, &val2, &timer);
  }
}

void sendToPC(float* data1, float* data2, float* data3)
{
  byte* byteData1 = (byte*)(data1);
  byte* byteData2 = (byte*)(data2);
  byte* byteData3 = (byte*)(data3);
  byte buf[12] = {byteData1[0], byteData1[1], byteData1[2], byteData1[3],
                  byteData2[0], byteData2[1], byteData2[2], byteData2[3],
                  byteData3[0], byteData3[1], byteData3[2], byteData3[3]
                 };
  Serial.write(buf, 12);
}

The code using an ISR:

#include "SAMDTimerInterrupt.h"

#define TIMER0_INTERVAL_MS 1

volatile uint32_t preMillisTimer0 = 0;
volatile uint32_t preMillisTimer1 = 0;

volatile float tmp1, tmp2 = 0;   // variables at conversion
volatile float val1, val2 = 0;   // variables after filtering

// SAMD51 Hardware Timer only TC3
SAMDTimer ITimer0(TIMER_TC3);


void TimerHandler0() {
  tmp1 = analogRead(A0) * 3.3 / 4095.0;     // convert reading into volts
  tmp2 = analogRead(A1) * 3.3 / 4095.0;
  val1 = 0.9875 * val1 + 0.01249 * tmp1;    // filter variables
  val2 = 0.9875 * val2 + 0.01249 * tmp2;
 }

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); // ADC resolution
  while (!Serial);
  delay(100);

  Serial.print(F("\nStarting TimerInterruptTest on ")); Serial.println(BOARD_NAME);
  Serial.println(SAMD_TIMER_INTERRUPT_VERSION);
  Serial.print(F("CPU Frequency = ")); Serial.print(F_CPU / 1000000); Serial.println(F(" MHz"));

  // Interval in microsecs
  if (ITimer0.attachInterruptInterval(TIMER0_INTERVAL_MS * 1000, TimerHandler0)) {
    preMillisTimer0 = millis();
    Serial.print(F("Starting ITimer0 OK, millis() = ")); Serial.println(preMillisTimer0);
  }
  else
    Serial.println(F("Can't set ITimer0. Select another freq. or timer"));
}


void loop() {
  static unsigned long lastTimer0   = 0; 
  if (millis() - lastTimer0 > TIMER0_INTERVAL_MS) {
    lastTimer0 = millis();

    preMillisTimer0 = millis();
    // Serial.print(F("Start ITimer0, millis() = ")); Serial.println(preMillisTimer0);
    ITimer0.restartTimer();
    sendToPC(&val1, &val2, &preMillisTimer0);
  }

}

void sendToPC(volatile float* data1, volatile float* data2, volatile uint32_t* data3)
{
  byte* byteData1 = (byte*)(data1);
  byte* byteData2 = (byte*)(data2);
  byte* byteData3 = (byte*)(data3);
  byte buf[12] = {byteData1[0], byteData1[1], byteData1[2], byteData1[3],
                  byteData2[0], byteData2[1], byteData2[2], byteData2[3],
                  byteData3[0], byteData3[1], byteData3[2], byteData3[3]
                 };
  Serial.write(buf, 12);
}

I just can't get the data at a 1kHZ sampling rate in my csv data. I don't understand datasheets well enough to program this using registers.

Thank you for your input.

How close are you getting? I see you get a micros value, what does it tell you.

You can increase the baud rate and shorten you packets by not reporting the micros value up.

Use a digital output pin and an oscilloscope or even a frequency meter instead of the full sent as text report.

You may then achieve your loop rate desires.

But I ask again, what are you getting now, in microseconds? We know 1000 is not enough, 2000 is.

FWIW I like doing awesome much without interrupts as possible.

a7

1 Like

Hey, thank you.

I will try increading baudrate, but my sensor data gets transmitted correctly. I need the micros value for analysis with a python library.

I checked the microseconds with Serial.print(timer), and I get a 1ms time! However, in my csv file, I'm getting
0.022281484678387642,0.02296610362827778, 9691.0791015625
0.02206721156835556,0.02244619093835354, 9743.0791015625
0.02238391898572445,0.022420145571231842, 9794.0791015625
0.02201131172478199,0.02212098054587841, 9844.0791015625
0.022331470623612404,0.02199425920844078, 9894.0791015625
0.021998587995767593,0.0214542206376791, 9944.0791015625

The python script is such that I have to send the same datatype, and since I need float variables for my digital filter I'm sending a float value.

code: (added / 1000.0)

  unsigned long currMicros = micros();
  if (currMicros - sampleTimer >= sampleInterval) // is it time for a sample?
  {
    sampleTimer = currMicros;
    float timer = sampleTimer / 1000.0;
    tmp1 = analogRead(A0) * 3.3 / 4095.0;     // convert reading into volts
    tmp2 = analogRead(A1) * 3.3 / 4095.0;
    val1 = 0.9875 * val1 + 0.01249 * tmp1;    // filter variables
    val2 = 0.9875 * val2 + 0.01249 * tmp2;
    sendToPC(&val1, &val2, &timer);
    // Serial.println(timer);
  }

Thank you :slight_smile:

In your non ISR variant, I would suggest that sampleTimer is incremented by sampleInterval rather than the current value of micros().

As @alto777 says, I would also suggest increasing your baud rate. If my rusty maths are correct, 12 bytes at 115200 baud takes roughly 1.14ms under ideal conditions.

1 Like

Why? If you are aiming for a consistent 1 millisecond loop time, this will only be telling you how much the loop was actually needed; one might think that as long as it was under 1000 microseconds you wouldn't care how much under. And shouldn't the numbers be almost the same?

I thought you were reporting only to verify success of looping fast enough, which is why I suggested my favorite trick for doing - monitor a pulse generated by the loop.

These numbers

 9944.0791015625

show 10 milliseconds? Have you posted a factor of 10 somewhere?

I ran your code but had to Serial.print readable text. Printing only the timer I can make the 1000 us. Baud rate 1000000. More later when I am at the big(ger) rig in my lab.

a7

Do the very simple arithmetic.
12 bytes, at 11520 bytes per second (maximum) ...

Is there a reason why this is better? I figured having the actual running time might be "better" when it comes to precision, for example if I modify the loop and it takes more than 1ms to execute, I'll see it in my data. Homegrown logic tough, I'm probably missing something.
Also for the baudrate, the options in arduino are 9600, 14.4k, 19.2k, etc. Are some numbers better than others? Like 230.4k?

That's just because the analysis library I'm trying to use needs a csv column for time, and others for readings. I could just fill a column with 1ms increments tough, if that's better.

Yeah, I can't figure that one out. The code is above, I don't get this. I'm taking the micros() value, and divide it by 1000 to get milliseconds. (The first 2 values from the sensors are floats, and get read properly over serial: range 0-3.3V)

That would be 12000 bytes/s minimum, right? + start/stop bits = 14000 minimum byterate = 140000 minimum baudrate?

Thanks to you all!

Don't forget the 200 or so microseconds lost to analogReads

At least it cuts down on the data being transmitted.

Looking at your code I wonder if you could just sendToPC the raw analogRead integers, squeezing out some time you waste spend doing it on the microprocessor.

If the 1 millisecond constraint is inflexible you don’t really have time to do much more anyway.

May we ask where you heading with this, what’s the real problem you are working?

a7

It depends on what you want to achieve and how much processing goes on in the main loop. If you wanted a 1 second interval (for example) and every now and again, one of the iterations round the main loop happened to take 1.5 seconds, then if you added current time, rather than the interval, then each interval after that will occur 0.5 seconds later than it should do compared to real time. So, there is a cumulative error each time the loop runs for longer than the interval.

I think that made more sense in my head than it does in print!

Yes, that could work, but then the digital lowpass filter would be gone, which works well on smoothing out noise from the sensors. It's just that 3rd float variable that's weird for some reason.

The reason I want to try to get it working under these conditions is to compare the results with a professional ADC, which runs at 1kHz. I'm supposed to use that device to take measurements for a project, but it's expensive and not often available. I'm trying to get something similar to work on my own device. The sample rate could be lower to get usable results, it's mainly for chest expansion recordings. But it's just something I wanted to try for the sake of it, I didn't think it would hang like this. It's nice to compare results if I get the same amount of datapoints.

I tried with a baudrate of 230400, same results for the timer value, 1000x too big. I write the 3 variables over serial exactly in the same way with the sendToPC function. I tried removing the / 1000.0; and get the same result?

Here's the entire code again. I tried changing the time to a variable that increments every loop, same thing. I tried writing the time variable to serial first, same result. The timer variable is right when Serial.print(timer) is done, but gets messed up when I send it over serial.

float tmp1, tmp2 = 0;   // variables at conversion
float val1, val2 = 0;   // variables after filtering

float timer = 0;

unsigned long sampleTimer = 0;
unsigned long sampleInterval = 1000; //100 us = 100Hz rate

void setup() {
  Serial.begin(230400);     // baudrate
  analogReadResolution(12); // ADC resolution
}

void loop() {
  unsigned long currMicros = micros();
  if (currMicros - sampleTimer >= sampleInterval) // is it time for a sample?
  {
    sampleTimer = currMicros;
    
    timer = (float)sampleTimer; // => this one gets scrambled
    tmp1 = analogRead(A0) * 3.3 / 4095.0;     // convert reading into volts
    tmp2 = analogRead(A1) * 3.3 / 4095.0;
    val1 = 0.9875 * val1 + 0.01249 * tmp1;    // filter variables
    val2 = 0.9875 * val2 + 0.01249 * tmp2;
    sendToPC(&val1, &val2, &timer);
    // Serial.println(timer);
  }
}

void sendToPC(float* data1, float* data2, float* data3)
{
  byte* byteData1 = (byte*)(data1);
  byte* byteData2 = (byte*)(data2);
  byte* byteData3 = (byte*)(data3);
  byte buf[12] = {byteData1[0], byteData1[1], byteData1[2], byteData1[3],
                  byteData2[0], byteData2[1], byteData2[2], byteData2[3],
                  byteData3[0], byteData3[1], byteData3[2], byteData3[3]
                 };
  Serial.write(buf, 12);
}

Thanks!

Further to @markd833 ‘s remarks.

If you are making the loop time, it makes no difference.

If you add the interval, and your loop takes way too long, you will get a rapid series of loop “activations”, so to speak, to make up for the exact number you missed.

Which is either what you want or not.

a7

Why are you sending an integer number of microseconds as a float?
After about eight seconds, it will be meaningless (23 bit float mantissa)

Aaah I see! My angle was having the transmitted data corresponding to the time it was sampled at, which can then be problematic for sampling time if the loop takes longer, which was the question in the first place. I thought doing it like this and checking if the time was around 1ms in the transmitted data would tell me if I was making the loop time of one ms or not.
I didn't think about the "catch-up" of the loops. Thanks for explaining.

Well, this is probably my answer. As you can tell, I don't understand this very well. I'm going to read up on it. Thank you :wink: