How can I read from an accelerometer through i2c/spi for ~1 second at frequency>2kHz with precise distancing between measures

Context

goal

My final goal is to perform an fft on acceleration signals with specs at least:

• band between ~10 and ~1000 Hz
• spectral resolution ~1Hz

on a portable, cheap, low-power device.

hardware

I'd like to do this with cheap portable low power hardware so I opted for an adxl345 as accelerometer connected via i2c (but also spi would be viable) to an esp32-wrover as micro-controller, powered by a 18650 lipo battery. Of course there is no problem, if necessary, to opt for more expensive hardware staying around ~100 euros/dollars.

software

I know and like python more than I know and like C, so I decided to use micropython on the microcontroller to acquire data from the sensor and push it via wifi to a computer that will actually perform the fft.

Question

My question is very focused on how to pass acceleration signals from the accelerometer to the micro-controller via I2C: I have implemented a loop that lasts for the desired acquisition time (say 1 second) and on each iteration tests how much time has passed since the beginning and if it's a multiple of the desired frequency it read a value from the accelerometer via i2c

start = ticks_ns()
while ticks_ns() - start < acquisition_time * 1000000000:
curr_time = ticks_ns()
if curr_time - start < (n_act_meas * 999999999. / sampling_rate):
continue
buf[n_act_meas * 6:n_act_meas * 6 + 6] = i2c_read_bytes(address, regAddress, length=6)  # 6bytes = 2bytes * 3 signals (xyz)
T[n_act_meas] = ticks_ns()
n_act_meas += 1


but this approach is not very precise on the interval between measures (~10% relative error) and has a top frequency of 2kHz in my implementation (below the max frequency of the accelerometer).

I feel like I'm doing something wrong: is it possible to read via i2c the last n measures from the accelerometer (where n~1000 in my case) so that the problem of the timing is relegated to the accelerometer that should have a system that is precise enough to guarantee the nominal max sampling frequency, and a precise distancing between measures?

The most interesting section is on the FIFO. See page 21:

The ADXL345 contains technology for an embedded memory management system with 32-level FIFO that can be used to minimize host processor burden. This buffer has four modes: bypass, FIFO, stream, and trigger (see FIFO Modes)

It sounds like you should set the FIFO mode to "stream". Set the "watermark" level to something suitable (e.g. half the FIFO depth, 16) and the sample speed (BW_RATE register). Then the device will tell the MCU when it is ready, and you simply repeatedly read the data registers (note that you need to do a multi-byte read to get all the axes properly), until you have 16 values. Those will be the values sampled from the last 16 time periods.

• As pointed out here, the FIFO is key. You also need a buffer in the MCU firmware. Be sure your reads are triggered by interrupts from the accel, and not by a polling loop in the MCU. Commented Oct 15, 2020 at 11:56
• Well, it looks like you can poll from the MCU as well if you want to, because the FIFO exposes a count of how many samples are in it in the FIFO_STATUS register, but either way you must avoid ever reading more samples than the device reported. Commented Oct 15, 2020 at 12:19
• Your answers are precious but brief for my knowledge: the first question that comes to my mind is if there is a specific reason for not setting the watermark to the full fifo size (32 values) so that I can read from the device the least number of times possible? Commented Oct 15, 2020 at 13:33
• The only reason I would avoid doing that is that you then have to read the device immediately, because if another result arrives you will lose it. Note that ultimately you have to do the same number of reads from the device because you want the same number of results? Even in FIFO mode it appears to only give you one three-coordinate result per bulk read. Commented Oct 15, 2020 at 13:37
• If you're asking for the FIFO count you don't need to check the watermark level as well, so you can just read 0x39 first. Then you have to (block read 6 bytes from 0x32)*number_of_values Commented Oct 16, 2020 at 8:42

Thanks to @pjc50 answer and @MarkLeavitt comment, suggesting the use of FIFO stream mode I was able to reduce noise generated by unprecise time intervals between measures @1.6kHz, but still I couldn't reach 3.2kHz probably because with this strategy I need many reads via I2C to know how many values are in the FIFO and read them, and this takes time.

Anyway I wanted to expand a bit @pjc50 answer for future readers with my limited knowledge of the topic, posting the modified loop in python language (actually very readable and easily translatable to any other language)

# definitions
acquisition_time = 1  # s
frequency = 1600  # Hz
n_exp_meas = int(acquisition_time * frequency)  # number of expected values to be read
n_exp_bytes = 6 * n_exp_meas
buf = bytearray(int(n_exp_bytes * 1.5))
# set up device
i2c.writeto_mem(add_device, 0x31, 0x11)  # set g=±2
i2c.writeto_mem(add_device, 0x2C, 0x0e)  # set frequency=1.6kHz
i2c.writeto_mem(add_device, 0x38, 0x90)  # set FIFO in 'stream' mode with a watermark level at 16 values
init = time.ticks_us()
i2c.writeto_mem(addr_device, 0x2D, 0x08)  # set device in 'measure' mode
# measure loop
n_act_meas = 0
while n_act_meas < n_exp_meas:
nvalues_available = i2c.readfrom_mem(addr_device, 0x39, 1)[0] & 0x1F  # first 5 bits corresponds to n values in FIFO
for _ in range(nvalues_available):  # read the FIFO
buf[n_act_meas * 6 : n_act_meas * 6 + 6] = i2c.readfrom_mem(addr_device, 0x32, 6)
n_act_meas += 1
print((time.ticks_us() - init)/1000000)  # check if actual acquisition time is as expected
i2c.writeto_mem(addr_device, 0x2D, 0x00)  # set device in 'standby' mode
# remove exceeding values
buf = buf[:6 * n_exp_meas]


this document is very helpful for understanding the use of FIFO

• You still may be able to pick up more speed. The accelerometer supports multi-byte reads, so instead of using a loop to read just 6 bytes at a time, try reading (6 * nvalues_available) bytes to get all the data in the FIFO in a single, extended i2c read transaction. Commented Oct 16, 2020 at 14:54
• I tried this but got strange values after converting to m/s^2, so I assumed this way I was reading following registers or dunno what... probably my fault but couldn't fix it, I'll try a second time following your suggestion Commented Oct 16, 2020 at 15:32
• @MarkLeavitt I tried again with these lines of code instead of the for loop: if nvalues_available: buf[(n_act_meas * 6):((n_act_meas * 6) + (3 * 2 * nvalues_available))] = i2c_rfm(addr_device, regaddr_acc, 3 * 2 * nvalues_available) n_act_meas += nvalues_available, but the output buffer contains strange bytes I've never obtained with the other approach like: \x00J, \x000, \x01C, \x004, \x00B, \x00@. I don't know if this is because the block read is not working and I'm reading following registers. Can you figure out what's happening? Commented Oct 19, 2020 at 8:20
• @MarkLeavitt example buffer head: bytearray(b'\x00\x00\x14\x00\x08\x01\x90\x1f\x00\x00\x00\x00\x00\x00\xe5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00J\x80\x000\x00\x00\x00\x01C\x00\x00\x004\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x08\x00\x00\x82\x00\xfc\xff\n\x00\n\x01\x90\x1f\x00\x00\x00\x00\x00\x00\xe5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00J\x80\x000\x00\x00\x00\x00B\x00\x00\x004\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x08\x00\x00\x82\x00\x00\x00\x06\x00\x04\x01\x90\x1f\x00\x00\x00\x00\x00') Commented Oct 19, 2020 at 8:28