fast (>1kHz) readings from accelerometers

All ESP32 boards running MicroPython.
Target audience: MicroPython users with an ESP32 board.
aleskeyfedorovich
Posts: 28
Joined: Mon Aug 27, 2018 4:43 pm

fast (>1kHz) readings from accelerometers

Post by aleskeyfedorovich » Mon Mar 02, 2020 9:46 am

My goal is to read accelerations at 3 kHz so that I can have a power spectrum till 1.5kHz.
My setup is an esp32-wroom32 and an inexpensive digital accelerometer ADXL345: when the concept will work I'll scale to better accelerometers.
Nominally this accelerometer can measure at 3.2kHz and the esp32 i2c can read at 400kHz.
Unfortunately with the code I wrote I can't read faster than 1kHz and from my tests this limit comes from the esp32.
I'm wondering if it's a physical limit and I wrongly interpreted esp32 datasheet or my code is wrong.


This is my code:

Code: Select all

import time
from micropython import const
from machine import Pin, I2C

acquisition_time = 1  # [s]
sampling_rate = 3200  # [Hz]
n_exp_meas = floor(acquisition_time * ceil(sampling_rate))
acc_x = [0] * (n_exp_meas + 1)
acc_y = [0] * (n_exp_meas + 1)
acc_z = [0] * (n_exp_meas + 1)

i2c = I2C(1, freq=400000)   # hardware i2c
i2c.writeto_mem(const(0x53), const(0x2C), b'\x0f')  # set sampling rate to 3.2kHz
i2c.writeto_mem(const(0x53), const(0x2D), b'\x08')  # set the device to measure mode

n_act_meas = 0
start = time.ticks_us()
while time.ticks_diff(time.ticks_us(), start) < acquisition_time * 1000000:  # measure must last acquisition_time seconds
  curr_time = time.ticks_us()
  if time.ticks_diff(curr_time, start) < (n_act_meas * 1000000. / sampling_rate):  # avoid repeated values if reading frequency is higher than sampling
    continue
  acc_x[n_act_meas], acc_y[n_act_meas], acc_z[n_act_meas] = get_xyz()
  n_act_meas += 1
 
 
def get_xyz(self):
    buff = i2c.readfrom_mem(self.address, self.regAddress, self.TO_READ)
    x = (int(buff[1]) << 8) | buff[0]
    y = (int(buff[3]) << 8) | buff[2]
    z = (int(buff[5]) << 8) | buff[4]
    if x > 32767:
      x -= 65536
    if y > 32767:
      y -= 65536
    if z > 32767:
      z -= 65536
    return x, y, z
 
I simplified my code for this question purposes so apologies if i mistaken something in cut/paste and the code doesn't really work.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: fast (>1kHz) readings from accelerometers

Post by jimmo » Mon Mar 02, 2020 10:22 am

aleskeyfedorovich wrote:
Mon Mar 02, 2020 9:46 am
Nominally this accelerometer can measure at 3.2kHz and the esp32 i2c can read at 400kHz.
The 400kHz here is the bit rate of the I2C bus (i.e. how fast it runs the SCL line). So divide by eight for bytes, divide by 6 for number of bytes (plus a few more bytes overhead), and so running the bus at 100% utilisation you're going to get 5kHz max.

But your program is also doing a lot of other stuff while not using the I2C bus, so perhaps a better way to think of it that you have a budget of 670us per sample, and 200us used up on the I2C bus time. So you've got 470us to process each sample in Python.

If you plan to do all the processing after the fact, then maybe a scheme where you build up a large bytearray of just the raw data as fast as possible, then process it into integers later.

Some combination of readfrom_mem_into and memoryview might be useful.

Code: Select all

def read_samples(n_samples):
  buf = bytearray(6 * n_samples)
  m = memoryview(buf)
  a = self.address
  r = self.regAddress
  i2crmi = i2c.readfrom_mem_into
  for i in range(n_samples):
    # delay to next sample
    i2crmi(a, r, m[i*6:i*6+6])
(Also using locals instead of global/class lookups for performance. An @micropython.native on the function might help too).

Note that for converting bytes to integers, using ustruct.unpack will likely be faster and simpler to write:

Code: Select all

x, y, z = unstruct.unpack('<H<H<H', buf)  # or a slice of the memoryview

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: fast (>1kHz) readings from accelerometers

Post by pythoncoder » Mon Mar 02, 2020 10:32 am

The ESP32 is not well suited to fast real time operation: the Pyboard is a much better target.

[EDIT] I see @aleskeyfedorovich has made similar suggestions while I was writing this, but I'll post anyway.

There are things you could do to speed up the code, especially if you can separate the data acquisition from the conversion to acceleration values (I appreciate you may not be able to do this). If you can, you could use I2C.read_from_mem_into to read each sample set into a memoryview into a pre-allocated buffer. This would avoid allocations. After acquisition was complete you could convert the contents of the buffer into x,y,z values.

If you haven't seen it I suggest reading this guide to optimisation in the official docs.
Peter Hinch
Index to my micropython libraries.

aleskeyfedorovich
Posts: 28
Joined: Mon Aug 27, 2018 4:43 pm

Re: fast (>1kHz) readings from accelerometers

Post by aleskeyfedorovich » Mon Mar 02, 2020 4:55 pm

Thank you for the useful suggestions.
what I can't make work is the unpack.
my test buf before conversion is:

bytearray(b'\x15\xff\xeb\xff\r\x00\x12\xff\xed\xff\x10\x00\x12\xff\xed\xff\x10\x00\x12\xff\xed\xff\x10\x00\x12\xff\xed\xff\x10\x00\x13\xff\xec\xff\r\x00')

but I get "ValueError: bad typecode" on the unpack.

what's wrong?

OutoftheBOTS_
Posts: 847
Joined: Mon Nov 20, 2017 10:18 am

Re: fast (>1kHz) readings from accelerometers

Post by OutoftheBOTS_ » Mon Mar 02, 2020 8:35 pm

All the Quadcopter guys use SPI to read the IMU as it is much faster than I2C (much less overeheads in the protcol as well as higher baud rate). They all tend to use the older MPU-6050 because it isn't as sensitive as the new MPU-9260 and tends to have less noise problems from 4 brushless motors screaming their heads off.

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: fast (>1kHz) readings from accelerometers

Post by jimmo » Mon Mar 02, 2020 10:20 pm

aleskeyfedorovich wrote:
Mon Mar 02, 2020 4:55 pm
but I get "ValueError: bad typecode" on the unpack.
Sorry, should be '<HHH' (i.e. three little-endian unsigned shorts).

https://docs.python.org/3/library/struct.html
and
https://docs.micropython.org/en/latest/ ... truct.html

aleskeyfedorovich
Posts: 28
Joined: Mon Aug 27, 2018 4:43 pm

Re: fast (>1kHz) readings from accelerometers

Post by aleskeyfedorovich » Tue Mar 03, 2020 10:43 am

I don't know if the code below is what you meant (@jimmo) but it is the best I could do with my current knowledge and your suggestions.
I think checking time passed from previous reading is now the bottleneck but I don't know how to avoid it without measuring multiple times the same value.
Also, I thought you meant ustruct.unpack convert the whole bytearray to integers at once but I couldn't make it work and I had to use a list comprehension.
My original function reached approx 0.9kHz.
This function reached 1.19kHz.
Setting the cpu frequency to 240MHz made it to 1.45kHz.

@OutoftheBOTS: The SPI is also something I was thinking about but I still have to learn how to properly use it for reading and writing to the device.

Code: Select all

  @micropython.native
  def read_samples(self, acquisition_time):
    # set up
    n_exp_meas = floor(acquisition_time * ceil(self.sampling_rate))
    buf = bytearray(6 * n_exp_meas)
    m = memoryview(buf)
    n_act_meas = 0
    
    # read bytes as fast as possible
    start = time.ticks_us()
    while time.ticks_diff(time.ticks_us(), start) < acquisition_time * 1000000:
      curr_time = time.ticks_us()
      if time.ticks_diff(curr_time, start) < (n_act_meas * 999999. / self.sampling_rate):
        continue
      self.i2c.readfrom_mem_into(self.address, self.regAddress, m[n_act_meas * 6:n_act_meas * 6 + 6])
      n_act_meas += 1

    # bytes --> lists of integers
    acc_x, acc_y, acc_z = zip(*[ustruct.unpack('<HHH', buf[i:i+6]) for i in range(len(buf)) if i % 6 == 0])
    del buf, m
    gc.collect()
    
    # remove exceeding zeros
    acc_x = acc_x[:n_act_meas]  
    acc_y = acc_y[:n_act_meas]
    acc_z = acc_z[:n_act_meas]
    
    # negative values rule
    acc_x = [x if x <= 32767 else x - 65536 for x in acc_x]
    acc_y = [y if y <= 32767 else y - 65536 for y in acc_y]
    acc_z = [z if z <= 32767 else z - 65536 for z in acc_z]
    
    print("measured samples: ", n_act_meas)
    print("expected samples: ", n_exp_meas)
    print("actual sampling rate: ", n_act_meas / acquisition_time)
    
    return acc_x, acc_y, acc_z

User avatar
jimmo
Posts: 2754
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: fast (>1kHz) readings from accelerometers

Post by jimmo » Tue Mar 03, 2020 11:18 am

aleskeyfedorovich wrote:
Tue Mar 03, 2020 10:43 am
don't know if the code below is what you meant (@jimmo)
Yep, pretty much. But I think they timing part could be optimised further. I guess the main thing to find out would be how many samples/second do you get if you remove the timing and just write:

Code: Select all

a = self.address
r = self.refAddress
i2crfmi = self.i2c.readfrom_mem_into
for i in range(n_exp_means):
  i2crfmi(a, r, m[i*6,i*6+6])
That will at least tell you whether what you're trying to do is possible.

The other thing is that removing the use of self.i2c, self.address and self.regAddress (by changing them into locals) will give a small improvement. (This is more obvious if you realise that x.y in Python is effectively a dictionary lookup x['y'], which will happen every time the variable is accessed, whereas local variables are specially optimised).
aleskeyfedorovich wrote:
Tue Mar 03, 2020 10:43 am
Also, I thought you meant ustruct.unpack convert the whole bytearray to integers at once
Sorry I wasn't very clear -- was mostly suggesting that you'd use it on one sample at a time. i.e. a similar loop as above to go through each sample and pull out six bytes at a time.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: fast (>1kHz) readings from accelerometers

Post by pythoncoder » Tue Mar 03, 2020 11:28 am

OutoftheBOTS_ wrote:
Mon Mar 02, 2020 8:35 pm
All the Quadcopter guys use SPI to read the IMU as it is much faster than I2C (much less overeheads in the protcol as well as higher baud rate). They all tend to use the older MPU-6050 because it isn't as sensitive as the new MPU-9260 and tends to have less noise problems from 4 brushless motors screaming their heads off.
SPI is inherently faster because it uses push-pull output whereas I2C uses open drain with pullups. The low to high transition on an open drain bus is slow and is the reason for the limited baudrate. With the right hardware SPI can be very fast indeed.

That observation about the MPU-6050 is interesting. Is the problem with the later device related to the presence of a magnetometer? I can imagine that doesn't take kindly to running next to high powered motors.
Peter Hinch
Index to my micropython libraries.

aleskeyfedorovich
Posts: 28
Joined: Mon Aug 27, 2018 4:43 pm

Re: fast (>1kHz) readings from accelerometers

Post by aleskeyfedorovich » Tue Mar 03, 2020 3:10 pm

Setting i2c freq to 1.25MHz which is the max after which the Micropython throws 'no device' I get approx 2kHz which is pretty nice.
Not much difference removing my check on the time of measures.

My last question is: could I assess before testing that eps32 would not be able to reach 3kHz? which micro controller hardware parameter should I look at? CPU frequency? Are there different i2c or specs are the same in all micro controllers?

Thank you everyone for the very interesting contributions!

Post Reply