嵌入式Linux驱动开发:ICM20608六轴传感器SPI驱动

嵌入式Linux驱动开发:ICM20608六轴传感器SPI驱动

1. 项目概述

本笔记详细记录了基于i.MX6ULL开发板的ICM20608六轴传感器(3轴陀螺仪+3轴加速度计)的SPI驱动开发全过程。项目包含完整的Linux内核模块驱动程序和用户空间测试应用程序,实现了对传感器数据的采集、处理和输出。驱动采用设备树方式配置硬件资源,符合现代Linux驱动开发规范。

2. 硬件平台介绍

2.1 i.MX6ULL处理器

NXP i.MX6ULL是一款基于ARM Cortex-A7架构的低功耗应用处理器,主要特性包括:

  • 单核ARM Cortex-A7,主频可达900MHz
  • 集成多种外设接口:SPI、I2C、UART、USB、Ethernet等
  • 支持Linux、Android等操作系统
  • 广泛应用于工业控制、智能家居、物联网等领域

2.2 ICM20608传感器

ICM20608是InvenSense公司生产的六轴运动传感器,集成了3轴陀螺仪和3轴加速度计,主要特性:

  • 陀螺仪量程:±250, ±500, ±1000, ±2000 °/s
  • 加速度计量程:±2g, ±4g, ±8g, ±16g
  • 内置温度传感器
  • 支持SPI和I2C两种通信接口
  • 内置11位ADC,数据输出速率可达8kHz
  • 工作电压:1.71V-3.6V

3. 软件架构设计

本驱动采用分层架构设计,主要包括以下几个层次:

复制代码
+---------------------+
|  用户空间应用程序   |
+---------------------+
|  字符设备驱动层     |
+---------------------+
|  SPI核心层          |
+---------------------+
|  SPI控制器驱动      |
+---------------------+
|  硬件物理层         |
+---------------------+

4. 设备树配置分析

4.1 设备树基础概念

设备树(Device Tree)是一种描述硬件配置的数据结构,它将硬件信息从内核代码中分离出来,使得内核可以支持多种硬件平台而无需重新编译。设备树文件以.dts为扩展名,编译后生成.dtb文件供内核使用。

4.2 ICM20608设备节点配置

imx6ull-alientek-emmc.dts文件中,ICM20608的设备树配置如下:

dts 复制代码
&ecspi3 {
    fsl,spi-num-chipselects = <1>;
    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_ecspi3>;
    status = "okay";

    spidev0: icm20608@0 {
        reg = <0>;
        compatible = "alientek,icm20608";
        spi-max-frequency = <8000000>;
    };
};
4.2.1 节点解析
  • &ecspi3:引用SPI3控制器节点
  • fsl,spi-num-chipselects = <1>:指定SPI控制器支持1个片选信号
  • cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>:配置片选GPIO,使用GPIO1_20,低电平有效
  • pinctrl-0 = <&pinctrl_ecspi3>:引用引脚控制配置
  • status = "okay":启用该设备
  • icm20608@0:子节点,表示连接在SPI总线上的ICM20608设备
    • reg = <0>:设备地址,SPI设备使用片选索引
    • compatible = "alientek,icm20608":兼容性字符串,用于匹配驱动程序
    • spi-max-frequency = <8000000>:SPI通信最大频率8MHz

4.3 引脚复用配置

&iomuxc节点中,SPI3的引脚复用配置如下:

dts 复制代码
pinctrl_ecspi3: ecspi3grp {
    fsl,pins = <
        MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20        0x10b0
        MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK    0x10b1
        MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI        0x10b1
        MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO        0x10b1
    >;
};
  • GPIO1_IO20:配置为SPI_CS片选信号
  • UART2_RX_DATA:复用为ECSPI3_SCLK(时钟信号)
  • UART2_CTS_B:复用为ECSPI3_MOSI(主出从入)
  • UART2_RTS_B:复用为ECSPI3_MISO(主入从出)

5. 驱动程序详解

5.1 头文件包含

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/input.h>
#include <linux/spi/spi.h>
#include <linux/delay.h>
5.1.1 关键头文件说明
  • <linux/module.h>:模块相关定义,如module_init、module_exit等
  • <linux/kernel.h>:内核常用宏和函数
  • <linux/fs.h>:文件系统相关定义,如file_operations结构体
  • <linux/cdev.h>:字符设备相关定义
  • <linux/device.h>:设备模型相关定义
  • <linux/of.h>:设备树相关API
  • <linux/spi/spi.h>:SPI子系统核心头文件
  • <linux/delay.h>:延时函数

5.2 寄存器定义

icm20608reg.h头文件中定义了ICM20608的所有寄存器地址:

c 复制代码
#define ICM20608G_ID 0XAF /* ID值 */
#define ICM20608D_ID 0XAE /* ID值 */

#define ICM20_SELF_TEST_X_GYRO 0x00
#define ICM20_SELF_TEST_Y_GYRO 0x01
#define ICM20_SELF_TEST_Z_GYRO 0x02
#define ICM20_SELF_TEST_X_ACCEL 0x0D
#define ICM20_SELF_TEST_Y_ACCEL 0x0E
#define ICM20_SELF_TEST_Z_ACCEL 0x0F

#define ICM20_XG_OFFS_USRH 0x13
#define ICM20_XG_OFFS_USRL 0x14
#define ICM20_YG_OFFS_USRH 0x15
#define ICM20_YG_OFFS_USRL 0x16
#define ICM20_ZG_OFFS_USRH 0x17
#define ICM20_ZG_OFFS_USRL 0x18

#define ICM20_SMPLRT_DIV 0x19
#define ICM20_CONFIG 0x1A
#define ICM20_GYRO_CONFIG 0x1B
#define ICM20_ACCEL_CONFIG 0x1C
#define ICM20_ACCEL_CONFIG2 0x1D
#define ICM20_LP_MODE_CFG 0x1E
#define ICM20_ACCEL_WOM_THR 0x1F
#define ICM20_FIFO_EN 0x23
#define ICM20_FSYNC_INT 0x36
#define ICM20_INT_PIN_CFG 0x37
#define ICM20_INT_ENABLE 0x38
#define ICM20_INT_STATUS 0x3A

#define ICM20_ACCEL_XOUT_H 0x3B
#define ICM20_ACCEL_XOUT_L 0x3C
#define ICM20_ACCEL_YOUT_H 0x3D
#define ICM20_ACCEL_YOUT_L 0x3E
#define ICM20_ACCEL_ZOUT_H 0x3F
#define ICM20_ACCEL_ZOUT_L 0x40

#define ICM20_TEMP_OUT_H 0x41
#define ICM20_TEMP_OUT_L 0x42

#define ICM20_GYRO_XOUT_H 0x43
#define ICM20_GYRO_XOUT_L 0x44
#define ICM20_GYRO_YOUT_H 0x45
#define ICM20_GYRO_YOUT_L 0x46
#define ICM20_GYRO_ZOUT_H 0x47
#define ICM20_GYRO_ZOUT_L 0x48

#define ICM20_SIGNAL_PATH_RESET 0x68
#define ICM20_ACCEL_INTEL_CTRL 0x69
#define ICM20_USER_CTRL 0x6A
#define ICM20_PWR_MGMT_1 0x6B
#define ICM20_PWR_MGMT_2 0x6C
#define ICM20_FIFO_COUNTH 0x72
#define ICM20_FIFO_COUNTL 0x73
#define ICM20_FIFO_R_W 0x74
#define ICM20_WHO_AM_I 0x75

#define ICM20_XA_OFFSET_H 0x77
#define ICM20_XA_OFFSET_L 0x78
#define ICM20_YA_OFFSET_H 0x7A
#define ICM20_YA_OFFSET_L 0x7B
#define ICM20_ZA_OFFSET_H 0x7D
#define ICM20_ZA_OFFSET_L 0x7E

5.3 驱动数据结构

c 复制代码
struct icm20608_dev
{
    dev_t devid;            /* 设备号 	 */
    struct cdev cdev;       /* cdev 	*/
    struct class *class;    /* 类 		*/
    struct device *device;  /* 设备 	 */
    struct device_node *nd; /* 设备节点 */
    int major;              /* 主设备号 */
    int minor;
    void *private_data; /* 私有数据 */
    int cs_gpio;
    signed int gyro_x_adc;  /* 陀螺仪X轴原始值 	 */
    signed int gyro_y_adc;  /* 陀螺仪Y轴原始值		*/
    signed int gyro_z_adc;  /* 陀螺仪Z轴原始值 		*/
    signed int accel_x_adc; /* 加速度计X轴原始值 	*/
    signed int accel_y_adc; /* 加速度计Y轴原始值	*/
    signed int accel_z_adc; /* 加速度计Z轴原始值 	*/
    signed int temp_adc;    /* 温度原始值 			*/
};
static struct icm20608_dev icm20608dev;
5.3.1 结构体成员说明
  • devid:设备号,由主设备号和次设备号组成
  • cdev:字符设备结构体,用于注册字符设备
  • class:设备类,用于在/sys/class/下创建设备类
  • device:设备结构体,用于在/dev/下创建设备节点
  • nd:设备节点指针,用于设备树相关操作
  • majorminor:主设备号和次设备号
  • private_data:私有数据,这里用于存储spi_device指针
  • cs_gpio:片选GPIO编号
  • 各种ADC原始值:存储从传感器读取的原始数据

5.4 SPI读写函数

5.4.1 多字节读取函数
c 复制代码
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    int ret = 0;
    struct spi_device *spi = dev->private_data;
    u8 data = reg | 0x80;
    spi_write_then_read(spi, &data, 1, buf, 1);
    return ret;
}

该函数使用spi_write_then_read()函数实现SPI读操作:

  1. 将寄存器地址最高位置1(0x80)表示读操作
  2. 调用spi_write_then_read()先发送寄存器地址,然后读取数据
  3. spi_write_then_read()是SPI核心层提供的便捷函数,自动处理片选信号
5.4.2 多字节写入函数
c 复制代码
static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len)
{
    int ret = 0;
    u8 data[len + 1];
    struct spi_device *spi = dev->private_data;
    data[0] = reg & 0x7f;
    memcpy(&data[1], buf, len);
    ret = spi_write(spi, data, len + 1);
    return ret;
}

该函数实现SPI写操作:

  1. 将寄存器地址最高位清0(0x7f)表示写操作
  2. 将要写入的数据复制到缓冲区
  3. 调用spi_write()函数发送数据
5.4.3 单字节读写函数
c 复制代码
static u8 icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
    u8 buf = 0;
    icm20608_read_regs(dev, reg, &buf, 1);
    return buf;
}

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
    u8 data = value;
    icm20608_write_regs(dev, reg, &data, 1);
}

单字节读写函数基于多字节读写函数实现,用于读写单个寄存器。

5.5 数据读取函数

c 复制代码
void icm20608_readdata(struct icm20608_dev *dev)
{
    unsigned char data[14] = {0};
    icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

    dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
    dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
    dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
    dev->temp_adc = (signed short)((data[6] << 8) | data[7]);
    dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
    dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
    dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
}

该函数读取14个连续的寄存器数据:

  • ICM20_ACCEL_XOUT_H(0x3B)开始,连续读取14个字节
  • 数据格式:AXH, AXL, AYH, AYL, AZH, AZL, TH, TL, GXH, GXL, GYH, GYL, GZH, GZL
  • 将高低字节组合成16位有符号整数

5.6 字符设备操作函数

5.6.1 open函数
c 复制代码
static int icm20608_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &icm20608dev;
    return 0;
}

open函数将设备结构体指针保存到file->private_data中,供其他函数使用。

5.6.2 read函数
c 复制代码
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    signed int data[7];
    long err = 0;
    struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

    icm20608_readdata(dev);
    data[0] = dev->gyro_x_adc;
    data[1] = dev->gyro_y_adc;
    data[2] = dev->gyro_z_adc;
    data[3] = dev->accel_x_adc;
    data[4] = dev->accel_y_adc;
    data[5] = dev->accel_z_adc;
    data[6] = dev->temp_adc;
    err = copy_to_user(buf, data, sizeof(data));
    return 0;
}

read函数执行以下操作:

  1. 调用icm20608_readdata()读取传感器数据
  2. 将数据组织成数组
  3. 使用copy_to_user()将数据复制到用户空间
5.6.3 release函数
c 复制代码
static int icm20608_release(struct inode *inode, struct file *filp)
{
    return 0;
}

release函数为空实现,因为不需要特殊处理。

5.6.4 file_operations结构体
c 复制代码
static const struct file_operations icm20608_ops = {
    .owner = THIS_MODULE,
    .open = icm20608_open,
    .read = icm20608_read,
    .release = icm20608_release,
};

定义了字符设备的操作函数集合。

5.7 寄存器初始化函数

c 复制代码
void icm20608_reginit(struct icm20608_dev *dev)
{
    u8 value = 0;
    icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);
    mdelay(50);
    icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);
    mdelay(50);

    value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
    printk("Driver: icm20608 ID = %#X\r\n", value);

    value = icm20608_read_onereg(dev, ICM20_PWR_MGMT_1);
    printk("Driver: ICM20_PWR_MGMT_1 = %#X\r\n", value);

    icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00);    /* 输出速率是内部采样率					*/
    icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18);   /* 陀螺仪±2000dps量程 				*/
    icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18);  /* 加速度计±16G量程 					*/
    icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04);        /* 陀螺仪低通滤波BW=20Hz 				*/
    icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
    icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00);    /* 打开加速度计和陀螺仪所有轴 				*/
    icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00);   /* 关闭低功耗 						*/
    icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);       /* 关闭FIFO						*/
}

该函数对ICM20608进行初始化配置:

  1. 先写0x80到ICM20_PWR_MGMT_1进行复位
  2. 延时50ms后写0x01启动设备
  3. 读取WHO_AM_I寄存器验证设备ID
  4. 配置采样率分频器为0
  5. 设置陀螺仪量程为±2000°/s(0x18)
  6. 设置加速度计量程为±16g(0x18)
  7. 设置陀螺仪低通滤波器带宽为20Hz
  8. 设置加速度计低通滤波器带宽为21.2Hz
  9. 启用所有轴的加速度计和陀螺仪
  10. 关闭低功耗模式
  11. 关闭FIFO功能

5.8 probe函数

c 复制代码
static int icm20608_probe(struct spi_device *spi)
{
    int ret = 0;
    printk("Driver: icm20608_probe\r\n");
    /* 1、构建设备号 */
    if (icm20608dev.major)
    {
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        register_chrdev_region(icm20608dev.devid, icm20608_CNT, icm20608_NAME);
    }
    else
    {
        alloc_chrdev_region(&icm20608dev.devid, 0, icm20608_CNT, icm20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
        icm20608dev.minor = MINOR(icm20608dev.devid);
    }
    printk("Driver: Major: %d, Minor: %d\r\n", icm20608dev.major, icm20608dev.minor);

    /* 2、注册设备 */
    cdev_init(&icm20608dev.cdev, &icm20608_ops);
    ret = cdev_add(&icm20608dev.cdev, icm20608dev.devid, icm20608_CNT);
    if (ret)
    {
        printk("Driver: cdev_add\r\n");
    }

    /* 3、创建类 */
    icm20608dev.class = class_create(THIS_MODULE, icm20608_NAME);
    if (IS_ERR(icm20608dev.class))
    {
        printk("Driver: icm20608dev.class\r\n");
        return PTR_ERR(icm20608dev.class);
    }

    /* 4、创建设备 */
    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, icm20608_NAME);
    if (IS_ERR(icm20608dev.device))
    {
        printk("Driver: icm20608dev.device\r\n");
        return PTR_ERR(icm20608dev.device);
    }

    spi->mode = SPI_MODE_0;
    spi_setup(spi);

    icm20608dev.private_data = spi;

    icm20608_reginit(&icm20608dev);
    return 0;
}

probe函数在设备匹配成功后被调用,执行以下初始化操作:

  1. 分配设备号
  2. 注册字符设备
  3. 创建设备类
  4. 创建设备节点
  5. 设置SPI模式为MODE_0
  6. 调用spi_setup()应用SPI设置
  7. 保存spi_device指针到私有数据
  8. 调用寄存器初始化函数

5.9 remove函数

c 复制代码
static int icm20608_remove(struct spi_device *spi)
{
    gpio_free(icm20608dev.cs_gpio);
    cdev_del(&icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, icm20608_CNT);
    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
    return 0;
}

remove函数在设备移除时被调用,执行资源清理:

  1. 释放GPIO
  2. 删除字符设备
  3. 释放设备号
  4. 销毁设备节点
  5. 销毁设备类

5.10 设备匹配表

c 复制代码
static struct of_device_id icm20608_id[] = {
    {
        .compatible = "alientek,icm20608",
    },
    {/* sentinel */}
};

static struct spi_device_id icm20608_spi_id[] = {
    {"alientek,icm20608", 0},
    {}};

定义了设备匹配表,内核通过compatible字符串匹配设备树节点和驱动程序。

5.11 SPI驱动结构体

c 复制代码
struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608",
        .owner = THIS_MODULE,
        .of_match_table = of_match_ptr(icm20608_id),
    },
    .id_table = icm20608_spi_id,
};

定义了SPI驱动结构体,包含了probe、remove函数指针和驱动信息。

5.12 模块初始化和退出函数

c 复制代码
static int __init icm20608_init(void)
{
    int ret = 0;
    ret = spi_register_driver(&icm20608_driver);
    if (ret != 0)
    {
        ret = -EINVAL;
        printk("Kernel: fail spi_register_driver\r\n");
    }
    return 0;
}

static void __exit icm20608_exit(void)
{
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");
  • module_init():指定模块初始化函数
  • module_exit():指定模块退出函数
  • MODULE_LICENSE():声明模块许可证
  • MODULE_AUTHOR():声明模块作者

6. SPI子系统架构分析

6.1 SPI子系统层次结构

Linux SPI子系统采用分层架构设计:

复制代码
+---------------------+
|  用户空间应用程序   |
+---------------------+
|  SPI设备驱动        |
+---------------------+
|  SPI核心层          |
+---------------------+
|  SPI控制器驱动      |
+---------------------+
|  硬件物理层         |
+---------------------+
6.1.1 SPI核心层

SPI核心层(spi.c)提供统一的API接口,管理SPI设备和控制器的注册、注销,以及数据传输的调度。主要数据结构包括:

  • struct spi_master:SPI控制器抽象
  • struct spi_device:SPI设备抽象
  • struct spi_driver:SPI驱动抽象
6.1.2 SPI控制器驱动

SPI控制器驱动负责实现具体的SPI控制器硬件操作,如i.MX6ULL的ECSPI控制器驱动。主要工作包括:

  • 初始化SPI控制器硬件
  • 实现数据传输函数
  • 处理中断(如果有)
6.1.3 SPI设备驱动

SPI设备驱动是针对具体SPI设备的驱动程序,如本例中的ICM20608驱动。主要工作包括:

  • 实现probe和remove函数
  • 实现设备特定的操作函数
  • 通过SPI核心层API与硬件通信

6.2 SPI数据传输机制

SPI数据传输通过struct spi_transferstruct spi_message结构体实现:

c 复制代码
struct spi_transfer {
    const void *tx_buf;
    void *rx_buf;
    unsigned len;
    dma_addr_t tx_dma;
    dma_addr_t rx_dma;
    struct sg_table tx_sg;
    struct sg_table rx_sg;
    unsigned cs_change:1;
    unsigned tx_nbits:3;
    unsigned rx_nbits:3;
    u8 bits_per_word;
    u16 delay_usecs;
    u32 speed_hz;
    struct list_head transfer_list;
};

struct spi_message {
    struct list_head transfers;
    struct spi_device *spi;
    unsigned is_dma_mapped:1;
    void (*complete)(void *context);
    void *context;
    unsigned actual_length;
    int status;
    struct list_head list;
};

数据传输流程:

  1. 初始化spi_message
  2. 添加一个或多个spi_transfer到消息中
  3. 调用spi_sync()spi_async()发送消息

6.3 SPI模式说明

SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:

模式 CPOL CPHA 时钟空闲状态 数据采样边沿
0 0 0 低电平 上升沿
1 0 1 低电平 下降沿
2 1 0 高电平 下降沿
3 1 1 高电平 上升沿

本驱动中设置为SPI_MODE_0(CPOL=0, CPHA=0),即时钟空闲为低电平,数据在上升沿采样。

7. 用户空间测试程序

7.1 程序源码分析

c 复制代码
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;
    char *filename;
    signed int databuf[7];
    unsigned char data[14];
    signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
    signed int accel_x_adc, accel_y_adc, accel_z_adc;
    signed int temp_adc;

    float gyro_x_act, gyro_y_act, gyro_z_act;
    float accel_x_act, accel_y_act, accel_z_act;
    float temp_act;

    int ret = 0;

    if (argc != 2)
    {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("can't open file %s\r\n", filename);
        return -1;
    }

    while (1)
    {
        ret = read(fd, databuf, sizeof(databuf));
        if (ret == 0)
        {
            gyro_x_adc = databuf[0];
            gyro_y_adc = databuf[1];
            gyro_z_adc = databuf[2];
            accel_x_adc = databuf[3];
            accel_y_adc = databuf[4];
            accel_z_adc = databuf[5];
            temp_adc = databuf[6];

            /* 计算实际值 */
            gyro_x_act = (float)(gyro_x_adc) / 16.4;
            gyro_y_act = (float)(gyro_y_adc) / 16.4;
            gyro_z_act = (float)(gyro_z_adc) / 16.4;
            accel_x_act = (float)(accel_x_adc) / 2048;
            accel_y_act = (float)(accel_y_adc) / 2048;
            accel_z_act = (float)(accel_z_adc) / 2048;
            temp_act = ((float)(temp_adc)-25) / 326.8 + 25;

            printf("\r\n原始值:\r\n");
            printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
            printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
            printf("temp = %d\r\n", temp_adc);
            printf("实际值:");
            printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
            printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
            printf("act temp = %.2f°C\r\n", temp_act);
        }
        usleep(100000); /*100ms */
    }
    close(fd);
    return 0;
}

7.2 程序工作流程

  1. 检查命令行参数
  2. 打开设备文件
  3. 循环读取传感器数据
  4. 将原始数据转换为实际物理量
  5. 打印数据
  6. 延时100ms后继续循环

7.3 数据转换公式

7.3.1 陀螺仪数据转换

陀螺仪量程设置为±2000°/s,16位ADC,因此:

  • 满量程范围:-32768 ~ 32767
  • 灵敏度:2000 / 32768 ≈ 16.4 LSB/(°/s)

转换公式:

复制代码
实际角速度 = 原始值 / 16.4
7.3.2 加速度计数据转换

加速度计量程设置为±16g,16位ADC,因此:

  • 满量程范围:-32768 ~ 32767
  • 灵敏度:16 / 32768 = 2048 LSB/g

转换公式:

复制代码
实际加速度 = 原始值 / 2048
7.3.3 温度数据转换

根据ICM20608数据手册,温度转换公式为:

复制代码
实际温度 = ((原始值 - 25) / 326.8) + 25

其中:

  • 25:室温校准值
  • 326.8:温度传感器灵敏度,单位LSB/°C

8. Makefile分析

makefile 复制代码
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)

obj-m := icm20608.o
build : kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modules

clean:
	$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean

8.1 变量说明

  • KERNERDIR:内核源码目录路径
  • CURRENTDIR:当前目录路径
  • obj-m:指定生成的模块文件名

8.2 编译过程

  1. make命令会进入内核源码目录
  2. 通过M=参数指定模块源码目录
  3. 编译生成icm20608.ko模块文件

8.3 编译命令

bash 复制代码
make -C /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/path/to/module modules

9. 驱动编译与测试

9.1 编译驱动模块

bash 复制代码
make

执行make命令后,会生成icm20608.ko文件。

9.2 加载驱动模块

bash 复制代码
insmod icm20608.ko

9.3 查看设备节点

bash 复制代码
ls /dev/icm20608

9.4 运行测试程序

bash 复制代码
./icm20608APP /dev/icm20608

9.5 卸载驱动模块

bash 复制代码
rmmod icm20608

源码仓库位置:https://gitee.com/dream-cometrue/linux_driver_imx6ull

相关推荐
2301_803554529 小时前
Docker(自写)
运维·docker·容器
惜.己9 小时前
docker命令(二)
运维·docker·容器
咕噜企业签名分发-淼淼9 小时前
搭建APP应用程序如何选择服务器
运维·服务器
Sadsvit9 小时前
Ansible变量的定义与使用
linux·运维·centos·自动化·ansible
特种加菲猫9 小时前
深入Linux内核:IPC资源管理揭秘
linux·笔记
北冥电磁电子智能10 小时前
江协科技STM32学习笔记补充之001。为什么C语言在对STM32编程过程中的二进制要用十六进制来进行读写。而不能直接用二进制来进行读写。
linux·stm32
工藤新一¹10 小时前
Linux 孤儿进程 (Orphan Process)
linux·孤儿进程·c/c++·orphan process
礼拜天没时间.10 小时前
Tomcat 企业级运维实战系列(四):Tomcat 企业级监控
运维·centos·tomcat·firefox