【Linux SPI Framework】
Linux SPI Framework
- 1.1 SPI 总线简介
- 1.2 LINUX驱动分层
- 1.3 LINUX设备模型
- 1.4 LINUX SPI 核心层:
- 1.4.1 SPI Host Master
- 1.4.2 SPI Slave Device
- 1.5 Linux SPI 控制器设备和驱动
- 1.5.1 spi master 注册
- 1.5.2 spi_register_controller
- 1.5.3 spi_controller_initialize_queue
- 1.6 LINUX SPI 外设驱动
- 1.6.1 SPI Driver Register
- 1.6.1.1 SPI 总线注册
- 1.6.1.2 spi_match_device 介绍
- 1.6.1.3 spi driver match
- 1.7 SPI 数据传输
- 1.7.1 spi 数据传输相关概念
- 1.7.1.1 SPI 异步方式难点
- 1.7.1.2 SPI 队列化传输方式
- 1.7.2 spi_transfer 的队列化
- 1.7.3 spi_message 发送请求
- 1.7.4 spi_transfer 数据发送
- 1.7.5 spi 信息统计
- 1.8 linux spi 通用驱动spidev.c
1.1 SPI 总线简介
SPI (同步外设接口) 是由摩托罗拉公司开发的全双工同步串行总线,其接口由四种信号构成:
- MISO(串行数据输入),
- MOSI(串行数据输出),
- SCK(串行移位时钟),
- SS/CS(从使能信号)
现在芯片技术日新月异, SPI 模块的结构也在变化中, 像OMAP 系列中的 SPI 模块还支持 5 线的一种模式),SS /CS决定了唯一的与主设备通信的从设备,主设备通过产生移位时钟来发起通讯。通讯时,数据由 MOSI 输出,MISO 输入,数据在时钟的上升或下降沿由 MOSI 输出,在紧接着的下降或上升沿由 MISO 读入,这样经过 8/16 次时钟的改变,完成 8/16 位数据的传输。
SPI 模块为了和外设进行数据交换, 根据外设工作要求, 其输出串行同步时钟极性(CPOL) 和相位 (CPHA) 可以进行配置:
- 如果
CPOL=0
, 串行同步时钟的空闲状态为低电平, - 如果
CPOL=1,
串行同步时钟的空闲状态为高电平, - 如果
CPHA=0
, 在串行同步时钟的第一个跳变沿(上升或下降)数据被采样, - 如果
CPHA=1
, 在串行同步时钟的第二个跳变沿(上升或下降)数据被采样,
1.2 LINUX驱动分层
在面向对象的程序设计中, 可以为某一类相似的事物定义一个基类, 而具体的事物可以继承这个基类中的函数。
Linux 内核中频繁使用到面向对象的设计思想。在设备驱动方面,往往为同类的设备设计了一个框架, 而框架中的核心层则实现了该设备通用的一些功能。而且具体的设备不想使用核心层的函数, 它可以重载之。这就是我们所说的在驱动设计中的分层思想。
此外, 在驱动的设计中, 还会使用分离的思想。如果一个设备的驱动和 host(Controller)
的驱动休戚相关, 那么, 这就意味着这个普通的设备如果用在不同的 host上, 会采用 n 个版本的驱动。
外设驱动与 host controller
的驱动不相关, host controller
的驱动不关心外设, 而外设驱动也不关心主机, 外设只是访问核心层的通用 API 进行数据传输, 主机和外设之间可以进行任意的组合。相当于在控制器驱动和设备驱动之间增加一层核心层, 对内对外都隐藏了对端的不确定性。仔细通读 USB/SPI/PCI
的代码就会发现这种思想的体现。
1.3 LINUX设备模型
设备驱动模型中, 主要包含总线、设备 和 驱动三个实体, 总线将设备和驱动绑定, 在系统每注册一个设备的时候, 会寻找与之匹配的驱动,反之,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。
根据这个模型的需求, 一个现实的 linux 设备和驱动通常都需要挂接在一种总线上, 否则无法管他们的匹配。
注册驱动和注册设备都是由不同的 API 来完成的。对于本身依附于 PCI/USB/I2C/SPI
的设备而言, 这自然不是问题, 他们都有
对应的总线,但是在嵌入式系统里面,Controller 系统中集成的外设控制器, 挂载在内存空间的外设确不依附于此类总线。就像 SPI 控制器, PCI 控制器等等都是这种情况。那如何处理呢?基于这一背景, Linux 发明了一种虚拟的总线称为 platform 总线,相应的设备称为 platform_device
, 而驱动称为 platform_driver
。
使用 platform 总线在驱动中大体有以下几个好处:
- 使得设备被挂接在一个总线上,使配套的 sysfs 节点、设备电源管理都成为可能。
- 隔离了 BSP 和驱动。BSP 中定义 platform 设备和设备使用的资源(可以使用
platform_data
的形式来包括
platform 设备的设备),设备的具体配置信息。而在驱动中,只需要通过通用 API 去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
1.4 LINUX SPI 核心层:
核心层代码负责这个框架中通用的部分,满足分层的思想,位于drivers/spi/spi.c。 主要承担的工作包括:注册 spi总线,提供基本 SPI 总线操作 API:
int spi_register_driver(struct spi_driver *sdrv);
struct spi_device *spi_alloc_device(struct spi_master *master);
int spi_add_device(struct spi_device *spi);
struct spi_device *spi_new_device(struct spi_master *master, struct spi_board_info *chip);
struct spi_master *spi_alloc_master(struct device *dev, unsigned size);
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);
int spi_sync(struct spi_device *spi, struct spi_message *message);
int spi_write_then_read(struct spi_device *spi, const u8 *txbuf, unsigned n_tx, u8 *rxbuf, unsigned n_rx);
spi_register_board_info(struct spi_board_info const *info, unsigned n);
可以参考 driver/spi/spi.c
1.4.1 SPI Host Master
在 Linux 中, 每一个类型的驱动都会有一个相应的结构体来描述, spi_master 就是用来描述 SPI host controller 驱动的,
其主要成员主要有以下几个:
- bus_num;
- cs;
- spi 模式;
- 时钟设置用到的函数;
- 数据传输用到的函数等。
具体定义如下:
struct spi_master {struct device dev;/* 表示是SPI主机控制器的编号。由平台代码决定 */s16 bus_num; /* 控制器支持的片选数量,即能支持多少个spi设备 */ u16 num_chipselect; /* 针对设备设置SPI的工作时钟及数据传输模式等。在spi_add_device函数中调用 */int (*setup)(struct spi_device *spi); /* 实现数据的双向传输,可能会睡眠 */int (*transfer)(struct spi_device *spi, struct spi_message *mesg); /* 注销时调用 */void (*cleanup)(struct spi_device *spi); ...
};
SPI Host Conctroller 驱动中分配/注册/注销等操作,会调用 spi core layer API:
spi_alloc_master(); spi_register_master(); spi_unregister_master();
实际上在 master 的驱动中实现的就是 spi_master
中的初始化以及注册,其中最重要的就是transfer(早期)和setup这两个操作的实现。spi 每次数据传输最终调用到的函数就是这里的transfer(早期)。该函数就对应着实际硬件上寄存器的控制。setup同理。
这里可能会有疑问, spi master controller
注册对应的驱动框架里的哪一层? 按说, SPI Host Controller
已经作为 platform 设备都注册过了。这里就需要用驱动设计里面的分层思想来解释。使用到 platform 总线 API 注册到 platform总线上的控制器设备和驱动, 都是 common 的部分。specific 的部分, 会在 platform driver
注册后, 在 probe 函数里面基于注册的 common 部分的资源信息来具体实现。称之为 spi_master
的注册部分。
1.4.2 SPI Slave Device
和spi master
依附于platform bus
不同,spi device 依附于 spi bus,每个 spi deivice
对应的 driver 都会有一个spi_driver
结构体来进行描述,结构体中定义对应的操作函数指针:
struct spi_driver {int (*probe)(struct spi_device *spi);int (*remove)(struct spi_device *spi);void (*shutdown)(struct spi_device *spi);int (*suspend)(struct spi_device *spi, pm_message_t mesg);int (*resume)(struct spi_device *spi);struct device_driver driver;
};
同时依附于SPI Bus
的设备都会有一个 spi_device
结构体来描述,结构中定义了设备的片选、模式、速率以及对应 Host
驱动的spi_master
等。
struct spi_device {struct device dev;struct spi_master *master;u32 max_speed_hz;u8 chip_select;u8 mode;
#define SPI_CPHA 0x01 /* clock phase */
#define SPI_CPOL 0x02 /* clock polarity */
#define SPI_MODE_0 (0|0) /* (original MicroWire) */
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
#define SPI_CS_HIGH 0x04 /* chipselect active high? */
#define SPI_LSB_FIRST 0x08 /* per-word bits-on-wire */
#define SPI_3WIRE 0x10 /* SI/SO signals shared */
#define SPI_LOOP 0x20 /* loopback mode */u8 bits_per_word;int irq;void *controller_state;void *controller_data;char modalias[32];
};
依附于SPI 总线的设备驱动对应的总线类型为 spi_bus_type
,在内核的 drivers/spi/spi.c
中定义。
struct bus_type spi_bus_type = { .name = "spi",.dev_attrs = spi_dev_attrs,.match = spi_match_device,.uevent = spi_uevent,.suspend = spi_suspend,.resume = spi_resume,
};
EXPORT_SYMBOL_GPL(spi_bus_type);
而 具体的 SPI 外设信息 linux spi 早期使用 struct spi_board_info
来描述,该结构体包含外设的片选号、总线号、模式以及传输速率等信息,在系统初始化时将会调用spi_register_board_info()
来注册硬件信息;现在都通过dts
进行描述。
SPI 设备驱动将会调用 spi_register_driver()
; 来注册一个spi_driver
驱动,注册时最终会调用 spi_bus_type
中的 match 来使spi_driver
和 spi_device
相匹配。设备驱动 spi_driver
是在驱动里定义的,但是 spi_device 呢?事实上上文提到的spi_master
的注册会在spi_register_board_info
之后,spi_master
注册的过程中会调用 scan_boardinfo
扫描 board_list
,找到挂接在它上面的spi设备,然后创建并注册spi_device。
1.5 Linux SPI 控制器设备和驱动
在嵌入式系统中,一般处理器中都集成了一个或者多个SPI控制器。这些控制器一般都为Master。
由设备模型可知,SPI 控制器的设备和驱动是要挂在总线上的。所以 SPI 控制器驱动用到了platform 虚拟总线。
SPI 控制器驱动一般放在drivers/spi
下。
由于spi controller
也是作为设备注册到内核中,所以就需要有对应的device node
对其进行描述,用户可以在其平台
对应的 dts 文件中添加 spi controller
设备的资源(若片上有n个spi控制器,可以在对应的dts中添加多个描述)。
以开源代码MTK平台为例:
arch/arm64/boot/dts/mediatek/mt8183.dtsi
spi0: spi@1100a000 {compatible = "mediatek,mt8183-spi";#address-cells = <1>;#size-cells = <0>;reg = <0 0x1100a000 0 0x1000>;interrupts = <GIC_SPI 120 IRQ_TYPE_LEVEL_LOW>;clocks = <&topckgen CLK_TOP_SYSPLL_D5_D2>,<&topckgen CLK_TOP_MUX_SPI>,<&infracfg CLK_INFRA_SPI0>;clock-names = "parent-clk", "sel-clk", "spi-clk";status = "disabled";};
spi0 contorller
对应 spi controller
的驱动 drivers/spi/spi-mt65xx.c:
static struct platform_driver mtk_spi_driver = {.driver = {.name = "mtk-spi",.pm = &mtk_spi_pm,.of_match_table = mtk_spi_of_match,},.probe = mtk_spi_probe,.remove = mtk_spi_remove,
};/** module_platform_driver是由module_driver封装而来,并填充* 了platform_driver_register、platform_driver_unregister , 见下面分析* /
module_platform_driver(mtk_spi_driver);
最后使用 platform_device_register
来进行注册,注册成功后可在 /sys/bus/platform/devices 看到相关信息
Linux 内核中会大量采用 module_platform_driver
来完成向 Linux 内核注册 platform 驱动的操作。module_platform_driver
定义在 include/linux/platform_device.h 文件中:
#define module_platform_driver(__platform_driver) \module_driver(__platform_driver, platform_driver_register, \platform_driver_unregister)
最终展开后就是如下形式:
static int __init xxx_init(void)
{return platform_driver_register(&xxx);
}
module_init(xxx_init);
static void __exit xxx_init(void)
{return platform_driver_unregister(&xxx);
}
module_exit(xxx_exit);
platform_driver
和 platform_device
(上文dts中的内容) 匹配后,会调用驱动中的 mtk_spi_probe
, 然后根据传入的 platform_device 参数,构建一个用于描述 SPI 控制器的结构体 spi_master,并注册。spi_register_master(master)
。
后续注册的spi_device 需要选定自己的spi_master
,并利用spi_master提供的传输功能传输 spi 数据。
1.5.1 spi master 注册
上文介紹了 spi master dvice 的描述及 spi mater driver 的簡單注冊,接下來以 MTK SPI Master 驱动为例,進行簡單介紹,
在這部分内容中,我们主要关注 devm_spi_register_master
。
static int mtk_spi_probe(struct platform_device *pdev)
{struct spi_master *master;struct mtk_spi *mdata;const struct of_device_id *of_id;struct resource *res;int i, irq, ret, addr_bits;master = spi_alloc_master(&pdev->dev, sizeof(*mdata));if (!master) {dev_err(&pdev->dev, "failed to alloc spi master\n");return -ENOMEM;}master->auto_runtime_pm = true;master->dev.of_node = pdev->dev.of_node;master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LSB_FIRST;/* SPI客户端驱动程序请求SPI主控制器来配置设备特定的CS设置,保持和非活动的时间要求 */master->set_cs = mtk_spi_set_cs;master->prepare_message = mtk_spi_prepare_message; // 来传输单个传输,同时对同时到达的传输进行排队master->transfer_one = mtk_spi_transfer_one; master->can_dma = mtk_spi_can_dma;master->setup = mtk_spi_setup; // 设置了设备时钟速率、SPI模式和字大小of_id = of_match_node(mtk_spi_of_match, pdev->dev.of_node);mdata = spi_master_get_devdata(master);mdata->dev_comp = of_id->data;if (mdata->dev_comp->must_tx)master->flags = SPI_MASTER_MUST_TX;platform_set_drvdata(pdev, master);// 資源解析res = platform_get_resource(pdev, IORESOURCE_MEM, 0);mdata->base = devm_ioremap_resource(&pdev->dev, res);//中断相关配置irq = platform_get_irq(pdev, 0);ret = devm_request_irq(&pdev->dev, irq, mtk_spi_interrupt,IRQF_TRIGGER_NONE, dev_name(&pdev->dev), master);// clk 相关配置mdata->sel_clk = devm_clk_get(&pdev->dev, "sel-clk");mdata->spi_clk = devm_clk_get(&pdev->dev, "spi-clk");ret = clk_prepare_enable(mdata->spi_clk);ret = clk_set_parent(mdata->sel_clk, mdata->parent_clk);clk_disable_unprepare(mdata->spi_clk);pm_runtime_enable(&pdev->dev);// spi master 注册ret = devm_spi_register_master(&pdev->dev, master);
在 probe 中调用 spi_alloc_master
分配 spi_master
设备,对 spi_master
进行赋值等,最后进行通过devm_spi_master
进行注册。
devm_spi_register_master 实现:
int devm_spi_register_controller(struct device *dev, struct spi_controller *ctlr)
{...ret = spi_register_controller(ctlr); //主要关注...
}
EXPORT_SYMBOL_GPL(devm_spi_register_controller);
1.5.2 spi_register_controller
int spi_register_controller(struct spi_controller *ctlr)
{.../** 检查传入的 spi_controller 结构中,挂接的 ops 是不是全的,也就是说 core 层需要的一些必* 要的钩子函数是不是已经指定好了,这里需要确定 SPI 控制器的那个传输的函数 transfer 、* transfer_one、transfer_one_message 至少挂接一个才行,也就是说,芯片厂家必须实现* 至少一种控制实际 SoC 的传输 SPI 的方式,Core 层才能够正常运行* /status = spi_controller_check_ops(ctlr); ...//一些链表,spin_lock 的初始化INIT_LIST_HEAD(&ctlr->queue);...//底层设备模型添加dev_set_name(&ctlr->dev, "spi%u", ctlr->bus_num);.../** 如果芯片厂家没有实现 transfer 钩子函数的话,那么至少使用了挂接了 transfer_one * 或者 transfer_one_message,这意味着将会使用 spi core 层的新的传输机制,* 这里初始化 queue 机制,后面马上进行详细描述;* /if (ctlr->transfer) {dev_info(dev, "controller is unqueued, this is deprecated\n");} else if (ctlr->transfer_one || ctlr->transfer_one_message) {status = spi_controller_initialize_queue(ctlr); ...// 将spi_controller 挂接到全局的 spi_controller_list 链表ist_add_tail(&ctlr->list, &spi_controller_list);...// 匹配 borad_info,也就是 spi slave 的 结构 spi_deviceist_add_tail(&ctlr->list, &spi_controller_list);spi_match_controller_to_boardinfo(ctlr, &bi->board_info);...
}
整个过程中,需要着重理解的是 transfer 的部分,在很久以前的结构中,spi_controller 叫做 spi_master
的时代,其实是只有 transfer 对接函数的,也就是芯片厂家底层实现 transfer(获取数据,指挥实际 SoC 的硬件进行数据传输),然后 SPI Core 软件层来用这个。现在提供了 transfer 、transfer_one、transfer_one_message
三种:
- transfer 是添加一个 message 到传输 queue;
- transfer_one 是最底层指挥硬件去传输一个单一的 spi_transfer 内容
- transfer_one_message 是最底层指挥硬件去传输一个单一的 spi_message 内容
note: transfer_one 和 transfer_one_message 两个接口互斥, 当两者都设置了时,通用子系统不会调用 transfer_one回调
此外,master->transfer
不能睡眠。它的责任是安排传输的发生和它的complete()
回调的发出。这两种情况通常会在其他传输完成后发生,如果控制器是空闲的,则需要启动它。该方法不用于队列控制器,如果实现了transfer_one_message()
和(un)prepare_transfer_hardware()
则必须为NULL。目前已经弃用。
既然现在都是用 transfer_one
(或者transfer_one_message
)那着重看看这个初始化 spi_controller_initialize_queue
1.5.3 spi_controller_initialize_queue
static int spi_controller_initialize_queue(struct spi_controller *ctlr)
{.../** 先将 transfer 赋值成为 spi_queued_transfer 调用(因为只有当transfer 没有挂* 指针的时候,才会走进这个函数* /ctlr->transfer = spi_queued_transfer; /** 如果没有指定 transfer_one_message,那么为它挂上core 层的 * spi_transfer_one_message,需要记住:* spi_controller->transfer 和 spi_controller->spi_transfer_one_message * 已经赋了 core 层的函数钩子;* /if (!ctlr->transfer_one_message) ctlr->transfer_one_message = spi_transfer_one_message;.../** 初始化了 spi_controller 结构的 kthread_worker 和 kthread_work * 结构(内核 kthread_worker 和 kthread_work 机制)这个机制相当于是* 内核线程,并指定了 work 的执行函数为 spi_pump_messages ,最后* 如果对接底层芯片的 spi_controller 指定了 spi_controller->rt 的话,意* 思是开启 realtime 发送,那么将执行线程变为 SCHED_FIFO 的实时调* 度类型,也就是更高的优先级(实时优先级)* /ret = spi_init_queue(ctlr);...ret = spi_start_queue(ctlr);...//注冊 spi slave devicelist_for_each_entry(bi, &board_list, list)spi_match_controller_to_boardinfo(ctlr, &bi->board_info);...
}
1.6 LINUX SPI 外设驱动
不同的 spi device 在 SPI 总线通信上的需求不尽相同,比如模式、时钟速率、片选脚等等。这些跟具体硬件相关的信息通常用 dts 来描述:
&spi1 {status = "okay";#address-cells = <1>;#size-cells = <0>;spi_test@0 {compatible = "demo,spi_test";reg = <0>;spi-max-frequency = <5200000>;status = "okay";spi2axi@0 {
驱动中对应的配置:
static const struct of_device_id spitest_dt_ids[] = {{ .compatible = "demo,spi_test", },{ }
};static struct spi_driver spi_test_driver = {.driver = {.name = "spitest",.of_match_table = of_match_ptr(spiteste_dt_ids),},.probe = spi_test_probe,.remove = spi_test_remove,
};static int __init spi_test_init(void)
{int ret = 0;ret = spi_register_driver(&spi_test_driver);if (ret) {pr_err("spi driver register failed!\n");return ret;}/* 注册设备文件,留给上层使用 */ret = misc_register(&spi_test_misc);return ret;
}
module_init(spi_test_init);
1.6.1 SPI Driver Register
不管是设备还是驱动,都是挂接在某条总线上的,也就是说我们根据总线类型的不同来区分各种设备和驱动。spi device 的注册会调用到 core layer 的 spi 注册函数 spi_register_driver
来将spi device注册到 spi bus总线上,所以我们先看下 core layer 的初始化,同时包含了spi bus的注册。spi core layer 的初始化函数是 spi_init(void)
,该函数的主要实现如下:
kernel-4.14/drivers/spi/spi.c
struct bus_type spi_bus_type = { //内核通过struct bus_type结构,抽象Bus.name = "spi", //总线名称.dev_groups = spi_dev_groups,/** 由具体的bus driver实现的回调函数。当任何属于该Bus的device或者* device_driver添加到内核时,内核都会调用该接口,如果新加的* device或device_driver匹配上了自己的另一半的话,* 该接口要返回非零值,此时Bus模块的核心逻辑就会执行后续的处理。*/.match = spi_match_device,/* * spi_uevent主要是在spi设备与spi驱动完成绑定时,发送给应用层的uevent条目,* 主要是增加 “MODALIAS=spi:spi->modalias”的uevent条目。*/.uevent = spi_uevent,
};
EXPORT_SYMBOL_GPL(spi_bus_type);static int __init spi_init(void)
{int status;/* 申请的buf空间用于在spi数据传输中 */buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL); /* 总线注册和类注册, platform_bus_init同样也调用这个函数 */status = bus_register(&spi_bus_type) /* 注册主设备的class */status = class_register(&spi_master_class);...
}
postcore_initcall(spi_init);
可以看到 spi core layer 的初始化 使用的宏 postcore_initcall(spi_init)
; 此宏在系统初始化时调用,早于于module_init()
执行的,
也即 spi_driver_register
所在的 moule_init
函数的执行。spi_init 函数主要完成了spi总线的注册和和申请由于spi 数据传输的buf, 下面看下SPI 总线的注册。
1.6.1.1 SPI 总线注册
kernel-4.14/drivers/base/bus.c
int bus_register(struct bus_type *bus)
{int retval;struct subsys_private *priv; //申请一个subsys_private结构体内存struct lock_class_key *key = &bus->lock_key;priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);if (!priv)return -ENOMEM;priv->bus = bus;bus->p = priv;BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);/* 总线的名字”spi”, 一个kobject对应一个目录,这里为这个目录赋值名字 */retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);if (retval)goto out;priv->subsys.kobj.kset = bus_kset;priv->subsys.kobj.ktype = &bus_ktype;priv->drivers_autoprobe = 1;/* 创建devices命名的目录 */retval = kset_register(&priv->subsys);if (retval)goto out;/* 创建属性文件 */retval = bus_create_file(bus, &bus_attr_uevent);if (retval)goto bus_uevent_fail;priv->devices_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj);if (!priv->devices_kset) {retval = -ENOMEM;goto bus_devices_fail;}priv->drivers_kset = kset_create_and_add("drivers", NULL, &priv->subsys.kobj);if (!priv->drivers_kset) {retval = -ENOMEM;goto bus_drivers_fail;}
从总线注册函数bus_register(struct bus_type *bus)
中可以发现,首先申请了一个subsys_private 结构体内存。该结构体中包含了三个kset 结构,分别是:
- struct kset subsys、
- struct kset *devices_kset
- struct kset *drivers_kset
struct subsys_private {struct kset subsys; //代表bus在sysfs中的类型struct kset *devices_kset; //代表bus目录下地devices子目录struct list_head interfaces;struct mutex mutex;struct kset *drivers_kset; //代表bus目录下的drivers子目录struct klist klist_devices; //存了本bus下所有的devicestruct klist klist_drivers; //存了本bus下所有的driver//用于在总线上内容发送变化时调用特定的函数struct blocking_notifier_head bus_notifier; //用于控制该bus下的drivers或者device是否自动probeunsigned int drivers_autoprobe:1; struct bus_type *bus; //保存上层的bus指针。struct kset glue_dirs;struct class *class; //保存上层的class
};
当发现一个设备或者驱动的时候,对于每一次设备或者驱动注册(设备是被插入了,驱动就是.ko模块被加载),都得分配一个device或者device_drive
结构,每一次都需要将device结构挂入drivers或devices(kset结构)链表中,这样才能通过总线找到挂接在这个总线上的所有设备和驱动。
这里仅仅将设备和驱动挂接在总线上,并不能表明设备和驱动之间的关系,这样的处理仅仅表明了驱动、设备与总线的关系,它们申明了我现在挂接在这条总线上,以后操作我就通过这条总线。
总线注册完成后,在后续的spi device 注册的时候就会先调用 spi bus总线上的 spi_match_device
函数,下面先介绍下该
函数。
1.6.1.2 spi_match_device 介绍
kernel-4.14/drivers/spi/spi.c
static int spi_match_device(struct device *dev, struct device_driver *drv)
{const struct spi_device *spi = to_spi_device(dev);const struct spi_driver *sdrv = to_spi_driver(drv);/* Attempt an OF style match */if (of_driver_match_device(dev, drv))return 1;/* Then try ACPI */if (acpi_driver_match_device(dev, drv))return 1;if (sdrv->id_table)return !!spi_match_id(sdrv->id_table, spi);return strcmp(spi->modalias, drv->name) == 0;
}
- 首先查看驱动和设备是否匹配,如果不匹配,退出;
- 判断驱动中是否支持id数组,如果支持,查找匹配此
id
的spi_device
; - 比较设备的名字的和驱动的名字是否相同;
在上面的匹配成功后就会调用到驱动中的xxx_probe
函数(如上面的spi_test_probe
)。接下来就可以利用spi子系统为我们完成数据交互了。
这里还有一个疑问?系统是如何及何时 触发 spi_match_device调用的呢?
1.6.1.3 spi driver match
驱动是如何插入到/sys/bus/drivers/spi目录下的?先看spi_register_driver的实现:
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv)
{sdrv->driver.owner = owner;sdrv->driver.bus = &spi_bus_type;if (sdrv->probe)sdrv->driver.probe = spi_drv_probe;if (sdrv->remove)sdrv->driver.remove = spi_drv_remove;if (sdrv->shutdown)sdrv->driver.shutdown = spi_drv_shutdown;// 完成挂接驱动至总线及生成设备树的过程return driver_register(&sdrv->driver);
}
EXPORT_SYMBOL_GPL(__spi_register_driver);
在 driver_register->driver_register->bus_add_driver 函数中有个重要的语句 drv->kobj.kset = &bus->drivers,
这里就是将 driver 的 kobj 所属的 kset 挂接上总线的 kset,接下一层一层来看:
driver_register 层:
int driver_register(struct device_driver *drv)
{...other = driver_find(drv->name, drv->bus); //在spi总线上查找该驱动是否已经存在,如果存在,忙退出if (other) {printk(KERN_ERR "Error: Driver '%s' is already registered, "aborting...\n", drv->name);return -EBUSY;}ret = bus_add_driver(drv); //如果该驱动在SPI总线上不存在,调用bus_add_driver(drv)增加该驱动if (ret)return ret;ret = driver_add_groups(drv, drv->groups); //增加驱动组...
}
EXPORT_SYMBOL_GPL(driver_register);
bus_add_driver 层:
int bus_add_driver(struct device_driver *drv)
{struct bus_type *bus;struct driver_private *priv;int error = 0;bus = bus_get(drv->bus);if (!bus)return -EINVAL;pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);priv = kzalloc(sizeof(*priv), GFP_KERNEL);...klist_init(&priv->klist_devices, NULL, NULL);priv->driver = drv;drv->p = priv;priv->kobj.kset = bus->p->drivers_kset; error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,"%s", drv->name);...klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);...error = driver_attach(drv); //如果驱动总线支持自动探测, 走到這裏...
driver_attach layer
int driver_attach(struct device_driver *drv)
{return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach); //调用__driver_attach函数
}
EXPORT_SYMBOL_GPL(driver_attach);static int __driver_attach(struct device *dev, void *data)
{...if (!dev->driver)driver_probe_device(drv, dev); //此函数就是我们要找的函数...
}int driver_probe_device(struct device_driver *drv, struct device *dev)
{...ret = really_probe(dev, drv);...
}static int really_probe(struct device *dev, struct device_driver *drv)
{...if (dev->bus->probe) { //因为在 BUS 中并没有给 probe 赋值,所以会直接调用驱动中的的 probeTIME_LOG_START();ret = dev->bus->probe(dev);TIME_LOG_END();bootprof_probe(ts, dev, drv, (unsigned long)dev->bus->probe);if (ret)goto probe_failed;} else if (drv->probe) {TIME_LOG_START();ret = drv->probe(dev); //调用驱动中对应的xxx_probe函数TIME_LOG_END();bootprof_probe(ts, dev, drv, (unsigned long)drv->probe);if (ret)goto probe_failed;}
SPI是专门独立出来了一个子系统,它的平台初台化是专门在spi.c中用 __init spi_init(void) 函数,和在一般的 platform驱动还是稍微有一些区别的。而平台总线因为是通用的,所以在platform.c 中调用 int __init platform_bus_init(void) 函数来实现
1.7 SPI 数据传输
1.7.1 spi 数据传输相关概念
SPI数据传输可以有两种方式:同步方式和异步方式。
同步方式:是指数据传输的发起者必须等待本次传输的结束,期间不能做其它事情,用代码来解释就是,调用传输的函数后,直到数据传输完成,函数才会返回。
异步方式:数据传输的发起者无需等待传输的结束,数据传输期间还可以做其它事情,用代码来解释就是,调用传输的函数后,函数会立刻返回而不用等待数据传输完成,我们只需设置一个回调函数,传输完成后,该回调函数会被调用以通知发起者数据传送已经完成。
同步方式简单易用,很适合处理那些少量数据的单次传输。但是对于数据量大、次数多的传输来说,异步方式就显得更加合适。
1.7.1.1 SPI 异步方式难点
对于SPI控制器来说,要支持异步方式必须要考虑以下两种状况:
1)对于同一个数据传输的发起者,既然异步方式无需等待数据传输完成即可返回,返回后,该发起者可以立刻又发起一个message,而这时上一个message还没有处理完。
2)对于另外一个不同的发起者来说,也有可能同时发起一次message传输请求。
1.7.1.2 SPI 队列化传输方式
队列化正是为了为了解决SPI异步传输的问题。
所谓队列化,是指把等待传输的 message 放入一个等待队列中,发起一个传输操作,其实就是把对应的 message按先后顺序放入一个等待队列中,系统会在不断检测队列中是否有等待传输的 message,如果有就不停地调度数据传输内核线程,逐个取出队列中的message进行处理,直到队列变空为止。SPI通用接口层为我们实现了队列化的基本框架。
1.7.2 spi_transfer 的队列化
先理解下面两个结构的含义:( 这两个结构的定义及详细注释参见 include/linux/spi/spi.h)
spi_message:描述一次完整的传输,即 CS 信号从高->底->高的传输
spi_transfer: 多个 spi_transfer
够成一个spi_message
对协议驱动来说,一个spi_message
是一次数据交换的原子请求,而spi_message
由多个spi_transfer
结构组成,这些spi_transfer
通过一个链表组织在一起,看这两个数据结构关于spi_transfer链表的相关字段:
struct spi_transfer { ...... const void *tx_buf; void *rx_buf; ...... struct list_head transfer_list;
}; struct spi_message { struct list_head transfers; struct spi_device *spi; ...... struct list_head queue; ......
};
可见,一个spi_message
结构有一个链表头字段:transfers,而每个spi_transfer
结构都包含一个链表头字段:transfer_list,通过这两个链表头字段,所有属于这次message传输的transfer都会挂在spi_message.transfers
字段下面。可以通过以下API向spi_message结构中添加一个spi_transfer结构:
static inline void
spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{ list_add_tail(&t->transfer_list, &m->transfers);
}
core layer 会以一个message为单位,在工作线程中调用控制器驱动的 transfer_one_message
回调函数来完成spi_transfer
链表的处理和传输工作。
1.7.3 spi_message 发送请求
以下的API可以被协议驱动程序用于发起一个message传输操作:
extern int spi_async(struct spi_device *spi, struct spi_message *message);
spi_async
函数是发起一个异步传输的API,它会把spi_message
结构挂在spi_master
的queue字段下,然后启动专门为spi传输准备的内核工作线程,由该工作线程来实际处理message的传输工作,因为是异步操作,所以该函数会立刻返回,不会等待传输的完成,这时,协议驱动程序(可能是另一个协议驱动程序)可以再次调用该API,发起另一个message传输请求,结果就是,当工作线程被唤醒时,spi_master
下面可能已经挂了多个待处理的spi_message
结构,工作线程会按先进先出的原则来逐个处理这些message请求,每个message传送完成后,对应spi_message结构的complete回调函数就会被调用,以通知协议驱动程序准备下一帧数据。这就是spi_message
的队列化。
1.7.4 spi_transfer 数据发送
以 kernel-4.14/drivers/gpu/drm/panel/panel-sitronix-st7789v.c 函数发送一个 cmd(8bit) 为例。
static int st7789v_spi_write(struct st7789v *ctx, enum st7789v_prefix prefix, u8 data)
{struct spi_transfer xfer = { }; // 定义了一个spi_transferstruct spi_message msg; // 定义了一个spi_messageu16 txbuf = ((prefix & 1) << 8) | data;spi_message_init(&msg);// 初始化其 transfers 链表xfer.tx_buf = &txbuf; // 赋值transfer的写指针xfer.bits_per_word = 9;xfer.len = sizeof(txbuf); // 赋值transfer的写长度spi_message_add_tail(&xfer, &msg); //添加到 spi_messagereturn spi_sync(ctx->spi, &msg); //进行同步方式发送,// spi_sync为同步方式发送,
}
上面 ctx->spi
为 struct spi_device 结构体指针, struct spi_device
结构体包含了所依赖的spi_controller
,在调用spi_sync
的时候会通过spi_device找到对应的spi_controller。
用户也可以选择一些封装好的函数,见 include/linux/spi/spi.h,如:
int spi_write_then_read(struct spi_device *spi, const u8 *txbuf, unsigned n_tx, u8 *rxbuf, unsigned n_rx);
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx)
1.7.5 spi 信息统计
由于spi controller 是通过平台设备注册的额,所以可以在devices/platform看到其相关信息,比如查看数据发送/数据接收时,可以直接cat bytes_tx/bytes_rx/transfer
demo:/sys/devices/platform/xxx.spi2/spi_master/spi2/statistics # ls
bytes spi_sync transfer_bytes_histo_16-31 transfer_bytes_histo_32768-65535 transfer_bytes_histo_8-15
bytes_rx spi_sync_immediate transfer_bytes_histo_16384-32767 transfer_bytes_histo_4-7 transfer_bytes_histo_8192-16383
bytes_tx timedout transfer_bytes_histo_2-3 transfer_bytes_histo_4096-8191 transfers
errors transfer_bytes_histo_0-1 transfer_bytes_histo_2048-4095 transfer_bytes_histo_512-1023 transfers_split_maxsize
messages transfer_bytes_histo_1024-2047 transfer_bytes_histo_256-511 transfer_bytes_histo_64-127
spi_async transfer_bytes_histo_128-255 transfer_bytes_histo_32-63 transfer_bytes_histo_65536+
1.8 linux spi 通用驱动spidev.c
/driver/spi/spidev.c是linux内核提供的一个spi通用驱动。如果不想写具体的spi芯片驱动,使用这个驱动能给我们带来很多便利,非常方便。在内核代码下的Documentation/spi/spidev是对spidev的描述。其中还提供一个spidev_test.c的测试程序。
使用很简单,只要在你的BSP代码中将spi_board_info
的modalias定义为.modalias = "spidev"
,这样设备启动后就可以在/dev看到spidev0.0字样. 0.0 就是bus_num.chip_select
(对应BSP中的设置)
spidev注册成字符设备,可以方便的使用其提供的标准read/write/ioctl功能函数对设备进行读写设置操作。设备的全双工半双工都得以实现。具体使用可以细看spidev.c或借鉴spidev_test.c
本文引用:
[https://blog.csdn.net/xiaochongtou123/article/details/7751280]
https://blog.csdn.net/weixin_42118770/article/details/116838615
https://blog.csdn.net/kunkliu/article/details/77989964https://blog.csdn.net/zhoutaopower/article/details/100040318
【Linux SPI Framework】相关推荐
- 【翻译】【linux设备驱动】linux地址类型
[翻译][linux设备驱动]linux地址类型 Linux中使用的地址类型列表: 用户虚拟地址(User virtual addresses) 用户空间程序可见的普通地址.用户虚拟地址的长度为32位 ...
- 【CentOS Linux 7】实验2【Linux多用户管理】
[CentOS Linux 7]实验1[Linux文件目录管理] [CentOS Linux 7]实验2[Linux多用户管理] 目 录 一.实验目的 二.实验内容 三.实验步骤和结果
- 【Linux系统编程】进程间通信之无名管道
00. 目录 文章目录 00. 目录 01. 管道概述 02. 管道创建函数 03. 管道的特性 04. 管道设置非阻塞 05. 附录 01. 管道概述 管道也叫无名管道,它是是 UNIX 系统 IP ...
- 【Linux系统编程】信号 (下)
00. 目录 文章目录 00. 目录 01. 信号集 02. 信号阻塞集 03. sigaction函数 04. 附录 01. 信号集 为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处 ...
- 【Linux系统编程】信号 (上)
00. 目录 文章目录 00. 目录 01. 信号概述 02. 信号编号 03. 信号产生方式 04. kill发送信号 05. pause等待信号 06. 信号处理方式 07. 信号处理函数 08. ...
- 【Linux系统编程】进程间通信概述
00. 目录 文章目录 00. 目录 01. 进程间通信概述 02. 进程间通信目的 03. 进程间通信机制 04. 附录 01. 进程间通信概述 进程是一个独立的资源分配单元,不同进程(这里所说的进 ...
- 【Linux系统编程】进程替换:exec 函数族
00. 目录 文章目录 00. 目录 01. exec函数族 02. 参考示例 2.1 execl函数示例 2.2 execv函数示例 2.3 execlp() 或 execvp()函数示例 2.4 ...
- 【Linux系统编程】特殊进程之守护进程
00. 目录 文章目录 00. 目录 01. 守护进程概述 02. 守护进程查看方法 03. 编写守护进程的步骤 04. 守护进程代码 05. 附录 01. 守护进程概述 守护进程(Daemon Pr ...
- 【Linux系统编程】特殊进程之孤儿进程
00. 目录 文章目录 00. 目录 01. 孤儿进程概述 02. 孤儿进程代码 03. 附录 01. 孤儿进程概述 父进程运行结束,但子进程还在运行的子进程就称为孤儿进程(Orphan Proces ...
最新文章
- LeetCode简单题之Fizz Buzz
- java时间转换为字符串格式错误_字符串转换为日期时间格式及其错误处理(转)
- RocketMQ:消息存储机制详解与源码解析
- 二倍图(精灵图的用法)
- 利用Pytorch的C++前端(libtorch)读取预训练权重并进行预测
- C语言九十五之实现经典的反转数组(通过指针或数组下标操作)
- php mysql 平均分_平均评级计算mysql php
- ajax 2分钟超时_ajax和axios、fetch的区别
- 机器学习交易——如何使用回归预测股票价格?【翻译】
- Jmeter系列之Jmeter+Grafana+InfluxDB实时监控
- SpringMVC-@RequestMapping的参数和用法
- node.js读取JSON文件
- SAP - 采购价格确定 ①
- 部署scrapy爬虫到AWS Ubuntu 18.04,用crontab定时执行
- 虚拟机屏幕自适应问题
- 【程序设计训练】3-15 公交系统
- TransReID学习记录
- 如何得到1-100中的质数
- 网络骗子的特征。大家一定要转载。
- QT实现简易的计算器
热门文章
- RedisTemplate添加List类型数据
- OPENGL报错NVD3DREL: GR-805 : DX9 Overlay is DISABLED
- win11蓝牙耳机电脑无法识别为耳机的可能解决办法
- js中会改变原数组的方法及不改变原数组的方法整理
- Centos7 Openssl安装/升级流程
- 格林威尔8000 OLT的使用
- matlab实现srt,SRT计划项目申请书提交版.doc
- java map遍历删除_Java Map在遍历过程中删除元素
- 华为mate40和苹果12pro参数对比 哪个好?看了这篇再决定
- vsto 启用 禁用加载项_新的Outlook VSTO加载项:如何禁用Outlook 2007中的全部答复和转发...