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 核心配置项说明
fsl,spi-num-chipselects:指定 SPI 控制器支持的片选数量,需与实际硬件一致;cs-gpios:定义硬件片选引脚,注释放置 "cant't use cs-gpios" 是因为部分 NXP 平台 ECSPI 硬件片选存在稳定性问题,可后续改用软件片选(本文暂用硬件片选);pinctrl-0:绑定引脚复用配置,需确保pinctrl_ecspi3节点已配置 ECSPI3 的 SCLK、MOSI、MISO、CS 引脚为 SPI 功能;compatible:驱动与设备匹配的核心关键字,必须和内核驱动中of_match_table的内容完全一致;spi-cpol/spi-cpha:ADXL345 默认支持 SPI 模式 3,因此需同时开启这两个配置;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_message和spi_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_table或id_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 开发板测试
-
将
adxl345_drv.ko和adxl345_app拷贝到开发板; -
加载驱动: bash
运行
insmod adxl345_drv.ko -
查看内核日志,确认驱动加载成功: bash
运行
dmesg | grep adxl345 # 正常输出: # adxl345_drv_init success! # Kernel:adxl345 probe success # adxl345 ID : 0xe5 -
运行应用程序: bash
运行
./adxl345_app # 输出示例: # x: 128, y: -64, z: 1024 -
卸载驱动(可选): 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的参数顺序:目标指针(用户空间)在前,源指针(内核空间)在后。
七、扩展与优化
本文实现的驱动满足基础的加速度读取需求,可根据实际场景扩展:
- 中断支持:ADXL345 支持数据就绪中断,可在驱动中注册中断处理函数,实现触发式数据读取(减少轮询开销);
- 量程配置 :通过
ioctl接口实现用户空间配置传感器量程(±2g/±4g 等); - 数据校准:添加零点校准、误差补偿逻辑,提升数据精度;
- 多设备支持:修改驱动适配同一 SPI 总线下的多个 ADXL345 设备;
- 功耗优化:在无数据读取时,将传感器切换到低功耗模式。
八、总结
本文从设备树配置、内核驱动开发、应用层测试三个维度,讲解了 ADXL345 SPI 驱动的实现过程。核心要点包括:
- 设备树需严格匹配驱动的
compatible和 SPI 时序; - Linux SPI 子系统的
spi_driver和miscdevice简化了驱动开发; - 内核与用户空间数据交互必须使用
copy_to_user/copy_from_user; - 硬件时序和连接是 SPI 通信成功的关键。