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

news2024/9/23 15:00:13

目录

  • 前言
  • 一、SPI_Master驱动程序框架
    • 1. SPI传输概述
      • 1.1 数据组织方式
      • 1.2 SPI控制器数据结构
    • 2. SPI传输函数的两种方法
      • 2.1 老方法
      • 2.2 新方法
  • 二、如何编写SPI_Master驱动程序
    • 1. 编写设备树
    • 2. 编写驱动程序
  • 三、SPI_Master驱动程序简单示例demo
    • 1. 使用老方法编写的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 传输的比特有不同的使用策略,因此需要多种协议调节选项。

.. 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传输函数的两种方法

/**
 * 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
};
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传输完成;
/**
 * 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);
/*
 * 函数__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 新方法

在这里插入图片描述

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;
}
/**
 * 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驱动程序

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>;
        };
};
#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驱动程序

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>;
	};
};
#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");

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2157818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WEB领域是不是黄了还是没黄

进入2024年后&#xff0c;WEB领域大批老表失业&#xff0c;一片哀嚎&#xff0c;个个饿的鬼叫狼嚎&#xff0c;为啥呢&#xff0c;下面是我个人的见解和看法。 中国程序员在应用层的集中 市场需求&#xff1a;中国的互联网行业在过去几年中经历了爆炸性增长&#xff0c;尤其是…

平板电容笔哪个牌子好?精选电容笔品牌排行榜前五名推荐!

在当今时代&#xff0c;平板电容笔已经成为平板电脑的重要配件&#xff0c;为人们的学习、工作和创作带来了极大的便利。然而&#xff0c;市场上平板电容笔的品牌众多&#xff0c;质量和性能也参差不齐&#xff0c;这让消费者在选择时常常感到困惑。平板电容笔究竟哪个牌子更好…

Revit 2018 提示 您使用的 Revit 授權無效。

昨天晚上想学下BIM&#xff0c;安装了这个软件&#xff0c;忘了给他断网了&#xff0c;今天早上起来一直提示这个信息&#xff0c;通过查看进程的位置找到了一个acwebbrowser 路径如下&#xff1a;C:\Program Files\Common Files\Autodesk Shared\CLM\V5\MSVC14\cliccore 防火…

如何使用 Rust 框架进行 RESTful API 的开发?

一、RESTful API 的开发 使用 Rust 框架进行 RESTful API 开发&#xff0c;你可以选择多种流行的 Rust Web 框架&#xff0c;如 Actix-web、Rocket、Warp 和 Tide 等。以下是使用这些框架进行 RESTful API 开发的基本步骤和概念&#xff1a; 选择框架&#xff1a;根据项…

OpenAI-gym how to implement a timer for a certain action in step()

题意&#xff1a;OpenAI-gym 如何在 step() 中为某个动作实现一个计时器 问题背景&#xff1a; One of the actions I want the agent to do needs to have a delay between every action. For context, in pygame I have the following code for shooting a bullet: 我希望代…

从趋势到常态:TikTok定制化产品的崛起与变革

随着数字化和TikTok的发展&#xff0c;定制化产品在消费者日常生活中愈发普及&#xff0c;逐渐从一种时尚潮流转变为常态。这一转变不仅改变了消费者的购物方式&#xff0c;也重塑了市场的供需关系、产品设计理念和商业模式。本文Nox聚星将和大家探讨TikTok定制化产品的未来发展…

QT 数据加密

一.使用环境 应该是通用的,此测试版本为如图 二.使用代码 1. 运行代码 QString data = "123abcAbc.-+";qDebug() << "加密:" << QAESEncryption::encodedText(data, "填入自己秘钥");qDebug() << "解密:" <…

Qemu开发ARM篇-4、kernel交叉编译运行演示

文章目录 1、kernel编译2、运行kernel3、FAQ 在前一篇 Qemu开发ARM篇-3、qemu运行uboot演示中&#xff0c;我们演示了如何使用 qemu运行uboot&#xff0c;在该篇中&#xff0c;我们将演示如何交叉编译 kernel并在qemu中运行 kernel. 1、kernel编译 本次演示使用kernel版本…

Java 中使用 Gson 实现深度克隆 #什么是深克隆与浅克隆?#clone方法为什么不能直接通过某个对象实例在外部类调用?

&#x1f310;Gson的jar包提供到本文上方&#xff0c;欢迎自取&#x1f310; 前言 &#x1f310;在 Java 编程中&#xff0c;克隆对象是一项常见的需求&#xff0c;特别是在处理不可变对象、避免引用传递时&#xff0c;深度克隆显得尤为重要。虽然 Java 提供了 clone() 方法&a…

【线程安全】如何区分线程安全还是线程不安全? 一文解释清楚线程安全问题,解释什么是锁重用、锁竞争、分段锁

线程安全问题 是一个重难点&#xff0c;编程就是这样&#xff0c;有的时候自己无论如何苦思冥想也弄不明白&#xff0c;但如果有人指点一二就能豁然开朗&#xff0c;希望本文可以给各位同学带来帮助 本文作者&#xff1a; csdn 孟秋与你 文章目录 如何判断一个类是否线程安全是…

抖音截流神器发布:不限量评论采集,实时推送,提升运营效率

在短视频风靡的今天&#xff0c;抖音成为品牌营销的新战场。如何在海量内容中脱颖而出&#xff0c;提升运营效率成为关键。本文将揭秘一款革命性的抖音运营工具&#xff0c;它不仅支持不限量评论采集&#xff0c;还实现了实时推送功能&#xff0c;助力运营者精准把握用户反馈&a…

解决事务提交延迟问题:Spring中的事务绑定事件监听机制解析

目录 一、背景二、事务绑定事件介绍三、事务绑定事件原理四、结语 一、背景 实际工作中碰到一个场景&#xff0c;现存系统有10w张卡需要进行换卡&#xff0c;简单来说就是为用户生成一张新卡&#xff0c;批量换卡申请需要进行审核&#xff0c;审核通过后异步进行处理。 为什么…

C++中string的使用

文章目录 string类对象的常见构造string类对象的容量操作size() / length()&#xff1a;返回字符串的长度&#xff08;字符数&#xff09;。capacity()&#xff1a;返回当前字符串分配的容量&#xff08;即在重新分配内存前可以保存的字符数&#xff09;。检查是否为空&#xf…

大数据可视化-三元图

三元图是一种用于表示三种变量之间关系的可视化工具&#xff0c;常用于化学、材料科学和地质学等领域。它的特点是将三个变量的比例关系在一个等边三角形中展示&#xff0c;使得每个点的位置代表三个变量的相对比例。 1. 结构 三个角分别表示三个变量的最大值&#xff08;通常…

Centos7.9 使用 Kubeadm 自动化部署 K8S 集群(一个脚本)

文章目录 一、环境准备1、硬件准备&#xff08;虚拟主机&#xff09;2、操作系统版本3、硬件配置4、网络 二、注意点1、主机命名格式2、网络插件 flannel 镜像拉取2.1、主机生成公私钥2.2、为啥有 Github 还用 Gitee2.3、将主机公钥添加到 Gitee2.3.1、复制主机上的公钥2.3.2、…

【C++篇】走进C++标准模板库:STL的奥秘与编程效率提升之道

文章目录 C STL 初探&#xff1a;打开标准模板库的大门前言第一章: 什么是STL&#xff1f;1.1 标准模板库简介1.2 STL的历史背景1.3 STL的组成 第二章: STL的版本与演进2.1 不同的STL版本2.2 STL的影响与重要性 第三章: 为什么学习 STL&#xff1f;3.1 从手动编写到标准化解决方…

FortiGate 防火墙 DNS 地址转换(DNS Translation)

简介 本例介绍 FortiGate 防火墙 DNS 地址转换&#xff08;DNS Translation&#xff09;配置方法。 一、 网络结构 网络结构如下图&#xff0c;PC1 连接在 FG60B 的 Internal 接口&#xff0c;FG60B 的 Wan1 接口连接 FG80CM 的 DMZ 接口&#xff0c;Wan1 接口开启 DNS 服务…

无人机之工作温度篇

无人机的工作温度是一个相对复杂的问题&#xff0c;因为它受到多种因素的影响&#xff0c;包括无人机的类型&#xff08;如民用、军用&#xff09;、设计规格、应用场景以及环境条件等。以下是对无人机工作温度范围的详细解析&#xff1a; 一、正常工作温度范围 一般来说&…

LeetcodeTop100 刷题总结(二)

LeetCode 热题 100&#xff1a;https://leetcode.cn/studyplan/top-100-liked/ 文章目录 八、二叉树94. 二叉树的中序遍历&#xff08;递归与非递归&#xff09;补充&#xff1a;144. 二叉树的前序遍历&#xff08;递归与非递归&#xff09;补充&#xff1a;145. 二叉树的后序遍…

RK3568驱动指南|第十六篇 SPI-第190章 配置模式下寄存器的配置

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…