Implementing I2C Communication for QMI8658 Sensor Module and Data Processing

Required I2C Communication Functions for QMI8658 Sensor Module

When working with the QMI8658 sensor module, the official sample code requires implementation of three custom communication functions for I2C operations. These functions are essential for proper sensor initialization and data retrieval.

Core Communication Functions

The following three functions form the foundation of I2C communication with the sensor:


unsigned char qmi8658_write_register(unsigned char reg, unsigned char value)
{
    unsigned char ret = 0;
    unsigned int retry = 0;

    while((!ret) && (retry++ < 5))
    {
#if defined(QMI8658_USE_SPI)
        ret = qst_imu_spi_write(reg, value);
#elif defined(QST_USE_SW_I2C)
        ret = qst_sw_write_byte(g_imu.slave << 1, reg, value);
#else
        ret = mx_i2c1_write(g_imu.slave << 1, reg, value);
#endif
    }
    return ret;
}

unsigned char qmi8658_write_registers(unsigned char reg, unsigned char *value, unsigned char length)
{
    int i, ret;

    for(i = 0; i < length; i++)
    {
#if defined(QMI8658_USE_SPI)
        ret = qst_imu_spi_write_bytes(reg, value, length);
#elif defined(QST_USE_SW_I2C)
        ret = qst_sw_write_bytes(g_imu.slave << 1, reg, value, length);
#else
        ret = I2C_BufferRead(g_imu.slave << 1, reg, value, length);
#endif
    }

    return ret;
}

unsigned char qmi8658_read_register(unsigned char reg, unsigned char* buffer, unsigned short length)
{
    unsigned char ret = 0;
    unsigned int retry = 0;

    while((!ret) && (retry++ < 5))
    {
#if defined(QMI8658_USE_SPI)
        ret = qst_8658_spi_read(reg, buffer, length);
#elif defined(QST_USE_SW_I2C)
        ret = qst_sw_read_byte(g_imu.slave << 1, reg, buffer, length);
#else
        ret = mx_i2c1_read(g_imu.slave << 1, reg, buffer, length);
#endif
    }
    return ret;
}

Step 1: Implementing the Three I2C Functions

Before implementing these functions, it's important to understand the basic principles of I2C communication. Below is a software I2C implementation that can be adapted to your specific hardware platform:


static void iic_delay(void)
{
    delay_us(2);    /* 2us delay for speeds up to 250Khz */
}

/**
 * @brief Generate I2C start signal
 */
void iic_start(void)
{
    IIC_SDA(1);
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(0);     /* START signal: SDA transitions from high to low while SCL is high */
    iic_delay();
    IIC_SCL(0);     /* Hold I2C bus ready for data transmission */
    iic_delay();
}

/**
 * @brief Generate I2C stop signal
 */
void iic_stop(void)
{
    IIC_SDA(0);     /* STOP signal: SDA transitions from low to high while SCL is high */
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(1);     /* Release I2C bus */
    iic_delay();
}

/**
 * @brief Wait for acknowledgment signal
 * @return 1 if acknowledgment failed, 0 if successful
 */
uint8_t iic_wait_ack(void)
{
    uint8_t waittime = 0;
    uint8_t ack_status = 0;

    IIC_SDA(1);     /* Release SDA line (external device can pull it low) */
    iic_delay();
    IIC_SCL(1);     /* SCL=1, slave can now return ACK */
    iic_delay();

    while (IIC_READ_SDA)    /* Wait for acknowledgment */
    {
        waittime++;
        if (waittime > 250)
        {
            iic_stop();
            ack_status = 1;
            break;
        }
    }

    IIC_SCL(0);     /* SCL=0, end ACK check */
    iic_delay();
    return ack_status;
}

/**
 * @brief Generate ACK signal
 */
void iic_ack(void)
{
    IIC_SDA(0);     /* SCL 0->1 with SDA=0 indicates ACK */
    iic_delay();
    IIC_SCL(1);     /* Generate clock pulse */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
    IIC_SDA(1);     /* Release SDA line */
    iic_delay();
}

/**
 * @brief Generate NACK signal
 */
void iic_nack(void)
{
    IIC_SDA(1);     /* SCL 0->1 with SDA=1 indicates NACK */
    iic_delay();
    IIC_SCL(1);     /* Generate clock pulse */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
}

/**
 * @brief Send one byte via I2C
 * @param data: Data to send
 */
void iic_send_byte(uint8_t data)
{
    uint8_t bit_index;
    
    for (bit_index = 0; bit_index < 8; bit_index++)
    {
        IIC_SDA((data & 0x80) >> 7);    /* MSB first */
        iic_delay();
        IIC_SCL(1);
        iic_delay();
        IIC_SCL(0);
        data <<= 1;     /* Shift left for next bit */
    }
    IIC_SDA(1);         /* Release SDA line after transmission */
}

/**
 * @brief Read one byte via I2C
 * @param ack: 1 to send ACK, 0 to send NACK
 * @return Received data
 */
uint8_t iic_read_byte(uint8_t ack)
{
    uint8_t i, received_data = 0;
	
    for (i = 0; i < 8; i++)    /* Receive 8 bits */
    {
        received_data <<= 1;  /* Shift left (MSB first) */
        IIC_SCL(1);
        iic_delay();

        if (IIC_READ_SDA)
        {
            received_data++;
        }
        
        IIC_SCL(0);
        iic_delay();
    }

    if (!ack)
    {
        iic_nack();     /* Send NACK */
    }
    else
    {
        iic_ack();      /* Send ACK */
    }

    return received_data;
}

Note: Hardware I2C implementations on STM32 often encounter issues, so a software implementation is generally more reliable.

Step 2: Implementing Functions Based on I2C Protocol

Based on the I2C read/write protocol, here are implementations for the three required functions. The 'slave' parameter is the device address, 'reg' is the register address, and 'value' contains the data to be written/read:


unsigned char qst_sw_write_byte(uint8_t device_addr, uint8_t reg, uint8_t value)
{
    iic_start();    /* Send start signal */
    iic_send_byte(device_addr);   
    iic_wait_ack();             /* Wait for ACK after each byte */
    iic_send_byte(reg);  /* Send register address */
    iic_wait_ack();             /* Wait for ACK, address transmission complete */
    
    /* No need to send start signal again for writing data */
    iic_send_byte(value);        /* Send data byte */
    iic_wait_ack();             /* Wait for ACK */
    iic_stop();                 /* Send stop condition */
    return 1;
}

unsigned char qst_sw_read_byte(uint8_t device_addr, uint8_t reg, uint8_t* buffer, uint8_t length)
{
    uint16_t i;
	
    iic_start();  
    iic_send_byte(device_addr);
    if(iic_wait_ack())
    {
        return 0;
    }
    iic_send_byte(reg);
    if(iic_wait_ack())
    {
        return 0;
    }

    iic_start();
    iic_send_byte(device_addr + 1);
    if(iic_wait_ack())
    {
        return 0;
    }

    for(i = 0; i < (length - 1); i++)
    {
        *buffer = iic_read_byte(1);
        buffer++;
    }
    *buffer = iic_read_byte(0);
    iic_stop();	// Send stop condition	    
    return 1;
}

unsigned char qst_sw_write_bytes(uint8_t device_addr, uint8_t reg, uint8_t* data, uint8_t length)
{
    uint8_t i;
    iic_start();    /* Send start signal */

    iic_send_byte(device_addr);   
    iic_wait_ack();             /* Wait for ACK after each byte */
    iic_send_byte(reg);        /* Send register address */
    iic_wait_ack();             /* Wait for ACK, address transmission complete */
    
    /* No need to send start signal again for writing data */
    for(i = 0; i < length; i++)
    {
        iic_send_byte(data[i]);	
        if(iic_wait_ack())
        {
            return 0;
        }
    }
    iic_wait_ack();             /* Wait for final ACK */
    iic_stop();                 /* Send stop condition */
    return 1;
}

Step 3: Modifications to Official Sample Code

1. Fixing Data Reading Erors

The official sample code may report errors when reading the first and second data frames. This is because it checks if consecutive frames are identical. Removing this check often resolves the issue:


void qmi8658_read_xyz(uint8_t device_id, float acceleration[3], float gyroscope[3])
{
    unsigned char status;
    unsigned char data_ready = 0;

    qmi8658_read_register(Qmi8658Register_Status0, &status, 1);
    if(status & 0x03)
    {
        data_ready = 1;
        if(data_ready)
        {
            qmi8658_read_sensor_data(acceleration, gyroscope);
            qmi8658_axis_convert(acceleration, gyroscope, 0);
        }
    }
}

2. Handling Multiple Devices

According to the datasheet, setting the AD0 pin to 1 changes the device address to 6A, while leaving it as 0 results in address 6B. When multiple devices are connected, the simplest approach is to rely on the initialization process that automatically detects all devices on the bus:


unsigned char qmi8658_get_device_id(void)
{
    unsigned char chip_id = 0x00;
    unsigned char revision_id = 0x00;
    unsigned char device_addresses[2] = {QMI8658_SLAVE_ADDR_L, QMI8658_SLAVE_ADDR_H};
    int retry = 0;
    unsigned char device_count = 0;
    unsigned char firmware_id[3];
    unsigned char uuid[6];
    unsigned int uuid_low, uuid_high;

    while(device_count < 2)
    {
        g_imu.slave = device_addresses[device_count];
        retry = 0;
        while((chip_id != 0x05) && (retry++ < 5))
        {
            qmi8658_read_register(Qmi8658Register_WhoAmI, &chip_id, 1);
        }
        if(chip_id == 0x05)
        {
            qmi8658_on_demand_calibration();

            g_imu.cfg.ctrl8_value = 0xc0;
            // Enable interrupts
            qmi8658_write_register(Qmi8658Register_Ctrl1, 0x60 | QMI8658_INT2_ENABLE | QMI8658_INT1_ENABLE);
            qmi8658_read_register(Qmi8658Register_Revision, &revision_id, 1);			
            qmi8658_read_register(Qmi8658Register_firmware_id, firmware_id, 3);
            qmi8658_read_register(Qmi8658Register_uuid, uuid, 6);
            qmi8658_write_register(Qmi8658Register_Ctrl7, 0x00);
            qmi8658_write_register(Qmi8658Register_Ctrl8, g_imu.cfg.ctrl8_value);
            uuid_low = (unsigned int)((unsigned int)(uuid[2] << 16) | (unsigned int)(uuid[1] << 8) | (uuid[0]));
            uuid_high = (unsigned int)((unsigned int)(uuid[5] << 16) | (unsigned int)(uuid[4] << 8) | (uuid[3]));
            printf("Device 0x%x initialized, Revision: 0x%x\n", g_imu.slave, revision_id);
            printf("Firmware ID[0x%x 0x%x 0x%x]\n", firmware_id[2], firmware_id[1], firmware_id[0]);
            printf("UUID[0x%x %x]\n", uuid_high, uuid_low);

            qmi8658_configure_registers(0);
            qmi8658_enable_sensors(g_imu.cfg.enabled_sensors);
            qmi8658_dump_registers();
            printf("0x%X INIT SUCCESS!\r\n", g_imu.slave);
        }
        device_count++;
    }

    return chip_id;
}

3. Modifying Data Acquisition

For data acquisition with multiple devices, you can modify the read function to switch between device addresses:


void qmi8658_read_xyz(uint8_t device_id, float acceleration[3], float gyroscope[3])
{
    unsigned char status;
    unsigned char data_ready = 0;
    
    // Set device address based on ID
    if(device_id == 0)
    {
        g_imu.slave = 0x6A;
    }
    else
    {
        g_imu.slave = 0x6B;
    }

    qmi8658_read_register(Qmi8658Register_Status0, &status, 1);
    if(status & 0x03)
    {
        data_ready = 1;
        if(data_ready)
        {
            qmi8658_read_sensor_data(acceleration, gyroscope);
            qmi8658_axis_convert(acceleration, gyroscope, 0);
        }
    }
}

Step 4: Additional Implementation Tips

1. Kalman Filter Implementation

A Kalman filter can be used to smooth sensor data. Here's an implementation based on existing solutions:


float kalman_filter(float input_data)
{
    static float previous_data = 0;
    // Initial P value can be arbitrary but not zero (zero would indicate optimal filtering)
    static float p = 0.01, process_noise = P_Q, measurement_noise = M_R, kalman_gain = 0;
    
    p = p + process_noise;
    kalman_gain = p / (p + measurement_noise);
 
    input_data = previous_data + (kalman_gain * (input_data - previous_data));
    p = (1 - kalman_gain) * p;
 
    previous_data = input_data;
 
    return input_data;
}

2. Using the Kalman Filter

To apply the Kalman filter to sensor data:


#include <math.h> 

// Sensor data variables
float acc_x, acc_y, acc_z;
float gyro_x, gyro_y, gyro_z;
float acceleration[3], gyroscope[3], angles[3];

// Read and filter data
for(uint8_t sample_count = 15; sample_count > 0; sample_count--)
{
    qmi8658_read_xyz(device_id, acceleration, gyroscope);
    
    if(sample_count > 5)
    {
        // Apply Kalman filter to acceleration data
        acc_x = kalman_filter(acceleration[0]);
        acc_y = kalman_filter(acceleration[1]);
        acc_z = kalman_filter(acceleration[2]);
        
        // Apply Kalman filter to gyroscope data
        gyro_x = kalman_filter(gyroscope[0]);
        gyro_y = kalman_filter(gyroscope[1]);
        gyro_z = kalman_filter(gyroscope[2]);
    }
}

// Print filtered data
printf("Device %d - X-axis: Accel=%.02F, Gyro=%.02F\n", device_id, acc_x, gyro_x);
printf("Device %d - Y-axis: Accel=%.02F, Gyro=%.02F\n", device_id, acc_y, gyro_y);
printf("Device %d - Z-axis: Accel=%.02F, Gyro=%.02F\n", device_id, acc_z, gyro_z);
</math.h>

3. Calculating Angles from Acceleration Data

While this only provides basic angle calculations, for full姿态 (attitude) information, you'll need to implement Euler angles or quaternion-based approaches:


// Calculate angles from accelerometer data
angles[0] = atan(acc_x / (sqrt(acc_y * acc_y + acc_z * acc_z))) * 180 / M_PI;  
angles[1] = atan(acc_y / (sqrt(acc_x * acc_x + acc_z * acc_z))) * 180 / M_PI;
angles[2] = atan(acc_z / (sqrt(acc_y * acc_y + acc_x * acc_x))) * 180 / M_PI;

Tags: QMI8658 I2C sensor communication Kalman filtering acceleration processing

Posted on Sun, 27 Sep 2026 16:15:26 +0000 by ShiloVir