Linux 驱动实战:ADXL345 驱动

Linux 驱动实战:从 SPI 到 ioctl、mmap,完整 ADXL345 驱动

最近整理嵌入式 Linux 驱动时,把 SPI 驱动、内核模块、字符设备、ioctl、mmap 串成了一个完整例子:用 SPI 读取 ADXL345 三轴加速度数据,并通过字符设备提供 read、ioctl 和 mmap 接口。

一、ADXL345 与 SPI 接口

ADXL345 是一款三轴加速度传感器,支持 SPI 和 I2C 接口,量程可配置为 ±2g / ±4g / ±8g / ±16g。

SPI 通信时,常用两种方式:

  • spi_write_then_read:适合简单的寄存器读写;
  • spi_message + spi_transfer:适合多字节、复杂传输。

读取三轴数据时,通常从 DATAX0 寄存器 0x32 开始连续读取 6 字节:

复制代码
tx[0] = 0x32 | 0x80 | 0x40;  /* 读操作 + 多字节 */

其中:

  • 0x80:读标志;
  • 0x40:多字节连续读;
  • 0x32:起始寄存器地址。

二、内核模块与驱动注册

Linux 驱动通常以内核模块形式存在。最基础写法是:

复制代码
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

如果驱动是平台设备驱动,可以用:

复制代码
module_platform_driver(my_driver);

如果是 SPI 设备驱动,更常见的是:

复制代码
module_spi_driver(my_spi_driver);

module_platform_driver 和 module_spi_driver 都是封装宏,内部仍然基于 module_init / module_exit,只是自动完成了 platform_driver_register 或 spi_driver 注册。

三、字符设备接口

字符设备驱动通过 file_operations 向用户空间暴露接口。

常见映射关系:

用户空间 内核回调
open() .open
read() .read
ioctl() .unlocked_ioctl
mmap() .mmap
close() .release

read 适合返回数据流,比如三轴加速度原始数据;ioctl 适合发送控制命令,比如设置量程、复位、读取器件 ID。

四、ioctl 控制命令

ioctl 命令码通常使用 <linux/ioctl.h> 中的宏定义:

复制代码
#define ADXL345_IOC_MAGIC     'A'
#define ADXL345_IOC_RESET     _IO(ADXL345_IOC_MAGIC, 0)
#define ADXL345_IOC_SET_RANGE _IOW(ADXL345_IOC_MAGIC, 1, unsigned char)
#define ADXL345_IOC_GET_ID    _IOR(ADXL345_IOC_MAGIC, 2, unsigned char)

方向从用户空间视角看:

  • _IO:无参数;
  • _IOW:用户向内核写;
  • _IOR:内核向用户读;
  • _IOWR:双向。

驱动实现示例:

复制代码
static long adxl345_ioctl(struct file *filp, unsigned int cmd,
                          unsigned long arg)
{
    struct adxl345_dev *dev = filp->private_data;

    switch (cmd) {
    case ADXL345_IOC_GET_ID:
        if (copy_to_user((unsigned char __user *)arg,
                         &dev->id, sizeof(dev->id)))
            return -EFAULT;
        break;

    case ADXL345_IOC_SET_RANGE:
        /* 从用户空间获取量程并写入寄存器 */
        break;

    case ADXL345_IOC_RESET:
        /* 复位或重新初始化传感器 */
        break;

    default:
        return -ENOTTY;
    }
    return 0;
}

五、mmap 内存映射

mmap 可以把内核物理内存映射到用户空间,避免每次读写都走 copy_to_user / copy_from_user。

驱动中实现 .mmap 时,通常步骤是:

  1. 分配物理连续内存;
  2. 计算物理页帧号 PFN;
  3. 设置 VMA 属性;
  4. 调用 remap_pfn_range 建立映射。

示例:

复制代码
static int adxl345_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct adxl345_dev *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn;

    if (size > dev->buf_size)
        return -EINVAL;

    pfn = virt_to_phys(dev->sample_buf) >> PAGE_SHIFT;

    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
    vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                           vma->vm_page_prot);
}

如果是 DMA 缓冲区,PFN 计算要改为使用 DMA 物理地址,不能简单依赖 virt_to_phys。

六、完整驱动骨架

复制代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/delay.h>
#include <linux/ioctl.h>
#include <linux/platform_device.h>
#include <linux/mm.h>

#define ADXL345_REG_DEVID     0x00
#define ADXL345_REG_POWER_CTL 0x2D
#define ADXL345_REG_DATAX0    0x32

#define ADXL345_IOC_MAGIC     'A'
#define ADXL345_IOC_RESET     _IO(ADXL345_IOC_MAGIC, 0)
#define ADXL345_IOC_SET_RANGE _IOW(ADXL345_IOC_MAGIC, 1, unsigned char)
#define ADXL345_IOC_GET_ID    _IOR(ADXL345_IOC_MAGIC, 2, unsigned char)

#define ADXL345_SAMPLE_NUM    64

struct adxl345_dev {
    struct spi_device *spi;
    struct cdev cdev;
    dev_t devno;
    struct mutex lock;

    unsigned char id;
    unsigned char range;

    void *sample_buf;
    unsigned int buf_size;
};

static struct adxl345_dev *g_adxl;

static int adxl345_spi_read_regs(struct spi_device *spi,
                                 unsigned char reg,
                                 unsigned char *buf, unsigned int len)
{
    unsigned char tx[7] = {0};
    unsigned char rx[7] = {0};
    struct spi_message msg;
    struct spi_transfer xfer;

    if (len > 6)
        return -EINVAL;

    tx[0] = reg | 0x80 | 0x40;

    memset(&xfer, 0, sizeof(xfer));
    xfer.tx_buf = tx;
    xfer.rx_buf = rx;
    xfer.len = len + 1;
    xfer.delay_usecs = 20;

    spi_message_init(&msg);
    spi_message_add_tail(&xfer, &msg);

    return spi_sync(spi, &msg) ? : (memcpy(buf, rx + 1, len), 0);
}

static int adxl345_spi_write_reg(struct spi_device *spi,
                                 unsigned char reg,
                                 unsigned char val)
{
    unsigned char tx[2] = { reg, val };
    return spi_write(spi, tx, sizeof(tx));
}

static int adxl345_read_xyz(struct adxl345_dev *dev,
                            short *x, short *y, short *z)
{
    unsigned char buf[6];
    int ret;

    ret = adxl345_spi_read_regs(dev->spi, ADXL345_REG_DATAX0, buf, 6);
    if (ret)
        return ret;

    *x = (short)((buf[1] << 8) | buf[0]);
    *y = (short)((buf[3] << 8) | buf[2]);
    *z = (short)((buf[5] << 8) | buf[4]);
    return 0;
}

static int adxl345_open(struct inode *inode, struct file *filp)
{
    filp->private_data = g_adxl;
    return 0;
}

static ssize_t adxl345_read(struct file *filp, char __user *buf,
                            size_t count, loff_t *ppos)
{
    struct adxl345_dev *dev = filp->private_data;
    short xyz[3];
    int ret;

    if (count < sizeof(xyz))
        return -EINVAL;

    mutex_lock(&dev->lock);
    ret = adxl345_read_xyz(dev, &xyz[0], &xyz[1], &xyz[2]);
    mutex_unlock(&dev->lock);
    if (ret)
        return ret;

    if (copy_to_user(buf, xyz, sizeof(xyz)))
        return -EFAULT;

    return sizeof(xyz);
}

static long adxl345_ioctl(struct file *filp, unsigned int cmd,
                          unsigned long arg)
{
    struct adxl345_dev *dev = filp->private_data;

    switch (cmd) {
    case ADXL345_IOC_RESET:
        mutex_lock(&dev->lock);
        adxl345_spi_write_reg(dev->spi, ADXL345_REG_POWER_CTL, 0x00);
        msleep(10);
        adxl345_spi_write_reg(dev->spi, ADXL345_REG_POWER_CTL, 0x08);
        mutex_unlock(&dev->lock);
        break;

    case ADXL345_IOC_SET_RANGE: {
        unsigned char range;
        if (copy_from_user(&range, (unsigned char __user *)arg,
                           sizeof(range)))
            return -EFAULT;
        mutex_lock(&dev->lock);
        dev->range = range;
        adxl345_spi_write_reg(dev->spi, 0x31, range);
        mutex_unlock(&dev->lock);
        break;
    }

    case ADXL345_IOC_GET_ID:
        mutex_lock(&dev->lock);
        if (copy_to_user((unsigned char __user *)arg, &dev->id,
                         sizeof(dev->id)))
            return -EFAULT;
        mutex_unlock(&dev->lock);
        break;

    default:
        return -ENOTTY;
    }
    return 0;
}

static int adxl345_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct adxl345_dev *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn;

    if (size > dev->buf_size)
        return -EINVAL;

    pfn = virt_to_phys(dev->sample_buf) >> PAGE_SHIFT;

    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
    vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                           vma->vm_page_prot);
}

static const struct file_operations adxl345_fops = {
    .owner          = THIS_MODULE,
    .open           = adxl345_open,
    .read           = adxl345_read,
    .unlocked_ioctl = adxl345_ioctl,
    .mmap           = adxl345_mmap,
};

static int adxl345_probe(struct platform_device *pdev)
{
    struct spi_device *spi;
    struct adxl345_dev *dev;
    int ret;

    spi = spi_alloc_dummy();
    if (!spi)
        return -ENOMEM;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        spi_unregister_device(spi);
        return -ENOMEM;
    }

    dev->spi = spi;
    mutex_init(&dev->lock);

    dev->buf_size = ADXL345_SAMPLE_NUM * sizeof(short) * 3;
    dev->sample_buf = kzalloc(dev->buf_size, GFP_KERNEL | GFP_DMA);
    if (!dev->sample_buf) {
        ret = -ENOMEM;
        goto err_free_dev;
    }

    ret = alloc_chrdev_region(&dev->devno, 0, 1, "adxl345");
    if (ret)
        goto err_free_buf;

    cdev_init(&dev->cdev, &adxl345_fops);
    dev->cdev.owner = THIS_MODULE;

    ret = cdev_add(&dev->cdev, dev->devno, 1);
    if (ret)
        goto err_unregister_chrdev;

    adxl345_spi_write_reg(spi, ADXL345_REG_POWER_CTL, 0x08);
    adxl345_spi_read_regs(spi, ADXL345_REG_DEVID, &dev->id, 1);
    pr_info("adxl345 id = 0x%02x\n", dev->id);

    g_adxl = dev;
    platform_set_drvdata(pdev, dev);
    return 0;

err_unregister_chrdev:
    unregister_chrdev_region(dev->devno, 1);
err_free_buf:
    kfree(dev->sample_buf);
err_free_dev:
    kfree(dev);
    spi_unregister_device(spi);
    return ret;
}

static int adxl345_remove(struct platform_device *pdev)
{
    struct adxl345_dev *dev = platform_get_drvdata(pdev);

    if (!dev)
        return 0;

    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->devno, 1);
    kfree(dev->sample_buf);
    kfree(dev);
    g_adxl = NULL;
    return 0;
}

static const struct of_device_id adxl345_of_match[] = {
    { .compatible = "adi,adxl345-spi" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, adxl345_of_match);

static struct platform_driver adxl345_driver = {
    .probe  = adxl345_probe,
    .remove = adxl345_remove,
    .driver = {
        .name           = "adxl345-spi",
        .of_match_table = adxl345_of_match,
    },
};

module_platform_driver(adxl345_driver);
MODULE_LICENSE("GPL");

七、用户空间测试

复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#define ADXL345_IOC_MAGIC     'A'
#define ADXL345_IOC_RESET     _IO(ADXL345_IOC_MAGIC, 0)
#define ADXL345_IOC_SET_RANGE _IOW(ADXL345_IOC_MAGIC, 1, unsigned char)
#define ADXL345_IOC_GET_ID    _IOR(ADXL345_IOC_MAGIC, 2, unsigned char)

#define SAMPLE_NUM 64

int main(void)
{
    int fd = open("/dev/adxl345", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    unsigned char id;
    ioctl(fd, ADXL345_IOC_GET_ID, &id);
    printf("ADXL345 ID: 0x%02x\n", id);

    unsigned char range = 0x0B;
    ioctl(fd, ADXL345_IOC_SET_RANGE, &range);

    short xyz[3];
    if (read(fd, xyz, sizeof(xyz)) == sizeof(xyz))
        printf("x=%d y=%d z=%d\n", xyz[0], xyz[1], xyz[2]);

    size_t size = SAMPLE_NUM * sizeof(short) * 3;
    short *buf = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    if (buf != MAP_FAILED) {
        printf("mmap addr = %p\n", buf);
        munmap(buf, size);
    }

    close(fd);
    return 0;
}

八、注意事项

  • 上板时 spi_device 通常来自设备树和 SPI 控制器匹配,示例里的 spi_alloc_dummy 只是骨架用法。
  • mmap 如果后续接 DMA 缓冲区,PFN 需要用 DMA 物理地址计算。
  • read 接口目前只演示单次读取,实际采样可以放到工作队列、hrtimer 或内核线程里周期性填充缓冲区。
  • 设备节点可以通过 class_create / device_create 自动生成,也可以手动 mknod。
  • 寄存器地址、量程配置、SPI 模式需要以实际 ADXL345 规格书和硬件连接为准。

相关推荐
Web3&Basketball1 小时前
昇腾950DT迁移实测:3笔成本账与决策树
服务器·算法·决策树
牛马工作号4 小时前
Mosquitto + ACME:从 Docker Compose 部署到 TLS 证书自动续期与加载
运维·docker·容器
强哥聊算力7 小时前
《算力江湖·英伟达传奇》第七集:ChatGPT出现前夜,英伟达发生了什么?
服务器·人工智能·算法·ai·chatgpt·gpu算力
Initialize-le7 小时前
Nginx 504 超时、499 一堆?网关排查法
运维·nginx
砚凝霜9 小时前
【软考信息安全】第二十二章 Web服务器安全分析与增强——Apache与IIS
服务器·前端·安全
Android系统攻城狮9 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_delay调用流程与实战(七十二)
linux·运维·服务器·gstreamer音视频·音视频进阶
快乐的韭菜根根10 小时前
ubuntu 安装slurm并配置
linux·运维·ubuntu
数字新视界10 小时前
机房动环监控提升运维效率
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
U-Mail邮件系统11 小时前
自建邮箱服务器有哪些软件?三款主流邮件服务器软件推荐
运维·服务器