SPI驱动框架和I2C很类似,都是分为主机控制器和设备驱动,主机控制器也就是SOC的SPI控制器接口。这个也是半导体原厂已经写好了的,所以我们只需要负责种类繁多的SPI设备驱动,使用控制器提供的API函数对驱动设备的寄存器进行配置。
SPI主机驱动
linux内核中使用spi_master表示SPI主机驱动,i2c_adapter代表I2C主机驱动。spi_master是个结构体,定义在在 include/linux/spi/spi.h 文件中,内容如下(有缩减):
第393行,transfer函数,和i2c_algorithm中的master_xfer函数一样,控制器数据传输函数。
第434行,tranfer_one_message函数,也用于SPI数据传输,用于发送一个spi_message,SPI的数据会打包成spi_message,然后以队列方式发送出去。也就是SPI主机端最终会通过transfer函数与SPI设备进行通讯,这个transfer函数就不需要我们管了,这个是原厂已经写好的了,我们只要会用就好。
SPI主机驱动的核心就是申请spi_master,然后初始化spi_master,最后向linux内核注册spi_master。
1,spi_master的申请与释放
spi_alloc_master函数用于申请spi_master,函数原型如下:
dev:设备,一般是platform_device中的dev成员变量
size:私有数据大小,可以通过spi_master_get_devdata函数获取到这些私有数据
返回值:申请到的spi_master
spi_master的释放通过spi_master_put函数完成,当我们删除一个SPI主机驱动的时候就需要释放掉前面申请的spi_master,spi_master_put函数原型如下: master:要释放的spi_master。
2,spi_master的注册与注销
当spi_master初始化完成以后就需要将其注册到linux内核,spi_master注册函数为spi_register_master,原型如下:
master:要注册的spi_master
返回值:0代表成功,负值代表失败
如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:
SPI设备驱动
spi设备驱动也和i2c设备驱动也很类似,linux内核使用spi_driver结构体来表示spi设备驱动,我们再编写SPI设备驱动的时候需要实现spi_driver。spi_driver结构体定义在include/linux/spi/spi.h 文件中,结构体内容如下:
可以看出,spi_driver,i2c_driver和platform_driver基本一样,当SPI设备和驱动匹配成功以后probe函数就会执行。
同样的,spi_driver初始化完后需要向linux内核注册,spi_driver注册函数为spi_register-driver,函数原型如下:
sdrv:要注册的spi_driver。
返回值:0代表注册成功,负值代表注册失败
注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:
spi_driver注册实例程序如下:
SPI设备和驱动匹配过程
SPI设备和驱动的匹配过程是由SPi总线来完成的,这点和platform,I2C等驱动一样,SPI总线为spi_bus_type定义如下:
可以看出spi_match_device就是SPI设备和驱动的匹配函数。
spi_match_device 函数和 i2c_match_device 函数的对于设备和驱动的匹配过程基本一样。
SPI设备驱动编写流程
1,IO的pinctrl子节点创建与修改
首先肯定是根据所使用的IO来创建或修改pinctrl子节点,唯独要注意的是我们需要注意检查相应的IO有没有被其他设备使用
2,spi设备节点的创建与修改
采用设备树的情况下,SPI 设备信息描述就通过创建相应的设备子节点来完成,我们可以打开 imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:
第 309 行,设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。
第 310 行,设置“cs-gpios”属性,也就是片选信号为 GPIO4_IO09。
第 311 行,设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。
第 312 行,设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。
第 313 行,将 ecspi1 节点的“status”属性改为“okay”。
第 315~320 行,ecspi1 下的 m25p80 设备信息,每一个 SPI 设备都采用一个子节点来描述其设备信息。第 315 行的“m25p80@0”后面的“0”表示 m25p80 的接到了 ECSPI 的通道 0上。这个要根据自己的具体硬件来设置。
第 318 行,SPI 设备的 compatible 属性值,用于匹配设备驱动。
第 319 行,“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 20MHz。
第 320 行,reg 属性设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的“0”一样。
3,SPi设备数据收发处理流程
SPI设备驱动的核心是spi_master。我们想内核注册成功spi_driver以后就可以使用SPI核心层提供的API函数对设备进行读写操作了。首先是spi_transfer结构体,此结构体用于描述SPI传输信息,结构体内容如下:
第 609 行,tx_buf 保存着要发送的数据。
第 610 行,rx_buf 用于保存接收到的数据。
第 611 行,len 是要进行传输的数据长度,SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。
spi_transfer需要组织成soi_massage,spi_massage也是一个结构体,内容如下:
在使用spi_massage之前需要对其进行初始化,spi_massage初始化函数为spi_massage_init。函数原型如下:
m:要初始化的spi_massage
spi_message初始化完成以后需要将spi_transfer添加到spi_message队列中,这里我们需用到spi_message-add_tail,函数原型如下:
t:要添加到队列中的api_transfer
m:spi_transfer要加入的spi_massage
spi_message 准备好以后既可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:
spi:要进行数据传输的spi_device
message:要传输的spi_message
- 申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量,tx_buf 为要发送的数据。然后设置rx_buf成员变量,rx_buf保存着接收到的数据,最后设置len成员变量,也就是要进行数据通信的长度
- 使用spi_message_init函数初始化spi_message
- 使用spi_message_add_tail函数将前面设置好的spi_transfer添加到api_message队列中
- 使用spi_sync函数完成SPI数据同步传输
/*
* @description : 从icm20608读取多个寄存器数据
* @param - dev: icm20608设备
* @param - reg: 要读取的寄存器首地址
* @param - val: 读取到的数据
* @param - len: 要读取的数据长度
* @return : 操作结果
*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
int ret = -1;
unsigned char txdata[1];
unsigned char * rxdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}
rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */
if(!rxdata) {
goto out1;
}
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,一共要读取len个字节长度的数据,*/
txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */
t->tx_buf = txdata; /* 要发送的数据 */
t->rx_buf = rxdata; /* 要读取的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
if(ret) {
goto out2;
}
memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */
out2:
kfree(rxdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}
/*
* @description : 向icm20608多个寄存器写入数据
* @param - dev: icm20608设备
* @param - reg: 要写入的寄存器首地址
* @param - val: 要写入的数据缓冲区
* @param - len: 要写入的数据长度
* @return : 操作结果
*/
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
int ret = -1;
unsigned char *txdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)dev->private_data;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
if(!t) {
return -ENOMEM;
}
txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
if(!txdata) {
goto out1;
}
/* 一共发送len+1个字节的数据,第一个字节为
寄存器首地址,len为要写入的寄存器的集合,*/
*txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零 */
memcpy(txdata+1, buf, len); /* 把len个寄存器数据拷贝到txdata里,等待发送 */
t->tx_buf = txdata; /* 要发送的数据 */
t->len = len+1; /* t->len=发送的长度+读取的长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
if(ret) {
goto out2;
}
out2:
kfree(txdata); /* 释放内存 */
out1:
kfree(t); /* 释放内存 */
return ret;
}