Hands-on exercise with the ADXL355 and ChibiOS SPI

Introduction

In this article, we will look at how to interface an ADXL355 accelerometer using SPI. To fully grasp the content of this article, you should be familiar with the fundamentals of SPI, including concepts like clock polarity and clock phase, as discussed in our previous article SPI 101: A beginner’s guide. Additionally, a basic understanding of the SPI API in ChibiOS/HAL, as shown in Leveraging ChibiOS/HAL’s SPI for Real-Time Applications is necessary.

Building on these fundamentals, we will delve into a practical experience. We will be using the ADXL355, a low-power, high-precision three-axis accelerometer from Analog Devices. This device, with its register-based interface compatible with both SPI and I2C, is an ideal choice for our initial foray into SPI. Our process will begin by connecting a SDP-K1 microcontroller to an EVAL-ADXL355-PMDZ. Following this, we will cover the communication protocol for the ADXL355, utilizing SPI to read from and write to the device’s registers. Once communication is established, we will demonstrate how to activate the accelerometer and retrieve acceleration data, ultimately creating a simple library to perform these tasks.

Establishing the Hardware Connection

Before we begin, it is essential to connect the EVAL-ADXL355-PMDZ to the SDP-K1 correctly for communication. This requires consulting several important documents:

For the ADXL355, we should examine the EVAL-ADXL355-PMDZ’s schematic to see how the header connector is linked to the chip. The datasheet will help us understand each pin’s function. We need to ensure that the power supply and digital interface are correctly connected, using appropriate voltages and logic levels.

The schematic of the EVAL-ADXL355-PMDZ

After a quick review of the schematic, we need to concentrate on these specific pins

ADXL355 LabelEVAL-ADXL355-PMDZ LabelEVAL-ADXL355-PMDZ LocationFunction
VSUPPLYVCC via 39 ohm ferrite beadConnector P1, pins 6 and 12Power Supply
VDDIOVCCConnector P1, pins 6 and 12Power Supply
VSSDGND via 330 ohm ferrite beadConnector P1, pins 5 and 11Ground
VSSIODGNDConnector P1, pins 5 and 11Ground
CSN/SCLCSConnector P1, pin 1SPI Chip Select
SCLK/VSSIOSCLKConnector P1, pin 4SPI Clock
MOSI/SADMOSIConnector P1, pin 2SPI MOSI
MISO/ASELMISOConnector P1, pin 3SPI MISO
Connection Summary between ADXL355 and EVAL-ADXL355-PMDZ

Power Supply connections

It is important to note that the ADXL355 provides two separate power inputs for its analog and digital components:

  • The VSUPPLY-VSS powers the analog stage, which includes the ADC, signal chain, and sensors.
  • The VDDIO-VSSIO supplies power to the logic components, such as registers and communication interfaces.

This separation at the chip level ensures that the digital interface does not interfere with the analog part’s requirements. The VSUPPLY has a limited voltage range from 2.25V to 3.6V, which is necessary due to the design of the sensor, signal chain, and ADCs. The digital logic can operate at lower voltages, down to 1.8V, to match microcontrollers that work at these lower levels.

Although the chip allows for separate power sources, the evaluation board is designed for ease of use and simplifies this by coupling the analog and digital supplies through ferrite beads. The ferrite beads used on the evaluation board serve a specific purpose in power supply management. They function as filters that present high resistance to high-frequency noise, which is common in digital circuits. By doing so, they help to prevent such noise from reaching and affecting the sensitive analog components of the accelerometer. This is crucial for maintaining the integrity of the analog signals, which can be easily distorted by digital noise, potentially compromising the performance of the device.

For our setup, we will connect the DGND to the microcontroller’s ground and provide the EVAL-ADXL355-PMDZ’s VCC with 3.3V from the SDP-K1.

Supplying power to the EVAL-ADXL355-PMDZ from the SDP-K1

SPI Interface connections

To interface with the SPI peripheral on the SDP-K1, the most straightforward approach is to use SPI1 via the Arduino connector’s D10, D11, D12, and D13 pins. This setup is based on information from the STM32F469 datasheet, the SDP-K1 schematic, and the typical configuration of Arduino connectors, which usually provide access to the microcontroller’s SPI peripheral. The image below illustrates the standard layout of an Arduino connector, highlighting that pins D10 to D13 are designated for SPI communication with the MCU.

The SPI bus connection between the EVAL-ADXL355-PMDZ and the SDP-K1

Overview of the connections

The table below presents a concise summary of the connections between the SDP-K1 and the EVAL-ADXL355-PMDZ. It provides a clear reference for the physical wiring required to link the two boards.

EVAL-ADXL355-PMDZ LabelEVAL-ADXL355-PMDZ ConnectorSDP-K1 LabelSDP Connector
CSP1.1D10P6.3
MOSIP1.2D11P6.4
MISOP1.3D12P6.5
SCLKP1.4D13P6.6
DGNDP1.5GNDP3.6
VCCP1.63.3VP3.4
Connection Map between the SDP-K1 and the EVAL-ADXL355-PMDZ

To accompany this summary, the following image illustrates the connection between the SDP-K1 and EVAL-ADXL355.

Full overview of the connections between the EVAL-ADXL355-PMDZ and the SDP-K1

ADXL355 communication interface

The ADXL355 integrates a three-axis sensor with dedicated ADCs that convert mechanical motion into digital signals corresponding to acceleration along each axis. The sensor’s signal chain includes filters, and it offers the possibility to adjust settings such as filter cut-off frequency, ADC sampling rate, and the accelerometer’s full-scale range.

The block diagram of the Analog Devices ADXL355

To extract data from the ADXL355, the ADCs must be enabled, and, if necessary, the sample rate, filtering, and full-scale settings should be configured. The data is then periodically read from the output registers. Essentially, interfacing with the ADXL355 boils down to reading from and writing to registers via the SPI interface. The ADXL355 datasheet outlines how to configure SPI for successful communication with the sensor, specifying an operational clock speed range for the SPI between 100kHz and 10MHz and the required SPI mode which is 0 (CPOL = 0, CPHA = 0). The next section will detail the significance of these settings for configuring SPI within ChibiOS.

The register interface

The ADXL355 features a register interface that consists of both configuration and data registers, each with a size of 8 bits. This implies that any read operation from a single register will yield 8 bits of data. These registers are assigned unique addresses ranging from 0x00 to 0x2F, indicating that the address space is represented with 7 bits. Some of these registers are read-only, while others can be both read from and written to.

The register map of the ADXL355

To interact with the sensor, it is necessary to implement functions in our firmware that can read from and write to these registers. The ADXL355 datasheet provides detailed instructions on how to accomplish these operations.

Command format

Communication with a single register on the ADXL355 involves a 16-bit data exchange. During this exchange, the first 8 bits represent a command sent to the sensor. This command specifies which register is being targeted and whether the operation is a read or a write. The image below illustrates the command format: the seven most significant bits represent the address of the targeted register, and the least significant bit indicates the operation type, with 1 for read and 0 for write.

The command format for the ADXL355 SPI communication

Given that SPI operates as a full-duplex interface, an 8-bit response from the device is expected for every 8-bit command sent. However, until the sensor processes the command, the MISO line remains high, and the initial 8 clock cycles return a dummy data word, 0xFF. The response during the subsequent 8 bits will vary depending on the specific command issued.

Single register read

When a read command is issued, the ADXL355 responds by transmitting an 8-bit value that corresponds to the contents of the specified register at that moment. During these 8 clock cycles, any data sent from the master to the device is disregarded by the sensor. The diagram below visualizes this process, highlighting the command phase, the data retrieval phase, and the periods during which the master’s input is ignored.

The diagram for a single-register read operation over SPI for the ADXL355

Single register write

Conversely, when a write command is executed, the master sends an 8-bit data packet to the device. During this transaction, the MISO line will remain high, indicating that no data is being sent back to the master. The following diagram illustrates the write operation, showing the command and data phases, and indicating when the sensor ignores input.

The diagram for a single-register write operation over SPI for the ADXL355

First interaction with the ADXL355: reading a register

Before we begin writing our library we want to make sure that we are able to communicate with the ADXL355 and check that we are on the right way with the analysis we conducted so far. When choosing between reading and writing operations, starting with a read is the obvious choice. Reading a register inherently provides a feedback for validation. Indeed, some of the register of the ADXL355 are preloaded with default values, which we can use to confirm if the read operation is correctly implemented. For this initial task, simplicity is key. We are dealing with multiple unknowns, and our goal is to confirm:

  • Are the connections done correctly?
  • Are the GPIOs configured correctly?
  • Is the SPI configuration accurate?
  • Are we using the SPI correctly to perform a register read?

If our code works, we can confidently affirm all these questions. To test this, we attempt to read the AD_DEVID register with the address 0x00, which is read-only and has a default value of 0xAD. The code below attempts to read this register, and then we will discuss how we developed this code step by step.

#include "ch.h"
#include "hal.h"
#define ADXL355_RW                          (1 << 0)
#define ADXL355_AD_DEVID_AD                 0x00
/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};
/* Buffer declaration. */
static uint8_t txbuf[2], rxbuf[2];
  
int main(void) {
  /* System initializations. */
  halInit();
  chSysInit();
  
  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  
  /* Activating the SPID1 with the chosen configuration. */
  spiStart(&SPID1, &spicfg);
  
  /* Preparing the first word with the command to read the 
     DEVID register. */
  txbuf[0] = (ADXL355_AD_DEVID_AD << 1) | ADXL355_RW;
  
  /* Performing an exchange. */
  spiSelect(&SPID1);
  spiExchange(&SPID1, 2, txbuf, rxbuf);
  spiUnselect(&SPID1);
  /* The value of the register is at this point 
     stored in rxbuf[1]. */
  while(true) {
    
    chThdSleepMilliseconds(100);
  }
}

This code performs a single read operation on the DEVID_AD register and then enters a loop that serves no purpose other than keeping the application running. This loop allows us to pause the execution and check the transmission buffer.

The key aspect of this operation involves exchanging two 8-bit words. The transmission buffer is pre-loaded so that the first word sent is the command to read the DEVID_AD register, while the second word remains unchanged as it is consistently disregarded by the slave device. After the exchange is completed, the second word in the receive buffer is filled with the value stored in the register, which, according to the datasheet, is 0xAD.

It is essential to note that for this application to build this code function correctly, you must have enabled the SPI peripheral in halconf.h and assigned the SPI1 peripheral to the driver in mcuconf.h.

GPIO configuration

Given that we are utilizing GPIO for SPI communication, it is crucial to ensure that the GPIO pins are configured correctly before any SPI activity begins. This configuration is handled by the following lines of code:

  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);


The rationale behind this configuration has been previously explained: every Arduino pin is directly linked to an STM32 GPIO. Pins ranging from D10 to D13 can be redirected to SPI1 when configured in Alternate Mode 5. The following table provides a concise summary of this information, which has been obtained by cross-referencing the SDP-K1 and the STM32F469NI datasheet:

Arduino Connector labelSTM32 GPIO labelModeFunction
ARD_D10PA15Output push-pullSPI Chip Select
ARD_D11PA7AF5SPI1 Master Output Slave Input
ARD_D12PB4AF5SPI1 Master Input Slave Output
ARD_D13PB3AF5SPI1 Clock
SPI pin map for the SDP-K1

SPI Configuration

The final step involves configuring the SPI, which is determined within the configuration structure defined at the beginning of the main code:

/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};

In our specific use case, we are employing SPI synchronously in non-circular mode. Thus, we disable the circular option and callbacks. The SPI is operated in master mode, which is why the slave parameter is set to false. It’s important to note that SPI_SELECT_MODE is configured as SPI_SELECT_MODE_LINE in halconf.h, allowing us to specify the chip select as a line, which here is LINE_ARD_D10.

The most complex part of this configuration lies in setting up the Control Register 1 of the SPI, the bit fields of which can be found in the microcontroller’s documentation.

The map of the SPI Control Register 1 of the STM32F4.

We provide extensive guidance on using bitmasks to compose register values for more user-friendly code in the article Registers and bitmasks, using specific registers as examples. While we encourage readers to refer to that article, what is crucial to understand here is that when we say the field cr1 is equal to SPI_CR1_BR_1 | SPI_CR1_BR_0, it means that the only bits set high by the user are bit 0 and bit 1 of the BR bitfield. Some of the bitfields are handled internally by the driver (e.g., MSRT, which determines whether the SPI operates in master or slave mode, or SPE, which enables the SPI). Here’s what we can derive from this configuration:

  • CPOL and CPHA are set to 0, putting the SPI in mode 0.
  • LSBFIRST is set to 0, indicating that the most significant bit of the word is transmitted first (suitable for most cases, including this one).
  • DFF is set to 0, meaning the SPI operates on 8-bit words.
  • Lastly, the BR field is set to 3, resulting in an SPI speed of fPCLK / 16.

Regarding the last statement, it’s important to note that the value of fPCLK represents the clock speed of the entire peripheral bus to which SPI1 is connected. For the STM32F469, this bus is APB2, which by default operates at 90MHz. Unless you’ve made changes to the clock tree configuration in your mcuconf.h, this should still be the case. This configuration sets the SPI speed to 5.625MHz, which falls within the acceptable range for the ADXL355. If you are interested in learning more about how to determine the clock speed of a peripheral in an ARM Cortex environment, we recommend reading ARM Cortex clock tree 101: Navigating clock domains, which provides a comprehensive overview and examples.

Testing the code

The most straightforward way to test this code is by placing a breakpoint immediately after the spiUnselect function call and evaluating the expression. If you are new to debugging in ChibiStudio, I recommend referring to the article Debugging on STM32 with ChibiStudio: the ultimate guide for comprehensive guidance. In the following picture, you can see a view of ChibiStudio right after the breakpoint has been triggered: the execution has paused, and the expression window is displaying the evaluation of rxbuf, confirming that the second word of the buffer has been filled with the expected value (0xAD). This demonstrates that with this code and the correct setup, we are indeed able to read registers from the ADXL355.

The screenshot of ChibiStudio during the evaluation of the receiving buffer after an spiExchange with the ADXL355

A reusable solution: creating a C library

In the previous example, we successfully performed a simple register read from the ADXL355 using SPI calls. This process can be summarized with the following code, taken from that example:

  /* Preparing the first word with the command to read the 
     DEVID register. */
  txbuf[0] = (ADXL355_AD_DEVID_AD << 1) | ADXL355_RW;
  
  /* Performing an exchange. */
  spiSelect(&SPID1);
  spiExchange(&SPID1, 2, txbuf, rxbuf);
  spiUnselect(&SPID1);
  /* The value of the register is at this point 
     stored in rxbuf[1]. */

However, this solution has limited reusability. Although it achieves its objective, applying it in other contexts is not easy. To improve code reusability, it is better to encapsulate this process in a function. This facilitates performing register reads. As the read register function is not the only one we will need, creating a C library for all ADXL355-related code is a smart decision. It simplifies building upon and reusing this code. This method also keeps the main code organized and makes future reuse more manageable.

The first thing we need to do is to create the structure of this library, include it in our project, and ensure that the project builds correctly with the new file properly included. To achieve this, we will create a subfolder within the project directory dedicated to the library. The project structure will then resemble the following description:

--{PROJECT ROOT}                - My project root.
  +--cfg/                       - Configuration files.
  |  +--chconf.h/               - OS configuration header.
  |  +--halconf.h/              - HAL configuration header.
  |  +--mcuconf.h/              - MCU configuration header.
  +--userlib/                   - Root of the library.
  |  +--adxl355.h               - Library header file.
  |  +--adxl355.c               - Library C source file.
  |  +--userlib.mk              - Userlib makefile.
  +--main.c                     - Our main.
  +--Makefile                   - Project Makefile.
  +--readme.txt                 - Optional readme.


To achieve this structure, we need to create a subfolder in our project named userlib. Within this subfolder, we should add three plain text files:

  1. adxl355.h: This file acts as the header file. It typically contains declarations for the ADXL355 library, such as function prototypes, macros, constants, and global variables.
  2. adxl355.c: This is the source file where the actual implementation of the ADXL355 library functions is written. It includes the logic and algorithms that drive the functionality described in the header file.
  3. userlib.mk: This file is a makefile specific to the user library. It contains the set of directives used by the make build automation tool to build the ADXL355 library, specifying how to compile and link the code.

Our first focus is on establishing the inclusion chain. To do so, we will make modifications to the Makefile, defining the location of the library and we will include the userlib makefile. This configuration takes place within the “Project, target, sources, and paths” section. Here, we set the variable USERLIB and include $(USERLIB)/userlib.mk.

##############################################################################
# Project, target, sources and paths
#
# Define project name here
PROJECT = ch
# Target settings.
MCU  = cortex-m4
# Imported source files and paths.
CHIBIOS  := ../../chibios2111
CONFDIR  := ./cfg
BUILDDIR := ./build
DEPDIR   := ./.dep
USERLIB  := ./userlib
# Licensing files.
include $(CHIBIOS)/os/license/license.mk
# Startup files.
include $(CHIBIOS)/os/common/startup/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk
# HAL-OSAL files (optional).
include $(CHIBIOS)/os/hal/hal.mk
include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk
include $(CHIBIOS)/os/hal/boards/ADI_EVAL_SDP_CK1Z/board.mk
include $(CHIBIOS)/os/hal/osal/rt-nil/osal.mk
# RTOS files (optional).
include $(CHIBIOS)/os/rt/rt.mk
include $(CHIBIOS)/os/common/ports/ARMv7-M/compilers/GCC/mk/port.mk
# Auto-build files in ./source recursively.
include $(CHIBIOS)/tools/mk/autobuild.mk
# Other files (optional).
include $(USERLIB)/userlib.mk

The next step involves populating the userlib.mk file to include the C file in the list of C source files and to add the library directory to the list of included directories.

# List of all the userlib files.
USERLIBSRC := $(USERLIB)/adxl355.c
# Required include directories
USERLIBINC := $(USERLIB)
# Shared variables
ALLCSRC += $(USERLIBSRC)
ALLINC  += $(USERLIBINC)

Finally, we can prepopulate both the header file and the source file with preprocessor guards and section dividers. We should also already include hal.h as this library is going to call the SPI API. Here’s how the adxl355.h file will look after this process:

#ifndef _ADXL355_H_
#define _ADXL355_H_
#include "hal.h"
/*===========================================================================*/
/* Driver constants.                                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Driver pre-compile time settings.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Derived constants and error checks.                                       */
/*===========================================================================*/
/*===========================================================================*/
/* Driver data structures and types.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Driver macros.                                                            */
/*===========================================================================*/
/*===========================================================================*/
/* External declarations.                                                    */
/*===========================================================================*/
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif /* _ADXL355_H_ */

Regarding the adxl355.c file, it remains largely empty, primarily serving as a placeholder with the inclusion of the appropriate header.

#include "adxl355.h"
/*===========================================================================*/
/* Driver local definitions.                                                 */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported variables.                                                */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local variables and types.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local functions.                                                   */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported functions.                                                */
/*===========================================================================*/

When attempting to build the code, there should be no errors, and you will observe that adxl355.c is now included in the building process.

ChibiStudio successfully compiles a project with the integration of a newly added library.

Now that we have the foundation we are going to build upon populating this files to have a fully reusable solution.

Registry level functions

First implementation of the read register function

At this stage, we aim to create a function that, given a register address, provide back the corresponding register value. This function will be structured as follows:

/**
 * @brief   Reads a generic register value using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       register address
 * @param[in] b         pointer to an output buffer.
 */
void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, uint8_t* b) {
  uint8_t txbuf[2], rxbuf[2];
  /* Preparing the transmission buffer. */
  txbuf[0] = ((reg) << 1) | ADXL355_RW;
  /* Performing an exchange. */
  spiSelect(spip);
  spiExchange(spip, 2, txbuf, rxbuf);
  spiUnselect(spip);
  *b = rxbuf[1];
}

This function receives an SPIDriver interface that must be initialized before calling the function. By doing so, the library does not need to handle hardware-specific details, allowing for those differences to be managed in the main code, keeping the library generic and abstract. The implementation of this function can be derived from our quick and dirty solution. This functions should be added to adxl355.c file under Driver exported function

/*===========================================================================*/
/* Driver exported functions.                                                */
/*===========================================================================*/
/**
 * @brief   Reads a generic register value using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       register address
 * @param[in] b         pointer to an output buffer.
 */
void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, uint8_t* b) {
  uint8_t txbuf[2], rxbuf[2];
  ...

But for the function to be reachable in the main its prototype needs to be added to the header file under External declarations

/*===========================================================================*/
/* External declarations.                                                    */
/*===========================================================================*/
#ifdef __cplusplus
extern "C" {
#endif
  void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, uint8_t* b);
#ifdef __cplusplus
}
#endif

At this point the function can be called from the main after including adxl355.h and the following code snippet demonstrates how to use this API in our main application:

adxl355ReadRegister(&SPID1, 9, &value);

An observation worth noting is the utilization of the magic number 9, which represents the address of the XDATA2 register. In practice, we can utilize constants to improve code readability. Indeed if 9 was defined as a constant called ADXL355_AD_XDATA2, our code would be easier to comprehend, and more intuitive. Looking at the following statement is immediately clear which register we intend to read.

adxl355ReadRegister(&SPID1, ADXL355_AD_XDATA2, &value);

Therefore, it is advisable to fill our library’s header files with all the constants related to our component that we might use in our application. This includes not just the register map but also various bitmasks, which will be useful when writing to registers, and additional parameters from the datasheet. The ADXL355 header file is the appropriate place for these constants under Driver constants. The following constants include parameters extracted from the datasheet, such as sensitivity and bias of the part, all register addresses identified by the prefix ADXL355_AD_, and bitmasks for configuration registers like the Filter, Power Control, and Range register, which we will use in the initialization phase.

/*===========================================================================*/
/* Driver constants.                                                         */
/*===========================================================================*/
/**
 * @brief   ADXL355 accelerometer subsystem characteristics.
 * @note    Sensitivity is expressed as milli-G/LSB whereas
 *          1 milli-G = 0.00980665 m/s^2.
 * @note    Bias is expressed as milli-G.
 *
 * @{
 */
#define ADXL355_ACC_SENS_2G                 0.003906f
#define ADXL355_ACC_SENS_4G                 0.007813f
#define ADXL355_ACC_SENS_8G                 0.015625f
#define ADXL355_ACC_BIAS                    0.0f
/** @} */
/**
 * @name    ADXL355 communication interfaces related bit masks
 * @{
 */
#define ADXL355_DI_MASK                     0xFF
#define ADXL355_DI(n)                       (1 << n)
#define ADXL355_AD_MASK                     0xFE
#define ADXL355_RW                          (1 << 0)
#define ADXL355_AD(n)                       (1 << (n + 1))
/** @} */
/**
 * @name    ADXL355 register addresses
 * @{
 */
#define ADXL355_AD_DEVID_AD                 0x00
#define ADXL355_AD_DEVID_MST                0x01
#define ADXL355_AD_PARTID                   0x02
#define ADXL355_AD_REVID                    0x03
#define ADXL355_AD_STATUS                   0x04
#define ADXL355_AD_FIFO_ENTRIES             0x05
#define ADXL355_AD_TEMP2                    0x06
#define ADXL355_AD_TEMP1                    0x07
#define ADXL355_AD_XDATA3                   0x08
#define ADXL355_AD_XDATA2                   0x09
#define ADXL355_AD_XDATA1                   0x0A
#define ADXL355_AD_YDATA3                   0x0B
#define ADXL355_AD_YDATA2                   0x0C
#define ADXL355_AD_YDATA1                   0x0D
#define ADXL355_AD_ZDATA3                   0x0E
#define ADXL355_AD_ZDATA2                   0x0F
#define ADXL355_AD_ZDATA1                   0x10
#define ADXL355_AD_FIFO_DATA                0x11
#define ADXL355_AD_OFFSET_X_H               0x1E
#define ADXL355_AD_OFFSET_X_L               0x1F
#define ADXL355_AD_OFFSET_Y_H               0x20
#define ADXL355_AD_OFFSET_Y_L               0x21
#define ADXL355_AD_OFFSET_Z_H               0x22
#define ADXL355_AD_OFFSET_Z_L               0x23
#define ADXL355_AD_ACT_EN                   0x24
#define ADXL355_AD_ACT_THRES_L              0x25
#define ADXL355_AD_ACT_THRES_H              0x26
#define ADXL355_AD_ACT_COUNTER              0x27
#define ADXL355_AD_FILTER                   0x28
#define ADXL355_AD_FIFO_SAMPLES             0x29
#define ADXL355_AD_INT_MAP                  0x2A
#define ADXL355_AD_SYNC                     0x2B
#define ADXL355_AD_RANGE                    0x2C
#define ADXL355_AD_POWER_CTL                0x2D
#define ADXL355_AD_SELF_TEST                0x2E
#define ADXL355_AD_RESET                    0x2F
/** @} */
/**
 * @name    ADXL355 Device Identifier
 * @{
 */
#define ADXL355_DEVID_MST                   0x1D
/** @} */
/**
 * @name    ADXL355_FILTER register bits definitions
 * @{
 */
#define ADXL355_FILTER_MASK                 0x7F
#define ADXL355_FILTER_ORD_LPF_0            (1 << 0)
#define ADXL355_FILTER_ORD_LPF_1            (1 << 1)
#define ADXL355_FILTER_ORD_LPF_2            (1 << 2)
#define ADXL355_FILTER_ORD_LPF_3            (1 << 3)
#define ADXL355_FILTER_HPF_CORNER_0         (1 << 4)
#define ADXL355_FILTER_HPF_CORNER_1         (1 << 5)
#define ADXL355_FILTER_HPF_CORNER_2         (1 << 6)
/** @} */
/**
 * @name    ADXL355_FIFO_SAMPLES register bits definitions
 * @{
 */
#define ADXL355_FIFO_SAMPLES_MASK           0x7F
#define ADXL355_FIFO_SAMPLES_BIT_0          (1 << 0)
#define ADXL355_FIFO_SAMPLES_BIT_1          (1 << 1)
#define ADXL355_FIFO_SAMPLES_BIT_2          (1 << 2)
#define ADXL355_FIFO_SAMPLES_BIT_3          (1 << 3)
#define ADXL355_FIFO_SAMPLES_BIT_4          (1 << 4)
#define ADXL355_FIFO_SAMPLES_BIT_5          (1 << 5)
#define ADXL355_FIFO_SAMPLES_BIT_6          (1 << 6)
/** @} */
/**
 * @name    ADXL355_INT_MAP register bits definitions
 * @{
 */
#define ADXL355_INT_MAP_MASK                0xFF
#define ADXL355_INT_MAP_RDY_EN1             (1 << 0)
#define ADXL355_INT_MAP_FULL_EN1            (1 << 1)
#define ADXL355_INT_MAP_OVR_EN1             (1 << 2)
#define ADXL355_INT_MAP_ACT_EN1             (1 << 3)
#define ADXL355_INT_MAP_RDY_EN2             (1 << 4)
#define ADXL355_INT_MAP_FULL_EN2            (1 << 5)
#define ADXL355_INT_MAP_OVR_EN2             (1 << 6)
#define ADXL355_INT_MAP_ACT_EN2             (1 << 7)
/** @} */
/**
 * @name    ADXL355_SYNC register bits definitions
 * @{
 */
#define ADXL355_SYNC_MASK                   0x07
#define ADXL355_SYNC_EXT_SYNC_0             (1 << 0)
#define ADXL355_SYNC_EXT_SYNC_1             (1 << 1)
#define ADXL355_SYNC_EXT_CLK                (1 << 2)
/** @} */
/**
 * @name    ADXL355_RANGE register bits definitions
 * @{
 */
#define ADXL355_RANGE_MASK                  0xC3
#define ADXL355_RANGE_RANGE_MASK            0x03
#define ADXL355_RANGE_RANGE_0               (1 << 0)
#define ADXL355_RANGE_RANGE_1               (1 << 1)
#define ADXL355_RANGE_INT_POL               (1 << 6)
#define ADXL355_RANGE_I2C_HS                (1 << 7)
/** @} */
 
/**
 * @name    ADXL355_POWER_CTL register bits definitions
 * @{
 */
#define ADXL355_POWER_CTL_MASK              0x07
#define ADXL355_POWER_CTL_STANDBY           (1 << 0)
#define ADXL355_POWER_CTL_TEMP_OFF          (1 << 1)
#define ADXL355_POWER_CTL_DRDY_OFF          (1 << 2)
/** @} */
/**
 * @name    ADXL355_SELT_TEST register bits definitions
 * @{
 */
#define ADXL355_SELF_TEST_MASK              0x03
#define ADXL355_SELF_TEST_ST1               (1 << 0)
#define ADXL355_SELF_TEST_ST2               (1 << 1)
/** @} */

At this point it is a good idea to create a basic main file that tests the components we have built so far and confirms that the functions perform as intended. In this main function, we initialize the system and configure the GPIOs for the SPI1 interface. We then activate SPI1 with our specified settings. The main task in this code is to read some register value from the ADXL355 sensor using the adxl355ReadRegister function. The retrieved value is stored in a variable named value.

#include "ch.h"
#include "hal.h"
#include "adxl355.h"
/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};
/* Buffer declaration. */
static uint8_t values[3] = {0x00};
int main(void) {
  /* System initializations. */
  halInit();
  chSysInit();
  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  /* Activating the SPID1 with the chosen configuration. */
  spiStart(&SPID1, &spicfg);
  while(true) {
    
    adxl355ReadRegister(&SPID1, ADXL355_AD_DEVID_AD, &values[0]);
    adxl355ReadRegister(&SPID1, ADXL355_AD_DEVID_MST, &values[1]);
    adxl355ReadRegister(&SPID1, ADXL355_AD_PARTID, &values[2]);
    chThdSleepMilliseconds(100);
  }
}

If we would place a breakpoint after the last adxl355ReadRegister then we could use the debugger to evaluate the content of the array value and verify that the function works as expected. According to the datasheet, these values should be 0xAD, 0x1D, and 0xED.

Testing the first implementation of the adxl355ReadRegister

A more efficient read register function

We have established the foundational elements of our library, and now it is time to focus on efficiency. When reading acceleration data from the ADXL355, we need to access 9 registers. Reading these registers individually isn’t very efficient due to the communication overhead. Each 16-bit communication frame, necessary for a single register read, uses half of its clock cycles for the read command itself. This leads to more than 50% overhead in the communication protocol, considering both the command and additional delays caused by nine separate DMA transactions.

A more efficient approach is to utilize the ADXL355’s multi-byte read feature. This method effectively extends the single byte read process. After the initial 16 clock cycles for a register read, providing additional clock pulses prompts the device to return the contents of the subsequent register in its address sequence. This technique significantly reduces the communication overhead

The diagram for a multi-register read operation over SPI for the ADXL355

For instance, if we initiate a read command for the register at address 0 and then provide 24 clock cycles, the ADXL355 will sequentially return the values of registers 0x00, 0x01, and 0x02. In software, this can be accomplished by conducting a longer SPI exchange before raising the chip select line. This leads to an improved implementation of the adxl355ReadRegister function.

/**
 * @brief   Reads one or multiple register values using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       starting register address
 * @param[in] n         number of consecutive registers to read
 * @param[out] b        pointer to the read buffer.
 */
void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b) {
  uint8_t txbuf;
  if(n > 0) {
    /* Preparing the transmission buffer. */
    txbuf = ((reg) << 1) | ADXL355_RW;
    spiSelect(spip);
    /* Sending the command. The data coming back is ignored. */
    spiSend(spip, 1, &txbuf);
    /* Reading back as many register as the value of n. */
    spiReceive(spip, n, b);
    spiUnselect(spip);
  }
}

As the prototype is now changed, to have this function correctly implemented we need to modify both the header and source files of our library. At this point we can read once again the registers DEVID_AD, DEVID_MST and PARTID but using the new and more efficient adxl355ReadRegister function.

#include "ch.h"
#include "hal.h"
#include "adxl355.h"
/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};
/* Buffer declaration. */
static uint8_t values[3];
int main(void) {
  /* System initializations. */
  halInit();
  chSysInit();
  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  /* Activating the SPID1 with the chosen configuration. */
  spiStart(&SPID1, &spicfg);
  while(true) {
    adxl355ReadRegister(&SPID1, ADXL355_AD_DEVID_AD, 3, values);
    chThdSleepMilliseconds(100);
  }
}

As usual, we can set a breakpoint on the chThdSleepMilliseconds to check if the buffer values match the expected ones.

Testing the new multi-byte implementation of the adxl355ReadRegister

The write register function

The next step involves writing to the registers of the ADXL355. We’ve already discussed how the device handles write operations. Compared to a single byte read, there are two key differences in a write operation:

  1. The read/write bit remains set to 0 when sending the command, indicating to the device that the operation is a write.
  2. During the second half of the communication, it’s the master that transmits the value to the device.

This communication is essentially unidirectional, as any data sent back from the device during a write operation is intentionally disregarded. In code, this sequence would be structured as follows:

/* Buffer declaration. */
uint8_t txbuf[2]
/* Preparing the first word with the command to read the register address reg. */
txbuf[0] = (reg << 1);
/* Preparing the second word with the value to write in the register. */
txbuf[1] = value;
spiSelect(&SPID1);
spiSend(&SPID1, 2, txbuf);
spiUnselect(&SPID1);

Similar to our previous discussion about reading registers, the current approach for writing to registers is not very efficient when dealing with multiple contiguous registers. To address this, we can consider a multi-byte write method. By continuously clocking in words, the master can write to contiguous registers in a more efficient manner.

The diagram for a multi-register write operation over SPI for the ADXL355

This necessitates the creation of a new function that we will add to our library. This function should be placed in the adxl355.c file, immediately following the previously implemented adxl355ReadRegister function.

/**
 * @brief   Writes one or multiple register values using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       starting register address
 * @param[in] n         number of consecutive registers to read
 * @param[in] b         pointer to the wrote buffer.
 */
void adxl355WriteRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b) {
  uint8_t txbuf;
  if(n > 0) {
    /* Preparing the transmission buffer. */
    txbuf = ((reg) << 1);
    spiSelect(spip);
    /* Sending the command. The data coming back is ignored. */
    spiSend(spip, 1, &txbuf);
    /* Writing as many register as the value of n. */
    spiSend(spip, n, b);
    spiUnselect(spip);
  }
}

To be able to call this function from our main program, we also need to place its prototype in the adxl355.h header file, specifically in the External declarations section where we previously placed the prototype for the read register function.

/*===========================================================================*/
/* External declarations.                                                    */
/*===========================================================================*/
#ifdef __cplusplus
extern "C" {
#endif
  void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
  void adxl355WriteRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
#ifdef __cplusplus
}
#endif

Before proceeding, it is a good idea to validate this new functionality employing a combination of write and read operations. Our strategy involves writing values to specific registers and then reading them back to ensure that it works.

The ideal candidates for this test are the contiguous OFFSET registers. These registers can be assigned any value without affecting the device’s operation. For this test, I plan to write values to the OFFSET_X_H, OFFSET_X_L, andOFFSET_Y_H registers, and then verify that they indeed hold the values I wrote.

#include "ch.h"
#include "hal.h"
#include "adxl355.h"
/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};
/* Buffer declaration. */
static uint8_t values[3] = {0x01, 0x04, 0x18};
static uint8_t readback[3] = {0x00, 0x00, 0x00};
int main(void) {
  /* System initializations. */
  halInit();
  chSysInit();
  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  /* Activating the SPID1 with the chosen configuration. */
  spiStart(&SPID1, &spicfg);
  adxl355WriteRegister(&SPID1, ADXL355_AD_OFFSET_X_H, 3, values);
  adxl355ReadRegister(&SPID1, ADXL355_AD_OFFSET_X_H, 3, readback);
  /* The value of the register is at this point
     stored in value. */
  while(true) {
    chThdSleepMilliseconds(100);
  }
}

The testing process involves the usual step of setting a convenient breakpoint and then evaluating the contents of the buffers.

Testing the adxl355WriteRegister

At this stage, we possess all the necessary tools to interact with the ADXL355 at the register level. However, our final application requires the ability to read the output data registers and this is going to be the goal of the next chapter

The ADXL355 output data registers

Understanding how to read data registers and extract meaningful values from the ADXL355 can be challenging, as it requires a fundamental understanding of certain electronic concepts that are often assumed to be known.

The primary challenge is that while the ADXL355’s ADCs produce 20-bit values, the register size is limited to 8 bits. This means the data for each axis is split across three registers. To obtain a complete data set, we need to read 9 registers in total (3 for each axis) and then combine these values axis by axis to obtain the final acceleration readings.

In the following we are going to make an example of how this works focusing on a single register and then expand the concept to all the register creating a function to read accelerations.

From register to meaningful data

For simplicity, we will focus on the X-axis, though the same method applies to the other axes. The raw data for the X-axis is contained in the registers XDATA3, XDATA2, and XDATA1.

The data register of the ADXL355

To form the complete value, we use bit shifting and bitwise OR operations. In C, this translates to:

int32_t x_value = (xdata3_val << 12) | (xdata2_val << 4) | (xdata1_val >> 4);
Assembling the value of the X-axis

However, this value is a 20-bit signed integer in a 32-bit signed integer format, which can cause representation errors for negative numbers. To address this, we perform sign extension: we identify the 20th bit (sign bit) and replicate it across the higher order bits (21st to 32nd), leaving the lower bits unchanged. For example, a 20-bit number 0b1011 0001 0001 0101 1110 becomes 0b1111 1111 1111 1011 0001 0001 0101 1110 when extended to 32 bits. Positive numbers remain unaffected by this process.

We can then expand our previous C code to include for this step:

/* Composing the value. */
int32_t x_value = (xdata3_val << 12) | (xdata2_val << 4) | (xdata1_val >> 4);
/* If the most significant bit is 1 then add the extra 1 bits. */
if(x_value & 0x80000) {
  x_value |= 0xFFF00000U;
}

The value obtained from this operation is a signed integer representing the acceleration on the X-axis in digital format. The measurement unit for this integer is the Least Significant Bit (LSB) of the ADC. To convert it into a physical acceleration value expressed in meters per second squared (m/s²), in ‘g’ units (where 1g = 9.81 m/s²), or in milli-g (mg, with 1000 mg = 9.81 m/s²), we must multiply the integer by the ADC’s sensitivity.

However, the sensitivity depends on the full-scale value of the accelerometer, which can be configured by writing to one of the ADXL355 registers.

An excerpt of the ADXL355 Datasheet reporting the ADC sensitivities

The sensitivity value is provided as LSB/g in the datasheet, which also offers the scale factor in micrograms per LSB (µg/LSB). These values have already been included in a previous step in the adxl355.h header file as a scale factor in milligrams per LSB (mg/LSB).

/**
 * @brief   ADXL355 accelerometer subsystem characteristics.
 * @note    Sensitivity is expressed as milli-G/LSB whereas
 *          1 milli-G = 0.00980665 m/s^2.
 * @note    Bias is expressed as milli-G.
 *
 * @{
 */
#define ADXL355_ACC_SENS_2G                 0.003906f
#define ADXL355_ACC_SENS_4G                 0.007813f
#define ADXL355_ACC_SENS_8G                 0.015625f
#define ADXL355_ACC_BIAS                    0.0f
/** @} */

If, for example, our ADXL355 is set to ±4g, converting the X-Axis reading from LSB to mg would require the following operation:

/* Converting the value in mg. */
x_value_f = (float)x_value * ADXL355_ACC_SENS_4G;

A function to read all the axes

Now that we understand how the output data registers operate, we can proceed to implement a function that reads data from all the axes and combines it into values usable by the application. Our library is still basic, and we will later see that we need to initialize some registers in the main application, including the full-scale value. This implies that our library does not know the current full-scale value or the sensitivity value to apply. Therefore, we will implement a read raw function that provides the acceleration data expressed in LSB.

This is represented by the following function which needs to be placed in adxl355.c after the previously implemented functions.

/**
 * @brief   Read all the accelerations from the ADXL355.
 * @pre     The SPI interface must be initialized and the driver started.
 * @note    This data is retrieved from MEMS register without any algebraical
 *          manipulation.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[out] b        pointer to the data buffer.
 */
void adxl355ReadRaw(SPIDriver* spip, int32_t* b) {
  uint8_t tmp[9], i;
  /* Reading all the data registes. */
  adxl355ReadRegister(spip, ADXL355_AD_XDATA3, 9, tmp);
  
   for(i = 0; i < 3; i++) {
    *b = (tmp[0 + 3 * i] << 12) | (tmp[1 + 3 * i] << 4) |
         (tmp[2 + 3 * i] >> 4);
    /* If the most significant bit is 1 then add the extra 1 bits. */
    if(*b & 0x80000) {
      *b |= 0xFFF00000;
    }
    /* Next buffer element. */
    b++;
  }
}

As before we need to add the prototype to the header file

/*===========================================================================*/
/* External declarations.                                                    */
/*===========================================================================*/
#ifdef __cplusplus
extern "C" {
#endif
  void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
  void adxl355WriteRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
  void adxl355ReadRaw(SPIDriver* spip, int32_t* b);
#ifdef __cplusplus
}
#endif

And we leave to the next chapter the task to write an application to exploit this function.

Overview of the library

Until now, we have only discussed segments of the library, offering fragments of the entire picture. Before proceeding with the final application, it is sensible to review everything holistically to ensure that the reader fully grasps what we have described. The following code sections plainly display the complete code of adxl355.h and adxl355.c.

#ifndef _ADXL355_H_
#define _ADXL355_H_
#include "hal.h"
/*===========================================================================*/
/* Driver constants.                                                         */
/*===========================================================================*/
/**
 * @brief   ADXL355 accelerometer subsystem characteristics.
 * @note    Sensitivity is expressed as milli-G/LSB whereas
 *          1 milli-G = 0.00980665 m/s^2.
 * @note    Bias is expressed as milli-G.
 *
 * @{
 */
#define ADXL355_ACC_SENS_2G                 0.003906f
#define ADXL355_ACC_SENS_4G                 0.007813f
#define ADXL355_ACC_SENS_8G                 0.015625f
#define ADXL355_ACC_BIAS                    0.0f
/** @} */
/**
 * @name    ADXL355 communication interfaces related bit masks
 * @{
 */
#define ADXL355_DI_MASK                     0xFF
#define ADXL355_DI(n)                       (1 << n)
#define ADXL355_AD_MASK                     0xFE
#define ADXL355_RW                          (1 << 0)
#define ADXL355_AD(n)                       (1 << (n + 1))
/** @} */
/**
 * @name    ADXL355 register addresses
 * @{
 */
#define ADXL355_AD_DEVID_AD                 0x00
#define ADXL355_AD_DEVID_MST                0x01
#define ADXL355_AD_PARTID                   0x02
#define ADXL355_AD_REVID                    0x03
#define ADXL355_AD_STATUS                   0x04
#define ADXL355_AD_FIFO_ENTRIES             0x05
#define ADXL355_AD_TEMP2                    0x06
#define ADXL355_AD_TEMP1                    0x07
#define ADXL355_AD_XDATA3                   0x08
#define ADXL355_AD_XDATA2                   0x09
#define ADXL355_AD_XDATA1                   0x0A
#define ADXL355_AD_YDATA3                   0x0B
#define ADXL355_AD_YDATA2                   0x0C
#define ADXL355_AD_YDATA1                   0x0D
#define ADXL355_AD_ZDATA3                   0x0E
#define ADXL355_AD_ZDATA2                   0x0F
#define ADXL355_AD_ZDATA1                   0x10
#define ADXL355_AD_FIFO_DATA                0x11
#define ADXL355_AD_OFFSET_X_H               0x1E
#define ADXL355_AD_OFFSET_X_L               0x1F
#define ADXL355_AD_OFFSET_Y_H               0x20
#define ADXL355_AD_OFFSET_Y_L               0x21
#define ADXL355_AD_OFFSET_Z_H               0x22
#define ADXL355_AD_OFFSET_Z_L               0x23
#define ADXL355_AD_ACT_EN                   0x24
#define ADXL355_AD_ACT_THRES_L              0x25
#define ADXL355_AD_ACT_THRES_H              0x26
#define ADXL355_AD_ACT_COUNTER              0x27
#define ADXL355_AD_FILTER                   0x28
#define ADXL355_AD_FIFO_SAMPLES             0x29
#define ADXL355_AD_INT_MAP                  0x2A
#define ADXL355_AD_SYNC                     0x2B
#define ADXL355_AD_RANGE                    0x2C
#define ADXL355_AD_POWER_CTL                0x2D
#define ADXL355_AD_SELF_TEST                0x2E
#define ADXL355_AD_RESET                    0x2F
/** @} */

/**
 * @name    ADXL355 Device Identifier
 * @{
 */
#define ADXL355_DEVID_MST                   0x1D
/** @} */
/**
 * @name    ADXL355_FILTER register bits definitions
 * @{
 */
#define ADXL355_FILTER_MASK                 0x7F
#define ADXL355_FILTER_ORD_LPF_0            (1 << 0)
#define ADXL355_FILTER_ORD_LPF_1            (1 << 1)
#define ADXL355_FILTER_ORD_LPF_2            (1 << 2)
#define ADXL355_FILTER_ORD_LPF_3            (1 << 3)
#define ADXL355_FILTER_HPF_CORNER_0         (1 << 4)
#define ADXL355_FILTER_HPF_CORNER_1         (1 << 5)
#define ADXL355_FILTER_HPF_CORNER_2         (1 << 6)
/** @} */
/**
 * @name    ADXL355_FIFO_SAMPLES register bits definitions
 * @{
 */
#define ADXL355_FIFO_SAMPLES_MASK           0x7F
#define ADXL355_FIFO_SAMPLES_BIT_0          (1 << 0)
#define ADXL355_FIFO_SAMPLES_BIT_1          (1 << 1)
#define ADXL355_FIFO_SAMPLES_BIT_2          (1 << 2)
#define ADXL355_FIFO_SAMPLES_BIT_3          (1 << 3)
#define ADXL355_FIFO_SAMPLES_BIT_4          (1 << 4)
#define ADXL355_FIFO_SAMPLES_BIT_5          (1 << 5)
#define ADXL355_FIFO_SAMPLES_BIT_6          (1 << 6)
/** @} */
/**
 * @name    ADXL355_INT_MAP register bits definitions
 * @{
 */
#define ADXL355_INT_MAP_MASK                0xFF
#define ADXL355_INT_MAP_RDY_EN1             (1 << 0)
#define ADXL355_INT_MAP_FULL_EN1            (1 << 1)
#define ADXL355_INT_MAP_OVR_EN1             (1 << 2)
#define ADXL355_INT_MAP_ACT_EN1             (1 << 3)
#define ADXL355_INT_MAP_RDY_EN2             (1 << 4)
#define ADXL355_INT_MAP_FULL_EN2            (1 << 5)
#define ADXL355_INT_MAP_OVR_EN2             (1 << 6)
#define ADXL355_INT_MAP_ACT_EN2             (1 << 7)
/** @} */
/**
 * @name    ADXL355_SYNC register bits definitions
 * @{
 */
#define ADXL355_SYNC_MASK                   0x07
#define ADXL355_SYNC_EXT_SYNC_0             (1 << 0)
#define ADXL355_SYNC_EXT_SYNC_1             (1 << 1)
#define ADXL355_SYNC_EXT_CLK                (1 << 2)
/** @} */
/**
 * @name    ADXL355_RANGE register bits definitions
 * @{
 */
#define ADXL355_RANGE_MASK                  0xC3
#define ADXL355_RANGE_RANGE_MASK            0x03
#define ADXL355_RANGE_RANGE_0               (1 << 0)
#define ADXL355_RANGE_RANGE_1               (1 << 1)
#define ADXL355_RANGE_INT_POL               (1 << 6)
#define ADXL355_RANGE_I2C_HS                (1 << 7)
/** @} */
/**
 * @name    ADXL355_POWER_CTL register bits definitions
 * @{
 */
#define ADXL355_POWER_CTL_MASK              0x07
#define ADXL355_POWER_CTL_STANDBY           (1 << 0)
#define ADXL355_POWER_CTL_TEMP_OFF          (1 << 1)
#define ADXL355_POWER_CTL_DRDY_OFF          (1 << 2)
/** @} */
/**
 * @name    ADXL355_SELT_TEST register bits definitions
 * @{
 */
#define ADXL355_SELF_TEST_MASK              0x03
#define ADXL355_SELF_TEST_ST1               (1 << 0)
#define ADXL355_SELF_TEST_ST2               (1 << 1)
/** @} */

/*===========================================================================*/
/* Driver pre-compile time settings.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Derived constants and error checks.                                       */
/*===========================================================================*/
/*===========================================================================*/
/* Driver data structures and types.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Driver macros.                                                            */
/*===========================================================================*/
/*===========================================================================*/
/* External declarations.                                                    */
/*===========================================================================*/
#ifdef __cplusplus
extern "C" {
#endif
  void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
  void adxl355WriteRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b);
  void adxl355ReadRaw(SPIDriver* spip, int32_t* b);
#ifdef __cplusplus
}
#endif
#endif /* _ADXL355_H_ */
#include "adxl355.h"
/*===========================================================================*/
/* Driver local definitions.                                                 */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported variables.                                                */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local variables and types.                                         */
/*===========================================================================*/
/*===========================================================================*/
/* Driver local functions.                                                   */
/*===========================================================================*/
/*===========================================================================*/
/* Driver exported functions.                                                */
/*===========================================================================*/
/**
 * @brief   Reads one or multiple register values using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       starting register address
 * @param[in] n         number of consecutive registers to read
 * @param[out] b        pointer to the read buffer.
 */
void adxl355ReadRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b) {
  uint8_t txbuf;
  if(n > 0) {
    /* Preparing the transmission buffer. */
    txbuf = ((reg) << 1) | ADXL355_RW;
    spiSelect(spip);
    /* Sending the command. The data coming back is ignored. */
    spiSend(spip, 1, &txbuf);
    /* Reading back as many register as the value of n. */
    spiReceive(spip, n, b);
    spiUnselect(spip);
  }
}
/**
 * @brief   Writes one or multiple register values using SPI.
 * @pre     The SPI interface must be initialized and the driver started.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[in] reg       starting register address
 * @param[in] n         number of consecutive registers to read
 * @param[in] b         pointer to the write buffer.
 */
void adxl355WriteRegister(SPIDriver* spip, uint8_t reg, size_t n, uint8_t* b) {
  uint8_t txbuf;
  if(n > 0) {
    /* Preparing the transmission buffer. */
    txbuf = ((reg) << 1);
    spiSelect(spip);
    /* Sending the command. The data coming back is ignored. */
    spiSend(spip, 1, &txbuf);
    /* Writing as many register as the value of n. */
    spiSend(spip, n, b);
    spiUnselect(spip);
  }
}
/**
 * @brief   Read all the accelerations from the ADXL355.
 * @pre     The SPI interface must be initialized and the driver started.
 * @note    This data is retrieved from MEMS register without any algebraical
 *          manipulation.
 *
 * @param[in] spip      pointer to @p SPIDriver interface.
 * @param[out] b        pointer to the data buffer.
 */
void adxl355ReadRaw(SPIDriver* spip, int32_t* b) {
  uint8_t tmp[9], i;
  /* Reading all the data registers. */
  adxl355ReadRegister(spip, ADXL355_AD_XDATA3, 9, tmp);
  
   for(i = 0; i < 3; i++) {
    *b = (tmp[0 + 3 * i] << 12) | (tmp[1 + 3 * i] << 4) |
         (tmp[2 + 3 * i] >> 4);
    /* If the most significant bit is 1 then add the extra 1 bits. */
    if(*b & 0x80000) {
      *b |= 0xFFF00000;
    }
    /* Next buffer element. */
    b++;
  }
}

A fully functional application

Configuring the ADXL355

To activate and configure the ADXL355 accelerometer, we need to modify a few registers. Initially, the device is in standby mode. To activate it, we set the STANDBY bitfield in the POWER_CTL register to 0. This action starts the ADCs, which then continuously sample and populate the data registers with acceleration readings.

The ADXL355 offers a configurable range (±2g, ±4g, or ±8g), set by default to ±2g. For our application, we’ll adjust this to ±4g using the RANGE register. We’ll also set the ADC’s Output Data Rate to its maximum of 4KHz. This setting, in turn, tunes the Low-Pass Filter to 1KHz. For the time being, we will disable the High Pass Filter.

With these settings, our initialization of the ADXL355 is as follows:

/* Putting the ADXL355 out of stand by. */
reg = 0;
adxl355WriteRegister(&SPID1, ADXL355_AD_POWER_CTL, 1, reg);
/* Configuring the range to ±4g. */
reg = ADXL355_RANGE_RANGE_1;
adxl355WriteRegister(&SPID1, ADXL355_AD_RANGE, 1, reg);
/* Configuring the ODR to 4KHz and disabling the HPF. */
reg = 0;
adxl355WriteRegister(&SPID1, ADXL355_AD_FILTER, 1, reg);

From this point forward, we can read data from the output registers to obtain acceleration measurements.

Completing the application

In this code segment, we’ve expanded our functionality to include all three axes of the ADXL355 and to output the results via the debugger’s serial connection

#include "ch.h"
#include "hal.h"
#include "adxl355.h"
#include "chprintf.h"
/*
 * Non-circular SPI.
 * CR1: CPOL = 0, CPHA = 0, BR = 5.625MHz, Word Size: 8-bit
 */
static SPIConfig spicfg = {
  .circular         = false,
  .slave            = false,
  .data_cb          = NULL,
  .error_cb         = NULL,
  .ssline           = LINE_ARD_D10,
  .cr1              = SPI_CR1_BR_1 | SPI_CR1_BR_0,
  .cr2              = 0
};

static uint8_t tmp[9] = {0}, i;
static int32_t raw_data[3];
static float cooked_data[3];
static BaseSequentialStream* chp = (BaseSequentialStream*) &SD5;
int main(void) {
  /* System initializations. */
  halInit();
  chSysInit();
  /* Activating the SD5 with the default configuration. */
  sdStart(&SD5, NULL);
  /* Configuring the GPIOs used by the SPI1. */
  palSetLineMode(LINE_ARD_D10, PAL_MODE_OUTPUT_PUSHPULL);
  palSetLineMode(LINE_ARD_D11, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D12, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  palSetLineMode(LINE_ARD_D13, PAL_MODE_ALTERNATE(5) | PAL_STM32_OSPEED_HIGHEST);
  /* Activating the SPID1 with the chosen configuration. */
  spiStart(&SPID1, &spicfg);
  /* Setting the offset registers to 0. */
  adxl355WriteRegister(&SPID1, ADXL355_AD_OFFSET_X_H, 6, tmp);
  /* Putting the ADXL355 out of stand by. */
  tmp[0] = 0;
  adxl355WriteRegister(&SPID1, ADXL355_AD_POWER_CTL, 1, tmp);
  /* Configuring the range to ±4g. */
  tmp[0] = ADXL355_RANGE_RANGE_1;
  adxl355WriteRegister(&SPID1, ADXL355_AD_RANGE, 1, tmp);
  /* Configuring the ODR to 4KHz and disabling the HPF. */
  tmp[0] = 0;
  adxl355WriteRegister(&SPID1, ADXL355_AD_FILTER, 1, tmp);
  /* The ADXL355 is not configured and ready to go. */
  while(true) {
    /* Getting the accelerations as LSB. */
    adxl355ReadRaw(&SPID1, raw_data);
    /* Printing the data. */
    chprintf(chp, "Data: ");
    for(i = 0; i < 3; i++) {
      /* Converting the value in mg. */
      cooked_data[i] = (float)raw_data[i] * ADXL355_ACC_SENS_4G;
      chprintf(chp, "%0.4f ", cooked_data[i]);
    }
    chprintf(chp, "\r\n");
    chThdSleepMilliseconds(10);
  }
}

In this code, we also needed to activate Serial Driver 5, which is connected to the SDP-K1 debugger’s debug port. Additionally, we imported the chprintf library to enable printing formatted strings via the serial connection. This step requires some changes to the makefile, but we will address those details later.

It’s important to note that before configuring the ADXL355, we set the offset registers to zero. This step is crucial, as we had previously experimented with these registers. Misconfigured offset registers could lead to inaccurate readings, manifesting as an unexpected offset in the data.

The main loop of our program expands upon the concepts introduced in the previous chapter. It reads all nine data registers and then assembles the raw values for each axis into the raw_data buffer. These values are subsequently scaled and stored in the cooked_data buffer, which is then printed to the serial output.

By connecting a terminal to the debug port at a baud rate of 38400, we can observe the accelerometer data being displayed in real time.

A screenshot of ChibiStudio while the final application is running printing on the terminal

As mentioned earlier, we’ve incorporated the chprintf library into our project. This integration necessitates two specific modifications to the makefile:

  1. Inclusion in the Project Section: Within the ‘Project, Target, Sources, and Paths’ section of the makefile, we must include streams.mk. This step ensures that the necessary streams for chprintf are available in our project.
  2. Enabling Floating Point Support: In the ‘User’ section of the makefile, we need to redefine CHPRINTF_USE_FLOAT to true. This change enables the use of floating-point numbers in the chprintf function, which is essential for accurately formatting and displaying our accelerometer data.
##############################################################################
# Project, target, sources and paths
#
# Define project name here
PROJECT = ch
# Target settings.
MCU  = cortex-m4
# Imported source files and paths.
CHIBIOS  := ../../chibios2111
CONFDIR  := ./cfg
BUILDDIR := ./build
DEPDIR   := ./.dep
USERLIB  := ./userlib
# Licensing files.
include $(CHIBIOS)/os/license/license.mk
# Startup files.
include $(CHIBIOS)/os/common/startup/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk
# HAL-OSAL files (optional).
include $(CHIBIOS)/os/hal/hal.mk
include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk
include $(CHIBIOS)/os/hal/boards/ADI_EVAL_SDP_CK1Z/board.mk
include $(CHIBIOS)/os/hal/osal/rt-nil/osal.mk
# RTOS files (optional).
include $(CHIBIOS)/os/rt/rt.mk
include $(CHIBIOS)/os/common/ports/ARMv7-M/compilers/GCC/mk/port.mk
# Auto-build files in ./source recursively.
include $(CHIBIOS)/tools/mk/autobuild.mk
# Other files (optional).
include $(USERLIB)/userlib.mk
include $(CHIBIOS)/os/hal/lib/streams/streams.mk
...
##############################################################################
# Start of user section
#
# List all user C define here, like -D_DEBUG=1
UDEFS = -DCHPRINTF_USE_FLOAT=1
# Define ASM defines here
UADEFS =
# List all user directories here
UINCDIR =
# List the user directory to look for the libraries here
ULIBDIR =
# List all user libraries here
ULIBS =

Conclusions

In this article, we have demonstrated how to develop code that utilizes the SPI interface to configure and retrieve data from an ADXL355 accelerometer. The library we’ve created is very rudimental, providing basic access at the register level and a read raw function. There’s potential to evolve this into a more comprehensive library. Such an enhanced library could offer a streamlined API for configuring the MEMS device through a single interface and retrieving accelerometer data already converted in the final format while abstractly managing the implementation details.

For now, we leave the development of this more advanced library as an exercise for the reader. It could also serve as a topic for a subsequent article. This step would not only simplify the user experience but also encapsulate the complexities, making the library more accessible and versatile for various applications.

Be the first to reply at Hands-on exercise with the ADXL355 and ChibiOS SPI

Leave a Reply