当前位置:   article > 正文

第十三节 SPI 子系统–oled 屏实验_spi驱动oled的代码

spi驱动oled的代码

本章我们以spi 接口的oled 显示屏为例讲解spi 驱动程序的编写。

本章主要分为五部分内容。

  • 第一部分,spi 驱动基本知识,简单讲解SPI 物理总线、时序和模式。
  • 第二部分,分析spi 驱动框架和后续使用到的核心数据结构。
  • 第三部分,分析spi 总线驱动和spi 核心层以及spi 控制器。
  • 第四部分,编写驱动时会使用到的函数,如同步、异步等。
  • 第五部分,实验,spi 驱动oled 液晶屏。

spi 物理总线

STM32MP157 的spi 总线都可以挂载多个设备,spi 支持标准的一主多从,全双工半双工通信等。

其中四根控制线包括:

  • SCK:时钟线,数据收发同步
  • MOSI:数据线,主设备数据发送、从设备数据接收
  • MISO:数据线,从设备数据发送,主设备数据接收
  • NSS:片选信号线

在这里插入图片描述
i2c 通过i2c 设备地址选择通信设备,而spi 通过片选引脚选中要通信的设备。

STM32MP157 的spi 接口支持有多个片选引脚,连接多个SPI 从设备,当然也可以使用外部GPIO扩展SPI 设备的数量,这样一个spi 接口可连接的设备数由片选引脚树决定。

  • 如果使用spi 接口提供的片选引脚,spi 总线驱动会处理好什么时候选spi 设备。
  • 如果使用外部GPIO 作为片选引脚需要我们在spi 设备驱动中设置什么时候选中spi。(或者在配置SPI 时指定使用的片选引脚)。

通常情况下无特殊要求我们使用spi 接口提供的片选引脚。

spi 时序

在这里插入图片描述

  • 起始信号:NSS 信号线由高变低

  • 停止信号:NSS 信号由低变高

  • 数据传输:在SCK 的每个时钟周期MOSI 和MISO 同时传输一位数据,高/低位传输没有硬性规定

    – 传输单位:8 位或16 位

    – 单位数量:允许无限长的数据传输

spi 通信模式

总线空闲时SCK 的时钟状态以及数据采样时刻

在这里插入图片描述

  • 时钟极性CPOL:指SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号:

    – CPOL=0 时,SCK 在空闲状态时为低电平

    – CPOL=1 时,SCK 在空闲状态时为高电平

  • 时钟相位CPHA:数据的采样的时刻:

    – CPHA=0 时,数据在SCK 时钟线的“奇数边沿”被采样

    – CPHA=1 时,数据在SCK 时钟线的“偶数边沿”被采样

在这里插入图片描述

如上图所示:

SCK 信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。CPHA=0,数据在SCK 时钟线的“奇数边沿”被采样,当CPOL=0 的时候,时钟的奇数边沿是上升沿,当CPOL=1的时候,时钟的奇数边沿是下降沿。

spi 驱动框架

spi 设备驱动和i2c 设备驱动非常相似,可对比学习。这一小节主要介绍spi 驱动框架以及主要的结构体。

在这里插入图片描述

如框架图所示,spi 可分为spi 总线驱动和spi 设备驱动。spi 总线驱动已经由芯片厂商提供,我们适当了解其实现机制。而spi 设备驱动由我们自己编写,则需要明白其中的原理。spi 设备驱动涉及到字符设备驱动、SPI 核心层、SPI 主机驱动,具体功能如下。

  • SPI 核心层:提供SPI 控制器驱动和设备驱动的注册方法、注销方法、SPI 通信硬件无关接口函数。
  • SPI 主机驱动:主要包含SPI 硬件体系结构中适配器(spi 控制器) 的控制,用于产生SPI 读写时序。
  • SPI 设备驱动:通过SPI 主机驱动与CPU 交换数据。

关键数据结构

这里对整个spi 驱动框架所涉及的关键数据结构进行整理,可先跳过,后续代码中遇到这些数据结构时再回来看详细定义。

spi_master

spi_master 会在SPI 主机驱动中使用到。spi_controller 实际是一个宏,指向spi_controller 结构体。

spi_controller

部分成员变量已经被省略,下面是spi_controller 关键成员变量:

列表1: spi_controller 结构体(内核源码/include/linux/spi/spi.h)

struct spi_controller {
	struct device 			dev;
	...
	struct list_head		list;
	s16	 					bus_num;
	u16 					num_chipselect;
	...
	struct spi_message 		*cur_msg;
	...
	int 					(*setup)(struct spi_device *spi);
	int 					(*transfer)(struct spi_device *spi,
	struct 					spi_message *mesg);
	void 					(*cleanup)(struct spi_device *spi);
	struct kthread_worker 	kworker;
	struct task_struct 		*kworker_task;
	struct kthread_work		pump_messages;
	struct list_head 		queue;
	struct spi_message 		*cur_msg;

	...
	int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi,struct spi_transfer *transfer);
	int (*prepare_transfer_hardware)(struct spi_controller *ctlr);
	int (*transfer_one_message)(struct spi_controller *ctlr,struct spi_message *mesg);
	void (*set_cs)(struct spi_device *spi, bool enable);
	...
	int 					*cs_gpios;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

spi_controller 中包含了各种函数指针,这些函数指针会在SPI 核心层中被使用。

  • list:链表节点,芯片可能有多个spi 控制器
  • bus_num: spi 控制器编号
  • num_chipselect: spi 片选信号的个数,对不同的从设备进行区分
  • cur_msg:spi_message 结构体类型,我们发送的信息都会被封装在这个结构体中。cur_msg,当前正带处理的消息队列
  • transfer:用于把数据加入控制器的消息队列中
  • cleanup:当spi_master 被释放的时候,完成清理工作
  • kworker:内核线程工人,spi 可以使用异步传输方式发送数据
  • pump_messages:具体传输工作
  • queue:所有等待传输的消息队列挂在该链表下
  • transfer_one_message:发送一个spi 消息,类似IIC 适配器里的algo->master_xfer,产生spi通信时序
  • cs_gpios:记录spi 上具体的片选信号。

spi_driver 结构体

列表2: spi_driver 结构体(内核源码/include/linux/spi/spi.h)

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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • id_table:用来和spi 进行配对。
  • .probe: spi 设备和spi 驱动匹配成功后,回调该函数指针

可以看到spi 设备驱动结构体和我们之前讲过的i2c 设备驱动结构体i2c_driver 、平台设备驱动结构体platform_driver 拥有相同的结构,用法也相同。

spi_device 设备结构体

在spi 驱动中一个spi 设备结构体代表了一个具体的spi 设备,它保存着这个spi 设备的详细信息,也可以说是配置信息。当驱动和设备匹配成功后(例如设备树节点)我们可以从.prob 函数的参数中得到spi_device 结构体。

  • dev: device 类型结构体。这是一个设备结构体,我们把它称为spi 设备结构体、i2c 设备结构体、平台设备结构体都是“继承”自设备结构体。它们根据各自的特点添加自己的成员,spi 设备添加的成员就是后面要介绍的成员
  • controller:当前spi 设备挂载在那个spi 控制器
  • master:spi_master 类型的结构体。在总线驱动中,一个spi_master 代表了一个spi 总线,这个参数就是用于指定spi 设备挂载到那个spi 总线上
  • max_speed_hz:指定SPI 通信的最大频率
  • chip_select: spi 总选用于区分不同SPI 设备的一个标号,不要误以为他是SPI 设备的片选引脚。指定片选引脚的成员在下面
  • bits_per_word:指定SPI 通信时一个字节多少位,也就是传输单位
  • mode: SPI 工作模式,工作模式如以上代码中的宏定义。包括时钟极性、位宽等等,这些宏定义可以使用或运算“|”进行组合,这些宏定义在SPI 协议中有详细介绍,这里不再赘述
  • irq:如果使用了中断,它用于指定中断号
  • cs_gpio:片选引脚。在设备树中设置了片选引脚,驱动和设别树节点匹配成功后自动获取片选引脚,我们也可以在驱动总通过设置该参数自定义片选引脚
  • statistics:记录spi 名字,用来和spi_driver 进行配对。

spi_transfer 结构体

在spi 设备驱动程序中,spi_transfer 结构体用于指定要发送的数据,后面称为传输结构体:

列表3: spi_transfer 结构体

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_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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

传输结构体的成员较多,需要我们自己设置的很少,这里只介绍我们常用的配置项。

  • tx_buf:发送缓冲区,用于指定要发送的数据地址。
  • rx_buf:接收缓冲区,用于保存接收得到的数据,如果不接收不用设置或设置为NULL.
  • len:要发送和接收的长度,根据SPI 特性发送、接收长度相等。
  • tx_dma、rx_dma:如果使用了DAM, 用于指定tx 或rx DMA 地址。
  • bits_per_word: speed_hz,分别用于设置每个字节多少位、发送频率。如果我们不设置这些参数那么会使用默认的配置,也就是我初始化spi 是设置的参数。

spi_message 结构体

总的来说spi_transfer 结构体保存了要发送(或接收)的数据,而在SPI 设备驱动中数据是以“消息”的形式发送。spi_message 是消息结构体,我们把它称为消息结构体,发送一个消息分四步,依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。

spi_message 结构体定义如下所示:

列表4: spi_message 结构体

struct spi_message {
	struct list_head transfers;

	struct spi_device *spi;

	unsigned is_dma_mapped:1;

/* REVISIT: we might want a flag affecting the behavior of the
* last transfer ... allowing things like "read 16 bit length L"
* immediately followed by "read L bytes". Basically imposing
* a specific message scheduling algorithm.
*
* Some controller drivers (message-at-a-time queue processing)
* could provide that as their default scheduling algorithm. But
* others (with multi-message pipelines) could need a flag to
* tell them about such special cases.
*/

	/* completion is reported through a callback */
	void (*complete)(void *context);
	void *context;
	unsigned frame_length;
	unsigned actual_length;
	int status;

/* for optional use by whatever driver currently owns the
* spi_message ... between calls to spi_async and then later
* complete(), that's the spi_master controller driver.
*/
	struct list_head queue;
	void *state;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

spi_message 结构体成员我们比较陌生,如果我们不考虑具体的发送细节我们可以不用了解这些成员的含义,因为spi_message 的初始化以及“绑定”spi_transfer 传输结构体都是由内核函数实现。唯一要说明的是第二个成员“spi”,它是一个spi_device 类型的指针,我们讲解spi_device 结构体时说过,一个spi 设备对应一个spi_device 结构体,这个成员就是用于指定消息来自哪个设备。

SPI 核心层

spi 总线注册

linux 系统在开机的时候就会执行,自动进行spi 总线注册。

列表5: spi 总线注册(内核源码/drivers/spi/spi.c)

static int __init spi_init(void)
{
	int status;
	...
	status = bus_register(&spi_bus_type);
	...
	status = class_register(&spi_master_class);
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

当总线注册成功之后,会在sys/bus 下面生成一个spi 总线,然后在系统中新增一个设备类,sys/class/目录下会可以找到spi_master 类。

spi 总线定义

spi_bus_type 总线定义,会在spi 总线注册时使用。

列表6: spi 总线定义

struct bus_type spi_bus_type = {
	.name 		= "spi",
	.dev_groups = spi_dev_groups,
	.match 		= spi_match_device,
	.uevent 	= spi_uevent,
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

.match 函数指针,设定了spi 设备和spi 驱动的匹配规则,具体如下spi_match_device。

spi_match_device() 函数

列表7: spi 总线注册(内核源码/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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

函数提供了四种匹配方式,设备树匹配方式和acpi 匹配方式以及id_table 匹配方式,如果前面三种都没有匹配成功,则通过设备名进行配对。

spi 控制器驱动

我们使用的STM32MP157 芯片有6 个spi 控制器,对应的设备树存在6 个节点

列表8: spi5 设备树节点(内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)

spi5: spi@44009000 {
			#address-cells = <1>;
			#size-cells = <0>;
			compatible = "st,stm32h7-spi";
			reg = <0x44009000 0x400>;
			interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
			clocks = <&rcc SPI5_K>;
			resets = <&rcc SPI5_R>;
			dmas = <&dmamux1 85 0x400 0x01>,
				<&dmamux1 86 0x400 0x01>;
			dma-names = "rx", "tx";
			power-domains = <&pd_core>;
			status = "disabled";
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

compatible 属性值与主机驱动匹配; 在STM32MP157 的SPI 主机驱动文件为内核源码
drivers/spi/spi-stm32.c 中可以找到:

列表9: (内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)

static const struct of_device_id stm32_spi_of_match[] = {
{ .compatible = "st,stm32h7-spi", },
	{},
};
  • 1
  • 2
  • 3
  • 4

reg 为spi5 寄存器组相关的起始地址为0x44009000,寄存器长度为0x400,其他属性暂时不需要了解。

对应的控制器驱动源码中, module_platform_driver() 宏

列表10: module_platform_driver() 宏(内核源码/include/linux/platform_device.h)

#define module_platform_driver(__platform_driver) \
	module_driver(__platform_driver, platform_driver_register, \
			platform_driver_unregister)
  • 1
  • 2
  • 3

module_driver() 展开如下:

列表11: module_driver(内核源码/include/linux/device.h)

#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
	return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • __driver:即为module_platform_driver() 宏中的__platform_driver,也就是stm32_spi_driver。
  • __register:platform_driver_register
  • __unregister:platform_driver_unregister
  • ##VA_ARGS:可变参数

向函数传入platform_driver 结构体类型的spi_imx_driver 结构体变量,module_
platform_driver(stm32_spi_driver),间接调用platform_driver_register 和platform_driver_unregister,实现平台驱动函数的注册和注销。

stm32_spi_probe() 函数

stm32_spi_probe() 函数主要有如下功能:获取设备树节点信息,初始化spi 时钟、dma、中断等。

代码如下(部分被省略):

列表12: stm32_spi_probe 函数(内核源码/drivers/spi/spistm32.c)

static int stm32_spi_probe(struct platform_device *pdev)
{
	struct spi_master *master;
	struct stm32_spi *spi;
	struct resource *res;
	int i, ret, num_cs, cs_gpio;

	master = spi_alloc_master(&pdev->dev, sizeof(struct stm32_spi));
	if (!master) {
		dev_err(&pdev->dev, "spi master allocation failed\n");
		return -ENOMEM;
	}
	platform_set_drvdata(pdev, master);

	spi = spi_master_get_devdata(master);
	spi->dev = &pdev->dev;
	spi->master = master;

	......

	spi->irq = platform_get_irq(pdev, 0);
	if (spi->irq <= 0) {
		ret = spi->irq;
		if (ret != -EPROBE_DEFER)
			dev_err(&pdev->dev, "failed to get irq: %d\n", ret);
		goto err_master_put;
	}
	ret = devm_request_threaded_irq(&pdev->dev, spi->irq, NULL,
					stm32_spi_irq, IRQF_ONESHOT,
					pdev->name, master);
	if (ret) {
		dev_err(&pdev->dev, "irq%d request failed: %d\n", spi->irq,
			ret);
		goto err_master_put;
	}
	......

	spi->dma_tx = dma_request_slave_channel(spi->dev, "tx");
	if (!spi->dma_tx)
		dev_warn(&pdev->dev, "failed to request tx dma channel\n");
	else
		master->dma_tx = spi->dma_tx;

	spi->dma_rx = dma_request_slave_channel(spi->dev, "rx");
	if (!spi->dma_rx)
		dev_warn(&pdev->dev, "failed to request rx dma channel\n");
	else
		master->dma_rx = spi->dma_rx;

	if (spi->dma_tx || spi->dma_rx)
		master->can_dma = stm32_spi_can_dma;

	pm_runtime_set_active(&pdev->dev);
	pm_runtime_enable(&pdev->dev);

	........

	ret = spi_register_master(master);
	if (ret) {
		dev_err(&pdev->dev, "spi master registration failed: %d\n",
			ret);
		goto err_dma_release;
	}

	dev_info(&pdev->dev, "driver initialized\n");

	return 0;
	.....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

在这里插入图片描述

  • 第2-7 行:声明一些必要的变量和结构体,相关结构体的关系如上所示;
  • 第8-17 行:申请内存,实例化master;
  • 第21-35 行:获取中断号,设置中断函数;
  • 第38-54 行:设置DMA 和运行时电源管理;
  • 第58 行:向SPI 子系统注册master 控制器。

spi 设备驱动

spi 总线驱动,由硬件供应商提供,我们只需要了解,学习其原理就行。下面涉及的函数,我们将会在spi 设备驱动中使用。

spi 设备的注册和注销函数分别在驱动的入口和出口函数中调用,这与平台设备驱动、i2c 设备驱动相同,

spi 设备注册和注销函数如下:

列表13: spi 设备注册和注销函数

int spi_register_driver(struct spi_driver *sdrv)
static inline void spi_unregister_driver(struct spi_driver *sdrv)
  • 1
  • 2

对比i2c 设备的注册和注销函数,不难发现把“spi”换成“i2c”就是i2c 设备的注册和注销函数
了,并且用法相同。

参数

  • spi spi_driver 类型的结构体(spi 设备驱动结构体),一个spi_driver 结构体就代表了一个spi设备驱动

返回值

  • 成功: 0
  • 失败:其他任何值都为错误码

spi_setup() 函数

函数设置spi 设备的片选信号、传输单位、最大传输速率等,函数中调用spi 控制器的成员controller->setup(),也就是master->setup, 在函数stm32_spi_probe() 中我们将stm32_spi_setup 赋予该结构体。

列表14: spi_setup 函数(内核源码/drivers/spi/spi.c)

int spi_setup(struct spi_device *spi)
  • 1

参数

  • spi spi_device spi 设备结构体

返回值

  • 成功: 0
  • 失败:其他任何值都为错误码

spi_message_init() 函数

初始化spi_message

列表15: spi_message_init 函数(内核源码/include/linux/spi/spi.h)

static inline void spi_message_init(struct spi_message *m)
{
	memset(m, 0, sizeof *m);
	spi_message_init_no_memset(m);
}
  • 1
  • 2
  • 3
  • 4
  • 5

参数

  • m spi_message 结构体指针,spi_message 结构体定义和介绍可在前面关键数据结构中找到。

返回值:无。

spi_message_add_tail() 函数

列表16: spi_message_init 函数(内核源码/include/linux/spi/spi.h)

static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{
	list_add_tail(&t->transfer_list, &m->transfers);
}
  • 1
  • 2
  • 3
  • 4

这个函数很简单就是将将spi_transfer 结构体添加到spi_message 队列的末尾。

spi 同步与互斥

spi_message 通过成员变量queue 将一系列的spi_message 串联起来,第一个spi_message 挂在structlist_head queue 下面spi_message 还有struct list_head transfers 成员变量,transfer 也是被串联起来的,如下图所示。

在这里插入图片描述

SPI 同步传输数据

阻塞当前线程进行数据传输,spi_sync() 内部调用__spi_sync() 函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。

列表17: spi_sync() 函数(内核源码/drivers/spi/spi.c)

int spi_sync(struct spi_device *spi, struct spi_message *message)
{
	int ret;

	mutex_lock(&spi->controller->bus_lock_mutex);
	ret = __spi_sync(spi, message);
	mutex_unlock(&spi->controller->bus_lock_mutex);

	return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

__spi_sync() 函数实现如下:

列表18: __spi_sync() 函数(内核源码/drivers/spi/spi.c)

static int __spi_sync(struct spi_device *spi, struct spi_message *message)
{
	int status;
	struct spi_controller *ctlr = spi->controller;
	unsigned long flags;

	status = __spi_validate(spi, message);
	if (status != 0)
		return status;

	message->complete = spi_complete;
	message->context = &done;
	message->spi = spi;
	...
	if (ctlr->transfer == spi_queued_transfer) {
		spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);

	trace_spi_message_submit(message);

	status = __spi_queued_transfer(spi, message, false);

	spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
	} else {
		status = spi_async_locked(spi, message);
	}


	if (status == 0) {
		...
		wait_for_completion(&done);
		status = message->status;
	}
	message->context = NULL;
	return status;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 第7-9 行:函数内部首先调用__spi_validate 对spi 各个通信参数进行校验
  • 第11-13 行:对message 结构体进行初始化,其中第11 行,当消息发送完毕后,spi_complete回调函数将被执行。
  • 第30 行:阻塞当前线程,当message 发送完成时结束阻塞。

SPI 异步传输数据

列表19: spi_async() 函数(内核源码/drivers/spi/spi.c)

int spi_async(struct spi_device *spi, struct spi_message *message)
{
	...
	ret = __spi_async(spi, message);
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在驱动程序中调用async 时不会阻塞当前进程,只是把当前message 结构体添加到当前spi 控制器成员queue 的末尾。然后在内核中新增加一个工作,这个工作的内容就是去处理这个message 结构体。

列表20: __spi_async() 函数(内核源码/drivers/spi/spi.c)

static int __spi_async(struct spi_device *spi, struct spi_message *message)
{
	struct spi_controller *ctlr = spi->controller;
	...
	return ctlr->transfer(spi, message);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

oled 屏幕驱动实验

spi_oled 驱动和我们上一节介绍的i2c_mpu6050 设备驱动非常相似,可对比学习,推荐先学习i2c_mpu6050 驱动。

硬件介绍

在oled 驱动中我们使用spi5,可以通过STM32MP157 的芯片手册查到,spi 使用到的引脚如下。

在这里插入图片描述

在我们实验中spi5 使用原理图的QSPI_IO0-QSPI_IO3 引脚,oled 屏和spi 引脚对应入下。

OLED 显示屏引脚芯片引脚说明开发板(排针丝印)
MOSIPF9MOSI 引脚SPI_IO1
未使用PF8MISO 引脚SPI_IO0
CLKPF7SPI 时钟引脚SPI_IO2
D/CPF6数据、命令控制引脚SPI_IO3
CSPB6片选引脚SPI_NCS
GND电源-GND
VCC电源+3.3V

设备树插件书写格式不变,我们重点讲解spi_oled 设备节点。

列表21: spi_oled 设备树插件(linux_driver/SPI_OLED/stm-fire-spi5-oled-overlay.dts)

/{
	fragment@0{
		target=<&spi5>;
		__overlay__{
			#address-cells = <1>;
			#size-cells = <0>;
			pinctrl-names = "default", "sleep";
			pinctrl-0 = <&spi5_pins_a>;
			pinctrl-1 = <&spi5_sleep_pins_a>;
			cs-gpios = <&gpiob 6 0>;
			status = "okay";

			spi_oled@0 {
				compatible = "fire,spi_oled";
				spi-max-frequency = <10000000>;
				d_c_control_pin = <&gpiof 6 0>;
				reg = <0>;
			};
		};
	};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 第3 行: 向spi5 节点追加内容
  • 第7-9 行: 指定spi5 使用的pinctrl 节点,也就是说指定spi5 要使用的引脚, 详细描述如下:

列表22: 设备树pinctrl 描述(内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)

/{
	......
	spi5_pins_a: spi5-0 {
					pins1 {
						pinmux = <STM32_PINMUX('F', 7, AF5)>, /* SPI5_SCK */
								<STM32_PINMUX('F', 9, AF5)>; /* SPI5_MOSI */
						bias-disable;
						drive-push-pull;
						slew-rate = <1>;
					};

					pins2 {
						pinmux = <STM32_PINMUX('F', 8, AF5)>; /* SPI5_MISO */
						
						bias-disable;
					};
				};

	spi5_sleep_pins_a: spi5-sleep-0 {
					pins {
						pinmux = <STM32_PINMUX('F', 7, ANALOG)>, /* SPI5_SCK */
								<STM32_PINMUX('F', 8, ANALOG)>, /* SPI5_MISO */
								<STM32_PINMUX('F', 9, ANALOG)>; /* SPI5_MOSI */
					};
				};

	........
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 第10 行: 指定使用的片选引脚,我们这里使用的是PB6。
  • 第13 行: 向spi5 节点追加spi_oled 子节点
  • 第15 行: 设置SPI 传输的最大频率,
  • 第16 行: 指定spi_oled 使用的D/C 控制引脚,在驱动程序中会控制该引脚设置发送的是命令还是数据。
  • 第17 行: 设置reg 属性为0, 表示spi_oled 连接到spi5 的通道0

向pinctrl 子系统添加引脚具体内容可参考GPIO 子系统章节,设备树的几个引脚与spi_oled 显示屏引脚对应关系、引脚的功能、以及在开发板上的位置如前面表格所示。需要注意的是spi_oled显示屏没有MISO 引脚,直接空出即可,spi_oled 显示屏需要一个额外的引脚连接D/C, 用于控制spi 发送的是数据还是控制命令(高电平是数据,低电平是控制命令)。

实验代码讲解

spi_oled 驱动使用设备树插件方式开发, 主要工作包三部分内容。

  • 第一,编写spi_oled 的设备树插件(硬件部分已介绍),
  • 第二,编写spi_oled 驱动程序,包含驱动的入口、出口函数实现,.prob 函数实现,file_operations函数集实现。
  • 第三,编写简单测试应用程序。

spi_oled 的驱动结构和上一章的i2c_mpu6050 完全相同。这里不再赘述,直接讲解实现代码。如下所示。

驱动的入口和出口函数实现

驱动入口和出口函数与I2C_mpu6050 驱动相似,只是把i2c 替换为spi, 源码如下所示。

列表23: 驱动入口函数实现(linux_driver/SPI_OLED/spi_oled.c)

/* 指定ID 匹配表*/
static const struct spi_device_id oled_device_id[] = {
	{"fire,spi_oled", 0},
	{}};

/* 指定设备树匹配表*/
static const struct of_device_id oled_of_match_table[] = {
	{.compatible = "fire,spi_oled"},
	{}};

/*spi 总线设备结构体*/
struct spi_driver oled_driver = {
	.probe = oled_probe,
	.remove = oled_remove,
	.id_table = oled_device_id,
	.driver = {
			.name = "spi_oled",
			.owner = THIS_MODULE,
			.of_match_table = oled_of_match_table,
	},
};

/*
* 驱动初始化函数
*/
static int __init oled_driver_init(void)
{
	int error;
	pr_info("oled_driver_init\n");
	error = spi_register_driver(&oled_driver);
	return error;
}

/*
* 驱动注销函数
*/
static void __exit oled_driver_exit(void)
{
	pr_info("oled_driver_exit\n");
	spi_unregister_driver(&oled_driver);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 第2-9 行:这里定义了两个匹配表,第一个是传统的匹配表(可省略)。第二个是和设备树节点匹配的匹配表,保证与设备树节点.compatible 属性设定值相同即可。
  • 第12-21 行:定义spi_driver 类型结构体。该结构体可类比i2c_driver 和platform_driver。
  • 第26-41 行:驱动的入口和出口函数,在入口函数只需要注册一个spi 驱动,在出口函数中注销它

.prob 函数实现

在.prob 函数中完成两个主要工作,第一,初始化spi,第二,申请一个字符设备。

列表24: .prob 函数实现(linux_driver/SPI_OLED/spi_oled.c)

static int oled_probe(struct spi_device *spi)
{
	printk(KERN_EMERG "\t match successed \n");
	/* 获取spi_oled 设备树节点*/
	oled_device_node = of_find_node_by_path("/soc/spi@44009000/spi_oled@0");
	if (oled_device_node == NULL)
	{
		printk(KERN_EMERG "\t get spi_oled@0 failed! \n");
	}
	/* 获取oled 的D/C 控制引脚并设置为输出,默认高电平*/
	oled_control_pin_number = of_get_named_gpio(oled_device_node, "d_c_control_pin", 0);
	gpio_direction_output(oled_control_pin_number, 1);
	/* 初始化spi*/
	oled_spi_device = spi;
	oled_spi_device->mode = SPI_MODE_0;
	oled_spi_device->max_speed_hz = 2000000;
	spi_setup(oled_spi_device);
	/* 注册字符设备*/
	ret = alloc_chrdev_region(&oled_devno, 0, DEV_CNT, DEV_NAME);
	if (ret < 0)
	{
		printk("fail to alloc oled_devno\n");
		goto alloc_err;
	}
	/* 关联字符设备结构体cdev 与文件操作结构体file_operations*/
	oled_chr_dev.owner = THIS_MODULE;
	cdev_init(&oled_chr_dev, &oled_chr_dev_fops);
	/* 添加设备至cdev_map 散列表中*/
	ret = cdev_add(&oled_chr_dev, oled_devno, DEV_CNT);
	if (ret < 0)
	{
		printk("fail to add cdev\n");
		goto add_err;
	}
	/* 创建类*/
	class_oled = class_create(THIS_MODULE, DEV_NAME);
	/* 创建设备DEV_NAME 指定设备*/
	device_oled = device_create(class_oled, NULL, oled_devno, NULL, DEV_NAME);

	...

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

.prob 函数介绍如下:

  • 第6-10 行:根据设备树节点路径获取节点。
  • 第13-14 行:获取spi_oled 的D/C 控制引脚。并设置为高电平。
  • 第17 行:.prob 函数传回的spi_device 结构体,根据之前讲解,该结构体代表了一个spi 设备,我们通过它配置SPI, 这里设置的内容将会覆盖设备树节点中设置的内容。
  • 第18 行:设置SPI 模式为SPI_MODE_0。
  • 第19 行:设置最高频率为2000000,设备树中也设置了该属性,则这里设置的频率为最终值。
  • 第23-46 行:注册字符设备、创建设备

字符设备操作函数集实现

字符设备操作函数集是驱动对外的接口,我们要在这些函数中实现对spi_oled 的初始化、写入、关闭等等工作。这里共实现三个函数,.open 函数用于实现spi_oled 的初始化,.write 函数用于向spi_oled 写入显示数据,.release 函数用于关闭spi_oled。

.open 函数实现

在open 函数中完成spi_oled 的初始化,代码如下:

列表25: .open 函数实现(linux_driver/SPI_OLED/spi_oled.c)

/* 字符设备操作函数集,open 函数实现*/
static int oled_open(struct inode *inode, struct file *filp)
{
	spi_oled_init(); //初始化显示屏
	return 0;
}

/*oled 初始化函数*/
void spi_oled_init(void)
{
11 /* 初始化oled*/
12 oled_send_commands(oled_spi_device, oled_init_data, sizeof(oled_init_
,→data));
13
14 /* 清屏*/
15 oled_fill(0x00);
16 }

static int oled_send_command(struct spi_device *spi_device, u8 *commands, u16 lenght)
{
	int error = 0;
	struct spi_message *message; //定义发送的消息
	struct spi_transfer *transfer; //定义传输结构体

	/* 申请空间*/
	message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
	transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
	/* 设置D/C 引脚为低电平*/
	gpio_direction_output(oled_control_pin_number, 0);
	/* 填充message 和transfer 结构体*/
	transfer->tx_buf = commands;
	transfer->len = lenght;
	spi_message_init(message);
	spi_message_add_tail(transfer, message);
	error = spi_sync(spi_device, message);

	kfree(message);
	kfree(transfer);
	if (error != 0)
	{
		printk("spi_sync error! \n");
		return -1;
	}
	return error;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

如上代码所示,open 函数只调用了自定义spi_oled_init 函数,在spi_oled_init 函数函数最终会调用oled_send_command 函数初始化spi_oled,然后调用清屏函数。这里主要讲解oled_send_command函数:

  • 第21、22 行:定义spi_message 结构体和spi_transfer 结构体。
  • 第25、26 行:为节省内核栈空间这里使用kzalloc 为它们分配空间,这两个结构体大约占用100 字节,推荐这样做。
  • 第28 行:设置D/C 引脚为低电平,前面说过,spi_oled 的D/C 引脚用于控制发送的命令或数据,低电平时表示发送的是命令。
  • 第30-34 行:这里就是我们之前讲解的发送流程依次为初始化spi_transfer 结构体指定要发送的数据、初始化消息结构体、将消息结构体添加到队尾部、调用spi_sync 函数执行同步发送。
  • 第36-43 行:释放空间。

.write 函数实现

.write 函数用于接收来自应用程序的数据,并显示这些数据。函数实现如下所示:

列表26: .write 函数实现(linux_driver/SPI_OLED/spi_oled.c)

/* 字符设备操作函数集,.write 函数实现*/
static int oled_write(struct file *filp, const char __user *buf, size_t cnt,loff_t *off)
{
	int copy_number=0;
	/* 申请内存*/
	oled_display_struct *write_data;
	write_data = (oled_display_struct*)kzalloc(cnt, GFP_KERNEL);
	copy_number = copy_from_user(write_data, buf,cnt);
	oled_display_buffer(write_data->display_buffer, write_data->x, write_ata->y, write_data->length);
	/* 释放内存*/
	kfree(write_data);
	return 0;
}

static int oled_display_buffer(u8 *display_buffer, u8 x, u8 y, u16 length)

/* 数据发送结构体*/
typedef struct oled_display_struct
{
	u8 x;
	u8 y;
	u32 length;
	u8 display_buffer[];
}oled_display_struct;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

代码介绍如下:

  • 第2 行:.write 函数,我们重点关注两个参数buf 保存来自应用程序的数据地址,我们需要把这些数据拷贝到内核空间才能使用,参数cnt 指定数据长度。
  • 第6 行:定义oled_display_struct 结构体并保存来自用户空间的数据。
  • 第7-8 行:使用kzalloc 为oled_display_struct 结构体分配空间,因为在应用程序中使用相同的结构体,所以这里直接根据参数“cnt”分配空间,分配成功后执行“copy_from_user”即可。
  • 第9 行:调用自定义函数oled_display_buffer 显示数据。
  • 第11 行:释放空间
  • 第15 行:函数原型如第四部分所示,参数display_buffer 指定要显示的点阵数据x、y 用于指定显示起始位置,length 指定显示长度。具体函数实现也很简单,这里不再赘述。
  • 第25 行:oled_display_struct 结构体是自定义的一个结构体。它是一个可变长度结构体,参数x 、y 用于指定数据显示位置,参数length 指定数据长度,柔性数组display_buffer[] 用于保存来自用户空间的显示数据。

.release 函数实现

.release 函数功能仅仅是向spi_oled 显示屏发送关闭显示命令,源码如下:

列表27: .release 函数实现(linux_driver/SPI_OLED/spi_oled.c)

/* 字符设备操作函数集,.release 函数实现*/
static int oled_release(struct inode *inode, struct file *filp)
{
	oled_send_command(oled_spi_device, 0xae);//关闭显示
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
oled 测试应用程序实现

测试应用程序主要工作是实现oled 显示屏实现刷屏、显示文字、显示图片。

测试程序需要用到字符以及图片的点阵数据保存在oled_code_table.c 文件,为方便管理我们编写了一个简单makefile 文件方便我们编译程序。

在这里插入图片描述

makefile 文件如下所示:

列表28: Makefile(linux_driver/SPI_OLED/test_app/Makefile.c)

out_file_name = "test_app"

all: test_app.c oled_code_table.c
	arm-linux-gnueabihf-gcc $^ -o $(out_file_name)

.PHONY: clean
clean:
	rm $(out_file_name)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Makefile 很简单,就不加以说明。

下面是我们的测试程序源码。如下所示:

列表29: 测试应用程序Makefile(linux_driver/SPI_OLED/test_app/test_app.c)

/* 点阵数据*/
extern unsigned char F16x16[];
extern unsigned char F6x8[][6];
extern unsigned char F8x16[][16];
extern unsigned char BMP1[];

int main(int argc, char *argv[])
{
	int error = -1;
	/* 打开文件*/
	int fd = open("/dev/spi_oled", O_RDWR);
	if (fd < 0)
	{
		printf("open file : %s failed !\n", argv[0]);
		return -1;
	}

	while(1)
	{
		/* 显示图片*/
		show_bmp(fd, 0, 0, BMP1, X_WIDTH*Y_WIDTH/8);

		sleep(2);
		oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏

		oled_show_F16X16_letter(fd,0, 0, F16x16, 4); //显示汉字
		oled_show_F8X16_string(fd,0,2,"F8X16:THIS IS SPI TEST APP");
		oled_show_F6X8_string(fd, 0, 6,"F6X8:THIS IS SPI TEST APP");

		sleep(2);
		oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏

		oled_show_F8X16_string(fd,0,0,"Testing is completed");

		sleep(2);
		oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏
	}

	/* 关闭文件*/
	error = close(fd);
	if(error < 0)
	{
		printf("close file error! \n");
	}

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

测试程序很简单,完整代码请参考配套例程,结合代码简单介绍如下:

  • 第2-5 行:测试程序要用到的点阵数据,我们显示图片、汉字之前都要把它们转化为点阵数据。野火spi_oled 模块配套资料提供有转换工具以及使用说明。
  • 第11 行:打开spi_oled 的设备节点,这个根据自己的驱动而定,我们使用的驱动源码就是这个路径。
  • 第18 行:显示图片测试,这里需要说明的是由于测试程序不那么完善,图片显示起始位置x 坐标应当设置为0,这样在循环显示时才不会乱。显示长度应当为显示屏的像素数除以8,因为每个字节8 位,这8 位控制8 个像素点。
  • 第22-25 行:测试显示汉字和不同规格的字符。
  • 第28-33 行:显示测试结束提示语。

实验准备

我们编写的设备树插件在linux_driver/SPI_OLED/stm-fire-spi5-oled-overlay.dts 将其拷贝到内核源码的arch/arm/boot/dts/overlays/下,在内核源码根目录下执行命令:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
  • 1
  • 2

在“内核源码的arch/arm/boot/dts/overlays/”目录下,会生成stm-fire-spi5-oled.dtbo,stm-fire-spi5-oled.dtbo 就是oled 屏的设备树插件

将linux_driver/SPI_OLED/ 拷贝到内核源码同级目录,执行里面的MakeFile,生成spi_oled.ko。

在这里插入图片描述

将linux_driver/SPI_OLED/test_app 目录中执行里面的MakeFile,生成test_app。

在这里插入图片描述

程序运行结果

将前面生成的设备树插件、驱动程序、应用程序通过scp 等方式拷贝到开发板。

将设备树插件拷贝到/usr/lib/linux-image-4.19.94-stm-r1/overlays, 在/boot/uEnv.txt 中添加dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-spi5-oled.dtbo

在这里插入图片描述
加载驱动陈程序sudo insmod spi_oled.ko。驱动程序打印match successed 和相关spi 信息

在这里插入图片描述

驱动加载成功后直接运行测试应用程序./test_app,正常情况下显示屏会显示设定的内容,如下所示。

在这里插入图片描述


参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/309450
推荐阅读
相关标签
  

闽ICP备14008679号