Linux驱动开发--SPI子系统

发布于:2025-03-29 ⋅ 阅读:(39) ⋅ 点赞:(0)

2.1 SPI协议介绍

参考资料:

  • 《SPI Block Guide V04.01.pdf》
  • 《S3C2440A_UserManual_Rev13.pdf》

引脚含义如下:

引脚 含义
DO(MOSI) Master Output, Slave Input,
SPI主控用来发出数据,SPI从设备用来接收数据
DI(MISO) Master Input, Slave Output,
SPI主控用来发出数据,SPI从设备用来接收数据
SCK Serial Clock,时钟
CS Chip Select,芯片选择引脚

SPI控制器内部结构

这个图等我们看完后面的SPI协议,再回过头来讲解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

传输示例

假设现在主控芯片要传输一个0x56数据给SPI Flash,时序如下:

首先CS0先拉低选中SPI Flash,0x56的二进制就是0b0101 0110,因此在每个SCK时钟周期,DO输出对应的电平。
SPI Flash会在每个时钟周期的上升沿读取D0上的电平。

SPI模式

在SPI协议中,有两个值来确定SPI的模式。
CPOL:表示SPICLK的初始电平,0为电平,1为高电平
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,1为第二个时钟沿

CPOL CPHA 模式 含义
0 0 0 SPICLK初始电平为低电平,在第一个时钟沿采样数据
0 1 1 SPICLK初始电平为低电平,在第二个时钟沿采样数据
1 0 2 SPICLK初始电平为高电平,在第一个时钟沿采样数据
1 1 3 SPICLK初始电平为高电平,在第二个时钟沿采样数据
我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。

极性选什么?格式选什么?通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:

SCLK的初始电平我们并不需要关心,只要保证在上升沿采样数据就行。

2.2 SPI驱动框架

2.2.1 SPI体系结构

在这里插入图片描述

2.2.2 重要的数据结构
struct spi_master {
	// 设备模型结构,用于表示SPI主控制器设备
	struct device		dev;
	// 用于将SPI主控制器加入链表管理
	struct list_head 	list;

	// SPI总线编号,负值表示动态分配
	s16			bus_num;
	// 支持的片选(Chip Select)数量
	u16			num_chipselect;
	// DMA缓冲区对齐要求
	u16			dma_alignment;

	// 支持的SPI设备模式标志
	u16			mode_bits;
	// 支持的数据位宽掩码
	u32			bits_per_word_mask;
    
	// 最小传输速度(单位:Hz)
	u32			min_speed_hz;
	// 最大传输速度(单位:Hz)
	u32			max_speed_hz;

	// 其他限制标志,如半双工、无接收/发送等
	u16			flags;

	// 获取最大传输大小的回调函数
	size_t (*max_transfer_size)(struct spi_device *spi);
	// 获取最大消息大小的回调函数
	size_t (*max_message_size)(struct spi_device *spi);

	// I/O互斥锁,用于保护SPI主控制器的I/O操作
	struct mutex		io_mutex;
	// SPI总线锁定的自旋锁和互斥锁
	spinlock_t			bus_lock_spinlock;
	struct mutex		bus_lock_mutex;

	// 标志位,表示SPI总线是否被锁定独占使用
	bool				bus_lock_flag;
	// 设置SPI设备模式、时钟等的回调函数
	int					(*setup)(struct spi_device *spi);
	// 执行SPI传输的回调函数
	int					(*transfer)(struct spi_device *spi,
								struct spi_message *mesg);

	// 释放SPI设备资源的回调函数
	void				(*cleanup)(struct spi_device *spi);
	// 判断是否支持DMA传输的回调函数
	bool				(*can_dma)(struct spi_master *master,
					  	 		struct spi_device *spi,
					   			struct spi_transfer *xfer);
    
	// 用于支持队列化传输的标志和工作线程相关字段
	bool					queued;
	struct kthread_worker	kworker;
	struct task_struct		*kworker_task;
	struct kthread_work		pump_messages;
    
	spinlock_t				queue_lock;
	struct list_head		queue;
	struct spi_message		*cur_msg;
    
	bool				idling;
	bool				busy;
	bool				running;
	bool				rt;
	bool				auto_runtime_pm;
	bool                            cur_msg_prepared;
	bool				cur_msg_mapped;
	struct completion               xfer_completion;
	size_t				max_dma_len;

	// 准备/取消硬件传输的回调函数
	int (*prepare_transfer_hardware)(struct spi_master *master);
	int (*transfer_one_message)(struct spi_master *master,
				    struct spi_message *mesg);
	int (*unprepare_transfer_hardware)(struct spi_master *master);

	// 准备/取消消息的回调函数
	int (*prepare_message)(struct spi_master *master,
			       	struct spi_message *message);
	int (*unprepare_message)(struct spi_master *master,
				 	struct spi_message *message);

	// SPI Flash读取相关回调函数
	int (*spi_flash_read)(struct  spi_device *spi,
			      	struct spi_flash_read_message *msg);
	bool (*flash_read_supported)(struct spi_device *spi);

	// 设置片选信号的回调函数
	void (*set_cs)(struct spi_device *spi, bool enable);

	// 执行单次传输的回调函数
	int (*transfer_one)(struct spi_master *master, struct spi_device *spi,
			    struct spi_transfer *transfer);

	// 处理错误的回调函数
	void (*handle_err)(struct spi_master *master,
			   struct spi_message *message);

	// GPIO片选引脚数组
	int			*cs_gpios;

	// SPI传输统计信息
	struct spi_statistics	statistics;

	// DMA通道
	struct dma_chan		*dma_tx;
	struct dma_chan		*dma_rx;

	// 全双工设备的虚拟数据缓冲区
	void			*dummy_rx;
	void			*dummy_tx;

	// 固件翻译片选编号的回调函数
	int (*fw_translate_cs)(struct spi_master *master, unsigned cs);
};

struct spi_driver {
	const struct spi_device_id *id_table;
	int			(*probe)(struct spi_device *spi);
	int			(*remove)(struct spi_device *spi);
	void		(*shutdown)(struct spi_device *spi);
	struct device_driver	driver;
};

2.3 SPI核心

SPI核心**(drivers/spi/spi.c)**中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解其中的主要函数非常关键,因为SPI总线驱动和设备驱动之间以SPI核心作为纽带。SPI核心中的主要函数如下。

(1)增加/删除spi_master

int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);

(2)增加/删除spi_driver

int  spi_register_driver(struct spi_driver *driver);
void spi_unregister_driver(struct spi_driver *driver);

(3)SPI传输

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消息,它会等待消息发送完成。spi_async() 函数用于异步方式发送SPI消息,它会立即返回,消息会在后台线程中处理。这两个函数内部会调用spi_mastertransfertransfer_one_message函数来完成消息的传输。

(4)SPI消息构建

void spi_message_init(struct spi_message *m);
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);

spi_message_init() 函数用于初始化一个SPI消息。spi_message_add_tail() 函数用于将一个SPI传输添加到消息的末尾。通过这两个函数,可以构建包含多个SPI传输的消息,然后通过spi_sync()spi_async()函数发送。

(5)SPI设备操作

int spi_write(struct spi_device *spi, const u8 *buf, size_t len);
int spi_read(struct spi_device *spi, u8 *buf, size_t len);
int spi_write_then_read(struct spi_device *spi, const u8 *txbuf, size_t txlen, u8 *rxbuf, size_t rxlen);

spi_write() 函数用于向SPI设备发送数据。spi_read() 函数用于从SPI设备读取数据。spi_write_then_read() 函数用于先向SPI设备发送数据,然后从设备读取数据。这些函数内部会构建SPI消息并调用spi_sync()函数来完成传输。

2.4 SPI控制器驱动程序

SPI控制器的驱动程序可以基于"平台总线设备驱动"模型来实现:

  • 在设备树里描述SPI控制器的硬件信息,在设备树子节点里描述挂在下面的SPI设备的信息
  • 在platform_driver中提供一个probe函数
    • 它会注册一个spi_master
    • 还会解析设备树子节点,创建spi_device结构体
(1)SPI Master的注册与注销

SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册spi_master

spi_master 申请与释放:

spi_alloc_master 函数用于申请 spi_master,函数原型如下:

struct spi_master *spi_alloc_master(struct device *dev, unsigned size)
{
	struct spi_master	*master;

	if (!dev)
		return NULL;

	master = kzalloc(size + sizeof(*master), GFP_KERNEL);
	if (!master)
		return NULL;

	device_initialize(&master->dev);
	master->bus_num = -1;
	master->num_chipselect = 1;
	master->dev.class = &spi_master_class;
	master->dev.parent = dev;
	pm_suspend_ignore_children(&master->dev, true);
	spi_master_set_devdata(master, &master[1]);

	return master;
}
EXPORT_SYMBOL_GPL(spi_alloc_master);
dev:设备,一般是 platform_device 中的 dev 成员变量。
size: 私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。

返回值: 申请到的 spi_master。

spi_master 的释放通过 spi_master_put 函数来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_masterspi_master_put 函数原型如下:

void spi_master_put(struct spi_master *master)

spi_master 注册

spi_master 初始化完成以后就需要将其注册到 Linux 内核, spi_master 注册函数为spi_register_master,函数原型如下:

int spi_register_master(struct spi_master *master)
{
	static atomic_t		dyn_bus_id = ATOMIC_INIT((1<<15) - 1); 	// 静态变量,用于动态分配总线编号
	struct device		*dev = master->dev.parent; 				// 获取主控制器的父设备
	struct boardinfo	*bi; 		// 用于遍历板级支持信息的指针
	int			status = -ENODEV; 	// 初始化返回状态
	int			dynamic = 0; 		// 标志位,用于指示是否动态分配总线编号

	// 检查主控制器是否有父设备
	if (!dev)
		return -ENODEV;

	// 尝试通过设备树注册主控制器
	status = of_spi_register_master(master);
	if (status)
		return status;

	// 检查是否有至少一个片选信号
	if (master->num_chipselect == 0)
		return -EINVAL;

	// 如果总线编号未指定且使用设备树,则尝试从设备树获取总线编号
	if ((master->bus_num < 0) && master->dev.of_node)
		master->bus_num = of_alias_get_id(master->dev.of_node, "spi");

	// 如果总线编号未指定,则动态分配一个总线编号
	if (master->bus_num < 0) {
		master->bus_num = atomic_dec_return(&dyn_bus_id); // 动态分配总线编号
		dynamic = 1;
	}

	// 初始化主控制器的队列和锁
	INIT_LIST_HEAD(&master->queue);
	spin_lock_init(&master->queue_lock);
	spin_lock_init(&master->bus_lock_spinlock);
	mutex_init(&master->bus_lock_mutex);
	mutex_init(&master->io_mutex);
	master->bus_lock_flag = 0;
	init_completion(&master->xfer_completion);
	if (!master->max_dma_len)
		master->max_dma_len = INT_MAX;

	// 注册主控制器设备
	dev_set_name(&master->dev, "spi%u", master->bus_num); 	// 设置设备名称
	status = device_add(&master->dev); 						// 将设备添加到系统中
	if (status < 0)
		goto done;

	// 输出调试信息
	dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),
			dynamic ? " (dynamic)" : "");

	// 检查是否使用了非队列化的传输函数
	if (master->transfer)
		dev_info(dev, "master is unqueued, this is deprecated\n");
	else {
		// 初始化队列化传输机制
		status = spi_master_initialize_queue(master);
		if (status) {
			device_del(&master->dev); // 如果初始化失败,删除设备
			goto done;
		}
	}

	// 初始化统计信息锁
	spin_lock_init(&master->statistics.lock);

	// 将主控制器加入全局列表并匹配板级支持信息
	mutex_lock(&board_lock);
	list_add_tail(&master->list, &spi_master_list);
	list_for_each_entry(bi, &board_list, list)
		spi_match_master_to_boardinfo(master, &bi->board_info);
	mutex_unlock(&board_lock);

	// 注册设备树和ACPI中的SPI设备
	of_register_spi_devices(master);
	acpi_register_spi_devices(master);

done:
	return status; // 返回最终状态
}

如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:

void spi_unregister_master(struct spi_master *master)

I.MX6U 的 SPI 主机驱动会采用 spi_bitbang_start 这个 API 函数来完成 spi_master 的注册, spi_bitbang_start 函数内部其实也是通过调用 spi_register_master 函数来完成 spi_master 的注册。

(2)实现SPI Master通信方式

SPI Master注册的时候需要实现传输函数,比如 transfer 函数和**transfer_one_message 函数**。

transfer 函数

  • 功能:这是 SPI 主机控制器的 核心数据传输函数,用于处理 SPI 设备之间的通信。它通常由 SPI 主机驱动实现,是与硬件直接交互的函数。
  • 工作方式
    • 它接收一个 struct spi_device 和一个 struct spi_message,负责将消息中的数据通过 SPI 总线发送到目标设备。
    • 它可能直接操作硬件寄存器,控制 SPI 总线的时钟、数据线等,完成数据的发送和接收。
    • 它通常是一个 阻塞式 的函数,调用后会等待传输完成。
  • 适用场景
    • 适用于简单的 SPI 传输场景,或者在不需要复杂队列管理的情况下直接进行数据传输。
    • 通常用于 非队列化 的 SPI 驱动实现。

transfer_one_message 函数

  • 功能:这是用于 队列化传输 的核心函数,主要用于处理一个完整的 spi_message
  • 工作方式
    • 它也是接收一个 struct spi_master 和一个 struct spi_message,但它的主要任务是将消息加入到一个传输队列中。
    • 它通常会启动一个工作线程(kworker),在后台逐步处理队列中的消息。
    • 它允许多个消息排队等待处理,适合多任务、高并发的场景。
  • 适用场景
    • 适用于需要支持 队列化传输 的 SPI 驱动,例如在多设备、多任务的复杂场景中。
    • 它通常用于 队列化 的 SPI 驱动实现。

两者的区别与关系

  • 区别
    • transfer 函数 是一个直接的、同步的传输函数,通常用于简单的、即时的传输任务。
    • transfer_one_message 函数 是一个异步的、队列化的传输函数,用于将消息加入队列,由工作线程在后台处理。
  • 关系
    • 如果驱动程序实现了 队列化传输机制(即 queued 标志为 true),那么 transfer 函数通常会被禁用(即不能同时定义 transfertransfer_one_message)。
    • 如果驱动程序没有实现队列化机制,则需要实现 transfer 函数来直接处理数据传输。
    • 从功能上来说,transfer_one_message 是对 transfer 的一种扩展,它通过队列化机制提高了系统的并发能力和灵活性。
(3)SPI Master的注册示例

和 I2C 的适配器驱动一样, SPI 主机驱动一般都由 SOC 厂商编写好了,打开 imx6ull.dtsi文件,找到如下所示内容:

ecspi3: ecspi@02010000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
    reg = <0x02010000 0x4000>;
    
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_ECSPI3>,
    <&clks IMX6UL_CLK_ECSPI3>;
    clock-names = "ipg", "per";
    dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
    dma-names = "rx", "tx";
    status = "disabled";
};

I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,在此文件中找到如下内容:

static struct platform_device_id spi_imx_devtype[] = {
    {
        .name = "imx1-cspi",
        .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
    }, {
        .name = "imx21-cspi",
        .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
        ......
    }, {
        .name = "imx6ul-ecspi",
        .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
        }, {
        /* sentinel */
        }
};


static const struct of_device_id spi_imx_dt_ids[] = {
    { 	.compatible = "fsl,imx1-cspi", 
        .data =&imx1_cspi_devtype_data, },
    ......
    {   .compatible = "fsl,imx6ul-ecspi", 
        .data =&imx6ul_ecspi_devtype_data, },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
    ......
    ......
static struct platform_driver spi_imx_driver = {
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = spi_imx_dt_ids,
        .pm = IMX_SPI_PM,
    },
    .id_table = spi_imx_devtype,
    .probe = spi_imx_probe,
    .remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);

spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册spi_master

static int spi_imx_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node; // 获取设备树节点
	const struct of_device_id *of_id =
			of_match_device(spi_imx_dt_ids, &pdev->dev); // 匹配设备树 ID
	struct spi_imx_master *mxc_platform_info =
			dev_get_platdata(&pdev->dev); 	// 获取平台数据
	struct spi_master *master; 				// SPI 主控制器结构
	struct spi_imx_data *spi_imx; 			// 私有数据结构
	struct resource *res; 					// 资源结构
	int i, ret, irq; 						// 临时变量

	// 检查是否提供了设备树节点或平台数据
	if (!np && !mxc_platform_info) {
		dev_err(&pdev->dev, "can't get the platform data\n");
		return -EINVAL;
	}

	// 分配 SPI 主控制器结构
	master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data));
	if (!master)
		return -ENOMEM;

	// 设置平台设备的私有数据
	platform_set_drvdata(pdev, master);

	// 初始化 SPI 主控制器的基本字段
	master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32); // 支持的数据位宽范围
	master->bus_num = np ? -1 : pdev->id; 					// 总线编号,设备树模式下为 -1

	// 获取 SPI 主控制器的私有数据
	spi_imx = spi_master_get_devdata(master);
	spi_imx->bitbang.master = master;	// 初始化 bitbang 结构
	spi_imx->dev = &pdev->dev; 			// 设置设备指针

	// 获取设备类型数据
	spi_imx->devtype_data = of_id ? of_id->data :
		(struct spi_imx_devtype_data *)pdev->id_entry->driver_data;

	// 如果提供了平台数据,初始化片选信号
	if (mxc_platform_info) {
		master->num_chipselect = mxc_platform_info->num_chipselect; // 片选数量
		master->cs_gpios = devm_kzalloc(&master->dev,
			sizeof(int) * master->num_chipselect, GFP_KERNEL); 		// 分配片选 GPIO 数组
		if (!master->cs_gpios)
			return -ENOMEM;

		for (i = 0; i < master->num_chipselect; i++)
			master->cs_gpios[i] = mxc_platform_info->chipselect[i]; // 初始化片选 GPIO
	}

	// 初始化 bitbang 回调函数
	spi_imx->bitbang.chipselect = spi_imx_chipselect;
	spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
	spi_imx->bitbang.txrx_bufs = spi_imx_transfer;
    
	spi_imx->bitbang.master->setup = spi_imx_setup;
	spi_imx->bitbang.master->cleanup = spi_imx_cleanup;
	spi_imx->bitbang.master->prepare_message = spi_imx_prepare_message;
	spi_imx->bitbang.master->unprepare_message = spi_imx_unprepare_message;
	spi_imx->bitbang.master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH; // 支持的 SPI 模式
	if (is_imx35_cspi(spi_imx) || is_imx51_ecspi(spi_imx))
		spi_imx->bitbang.master->mode_bits |= SPI_LOOP; // 支持回环模式

	// 初始化传输完成的完成变量
	init_completion(&spi_imx->xfer_done);

	// 获取并映射内存资源
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	spi_imx->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(spi_imx->base)) {
		ret = PTR_ERR(spi_imx->base);
		goto out_master_put;
	}
	spi_imx->base_phys = res->start; // 物理地址

	// 获取中断资源
	irq = platform_get_irq(pdev, 0);
	if (irq < 0) {
		ret = irq;
		goto out_master_put;
	}

	// 请求中断
	ret = devm_request_irq(&pdev->dev, irq, spi_imx_isr, 0,
			       dev_name(&pdev->dev), spi_imx);
	if (ret) {
		dev_err(&pdev->dev, "can't get irq%d: %d\n", irq, ret);
		goto out_master_put;
	}

	// 获取时钟资源
	spi_imx->clk_ipg = devm_clk_get(&pdev->dev, "ipg");
	if (IS_ERR(spi_imx->clk_ipg)) {
		ret = PTR_ERR(spi_imx->clk_ipg);
		goto out_master_put;
	}

	spi_imx->clk_per = devm_clk_get(&pdev->dev, "per");
	if (IS_ERR(spi_imx->clk_per)) {
		ret = PTR_ERR(spi_imx->clk_per);
		goto out_master_put;
	}

	// 启用时钟
	ret = clk_prepare_enable(spi_imx->clk_per);
	if (ret)
		goto out_master_put;

	ret = clk_prepare_enable(spi_imx->clk_ipg);
	if (ret)
		goto out_put_per;

	// 获取时钟频率
	spi_imx->spi_clk = clk_get_rate(spi_imx->clk_per);

	// 初始化 DMA(如果支持)
	if (is_imx51_ecspi(spi_imx)) {
		ret = spi_imx_sdma_init(&pdev->dev, spi_imx, master);
		if (ret == -EPROBE_DEFER)
			goto out_clk_put;

		if (ret < 0)
			dev_err(&pdev->dev, "dma setup error %d, use pio\n",
				ret);
	}

	// 调用设备类型特定的复位函数
	spi_imx->devtype_data->reset(spi_imx);

	// 禁用中断
	spi_imx->devtype_data->intctrl(spi_imx, 0);

	// 设置设备树节点
	master->dev.of_node = pdev->dev.of_node;

	// 启动 bitbang
	ret = spi_bitbang_start(&spi_imx->bitbang);
	if (ret) {
		dev_err(&pdev->dev, "bitbang start failed with %d\n", ret);
		goto out_clk_put;
	}

	// 检查片选 GPIO 是否有效
	if (!master->cs_gpios) {
		dev_err(&pdev->dev, "No CS GPIOs available\n");
		ret = -EINVAL;
		goto out_clk_put;
	}

	// 请求片选 GPIO
	for (i = 0; i < master->num_chipselect; i++) {
		if (!gpio_is_valid(master->cs_gpios[i]))
			continue;

		ret = devm_gpio_request(&pdev->dev, master->cs_gpios[i],
					DRIVER_NAME);
		if (ret) {
			dev_err(&pdev->dev, "Can't get CS GPIO %i\n",
				master->cs_gpios[i]);
			goto out_clk_put;
		}
	}

	// 打印成功信息
	dev_info(&pdev->dev, "probed\n");

	// 禁用时钟
	clk_disable_unprepare(spi_imx->clk_ipg);
	clk_disable_unprepare(spi_imx->clk_per);
	return ret;

out_clk_put:
	clk_disable_unprepare(spi_imx->clk_ipg);
out_put_per:
	clk_disable_unprepare(spi_imx->clk_per);
out_master_put:
	spi_master_put(master); // 释放 SPI 主控制器

	return ret;
}

对于 I.MX6U 来讲, SPI 主机的最终数据收发函数为 spi_imx_transfer,此函数通过如下层层调用最终实现 SPI 数据发送:

spi_imx_transfer
	-> spi_imx_pio_transfer
		-> spi_imx_push
    		-> spi_imx->tx

spi_imx 是个 spi_imx_data 类型的机构指针变量,其中 txrx 这两个成员变量分别为 SPI数据发送和接收函数。 I.MX6U SPI 主机驱动会维护一个 spi_imx_data 类型的变量 spi_imx,并且使用 spi_imx_setupxfer 函数来设置 spi_imxtxrx 函数。根据要发送的数据数据位宽的不同,分别有 8 位、 16 位和 32 位的发送函数,如下所示:

struct spi_imx_data {
	struct spi_bitbang bitbang;
	struct device *dev;

	struct completion xfer_done;
	void __iomem *base;
	unsigned long base_phys;

	struct clk *clk_per;
	struct clk *clk_ipg;
	unsigned long spi_clk;
	unsigned int spi_bus_clk;

	unsigned int bytes_per_word;

	unsigned int count;
	void (*tx)(struct spi_imx_data *);
	void (*rx)(struct spi_imx_data *);
	void *rx_buf;
	const void *tx_buf;
	unsigned int txfifo; /* number of words pushed in tx FIFO */

	/* DMA */
	bool usedma;
	u32 wml;
	struct completion dma_rx_completion;
	struct completion dma_tx_completion;

	const struct spi_imx_devtype_data *devtype_data;
};
spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32

spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32

我们就以 spi_imx_buf_tx_u8 这个函数为例,看看,一个自己的数据发送是怎么完成的,在spi-imx.c 文件中找到如下所示内容:

#define MXC_SPI_BUF_RX(type)						\
static void spi_imx_buf_rx_##type(struct spi_imx_data *spi_imx)		\
{													\
	unsigned int val = readl(spi_imx->base + MXC_CSPIRXDATA);		\
													\
	if (spi_imx->rx_buf) {							\
		*(type *)spi_imx->rx_buf = val;				\
		spi_imx->rx_buf += sizeof(type);			\
	}												\
}

#define MXC_SPI_BUF_TX(type)						\
static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx)		\
{													\
	type val = 0;									\
													\
	if (spi_imx->tx_buf) {							\
		val = *(type *)spi_imx->tx_buf;				\
		spi_imx->tx_buf += sizeof(type);			\
	}												\
													\
	spi_imx->count -= sizeof(type);					\
													\
	writel(val, spi_imx->base + MXC_CSPITXDATA);	\
}

MXC_SPI_BUF_RX(u8)
MXC_SPI_BUF_TX(u8)
MXC_SPI_BUF_RX(u16)
MXC_SPI_BUF_TX(u16)
MXC_SPI_BUF_RX(u32)
MXC_SPI_BUF_TX(u32)

spi_imx_buf_tx_u8 函数是通过 MXC_SPI_BUF_TX 宏来实现的。就是将要发送的数据值写入到 ECSPITXDATA 寄存器里面去,这和我们 SPI 裸机实验的方法一样。将 MXC_SPI_BUF_TX(u8)展开就是 spi_imx_buf_tx_u8 函数。其他的 txrx 函数都是这样实现的,这里就不做介绍了。关于 I.MX6U 的主机驱动程序就讲解到这里,基本套路和 I2C 的适配器驱动程序类似。

2.5 SPI设备驱动程序

跟"平台总线设备驱动模型"类似,Linux中也有一个"SPI总线设备驱动模型":

  • 左边是spi_driver,使用C文件实现,里面有id_table表示能支持哪些SPI设备,有probe函数
  • 右边是spi_device,用来描述SPI设备,比如它的片选引脚、频率
    • 可以来自设备树:比如由SPI控制器驱动程序解析设备树后创建、注册spi_device
    • 可以来自C文件:比如使用spi_register_board_info创建、注册spi_device
(1)SPI driver的注册
struct spi_driver {
	const struct spi_device_id *id_table;
	int			(*probe)(struct spi_device *spi);
	int			(*remove)(struct spi_device *spi);
	void		(*shutdown)(struct spi_device *spi);
	struct device_driver	driver;
};

同样的, spi_driver 初始化上面结构体的成员变量,然后需要向 Linux 内核注册, spi_driver 注册函数为spi_register_driver,函数原型如下:

/* use a define to avoid include chaining to get THIS_MODULE */
#define spi_register_driver(driver) \
	__spi_register_driver(THIS_MODULE, driver)
/*************************************************************/	
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv)
{
	sdrv->driver.owner = owner; 		// 设置驱动程序的所有者模块
	sdrv->driver.bus = &spi_bus_type; 	// 设置驱动程序所属的总线类型为 SPI 总线

	// 如果用户定义了 probe 函数,则设置 driver 的 probe 函数
	if (sdrv->probe)
		sdrv->driver.probe = spi_drv_probe;

	// 如果用户定义了 remove 函数,则设置 driver 的 remove 函数
	if (sdrv->remove)
		sdrv->driver.remove = spi_drv_remove;

	// 如果用户定义了 shutdown 函数,则设置 driver 的 shutdown 函数
	if (sdrv->shutdown)
		sdrv->driver.shutdown = spi_drv_shutdown;

	// 调用 driver_register 注册驱动程序
	return driver_register(&sdrv->driver);
}
EXPORT_SYMBOL_GPL(__spi_register_driver);
/*************************************************************/	
int driver_register(struct device_driver *drv)
{
	int ret;
	struct device_driver *other;

	// 检查驱动程序的总线是否已初始化
	BUG_ON(!drv->bus->p);

	// 检查驱动程序是否使用了过时的回调函数
	if ((drv->bus->probe && drv->probe) ||
	    (drv->bus->remove && drv->remove) ||
	    (drv->bus->shutdown && drv->shutdown))
		printk(KERN_WARNING "Driver '%s' needs updating - please use "
			"bus_type methods\n", drv->name);

	// 检查是否已经注册了同名的驱动程序
	other = driver_find(drv->name, drv->bus);
	if (other) {
		// 如果已注册,打印错误信息并返回 -EBUSY
		printk(KERN_ERR "Error: Driver '%s' is already registered, "
			"aborting...\n", drv->name);
		return -EBUSY;
	}

	// 将驱动程序添加到总线中
	ret = bus_add_driver(drv);
	if (ret)
		return ret;

	// 添加驱动程序的属性组
	ret = driver_add_groups(drv, drv->groups);
	if (ret) {
		// 如果添加属性组失败,从总线中移除驱动程序
		bus_remove_driver(drv);
		return ret;
	}

	// 发送 KOBJ_ADD 事件,通知用户空间驱动程序已注册
	kobject_uevent(&drv->p->kobj, KOBJ_ADD);

	return ret; // 返回最终结果
}
EXPORT_SYMBOL_GPL(driver_register);

注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:

void spi_unregister_driver(struct spi_driver *sdrv)
(2)SPI driver的注册示例
/* probe 函数 */
static int xxx_probe(struct spi_device *spi)
{
    /* 具体函数内容 */
    return 0;
}
8
/* remove 函数 */
static int xxx_remove(struct spi_device *spi)
{
    /* 具体函数内容 */
    return 0;
}
/* 传统匹配方式 ID 列表 */
static const struct spi_device_id xxx_id[] = {
    {"xxx", 0},
    {}
};

/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
    { .compatible = "xxx" },
    { /* Sentinel */ }
};

/* SPI 驱动结构体 */
static struct spi_driver xxx_driver = {
    .probe = xxx_probe,
    .remove = xxx_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "xxx",
        .of_match_table = xxx_of_match,
    },
    .id_table = xxx_id,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
 	return spi_register_driver(&xxx_driver);
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	spi_unregister_driver(&xxx_driver);
}

module_init(xxx_init);
module_exit(xxx_exit);
(3)SPI 设备和驱动匹配过程

SPI总线为 spi_bus_type,定义在 drivers/spi/spi.c 文件中,内容如下:

struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};
EXPORT_SYMBOL_GPL(spi_bus_type);

SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:

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;
	/* 传统的、无设备树的 SPI 设备和驱动匹配 */
	if (sdrv->id_table)
		return !!spi_match_id(sdrv->id_table, spi);

	return strcmp(spi->modalias, drv->name) == 0;
}
(4)SPI 设备数据收发处理流程

SPI 设备驱动的核心是 spi_driver。当我们向 Linux 内核注册成功 spi_driver 以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,结构体内容如下:

struct spi_transfer {
	/* it's ok if tx_buf == rx_buf (right?)
	 * for MicroWire, one buffer must be null
	 * buffers must work with dma_*map_single() calls, unless
	 *   spi_message.is_dma_mapped reports a pre-existing mapping
	 */
	const void	*tx_buf;		// 发送缓冲区,存储要发送的数据
	void		*rx_buf; 		// 接收缓冲区,存储接收到的数据
	unsigned	len;			 

	dma_addr_t	tx_dma;			// DMA 地址,用于发送数据
	dma_addr_t	rx_dma;		    // DMA 地址,用于接收数据
	struct sg_table tx_sg;		// 散列缓冲区,用于发送数据(支持 DMA)
	struct sg_table rx_sg; 		// 散列缓冲区,用于接收数据(支持 DMA)

	unsigned	cs_change:1;	// 是否改变片选信号,1 表示在传输后改变片选信号
	unsigned	tx_nbits:3; 	// 每个字的发送位数(1、2 或 4 位)
	unsigned	rx_nbits:3;
#define	SPI_NBITS_SINGLE	0x01 /* 1bit transfer */
#define	SPI_NBITS_DUAL		0x02 /* 2bits transfer */
#define	SPI_NBITS_QUAD		0x04 /* 4bits transfer */
	u8		bits_per_word;
	u16		delay_usecs;
	u32		speed_hz;

	struct list_head transfer_list;// 链表节点,用于将多个传输添加到 SPI 消息队列
};

len 是要进行传输的数据长度, SPI 是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。

spi_transfer 需要组织成 spi_messagespi_message 也是一个结构体,内容如下:

struct spi_message {
    struct list_head transfers;       // 存储该消息中的所有 SPI 传输
    struct spi_device *spi;           // 关联的 SPI 设备
    unsigned is_dma_mapped:1;         // 是否进行了 DMA 映射
    void (*complete)(void *context);  // 完成回调函数
    void *context;                    // 回调函数的上下文参数
    unsigned frame_length;            // 消息的总帧长度(位)
    unsigned actual_length;           // 实际传输的数据长度(字节)
    int status;                       // 消息的处理状态
    struct list_head queue;           // 用于将消息添加到队列
    void *state;                      // 供当前拥有该消息的驱动使用
    struct list_head resources;       // 存储与消息关联的资源
};

在使用spi_message之前需要对其进行初始化, spi_message初始化函数为spi_message_init,函数原型如下:

void spi_message_init(struct spi_message *m)

spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,这里我们要用到 spi_message_add_tail 函数,此函数原型如下:

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

int spi_sync(struct spi_device *spi, struct spi_message *message)

异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传输函数为 spi_async,函数原型如下:

int spi_async(struct spi_device *spi, struct spi_message *message)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为什么 spi_transfer 需要组织成 spi_message

  1. 按顺序执行多个传输
    • 在 SPI 通信中,一个设备可能需要连续执行多个操作(如发送命令、读取数据等)。每个操作可以表示为一个 spi_transfer,而 spi_message 则将这些传输组织成一个逻辑上的“消息”,确保它们按顺序执行。
    • 例如,读取一个寄存器的值可能需要先发送寄存器地址(一个 spi_transfer),然后读取数据(另一个 spi_transfer)。通过将这两个传输组织成一个 spi_message,可以确保它们按顺序执行。
  2. 整体管理
    • spi_message 提供了一个整体的管理机制,可以设置消息的属性(如 DMA 映射、完成回调等),并跟踪整个消息的执行状态。
    • 例如,spi_messagecomplete 回调函数可以在消息处理完成后通知应用程序。

队列在 SPI 通信中的作用

  1. 按顺序处理消息
    • SPI 主控制器(spi_master)通常会有一个队列来管理多个 spi_message。队列的作用是按顺序处理这些消息,确保消息的执行顺序。
    • 例如,spi_controller 结构体中的 queue 成员用于存储等待传输的消息队列。
  2. 异步处理
    • 队列支持异步操作,允许应用程序提交多个消息,而无需等待每个消息完成。
    • 例如,spi_async 函数可以将消息添加到队列中,然后由内核线程(如 kworker)异步处理

linux设备驱动 spi详解4-spi的数据传输流程 - Action_er - 博客园

2.6 示例

(1)I.MX6U ECSPI 简介

I.MX6U 自带的 SPI 外设叫做 ECSPI,全称是 Enhanced Configurable Serial Peripheral Interface,别看前面加了个“EC”就以为和标准 SPI 有啥不同的, 其实就是 SPI。 ECSPI 有 6432 个接收FIFO(RXFIFO)和 6432 个发送 FIFO(TXFIFO) 。

①、全双工同步串行接口。

②、可配置的主/从模式。

③、四个片选信号,支持多从机。

④、发送和接收都有一个 32x64 的 FIFO。

⑤、片选信号 SS/CS,时钟信号 SCLK 极性可配置。

⑥、支持 DMA。

I.MX6U 的 ECSPI 可以工作在主模式或从模式,本章我们使用主模式, I.MX6U 有 4 个ECSPI每个 ECSPI 支持四个片选信号,也就说,如果你要使用 ECSPI 的硬件片选信号的话,一个 ECSPI 可以支持 4 个外设如果不使用硬件的片选信号就可以支持无数个外设,本章实验我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选 IO,软件片选的话可以使用任意的 IO。

均抄录自 《正点原子 imx6ull 驱动开发指南》

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(2)ICM-20608 简介

ICM-20608 是 InvenSense 出品的一款 6 轴 MEMS 传感器,包括 3 轴加速度和 3 轴陀螺仪。ICM-20608 尺寸非常小,只有 3x3x0.75mm,采用 16P 的 LGA 封装。 ICM-20608 内部有一个 512字节的 FIFO。陀螺仪的量程范围可以编程设置,可选择± 250,±500,±1000 和±2000° /s,加速度的量程范围也可以编程设置,可选择± 2g,±4g, ±8g 和±16g。陀螺仪和加速度计都是 16 位的 ADC,并且支持 I2C 和 SPI 两种协议,使用 I2C 接口的话通信速度最高可以达到400KHz,使用 SPI 接口的话通信速度最高可达到 8MHz。

①、陀螺仪支持 X,Y 和 Z 三轴输出,内部集成 16 位 ADC,测量范围可设置:±250,± 500,±1000 和±2000° /s

②、加速度计支持 X,Y 和 Z 轴输出,内部集成 16 位 ADC,测量范围可设置:±2g,±4g, ±4g,±8g 和±16g

③、用户可编程中断

④、内部包含 512 字节的 FIFO

⑤、内部包含一个数字温度传感器。

⑥、耐 10000g 的冲击。

⑦、支持快速 I2C,速度可达 400KHz

⑧、支持 SPI,速度可达 8MHz。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果使用 IIC 接口的话 ICM-20608 的 AD0 引脚决定 I2C 设备从地址的最后一位,如果 AD0为 0 的话 ICM-20608 从设备地址是 0X68,如果 AD0 为 1 的话 ICM-20608 从设备地址为 0X69。

本章我们使用 SPI 接口,跟上一章使用 AP3216C 一样, ICM-20608 也是通过读写寄存器来配置和读取传感器数据,使用 SPI 接口读写寄存器需要 16 个时钟或者更多(如果读写操作包括多个字节的话)

第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为 1,如果是写的话寄存器地址最高位要为 0,剩下的 7 位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。

表 27.1.3.1 列出了本章实验用到的一些寄存器和位,关于 ICM-20608 的详细寄存器和位的介绍请参考 ICM-20608 的寄存器手册:

寄存器地址 寄存器功能 描述
0X19 SMLPRT_DIV[7:0] 输出速率设置 设置输出速率,输出速率计算公式如下:SAMPLE_RATE=INTERNAL_SAMPLE_RATE/(1 + SMPLRT_DIV)
0X1A DLPF_CFG[2:0] 芯片配置 设置陀螺仪低通滤波。可设置 0~7。
0X1B FS_SEL[1:0] 陀螺仪量程设置 0:±250dps;1:±500dps;2:±1000dps;3:±2000dps
0X1C ACC_FS_SEL[1:0] 加速度计量程设置 0:±2g;1:±4g;2:±8g;3:±16g
0X1D A_DLPF_CFG[2:0] 加速度计低通滤波设置 设置加速度计的低通滤波,可设置 0~7。
0X1E GYRO_CYCLE[7] 陀螺仪低功耗使能 0:关闭陀螺仪的低功耗功能。
1:使能陀螺仪的低功耗功能。
0X23 YG_FIFO_EN[5] FIFO 使能控制 1:使能陀螺仪 Y 轴 FIFO。
0:关闭陀螺仪 Y 轴 FIFO。
0X23 ZG_FIFO_EN[4] FIFO 使能控制 1:使能陀螺仪 Z 轴 FIFO。
0:关闭陀螺仪 Z 轴 FIFO。
0X3B ACCEL_FIFO_EN[3] FIFO 使能控制 1:使能加速度计 FIFO。
0:关闭加速度计 FIFO。
0X3B ACCEL_XOUT_H[7:0] 加速度 X 轴数据高 8 位 加速度 X 轴数据高 8 位
0X3C ACCEL_XOUT_L[7:0] 加速度 X 轴数据低 8 位 加速度 X 轴数据低 8 位
0X3D ACCEL_YOUT_H[7:0] 加速度 Y 轴数据高 8 位 加速度 Y 轴数据高 8 位
0X3E ACCEL_YOUT_L[7:0] 加速度 Y 轴数据低 8 位 加速度 Y 轴数据低 8 位
0X3F ACCEL_ZOUT_H[7:0] 加速度 Z 轴数据高 8 位 加速度 Z 轴数据高 8 位
0X40 ACCEL_ZOUT_L[7:0] 加速度 Z 轴数据低 8 位 加速度 Z 轴数据低 8 位
0X41 TEMP_OUT_H[7:0] 温度数据高 8 位 温度数据高 8 位
0X42 TEMP_OUT_L[7:0] 温度数据低 8 位 温度数据低 8 位
0X43 GYRO_XOUT_H[7:0] 陀螺仪 X 轴数据高 8 位 陀螺仪 X 轴数据高 8 位
0X44 GYRO_XOUT_L[7:0] 陀螺仪 X 轴数据低 8 位 陀螺仪 X 轴数据低 8 位
0X45 GYRO_YOUT_H[7:0] 陀螺仪 Y 轴数据高 8 位 陀螺仪 Y 轴数据高 8 位
0X46 GYRO_YOUT_L[7:0] 陀螺仪 Y 轴数据低 8 位 陀螺仪 Y 轴数据低 8 位
0X47 GYRO_ZOUT_H[7:0] 陀螺仪 Z 轴数据高 8 位 陀螺仪 Z 轴数据高 8 位
0X48 GYRO_ZOUT_L[7:0] 陀螺仪 Z 轴数据低 8 位 陀螺仪 Z 轴数据低 8 位
0X6B DEVICE_RESET[7] 电源管理寄存器 1 1:复位 ICM-20608。
0X6B SLEEP[6] 电源管理寄存器 1 0:退出休眠模式;1,进入休眠模式
0X6C STBY_XA[5] 电源管理寄存器 2 0:使能加速度计 X 轴。
1:关闭加速度计 X 轴。
0X6C STBY_YA[4] 电源管理寄存器 2 0:使能加速度计 Y 轴。
1:关闭加速度计 Y 轴。
0X6C STBY_ZA[3] 电源管理寄存器 2 0:使能加速度计 Z 轴。
1:关闭加速度计 Z 轴。
0X6C STBY_XG[2] 电源管理寄存器 2 0:使能陀螺仪 X 轴。
1:关闭陀螺仪 X 轴。
0X6C STBY_YG[1] 电源管理寄存器 2 0:使能陀螺仪 Y 轴。
1:关闭陀螺仪 Y 轴。
0X6C STBY_ZG[0] 电源管理寄存器 2 0:使能陀螺仪 Z 轴。
1:关闭陀螺仪 Z 轴。
0X75 WHOAMI[7:0] ID 寄存器 ICM-20608G 的 ID 为 0XAF,ICM-20608D 的 ID 为 0XAE。

(3)添加设备树

(4)设备驱动

(5)测试程序

2.7 IIC、SPI、USB驱动架构类比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传