Linux下SPI设备驱动实验:实现SPI发送/接收数据的函数

一. 简介

前面文章介绍了SPI设备数据收发处理流程,后面几篇文章实现了SPI设备驱动框架,加入了字符设备驱动框架代码。文章如下:

SPI 设备驱动编写流程:SPI 设备数据收发处理流程中涉及的结构体与函数-CSDN博客

SPI 设备驱动编写流程:SPI 设备数据收发处理流程-CSDN博客

Linux下SPI设备驱动实验:SPI设备驱动框架编写-CSDN博客

Linux下SPI设备驱动实验:向SPI驱动框架中加入字符设备驱动框架代码-CSDN博客

本文在此基础上,编写向SPI设备发送数据的函数,从SPI设备接收数据的函数。

二. Linux下SPI设备驱动实验:实现SPI发送/接收数据的函数

1. 读写SPI设备中数据的实现思路

向SPI设备中发送数据,从SPI设备接收数据的实现,需要使用内核提供的结构体与 API函数。

涉及的结构体为 spi_transferspi_message

涉及的API函数如下:

复制代码
void spi_message_init(struct spi_message *m) //初始化spi_message
//添加spi_transfer到spi_message队列中
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
//同步传输数据
int spi_sync(struct spi_device *spi, struct spi_message *message)
//异步传输数据
int spi_async(struct spi_device *spi, struct spi_message *message)

实现思路

(1) 构造spi_transfer结构体,初始化 spi_message结构体(使用spi_message_init 函数来初始化spi_message)。

(2) 将spi_transfer 添加到 spi_message队列中,使用 spi_message_add_tail函数。

(3) 进行数据传输(调用 spi_sync 或者 spi_async函数)。

三. 向SPI驱动框架中添加读写SPI设备中数据的函数

通过阅读 ICM20608数据手册可知,如下信息:

可以看出,第一字节为SPI设备地址,后面的多个字节为SPI数据。而第一个字节中第一位为读/写标志位,后面七位为SPI设备地址。

打开 18_spi工程,spi_icm20608.c文件中在前面SPI驱动框架代码的基础上, 在spi_driver结构体的成员函数 .probe函数添加了SPI代码,读/写SPI设备中数据的函数。

.probe函数如下:

复制代码
static int icm20608_probe(struct spi_device* spi_dev)
{
    int ret = 0;
    printk("icm20608_probe!\n");
        //设备号的分配
    icm20608_dev.major = 0;
    if(icm20608_dev.major) //给定主设备号
    {
        icm20608_dev.devid = MKDEV(icm20608_dev.major, 0);
        ret = register_chrdev_region(icm20608_dev.devid, ICM20608_CNT, ICM20608_NAME);
    }
    else{ //没有给定主设备号
        ret = alloc_chrdev_region(&(icm20608_dev.devid), 0, ICM20608_CNT, ICM20608_NAME);	
        icm20608_dev.major = MAJOR(icm20608_dev.devid);
        icm20608_dev.minor = MINOR(icm20608_dev.devid);
    }
    printk("dev.major: %d\n", icm20608_dev.major);
    printk("dev.minor: %d\n", icm20608_dev.minor);
	if (ret < 0) {
		printk("register-chrdev failed!\n");
		goto fail_devid;
	}
    //初始化设备
    icm20608_dev.led_cdev.owner = THIS_MODULE;
    cdev_init(&icm20608_dev.led_cdev, &fops);
    
    //注册设备
    ret = cdev_add(&icm20608_dev.led_cdev, icm20608_dev.devid, ICM20608_CNT);
    if(ret < 0)
    {
        printk("cdev_add failed!\r\n");
        goto fail_adddev;
    }

/* 3.自动创建设备节点 */
    //创建类
    icm20608_dev.class = class_create(THIS_MODULE, ICM20608_NAME); 
    if (IS_ERR(icm20608_dev.class)) {
		ret = PTR_ERR(icm20608_dev.class);
		goto fail_class;
	}   
    //创建设备
    icm20608_dev.device = device_create(icm20608_dev.class, NULL, icm20608_dev.devid, NULL, ICM20608_NAME);
    if (IS_ERR(icm20608_dev.device)) {
		ret = PTR_ERR(icm20608_dev.device);
		goto fail_dev_create;
	}  

    /*SPI通信:设置模式*/
    spi_dev->mode = SPI_MODE_0;
    spi_setup(spi_dev);  //调用spi_setup函数设置模式,字节数,传输速率等
    icm20608_dev.private_data = spi_dev; //设置私有数据
    //ICM20608设备初始化
    icm20608_register_init(&icm20608_dev);

    return 0;

fail_dev_create:
    class_destroy(icm20608_dev.class);
fail_class:
    cdev_del(&icm20608_dev.led_cdev);
fail_adddev:
    unregister_chrdev_region(icm20608_dev.devid, ICM20608_CNT);
fail_devid:
    return ret;
}

.probe函数这里对应就是 icm20608_probe函数,添加了 SPI配置代码,私有数据指针赋值,ICM20608设备的初始化,添加如下代码:

复制代码
    /*SPI通信:设置模式*/
    spi_dev->mode = SPI_MODE_0;
    spi_setup(spi_dev);  //调用spi_setup函数设置模式,字节数,传输速率等
    icm20608_dev.private_data = spi_dev; //设置私有数据
    //ICM20608设备初始化
    icm20608_register_init(&icm20608_dev);

读/写SPI设备中数据的函数,ICM2068设备初始化函数实现如下:

前面文章分析过,NXP官方提供的 IMX6U系列的 SPI主机控制器的驱动关于片选的部分:如果设备树中**"cs-gpios"** 设置了,则SPI主机控制器的驱动代码中就获取了cs-gpios属性值,并设置了其电平,说明直接将处理成软件片选。

所以,设备树的SPI节点下设置了cs-gpios属性值,则这里编写SPI设备驱动就不需要手动设置片选拉低操作(即传输数据前拉低片选,传输结束后拉高片选)。

复制代码
 /*向icm20608设备中多个寄存器写入数据*/
static int spi_write_regs(struct icm20608_Dev* dev, u8 reg_addr, void* buf, int len)
{
    int ret = 0;
    unsigned char* tx_data = NULL;
    struct spi_transfer spi_t = {0};
    struct spi_message spi_msg; 
    struct spi_device* spi_dev = (struct spi_device*)dev->private_data;
    tx_data = kzalloc((1+len)*sizeof(unsigned char), GFP_KERNEL);

    tx_data[0] = reg_addr & ~0x80; //第一个字节的最高位为写标志位,其他七位为SPI设备地址
    memcpy(tx_data+1, buf, len);
    spi_t.tx_buf = tx_data;
    spi_t.len = len+1;  //发送数据的长度+接收数据的长度

    spi_message_init(&spi_msg); //初始化spi_message队列
    spi_message_add_tail(&spi_t, &spi_msg); //将spi_transfer添加到spi_message队列中
    ret = spi_sync(spi_dev, &spi_msg); //同步发送
    if(ret != 0)
        printk("spi_write_regs error!\n");

    kfree(tx_data);
    return ret;
 }

/*从SPI设备多个寄存器中读取数据*/
static int spi_read_regs(struct icm20608_Dev* dev, u8 reg_addr, void* buf, int len)
{
    int ret = 0;
    unsigned char tx_data[1] = {0};
    unsigned char* rx_data = NULL;
    struct spi_transfer spi_t = {0};
    struct spi_message spi_msg;
    struct spi_device* spi_dev = (struct spi_device*)dev->private_data;

    rx_data = kzalloc((len+1)*sizeof(unsigned char), GFP_KERNEL);
    
    tx_data[0] = reg_addr|0x80; //第一个字节的最高位为读标志位,置1,其他七位为SPI设备地址
    spi_t.tx_buf = tx_data;
    spi_t.rx_buf = rx_data;
    spi_t.len = len+1;

    spi_message_init(&spi_msg); //初始化spi_message队列
    spi_message_add_tail(&spi_t, &spi_msg); //将spi_transfer添加到spi_message队列中
    ret = spi_sync(spi_dev, &spi_msg); //同步发送
    if(ret < 0)
        printk("spi_read_regs error!\n");

    //第一个字节是告诉设备我们要进行读还是写,后面的 n个字节数据才是要读取的数据
    memcpy(buf, rx_data+1, len); 

    kfree(rx_data);
    return ret;
}

/*从SPI设备读取一个寄存器的数据*/
static int spi_write_reg_onebyte(struct icm20608_Dev* dev, u8 reg_addr, int data)
{
    int ret = 0;
    ret = spi_write_regs(dev, reg_addr, &data, 1);
    if(ret < 0)
        printk("spi_write_reg_onebyte error!\n");
    return ret;
}

/*向SPI设备的一个寄存器写入数据*/
static int spi_read_reg_onebyte(struct icm20608_Dev* dev, u8 reg_addr)
{
    int ret = 0,data = 0;
    ret = spi_read_regs(dev, reg_addr, &data, 1);
    if(ret < 0)
        printk("spi_read_reg_onebyte error!\n");

    return data;
}

/*ICM20608设备初始化(即SPI设备初始化)*/
static int icm20608_register_init(struct icm20608_Dev* dev)
{
    unsigned char value = 0;

    spi_write_reg_onebyte(&icm20608_dev, ICM20_PWR_MGMT_1, 0x80); /*复位,复位后为0x40,睡眠模式 */
    mdelay(50);
    spi_write_reg_onebyte(&icm20608_dev, ICM20_PWR_MGMT_1, 0x01);  /*关闭睡眠,自动选择时钟 */
    mdelay(50);

    value = spi_read_reg_onebyte(&icm20608_dev,ICM20_WHO_AM_I);
    printk("ICM20_WHO_AM_I:read_data: 0x%02X\r\n", value);
    if((value != ICM20608G_ID) && (value != ICM20608D_ID))
    {
        return 1;
    }
    return 0;
}

/*ICM20608设备初始化(即SPI设备初始化)*/
static int icm20608_register_init(struct icm20608_Dev* dev)
{
    unsigned char value = 0;

    spi_write_reg_onebyte(&icm20608_dev, ICM20_PWR_MGMT_1, 0x80); /*复位,复位后为0x40,睡眠模式 */
    mdelay(50);
    spi_write_reg_onebyte(&icm20608_dev, ICM20_PWR_MGMT_1, 0x01);  /*关闭睡眠,自动选择时钟 */
    mdelay(50);

    value = spi_read_reg_onebyte(&icm20608_dev,ICM20_WHO_AM_I);
    printk("ICM20_WHO_AM_I: 0x%02X\r\n", value);
    if((value != ICM20608G_ID) && (value != ICM20608D_ID))
    {
        return 1;
    }
    value = spi_read_reg_onebyte(&icm20608_dev,ICM20_PWR_MGMT_1);
    printk("ICM20_PWR_MGMT_1: 0x%02X\r\n", value);

    return 0;
}

读/写SPI设备中数据的函数:

从 icm20608设备中读取连续多个寄存器数据;

注意:在本实验中,SPI 为全双工通讯没有所谓的发送和接收长度之分。要读取或者发送 N 个字节就要封装 N+1 个字节,第 1 个字节是告诉设备我们要进行读还是写,后面的 N 个字节才是我们 要读或者发送的数据。因为是读操作,因此在第 31 行设置第一个数据 bit7 位 1,表示读操作。

这里在实现 读/写SPI设备中数据的函数时,发现一个问题:

如果 在定义了 spi_transfer结构体变量后没有初始化为 0的话,则SPI通信会出错。具体不知道什么原因。所以,定义好spi_transfer变量后要进行初始化为 0!!!

ICM20608设备初始化函数:

**ICM20608设备初始化工作包括:**复位 ICM20608设备,关闭睡眠模式(ICM20608设备默认是睡眠模式),设置为自动选择时钟。

icm20608_reg.h头文件如下:

复制代码
#ifndef  ICM20608_REG_H_
#define  ICM20608_REG_H_

#define ICM20608G_ID			0XAF	/* ID值 */
#define ICM20608D_ID			0XAE	/* ID值 */

/* ICM20608寄存器 
 *复位后所有寄存器地址都为0,除了
 *Register 107(0X6B) Power Management 1 	= 0x40
 *Register 117(0X75) WHO_AM_I 				= 0xAF或0xAE
 */
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#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

struct icm20608_dev_struc
{
	signed int accel_x_adc;		/* 加速度计X轴原始值 			*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值 			*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 			*/
	signed int temp_adc;		/* 温度原始值 				*/
    signed int gyro_x_adc;		/* 陀螺仪X轴原始值 			*/
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值 			*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 			*/

	/* 下面是计算得到的实际值,扩大100倍 */
	signed int accel_x_act;		/* 加速度计X轴实际值 			*/
	signed int accel_y_act;		/* 加速度计Y轴实际值 			*/
	signed int accel_z_act;		/* 加速度计Z轴实际值 			*/
	signed int temp_act;		/* 温度实际值 				*/
    signed int gyro_x_act;		/* 陀螺仪X轴实际值 			*/
	signed int gyro_y_act;		/* 陀螺仪Y轴实际值 			*/
	signed int gyro_z_act;		/* 陀螺仪Z轴实际值 			*/
};

#endif

编译驱动

终端进入 18_spi工程的根目录下,输入 make命令,对上面驱动代码进行模块化编译:

复制代码
make

可以看出,驱动模块正常编译通过。

**ICM20608设备初始化的函数中,添加了读取 ICM20608寄存器的ID号,为了验证SPI读取数据函数,**接下来就是在开发板上加载驱动,确定SPI读取函数是否正常。

相关推荐
虾..17 小时前
Linux 软硬链接和动静态库
linux·运维·服务器
Evan芙18 小时前
Linux常见的日志服务管理的常见日志服务
linux·运维·服务器
hkhkhkhkh12319 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
HZero.chen21 小时前
Linux字符串处理
linux·string
张童瑶21 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功12321 小时前
什么是SELinux
linux
石小千21 小时前
Linux安装OpenProject
linux·运维
柏木乃一21 小时前
进程(2)进程概念与基本操作
linux·服务器·开发语言·性能优化·shell·进程
Lime-309021 小时前
制作Ubuntu 24.04-GPU服务器测试系统盘
linux·运维·ubuntu
百年渔翁_肯肯1 天前
Linux 与 Unix 的核心区别(清晰对比版)
linux·运维·unix