【驱动】SPI驱动分析(六)-RK SPI驱动分析

发布时间 2023-11-30 22:53:38作者: 嵌入式与Linux那些事

前言

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_MASTERCONFIG_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框架

  1. spi控制器驱动的实现(毕竟spi控制器的驱动还是有可能要接触的)
  2. spi设备的驱动(我们更多的是编写设备的驱动,还是以eeprom为例吧,虽然我很想以spi接口的nor flash驱动为例,但是那又会牵涉出mtd子系统,这个留在mtd子系统分析吧)
  3. 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控制器设备端相关的代码:

  1. 使用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);
  1. 使用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;
	}
  1. 使用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;
	}
  1. 使用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;
	}
  1. 调用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);
  1. 使用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;
}
  1. 初始化自旋锁。自旋锁用于保护共享资源,防止多个进程同时访问造成冲突。设置设备的运行时PM状态为活动,并启用运行时PM。允许系统在不需要SPI设备时将其置于低功耗状态。
spin_lock_init(&rs->lock);

pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
  1. 设置主设备的一些属性,例如自动运行时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;
  1. 使用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)) {
		/* Check tx to see if we need defer probing driver */
		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");
	}
  1. 使用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;
	}
  1. 使用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,
};
  1. 检查spi->dev.platform_data是否存在,如果不存在,则调用at25_fw_to_chip函数将固件信息转换为芯片描述,并将其存储在chip结构体中。如果存在,则直接将spi->dev.platform_data强制类型转换为spi_eeprom结构体,并将其赋值给chip
	/* Chip description */
	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;
  1. 根据chip结构体中的标志位判断EEPROM的地址长度是8位、16位还是24位,并将相应的值赋给addrlen变量。
	/* For now we only support 8/16/24 bit addressing */
	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;
	}
  1. 通过发送AT25_RDSR指令读取EEPROM的状态寄存器。如果读取失败或状态寄存器中的AT25_SR_nRDY位为1,表示EEPROM不可用,返回错误码-ENXIO
	/* Ping the chip ... the status register is pretty portable,
	 * unlike probing manufacturer IDs.  We do expect that system
	 * firmware didn't write it in the past few milliseconds!
	 */
	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;
	}
  1. 使用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;
  1. 创建应用层用来操作的文件,使用sysfs_bin_attr_init函数初始化at25->bin成员变量,其中at25->binstruct 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;
  1. 根据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;
	}
  1. 使用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;
  1. 如果chipsetup字段不为空,将调用chip.setup函数,并将at25->memchip.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。