spi 子系统

这篇具有很好参考价值的文章主要介绍了spi 子系统。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

spi在应用层的体现

spi 分为主机模式和从机模式,一般soc 自带的spi 控制器,我们都将它用作主机模式与外挂的从设备通信。从设备例如 oled芯片、flash芯片、陀螺仪芯片等等。
那么spi 驱动和设备,自然也就分为主机驱动、设备和从机驱动、设备。那么如何在Linux 下查看这些信息呢?

首先查看spi 控制器的驱动和设备信息:
spi 控制器的驱动和设备在内核中由platform 总线来管理,使用platform_device 表示设备,platform_driver 表示驱动。所以查找/sys/bus/platform/devices/ 目录就可以发现spi 控制器的设备信息,设备名字由设备树的节点名来决定:
在设备树中找到以下的设备节点,“ecspi@02010000” 是它的节点名
spi 子系统
所以在/sys/bus/platform/devices/ 目录下就可以找到2010000.ecspi 这个文件夹。
它的driver 软链接指向/sys/bus/platform/drivers/spi_imx 就代表它已经匹配上了它的驱动就是spi_imx。
spi 子系统
另外还可以通过查看spi_master 文件夹下的文件夹名字确认 spi_master的总线号(spi_master->bus_num)
spi 子系统
spi_imx 就是imx6ull平台上的spi 控制器驱动,名字根据platform_driver->driver->name 决定。
如果/sys/bus/platform/devices/driver 没有链接到驱动目录,也就是说设备 和驱动没有匹配成功我们也可以到/sys/bus/platform/drivers/ 目录下根据名字来查找。
spi 子系统
除了soc 自带的spi 控制器外,内核还提供了gpio 模拟spi的驱动,以下是它的设备与驱动体现。
(这个gpio 模拟的spi控制器,spi总线号是327666,为啥这么奇怪呢,这个会在之后的spi master驱动中讲解)
spi 子系统
spi 子系统
spi 从设备的设备信息:
spi 从设备的驱动和设备由spi 总线模型管理,使用spi_driver 表示驱动、spi_device 表示设备。
可以在/sys/bus/spi/devices/ 目录下找到所有的spi 从设备。
它们名字的定义是:spiB.D,B表示spi_master 的总线号,D表示该设备是这条spi总线上的第几个设备。(D的值来源于dtb spi从设备节点的reg属性,也表示它使用的是spi 总线的第几个片选)
spi 子系统
同样的设备文件夹中的driver 也链接到驱动文件夹,所有的spi 从设备驱动都在"/sys/bus/spi/drivers" 文件夹下。
spi 子系统

spi 子系统中的重要数据结构

spi 子系统
spi 硬件连接框图,如上图spi 设备有主设备、从设备之分,一般将soc 自带的spi 控制器作为主设备,外接的flash、oled 等芯片作为从设备。

spi_master

内核使用一个struct spi_master 来描述一个spi 主设备(这里也可以默认把它理解成spi 控制器);
对于一个spi 控制器来说,最关心的就是发送和接收数据,所以在spi_master 中最重要的成员就是传输函数spi_master->transfer
//定义于 include\linux\spi\spi.h
spi 子系统
以下内核提供的回调函数都是在spi_master 注册过程中设置的。
(下面标出的回调函数在spi_sync中都会调用到,尤其是master->transfer_one_message、master->transfer_one 这两个函数极为重要)
spi 子系统

spi_device

使用struct spi_device 来描述一个spi 从设备(主要就是一些硬件信息),里面记录有设备的片选引脚、最大传输速率、挂在哪个SPI控制器下面、工作模式等等。(可以参考设备树节点内容来记忆,因为它们的值大多都来自设备树)
在注册spi_master 的过程中会扫描spi_master->device.of_node(spi 控制器设备树节点) 下的所有子节点,将每个子节点转化成一个spi_device 并注册。
//include\linux\spi\spi.h
spi 子系统

spi_transfer

知道了如何描述图中的主设备、从设备,该如何描述主设备与从设备之间传输的数据?

描述一次传输用spi_transfer
在spi_transfer 中最重要的就是tx_bufrx_buf,它们分别用于指向发送和接收的buf地址(buf 内存需要自己分配),len 则是长度。
transfer_list:添加到spi_message->transfers 链表中的节点。

(注意这里的长度并非单指rx_buf 的长度或tx_buf 的长度,而是以它们中最长的为准。
比如读一个寄存器,寄存器地址长度1字节,读1个字节长度的内容。你可以有两种读法:

  1. 用一个spi_transfer 来完成,让tx_buf 指向一个 1字节长度的buf(保存着reg地址),让rx_buf 指向一个 2字节长度的buf(读取到的值将会保存在buf的第二个字节中,对于第一个字节不需要关心它的内容),那么就要设置len 为2,也就是rx_buf 的长度。
  2. 用两个spi_transfer 来完成,第一个spi_transfer 让tx_buf 指向一个 1字节长度的buf(保存着reg地址),len 设置为1,不关心rx_buf;随后第二个spi_transfer 让rx_buf 指向一个 1字节长度的buf(读到的数据将会保存在其中),len 设置为1,不关心tx_buf。)
    spi 子系统
spi_message

spi_message 可以理解为一个消息:
一次动作可能会需要传输多个spi_transfer(比如上述第二种读reg 的情况),为了管理这些spi_transfer,定义了一个spi_message 结构体。
spi 子系统

(详细的传输过程参考下面的 SPI传输原理和 spi_sync 解析章节)

spi_driver

与其它驱动一样,spi 从设备驱动也要按照分离原则设计,spi_driver 就是用来描述一个spi 从设备驱动。
//include\linux\spi\spi.h
spi 子系统
spi 从设备驱动使用spi 总线驱动模型来管理,当spi_device 与spi_driver 匹配时就会调用spi_driver->probe 来执行从设备的驱动代码。

设备树解读

参考内核设备树绑定文档:Documentation\devicetree\bindings\spi\spi-bus.txt
spi 子系统
spi 控制器节点有以下 4个必须的属性:
(#address-cells、#size-cells 这两个属性一般是用来描述子节点需要用几个cells 来描述一段地址,#address-cells 表示起始地址、#size-cells 表示长度。但是在spi 控制器节点中它们有不同的定义)

#address-cells:描述子节点需要用几个cells 来定义片选 (一般情况等于1)。(用子节点的reg属性来定义使用哪个片选)
#size-cells:必须为0。
compatible:描述与该设备兼容的驱动。
spi 子系统
其它比较重要的可选属性:
cs-gpios:描述被用作片选的gpio 引脚。(可能会有一个或多个gpio引脚)
如下图GPIO1_IO00 表示第一个片选的gpio引脚,GPIO1_IO01 表示第二个片选的gpio引脚,以此类推。
spi 子系统
num-cs:片选引脚总数

spi 从设备的设备树属性如下:
spi 从设备节点必须是spi 主设备节点的子节点。
必要属性:

  • reg:用来指示该从设备使用第几个片选引脚。
  • compatible:指示与该设备兼容的驱动。
  • spi-max-frequency:表示从设备所能支持的最大时钟频率。

可选属性:

  • spi-cpol:表示时钟的极性(空闲时是高电平还是低电平)。它是一个空属性(没有值,spi-cpol; 即可不用写= 多少),这个属性存在时表示cpol=1,不存在时表示cpol=0;
  • spi-cpha:表示时钟相位(数据线在时钟第一个跳变沿采集数据,还是第二个跳变沿采集数据)。同上;
  • spi-cs-high:大多数从设备片选都是低电平备选中,有一些特殊的芯片相反是片选高电平选中。当存在spi-cs-high 属性时表示该设备为片选高电平选中。
  • spi-3wire:这是一个空属性(没有值),表示使用SPI 三线模式
  • spi-lsb-first:这是一个空属性(没有值),表示使用SPI传输数据时先传输最低位(LSB)
  • spi-tx-bus-width:表示有几条MOSI引脚;没有这个属性时默认只有1条MOSI引脚
  • spi-rx-bus-width:表示有几条MISO引脚;没有这个属性时默认只有1条MISO引脚
  • spi-rx-delay-us:单位是毫秒,表示每次读传输后要延时多久
  • spi-tx-delay-us:单位是毫秒,表示每次写传输后要延时多久
    spi 子系统
    IMX6ULL SPI 设备树节点实例:
    spi 子系统
    spi 子系统

spi_device 成员解释:
spi 子系统
我们知道spi_device 用来描述一个spi从设备,里面包含从设备的硬件信息,硬件信息有用设备树来描述,那么设备中的属性对应spi_device 中的哪些成员。
master: 表示从设备挂在哪个控制器(哪条spi 总线)下。
max_speed_hz: 最大的时钟速率,来自spi-max-frequency属性。
chip_select: 表示使用第几个片选信号线,来自reg属性。
bits_per_word: 来自应用层传递的参数,表示每次传输至少需要几位。8/12/16 bit等等。
cs-gpio:保存着从设备片选对应的gpio编号,设备树节点中cs-gpios 属性会被解析成cs_gpios[] 数组,可以用cs_gpios[spi_device。chip_select] 来获取它。
mode 相当于flag,u16 中每一个bit 都有特殊的意义:
SPI_CPHA:在第1个周期采样,在第2个周期采样?
SPI_CPOL:平时时钟极性
SPI_CPHA和SPI_CPOL组合起来就可以得到4种模式
SPI_MODE_0:平时SCK为低(SPI_CPOL为0),在第1个周期采样(SPI_CPHA为0)
SPI_MODE_1:平时SCK为低(SPI_CPOL为0),在第2个周期采样(SPI_CPHA为1)
SPI_MODE_2:平时SCK为高(SPI_CPOL为1),在第1个周期采样(SPI_CPHA为0)
SPI_MODE_3:平时SCK为高(SPI_CPOL为1),在第2个周期采样(SPI_CPHA为1)
SPI_CS_HIGH:一般来说片选引脚时低电平有效,SPI_CS_HIGH表示高电平有效
SPI_LSB_FIRST:一般来说先传输MSB(最高位),SPI_LSB_FIRST表示先传LSB(最低位);很多SPI控制器并不支持SPI_LSB_FIRST
SPI_3WIRE:三线模式,SO、SI共用一条线
SPI_LOOP:回环模式,就是SO、SI连接在一起
SPI_NO_CS:只有一个SPI设备,没有片选信号,也不需要片选信号
SPI_READY:SPI从设备可以拉低信号,表示暂停、表示未就绪
SPI_TX_DUAL:发送数据时有2条信号线
SPI_TX_QUAD:发送数据时有4条信号线
SPI_RX_DUAL:接收数据时有2条信号线
SPI_RX_QUAD:接收数据时有4条信号线

spi 驱动框架

spi 子系统
spi 的驱动框架分为控制器驱动 和从设备驱动。
spi 控制器驱动使用"platform 驱动模型" 来管理,在设备树中会有如下图的dtb节点来描述一个spi 控制器,由内核解析成一个platform_device,在spi控制器驱动代码中会注册一个platform_driver,当platform_device 与platform_driver 匹配时就会调用platform_driver->probe。
在probe 函数中,会根据设备树的硬件信息 初始化spi控制器,并构建一个spi_master,调用spi_register_master 来注册一个spi_mster,在注册过程中会再此解析设备树,遍历spi 控制器节点下的所有子节点,把它们转化为一个个spi_device,注册spi_device。

spi 从设备驱动由spi 总线驱动模型来管理,spi_device 在控制器驱动中已经被解析并注册好了,在spi 从设备驱动代码中会构建一个spi_driver 并注册,在注册过程中会查找系统中所有的spi_deivce 进行比较,当匹配成功就调用spi_dirver->probe(在probe中实现从设备的功能)。
(没有设备树的版本中,设备信息使用.c 文件(结构体) 来描述)
spi 子系统

spi 总线驱动模型

在driver/spi/spi.c 的spi_init() 函数中调用bus_register() 注册了spi_bus_type
spi 子系统
spi 子系统
spi 总线驱动模型与 platform类似,都是基于设备-总线-驱动 模型来设计的(匹配原理和过程参考 platform总线)。
在spi 总线驱动模型中 需要调用spi_add_device 来注册spi_device,调用spi_register_driver 来注册spi_driver;
spi_register_driver 中设置spi_driver->bus = spi_bus_type,设置spi_driver->device_driver->probe = spi_drv_probe,然后调用driver_register 注册spi_driver->device_driver。
spi 子系统
在spi_add_device 中,最后会调用device_add 向内核注册spi_device->dev (struct device)
spi 子系统
device 与device_driver 的注册过程就是开始走入设备驱动模型那一套匹配流程。
注册device 时会将device添加到bus_type->p->klist_devices链表,然后遍历bus_type->p->klist_drivers 链表,与链表中的每一个device_driver 一一比较,比较的方法就是调用bus_type->match。
注册device_driver 时会将device_driver添加到bus_type->p->klist_drivers链表,然后遍历bus_type->p->klist_devices 链表,与链表中的每一个device 比较,调用bus_type->match 进行匹配。
详细的匹配过程参考:设备-总线-驱动模型

如果匹配则调用device_driver->probe(因为没有提供bus_type->probe,所以会直接调用device_driver->probe),而在spi_register_driver 函数中device_driver->probe 被设为了spi_drv_probe,在spi_drv_probe 里最终会调用spi_driver->probe。
spi 子系统

简单的看一下spi_bus_type->match——spi_match_device()
spi 子系统

spi 从设备驱动

spi从设备驱动框架

一个spi 从设备驱动主要分为两部分(遵从硬件信息、驱动代码分离原则):硬件信息(spi_device)和驱动代码(spi_driver)。
(在3.x 以后的内核版本spi_device 使用设备树来描述)

所以编写一个从设备驱动有以下两个步骤:

  1. 首先,你需要在设备树spi控制器下添加spi 从设备的子节点。(在spi控制器驱动注册spi_master 时会扫描控制器节点,将每一个子节点创建成一个spi_device)
  2. 其次,你需要编写spi从设备驱动,构建一个spi_driver 并注册它。当spi_device 与spi_driver 匹配时就会调用spi_driver->probe 执行任何你想要做的事,比如调用内核封装的spi 通信函数读写从设备,创建字符设备等等。(spi_device 与spi_driver 匹配过程参考前面的spi 总线驱动模型)
    spi 子系统
    spi 从设备框架示例代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/acpi.h>
#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>
#include <linux/uaccess.h>

static int example_probe(struct spi_device *spi)
{
	return 0;
}

static int example_remove(struct spi_device *spi)
{
	return 0;
}

static const struct of_device_id spidev_dt_ids[] = {
	{ .compatible = "example,spi_drv" },
	{},
};

static struct spi_driver example_spi_driver = {
	.driver = {
		.name =		"example_spi_drv",
		.of_match_table = of_match_ptr(example_dt_ids),
	},
	.probe =	example_probe,
	.remove =	example_remove,
};


static int __init example_init(void)
{
	return spi_register_driver(&example_spi_driver);
}


static void __exit example_exit(void)
{
	spi_unregister_driver(&spidev_spi_driver);
}

module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL");
关于spi 核心层提供的一些spi 通信函数

spi 核心层主要封装了2 个spi 通信的函数:spi_sync 和spi_async,定义于driver/spi/spi.c。
int spi_sync(struct spi_device *spi, struct spi_message *message)
int spi_async(struct spi_device *spi, struct spi_message *message)

它们俩的区别是spi_sync 为同步传输函数,在其中会等待传输完成(可能会睡眠),返回时已经传输完成了(失败或者成功);
spi_async 为异步传输函数,返回时并不代表spi 传输结束,可能还在传输中。
(spi_sync 与spi_async 的使用可以参考前面的spidev 驱动解析 read 章节)

spi_syncspi_async 使用示例:
我们可以参考内核封装的spi_read 与spi_write 代码,对于读数据需要填充rx_buf,对于写数据需要填充tx_buf。
spi_read、spi_write是最基本的使用方法,根据我们的需要也可以构建多个spi_transfer 结构体添加到spi_message 链表中,最后调用spi_sync 传输(根据需求自由发挥)。
spi 子系统
spi 子系统
spi_message_init 与spi_message_add_tail 代码解析
spi 子系统
spi 核心层还在spi_sync 和spi_async 的基础上封装了读写函数,更方便驱动编写(省去 struct spi_transfer 与struct spi_message相关的操作)。
定义于include/spi/spi.h
读取n 个字节
static inline int spi_read(struct spi_device *spi, void *buf, size_t len)
写n个字节
static inline int spi_write(struct spi_device *spi, const void *buf, size_t len)
先写后读n个字节
int spi_write_then_read(struct spi_device *spi,const void *txbuf, unsigned n_tx,void *rxbuf, unsigned n_rx);

应用层读写spi 设备驱动:spidev

在i2c 子系统有自带的drivers/i2c/i2c-dev.c 驱动为应用层创建了/dev/i2c-0、/dev/i2c-1 等等的设备节点,注册了字符设备;我们可以直接使用文件IO 访问设备节点来读写i2c 从设备。
那么在i2c 子系统中有没有类似的驱动呢,有——drivers/spi/spidev.c

字符设备相关的内容参考

spidev 实际上也是一个spi从设备驱动,相比于普通的从设备驱动,
它多注册了字符设备,将SPI IO函数(spi_sync) 封装在file_operations的read、write、ioctl 函数中,这样我们可以在应用层通过文件IO 读写spi数据。

spidev 注册流程

先来简单的看一下spidev.c 中的内容:
从驱动的入口为spidev_init开始,在spidev_init 中它首先调用了register_chardev 申请了SPIDEV_MAJOR 这个主设备号下的0~255 个次设备号,并且使用这个256个次设备号注册了同一个cdev(这说明这256个设备号都使用同一个cdev、file_operations)。
然后创建了一个类。
按照以往的经验来说,它还会为每个设备号创建一个struct device 并注册它(创建/dev/ 目录下的设备节点),但是这里却没有——而是注册了spi_driver。
这是一个spi 从设备驱动,只有存在与其匹配的spi_device 是才会调用spi_driver->probe;先不管从设备从何而来,我们先看看probe 函数中会做什么。
spi 子系统
spi_driver 的实例为spidev_spi_driver,它的probe函数是spidev_probe。
spi 子系统
假设有一个spi 从设备spi_device 与这个spi_driver匹配,调用了spi_driver->probe,传入的参数就是匹配的这个spi_device;
在probe 中首先创建了一个spidev_data 结构体,然后把spi_device 记录到spidev_data->spi 上,初始化了spidev_data->device_entry 链表节点。
(在spidev.c 中spidev_data 是非常重要的数据结构,每一个spidev_data 都对应着一个spi_device ,只有当spi_device 与spidev_spi_driver 匹配后,才会为它创建。
在spidev_data 中描述了它对应着哪个从设备、设备号等等。)
spi 子系统
spi 子系统
在spidev_init 中我们申请了主设备号SPIDEV_MAJOR 下的0~255 个次设备号,在这里它获取0-N_SPI_MINORS 范围内第一个空闲的次设备号,然后赋值到spidev_data->devt。
调用device_create 根据设备号devt、class 创建一个struct device,并向内核注册。
(创建/dev/设备节点,class 就是spidev_init 中所创建的类
名字是spidev.B.D:B 表示这个从设备挂在那一条总线(控制器)下面、D代表它是spi控制器下第n个设备(该设备使用的是控制器的第n个片选引脚) )。

将spidev_data->device_entry 节点添加到一个全局的链表device_list,之后在调用file_operations->open函数时就可以凭借设备号从链表中找出对应的spidev_data。
(在Linux中每一个文件都有唯一的inode 结构体来描述,对于设备文件来说在inode->i_rdev 中记录着设备节点的设备号)
spi 子系统
这下我们就清楚了,spidev.c 注册过程中会申请多个设备号并注册字符设备cdev,创建一个类,还会为每个与spidev_spi_driver 匹配的spi_device 创建一个设备节点。
要知道每个spi_device 就代表一个spi 从设备,有了cdev(里面包含file_operations),又有/dev/ 目录下的设备节点,我们不就可以使用文件IO 来读写spi从设备了嘛(按照i2c_dev.c 驱动的经验在file_operations 的read、write、ioctl 函数中肯定会调用spi 的读写函数来访问spi 从设备)。
那么到底什么样的从设备可以使用spidev 这个驱动来访问?问题的关键就是spi_device 如何与这个spi_drver 匹配。
我们知道spi_device 是在注册spi_master 的过程中遍历spi master设备树节点的所有子节点生成的,那么如何匹配——就是用compatible 来匹配。
查看spidev_spi_driver(struct spi_driver) 的匹配条件,compatible 为"rohm,dh2228fv" ,所以我们只需要在控制器节点下创建一个从设备节点,compatible 值写为"rohm,dh2228fv" 就可以。
spi 子系统
以imx6ull 为例,在设备树中添加如下节点后生成/dev/spidev2.0 节点。
spi 子系统
既然/dev/spidev2.0 是字符设备那么我们就可以用open、read、write… 去访问它,先看看底层的file_operations 中的open、read、write…是怎么写的。
spi 子系统

spidev open流程

当应用层调用open打开/dev/spidevB.D 设备节点时,会调用到spidev_open
spidev_open 中根据inode->i_rdev 记录的设备号在device_list 链表中找到spidev_data,然后为其申请tx_buffer、rx_buffer 的内存空间,并将它设置为file 的私有数据。这样在后面的read、write 等函数中都可以通过私有数据获取到spidev_data。
spi 子系统

spidev read流程

当应用层调用read 读取/dev/spidevB.D 设备节点时,会调用到spidev_read
在spidev_read 函数中从file 私有数据获取到spidev_data,调用spidev_sync_read 读取spi从设备数据,读到的数据会保存到spidev_data->rx_buffer,最后将数据拷贝到应用层buf,返回读取到的数据长度(字节)。
spi 子系统
spidev_sync_read:
spi 传输需要构建一个spi_transfer 结构体用来承载发送和接收的数据,然后将spi_transfer 添加到spi_message 中,调用spidev_sync传输spi_message。(spidev_sync_read 与spidev_sync 都是spidev.c 定义,并非核心层)
(spi_transfer 和spi_message 是用来描述数据的,都很关键)
spi 子系统
spi 子系统
spidev_sync:
spidev_sync 实质是调用spi_async (由spi核心层 driver/spi/spi.c 定义)传输数据:
int spi_async(struct spi_device *spi, struct spi_message *message)
spi_device:指明要向哪个从设备传输数据;
spi_messge:用来描述需要传输的数据;
spi 子系统

spidev write流程

write 的流程和read 是差不多的,只不过读方向换成了写方向,调用spidev_sync_write 来发送数据
spi 子系统
spi 子系统

spidev ioctl流程

ioctl 的内容分为三部分:读取当前spi 设备通信的模式设置spi 设备通信的模式向spi 从设备传输数据
前面的内容和read、write类似,获取spidev_data、spi_device。
spi 子系统
如果应用层传递的cmd 是下列的值,那么就是读取spi 从设备的传输模式,__put_user 会把spi->mode 的值会放入到arg,然后返回应用层。
spi 子系统
如果cmd 的值是下列的值,那么就是设置spi 的从设备传输模式。
__get_user 会把arg 的值拷贝到tmp中,调用spi_setup 设置spi 从设备模式。
spi 子系统
spi 子系统
spi 子系统
最后默认的cmd 值进行传输数据,arg 中保存的数据是struct spi_ioc_transfer(类似于spi_transfer),调用spidev_message 传输数据,n_ioc 是spi_ioc_transfer 结构体的个数。
(ioc 也是spi_ioc_transfer 类型,spidev_get_ioc_message 只不过是将数据从应用层的spi_ioc_transfer 拷贝到内核层的spi_ioc_transfer)
(spidev_message 最终调用核心层提供的spidev_sync 传输数据)
spi 子系统
spi 子系统
spi 子系统
spi 子系统

spidev close流程

close 对应的是file_operations 中的release,即spidev_release。
在spidev_release 中就是做open的反向操作:清除file 的私有数据、释放申请的tx_buffer、rx_buffer 的内存
spi 子系统

spi 应用层读写示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <linux/types.h>
#include <linux/spi/spidev.h>

int fd;

/*
关于读寄存器,可以使用两个transfer(第一个写reg地址,第二个接收数据;参考read_reg_byte),也可以只使用一个transfer(参考read_reg_byte1)
int read_reg_byte1(unsigned char reg,unsigned char *pvalue)
{
        struct spi_ioc_transfer xfer;
        char rxbuf[2] = {0};
        int status;

        memset(&xfer, 0,sizeof(xfer));
        xfer.tx_buf = (unsigned long)&reg;
        xfer.len = 2;
        xfer.rx_buf = (unsigned long)rxbuf;

        status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
        if (status < 0) {
                perror("SPI_IOC_MESSAGE");
                return -1;
        }

        *pvalue = rxbuf[1];

        return 0;
}
*/

/*读取寄存器1个字节,成功返回0,失败返回负数
 * */
int read_reg_byte(unsigned char reg,unsigned char *pvalue)
{
	struct spi_ioc_transfer	xfer[2];
	unsigned char filler = 0xff;	
	int status;

	memset(xfer, 0,sizeof(xfer));
	xfer[0].tx_buf = (unsigned long)&reg;
	xfer[0].len = 1;
	xfer[1].rx_buf = (unsigned long)pvalue;
	xfer[1].len = 1;
	
	status = ioctl(fd, SPI_IOC_MESSAGE(2), xfer);
	if (status < 0) {
		perror("SPI_IOC_MESSAGE");
		return -1;
	}

	return 0;
}

/*写入寄存器1个字节,成功返回0,失败返回负数
 * */
int write_reg_byte(unsigned char reg,unsigned char value)
{
        struct spi_ioc_transfer xfer;
	unsigned char buf[2] = {0};
	int status;

	buf[0] = reg;
	buf[1] = value;

        memset(&xfer, 0,sizeof(xfer));
        xfer.tx_buf = (unsigned long)buf;
        xfer.len = 2;

        status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
        if (status < 0) {
                perror("SPI_IOC_MESSAGE");
                return -1;
        }

        return 0;
}



/* usege:
 * read:./spi_reg /dev/spidevB.D reg
 * write:/spi_reg /dev/spidevB.D reg value
 *
 * 读写长度默认是1字节*/

int main(int argc,char** argv)
{
	unsigned char reg = 0,value = 0;
	int ret;

	if(argc == 3)
	{
		reg = (unsigned char)strtoul(argv[2],NULL,0);
	}else if(argc == 4)
	{
		reg = (unsigned char)strtoul(argv[2],NULL,0);
		value = (unsigned char)strtoul(argv[3],NULL,0);
	}else{
		printf("usege:\n read:./spi_reg /dev/spidevB.D reg\n write:/spi_reg /dev/spidevB.D reg value\n");
		return -1;
	}
	
	fd = open(argv[1], O_RDWR);
	if (fd < 0) {
		perror("open");
		return 1;
	}

	/*
	 *本示例用于读写icm20608芯片,该芯片reg地址只有7bit,最高位为读写位(读1,写0)
	 * */
	if(argc == 3)
        {
        	ret = read_reg_byte(reg | 0x80,&value);
	 	if(ret)
			return -1;
		printf("read value 0x%x\n",value);	
        }else if(argc == 4)
        {
        	ret = write_reg_byte(reg & (~0x80),value);
	 	if(ret)
			return -1;
        }

	close(fd);

	return 0;
}

SPI传输原理

使用SPI传输时,最小的传输单位是"spi_transfer",
对于一个设备,可以发起多个spi_transfer,
这些spi_transfer,会放入一个spi_message里,
属于同一spi_master 的spi_message 会放入一个queue 队列里。

  • spi_transfer:指定tx_buf、rx_buf、len
    spi 子系统

  • 一次动作的一个或多个spi_transfer,使用spi_message来管理(添加到spi_message->transfers 链表里):
    spi 子系统

  • 同一个SPI Master下的spi_message,放在一个队列里(spi_master->queue):
    spi 子系统
    所以,反过来,SPI传输的流程是这样的:

  • 从spi_master的队列里取出每一个spi_message

    • 从spi_message的队列里取出一个spi_transfer
      • 处理spi_transfer

spi_sync 传输的过程就是根据以上原理来编写的。

spi_sync 解析

spi_sync 是spi 核心层提供的spi 数据传输函数,它只是一些纯软件代码,最终传输数据还是需要调用spi 控制器驱动提供的传输函数。
对于spi 控制器驱动来说,编写方法有两种(一种老方法,一种新方法),因此由于spi 控制器驱动的编写方法不同spi_sync 的处理方式也会不同。
那么接下来就来看看这两种方法。

老方法:

int spi_sync(struct spi_device *spi, struct spi_message *message)
spi_sync 传递的参数有spi_device 和spi_message;spi_device 表示向哪个从设备传输,spi_message 包含有需要传输的数据。

第一步,spi_sync 会通过spi_device 得到spi_master 也就是spi的控制器,调用spi_master->transfer 来传输数据:
在spi_master->transfer 函数中,会调用list_add_tail 将要传输的数据spi_message 放入spi_master->queue 队列尾部,然后执行schedule_work() 调度工作队列启动传输(注意:这里只是启动传输,接下去transfer 会立马返回,然后进入休眠等待spi_message传输完成)

第二步,work_struct->func 被调用:
它需要取出 spi_master->queue 队列里的每一个spi_message,对每一个spi_message 进行传输。(被取出的spi_message 从master->queue 中删除)
一个spi_message 中可能包含有多个spi_transfer,所以对于一个message 的处理,需要遍历message->queue 链表里的每一个spi_transfer,处理每一个spi_transfer。
处理一个spi_transfer:取出tx_buffer、rx_buffer 开始发送、接收数据。
发送:将tx_buffer 数据一个字节一个字节放入发送数据寄存器(txFIFO)。当txFIFO 满时就停下来等待,等到txFIFO 中的数据被发出去,就会产生硬件中断,在中断处理函数中唤醒休眠,继续放入数据,如此反复,直到tx_buffer 中所有的数据被发送完成。
接收:发送的过程中,spi 总线上的数据同时也会被放入rxFIFO,从rxFIFO 中将数据读入rx_buffer。
当一个spi_message 中所有的spi_transfer 都被处理完了,那么就代表这个message 处理好了,需要调用msg->complete(msg->context) 来唤醒spi_sync 中的休眠线程。
(spi_message->context 是一个struct completion 类型的,每一个msg对应的completion 都是唯一的,调用msg->complete(msg->context) 时只会唤醒传输当前的msg 那个线程)

(对于老方法:spi_master->transfer函数和work_struct->func 都是由spi master驱动提供的(work_struct 需要在probe中提前初始化好))
spi 子系统
spi 子系统

代码解析
spi 子系统
在__spi_sync 中会设置spi_message->complete(当传输完成时会调用complete唤醒休眠的线程),然后调用spi_async_locked 开始传输数据(这里只是调度工作队列开始传输流程,并没有真正的传输),从spi_async_locked 返回后,如果数据没有传输完成,该线程就会进入休眠。
spi 子系统
spi 子系统
spi 子系统
调用__spi_async
spi 子系统
调用spi_master->transfer,该函数是由spi 控制器驱动编写的。
spi 子系统
使用老方法的spi 控制器驱动并不多,参考 drivers/spi/spi-sh.c。
找到spi-sh.c 中的spi_master->transfer 函数——spi_sh_transfer
在spi_sh_transfer 中将spi_message 添加到spi_master->queue 链表里,然后调度工作线程(工作线程会执行work_struct->func 我们提供的工作函数)。
spi 子系统
在spi_sh_probe 中初始化了ss->ws(struct work_struct) 和ss->workqueue,ss->ws的工作函数为spi_sh_work。
spi 子系统
在spi_sh_work 函数中依次取出spi_master->queue 链表中的spi_message,对每个spi_message 遍历message->transfers 链表中的每一个spi_transfer 进行传输,调用spi_sh_send 发送数据或调用spi_sh_receive 接收数据,当一个spi_message 中所有的spi_transfer 被发送完成后就会调用spi_message->complete 唤醒在__spi_sync 中被休眠的线程。
spi 子系统
在spi_sh_send 中执行真正的硬件发送工作:
首先计算出当前txFIFO 中剩余的空间cur_len,先将spi_transfer->tx_buffer 中cur_len 长度的数据一个字节一个字节的写入到发送数据寄存器。然后判断是否发送完所有的数据,如果还有数据没被发送,那么就进入休眠等待,等到上一次被放入txfifo的数据发送完成就会长生中断,在中断中唤醒休眠的发送线程,接着发送剩余的数据。
如果当前的spi_transfer 是spi_message 中的最后一个transfer,那么就需要等待最后一组数据发送完成才能返回。
spi 子系统
spi 子系统
spi 子系统
spi_sh_receive 接收函数
spi 子系统

新方法5.4:

**pump: ** 译为抽出、注入等意思。这里理解成将msg从master->queue 队列中抽出,然后传输msg。

新方法相比老方法在框架上复杂了很多,理解更难,但是对编写控制器驱动来说会方便一些。
对于老方法来说,将msg 放入队列、再从队列取出msg、再取出msg中的每一个spi_transfer、处理spi_transfer 这些操作都是由控制器驱动编写的spi_master->transfer 和工作函数来完成的。

对于新方法,__spi_queued_transfer 函数会负责将msg 添加到master->queue 队列;
__spi_pump_messages函数,它有两种情况:
它先判断spi_master->cur_msg是否为空,空那么代表当前spi_master 没有在处理任何msg,那么它就会从队列抽出第一个msg,然后处理msg。
如果spi_master->cur_msg 不为空,那么就直接返回,并等待。(它已经把msg 放入到队列里,自然会有work func 来处理msg)
在处理msg前多了几步准备工作:执行master->prepare_transfer_hardware(它是由spi核心层提供的);另外如果你的驱动提供了master->prepare_message 那么就会执行prepare_message。
执行spi_map_msg。
然后在__spi_pump_messages 函数中会调用master->transfer_one_message (它代表了处理一个spi_message,它是由内核提供的)。
怎么处理一个msg呢?处理msg 中的每一个spi_transfer。
传输spi_transfer 前,对于同属一个spi_message的spi_transfer,它们肯定是发向同一个spi_device 的,所以在这里拉下片选。
然后开始遍历spi_transfer,传输每一个spi_transfer。
为了保证每个spi_transfer 都能被同步的传输,在传输spi_transfer的前后要加上reinit_complete(&master->xfer_completion) 和wait_for_completion_timeout(&master->xfer_completion,timeout)(等待),一个spi_transfer 传输完成后要调用spi_finalize_current_transfer(master) 来提示完成一个spi_transfer 传输,spi_finalize_current_transfer 中会调用complete(&master->xfer_completion) 唤醒等待。
那么怎么传输一个spi_transfer 呢?调用master->transfer_one(它代表处理一个spi_transfer,它也是由内核提供的)。
在transfer_one 中:
调用bitbang->setup_transfer(它是传输前的硬件设置,由spi master 驱动提供,也可不提供不执行)
调用bitbang->txrx_bufs 来传输spi_transfer 中的数据。(这是真正的数据发送、接收函数,由spi master 驱动提供)
最后调用spi_finalize_current_transfer 来结束一个spi_transfer 传输。

如此循环的执行红色方框中的内容,直到所有的spi_transfer 都被传输完毕,代表一个spi_message 处理完毕,此时拉起片选。
一个spi_message 传输完毕则要调用spi_finalize_current_message 提示spi_message 传输完毕。
在spi_finalize_current_message 中需要做准备的反向工作:
spi_unmap_msg(master,mesg);
master->unprepare_message(master,mesg);
kthread_queue_work(&master->kworker,&master->pump_messages); //待研究
msg->complete(msg->context); //唤醒__spi_sync 中的等待

(在bitbang->txrx_bufs 传输数据的过程中,还需要添加额外的等待、唤醒函数(由中断唤醒))
spi 子系统
代码解析:
spi 子系统
新方法调用__spi_queued_transfer 将spi_message 添加到spi_master->queue 后立即返回__spi_sync
spi 子系统
spi 子系统
__spi_sync 调用__spi_pump_messages 开始处理spi_message。
__spi_pump_messages 的处理状态有两种:
1、如果spi_master->cur_msg 不为空,表示已经有一个msg在处理,那么就会直接返回,等待msg处理完成。(msg 已经被添加到队列中,spi_master->kworker 线程会从队列中一一取出msg,并处理它们)
2、如果spi_master->cur_msg == NULL,那么代表当前没有msg 正在处理,那么就会取出队列的第一个msg 处理它。(处理完第一个msg后还会启动spi_master->kworker 线程,让它负责处理剩余的msg。)
spi 子系统
spi 子系统
从队列中取出第一个msg 赋值到master->cur_msg,并将这个msg 从队列移除。
spi 子系统
__spi_pump_messages 传输msg 前做准备工作:
ctlr->prepare_transfer_hardware
ctlr->prepare_message
spi_map_msg
最后调用ctlr->transfer_one_message 传输一个msg,即master->cur_msg。
spi 子系统

在kernel 5.4版本中ctlr->transfer_one_message 被设置为了spi_transfer_one_message
spi_bitbang_start //注册spi_bitbang
->spi_register_master //注册spi_master
->spi_controller_initialize_queue
spi 子系统
查看spi_transfer_one_message 函数,它的目的就是传输一个spi_message。
所谓的传输msg,就是传输msg中的每一个spi_transfer,在一个msg里的transfer 毫无疑问都是发给同一个spi_device 的,所以在开始传输transfer前设置片选。
遍历spi_message->transfers 链表中的所有spi_transfer,遍历过程中调用ctlr->transfer_one 来传输一个spi_transfer。(在kernel5.4 中ctlr->transfer_one 被设置为spi_bitbang_transfer_one,它是内核提供的)
在ctlr->transfer_one 后需要等待transfer 传输完成。
每当一个transfer传输完成就需要统计以处理的长度 msg->actual_length 加上当前transfer的数据长度。
当msg中的所有spi_transfer 被传输完后,取消片选、设置msg->status = 0、调用spi_finalize_current_message 结束一个spi_message 传输流程。
spi 子系统
spi 子系统
查看spi_bitbang_transfer_one 如何传输一个spi_transfer:
在传输spi_transfer 前,你可以做一些准备工作bitbang->setup_transfer(由spi_master驱动提供),比如在spi-imx.c 中它会根据spi_transfer 的位数长度、主从模式等设置发送、接收函数。
接着调用bitbang->txrx_bufs 传输spi_transfer。(真正的数据发送、接收函数,是由spi_master驱动提供)
最后调用spi_finalize_current_transfer 结束一个spi_transfer 传输。
spi 子系统
bitbang->setup_transfer
spi 子系统
以spi-imx.c 为例bitbang->txrx_bufs 是spi_imx_transfer
spi 子系统
spi 子系统
spi_transfer数据已经在txrx_bufs 函数中传输完成,查看spi_finalize_current_transfer 结束一个spi_transfer 传输需要哪些收尾工作:
这不就是提示spi_transfer 完成的函数(执行过complete后spi_transfer_one_message 中的等待函数便不会等待,返回后直接走过。)
spi 子系统
spi_transfer_one_message
spi 子系统
就这样不停的遍历spi_transfer,不停的调用ctlr->transfer_one 传输spi_transfer,所有的transfer 都遍历完了,一个spi_message 的数据就处理完了。
看看spi_finalize_current_message 有哪些收尾工作:
spi 子系统
spi_register_master 注册spi_master 时master->pump_messages 的work 函数被设置为spi_pump_message
spi 子系统
spi 子系统
linux 5.4 内核spi_sync 传输流程解析完毕。
遗留问题:队列中的其它msg 交由worker线程执行__spi_pump_messages 是如何处理的?
涉及到completion 等待、__spi_pump_messages 处理流程等问题。

新方法4.1.15

spi 子系统
spi 子系统

在4.x 与5.x 的内核中spi_sync 这块略有不同:
对于5.x 的内核它提供了master->transfer_one_message (spi_transfer_one_message) 表示传输一个spi_message,在spi_transfer_one_message 中会遍历一个spi_message中的所有spi_transfer 反复调用master->transfer_one (spi_bitbang_transfer_one) 表示传输一个spi_transfer,在spi_bitbang_transfer_one 中会调用spi_bitbang->txrx_bufs 来真正的传输一个spi_transfer。

而对于4.x 的内核master->transfer_one_message 被设置为spi_bitbang_transfer_one,在spi_bitbang_transfer_one中会遍历一个spi_message 中的所有 spi_transfer,然后直接调用spi_bitbang->txrx_bufs 来传输spi_transfer。

以上只是内核的框架代码变了,对于驱动编写来说还是一样的,最终实现还是调用spi_bitbang.chipselect、bitbang.setup_transfer、bitbang.txrx_bufs 这几个函数,我们只要提供它们就好了。

4.1.15 master->transfer_one_message (spi_bitbang_transfer_one) 代码解析
spi 子系统spi 子系统
spi 子系统

spi控制器驱动解析

(以imx6ull spi 控制器驱动为例,它使用的是新版spi 驱动框架)
在spi 控制器驱动中,我们主要的任务就是构建spi_master、spi_bitbang、填充它们,最后注册它们,这一过程主要在probe 函数中进行。
在spi_bitbang 中最重要的成员:为了实现spi 数据的传输,我们需要实现片选设置回调函数(bitbang.chipselect)、传输设置回调函数(bitbang.setup_transfer)、数据发送接收回调函数(bitbang.txrx_bufs)等等。

下面以spi-imx 驱动为例,看看具体是怎么做的:
spi_imx_probe
spi 子系统

spi 子系统
spi 子系统
spi 子系统
spi 子系统

spi_imx_chipselect 片选设置函数
spi 子系统
spi_imx_transfer spi数据传输函数
spi 子系统
spi 子系统

spi_master 注册流程

我们将struct spi_master 理解成为一个spi 控制器,在控制器驱动probe 函数中需要构建、设置、注册一个spi_master。调用spi_register_master 注册。
对于新版本的spi 控制器驱动,spi_master 被包含在struct spi_bitbang 内,所以我们需要构建、设置spi_master 之外,还需要构建、设置、注册一个spi_bitbang。调用spi_bitbang_start 注册spi_bitbang,其中会调用spi_register_master 注册master,所以probe 中无需注册master。

(代码参考Linux-5.4.47)
spi_bitbang_start
接下来就分析spi_bitbang_start 函数的调用过程:
spi 子系统
在spi_bitbang_init 中设置了master的几个重要的回调函数:
master->prepare_transfer_hardware
master->unprepare_transfer_hardware
master->transfer_one
master->set_cs

它们在spi_sync 中都有调用到。
spi 子系统
接下来真正的spi_master 注册流程就开始了:spi_register_master
在5.4 内核中,它将spi_master 改名为spi_controller,所以注册函数也改名为spi_register_controller。
#define spi_register_master(_ctlr) spi_register_controller(_ctlr)

spi_register_controller
在spi_register_controller 中首先会设置spi_master->num_bus。
有两种方式可以获取master->bus_num:

  1. 通过设备树的别名获取,bus_num 2 就是通过设备树别名获得。(使用这种方式要在probe中设置master->bus_num = -1)
    spi 子系统
  2. 通过idr_alloc 获取,这种方式是利用(1<<15)-1 = 32767的方式来计算的,会根据系统中的总线依次减小32766、32765…,下图中的32766 就是用这种方式得出的。
    spi 子系统
    spi 子系统
    初始化spi_master 的链表、锁、completion等等。master->xfer_completion 是用来保证spi_transfer 同步传输。(参考spi_sync 解析)
    spi 子系统
    获取设备树cs-gpios 属性中描述的片选引脚gpio 编号,保存到spi_master->cs_gpios。(它是一个int* 型指针指向一个数组,数组的每一项保存着每个片选引脚gpio 编号)
    spi 子系统
    spi 子系统
    调用device_add 注册spi_master->dev (struct device),注册这个device 应该是在spi_mater 文件夹下生成spi2 文件夹。
    spi 子系统
    spi 子系统
    spi 子系统
    设置spi_master->transferspi_master->transfer_one_message (代表传输一个spi_message,参考spi_sync)。
    如果你在驱动中设置了spi_master->transfer 那么你就是使用的老方法,不会走入else 分支。
    spi 子系统
    spi_master->transfer_one_message 被设置为了spi_transfer_one_message
    spi 子系统
    内核为了管理这些spi_master,建立了一个全局链表spi_controller_list,注册spi_master 的时候就会把它添加到链表中。

重点of_register_spi_devices ,它会遍历master->dev.of_node 所有子节点,并按照每个子节点生成spi_device(描述一个spi从设备)。

对于这种检索子节点,然后生成从设备的形式,其它总线也有类似的,比如i2c、mdio 等等,我们可以把这种套路记忆下来,对类似代码比较容易理解。
spi 子系统
spi 子系统
of_register_spi_device 中会根据of_node 创建一个spi_device,解析设备树的各个属性值,填充到spi_device 中,最终调用spi_add_device 注册spi_device。
spi_add_device 这里就开始走入总线、设备、驱动模型了,它会调用device_add 添加spi_device->dev,然后与bus_type 下的driver 进行匹配,匹配后调用driver->probe 或bus_type->probe。(详细参考spi总线驱动模型)
spi 子系统
spi_alloc_device
spi 子系统
of_modalias_node
spi 子系统
of_spi_parse_dt
spi 子系统
关于片选,我们在控制的时候就可以通过spi_master->cs_gpios[spi_device->chip_select] 来获取从设备对应的片选gpio编号。
spi 子系统
spi_add_device 注册一个spi_device,注册过程参考前面的spi总线驱动模型。

spi 控制器虚拟驱动编写

参照老方法编写一个虚拟的spi 控制器驱动

在设备树中创建一个虚拟的spi master 节点,同时包含一个spi_device 节点 (compatible设置为spidev.c 的值)。
编写一个虚拟的spi master驱动代码,注册一个platform_driver,在probe中 注册spi_master。
内核启动后spi master设备树节点会被转化成一个platform_device。
当我们加载驱动时,如果platform_driver与platform_device 匹配就会调用platform_driver->probe 注册spi_master,master注册过程中创建子节点对应的spi_device。
spi_device 与spidev.c 驱动(spi_driver) 匹配,就会注册字符设备、创建/dev/spidev.B.D 设备节点,这样我们就可以使用应用程序来控制发送和接收。
效果如下:
spi 子系统
spi 子系统

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h>
#include <linux/workqueue.h>

#include <linux/spi/spi.h>
#include <linux/spi/spi_bitbang.h>

#define DRIVER_NAME "virt_spi"

static struct spi_master *master;
static struct work_struct* ws;

/*
 * 在work 函数中需要:
 * 取出spi_master->queue 链表中的每一个spi_message,处理spi_message。
 *
 * 对于处理一个spi_message 需要:
 * 遍历spi_message->queue 链表中的每一个spi_transfer,处理spi_transfer.
 * 
 * 处理一个spi_transfer:
 * 将spi_transfer->tx_buffer数据一个个字节放入发送数据寄存器(txFIFO)。
 * (当txFIFO满,来不及发送时需要休眠等待,
 *  txfifo中的数据被发送出去后会产生中断,由中断唤醒work中的等待线程)
 * 并且将接收数据寄存器中的数据读到rx_buffer。
 *
 * 当处理完一个spi_message 之后要调用mesg->complete(mesg->context) 唤醒spi_sync 中的线程。
 * work 函数一直处理,直到master->queue 中所有的mesg被处理完。
 *
 * */
static void spi_virt_work(struct work_struct *work)
{
	struct spi_message *mesg;
	struct spi_transfer *t;

	while(!list_empty(&master->queue))	//判断链表是否不为空
	{
		//获取master->queue 链表中第一个节点对应的spi_message
		mesg = list_entry(master->queue.next,struct spi_message,queue);	
		list_del_init(&mesg->queue);	//将mesg 节点从master->queue 链表中删除


		list_for_each_entry(t,&mesg->transfers,transfer_list)	//遍历每一个spi_transfer
		{
			//发送接收数据略过(对于具体硬件的发送、接收过程中还会涉及到等待、唤醒等操作)
			//tx 将数据放入txfifo 然后等待发送完成,发送完成后产生中断,在中断处理函数中唤醒等待,继续发送...
			//rx ...
			if (t->tx_buf) {
				printk("%s tx\n",__func__);
			}

			if (t->rx_buf) {
				printk("%s rx\n",__func__);
			}

			mesg->actual_length += t->len; //已处理的长度++
		}

		//处理完成一个spi_message
		mesg->status = 0;
		mesg->complete(mesg->context);
	}

}

int virt_spi_transfer(struct spi_device *spi,struct spi_message *mesg)
{

	mesg->actual_length = 0;	//actual_length 表示已处理的数据长度
	mesg->status = -EINPROGRESS;	//-EINPROGRESS 表示正在处理中

	list_add_tail(&mesg->queue,&spi->master->queue);
	schedule_work(ws);
	return 0;
}

/*在probe 函数中需要创建、设置、注册一个spi_master 
 *
 * */
static int virt_spi_probe(struct platform_device *pdev)
{
	int ret;
	ws = devm_kzalloc(&pdev->dev,sizeof(struct work_struct),GFP_KERNEL);

	//创建spi_master
	//spi_alloc_master 第二个参数为申请一段私有数据的内存,我们不需要
	master = spi_alloc_master(&pdev->dev,0);
	if (master == NULL) {
		dev_err(&pdev->dev, "spi_alloc_master error.\n");
		return -ENOMEM;
	}

	//根据spi_sync 分析,老方法需要提供spi_master->transfer 函数,传输数据
	master->transfer = virt_spi_transfer;
	//注册spi_master 过程中,需要扫描spi 控制器节点下的所有子节点,转化为spi_device。
	//所以需要将spi_master->dev.of_node 设置为pdev->dev.of_node。
	//如果不设置spi_master->dev.of_node,那么将不会创建spi_device。(不会报错!!!)
	master->dev.of_node = pdev->dev.of_node;

	INIT_WORK(ws, spi_virt_work);	//初始化work_struct->func 工作函数		
	INIT_LIST_HEAD(&master->queue);

	ret = spi_register_master(master);
	if (ret < 0) {
		printk(KERN_ERR "spi_register_master error.\n");
		spi_master_put(master);
		return ret;
	}

	return 0;
}
/*
 * 在remove 中需要注销spi_master
 * */
static int virt_spi_remove(struct platform_device *pdev)
{
	spi_unregister_master(master);
	return 0;
}

static const struct platform_device_id virt_spi_devtype[] = {
	{ .name = DRIVER_NAME, }, 
	{	/* sentinel */}
};

static const struct of_device_id virt_spi_dt_ids[] = {
	{ .compatible = DRIVER_NAME, },
	{ /* sentinel */ }
};

static struct platform_driver virt_spi_platform_driver = {
	.probe = virt_spi_probe,
	.remove = virt_spi_remove,

	.id_table = virt_spi_devtype,
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = virt_spi_dt_ids,
	},
};


static int __init virt_spi_init(void)
{
	return platform_driver_register(&virt_spi_platform_driver);
}

static void __exit virt_spi_exit(void)
{
	platform_driver_unregister(&virt_spi_platform_driver);
}

module_init(virt_spi_init);
module_exit(virt_spi_exit);
MODULE_LICENSE("GPL");

参照新方法编写一个虚拟的spi 控制器驱动

与老方法不同的是,在新方法probe 中需要构建、注册一个spi_bitbang,注册spi_bitbang 时会注册spi_master。
设置bitbang 时比较重要的成员:
spi_bitbang.master
bitbang.chipselect = spi_virt_chipselect; //片选设置函数(必须)
bitbang.txrx_bufs = spi_virt_transfer; //硬件传输函数(必须)
bitbang.setup_transfer //传输前的硬件设置函数(根据硬件需要选择是否提供)文章来源地址https://www.toymoban.com/news/detail-498738.html

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_gpio.h>
#include <linux/workqueue.h>

#include <linux/spi/spi.h>
#include <linux/spi/spi_bitbang.h>

#define DRIVER_NAME "virt_spi"

struct virt_spi{
	struct spi_bitbang bitbang;
	struct completion xfer_done;	//确保一个spi_transfer 传输完成
};

static struct spi_master *master;
static struct virt_spi *virt_spi;

//spi_bitbang->txrx_bufs 数据传输函数
static int spi_virt_transfer(struct spi_device *spi,struct spi_transfer *transfer)
{
	reinit_completion(&virt_spi->xfer_done);

	//假装发送
	printk("%s \n",__func__);

	//唤醒completion,对于真实的硬件驱动,唤醒工作应该由中断来做,为了简单直接在这里执行完成
	complete(&virt_spi->xfer_done);

	wait_for_completion(&virt_spi->xfer_done);	//等待从completion 完成

	return transfer->len;
}

//spi_bitbang->chipselect 片选设置函数
static void spi_virt_chipselect(struct spi_device *spi, int is_active)
{
}

/*在probe 函数中需要:
 * 创建、设置、注册一个spi_master
 * 创建、设置、注册一个spi_bitbang 
 *
 * */
static int virt_spi_probe(struct platform_device *pdev)
{
	int ret;

	//创建spi_master、spi_bitbang
	//创建spi_master + 私有数据,私有数据中包含了spi_bitbang
	master = spi_alloc_master(&pdev->dev,sizeof(struct virt_spi));
	if (master == NULL) {
		dev_err(&pdev->dev, "spi_alloc_master error.\n");
		return -ENOMEM;
	}

	virt_spi = spi_master_get_devdata(master);	//获取spi_master私有数据,master[1]
	virt_spi->bitbang.master = master;
	virt_spi->bitbang.chipselect = spi_virt_chipselect;	//片选设置函数
	virt_spi->bitbang.txrx_bufs = spi_virt_transfer;		//传输函数

	init_completion(&virt_spi->xfer_done);		//completion 使用前,先要初始化它


	//注册spi_master 过程中,需要扫描spi 控制器节点下的所有子节点,转化为spi_device。
	//所以需要将spi_master->dev.of_node 设置为pdev->dev.of_node
	master->dev.of_node = pdev->dev.of_node;

	//注册spi_bitbang 时会调用spi_register_master 注册spi_master
	ret = spi_bitbang_start(&virt_spi->bitbang);
	if (ret) {
		dev_err(&pdev->dev, "bitbang start failed with %d\n", ret);
	}

	return 0;
}
/*
 * 在remove 中需要注销spi_bitbang、释放spi_master
 * 注销spi_bitbang 时会注销spi_master
 * */
static int virt_spi_remove(struct platform_device *pdev)
{
	spi_bitbang_stop(&virt_spi->bitbang);
	spi_master_put(master);
	return 0;
}

static const struct platform_device_id virt_spi_devtype[] = {
	{ .name = DRIVER_NAME, }, 
	{	/* sentinel */}
};

static const struct of_device_id virt_spi_dt_ids[] = {
	{ .compatible = DRIVER_NAME, },
	{ /* sentinel */ }
};

static struct platform_driver virt_spi_platform_driver = {
	.probe = virt_spi_probe,
	.remove = virt_spi_remove,

	.id_table = virt_spi_devtype,
	.driver = {
		.name = DRIVER_NAME,
		.of_match_table = virt_spi_dt_ids,
	},
};


static int __init virt_spi_init(void)
{
	return platform_driver_register(&virt_spi_platform_driver);
}

static void __exit virt_spi_exit(void)
{
	platform_driver_unregister(&virt_spi_platform_driver);
}

module_init(virt_spi_init);
module_exit(virt_spi_exit);
MODULE_LICENSE("GPL");

到了这里,关于spi 子系统的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 应用层:域名系统DNS

    笔记来源: 湖科大教书匠:应用层概述 湖科大教书匠:域名系统DNS 声明:该学习笔记来自湖科大教书匠,笔记仅做学习参考 DNS报文使用运输层的UDP协议进行封装,运输层端口号53 域名系统DNS的作用 域名(www.×××.com)- DNS - IP地址 - ARP - MAC地址 因特网不能只使用一台DNS服务

    2024年02月13日
    浏览(56)
  • 【系统设计系列】 应用层与微服务

    System Design Primer: 英文文档 GitHub - donnemartin/system-design-primer: Learn how to design large-scale systems. Prep for the system design interview. Includes Anki flashcards. 中文版: https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md 初衷主要还是为了学习系统设计,但是这个中文版看起来就像机

    2024年02月09日
    浏览(44)
  • OpenHarmony开发- 应用子系统/Launcher

    Launcher 作为系统人机交互的首要入口,提供应用图标的显示、点击启动、卸载应用,并提供桌面布局设置以及最近任务管理等功能。 Launcher 采用 扩展的TS语言(ArkTS)开发,主要的结构如下: product 业务形态层:区分不同产品、不同屏幕的各形态桌面,含有桌面窗口、个性化

    2024年04月16日
    浏览(47)
  • 鸿蒙OpenHarmony技术:【应用子系统/Launcher】

    Launcher 作为系统人机交互的首要入口,提供应用图标的显示、点击启动、卸载应用,并提供桌面布局设置以及最近任务管理等功能。 Launcher 采用 扩展的TS语言(ArkTS)开发,主要的结构如下:  product  业务形态层:区分不同产品、不同屏幕的各形态桌面,含有桌面窗口、个性

    2024年04月14日
    浏览(33)
  • 【WSA】启动 Windows 安卓子系统的内置设置应用

            Windows 11 终于推送了安卓子系统,本想在 Windows 上尝试基于无障碍服务的脚本运行,结果发现微软将无障碍服务入口隐藏了,即使通过软件触发,也是跳转到 Windows 系统的无障碍设置页面。         既然无法通过应用跳转到无障碍服务设置页面,那就只能通过

    2024年02月12日
    浏览(59)
  • Windows11安装安卓/Android子系统运行安卓应用程序详细教程

    开启电脑的虚拟化支持,在控制面板-程序和功能-启用和关闭windows功能。选择 Hyper-V 和 虚拟机平台 ,然后重启电脑即可。 访问 https://store.rg-adguard.net/ 搜索 https://www.microsoft.com/store/productId/9P3395VX91NR 下载最大的一个文件,即安卓子系统文件 下载地址: http://tlu.dl.delivery.mp.mi

    2024年02月03日
    浏览(55)
  • 解决WIN11安卓子系统WSA闪退导致无法打开应用的方法

    在最近的Windows 11操作系统中,引入了Windows Subsystem for Android(WSA),它允许用户在Windows系统上运行Android应用程序。然而,有时候可能会遇到WSA闪退的问题,导致无法正常打开应用程序。本文将介绍一些可能的解决方法,帮助您解决这个问题。 方法一:重新启动WSA服务 打开命

    2024年02月06日
    浏览(55)
  • WIN11安卓子系统WSA闪退之后无法打开应用的解决方法

    网上很多方法都试过,什么“修复”,关掉进程,重启都试过了,但是想保留数据所以不想重置 最后发现打不开的原因:C盘空间满了 但是我在安装的时候明明设置在别的盘 但是用TreeSizeFree一查才知道,WSA背地里把数据都放在C盘里这个叫userdata.vhdx的文件 解决方法就是使用软

    2024年02月08日
    浏览(83)
  • 【计网笔记06】计算机网络之应用层协议(SMTP协议、POP3协议、HTTP协议)、DNS域名系统、电子邮件系统

    这篇文章,主要介绍计算机网络之应用层协议(SMTP协议、POP3协议、HTTP协议)、DNS域名系统、电子邮件系统。 目录 一、计算机网络之应用层 1.1、应用层介绍 1.2、网络应用模型

    2024年02月08日
    浏览(55)
  • 计算机网络 应用层上 | 域名解析系统DNS 文件传输协议FTP,NFS 万维网URL HTTP HTML

    之前我们讲运输层的时候已经讲了运输层可以给不同进程之间通信,但我们还需要应用层原因是,许多 应用需要多个进程之间相互配合完成,所以应用层进程用来约束这些配合! 每个应用层协议用来解决一个问题 应用层的许多协议都是基于客户服务器方式 客户是请求方,服

    2024年01月24日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包