SPI驱动学习六(SPI_Master驱动程序)

目录

  • 前言
  • 一、SPI_Master驱动程序框架
    • [1. SPI传输概述](#1. SPI传输概述)
      • [1.1 数据组织方式](#1.1 数据组织方式)
      • [1.2 SPI控制器数据结构](#1.2 SPI控制器数据结构)
    • [2. SPI传输函数的两种方法](#2. SPI传输函数的两种方法)
      • [2.1 老方法](#2.1 老方法)
      • [2.2 新方法](#2.2 新方法)
  • 二、如何编写SPI_Master驱动程序
    • [1. 编写设备树](#1. 编写设备树)
    • [2. 编写驱动程序](#2. 编写驱动程序)
  • 三、SPI_Master驱动程序简单示例demo
    • [1. 使用老方法编写的SPI Master驱动程序](#1. 使用老方法编写的SPI Master驱动程序)
    • [2. 使用新方法编写的SPI Master驱动程序](#2. 使用新方法编写的SPI Master驱动程序)

前言

SPI 是"串行外设接口"的缩写,它在嵌入式系统中广泛使用,因为它是一个简单且高效的接口:基本上是一个多路复用的移位寄存器。它的三个信号线分别为时钟线(SCK,通常在 1-20 MHz 范围内)一个"主机输出从机输入"(MOSI)数据线一个"主机输入从机输出"(MISO)数据线。SPI 是一种全双工协议;每在MOSI线上移出一位(每时钟一位),MISO线上就会移入一位。这些位在去往和从系统内存传送的过程中会被组装成各种大小的字。一个额外的芯片选择线通常是低电平有效的(nCS);通常每个外设使用四个信号线,有时还会有一个中断线。

SPI 总线设施提供了一个通用接口,用于声明 SPI 总线和设备,按照标准 Linux 驱动模型进行管理,并执行输入/输出操作。目前,仅支持"主设备"侧接口,即Linux与SPI外围设备通信,自己不实现这样的外围设备。(支持实现 SPI 从设备的接口必然会有所不同。)

编程接口围绕两种类型的驱动程序和两种类型的设备构建。一个"控制器驱动程序"抽象了控制器硬件,可能是简单的 GPIO 引脚集,也可能是连接到 SPI 移位寄存器另一侧的双 DMA 引擎的一对 FIFO(以最大化吞吐量)。这种驱动程序在它们所在的总线(通常是平台总线)与 SPI 之间架起桥梁,并将其设备的 SPI 侧暴露为 struct spi_controller。SPI 设备是该主设备的子设备,以 struct spi_device 表示,并通过 struct spi_board_info 描述符构建,这些描述符通常由特定于板的初始化代码提供。一个 struct spi_driver 被称为"协议驱动程序",并通过正常的驱动模型调用与 spi_device 绑定。

I/O 模型是一个排队的消息集合。协议驱动程序提交一个或多个 struct spi_message 对象,这些对象会异步处理和完成(不过也有同步的封装)。消息由一个或多个 struct spi_transfer 对象构成,每个对象封装了一个全双工的 SPI 传输。由于不同芯片对 SPI 传输的比特有不同的使用策略,因此需要多种协议调节选项。

shell 复制代码
.. kernel-doc:: include/linux/spi/spi.h
   :internal:

.. kernel-doc:: drivers/spi/spi.c
   :functions: spi_register_board_info

.. kernel-doc:: drivers/spi/spi.c
   :export:

----------------来源于kernel/Documentation/driver-api/spi.rst

一、SPI_Master驱动程序框架

  • 参考内核源码: drivers\spi\spi.c

1. SPI传输概述

1.1 数据组织方式

使用SPI传输时,最小的传输单位是"spi_transfer",对于一个设备,可以发起多个spi_transfer,这些spi_transfer,会放入一个spi_message里。

  • spi_transfer:指定tx_buf、rx_buf、len
  • 同一个SPI设备的spi_transfer,使用spi_message来管理:
  • 同一个SPI Master下的spi_message,放在一个队列queue里:
  • 所以,反过来,SPI传输的流程是这样的:
    • 从spi_master的队列里取出每一个spi_message
      • 从spi_message的队列里取出一个spi_transfer
        • 处理spi_transfer

一个queue里可以有多个spi_message,一个spi_message可以有多个spi_transfer;

1.2 SPI控制器数据结构

参考内核文件:include\linux\spi\spi.h,Linux中使用spi_master结构体描述SPI控制器,有两套传输方法:

2. SPI传输函数的两种方法

c 复制代码
/**
 * struct spi_message - 一个包含多段SPI传输的事务
 * @transfers: 本次事务中传输段的列表
 * @spi: SPI设备,事务将被排队到该设备
 * @is_dma_mapped: 如果为真,调用者为每个传输缓冲区提供了DMA和CPU虚拟地址
 * @complete: 被调用以报告事务完成情况
 * @context: 当complete()被调用时传递给它的参数
 * @frame_length: 消息中的总字节数
 * @actual_length: 在所有成功段中传输的总字节数
 * @status: 零表示成功,否则为负的errno值
 * @queue: 由当前拥有消息的驱动程序使用
 * @state: 由当前拥有消息的驱动程序使用
 * @resources: 在处理SPI消息时用于资源管理
 *
 * spi_message用于执行一个原子序列的数据传输,每个传输段由一个spi_transfer结构表示。
 * 这个序列是"原子"的,意味着直到序列完成之前,任何其他spi_message都不能使用该SPI总线。
 * 在某些系统上,许多这样的序列可以作为单个编程的DMA传输执行。在所有系统上,这些消息都被排队,
 * 并且可能在其他设备的事务之后完成。发送到特定spi_device的消息总是以FIFO顺序执行。
 *
 * 提交spi_message(及其spi_transfers)到较低层的代码负责管理其内存。
 * 零初始化每个你没有显式设置的字段,以防止未来的API更新影响。在你提交消息及其传输后,
 * 直到其完成回调之前,忽略它们。
 */
struct spi_message {
    struct list_head transfers; // 传输段列表

    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; // 状态,成功为0,失败为负值

    // 由当前拥有spi_message的驱动程序可选使用
    struct list_head queue; // 队列
    void *state; // 状态信息

    // 处理SPI消息时的资源列表
    struct list_head resources;

    ANDROID_KABI_RESERVE(1); // 保留字段,用于Android KABI
};
c 复制代码
APP -> Driver -> spi_sync 函数
1. 从spi device找到spi_master
2. 把message放到spi_master的queue
3. scheduler work:
	a.从queue取出message
	b.启动传输
	c.等待传输完成
	d.传输完成触发中断,去唤醒等待传输完成的程序
4. 等待message传输完成;
c 复制代码
/**
 * spi_sync - 阻塞/同步SPI数据传输
 * @spi: 与之交换数据的设备
 * @message: 描述数据传输
 * Context: 可以睡眠
 *
 * 此调用仅可用于可以从允许睡眠的上下文中使用。睡眠是不可中断的,没有超时。
 * 低开销的控制器驱动程序可以直接DMA到消息缓冲区和从中DMA出来。
 *
 * 注意,SPI设备的片选信号在消息期间是激活的,然后通常在消息之间禁用。
 * 一些常用设备的驱动程序可能希望减少选择芯片的成本,通过在芯片被选中后保持选中状态,
 * 以期待下一条消息将发送到相同的芯片。(这可能会增加功耗使用。)
 *
 * 此外,调用者保证在该调用返回之前,不会释放与消息关联的内存。
 *
 * 返回: 成功时返回零,否则返回一个负的错误码。
 */
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
    int ret;

    // 锁定SPI总线,以确保数据传输的同步性
    mutex_lock(&spi->controller->bus_lock_mutex);
    // 执行实际的SPI同步传输操作
    ret = __spi_sync(spi, message);
    // 解锁SPI总线
    mutex_unlock(&spi->controller->bus_lock_mutex);

    return ret;
}
// 将spi_sync符号导出,允许其他模块使用
EXPORT_SYMBOL_GPL(spi_sync);
c 复制代码
/*
 * 函数__spi_sync用于在SPI设备上同步传输数据。
 * 它负责将SPI消息排队并等待传输完成。
 * 
 * 参数:
 * spi: 指向SPI设备结构的指针。
 * message: 指向SPI消息结构的指针,包含要传输的数据和配置。
 * 
 * 返回:
 * 传输操作的状态,0表示成功,非0表示错误代码。
 */
static int __spi_sync(struct spi_device *spi, struct spi_message *message)
{
	// 在栈上声明一个完成量,用于同步传输完成。
	DECLARE_COMPLETION_ONSTACK(done);
	int status;
	struct spi_controller *ctlr = spi->controller;
	unsigned long flags;

	// 验证SPI设备和消息的有效性。
	status = __spi_validate(spi, message);
	if (status != 0)
		return status;

	// 设置消息的完成回调和上下文。
	message->complete = spi_complete;
	message->context = &done;
	message->spi = spi;

	// 增加控制器和设备的spi_sync操作统计。
	SPI_STATISTICS_INCREMENT_FIELD(&ctlr->statistics, spi_sync);
	SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics, spi_sync);

	// 新方法,使用内核提供的transfer 函数
	// 如果不使用老的传输方法,将尝试在调用上下文中传输,需要特殊处理。
	if (ctlr->transfer == spi_queued_transfer) {
		// 锁定控制器的总线锁,并保存中断状态。
		spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);

		// 记录SPI消息提交的跟踪信息。
		trace_spi_message_submit(message);

		// 执行排队传输,不启用DMA。
		status = __spi_queued_transfer(spi, message, false);

		// 解锁控制器的总线锁,并恢复中断状态。
		spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
	} else {
		// 使用异步锁定方式传输。
		status = spi_async_locked(spi, message);
	}

	// 如果传输状态为0,表示成功启动传输,则继续处理。
	if (status == 0) {
		// 如果使用的是排队传输方式,尝试立即推送消息。
		if (ctlr->transfer == spi_queued_transfer) {
			// 增加立即同步传输的统计。
			SPI_STATISTICS_INCREMENT_FIELD(&ctlr->statistics, spi_sync_immediate);
			SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics, spi_sync_immediate);
			// 推送消息到SPI控制器。
			__spi_pump_messages(ctlr, false);
		}

		// 等待传输完成。
		wait_for_completion(&done);
		// 获取传输后的状态。
		status = message->status;
	}
	// 清空消息的上下文。
	message->context = NULL;
	// 返回传输操作的状态。
	return status;
}

2.1 老方法

老方法需要自己实现对queue的管理!

2.2 新方法

c 复制代码
int spi_queued_transfer(struct spi_device *spi, struct spi_message *msg)
	__spi_queued_transfer

/**
 * 函数:__spi_queued_transfer
 * 功能:将SPI消息添加到传输队列中
 * 描述:此函数将给定的SPI消息(msg)添加到其控制器(ctlr)的传输队列中,
 *       并根据控制器的状态和是否需要启动传输来决定是否启动传输工作。
 * 参数:
 *   - spi: 指向spi_device结构体的指针,表示SPI设备信息
 *   - msg: 指向spi_message结构体的指针,表示待传输的SPI消息
 *   - need_pump: 布尔值,表示是否在队列为空时启动传输
 * 返回值:
 *   - 0: 表示成功将消息加入队列
 *   - 负值: 表示错误,如控制器正在关闭则返回-ESHUTDOWN
 */
static int __spi_queued_transfer(struct spi_device *spi,
				 struct spi_message *msg,
				 bool need_pump)
{
	// 获取SPI控制器的信息
	struct spi_controller *ctlr = spi->controller;
	// 用于在中断上下文中保存和恢复中断状态的变量
	unsigned long flags;

	// 加锁以保护队列,防止同时修改
	spin_lock_irqsave(&ctlr->queue_lock, flags);

	// 检查控制器是否正在关闭
	if (!ctlr->running) {
		// 如果是,解锁并返回错误
		spin_unlock_irqrestore(&ctlr->queue_lock, flags);
		return -ESHUTDOWN;
	}
	// 初始化消息的实际长度和状态
	msg->actual_length = 0;
	msg->status = -EINPROGRESS;

	// 将消息添加到队列尾部
	list_add_tail(&msg->queue, &ctlr->queue);
	// 如果控制器空闲且需要启动传输,则启动传输工作
	if (!ctlr->busy && need_pump)
		kthread_queue_work(ctlr->kworker, &ctlr->pump_messages);

	// 解锁并恢复中断状态
	spin_unlock_irqrestore(&ctlr->queue_lock, flags);
	return 0;
}
c 复制代码
/**
 * struct spi_bitbang_cs - 用于SPI通信的位爆炸(bit-bang)芯片选择结构体
 *
 * @nsecs: 用于表示时钟周期时间的一半,作为时间基准,用于在spi_transfer过程中计算延迟。
 * @txrx_word: 是一个函数指针,用于发送(接收)一个单词大小的数据
 * @txrx_bufs: 是一个函数指针,用于处理缓冲区的发送(接收)操作
 *
 * 该结构体主要用于在SPI通信中通过软件方式控制芯片选择(Chip Select, CS)信号
 * 提供的操作函数指针允许在不同SPI设备之间发送和接收数据
 */
struct spi_bitbang_cs {
	unsigned	nsecs;	/* (clock cycle time)/2 */
	u32		(*txrx_word)(struct spi_device *spi, unsigned nsecs,
					u32 word, u8 bits, unsigned flags);
	unsigned	(*txrx_bufs)(struct spi_device *,
					u32 (*txrx_word)(
						struct spi_device *spi,
						unsigned nsecs,
						u32 word, u8 bits,
						unsigned flags),
					unsigned, struct spi_transfer *,
					unsigned);
};

/*
 * 发送接收单个字的函数指针。
 * 用于在spi_device上执行一次SPI传输操作。
 * 
 * 参数:
 *   spi - SPI设备结构体指针。
 *   nsecs - SPI控制器的时钟周期时间的一半。
 *   word - 要发送的数据字。
 *   bits - 数据字的位数。
 *   flags - 传输操作的标志。
 * 
 * 返回值:
 *   从SPI设备接收到的数据字。
 */
u32 (*txrx_word)(struct spi_device *spi, unsigned nsecs,
		u32 word, u8 bits, unsigned flags);

/*
 * 发送接收缓冲区的函数指针。
 * 用于在spi_device上执行一系列SPI传输操作。
 * 
 * 参数:
 *   spi - SPI设备结构体指针。
 *   txrx_word - 发送接收单个字的函数指针。
 *   nsecs - SPI控制器的时钟周期时间的一半。
 *   words - 数据字数组。
 *   bits - 数据字的位数。
 *   flags - 传输操作的标志。
 * 
 * 返回值:
 *   传输操作的数量。
 */
unsigned (*txrx_bufs)(struct spi_device *,
		u32 (*txrx_word)(struct spi_device *spi,
				unsigned nsecs,
				u32 word, u8 bits,
				unsigned flags),
		unsigned, struct spi_transfer *,
		unsigned);

二、如何编写SPI_Master驱动程序

1. 编写设备树

在设备树中,对于SPI Master,必须的属性如下:

  • #address-cells:这个SPI Master下的SPI设备,需要多少个cell来表述它的片选引脚
  • #size-cells:必须设置为0
  • compatible:根据它找到SPI Master驱动

可选的属性如下:

  • cs-gpios:SPI Master可以使用多个GPIO当做片选,可以在这个属性列出那些GPIO
  • num-cs:片选引脚总数

其他属性都是驱动程序相关的,不同的SPI Master驱动程序要求的属性可能不一样。

在SPI Master对应的设备树节点下,每一个子节点都对应一个SPI设备,这个SPI设备连接在该SPI Master下面。

这些子节点中,必选的属性如下:

  • compatible:根据它找到SPI Device驱动
  • reg:用来表示它使用哪个片选引脚
  • spi-max-frequency:必选,该SPI设备支持的最大SPI时钟

可选的属性如下:

  • spi-cpol:这是一个空属性(没有值),表示CPOL为1,即平时SPI时钟为低电平
  • spi-cpha:这是一个空属性(没有值),表示CPHA为1,即在时钟的第2个边沿采样数据
  • spi-cs-high:这是一个空属性(没有值),表示片选引脚高电平有效
  • spi-3wire:这是一个空属性(没有值),表示使用SPI 三线模式
  • spi-lsb-first:这是一个空属性(没有值),表示使用SPI传输数据时先传输最低位(LSB)
  • spi-tx-bus-width:表示有几条MOSI引脚;没有这个属性时默认只有1条MOSI引脚
  • spi-rx-bus-width:表示有几条MISO引脚;没有这个属性时默认只有1条MISO引脚
  • spi-rx-delay-us:单位是毫秒,表示每次读传输后要延时多久
  • spi-tx-delay-us:单位是毫秒,表示每次写传输后要延时多久

2. 编写驱动程序

  • 核心为:分配/设置/注册spi_master结构体
  • 对于老方法,spi_master结构体的核心是transfer函数

数据传输流程:

三、SPI_Master驱动程序简单示例demo

1. 使用老方法编写的SPI Master驱动程序

powershell 复制代码
virtual_spi_master {
        compatible = "100ask,virtual_spi_master";
        status = "okay";
        cs-gpios = <&gpio4 27 GPIO_ACTIVE_LOW>;
        num-chipselects = <1>;
        #address-cells = <1>;
        #size-cells = <0>;

        virtual_spi_dev: virtual_spi_dev@0 {
                compatible = "spidev";
                reg = <0>;
                spi-max-frequency = <100000>;
        };
};
c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/list.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/spi/spi.h>

static struct spi_master *g_virtual_master;
static struct work_struct g_virtual_ws;

static const struct of_device_id spi_virtual_dt_ids[] = {
	{ .compatible = "100ask,virtual_spi_master", },
	{ /* sentinel */ }
};

static void spi_virtual_work(struct work_struct *work)
{
	struct spi_message *mesg;
	
	while (!list_empty(&g_virtual_master->queue)) {
		mesg = list_entry(g_virtual_master->queue.next, struct spi_message, queue);
		list_del_init(&mesg->queue);
		
		/* 假装硬件传输已经完成 */

		mesg->status = 0;
		if (mesg->complete)
			mesg->complete(mesg->context);

	}	
}

static int spi_virtual_transfer(struct spi_device *spi, struct spi_message *mesg)
{
#if 0	
	/* 方法1: 直接实现spi传输 */
	/* 假装传输完成, 直接唤醒 */
	mesg->status = 0;
	mesg->complete(mesg->context);
	return 0;
	
#else
	/* 方法2: 使用工作队列启动SPI传输、等待完成 */
	/* 把消息放入队列 */
	mesg->actual_length = 0;
	mesg->status = -EINPROGRESS;
	list_add_tail(&mesg->queue, &spi->master->queue);
	
	/* 启动工作队列 */
	schedule_work(&g_virtual_ws);
	
	/* 直接返回 */
	return 0;
#endif	
}

static int spi_virtual_probe(struct platform_device *pdev)
{
	struct spi_master *master;
	int ret;
	
	/* 分配/设置/注册spi_master */
	g_virtual_master = master = spi_alloc_master(&pdev->dev, 0);
	if (master == NULL) {
		dev_err(&pdev->dev, "spi_alloc_master error.\n");
		return -ENOMEM;
	}

	master->transfer = spi_virtual_transfer;
	INIT_WORK(&g_virtual_ws, spi_virtual_work);

	master->dev.of_node = pdev->dev.of_node;
	ret = spi_register_master(master);
	if (ret < 0) {
		printk(KERN_ERR "spi_register_master error.\n");
		spi_master_put(master);
		return ret;
	}

	return 0;

	
}

static int spi_virtual_remove(struct platform_device *pdev)
{
	/* 反注册spi_master */
	spi_unregister_master(g_virtual_master);
	return 0;
}


static struct platform_driver spi_virtual_driver = {
	.probe = spi_virtual_probe,
	.remove = spi_virtual_remove,
	.driver = {
		.name = "virtual_spi",
		.of_match_table = spi_virtual_dt_ids,
	},
};

static int virtual_master_init(void)
{
	return platform_driver_register(&spi_virtual_driver);
}

static void virtual_master_exit(void)
{
	platform_driver_unregister(&spi_virtual_driver);
}

module_init(virtual_master_init);
module_exit(virtual_master_exit);

MODULE_DESCRIPTION("Virtual SPI bus driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("www.100ask.net");

2. 使用新方法编写的SPI Master驱动程序

bash 复制代码
vitural_spi_master {
	compatible = "100ask,virtual_spi_master";
	status = "okay";
	cs-gpios = <&gpio4 27 GPIO_ACTIVE_LOW>;
	num-chipselects = <1>;
	#address-cells = <1>;
	#size-cells = <0>;

	virtual_spi_dev: virtual_spi_dev@0 {
		compatible = "spidev";
		reg = <0>;
		spi-max-frequency = <100000>;
	};
};
c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/list.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi_bitbang.h>

static struct spi_master *g_virtual_master;
static struct spi_bitbang *g_virtual_bitbang;
static struct completion g_xfer_done;


static const struct of_device_id spi_virtual_dt_ids[] = {
	{ .compatible = "100ask,virtual_spi_master", },
	{ /* sentinel */ }
};

/* xxx_isr() { complete(&g_xfer_done)  } */

static int spi_virtual_transfer(struct spi_device *spi,
				struct spi_transfer *transfer)
{
	int timeout;

#if 1	
	/* 1. init complete */
	reinit_completion(&g_xfer_done);

	/* 2. 启动硬件传输 */
	complete(&g_xfer_done);

	/* 3. wait for complete */
	timeout = wait_for_completion_timeout(&g_xfer_done,
					      100);
	if (!timeout) {
		dev_err(&spi->dev, "I/O Error in PIO\n");
		return -ETIMEDOUT;
	}
#endif
	return transfer->len;
}

static void	spi_virtual_chipselect(struct spi_device *spi, int is_on)
{
}


static int spi_virtual_probe(struct platform_device *pdev)
{
	struct spi_master *master;
	int ret;
	
	/* 分配/设置/注册spi_master */
	g_virtual_master = master = spi_alloc_master(&pdev->dev, sizeof(struct spi_bitbang));
	if (master == NULL) {
		dev_err(&pdev->dev, "spi_alloc_master error.\n");
		return -ENOMEM;
	}

	g_virtual_bitbang = spi_master_get_devdata(master);

	init_completion(&g_xfer_done);

	/* 怎么设置spi_master?
	 * 1. spi_master使用默认的函数
	 * 2. 分配/设置 spi_bitbang结构体: 主要是实现里面的txrx_bufs函数
	 * 3. spi_master要能找到spi_bitbang
	 */
	g_virtual_bitbang->master = master;
	g_virtual_bitbang->txrx_bufs  = spi_virtual_transfer;
	g_virtual_bitbang->chipselect = spi_virtual_chipselect;
	master->dev.of_node = pdev->dev.of_node;

	ret = spi_bitbang_start(g_virtual_bitbang);
	if (ret) {
		printk("bitbang start failed with %d\n", ret);
		return ret;
	}

	return 0;
}

static int spi_virtual_remove(struct platform_device *pdev)
{
	spi_bitbang_stop(g_virtual_bitbang);
	spi_master_put(g_virtual_master);
	return 0;
}


static struct platform_driver spi_virtual_driver = {
	.probe = spi_virtual_probe,
	.remove = spi_virtual_remove,
	.driver = {
		.name = "virtual_spi",
		.of_match_table = spi_virtual_dt_ids,
	},
};

static int virtual_master_init(void)
{
	return platform_driver_register(&spi_virtual_driver);
}

static void virtual_master_exit(void)
{
	platform_driver_unregister(&spi_virtual_driver);
}

module_init(virtual_master_init);
module_exit(virtual_master_exit);

MODULE_DESCRIPTION("Virtual SPI bus driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("www.100ask.net");

本文章参考了韦东山老师驱动大全部分笔记,其余内容为自己整理总结而来。水平有限,欢迎各位在评论区指导交流!!!😁😁😁

相关推荐
逻极1 分钟前
Claude Code 实战:Spec-Kit、Kiro、OpenSpec 规范驱动开发三剑客
ide·人工智能·驱动开发·ai·自动化
烤麻辣烫13 分钟前
黑马程序员苍穹外卖(新手)Day1
java·数据库·spring boot·学习·mybatis
大锦终16 分钟前
【Linux】网络层与数据链路层中重点介绍
linux·运维·服务器·网络
提娜米苏31 分钟前
Bash Shell脚本学习——唇读数据集验证脚本
开发语言·学习·bash
lht6319356121 小时前
从Windows通过XRDP远程访问和控制银河麒麟 v10服务器
linux·运维·服务器·windows
qiudaorendao1 小时前
作业11.9
linux·服务器·apache
阿豪学编程1 小时前
环境变量与程序地址空间
linux·运维·windows
秃秃秃秃哇1 小时前
X5的相机同步方案
linux
xwz小王子2 小时前
PerAct2:机器人双臂操作任务的基准测试和学习
学习·机器人
CaracalTiger3 小时前
本地部署 Stable Diffusion3.5!cpolar让远程访问很简单!
java·linux·运维·开发语言·python·微信·stable diffusion