4
\$\begingroup\$

Background

Here is an awkward pattern I've come up with to handle initializing the ID of an MPU for an embedded system based on the fairly unique factory self-test values stored in the MPU registers. (the MPU is the Inversense MPU-9250). The code will eventually be put on github/gitlab so the goal was to move all chip-unique configuration values to a separate source and header that the user can fill in (or leave with the pointer to the lookup set to 0 default).

If the user has saved self-test values to allow the individual chip to be ID'ed, then the ID is assigned and all other stored data that requires calibration by moving the chip through the full-range of all 3-axis can be applied from data provided in the source without having to go though the calibration sequence each time any program is run that uses the chip. (not really something that can be done reliably by simply moving the device in figure-eights, etc..) The additional saved configurations for gyro and accel axis-scale and magnetometer hard/soft iron corrections are done with simple functions and a switch (mpu->id) { ... }, just standard setup.

The source and header caldata.[ch] have the pattern I'm concerned with. The other two short sources are just additional source and header files that are involved in the calibration which have MCREs included below to demonstrate the actual program flow (and for comment if what is being done is ill-advised for some reason) The mpucaldata.c is just the MCRE for a driver to put it altogether and a Makefile is provided for convenience (with the command line option BLDOPT=-DCHECKNULL triggering the alternative test of a user with no saved self-test data.

Short Version

The basic approach is to provide a lookup table of factory self-test values in caldata.c with the values exposed via a pointer that is provided extern in caldata.h. If the user has no data, the pointer is simply initialized 0. To provide the number of rows in the lookup table, the function get_mpu_st_list_sz() is simply used as a getter to make that available. Since this part of the configuration will occur early as part of startup and weaves in and out of several additional source files, I would like feedback on whether there may be a better way of doing this or if there is anything out of the ordinary in the approach. I can't see anything wrong with this approach, but sometimes you miss the forest for the trees.

The Code

The user data MCRE header:

#ifndef caldata_h
#define caldata_h  1

#include <stdint.h>

/**
 *  pointer for extern of self-test data in header
 */
extern const uint8_t (*mpu_st_data)[6];

/**
 *  getter to provide size of mpu_st_list
 */
int get_mpu_st_list_sz (void);

#endif

source:

#include "caldata.h"

/**
 *  saved factory self-test values for each unique MPU chip.
 *  used as a fingerprint to ID which chip is attached allowing
 *  saved accel/gyro scale and magnetometer bias and scale values
 *  to be automatically applied from saved data avoiding repeated
 *  movement of MPU through the full range of all 3 axis on every
 *  program start.
 */
const uint8_t mpu_st_list[][6] = { {  93,  88, 115, 191, 198, 225 },
                                   { 104,  94, 122, 199, 207, 226 },
                                   {  94,  95, 120, 198, 215, 219 } };

/**
 *  pointer for extern of self-test data in header
 *  NOTE: must be initialize to data, as above, or
 *  set 0 (NULL) if no saved data exists so a check of
 *  if (mpu_st_data) validates the presence or absence
 *  of saved self-test values.
 */
#ifndef CHECKNULL
const uint8_t (*mpu_st_data)[6] = mpu_st_list;
#else
const uint8_t (*mpu_st_data)[6] = 0;
#endif

/**
 *  getter to provide size of mpu_st_list
 */
int get_mpu_st_list_sz (void)
{
  return sizeof mpu_st_list / sizeof *mpu_st_list;
}

The main calibration MCRE header:

#ifndef calibrate_h
#define calibrate_h  1

#include "mpu.h"
#include "caldata.h"

/**
 *  compare mpu->self-test for current mpu to the self-test results
 *  in mpu_st_list. if matched, the self-test can serve as a
 *  fingerprint to set the mpu->id.
 */
uint8_t get_id_from_factory_st (mpu_t *mpu);

#endif

source:

#include "calibrate.h"

/**
 *  compare mpu->self-test for current mpu to the self-test results
 *  in mpu_st_list. if matched, the self-test can serve as a
 *  fingerprint to set the mpu->id.
 *
 *  ID must be 1-based to allow zero-initialized struct to set default.
 */
uint8_t get_id_from_factory_st (mpu_t *mpu)
{
  int nids  = get_mpu_st_list_sz(),
      nelem = NAXIS * 2;
  const uint8_t mpuid[6] =  { mpu->accel_st.x, mpu->accel_st.y, mpu->accel_st.z,
                              mpu->gyro_st.x, mpu->gyro_st.y, mpu->gyro_st.z };
  int i = 0;

  for (; i < nids; i++) {                     /* loop over each known id */
    bool found = true;                        /* initialize flag true */
    for (int j = 0; j < nelem; j++) {         /* loop over each st value */
      if (mpuid[j] != mpu->st_data[i][j]) {   /* if any fails to match */
        found = false;                        /* set found flag false */
        break;
      }
    }
    if (found) {                    /* if self-test data matched */
      return (mpu->id = i + 1);     /* return one-based index for mpu */
    }
  }

  return 0;
}

The primary MPU config header:

#ifndef mpu_h
#define mpu_h  1

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>

#define NAXIS                 3     /* No. axis */
#define RWBUFSZ              16     /* for general 16-byte buffer size */

/**
 * vector typdefs for common types
  */
typedef union {
  uint8_t arr[NAXIS];
  struct { uint8_t x, y, z; };
} vect_uint8_t;

typedef union {
  uint16_t arr[NAXIS];
  struct { uint16_t x, y, z; };
} vect_uint16_t;

typedef union {
  float arr[NAXIS];
  struct { float x, y, z; };
} vect_float_t;

/**
 *  main configuration for mpu
 */
typedef struct mpu_t {

  uint8_t addr, id;               /* mpu I2C address and individual chip-ID */
  
  vect_uint8_t  accel_st, gyro_st;        /* factory self-test values */
  
  const uint8_t (*st_data)[NAXIS*2];      /* pointer to array of stored st values */
  
  
} mpu_t;

/**
 *  Initialize mpu
 */
void mpuinit (mpu_t *mpu);

#endif

and source:

#include "mpu.h"
#include "caldata.h"

/**
 *  Initialize mpu
 */
void mpuinit (mpu_t *mpu)
{
  mpu->st_data = mpu_st_data;
}

A short test program:

#include <stdio.h>

#include "mpu.h"
#include "caldata.h"
#include "calibrate.h"

int main (void) {
  
  /* declare/initialize struct */
  mpu_t mpu = { 0x68, 0, 
                {{ 94, 95, 120 }}, 
                {{ 198, 215, 219 }}, 
                NULL };
  
  mpuinit (&mpu);                       /* initialize mpu */
  
  if (mpu.st_data) {                    /* check if data if table */
    get_id_from_factory_st (&mpu);      /* set id from self-test vals */
  }
  
  printf ("mpu.id: %hhu\n", mpu.id);    /* output result */
}

And finally a Makefile to build. Allowing make BLDOPT=-DCHECKNULL to check the case where the pointer is initialize 0:

TARGET  = mpucaldata

CC  = gcc

BIN     = bin
SRC = src
OBJ = obj
SRCS    = $(wildcard $(SRC)/*.c)
OBJS    = $(patsubst $(SRC)%.c,  $(OBJ)/%.o, $(SRCS))

BLDOPT  =

CFLAGS  += -Wall -Wextra -pedantic -Wshadow -Werror -std=c11
CFLAGS  += -Iinclude
CFLAGS  += $(BLDOPT)
LDFLAGS +=

# clean before build and create directories
all:    clean setup $(TARGET)

setup:
    @mkdir -p $(BIN)
    @mkdir -p $(OBJ)

# build executable
$(TARGET):  $(OBJS)
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN)/$(TARGET) $^

# compile objects
$(OBJ)/%.o: $(SRC)/%.c
    $(CC) -c $(CFLAGS) $< -o $@

# clean
clean:
    @rm -rvf $(OBJ)
    @rm -rvf $(BIN)

(note: If you use the Makefile put all sources in a src subdirectory and all headers in an include subdirectory. The Makefile puts the objects in the ./obj directory and the executable in ./bin)

Anything look amiss?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Are self-test values unique enough?

It's tempting to look at the self-test values of a small number of MPUs, and from seeing that they are all different, conclude that they are therefore unique. But are they really? If you add a new MPU, can you be confident it too will have a unique set of self-test values?

Looking at the spread in the values, I would say you get about 3 to 4 bits of entropy from each of the 6 bytes. Lets go with 3 bits to be conservative, that means you have about \$2^{3\cdot 6}\$ = 262144 possible combinations of self-test values. Assuming that they are all equally likely, the birthday paradox then says that if you have a set of 512 MPUs, the chance that at least two of them will have exactly the same self-test values is approximately 50%.

So, if you have much less than 512 MPUs to worry about, it's probably fine.

Use memcmp()

You can greatly simplify get_id_from_factory_st() by using memcmp():

uint8_t get_id_from_factory_st (mpu_t *mpu)
{
  const uint8_t mpuid[6] = {mpu->accel_st.x, mpu->accel_st.y, mpu->accel_st.z,
                            mpu->gyro_st.x, mpu->gyro_st.y, mpu->gyro_st.z};

  for (uint8_t i = 0; i < get_mpu_st_list_sz(); i++)
  {
    if (memcmp(mpu->st_data[i], mpuid, sizeof mpuid) == 0)
    {
      return (mpu->id = i + 1); /* return one-based index for mpu */
    }
  }

  return 0;
}
\$\endgroup\$
2
  • \$\begingroup\$ All great suggestions. With 4 of the MPUs, the self-tests are unique enough. With more a dupe check would be needed (worth a note in the code). I decided against memcmp() due to the fact that nothing else in the code includes string.h and to keep the size as small as possible. (code builds on PI, Pi-pico and Milkv-Duo) With a comparison of 6 values, we don't see any performance benefit from memcmp() (which depending on how implemented generally iterates casting the char-type to unsigned (or uint32_t) to make 4-comparisons per-iteration). Glad to know I wasn't nuts :) \$\endgroup\$ Mar 27 at 8:02
  • \$\begingroup\$ Sorry for the delay swinging back by to accept the answer. Thank you again for your comments. \$\endgroup\$ Apr 3 at 9:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.