一. 简介
前面文章介绍了SPI设备数据收发处理流程,后面几篇文章实现了SPI设备驱动框架,加入了字符设备驱动框架代码。文章如下:
SPI 设备驱动编写流程:SPI 设备数据收发处理流程中涉及的结构体与函数-****博客
SPI 设备驱动编写流程:SPI 设备数据收发处理流程-****博客
Linux下SPI设备驱动实验:SPI设备驱动框架编写-****博客
Linux下SPI设备驱动实验:向SPI驱动框架中加入字符设备驱动框架代码-****博客
本文在此基础上,编写向SPI设备发送数据的函数,从SPI设备接收数据的函数。
二. Linux下SPI设备驱动实验:实现SPI发送/接收数据的函数
1. 读写SPI设备中数据的实现思路
向SPI设备中发送数据,从SPI设备接收数据的实现,需要使用内核提供的结构体与 API函数。
涉及的结构体为 spi_transfer 与spi_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读取函数是否正常。