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 时,通常步骤是:
- 分配物理连续内存;
- 计算物理页帧号 PFN;
- 设置 VMA 属性;
- 调用
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 规格书和硬件连接为准。