嵌入式驱动开发详解14(SPI驱动架构实现)

文章目录

前言

SPI 是很常用的串行通信协议,可以通过 SPI 来连接众多的传感器,相比 I2C 接 口,SPI 接口的通信速度很快,I2C 最多 400KHz,但是 SPI 可以到达几十 MHz。本文章主要讲解SPI串行通信协议相关的基本内容和在Linux操作系统下如何进行SPI外设的开发。

SPI简介

SPI介绍

SPI 全称是 Serial Perripheral Interface,也就是串行外围设备接口。SPI 是 Motorola 公司推出的一种同步串行接口 技术,是一种高速、全双工的同步通信总线,SPI 时钟频率相比 I2C 要高很多,最高可以工作 在上百 MHz。

SPI 以主从方式工作,通常是有一个主设备和一个或多个从设备,一般 SPI 需要 4 根线,但是也可以使用三根线(单向传输),这四根线如下:

①、CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。 I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI 主机不需要发送从机 设备,直接将相应的从机设备片选信号拉低即可。

②、SCK,Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。

③、MOSI/SDO,Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线 只能用于主机向从机发送数据,也就是主机输出,从机输入。

④、MISO/SDI,Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只 能用户从机向主机发送数据,也就是主机输入,从机输出。

SPI 通信都是由主机发起的,主机需要提供通信的时钟信号,如下图所示:

SPI 的传输方式:
全双工通信 :在任何时刻,主机与从机之间都可以同时进行数据的发送和接收。
单工通信 :在同一时刻,只有一个传输的方向,发送或者是接收。
半双工通信 :在同一时刻,只能为一个方向传输数据。
单工和双工的区别在于是否能同时发送或者接收,全或者半的区别在于是双向的还是单向的。

SPI工作模式

SPI 有四种工作模式

  • CPOL,详称Clock Polarity,就是时钟极性,当主从机没有数据传输的时候 SCL 线的电平状态(即空闲状态)。假如空闲状态是高电平, CPOL = 1;若空闲状态时低电平,那么 CPOL = 0。
  • CPHA,详称Clock Phase,就是时钟相位。在这里先科普一下数据传输的常识: 同步通信时,数据的变化和采样都是在时钟边沿上进行的,每一个时钟周期都会有上升沿和下降沿两个边沿,那么数据的变化和采样就分别安排在两个不同的边沿,由于数据在产生和到它稳定是需要一定的时间,那么假如我们在第 1 个边沿信号把数据输出了,从机只能从第 2 个边沿信号去采样这个数据。

通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:

  • CPOL = 0&&CPHA = 0时序 :由于配置了 CPOL = 0,可以看到当数据未发送或者发送完毕,SCL 的状态是低电平,再者 CPHA = 0 即是奇数边沿采集。所以传输的数据会在奇数边沿上升沿被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻, MOSI和 MISO 的有效信号才发生变化。

  • CPOL = 0&CPHA = 1 时序 :由于 CPOL = 0,所以 SCL 的空闲状态依然是低电平,CPHA = 1 数据就从偶数边沿采样,至于是上升沿还是下降沿,从上图就可以知道,是下降沿。这里有一个误区,空闲状态是低电平的情况下,不是应该上升沿吗,为什么这里是下降沿?首先我们先明确这里是偶数边沿采样,那么看图就很清晰,SCL 低电平空闲状态下,上升沿是在奇数边沿上,下降沿是在偶数边沿上。

  • CPOL = 1&CPHA = 0 时序 :只是这里是 CPOL = 1,即 SCL 空闲状态为高电平,在 CPHA = 0,奇数边沿采样的情况下,数据在奇数边沿下降沿要保持稳定并等待采样。

  • CPOL = 1&&CPHA = 1 :可以看到未发送数据和发送数据完毕,SCL的状态是高电平,奇数边沿的边沿极性是上升沿,偶数边沿的边沿极性是下降沿。因为 CPHA= 1,所以数据在偶数边沿上升沿被采样。在奇数边沿的时候 MOSI 和 MISO 会发生变化,在偶数边沿时候是稳定的。

SPI特点

  • SPI的优点:
    1. 全双工串行通信;
    2. 高速数据传输速率。
    3. 简单的软件配置;
    4. 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
    5. 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
  • SPI的缺点:
    1. 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
    2. 通常仅支持一个主设备;
    3. 需要更多的引脚(与I2C不同);
    4. 没有定义硬件级别的错误检查协议;
    5. 与RS-232和CAN总线相比,只能支持非常短的距离;

驱动开发

驱动架构

SPI控制器驱动

SPI 主机驱动就是 SOC 的 SPI 控制器驱动,Linux 内核使用 spi_master 表示 SPI 主机驱动,spi_master 是个结构体,定义在 include/linux/spi/spi.h 文件中。

c 复制代码
struct spi_master {
	struct device	dev;
	int			(*transfer)(struct spi_device *spi,
						struct spi_message *mesg);
	int (*transfer_one_message)(struct spi_master *master,
				    struct spi_message *mesg);
};

transfer 函数,和i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message, SPI 的数据会打包成 spi_message,然后以队列方式发送出去。 也就是 SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,

因此对于 SPI 主机控制器的驱动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一 样。和 I2C 适配器驱动一样,SPI 主机驱动一般都是 SOC 厂商去编写的,所以我们作为 SOC 的 使用者,这一部分的驱动就不用操心了,除非你是在 SOC 原厂工作,内容就是写 SPI 主机驱动。

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

  • spi_master 申请与释放
    spi_alloc_master 函数用于申请 spi_master,函数原型如下:
c 复制代码
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)

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

c 复制代码
void spi_master_put(struct spi_master *master)
  • spi_master 的注册与注销
    当 spi_master 初始化完成以后就需要将其注册到 Linux 内核,spi_master 注册函数为 spi_register_master,函数原型如下:
c 复制代码
int spi_register_master(struct spi_master *master)

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

c 复制代码
void spi_unregister_master(struct spi_master *master)

SPI 主机驱动一般都由 SOC 厂商编写好了,在设备树中可以看到以下内容:

c 复制代码
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>;
	status = "disabled";
};

在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI) 主机驱动,I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,这个地方文件的命名与不同的芯片对应的名牌名称有关。对应的主机驱动(控制器驱动)也是通过platform架构实现的。

c 复制代码
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,
};

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

spi_imx_probe->设置spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;//设置发送或者接收字节数量
			  ->设置spi_imx->bitbang.txrx_bufs = spi_imx_transfer->spi_imx_pio_transfer()->spi_imx_push()->spi_imx->tx(spi_imx);//发送函数配置
			  ->spi_bitbang_start->注册spi_register_master;
			 					 ->设置master->transfer_one_message = spi_bitbang_transfer_one;

spi_imx 是个 spi_imx_data 类型的机构指针变量,其中 tx 和 rx 这两个成员变量分别为 SPI 数据发送和接收函数,最终会将spi_bitbang_transfer_one函数配置为第二行的发生配置函数。

SPI设备驱动

spi 设备驱动和 i2c 设备驱动也很类似,Linux 内核使用 spi_driver 结构体来表示 spi 设备驱 动,我们在编写 SPI 设备驱动的时候需要实现 spi_driver 。 spi_driver 结构体定义在 include/linux/spi/spi.h 文件中,结构体内容如下:

c 复制代码
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 注册函数为 spi_register_driver,函数原型如下:

c 复制代码
int spi_register_driver(struct spi_driver *sdrv)

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

c 复制代码
void spi_unregister_driver(struct spi_driver *sdrv)

SPI 设备和驱动匹配过程

SPI 设备和驱动的匹配过程是由 SPI 总线来完成的,这点和 platform、I2C 等驱动一样,SPI 总线为 spi_bus_type,定义在 drivers/spi/spi.c 文件中,驱动的匹配函数为 spi_match_device。

c 复制代码
struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};

SPI其他相关API函数

spi_transfer 结构体用于描述 SPI 传输信息

c 复制代码
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; //tx_buf 保存着要发送的数据。
	void		*rx_buf;  //rx_buf 用于保存接收到的数据。
	unsigned	len;

	dma_addr_t	tx_dma;
	dma_addr_t	rx_dma;
	struct sg_table tx_sg;
	struct sg_table rx_sg;

	unsigned	cs_change:1;
	unsigned	tx_nbits:3;
	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_transfer 需要组织成 spi_message,spi_message 也是一个结构体

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

c 复制代码
void spi_message_init(struct spi_message *m)

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

c 复制代码
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

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

c 复制代码
int spi_sync(struct spi_device *spi, struct spi_message *message)

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

c 复制代码
int spi_async(struct spi_device *spi, struct spi_message *message)

参考文献

  1. 个人专栏系列文章
  2. 正点原子嵌入式驱动开发指南
  3. 对代码有兴趣的同学可以查看链接https://github.com/NUAATRY/imx6ull_dev
相关推荐
给我买个墨镜戴16 分钟前
黑马商城微服务复习(5)
微服务·云原生·架构
开源架构师2 小时前
开源架构与云计算的传奇融合
大数据·架构·开源·云计算·应用案例·开源架构·容器化部署
开源架构师4 小时前
开源架构的性能优化:极致突破,引领卓越
性能优化·架构·开源·代码优化·数据库优化·异步处理·缓存策略
君逸~~5 小时前
RK3568(二)——字符设备驱动开发
linux·驱动开发·笔记·学习·rk3568
一只拉古6 小时前
后端编程大师之路:在 .NET 应用中使用 ElasticSearch 和 Kibana 进行日志管理
后端·elasticsearch·架构
白露与泡影8 小时前
复杂系统如何架构?
架构
gikod8 小时前
【笔记】架构上篇Day6 法则四:为什么要顺应技术的生命周期?
大数据·人工智能·笔记·架构
第八学期8 小时前
用Ansible Roles重构LNMP架构(Linux+Nginx+Mariadb+PHP)
linux·nginx·重构·架构·ansible·自动化运维
m0_663234019 小时前
计算机网络ENSP课设--三层架构企业网络
网络·计算机网络·架构
程序猿进阶11 小时前
可视化逻辑表达式编辑器
java·spring boot·后端·面试·性能优化·架构·编辑器