Speed up SPI and sensor reading

I'm trying to read 2 accelerometers at a high rate (~1k Hz each) but the fastest I seem to be able to make it go is 250 Hz. I was hoping that someone might be able to help me speed it up to meet specification.

I'm using an Adafruit Flora, reading 2 ADXL345 accelerometers, and logging to an Openlog on Serial1. The code I'm using is below. Any advice on how to speed this up would be great!

#include <SPI.h>

// Chip select pins for the two accelerometers
int CS0 = 6;
int CS1 = 12;

char POWER_CTL = 0x2D;        // Power control register
char DATA_FORMAT = 0x31;      // Register to set the data format
char DATA_RATE = 0x2C;        // Register to set the data rate
int MAX_DATA_RATE = 5000000;  // maximum data rate for communication
char SIGNIFICANT_BIT = MSBFIRST; // SPI byte endianness
char CLOCK_PHASE = SPI_MODE3; // SPI clock and bit phase

// Data storage bit for reading
char DATAX0 = 0x32;	//X-Axis least significant byte
char DATAX1 = 0x33;	//X-Axis most significant byte
char DATAY0 = 0x34;	//Y-Axis least significant byte
char DATAY1 = 0x35;	//Y-Axis most significant byte
char DATAZ0 = 0x36;	//Z-Axis least significant byte
char DATAZ1 = 0x37;	//Z-Axis most significant byte

// output buffer to hold data read from registers
char values0[6];
char values1[6];

// variables to hold the accelerometer values
int x0, y0, z0;
int x1, y1, z1;

void setup() {
  Serial1.begin(115200);
  // set the onboard CS pin to output to tie it high
  // to prevent arduino from going into slave mode
  pinMode(10, OUTPUT);
  digitalWrite(10, HIGH);
  // set the SPI CS pins to high to initialize and disable
  pinMode(CS0, OUTPUT);
  digitalWrite(CS0, HIGH);
  pinMode(CS1, OUTPUT);
  digitalWrite(CS1, HIGH);
  SPI.begin();
  // setup sensor 0
  sensorSetup(CS0);
  // setup sensor 1
  sensorSetup(CS1);
}

void loop() {
    // read the accelerations
    unsigned long t0 = micros();
    readRegister(CS0, DATAX0, 6, values0);
    unsigned long t1 = micros();
    readRegister(CS1, DATAX0, 6, values1);

    // data registers are LS byte followed by MS byte, so rearrange
    x0 = (values0[1] << 8) | values0[0];
    y0 = (values0[3] << 8) | values0[2];
    z0 = (values0[5] << 8) | values0[4];

    x1 = (values1[1] << 8) | values1[0];
    y1 = (values1[3] << 8) | values1[2];
    z1 = (values1[5] << 8) | values1[4];

    // Send the data to the Openlog
    Serial1.print("Sensor0: "); Serial1.print(t0); Serial1.print (" ");
    Serial1.print(x0, DEC); Serial1.print(" ");
    Serial1.print(y0, DEC); Serial1.print(" ");
    Serial1.print(z0, DEC);

    Serial1.print(" Sensor1: "); Serial1.print(t1); Serial1.print (" ");
    Serial1.print(x1, DEC); Serial1.print(" ");
    Serial1.print(y1, DEC); Serial1.print(" ");
    Serial1.println(z1, DEC);

}

void writeRegister(int CS, char register_address, char value)
/* write a register over SPI
int CS = pin number of the chip select pin
char register_address = the address of the register to write
char value = the value to write to the register
*/
{
  // setup the SPI transation
  SPI.beginTransaction(SPISettings(MAX_DATA_RATE, SIGNIFICANT_BIT, CLOCK_PHASE));
  // turn on the selected chip
  digitalWrite(CS, LOW);
  // let it know the resister you are writing to
  SPI.transfer(register_address);
  // send it the value
  SPI.transfer(value);
  // turn off the selected chip
  digitalWrite(CS, HIGH);
  // end the transaction
  SPI.endTransaction();
}

void readRegister(int CS, char register_address, int num_Bytes, char *values)
/* read a register
int CS = the pin number of the chip select pin
char register_address = the address of the register to start the read.
int num_Bytes = the number of bytes to read
char *values = pointer to values array to store the read data
*/
{
  // set bit 7 bit of the register to indicate pending read.
  char address = 0x80 | register_address;
  // multip-byte reads are signaled using bit 6
  if (num_Bytes > 1) address |= 0x40;
  
  // setup the SPI transaction
  SPI.beginTransaction(SPISettings(MAX_DATA_RATE, SIGNIFICANT_BIT, CLOCK_PHASE));
  // turn on the selected chip
  digitalWrite(CS, LOW);
  // transfer the starting address
  SPI.transfer(address);
  // read the registeres until we've read the specified number of bytes,
  // store the resuts in the input buffer.
  for (int i = 0; i < num_Bytes; i++)
  {
    values[i] = SPI.transfer(0);
  }
  // we're done here. turn chip off
  digitalWrite(CS, HIGH);
  // end transaction
  SPI.endTransaction();
}

void sensorSetup(int CS)
/* setup the sensor
int CS = chip select pin
*/
{
  // put the sensor into measure mode.  This will prevent sleep
  writeRegister(CS, POWER_CTL, 0x08);
  char dataformat = 0;
  // set the senso to full range 0000 0100
  dataformat |= 0x04;
  // set the range to 8g, use 0x01 for 4g; 0000 0010
  dataformat |= 0x02;
  // set the data format register
  writeRegister(CS, DATA_FORMAT, dataformat);
  // set the data rate to maximum
  writeRegister(CS, DATA_RATE, 0x0F);
}

I'd guess that the Serial.print calls slow down your program. When you already take the time (t0,t1) for the transmissions, you can print out the difference in order to get an idea of the time spent in readRegister.

You can indeed speed up the serial printing by making the fixed strings shorter and write the single space as a char iso as a string. less data => shorter time. However it will not be significant

    // Send the data to the Openlog
    Serial1.print("S0: "); Serial1.print(t0); Serial1.write(' ');
    Serial1.print(x0, DEC); Serial1.write(' ');
    Serial1.print(y0, DEC); Serial1.write(' ');
    Serial1.print(z0, DEC);

    Serial1.print(" S1: "); Serial1.print(t1); Serial1.write(' ');
    Serial1.print(x1, DEC); Serial1.write(' ');
    Serial1.print(y1, DEC); Serial1.write(' ');
    Serial1.println(z1, DEC);

more gain might be when you put the baudrate to e.g. 250000
and use a terminal program iso the serial of the IDE.

make the void readRegister ==> inline void readRegister

change CS parameter to a byte. num_bytes too (I guess that would work)

Hi Rob,
Thanks for the tips. I appreciate your help.

Unfortunately, this project will be used in a wearable, which means that I need to log rather than stream the data to a terminal or serial monitor. Openlog specifies a max speed of 115200, which is why I'm using that. Can you think of other, faster logging methods? Would I be able to send to an EEPROM and then read it out of there later, when the device is hooked up to a computer? Alternatively, would buffering the data in an EEPROM and then writing to openlog in bursts be faster?

For logging purposes it should be sufficient to store or transmit the values in binary, 12 bytes (6 int) per record. Eventually a time stamp should be added, either to every record or every second.

Then consider the amount of data to store. With just 1kHz sample rate this makes 12kB per second, way too much for EEPROM. You'll need either some mass storage device (SD card), or much faster online transmission (BT, WiFi).

Or you compute e.g. positions from the sensor values, and output the positions at a lower sample rate.

What do you want to do with so many data?

headphones54321:
Hi Rob,
Thanks for the tips. I appreciate your help.

Unfortunately, this project will be used in a wearable, which means that I need to log rather than stream the data to a terminal or serial monitor. Openlog specifies a max speed of 115200, which is why I'm using that. Can you think of other, faster logging methods? Would I be able to send to an EEPROM and then read it out of there later, when the device is hooked up to a computer? Alternatively, would buffering the data in an EEPROM and then writing to openlog in bursts be faster?

A faster way of logging is to use binary format and short codes. These can be decoded/expanded when reading back.

    // Send the data to the Openlog
    Serial1.print("Sensor0: "); Serial1.print(t0); Serial1.print (" ");
    Serial1.print(x0, DEC); Serial1.print(" ");
    Serial1.print(y0, DEC); Serial1.print(" ");
    Serial1.print(z0, DEC);

    Serial1.print(" Sensor1: "); Serial1.print(t1); Serial1.print (" ");
    Serial1.print(x1, DEC); Serial1.print(" ");
    Serial1.print(y1, DEC); Serial1.print(" ");
    Serial1.println(z1, DEC);

the above part sends 9 + len(t0) + 1 + len(x0) + 1 + len(y0) + 1 + len(z0) ... = about 50(? in fact variable) bytes for storing a 6 ints and 2 unsigned longs record in human readable form. Furthermore it needs to process the \0 and \r\n chars. That is another 10 bytes to process.

By storing the log in binary format it only needs 2x4bytes + 6x2bytes = 20 bytes (fixed size) That is a factor 2.5 better.

As the timestamps t0 and t1 are always near each other, t1 could be stored as a 2 byte offset from t0 (allows the read to take ~65000 micros) That would decrease it to 18 bytes.
BUT
As the measurements t0/t1 are always made 'directly' after each other you may safely assume the time diff is constant which removes the need to store t1 altogether. That would decrease log data to 16 bytes.

So a factor 3 is within reach by going binary.

As I do not know the values for {x, y, z) I don't know if they are compressible e.g. run length or delta compressing or otherwise

And by storing the needed data in a struct first, you can store them in one write operation which is faster than all those individual calls.

struct
{
  uint32_t t0;
  uint16_t d[6];
} data;
.. fill data
Serial1.write(&data, sizeof(data));

Hi Rob,
Thanks for all the suggestions. I imagine that using the structure to store the data and write the data and then upgrading to a 16MHz system will probably get me there. Your suggestions have been super helpful. Cheers!

Seth

For more storage space, you can also consider Adafruit I2C Non-Volatile FRAM Breakout - 256Kbit / 32KByte; up to 256kByte with 8 devices.

Hi all,
I've implemented the changes that Rob suggested, writing the data structure using Serial.write directly:

// output buffer to hold data read from registers
uint8_t values0[6];
uint8_t values1[6];

// structure for writing the data
struct data_structure{
unsigned long t0;
unsigned long t1;
uint16_t values[6];
}

[...]

void loop(){
data_structure data;

// read sensor 0
data.t0 = micros();
readRegister(CS0, DATAX0, 6, values0);

// read sensor 1
data.t1 = micros();
readRegister(CS1, DATAX0, 6, values1);

// put the data into the structure
data.accels[0] = (values0[1] << 8) | values0[0];
data.accels[1] = (values0[3] << 8) | values0[2];
data.accels[2] = (values0[5] << 8) | values0[4];

data.accels[3] = (values1[1] << 8) | values1[0];
data.accels[4] = (values1[3] << 8) | values1[2];
data.accels[5] = (values1[5] << 8) | values1[4];

Serial1.write((uint8_t*)&data, sizeof(data));
}

I then read this using python with the code

import struct as stc
import xlwt

def ReadDataFile(file_name):
    """

    :param file_name: string of the file name
    :return results: list of tuples of the data in the format
    (time_sensor_0, time_sensor_1, s0_x, s0_y, s0_z, s1_x, s1_y, s1_z)
    """

    # the structure format used to store the accelerometer data
    myStruct = stc.Struct('=LL6H')

    with open(file_name, 'rb') as file:
        results = []
        while True:
            buf = file.read(myStruct.size)
            print(buf)
            if len(buf) is not myStruct.size:
                break
            results.append(myStruct.unpack_from(buf))

    return results

def WriteXlsFile(file_name, data):
    """

    :param file_name: Name of the file to be written to
    :param data: data to be written to the file. Expected to be in the format returned by ReadDataFile
    :return:
    """

    workbook = xlwt.Workbook()
    worksheet = workbook.add_sheet('Accelerometers')

    worksheet.write(0, 0, 'Time 0')
    worksheet.write(0, 1, 'x0')
    worksheet.write(0, 2, 'y0')
    worksheet.write(0, 3, 'z0')

    worksheet.write(0, 5, 'Time 1')
    worksheet.write(0, 6, 'x1')
    worksheet.write(0, 7, 'y1')
    worksheet.write(0, 8, 'z1')

    for i, reading in enumerate(data):
        worksheet.write(i+1, 0, reading[0]) #time s0
        worksheet.write(i+1, 5, reading[1]) #time s1
        worksheet.write(i+1, 1, reading[2]) #x0
        worksheet.write(i+1, 2, reading[3]) #y0
        worksheet.write(i+1, 3, reading[4]) #z0
        worksheet.write(i+1, 6, reading[5]) #x1
        worksheet.write(i+1, 7, reading[6]) #y1
        worksheet.write(i+1, 8, reading[7]) #z1

    workbook.save(file_name)


if __name__ == '__main__':
    in_file = '/Volumes/NO NAME/LOG00069.TXT'
    out_file = in_file[0:-3]+'xls'
    results = ReadDataFile(in_file)
    WriteXlsFile(out_file, results)

The problem is that the data that comes out dose not make any sense. t0 and t1 do not increase monotonically, and the accelerations are all over the place! Any ideas of what I could do to make this work would be most helpful!

Thanks,
Seth

What does the data you're getting out look like?

Is it printing byte values rather than the string representations of them? That's what was being talked about above to make it faster, I think.

But I'm not sure whether your python script is aware of that, or thinks it's getting numbers as strings.

Before involving a second point of failure (your python script) you should know what is being send down the wire.

Connect to it using a nice serial terminal program (I like hterm) that lets you show the data as ASCII or hex/binary/decimal, so you can see for sure what it's sending out/.

Hi DrAzzy,
I'm reasonable sure that it is writing the bytes. I've checked the outputs using print(buf) in the python read loop and also compared that to the output of Hex Fiend. One of the things I tried was using

struct data_structure{
uint32_t marker = 0xFFFFFFFF;
uint32_t t0;
uint32_t t1;
uint16_t values[6];
}

and then inspecting the output to make sure that each structure I was reading with python started with 0xFF 0xFF 0xFF 0xFF.

I discovered that the marker was not always at be beginning of the data that was read and figured out that I needed to reset openlog after inserting the SD card to ensure that the data started at the beginning of the file.

I've got it working. There were two problems:

  1. I was writing too quickly to the SD card causing buffer over runs.
  2. The python code was setup to use native endianess, where the Arduino was writing little endian.

Here's the working code.
Adafruit Flora:

#include <SPI.h>

// Chip select pins for the two accelerometers
char CS0 = 6;
char CS1 = 12;

// openlog reset pin
char olReset = 9;

uint8_t POWER_CTL = 0x2D;        // Power control register
uint8_t DATA_FORMAT = 0x31;      // Register to set the data format
uint8_t DATA_RATE = 0x2C;        // Register to set the data rate
unsigned int MAX_DATA_RATE = 5000000;  // maximum data rate for communication
uint8_t SIGNIFICANT_BIT = MSBFIRST; // SPI byte endianness
uint8_t CLOCK_PHASE = SPI_MODE3; // SPI clock and bit phase

// Data storage bit for reading
uint8_t DATAX0 = 0x32;	//X-Axis least significant byte
uint8_t DATAX1 = 0x33;	//X-Axis most significant byte
uint8_t DATAY0 = 0x34;	//Y-Axis least significant byte
uint8_t DATAY1 = 0x35;	//Y-Axis most significant byte
uint8_t DATAZ0 = 0x36;	//Z-Axis least significant byte
uint8_t DATAZ1 = 0x37;	//Z-Axis most significant byte

// output buffer to hold data read from registers
uint8_t values0[6];
uint8_t values1[6];

// variables to hold the accelerometer values
struct Data_Structure {
  uint32_t t0;
  uint32_t t1;
  uint16_t accels[6];
};


void setup() {
  Serial1.begin(115200);
  pinMode(olReset, OUTPUT);
  digitalWrite(olReset, LOW);
  delay(100);
  digitalWrite(olReset, HIGH);
  delay(1000); // time for logger to initialize
  
  // set the onboard CS pin to output to tie it high
  // to prevent arduino from going into slave mode
  pinMode(10, OUTPUT);
  digitalWrite(10, HIGH);
  // set the SPI CS pins to high to initialize and disable
  pinMode(CS0, OUTPUT);
  digitalWrite(CS0, HIGH);
  pinMode(CS1, OUTPUT);
  digitalWrite(CS1, HIGH);
  SPI.begin();
  // setup sensor 0
  sensorSetup(CS0);
  // setup sensor 1
  sensorSetup(CS1);
}

void loop() {
    Data_Structure data;
    // read the accelerations
    data.t0 = micros();
    readRegister(CS0, DATAX0, 6, values0);
    data.t1 = micros();
    readRegister(CS1, DATAX0, 6, values1);

    // data registers are LS byte followed by MS byte, so rearrange
    data.accels[0] = (values0[1] << 8) | values0[0];
    data.accels[1] = (values0[3] << 8) | values0[2];
    data.accels[2] = (values0[5] << 8) | values0[4];

    data.accels[3] = (values1[1] << 8) | values1[0];
    data.accels[4] = (values1[3] << 8) | values1[2];
    data.accels[5] = (values1[5] << 8) | values1[4];
    
    byte n_written = Serial1.write((uint8_t*)&data, sizeof(data));
    
    delayMicroseconds(2000);
}

void writeRegister(byte CS, uint8_t register_address, uint8_t value)
/* write a register over SPI
int CS = pin number of the chip select pin
char register_address = the address of the register to write
char value = the value to write to the register
*/
{
  // setup the SPI transation
  SPI.beginTransaction(SPISettings(MAX_DATA_RATE, SIGNIFICANT_BIT, CLOCK_PHASE));
  // turn on the selected chip
  digitalWrite(CS, LOW);
  // let it know the resister you are writing to
  SPI.transfer(register_address);
  // send it the value
  SPI.transfer(value);
  // turn off the selected chip
  digitalWrite(CS, HIGH);
  // end the transaction
  SPI.endTransaction();
}

void readRegister(byte CS, uint8_t register_address, int num_Bytes, uint8_t *values)
/* read a register
int CS = the pin number of the chip select pin
char register_address = the address of the register to start the read.
int num_Bytes = the number of bytes to read
char *values = pointer to values array to store the read data
*/
{
  // set bit 7 bit of the register to indicate pending read.
  register_address |= 0x80;
  // multip-byte reads are signaled using bit 6
  if (num_Bytes > 1) register_address |= 0x40;
  // setup the SPI transaction
  SPI.beginTransaction(SPISettings(MAX_DATA_RATE, SIGNIFICANT_BIT, CLOCK_PHASE));
  // turn on the selected chip
  digitalWrite(CS, LOW);
  // transfer the starting address
  SPI.transfer(register_address);
  // read the registeres until we've read the specified number of bytes,
  // store the resuts in the input buffer.
  for (int i = 0; i < num_Bytes; i++)
  {
    values[i] = short(SPI.transfer(0));
  }
  // we're done here. turn chip off
  digitalWrite(CS, HIGH);
  // end transaction
  SPI.endTransaction();
}

void sensorSetup(byte CS)
/* setup the sensor
int CS = chip select pin
*/
{
  // put the sensor into measure mode.  This will prevent sleep
  writeRegister(CS, POWER_CTL, 0x08);
  char dataformat = 0;
  // set the senso to full range 0000 1000
  dataformat |= 0x08;
  // set the range to 8g, use 0x01 for 4g; 0000 0010
  dataformat |= 0x02;
  // set the data format register
  writeRegister(CS, DATA_FORMAT, dataformat);
  // set the data rate to maximum
  writeRegister(CS, DATA_RATE, 0x0F);
}

Python:

import struct as stc
import xlwt

def ReadDataFile(file_name):
    """

    :param file_name: string of the file name
    :return results: list of tuples of the data in the format
    (time_sensor_0, time_sensor_1, s0_x, s0_y, s0_z, s1_x, s1_y, s1_z)
    """

    # the structure format used to store the accelerometer data
    myStruct = stc.Struct('<LL6h')

    with open(file_name, 'rb') as file:
        results = []
        while True:
            buf = file.read(myStruct.size)
            if len(buf) is not myStruct.size:
                break
            results.append(myStruct.unpack_from(buf))

    return results

def WriteXlsFile(file_name, data):
    """

    :param file_name: Name of the file to be written to
    :param data: data to be written to the file. Expected to be in the format returned by ReadDataFile
    :return:
    """

    workbook = xlwt.Workbook()
    worksheet = workbook.add_sheet('Accelerometers')

    worksheet.write(0, 0, 'Time 0')
    worksheet.write(0, 1, 'x0')
    worksheet.write(0, 2, 'y0')
    worksheet.write(0, 3, 'z0')

    worksheet.write(0, 5, 'Time 1')
    worksheet.write(0, 6, 'x1')
    worksheet.write(0, 7, 'y1')
    worksheet.write(0, 8, 'z1')

    gPerLSB = 0.004 # defined in the sensor driver

    for i, reading in enumerate(data):
        worksheet.write(i+1, 0, reading[0])
        worksheet.write(i+1, 5, reading[1])
        worksheet.write(i+1, 1, reading[2]*gPerLSB)
        worksheet.write(i+1, 2, reading[3]*gPerLSB)
        worksheet.write(i+1, 3, reading[4]*gPerLSB)
        worksheet.write(i+1, 6, reading[5]*gPerLSB)
        worksheet.write(i+1, 7, reading[6]*gPerLSB)
        worksheet.write(i+1, 8, reading[7]*gPerLSB)


    workbook.save(file_name)


if __name__ == '__main__':
    in_file = '/Volumes/~1         /LOG00130.TXT'
    out_file = in_file[0:-3]+'xls'
    results = ReadDataFile(in_file)
    WriteXlsFile(out_file, results)

Thanks to all those who helped and suggested a way to solve this issue.