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

时间:2024-04-19 17:59:38

一. 简介

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

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

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

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

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

本文在此基础上,编写向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读取函数是否正常。