SPI Driver for ADXL345 Accelerometer
1. Core Principles of SPI Protocol
SPI is a full-duplex, synchronous, master-slave communication bus. To drive peripherals, first master the physical layer wiring and timing rules.
1.1 Physical Layer: Four-Wire Connection Logic
Each wire in the standard SPI four-wire system has a clear function. The specific connections between the IMX6ULL (master device) and ADXL345 (slave device) are as follows:
| Signal Line | Function Description | IMX6ULL Pin (ECSPI3) | ADXL345 Pin |
|---|---|---|---|
| SCLK | Serial clock, generated by master | UART2_RX_DATA | SCLK |
| MOSI | Master Out Slave In | UART2_CTS_B | MOSI |
| MISO | Master In Slave Out | UART2_RTS_B | MISO |
| CS/SS | Slave Select (active low, GPIO emulated) | UART2_TX_DATA (GPIO1_IO20) | CS |
Note: The IMX6ULL's ECSPI3 has a built-in hardware CS, but this article uses GPIO to emulate CS for greater flexibility and to help beginners understand the core logic of "chip select."
1.2 Timing Rules: CPOL and CPHA Determine Communication Mode
SPI timing is defined by two key parameters: CPOL (Clock Polarity) and CPHA (Clock Phase) , which combine to form four communication modes. ADXL345 recommends Mode 3 (CPOL=1, CPHA=1).
| Parameter | Meaning | Mode 3 Config |
|---|---|---|
| CPOL | Clock idle level: 0=low idle, 1=high idle | 1 |
| CPHA | Data sampling edge: 0=first clock edge, 1=second clock edge | 1 |
Timing logic for Mode 3:
- SCLK remains high when idle;
- The master sends data on the falling edge of SCLK, and the slave samples data on the rising edge (second edge).
2. IMX6ULL ECSPI Controller: From Pins to Register Configuration
The IMX6ULL's SPI controller is called ECSPI (Enhanced Configurable SPI). Using ECSPI3 as an example, we complete the underlying hardware initialization.
2.1 Pin Multiplexing Configuration
IMX6ULL pins are multifunctional. The corresponding pins must first be multiplexed as ECSPI3 functions, and electrical characteristics (pull-up, speed, etc.) must be configured.
c
#include "imx6ull.h"
// Pin multiplexing configuration function (low-level register encapsulation, no modification needed)
void IOMUXC_SetPinMux(unsigned int mux_register, unsigned int mux_mode);
void IOMUXC_SetPinConfig(unsigned int config_register, unsigned int config_value);
// ECSPI3 pin initialization
void spi3_pin_init(void)
{
// 1. MISO: UART2_RTS_B multiplexed as ECSPI3_MISO
IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1); // Pull-up, 100MHz speed
// 2. MOSI: UART2_CTS_B multiplexed as ECSPI3_MOSI
IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
// 3. SCLK: UART2_RX_DATA multiplexed as ECSPI3_SCLK
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
// 4. CS: UART2_TX_DATA multiplexed as GPIO1_IO20
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
// GPIO1_IO20 set as output, initial high (ADXL345 not selected)
GPIO1->GDIR |= (1 << 20);
GPIO1->DR |= (1 << 20);
}
2.2 ECSPI3 Core Register Configuration
The IMX6ULL's ECSPI controller core configuration is in CONREG (Control Register) and CONFIGREG (Configuration Register). The focus is on matching ADXL345's Mode 3 timing while setting master mode, data length, and clock division.
c
// ECSPI3 initialization: Configure timing, mode, and data length
void spi3_init(void)
{
// 1. Initialize pins first
spi3_pin_init();
// 2. Reset CONREG register
ECSPI3->CONREG = 0;
/* CONREG parameter breakdown:
* 7<<20: BURST_LENGTH=7 → 8-bit data length (0~7 total 8 bits)
* 0x0E<<12: CLK_CTL=14 → Clock division (IPG_CLK=66MHz → 66/(14+1)=4.4MHz, compatible with ADXL345)
* 2<<8: CHIP_SELECT=2 → Select channel 0 (ECSPI3_CH0)
* 1<<4: MODE=1 → Master mode
* 1<<3: SCLK_CTL=1 → Clock controlled by CONREG's CLK_CTL
* 1<<0: ENABLE=1 → Enable ECSPI3
*/
ECSPI3->CONREG |= (7 << 20) | (0x0E << 12) | (2 << 8) | (1 << 4) | (1 << 3) | (1 << 0);
// 3. Reset CONFIGREG register
ECSPI3->CONFIGREG = 0;
/* CONFIGREG parameter breakdown:
* 1<<20: SCLK_POL=1 → CPOL=1 (clock idle high)
* 1<<4: MOSI_POL=0 → MOSI pin polarity normal (no inversion)
* 1<<0: PHASE=1 → CPHA=1 (sample on second edge)
*/
ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
}
3. ADXL345 Driver Implementation: SPI Read/Write + Sensor Control
The core operation of ADXL345 is "register read/write"---writing configurations and reading data via SPI to/from sensor registers. First, implement the underlying SPI read/write, then encapsulate sensor-specific interfaces.
3.1 SPI Full-Duplex Read/Write Base Function
SPI is full-duplex communication: sending 1 byte while simultaneously receiving 1 byte. Thus, the core of the read/write function is "send data + wait for receive," using ECSPI's TX/RX FIFO for data transfer.
c
// SPI3 channel 0 read/write function: Send 1 byte, receive 1 byte (full-duplex)
unsigned int spi3_ch0_write_and_read(unsigned int data)
{
// Wait for TX FIFO to be empty (ready for new data)
while((ECSPI3->STATREG & (1 << 0)) == 0);
// Write data to TXDATA
ECSPI3->TXDATA = data;
// Wait for RX FIFO to have data (data received)
while((ECSPI3->STATREG & (1 << 3)) == 0);
// Return received data
return ECSPI3->RXDATA;
}
3.2 ADXL345 Register Read/Write Encapsulation
ADXL345 register read/write follows fixed rules:
- Read: Set the highest bit of the register address (reg_addr | 0x80), send the address followed by 0xFF (dummy data) to trigger the clock, then read the return value;
- Write: Clear the highest bit of the register address, send the address followed by the data to be written.
c
// ADXL345 read register: reg_addr=register address, returns register value
unsigned char adxl345_read(unsigned char reg_addr)
{
unsigned char ret = 0;
// 1. Pull CS low to select ADXL345
GPIO1->DR &= ~(1 << 20);
// 2. Send read command (highest bit of address set to 1)
spi3_ch0_write_and_read(reg_addr | 0x80);
// 3. Send dummy data 0xFF, read sensor return value
ret = spi3_ch0_write_and_read(0xFF);
// 4. Pull CS high to end communication
GPIO1->DR |= (1 << 20);
return ret;
}
// ADXL345 write register: reg_addr=register address, data=value to write
void adxl345_write(unsigned char reg_addr, unsigned char data)
{
// 1. Pull CS low to select ADXL345
GPIO1->DR &= ~(1 << 20);
// 2. Send write command (highest bit of address cleared)
spi3_ch0_write_and_read(reg_addr & ~0x80);
// 3. Send data to write
spi3_ch0_write_and_read(data);
// 4. Pull CS high to end communication
GPIO1->DR |= (1 << 20);
}
3.3 ADXL345 Initialization and Data Acquisition
The ADXL345 requires initialization (range configuration, entering measurement mode) before use, followed by reading triaxial acceleration data (16-bit, LSB first).
c
// Define ADXL345 data structure: stores X/Y/Z triaxial acceleration values
typedef struct {
short x; // X-axis acceleration
short y; // Y-axis acceleration
short z; // Z-axis acceleration
} ADXL345_Data;
// ADXL345 initialization: verify ID + configure range + enter measurement mode
int adxl345_init(void)
{
unsigned char dev_id;
// 1. Read device ID (DEVID register address 0x00, fixed value 0xE5)
dev_id = adxl345_read(0x00);
if(dev_id != 0xE5) {
return -1; // ID error, initialization failed
}
// 2. Configure DATA_FORMAT (0x31): ±16g range (0x0F)
adxl345_write(0x31, 0x0F);
// 3. Configure POWER_CTL (0x2D): 0x08 → enter measurement mode
adxl345_write(0x2D, 0x08);
return 0; // Initialization successful
}
// Read ADXL345 triaxial acceleration data
ADXL345_Data adxl345_read_data(void)
{
ADXL345_Data data;
// X-axis: low byte (0x32) + high byte (0x33)
data.x = adxl345_read(0x32);
data.x |= (short)(adxl345_read(0x33) << 8);
// Y-axis: low byte (0x34) + high byte (0x35)
data.y = adxl345_read(0x34);
data.y |= (short)(adxl345_read(0x35) << 8);
// Z-axis: low byte (0x36) + high byte (0x37)
data.z = adxl345_read(0x36);
data.z |= (short)(adxl345_read(0x37) << 8);
return data;
}
4. Main Function Test: Verify SPI Driver Functionality
Finally, write the main function to initialize hardware and loop through reading acceleration data, which can be printed via UART (requires prior UART initialization).
c
int main(void)
{
ADXL345_Data accel_data;
// 1. Initialize SPI3
spi3_init();
// 2. Initialize ADXL345
if(adxl345_init() != 0) {
// Print ID error via UART (requires uart_printf implementation)
uart_printf("ADXL345 init failed! ID error\r\n");
while(1);
}
uart_printf("ADXL345 init success!\r\n");
// 3. Loop to read triaxial data
while(1) {
accel_data = adxl345_read_data();
// Print X/Y/Z axis data
uart_printf("X: %d, Y: %d, Z: %d\r\n", accel_data.x, accel_data.y, accel_data.z);
// Simple delay
for(int i=0; i<1000000; i++);
}
return 0;
}
5. Practical Debugging: Common Issue Troubleshooting
Beginners most frequently encounter issues like "ID read as 0/255" or "data fluctuations." Follow these steps to troubleshoot:
5.1 Abnormal ID Reading (0/255)
- Hardware Wiring: Check if SCLK/MOSI/MISO/CS pins are reversed or poorly soldered.
- CS Level: Use a multimeter to measure GPIO1_IO20---ensure it pulls low during register reads and high afterward.
- SPI Mode: Confirm CONFIGREG's CPOL (1<<20) and CPHA (1<<0) are correctly configured (Mode 3).
- Clock Division: ECSPI3's clock must not exceed ADXL345's maximum rate (5MHz). This example uses 4.4MHz after division, meeting requirements.
- Logic Analyzer Capture : Observe whether MOSI sends
0x80(read ID command) and MISO returns0xE5.
5.2 Data Fluctuations
- Power Stability: ADXL345 requires 3.3V power supply---ensure minimal ripple.
- Grounding: Sensor GND must share ground with IMX6ULL to avoid interference.
- Range Configuration: Verify DATA_FORMAT register range matches data parsing (e.g., ±16g corresponds to 13mg/LSB resolution).
6. Summary
This article starts with SPI protocol principles, completes IMX6ULL ECSPI3 low-level configuration, encapsulates ADXL345 register read/write operations, and ultimately achieves triaxial acceleration data acquisition. Key takeaways:
- SPI's core lies in timing matching (CPOL/CPHA), which must align with the peripheral.
- IMX6ULL ECSPI configuration focuses on "master mode + data length + clock division."
- ADXL345's essence is "register read/write rules" (read address MSB=1, write address MSB=0).
- In bare-metal development, "FIFO waiting" and "CS level control" are critical SPI communication details.