spi 子系统

spi在应用层的体现

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

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

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_master 注册过程中设置的。
(下面标出的回调函数在spi_sync中都会调用到,尤其是master->transfer_one_message、master->transfer_one 这两个函数极为重要)
在这里插入图片描述

spi_device

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

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_message

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

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

spi_driver

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

设备树解读

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

#address-cells:描述子节点需要用几个cells 来定义片选 (一般情况等于1)。(用子节点的reg属性来定义使用哪个片选)
#size-cells:必须为0。
compatible:描述与该设备兼容的驱动。
在这里插入图片描述
其它比较重要的可选属性:
cs-gpios:描述被用作片选的gpio 引脚。(可能会有一个或多个gpio引脚)
如下图GPIO1_IO00 表示第一个片选的gpio引脚,GPIO1_IO01 表示第二个片选的gpio引脚,以此类推。
在这里插入图片描述
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:单位是毫秒,表示每次写传输后要延时多久
    在这里插入图片描述
    IMX6ULL SPI 设备树节点实例:
    在这里插入图片描述
    在这里插入图片描述

spi_device 成员解释:
在这里插入图片描述
我们知道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 控制器驱动使用"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 总线驱动模型

在driver/spi/spi.c 的spi_init() 函数中调用bus_register() 注册了spi_bus_type
在这里插入图片描述
在这里插入图片描述
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_add_device 中,最后会调用device_add 向内核注册spi_device->dev (struct device)
在这里插入图片描述
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_bus_type->match——spi_match_device()
在这里插入图片描述

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 从设备框架示例代码:
#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_message_init 与spi_message_add_tail 代码解析
在这里插入图片描述
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_driver 的实例为spidev_spi_driver,它的probe函数是spidev_probe。
在这里插入图片描述
假设有一个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 中描述了它对应着哪个从设备、设备号等等。)
在这里插入图片描述
在这里插入图片描述
在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 中记录着设备节点的设备号)
在这里插入图片描述
这下我们就清楚了,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" 就可以。
在这里插入图片描述
以imx6ull 为例,在设备树中添加如下节点后生成/dev/spidev2.0 节点。
在这里插入图片描述
既然/dev/spidev2.0 是字符设备那么我们就可以用open、read、write… 去访问它,先看看底层的file_operations 中的open、read、write…是怎么写的。
在这里插入图片描述

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。
在这里插入图片描述

spidev read流程

当应用层调用read 读取/dev/spidevB.D 设备节点时,会调用到spidev_read
在spidev_read 函数中从file 私有数据获取到spidev_data,调用spidev_sync_read 读取spi从设备数据,读到的数据会保存到spidev_data->rx_buffer,最后将数据拷贝到应用层buf,返回读取到的数据长度(字节)。
在这里插入图片描述
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 是用来描述数据的,都很关键)
在这里插入图片描述
在这里插入图片描述
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:用来描述需要传输的数据;
在这里插入图片描述

spidev write流程

write 的流程和read 是差不多的,只不过读方向换成了写方向,调用spidev_sync_write 来发送数据
在这里插入图片描述
在这里插入图片描述

spidev ioctl流程

ioctl 的内容分为三部分:读取当前spi 设备通信的模式设置spi 设备通信的模式向spi 从设备传输数据
前面的内容和read、write类似,获取spidev_data、spi_device。
在这里插入图片描述
如果应用层传递的cmd 是下列的值,那么就是读取spi 从设备的传输模式,__put_user 会把spi->mode 的值会放入到arg,然后返回应用层。
在这里插入图片描述
如果cmd 的值是下列的值,那么就是设置spi 的从设备传输模式。
__get_user 会把arg 的值拷贝到tmp中,调用spi_setup 设置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 传输数据)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

spidev close流程

close 对应的是file_operations 中的release,即spidev_release。
在spidev_release 中就是做open的反向操作:清除file 的私有数据、释放申请的tx_buffer、rx_buffer 的内存
在这里插入图片描述

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_transfer,使用spi_message来管理(添加到spi_message->transfers 链表里):
    在这里插入图片描述

  • 同一个SPI Master下的spi_message,放在一个队列里(spi_master->queue):
    在这里插入图片描述
    所以,反过来,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_sync 中会设置spi_message->complete(当传输完成时会调用complete唤醒休眠的线程),然后调用spi_async_locked 开始传输数据(这里只是调度工作队列开始传输流程,并没有真正的传输),从spi_async_locked 返回后,如果数据没有传输完成,该线程就会进入休眠。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
调用__spi_async
在这里插入图片描述
调用spi_master->transfer,该函数是由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_sh_probe 中初始化了ss->ws(struct work_struct) 和ss->workqueue,ss->ws的工作函数为spi_sh_work。
在这里插入图片描述
在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_sh_send 中执行真正的硬件发送工作:
首先计算出当前txFIFO 中剩余的空间cur_len,先将spi_transfer->tx_buffer 中cur_len 长度的数据一个字节一个字节的写入到发送数据寄存器。然后判断是否发送完所有的数据,如果还有数据没被发送,那么就进入休眠等待,等到上一次被放入txfifo的数据发送完成就会长生中断,在中断中唤醒休眠的发送线程,接着发送剩余的数据。
如果当前的spi_transfer 是spi_message 中的最后一个transfer,那么就需要等待最后一组数据发送完成才能返回。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
spi_sh_receive 接收函数
在这里插入图片描述

新方法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_queued_transfer 将spi_message 添加到spi_master->queue 后立即返回__spi_sync
在这里插入图片描述
在这里插入图片描述
__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。)
在这里插入图片描述
在这里插入图片描述
从队列中取出第一个msg 赋值到master->cur_msg,并将这个msg 从队列移除。
在这里插入图片描述
__spi_pump_messages 传输msg 前做准备工作:
ctlr->prepare_transfer_hardware
ctlr->prepare_message
spi_map_msg
最后调用ctlr->transfer_one_message 传输一个msg,即master->cur_msg。
在这里插入图片描述

在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_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_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 传输。
在这里插入图片描述
bitbang->setup_transfer
在这里插入图片描述
以spi-imx.c 为例bitbang->txrx_bufs 是spi_imx_transfer
在这里插入图片描述
在这里插入图片描述
spi_transfer数据已经在txrx_bufs 函数中传输完成,查看spi_finalize_current_transfer 结束一个spi_transfer 传输需要哪些收尾工作:
这不就是提示spi_transfer 完成的函数(执行过complete后spi_transfer_one_message 中的等待函数便不会等待,返回后直接走过。)
在这里插入图片描述
spi_transfer_one_message
在这里插入图片描述
就这样不停的遍历spi_transfer,不停的调用ctlr->transfer_one 传输spi_transfer,所有的transfer 都遍历完了,一个spi_message 的数据就处理完了。
看看spi_finalize_current_message 有哪些收尾工作:
在这里插入图片描述
spi_register_master 注册spi_master 时master->pump_messages 的work 函数被设置为spi_pump_message
在这里插入图片描述
在这里插入图片描述
linux 5.4 内核spi_sync 传输流程解析完毕。
遗留问题:队列中的其它msg 交由worker线程执行__spi_pump_messages 是如何处理的?
涉及到completion 等待、__spi_pump_messages 处理流程等问题。

新方法4.1.15

在这里插入图片描述
在这里插入图片描述

在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控制器驱动解析

(以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_imx_chipselect 片选设置函数
在这里插入图片描述
spi_imx_transfer 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_bitbang_init 中设置了master的几个重要的回调函数:
master->prepare_transfer_hardware
master->unprepare_transfer_hardware
master->transfer_one
master->set_cs

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

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

对于这种检索子节点,然后生成从设备的形式,其它总线也有类似的,比如i2c、mdio 等等,我们可以把这种套路记忆下来,对类似代码比较容易理解。
在这里插入图片描述
在这里插入图片描述
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_alloc_device
在这里插入图片描述
of_modalias_node
在这里插入图片描述
of_spi_parse_dt
在这里插入图片描述
关于片选,我们在控制的时候就可以通过spi_master->cs_gpios[spi_device->chip_select] 来获取从设备对应的片选gpio编号。
在这里插入图片描述
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 设备节点,这样我们就可以使用应用程序来控制发送和接收。
效果如下:
在这里插入图片描述
在这里插入图片描述

#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 //传输前的硬件设置函数(根据硬件需要选择是否提供)

#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");
  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Gatys et al. (2016) proposed an algorithm for style transfer, which can generate an image that combines the content of one image and the style of another image. The algorithm is based on the neural style transfer technique, which uses a pre-trained convolutional neural network (CNN) to extract the content and style features from the input images. In this algorithm, the content and style features are extracted from the content and style images respectively using the VGG-19 network. The content features are extracted from the output of one of the convolutional layers in the network, while the style features are extracted from the correlations between the feature maps of different layers. The Gram matrix is used to measure these correlations. The optimization process involves minimizing a loss function that consists of three components: the content loss, the style loss, and the total variation loss. The content loss measures the difference between the content features of the generated image and the content image. The style loss measures the difference between the style features of the generated image and the style image. The total variation loss is used to smooth the image and reduce noise. The optimization is performed using gradient descent, where the gradient of the loss function with respect to the generated image is computed and used to update the image. The process is repeated until the loss function converges. The code for this algorithm is available online, and it is implemented using the TensorFlow library. It involves loading the pre-trained VGG-19 network, extracting the content and style features, computing the loss function, and optimizing the generated image using gradient descent. The code also includes various parameters that can be adjusted, such as the weight of the content and style loss, the number of iterations, and the learning rate.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值