前言
Linux的spi接口驱动实现目录在kernel\drivers\spi
下。这个目录和一些层次比较明显的驱动目录布局不同,全放在这个文件夹下,因此还是只好通过看Kconfig 和 Makefile来找找思路
先看Makefile,里面关键几行:
obj-$(CONFIG_SPI_MASTER) += spi.o
//这个是针对有spi控制器的soc选项,一般的soc都有spi控制器吧。
# SPI master controller drivers (bus)
//下面的这些就是针对不同soc上的spi控制器的驱动了,我们可以通过make menuconfig的时候选上自己对应平台的
| # SPI master controller drivers (bus) |
| obj-$(CONFIG_SPI_ALTERA) += spi-altera.o |
| obj-$(CONFIG_SPI_ARMADA_3700) += spi-armada-3700.o |
| ............ |
| obj-$(CONFIG_SPI_AXI_SPI_ENGINE) += spi-axi-spi-engine.o |
下面这些就是针对于主机作为spi从设备的时候用的,暂时貌似没支持,毕竟现实中几乎没有用过,而是作为master端出现
| # SPI slave protocol handlers |
| obj-$(CONFIG_SPI_SLAVE_TIME) += spi-slave-time.o |
| obj-$(CONFIG_SPI_SLAVE_SYSTEM_CONTROL) += spi-slave-system-control.o |
再看Kconfig,第一个SPI选项我觉得有必要贴一下,首先只有选择它了才能进行后面的配置,其次它的help对spi的描述说的很清楚!
| # |
| # SPI driver configuration |
| # |
| menuconfig SPI |
| bool "SPI support" |
| depends on HAS_IOMEM |
| help |
| The "Serial Peripheral Interface" is a low level synchronous |
| protocol. Chips that support SPI can have data transfer rates |
| up to several tens of Mbit/sec. Chips are addressed with a |
| controller and a chipselect. Most SPI slaves don't support |
| dynamic device discovery; some are even write-only or read-only. |
| |
| SPI is widely used by microcontrollers to talk with sensors, |
| eeprom and flash memory, codecs and various other controller |
| chips, analog to digital (and d-to-a) converters, and more. |
| MMC and SD cards can be accessed using SPI protocol; and for |
| DataFlash cards used in MMC sockets, SPI must always be used. |
| |
| SPI is one of a family of similar protocols using a four wire |
| interface (select, clock, data in, data out) including Microwire |
| (half duplex), SSP, SSI, and PSP. This driver framework should |
| work with most such devices and controllers. |
我们其次需要配上的选项就是SPI_MASTER
和CONFIG_SPI_ROCKCHIP
(我手上的是RK的SDK)。
| config SPI_MASTER |
| # bool "SPI Master Support" |
| bool |
| default SPI |
| help |
| If your system has an master-capable SPI controller (which |
| provides the clock and chipselect), you can enable that |
| controller and the protocol drivers for the SPI slave chips |
| that are connected. |
| |
| config SPI_ROCKCHIP |
| tristate "Rockchip SPI controller driver" |
| help |
| This selects a driver for Rockchip SPI controller. |
| |
| If you say yes to this option, support will be included for |
| RK3066, RK3188 and RK3288 families of SPI controller. |
| Rockchip SPI controller support DMA transport and PIO mode. |
| The main usecase of this controller is to use spi flash as boot |
| device. |
于是从Makefile里得到如下语句和我们相关:
| obj-$(CONFIG_SPI_ROCKCHIP) += spi-rockchip.o |
于是我们要分析的代码主要有:spi.c spi-rockchip.c
,瞬间没压力了,就两个文件,呵呵
下面主要从三个方面来分析spi框架
- spi控制器驱动的实现(毕竟spi控制器的驱动还是有可能要接触的)
- spi设备的驱动(我们更多的是编写设备的驱动,还是以eeprom为例吧,虽然我很想以spi接口的nor flash驱动为例,但是那又会牵涉出mtd子系统,这个留在mtd子系统分析吧)
- spi核心层的实现(上面1、2都是以各自的驱动实现为目标,并不深入到spi核心层,也就是至于spi核心层怎么为我们提供的服务不去关心,只需要按spi核心层使用它提供的服务就是了。所以现在统一分析spi核心层,看它是怎么提供的服务)
spi控制器驱动的实现
以spi-rockchip.c
为例,直接看module_platform_driver
:
| static struct platform_driver rockchip_spi_driver = { |
| .driver = { |
| .name = DRIVER_NAME, |
| .pm = &rockchip_spi_pm, |
| .of_match_table = of_match_ptr(rockchip_spi_dt_match), |
| }, |
| .probe = rockchip_spi_probe, |
| .remove = rockchip_spi_remove, |
| }; |
平台驱动的内部流程就不分析了,直接看匹配成功后rockchip_spi_probe
的调用,但这里还是插入平台spi控制器设备端相关的代码:
- 使用
spi_alloc_master
函数为平台设备pdev
分配一个SPI主设备结构体,并将其大小设置为sizeof(struct rockchip_spi)
。这个函数会分配内存并初始化主设备结构体的各个字段。调用platform_set_drvdata
函数将主设备结构体指针保存在平台设备的私有数据中,以便后续在驱动的其他函数中可以访问该指针。使用spi_master_get_devdata
函数获取之前保存在私有数据中的主设备结构体指针rs
。
| master = spi_alloc_master(&pdev->dev, sizeof(struct rockchip_spi)); |
| if (!master) |
| return -ENOMEM; |
| platform_set_drvdata(pdev, master); |
| rs = spi_master_get_devdata(master); |
- 使用
platform_get_resource
函数获取SPI控制器的IO资源。这些资源包括寄存器地址、中断号等信息,向操作系统请求资源空间并建立起映射为以后所用。
| mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| rs->regs = devm_ioremap_resource(&pdev->dev, mem); |
| if (IS_ERR(rs->regs)) { |
| ret = PTR_ERR(rs->regs); |
| goto err_ioremap_resource; |
| } |
- 使用
devm_clk_get
函数获取SPI控制器所需的时钟,包括"apb_pclk"和"spiclk"。这些时钟用于控制SPI控制器的时序和传输速率。
| rs->apb_pclk = devm_clk_get(&pdev->dev, "apb_pclk"); |
| if (IS_ERR(rs->apb_pclk)) { |
| dev_err(&pdev->dev, "Failed to get apb_pclk\n"); |
| ret = PTR_ERR(rs->apb_pclk); |
| goto err_ioremap_resource; |
| } |
| rs->spiclk = devm_clk_get(&pdev->dev, "spiclk"); |
| if (IS_ERR(rs->spiclk)) { |
| dev_err(&pdev->dev, "Failed to get spi_pclk\n"); |
| ret = PTR_ERR(rs->spiclk); |
| goto err_ioremap_resource; |
| } |
- 使用
clk_prepare_enable
函数使获取到的时钟生效,确保SPI控制器可以正常工作。
| ret = clk_prepare_enable(rs->apb_pclk); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to enable apb_pclk\n"); |
| goto err_ioremap_resource; |
| } |
| |
| ret = clk_prepare_enable(rs->spiclk); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to enable spi_clk\n"); |
| goto err_spiclk_enable; |
| } |
- 调用
spi_enable_chip
函数使SPI芯片处于可用状态。这个函数会执行一些特定的SPI控制器寄存器的配置,以便使芯片可以正常通信。设置SPI主设备的属性。这些属性包括SPI总线类型、主设备指针、设备指针、最大频率等。
| spi_enable_chip(rs, 0); |
| |
| rs->type = SSI_MOTO_SPI; |
| rs->master = master; |
| rs->dev = &pdev->dev; |
| rs->max_freq = clk_get_rate(rs->spiclk); |
- 使用
of_property_read_u32
函数从设备节点中读取属性值。在这个例子中,它读取了"rx-sample-delay-ns"属性,并将其存储在rsd_nsecs
变量中。调用get_fifo_len
函数获取FIFO的长度。FIFO用于在SPI传输过程中暂存数据。
| if (!of_property_read_u32(pdev->dev.of_node, "rx-sample-delay-ns", |
| &rsd_nsecs)) |
| rs->rsd_nsecs = rsd_nsecs; |
| |
| rs->fifo_len = get_fifo_len(rs); |
| if (!rs->fifo_len) { |
| dev_err(&pdev->dev, "Failed to get fifo length\n"); |
| ret = -EINVAL; |
| goto err_get_fifo_len; |
| } |
- 初始化自旋锁。自旋锁用于保护共享资源,防止多个进程同时访问造成冲突。设置设备的运行时PM状态为活动,并启用运行时PM。允许系统在不需要SPI设备时将其置于低功耗状态。
| spin_lock_init(&rs->lock); |
| |
| pm_runtime_set_active(&pdev->dev); |
| pm_runtime_enable(&pdev->dev); |
- 设置主设备的一些属性,例如自动运行时PM、总线号、模式位、芯片选择数量、设备节点等。同时设置主设备的回调函数。这些回调函数将在SPI传输中的不同阶段被调用,以执行相应的操作
| master->auto_runtime_pm = true; |
| master->bus_num = pdev->id; |
| master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_LSB_FIRST; |
| master->num_chipselect = 2; |
| master->dev.of_node = pdev->dev.of_node; |
| master->bits_per_word_mask = SPI_BPW_MASK(16) | SPI_BPW_MASK(8); |
| |
| master->set_cs = rockchip_spi_set_cs; |
| master->prepare_message = rockchip_spi_prepare_message; |
| master->unprepare_message = rockchip_spi_unprepare_message; |
| master->transfer_one = rockchip_spi_transfer_one; |
| master->handle_err = rockchip_spi_handle_err; |
- 使用
dma_request_slave_channel
函数请求DMA通道,如果同时成功请求到了TX和RX的DMA通道,则设置DMA传输的地址和方向,并将相应的DMA通道设置为主设备的属性。
| rs->dma_tx.ch = dma_request_slave_channel(rs->dev, "tx"); |
| if (IS_ERR_OR_NULL(rs->dma_tx.ch)) { |
| |
| if (PTR_ERR(rs->dma_tx.ch) == -EPROBE_DEFER) { |
| ret = -EPROBE_DEFER; |
| goto err_get_fifo_len; |
| } |
| dev_warn(rs->dev, "Failed to request TX DMA channel\n"); |
| } |
| |
| rs->dma_rx.ch = dma_request_slave_channel(rs->dev, "rx"); |
| if (!rs->dma_rx.ch) { |
| if (rs->dma_tx.ch) { |
| dma_release_channel(rs->dma_tx.ch); |
| rs->dma_tx.ch = NULL; |
| } |
| dev_warn(rs->dev, "Failed to request RX DMA channel\n"); |
| } |
- 使用
pinctrl_lookup_state
函数查找高速模式的引脚控制状态。检查查找高速模式的引脚控制状态是否成功。
| rs->high_speed_state = pinctrl_lookup_state(rs->dev->pins->p, |
| "high_speed"); |
| if (IS_ERR_OR_NULL(rs->high_speed_state)) { |
| dev_warn(&pdev->dev, "no high_speed pinctrl state\n"); |
| rs->high_speed_state = NULL; |
| } |
- 使用
devm_spi_register_master
函数注册SPI主设备。
| ret = devm_spi_register_master(&pdev->dev, master); |
| if (ret) { |
| dev_err(&pdev->dev, "Failed to register master\n"); |
| goto err_register_master; |
| } |
暂时不进入到spi核心层分析,这里我们只需要知道调用核心层的注册函数后,核心层会遍历所有注册到核心层的设备(实际最开始是加入到一个全局链表里,和I2C核心层的实现类似),然后尝试着添加每一个总线号为该控制器衍生出的总线号的设备到该spi控制器上,当然如果该spi控制器不支持某一个设备,那就取消添加这个设备,如果添加成功,那么该设备将会添加到spi总线上去,这条总线是spi核心层注册的,用来管理spi接口的设备和spi接口的驱动。
总结下probe函数的主要工作:
- 分配和初始化SPI主设备结构体。
- 获取并映射IO资源。
- 获取和使能时钟。
- 设置SPI主设备的属性和回调函数。
- 请求并设置DMA通道。
- 注册SPI主设备。
spi设备的驱动
以eeprom为例,我们分析下文件at25.c:
同样的,driver的注册过程我们就不深入了解了,其实就是一个总线设备驱动模型。我们直接看probe函数做了什么。
| static const struct of_device_id at25_of_match[] = { |
| { .compatible = "atmel,at25", }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, at25_of_match); |
| |
| static struct spi_driver at25_driver = { |
| .driver = { |
| .name = "at25", |
| .of_match_table = at25_of_match, |
| }, |
| .probe = at25_probe, |
| .remove = at25_remove, |
| }; |
- 检查
spi->dev.platform_data
是否存在,如果不存在,则调用at25_fw_to_chip
函数将固件信息转换为芯片描述,并将其存储在chip
结构体中。如果存在,则直接将spi->dev.platform_data
强制类型转换为spi_eeprom
结构体,并将其赋值给chip
。
| |
| if (!spi->dev.platform_data) { |
| err = at25_fw_to_chip(&spi->dev, &chip); |
| if (err) |
| return err; |
| } else |
| chip = *(struct spi_eeprom *)spi->dev.platform_data; |
- 根据
chip
结构体中的标志位判断EEPROM的地址长度是8位、16位还是24位,并将相应的值赋给addrlen
变量。
| |
| if (chip.flags & EE_ADDR1) |
| addrlen = 1; |
| else if (chip.flags & EE_ADDR2) |
| addrlen = 2; |
| else if (chip.flags & EE_ADDR3) |
| addrlen = 3; |
| else { |
| dev_dbg(&spi->dev, "unsupported address type\n"); |
| return -EINVAL; |
| } |
- 通过发送
AT25_RDSR
指令读取EEPROM的状态寄存器。如果读取失败或状态寄存器中的AT25_SR_nRDY
位为1,表示EEPROM不可用,返回错误码-ENXIO
。
| |
| |
| |
| |
| sr = spi_w8r8(spi, AT25_RDSR); |
| if (sr < 0 || sr & AT25_SR_nRDY) { |
| dev_dbg(&spi->dev, "rdsr --> %d (%02x)\n", sr, sr); |
| return -ENXIO; |
| } |
- 使用
devm_kzalloc
函数为at25_data
结构体分配内存,并使用GFP_KERNEL
标志指定内存分配的上下文。初始化互斥锁at25->lock
,用于保护共享资源的访问。将chip
结构体和spi
设备保存在at25_data
结构体中。使用spi_set_drvdata
函数将at25_data
结构体指针存储在spi
设备的私有数据中,以便在后续的函数中可以方便地访问。将地址长度addrlen
保存在at25_data
结构体的addrlen
字段中。
| at25 = devm_kzalloc(&spi->dev, sizeof(struct at25_data), GFP_KERNEL); |
| if (!at25) |
| return -ENOMEM; |
| |
| mutex_init(&at25->lock); |
| at25->chip = chip; |
| at25->spi = spi_dev_get(spi); |
| spi_set_drvdata(spi, at25); |
| at25->addrlen = addrlen; |
- 创建应用层用来操作的文件,使用
sysfs_bin_attr_init
函数初始化at25->bin
成员变量,其中at25->bin
是struct bin_attribute
类型的变量。设置at25->bin
的属性名称为"eeprom",访问权限为用户只读模式(S_IRUSR
)。设置at25->bin
的读回调函数为at25_bin_read
,写回调函数为at25_bin_write
。
| sysfs_bin_attr_init(&at25->bin); |
| at25->bin.attr.name = "eeprom"; |
| at25->bin.attr.mode = S_IRUSR; |
| at25->bin.read = at25_bin_read; |
| at25->mem.read = at25_mem_read; |
- 根据
chip
的只读标志位(EE_READONLY
),确定是否将写回调函数和写权限添加到at25->bin
。
| at25->bin.size = at25->chip.byte_len; |
| if (!(chip.flags & EE_READONLY)) { |
| at25->bin.write = at25_bin_write; |
| at25->bin.attr.mode |= S_IWUSR; |
| at25->mem.write = at25_mem_write; |
| } |
- 使用
sysfs_create_bin_file
函数将at25->bin
添加到SPI设备的内核对象(spi->dev.kobj
)中,以便将EEPROM字节通过sysfs导出。
| err = sysfs_create_bin_file(&spi->dev.kobj, &at25->bin); |
| if (err) |
| return err; |
- 如果
chip
的setup
字段不为空,将调用chip.setup
函数,并将at25->mem
和chip.context
作为参数传递。使用dev_info
函数打印一条设备信息消息,包括EEPROM的大小、名称、是否只读以及页面大小。
| if (chip.setup) |
| chip.setup(&at25->mem, chip.context); |
| |
| dev_info(&spi->dev, "%Zd %s %s eeprom%s, pagesize %u\n", |
| (at25->bin.size < 1024) |
| ? at25->bin.size |
| : (at25->bin.size / 1024), |
| (at25->bin.size < 1024) ? "Byte" : "KByte", |
| at25->chip.name, |
| (chip.flags & EE_READONLY) ? " (readonly)" : "", |
| at25->chip.page_size); |
该代码的功能是在SPI设备上探测并初始化一个EEPROM芯片,然后将EEPROM的字节通过sysfs导出,以便其他内核代码或用户空间程序可以方便地访问和操作EEPROM数据。
spi核心层的实现
主要看spi.c文件:
| static int __init spi_init(void); |
| postcore_initcall(spi_init); |
从这里可以知道spi_init
的调用(也就是spi核心层的初始化)是在驱动加载前的。
调用kmalloc
函数为SPI子系统分配一个大小为SPI_BUFSIZ
的内核内存缓冲区,并将返回的指针赋值给buf
。
| buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL); |
| if (!buf) { |
| status = -ENOMEM; |
| goto err0; |
| } |
调用bus_register
函数注册SPI总线类型,将其添加到系统总线列表中。如果注册失败,将status
设置为返回的错误码,并跳转到err1
标签处进行错误处理。
| status = bus_register(&spi_bus_type); |
| if (status < 0) |
| goto err1; |
调用class_register
函数注册SPI主控制器类,将其添加到系统设备类列表中。如果注册失败,将status
设置为返回的错误码,并跳转到err2
标签处进行错误处理。
| status = class_register(&spi_master_class); |
| if (status < 0) |
| goto err2; |
所有注册到核心层的spi控制器都属于这个class。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架