基于 Linux SPI 子系统的 ADXL345 加速度传感器驱动开发

ADXL345 是 ADI 公司推出的一款低功耗、三轴加速度传感器,广泛应用于运动检测、姿态识别、振动监测等场景。本文以 NXP 平台的 ECSPI3 控制器为例,详细讲解基于 Linux 内核 SPI 子系统的 ADXL345 驱动开发流程,包括设备树配置、内核驱动编写、应用层测试及常见问题排查,所有代码与配置均经过实际验证。

一、硬件基础与 SPI 时序说明

1.1 ADXL345 核心特性

  • 三轴加速度检测,量程支持 ±2g/±4g/±8g/±16g 可配置;
  • 支持 SPI(最高 5MHz)和 I2C 两种数字接口;
  • 内置电源管理,支持低功耗模式;
  • 数据输出为 10 位二进制补码,存储在 16 位寄存器中。

1.2 硬件连接与 SPI 时序

本文中 ADXL345 挂载在 NXP 芯片的 ECSPI3 总线上,硬件连接关键信息:

  • SPI 片选(CS):GPIO1_20(低电平有效);
  • SPI 时序:采用模式 3(CPOL=1,CPHA=1),即空闲时 SCLK 为高电平,数据在 SCLK 第二个边沿采样;
  • 通信频率:1MHz(兼顾稳定性与响应速度)。

二、设备树(DTS)配置详解

Linux 设备树(Device Tree)是描述硬件拓扑的核心,需为 ECSPI3 控制器和 ADXL345 设备配置正确的节点信息,以下是关键配置片段及解析:

dts

复制代码
&ecspi3 {
    fsl,spi-num-chipselects = <1>;  // 控制器支持的片选数量为1
    cs-gpios= <&gpio1 20 GPIO_ACTIVE_LOW>; /* 硬件CS引脚(实际开发中需注意兼容性) */
    pinctrl-names = "default";      // 引脚配置集名称
    pinctrl-0 = <&pinctrl_ecspi3>;  // 绑定ECSPI3的引脚复用配置(需提前定义)
    status = "okay";                // 启用ECSPI3控制器

    spidev0: adxl345@0{
        compatible = "pute,puteadxl345";  // 驱动匹配关键字(必须与驱动中一致)
        spi-max-frequency = <1000000>;    // SPI最大通信频率1MHz
        spi-cpol;                         // 配置CPOL=1(时钟极性)
        spi-cpha;                         // 配置CPHA=1(时钟相位)
        reg = <0>;                        // SPI片选地址(对应CS0)
    };
};

2.1 核心配置项说明

  1. fsl,spi-num-chipselects:指定 SPI 控制器支持的片选数量,需与实际硬件一致;
  2. cs-gpios:定义硬件片选引脚,注释放置 "cant't use cs-gpios" 是因为部分 NXP 平台 ECSPI 硬件片选存在稳定性问题,可后续改用软件片选(本文暂用硬件片选);
  3. pinctrl-0:绑定引脚复用配置,需确保pinctrl_ecspi3节点已配置 ECSPI3 的 SCLK、MOSI、MISO、CS 引脚为 SPI 功能;
  4. compatible:驱动与设备匹配的核心关键字,必须和内核驱动中of_match_table的内容完全一致;
  5. spi-cpol/spi-cpha:ADXL345 默认支持 SPI 模式 3,因此需同时开启这两个配置;
  6. reg:SPI 设备的片选地址,0 对应 CS0,1 对应 CS1,需与硬件连接一致。

2.2 配置注意事项

  • 设备树编译后需烧录到开发板,或通过动态设备树叠加生效;
  • 若 ECSPI3 被其他设备占用,需先禁用无关设备节点(如注释掉 icm20608 节点);
  • 时序配置(CPOL/CPHA)错误会导致传感器无响应,需严格匹配 ADXL345 规格书。

三、Linux 内核 SPI 驱动开发

Linux SPI 子系统将驱动分为 "控制器驱动"(内核已实现 ECSPI3)和 "设备驱动"(需编写 ADXL345),设备驱动通过spi_driver注册,匹配成功后执行probe函数初始化设备。

3.1 驱动核心结构(adxl345_drv.c)

3.1.1 头文件与全局变量

c

运行

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/i2c.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
#include <linux/spi/spi.h>

static struct spi_device *padxl345_device;  // 保存匹配到的SPI设备指针

核心头文件说明:linux/spi/spi.h提供 SPI 子系统 API,linux/miscdevice.h用于简化字符设备开发(无需手动分配主设备号)。

3.1.2 传感器初始化函数

初始化函数负责读取设备 ID、配置量程和电源模式,是传感器正常工作的基础:

c

运行

复制代码
static void adxl345_init(void)
{ 
    int ret = 0;
    char sendbuff[7] = {0};
    char recvbuff[7] = {0};

    // 读取设备ID(0x00寄存器),0x80表示读操作
    sendbuff[0] = 0x00 | 0x80;
    ret = spi_write_then_read(padxl345_device, sendbuff, 1, recvbuff, 1);
    if(ret != 0) {
        pr_info("Kernel:adxl345 init fail\n");
        return;
    }
    pr_info("adxl345 ID : %#x\n", recvbuff[0]);  // 正常ID应为0xE5

    // 配置数据格式寄存器(0x31):量程±2g
    sendbuff[0] = 0x31;
    sendbuff[1] = 0x08;
    ret = spi_write_then_read(padxl345_device, sendbuff, 2, NULL, 0);
    if(ret != 0) {
        pr_info("Kernel:adxl345 init fail\n");
        return;
    }

    // 配置电源控制寄存器(0x2D):唤醒传感器,进入测量模式
    sendbuff[0] = 0x2D;
    sendbuff[1] = 0x08;
    ret = spi_write_then_read(padxl345_device, sendbuff, 2, NULL, 0);
    if(ret != 0) {
        pr_info("Kernel:adxl345 init fail\n");
        return;
    }
    return;
}

关键 API:spi_write_then_read,先发送指定长度数据,再读取指定长度数据,适用于 SPI 寄存器读写。

3.1.3 加速度数据读取函数

通过spi_messagespi_transfer实现多字节连续读取(ADXL345 支持多字节读,0x40 为多字节标志):

c

运行

复制代码
static int adxl345_readdata(short *x, short *y, short *z)
{
    struct spi_message msg;
    char sendbuff[7] = {0};
    char recvbuff[7] = {0};
    struct spi_transfer xfer= {
        .tx_buf = sendbuff,
        .rx_buf = recvbuff,
        .len = 7,
        .delay_usecs = 20,
    };

    // 0x80=读操作,0x40=多字节读,0x32=X轴数据起始寄存器
    sendbuff[0] = 0x32 | 0x80 | 0x40;
    spi_message_init(&msg);
    spi_message_add_tail(&xfer, &msg);
    spi_sync(padxl345_device, &msg);

    // 拼接16位数据(高字节<<8 | 低字节)
    *x = (recvbuff[2] << 8) | recvbuff[1];
    *y = (recvbuff[4] << 8) | recvbuff[3];
    *z = (recvbuff[6] << 8) | recvbuff[5];

    return 0;
}

注:ADXL345 的 X/Y/Z 轴数据分别存储在 0x32-0x33、0x34-0x35、0x36-0x37 寄存器,低字节在前、高字节在后。

3.1.4 Misc 设备封装(字符设备接口)

Linux Misc 设备简化了字符设备开发,无需手动分配主设备号,直接注册即可生成设备文件:

c

运行

复制代码
// 应用层read系统调用对应的处理函数
static ssize_t adxl345_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
    unsigned long nret = 0;
    short a[3] = {0};

    adxl345_readdata(&a[0], &a[1], &a[2]);

    // 内核数据拷贝到用户空间(必须用copy_to_user,禁止直接访问用户空间指针)
    nret = copy_to_user(puser, a, sizeof(a));
    if(nret != 0){
        pr_info("copy_to_user error\n");
    }

    return sizeof(a);  // 返回读取的字节数(3个short=6字节)
}

// 文件操作集
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = adxl345_read,
};

// Misc设备结构体
static struct miscdevice adxl345_misc = {
    .minor = MISC_DYNAMIC_MINOR,  // 动态分配次设备号
    .name = "adxl345_misc",       // 设备文件名:/dev/adxl345_misc
    .fops = &fops,
};

关键注意点:

  • 内核空间与用户空间数据交互必须使用copy_to_user/copy_from_user,避免内存访问异常;
  • miscdevice注册后自动生成/dev/adxl345_misc设备文件,无需手动创建设备节点。
3.1.5 SPI 驱动注册与卸载

c

运行

复制代码
// SPI设备匹配成功后执行的probe函数
static int adxl345_probe(struct spi_device *spi)
{
    int ret = 0;

    padxl345_device = spi;  // 保存SPI设备指针

    // 注册Misc设备
    ret = misc_register(&adxl345_misc);
    if (ret != 0) {
        pr_info("misc register fail adxl345\n");
        goto err_mis_register;
    }

    pr_info("Kernel:adxl345 probe success\n");
    adxl345_init();  // 初始化传感器

    return 0;

err_mis_register:
    misc_deregister(&adxl345_misc);
    return -1;
}

// SPI设备卸载时执行的remove函数
static int adxl345_remove(struct spi_device *spi)
{
    misc_deregister(&adxl345_misc);
    pr_info("Kernel:adxl345 remove success\n");
    return 0;
}

// 设备树匹配表(与DTS的compatible一致)
static const struct of_device_id adxl345_of_match_table[] = {
    {.compatible = "pute,puteadxl345"},
    {},
};

// SPI设备ID表(非设备树匹配时使用)
static const struct spi_device_id adxl345_id_table[] = {
    {.name = "puteadxl345"},
    {},
};

// SPI驱动结构体
static struct spi_driver adxl345_drv = {
    .probe = adxl345_probe,
    .remove = adxl345_remove,
    .driver = {
        .name = "puteadxl345",
        .of_match_table = adxl345_of_match_table,
    },
    .id_table = adxl345_id_table,
};

// 模块入口:注册SPI驱动
static int __init adxl345_drv_init(void)
{
    spi_register_driver(&adxl345_drv);
    pr_info("adxl345_drv_init success!\n");
    return 0;
}

// 模块出口:注销SPI驱动
static void __exit adxl345_drv_exit(void)
{
    spi_unregister_driver(&adxl345_drv);
    pr_info("adxl345_drv_exit success!\n");
    return;
}

module_init(adxl345_drv_init);
module_exit(adxl345_drv_exit);

MODULE_LICENSE("GPL");  // 必须声明GPL许可证,否则内核加载失败
MODULE_AUTHOR("pute");

核心逻辑:

  • spi_driver注册后,内核会遍历 SPI 总线设备,匹配of_match_tableid_table,匹配成功则执行probe
  • probe函数中完成 Misc 设备注册和传感器初始化;
  • remove函数中注销 Misc 设备,释放资源。

四、应用层测试程序

应用层通过标准文件操作接口访问传感器,代码简洁易懂(adxl345_app.c):

c

运行

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <sys/select.h>
#include <sys/time.h>
#include <linux/input.h>

#define KEY_ON  1
#define KEY_OFF 0

void delay_ms(int ms)
{
    usleep(ms * 1000);
}

int main(void)
{
    int fd = 0;
    int ret = 0;
    short data[3];  // 存储X/Y/Z三轴加速度

    // 打开Misc设备文件
    fd = open("/dev/adxl345_misc", O_RDONLY);
    if (-1 == fd)
    {
        printf("open error\n");
        return -1;
    }

    // 循环读取并打印数据
    while (1)
    {
        read(fd, data, sizeof(data));
        printf("x:%6d, y:%6d, z:%6d\r", data[0], data[1], data[2]);
        fflush(stdout);  // 强制刷新输出缓冲区,实时显示
        delay_ms(10);    // 10ms刷新一次(100Hz)
    }
    
    close(fd);
    return 0;
}

关键说明:

  • open打开设备文件,read读取三轴数据(每次 6 字节);
  • \r使光标回到行首,实现同行了刷新;
  • fflush(stdout)避免输出缓冲导致的显示延迟。

五、编译与测试流程

5.1 驱动编译

编写 Makefile(需指定内核源码路径):

makefile

复制代码
#目标文件
OBJ := adxl345_drv


#内核路径
kerdir := (填自己的内核路径)

#当前驱动工程目录
curdir := $(shell pwd)

#代码添加到工程编译选项中
obj-m += $(OBJ).o 

all:
	make -C $(kerdir) M=$(curdir) modules
	cp $(OBJ).ko ~/nfs/rootfs

.PHONY:

clean:
	make -C $(kerdir) M=$(curdir) modules clean

distclean:
	make -C $(kerdir) M=$(curdir) modules clean
	rm ~/nfs/rootfs/$(OBJ).ko

编译命令:

bash

运行

复制代码
make -j4  # 生成adxl345_drv.ko

5.2 应用层编译

使用交叉编译器(根据平台选择,如 arm-linux-gnueabihf-gcc):

bash

运行

复制代码
arm-linux-gnueabihf-gcc adxl345_app.c -o adxl345_app

5.3 开发板测试

  1. adxl345_drv.koadxl345_app拷贝到开发板;

  2. 加载驱动: bash

    运行

    复制代码
    insmod adxl345_drv.ko
  3. 查看内核日志,确认驱动加载成功: bash

    运行

    复制代码
    dmesg | grep adxl345
    # 正常输出:
    # adxl345_drv_init success!
    # Kernel:adxl345 probe success
    # adxl345 ID : 0xe5
  4. 运行应用程序: bash

    运行

    复制代码
    ./adxl345_app
    # 输出示例:
    # x:   128, y:   -64, z:  1024
  5. 卸载驱动(可选): bash

    运行

    复制代码
    rmmod adxl345_drv

六、常见问题与排查

6.1 驱动 probe 不执行

  • 检查设备树compatible是否与驱动of_match_table完全一致(大小写、标点都要匹配);
  • 检查 ECSPI3 的status是否为okay
  • 查看内核日志:dmesg | grep spi,确认 SPI 控制器初始化成功;
  • 确认设备树已正确编译并烧录。

6.2 读取设备 ID 失败

  • 检查 SPI 时序(CPOL/CPHA)是否配置正确(ADXL345 模式 3);
  • 用示波器抓 SPI 总线,验证 SCLK、MOSI、CS 信号是否正常;
  • 检查硬件连接:SCLK、MOSI、MISO、CS 引脚是否接反 / 虚焊;
  • 降低 SPI 通信频率(如从 1MHz 改为 500KHz),排除频率过高导致的通信失败。

6.3 应用层打开设备文件失败

  • 检查 Misc 设备是否注册成功:ls /dev/adxl345_misc,若不存在,说明驱动 probe 执行失败;
  • 权限问题:执行chmod 666 /dev/adxl345_misc赋予读写权限(或在驱动中设置默认权限)。

6.4 copy_to_user 出错

  • 确认用户空间缓冲区长度足够(应用层read的长度为sizeof(data)=6);
  • 检查copy_to_user的参数顺序:目标指针(用户空间)在前,源指针(内核空间)在后。

七、扩展与优化

本文实现的驱动满足基础的加速度读取需求,可根据实际场景扩展:

  1. 中断支持:ADXL345 支持数据就绪中断,可在驱动中注册中断处理函数,实现触发式数据读取(减少轮询开销);
  2. 量程配置 :通过ioctl接口实现用户空间配置传感器量程(±2g/±4g 等);
  3. 数据校准:添加零点校准、误差补偿逻辑,提升数据精度;
  4. 多设备支持:修改驱动适配同一 SPI 总线下的多个 ADXL345 设备;
  5. 功耗优化:在无数据读取时,将传感器切换到低功耗模式。

八、总结

本文从设备树配置、内核驱动开发、应用层测试三个维度,讲解了 ADXL345 SPI 驱动的实现过程。核心要点包括:

  • 设备树需严格匹配驱动的compatible和 SPI 时序;
  • Linux SPI 子系统的spi_drivermiscdevice简化了驱动开发;
  • 内核与用户空间数据交互必须使用copy_to_user/copy_from_user
  • 硬件时序和连接是 SPI 通信成功的关键。
相关推荐
顺风尿一寸2 小时前
深入Linux内核启动:从kernel_init到第一个用户进程的完整旅程
linux
郝学胜-神的一滴2 小时前
深入epoll反应堆模型:从libevent源码看高性能IO设计精髓
linux·服务器·开发语言·c++·网络协议·unix·信息与通信
H_老邪2 小时前
CentOS 9 解决 root 登录及重置密码指南
linux·运维·centos
Full Stack Developme2 小时前
Linux CURL 教程
linux·运维·chrome
Lumos_7772 小时前
Linux -- 共享内存
java·linux·运维
李日灐3 小时前
<5> Linux 开发工具:包管理 + Vim 实操 + GCC 编译流程 + 静态与动态链接详解
linux·运维·服务器·面试·vim·gcc
我也不曾来过13 小时前
传输层协议UDP和TCP
linux·网络·udp
molihuan3 小时前
最新VMware Ubuntu 1分钟极速安装 植物人教程
linux·ubuntu
sdm0704273 小时前
深刻理解进程信号
linux·运维·服务器