【linux kernel】linux的SPI框架分析

文章目录

    • 一、linux内核中的SPI框架
    • 二、SPI核心的初始化
    • 三、SPI核心的数据结构
      • 1、struct spi_statistics
      • 2、struct spi_delay
      • 3、struct spi_device
      • 4、struct spi_driver
      • 5、struct spi_controller
      • 6、struct spi_res
      • 7、struct spi_transfer
      • 8、struct spi_message
      • 9、struct spi_board_info
    • 四、SPI框架的常用API总结
    • 五、SPI驱动实例分析
      • (5-1)SPI主机侧驱动
      • (5-2)SPI设备侧驱动
    • 六、SPI驱动调试总结

一、linux内核中的SPI框架

在嵌入式linux开发中,SPI通信是一种常见的通信方式,如下图所示:

常见的属于SPI设备包括RF芯片、智能卡、EEPROM、RTC、触摸传感器等等。

在内核中,与I2C一样,也同样提供了一个SPI框架,本文围绕这个框架展看,来分析内核提供的SPI框架是如何运作的。内核中与SPI相关的代码规范放置在/drivers/spi路径下,

从Makefile可知,内核提供的SPI框架主要实现在spi.cspidev.c文件中。

  • spi.c文件实现了spi核心的初始化,以及实现spi框架的相关API接口。(如果想让系统支持spi,此文件必须被编译)
  • spidev.c文件用于实现SPI设备同步用户空间接口。(该文件为可选特性)

存在/drivers/spi路径下其他洋洋洒洒的文件则是不同厂家提供的SPI控制器的驱动程序,这些文件往往由芯片厂家开发,然后合并到linux内核源码中,以适配自家芯片。

二、SPI核心的初始化

SPI核心的初始化实现在/drivers/spi/spi.c文件中,如下代码:

static int __init spi_init(void)
{
	int	status;

	buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
	if (!buf) {
		status = -ENOMEM;
		goto err0;
	}

	status = bus_register(&spi_bus_type);
	if (status < 0)
		goto err1;

	status = class_register(&spi_master_class);
	if (status < 0)
		goto err2;

	if (IS_ENABLED(CONFIG_OF_DYNAMIC))
		WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));

	return 0;

err2:
	bus_unregister(&spi_bus_type);
err1:
	kfree(buf);
	buf = NULL;
err0:
	return status;
}

/* board_info is normally registered in arch_initcall(),
 * but even essential drivers wait till later
 *
 * REVISIT only boardinfo really needs static linking. the rest (device and
 * driver registration) _could_ be dynamically linked (modular) ... costs
 * include needing to have boardinfo data structures be much more public.
 */
postcore_initcall(spi_init);

postcore_initcall()导出的spi核心的初始化过程中,主要做了以下几件事情:

  • 1、分配一个用于SPI的buffer。
  • 2、注册spi总线。
  • 3、注册spi_master主机类。

以上操作是内核中面向对象的基础构件过程,spi框架也不例外,也必须这样实现,以获得内核设备驱动模型的管理。

关于spi bus总线对设备和驱动的匹配过程:在spi/spic.c文件中定义了用于描述spi的bus总线,命名为spi,该总线在spi_init()函数中注册,匹配过程由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);

	/* Check override first, and if set, only use the named driver */
	if (spi->driver_override)
		return strcmp(spi->driver_override, drv->name) == 0;

	/* 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->modalias);

	return strcmp(spi->modalias, drv->name) == 0;
}

在上述代码中,描述了对spi设备和驱动匹配的四种方式。

三、SPI核心的数据结构

注意:几乎芯片原厂都要提供一个主机侧的SPI驱动,以支持自家的芯片。

编程接口是围绕两种驱动程序和两种设备构建的。SPI控制器驱动程序抽象了控制器硬件,它可以像一组GPIO引脚一样简单,也可以像一对fifo一样复杂,也有可能支持DMA引擎(实现数据的最大化吞吐量)。这样的驱动程序在它们所在的总线(通常是平台总线)和SPI之间架桥,并将其设备的SPI端作为struct spi_controller公开。

SPI设备是主设备的子设备,由struct spi_device表示,并由struct spi_board_info描述符进行描述,这些描述符通常由特定板卡的初始化代码提供。

struct spi_driver称为协议驱动程序,并通过使用正常的驱动程序模型调用绑定到spi_device

SPI的I/O模型是一组排队的消息,在协议驱动程序中可提交一个或多个struct spi_message对象,这些对象被异步处理和完成(包含同步包装器)。消息是从一个或多个struct spi_transfer对象构建,每个对象都封装了一个全双工SPI传输,开发中需要对各种协议进行配置,因为不同的芯片采用不同的策略来使用SPI传输的数据。

1、struct spi_statistics

struct spi_statistics描述spi传输的统计信息。

该结构中放置了几个u64_stats_t类型的数据,描述统计了spi传输的统计信息,该结构实现如下:

struct spi_statistics {
	struct u64_stats_sync	syncp;  //该参数用于保护这个结构体中的成员,在32位系统上实现per-cpu更新。

	u64_stats_t		messages;      //处理的spi消息数。
	u64_stats_t		transfers;     //处理的spi_transfer的数量。
	u64_stats_t		errors;        //在spi_transfer过程中的错误数。
	u64_stats_t		timedout;      //spi_transfer期间的timeout。

	u64_stats_t		spi_sync;            //使用spi_sync的次数。
	u64_stats_t		spi_sync_immediate;  //立即执行spi_sync的次数(在调用上下文时不需要排队和调度)。
	u64_stats_t		spi_async;           //使用spi_async的次数。

	u64_stats_t		bytes;               //传输到/从设备接收的字节数。
	u64_stats_t		bytes_rx;            //从设备接收的字节数。
	u64_stats_t		bytes_tx;            //发送到设备的字节数。

#define SPI_STATISTICS_HISTO_SIZE 17
	u64_stats_t	transfer_bytes_histo[SPI_STATISTICS_HISTO_SIZE];  //用于描述直方图的数据数组。

	u64_stats_t	transfers_split_maxsize;  //传输数最大尺寸限制。
};

2、struct spi_delay

struct spi_delay用于描述SPI延时信息。

在linux内核中有特定的延时方法,但是spi框架基于udelay()实现了自己的延时,这个延时用于spi的数据传输,struct spi_delay实现如下:

struct spi_delay {
#define SPI_DELAY_UNIT_USECS	0
#define SPI_DELAY_UNIT_NSECS	1
#define SPI_DELAY_UNIT_SCK	2
	u16	value;      //延时的值。
	u8	unit;       //延时的单位。
};

3、struct spi_device

struct spi_device用于描述控制器端SPI从设备。

该数据结构为linux内核spi子系统的内部结构。

struct spi_device定义如下:

struct spi_device {
    struct device           dev;       //设备的驱动模型表示。
    struct spi_controller   *controller; //与设备配套使用的SPI控制器。
    struct spi_controller   *master;   //控制器的副本(用于为了实现向后兼容)。
    u32 max_speed_hz;                  //此芯片(在此板上)使用的最大时钟速率;可能由设备的驱动程序更改。```spi_transfer.speed_hz```可以在每次传输时覆盖此设置。
    u8 chip_select;                    //芯片选择,用于区分由控制器处理的芯片。
    u8 bits_per_word;                  //表示数据传输涉及的字长,例如8位或12位这样的字长很常见。可以在每次传输spi_transfer.bits_per_word时重写此设置。
    bool rt;                           //该参数用于开始是否线程实时优先特性。
#define SPI_NO_TX               BIT(31)         ;
#define SPI_NO_RX               BIT(30)         ;
#define SPI_TPM_HW_FLOW         BIT(29)         ;
#define SPI_MODE_KERNEL_MASK    (~(BIT(29) - 1));
    u32 mode;
    int irq;                          //该参数可能为负值,或者传递给request_irq()以接收来自该设备的中断的数字。
    void *controller_state;           //控制器的运行状态。
    void *controller_data;            //特定于主板的控制器定义,例如FIFO初始化参数;来自于board_info.controller_data。
    char modalias[SPI_NAME_SIZE];     //要与此设备一起使用的驱动程序名称,或该名称的别名。这出现在用于驱动冷插拔的sysfs的“modalias”属性中,以及用于热插拔的事件中。
    const char              *driver_override; //如果将驱动程序的名称写入此属性,则设备将绑定到命名的驱动程序,并且仅绑定到命名的驱动程序。
    struct gpio_desc        *cs_gpiod;        //芯片选择线(CS)的GPIO描述符(该参数可选,如果不使用GPIO line该参数为NULL)。
    struct spi_delay        word_delay;       //表示在传送的连续字(Word)之间插入的延迟。
    struct spi_delay        cs_setup;         //表示在CS被断言后由控制器引入的延迟。
    struct spi_delay        cs_hold;          //表示控制器在CS解除断言之前引入的延迟。
    struct spi_delay        cs_inactive;      //CS解除断言后控制器引入的延时。如果在spi_transfer中使用cs_change_delay,则两个延迟将相加。
    struct spi_statistics __percpu  *pcpu_statistics; //表示spi_device的统计信息。
};

4、struct spi_driver

struct spi_driver用于描述主机端“协议”驱动程序。

为什么叫“协议”驱动程序,实属不易理解,这个词我是从官方文档中的描述(“protocol”)直接音译过来,因为这个结构主要用于基于spi总线协议通信的从设备。

struct spi_driver结构实现如下:

struct spi_driver {
	const struct spi_device_id *id_table;                //描述这个驱动程序支持的SPI设备列表。
	int			(*probe)(struct spi_device *spi);        //用于将此驱动程序绑定到SPI设备。驱动程序可以验证设备是否实际存在,可能需要配置不需要的特征(例如bits_per_word)。用于系统启动过程中完成初始配置。
	void			(*remove)(struct spi_device *spi);   //从SPI设备解除与这个驱动程序的绑定。
	void			(*shutdown)(struct spi_device *spi); //在系统状态转换期间使用的标准关机回调,如powerdown/halt和kexec。
	struct device_driver	driver;                      //SPI设备驱动程序应该初始化此结构的name和owner字段。
};

5、struct spi_controller

struct spi_controller描述到SPI主或从控制器的接口。

每个SPI控制器可以与一个或多个spi_device代表的子设备通信。这些设备通常使用4线spi总线:共享MOSI, MISOSCK信号,但不共享芯片选择信号。每个设备可以配置为使用不同的时钟速率。

SPI控制器的驱动程序通过spi_message事务队列管理对这些设备的访问,在CPU内存和SPI从设备之间复制数据。对于它所排队的每条这样的消息,在事务完成时将调用spi_message*complete回调函数。

struct spi_controller实现如下:

struct spi_controller {
    struct device   dev;    //此驱动程序的设备接口。
    struct list_head list;  //链接到全局spi_controller列表。
    s16 bus_num;            //给定SPI控制器的特定板级标识符。
    u16 num_chipselect;     //chipselects用于区分各个SPI从机,编号从0到num_chipselects。每个从机都有一个芯片选择信号。
    u16 dma_alignment;      //SPI控制器对DMA缓冲区对齐的约束。
    u32 mode_bits;          //由控制器驱动程序解析的标志。
    u32 buswidth_override_bits; //要覆盖此控制器驱动程序的标志
    u32 bits_per_word_mask;    //一个掩码参数,指示驱动程序支持bits_per_word的哪些值,第n位表示支持的bits_per_word为n+1
#define SPI_BPW_MASK(bits) BIT((bits) - 1);
#define SPI_BPW_RANGE_MASK(min, max) GENMASK((max) - 1, (min) - 1);
    u32 min_speed_hz;        //最低支持的传输速度。
    u32 max_speed_hz;        //最高支持的传输速度
    u16 flags;               //与此驱动程序相关的其他约束标志
#define SPI_CONTROLLER_HALF_DUPLEX      BIT(0)  ;
#define SPI_CONTROLLER_NO_RX            BIT(1)  ;
#define SPI_CONTROLLER_NO_TX            BIT(2)  ;
#define SPI_CONTROLLER_MUST_RX          BIT(3)  ;
#define SPI_CONTROLLER_MUST_TX          BIT(4)  ;
#define SPI_CONTROLLER_GPIO_SS          BIT(5)  ;
    bool devm_allocated;    //该结构体的分配是否为线程管理
    union {
        bool slave;         //表示这是一个SPI从控制器
        bool target;        //表示这是一个SPI目标控制器
    };
    size_t (*max_transfer_size)(struct spi_device *spi); //返回spi_device的最大传输大小的回调函数;该回调函数指针可能为NULL,这时候将使用默认的SIZE_MAX。
    size_t (*max_message_size)(struct spi_device *spi); //返回spi_device的最大消息大小的回函数调;该回调函数可能为NULL,这时候将使用默认的SIZE_MAX。
    struct mutex            io_mutex;  //用于物理总线访问的互斥锁。
    struct mutex            add_lock;  //该互斥锁用于避免将设备添加到相同的芯片选择。
    spinlock_t bus_lock_spinlock;      //用于SPI总线锁定的自旋锁。
    struct mutex            bus_lock_mutex;  //该互斥锁用于排除多个调用者。
    bool bus_lock_flag;                   //表示SPI总线为独占使用而被锁定。     
    int (*setup)(struct spi_device *spi);  //更新设备的SPI控制器使用的设备模式和时钟记录;协议代码可以调用这个。如果请求无法识别或不支持的模式,则此操作必须失败。
    int (*set_cs_timing)(struct spi_device *spi); //SPI设备请求SPI主控制器配置特定的CS设置时间,保持时间和非活动延迟的时钟计数的回调函数。该函数可选。
    int (*transfer)(struct spi_device *spi, struct spi_message *mesg); //将消息添加到控制器的传输队列。
    void (*cleanup)(struct spi_device *spi);  //释放特定于控制器的状态
    bool (*can_dma)(struct spi_controller *ctlr,struct spi_device *spi, struct spi_transfer *xfer); //判断该控制器是否支持DMA
    struct device *dma_map_dev;    //可以用于DMA映射的设备。
    struct device *cur_rx_dma_dev;  //当前用于RX DMA映射的设备。
    struct device *cur_tx_dma_dev; //当前用于TX DMA映射的设备。
    bool queued;                   //此控制器是否提供内部消息队列。
    struct kthread_worker           *kworker;      //指向消息pump的线程结构的指针。
    struct kthread_work             pump_messages;  //将工作安排到消息pump的工作结构。
    spinlock_t queue_lock;          //该自旋锁用于同步对消息队列的访问。
    struct list_head                queue;    //消息队列。
    struct spi_message              *cur_msg; //当前正在传输的消息。
    struct completion               cur_msg_completion;   //完成当前正在运行的消息。
    bool cur_msg_incomplete;    //内部使用标志,用于跳过cur_msg_completion。此标志用于检查驱动程序是否已经调用了spi_finalize_current_message()。
    bool cur_msg_need_completion;  //内部使用标志,用于跳过cur_msg_completion。此标志用于通知正在运行spi_finalize_current_message()的上下文,它需要complete()
    bool busy;                  //用于描述消息pump是否busy。
    bool running;               //用于描述消息pump是否running。
    bool rt;                    //是否将此队列设置为作为实时任务运行。
    bool auto_runtime_pm;       //该标志用于描述是否内核应该确保在硬件准备好时保持运行时PM的引用,使用父设备进行扩展。
    bool cur_msg_mapped;        //用于描述消息是否已映射为DMA。
    char last_cs;               //表示set_cs记录的最后一个chip_select,在非芯片选择时值为-1。
    bool last_cs_mode_high;     
    bool fallback;
    struct completion               xfer_completion;  //该参数由transfer_one_message()使用
    size_t max_dma_len;         //设备DMA传输的最大长度。
    int (*prepare_transfer_hardware)(struct spi_controller *ctlr); //spi子系统请求驱动程序通过发出此调用来准备传输硬件
    int (*transfer_one_message)(struct spi_controller *ctlr, struct spi_message *mesg); //spi子系统调用驱动程序来传输单个消息,同时对到达的传输进行排队。
    当驱动程序处理完这个消息后,必须调用spi_finalize_current_message(),这样spi子系统才能发出下一个消息。
    int (*unprepare_transfer_hardware)(struct spi_controller *ctlr); //当前队列上没有更多的消息时,spi子系统将通知驱动程序,spi子系统通过调用该回调来释放硬件。
    int (*prepare_message)(struct spi_controller *ctlr, struct spi_message *message);//该回调用于设置控制器以传输单个消息,例如:进行DMA映射,该回调在线程上下文中调用。
    int (*unprepare_message)(struct spi_controller *ctlr, struct spi_message *message); //撤销prepare_message()所做的所有操作。
    union {
        int (*slave_abort)(struct spi_controller *ctlr);  //该回调用于中止SPI从控制器上正在进行的传输请求。
        int (*target_abort)(struct spi_controller *ctlr); //该回调用于中止在SPI目标控制器上正在进行的传输请求
    };
    void (*set_cs)(struct spi_device *spi, bool enable); //设置芯片选择线(CS)的逻辑电平。可以从中断上下文调用。
    
    //传输单个spi_transfer。如果传输完成,返回0;如果传输仍在进行中,返回1。当驱动程序完成此传输时,它必须调用spi_finalize_current_transfer(),以便子系统可以发出下一次传输。
    int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *transfer); 
    
    //spi子系统调用驱动程序中的该函数来处理在transfer_one_message()中发生的错误。
    void (*handle_err)(struct spi_controller *ctlr, struct spi_message *message);
    const struct spi_controller_mem_ops *mem_ops; //与SPI内存交互的优化/专用操作。该字段可选,只有在控制器具有内存类操作的原生支持时才应该实现。
    const struct spi_controller_mem_caps *mem_caps;//处理内存操作的控制器能力。
    struct gpio_desc        **cs_gpiods; //用作芯片选择线的GPIO描述符数组;每个CS号码一个。对于非gpio(由SPI控制器本身驱动)的CS线,任何单个值都可能为NULL。
    bool use_gpio_descriptors; //是否打开SPI核心中的代码来解析和获取GPIO描述符,此后将填充cs_gpiods,如果为芯片选择找到了GPIO线,SPI设备将分配cs_gpiods。
    s8 unused_native_cs; //当使用cs_gpiods时,spi_register_controller()将用第一个未使用的原生CS填充此字段,供在使用GPIO CS时需要驱动原生CS的SPI控制器驱动程序使用。
    s8 max_native_cs;   //当使用cs_gpiods并填充此字段时,spi_register_controller()将根据此值验证所有原生CS(包括未使用的原生CS)。
    struct spi_statistics __percpu  *pcpu_statistics; //为spi_controller的统计。
    struct dma_chan         *dma_tx;    //DMA传输通道。
    struct dma_chan         *dma_rx;    //DMA接收通道。
    void *dummy_rx;      //全双工设备的虚拟接收缓冲区
    void *dummy_tx;      //全双工设备的虚拟传输缓冲器
    int (*fw_translate_cs)(struct spi_controller *ctlr, unsigned cs); //如果引导固件使用与Linux期望编号方案不同,则可以使用这个可选回调在二者之间进行转换。
    bool ptp_sts_supported;  //如果驱动程序将其设置为true,则驱动程序必须在spi_transfer->ptp_sts中提供一个时间快照,尽可能接近spi_transfer->ptp_sts_word_pre和spi_transfer->ptp_sts_word_post传输的时刻。如果驱动程序没有设置这个参数,SPI核心将尽可能接近驱动程序移交的快照。
    unsigned long           irq_flags;  //表示PTP系统时间戳期间中断使能状态。
    bool queue_empty;      //该参数用于表示消息队列是否为空。
    bool must_async;       //该参数用于表示是否关闭spi框架的所有快速路径操作。
};

struct spi_controller中的组成元素算比较多的了。

备注:在旧版的linux内核中使用struct spi_master描述SPI控制器。在较新的linux内核版本中,使用struct spi_controller替换了struct spi_master

6、struct spi_res

struct spi_res用于描述资源管理结构,侧重于在spi_message处理期间的生命周期管理。该结构定义如下:

struct spi_res {
    struct list_head        entry;      //资源链表项    
    spi_res_release_t release;          //释放此资源之前调用的释放代码
    unsigned long long      data[];     //为特定用例分配的额外数据
};

7、struct spi_transfer

struct spi_transfer用于描述读/写缓冲对,该结构定义定义如下:

struct spi_transfer {
    const void      *tx_buf;  //要写入的数据(DMA安全内存),该值可能为NULL。
    void *rx_buf;    //要读取的数据(DMA安全内存),该值可能为NULL
    unsigned len;    //rx和tx缓冲区的大小(字节为单位)
#define SPI_TRANS_FAIL_NO_START BIT(0);
    u16 error;      //SPI控制器驱动程序记录的错误状态。
    dma_addr_t tx_dma; //tx_buf的DMA地址,如果spi_message.is_dma_mapped。
    dma_addr_t rx_dma; //rx_buf的DMA地址,如果spi_message.is_dma_mapped。
    struct sg_table tx_sg; //用于传输的散列表。
    struct sg_table rx_sg; //用于接收的散列表;
    unsigned dummy_data:1;
    unsigned cs_off:1;
    unsigned cs_change:1;
    unsigned tx_nbits:3; //用于写入的位数。如果为0,则使用默认值(SPI_NBITS_SINGLE)。
    unsigned rx_nbits:3; //用于读取的位数。如果为0,则使用默认值(SPI_NBITS_SINGLE)。
    unsigned timestamped:1;  //如果传输有时间戳,则为True
#define SPI_NBITS_SINGLE        0x01 ;
#define SPI_NBITS_DUAL          0x02 ;
#define SPI_NBITS_QUAD          0x04 ;
    u8 bits_per_word;    //为此传输选择一个bits_per_word,而不是设备默认值。如果为0,则使用默认值(来自spi_device)。
    struct spi_delay        delay; //在此传输之后(可选地)更改chipselect状态之前引入的延迟,然后开始下一次传输或完成此spi_message。
    struct spi_delay        cs_change_delay; //当设置了cs_change并且spi_transfer不是spi_message中的最后一个时,在cs deassert和assert之间的延迟。
    struct spi_delay        word_delay; //每个字长(由bits_per_word设置)传输后引入的字间延迟。
    u32 speed_hz;  //为此传输选择设备默认速度以外的速度。如果为0,则使用默认值(来自spi_device)。
    u32 effective_speed_hz; //用于传输此传输的有效sck速度。如果SPI总线驱动不支持,设置为0。
    unsigned int    ptp_sts_word_pre; //在tx_buf中的字(受bits_per_word语义约束)偏移量,SPI设备为此请求开始此传输的时间快照。在完成SPI传输后,该值可能与请求的值相比发生了变化,这取决于可用的快照分辨率(DMA传输、ptp_sts_supported为false等)。
    unsigned int    ptp_sts_word_post;
    struct ptp_system_timestamp *ptp_sts;
    struct list_head transfer_list; //通过spi_message.transfers进行排序的transfer。
};

SPI传输总是需要写入与读取相同数量的字节。struct spi_driverSPI设备驱动程序应该提供rx_buftx_buf。在某些情况下,可能还需要为正在传输的数据提供DMA地址,当底层驱动程序使用DMA时,这样会减少CPU开销。

如果传输缓冲区为NULL,则在填充rx_buf时将移出零。如果接收缓冲区为NULL,则移入的数据将被丢弃。只有len字节移出(或移进)。

内存中的数据总是按照本地CPU字节顺序进行排列,从有线字节顺序转换而来(大端字节顺序,SPI_LSB_FIRST除外)。例如:当bits_per_word为16时,缓冲区是2N字节长(len = 2N),并按CPU字节顺序保存N个16位字。

当SPI传输的字长不是8的2次幂倍数时,内存中的字包含额外的位。内存中的字总是被协议驱动程序视为右对齐,因此未定义(rx)或未使用(tx)位始终是最重要的位。

所有SPI传输从芯片选择(CS)被激活开始。通常,直到消息中的最后一次传输之后,它才会被选中。驱动程序可以使用cs_change影响芯片选择(CS)信号:

  • (1)如果传输不是消息中的最后一个,则此标志用于使CS在消息中间短暂地处于非活动状态。以这种方式切换CS可能需要终止一个芯片命令,让单个spi_message一起执行所有的芯片事务组。

  • (2)当传输是消息中的最后一个传输时,芯片可以保持选中状态直到下一次传输。在多设备SPI总线上,没有阻止消息到其他设备,这只是一个性能提示;向另一个设备发送消息将取消选中该设备。但在其他情况下,这可以用来确保正确性,一些SPI设备需要通过一系列spi_message提交来构建协议事务,其中一条消息的内容由之前消息的结果决定,当CS处于非活动状态时,整个事务结束。

当SPI可以在1x,2x或4x传输时。它可以通过tx_nbitsrx_nbits从设备获取传输信息。在双向传输中,tx_nbitsrx_nbits都应该被设置。用户可以设置传输模式SPI_NBITS_SINGLE(1x)、SPI_NBITS_DUAL(2x)和SPI_NBITS_QUAD(4x)来支持这三种传输方式。

spi_message(及其spi_transfers)提交给较低层的代码负责管理其内存。因此零初始化没有显式设置的字段,可以防止未来API更新。在提交消息及其传输之后,忽略它们,直到它完成回调。

8、struct spi_message

spi_message用于执行数据传输的原子序列,每个序列由struct spi_transfer表示。该结构定义如下:

struct spi_message {
    struct list_head        transfers;   //该传输中传输段链表
    struct spi_device       *spi;       //表示该传输排队到的SPI设备
    unsigned is_dma_mapped:1;           //如果为true,则调用者为每个传输缓冲区提供DMA和CPU虚拟地址。
    bool prepared;                      //是否为此消息调用spi_prepare_message()
    int status;                         //表示传输状态,0表示成功,否则为负errno
    void (*complete)(void *context);    //调用该回调以报告事务的完成情况。
    void *context;                      //调用complete()时的参数。
    unsigned frame_length;              //message中的总字节数。
    unsigned actual_length;             //在所有成功的段中传输的字节总数。
    struct list_head        queue;      //该参数供当前拥有该消息的驱动程序使用。
    void *state;                        //该参数供当前拥有该消息的驱动程序使用。
    struct list_head        resources;  //用于处理SPI消息时的资源管理。
    struct spi_transfer     t[];        //该组成元素用于spi_message_alloc()。(当消息和传输已经一起分配时)
};

一个spi_message用于执行数据传输的原子序列,每个序列由struct spi_transfer结构表示。该序列是“原子的”,因为在该序列完成之前,没有其他spi_message可以使用该SPI总线。在一些系统中,许多这样的序列可以作为单个编程的DMA传输来执行。在所有系统上,这些消息都是以队列方式组织的,并且可能在发送到其他设备的事务之后完成,发送到给定spi_device的消息总是按照FIFO顺序执行。

spi_message(及其spi_transfers)提交给较底层的代码负责管理其内存。使用零初始化没有显式设置的每个字段,以隔离后续可能发生的API更新带来的影响。

9、struct spi_board_info

struct spi_board_info用于SPI设备的特定板卡模板。该结构定义如下:

struct spi_board_info {
    char modalias[SPI_NAME_SIZE];    //用于初始化spi_device.modalias,用于识别驱动程序。
    const void      *platform_data;    //用于初始化spi_device.platform_data,用于存储特定数据。
    const struct software_node *swnode; //用于描述设备的软件节点
    void *controller_data;  //用于初始化spi_device.controller_data;一些控制器需要提示硬件设置,例如DMA。
    int irq;  //用于初始化spi_device.irq;取决于板卡的连接。
    u32 max_speed_hz; //用于初始化spi_device.max_speed_hz;基于芯片数据表和主板特定信号质量问题的限制。
    u16 bus_num;  //识别哪些spi_controller作为spi_device的父设备;在spi_new_device()中未使用,取决于板卡接线。
    u16 chip_select; //用于初始化spi_device.chip_select;取决于板卡连接。
    u32 mode; //用于初始化spi_device.mode;根据芯片数据表,电路板布线。
};

当向设备树中添加新的SPI设备时,该结构可用作设备模板,该结构在两个地方使用,第一个作用是可存储在板卡特定设备描述符的表中,这些描述符在板卡初始化的早期声明,然后在控制器的驱动程序初始化之后使用。第二个作用是作为spi_new_device()调用的参数。

四、SPI框架的常用API总结

linux内核不同版本的SPI框架开放的API可能不同,以具体源码为主!

//1、初始化spi_message并附加到transfer
void spi_message_init_with_transfers(struct spi_message *m, struct spi_transfer *xfers, unsigned int num_xfers)

//2、检查是否支持每字位
bool spi_is_bpw_supported(struct spi_device *spi, u32 bpw)

//3、计算一个合适的超时值
unsigned int spi_controller_xfer_timeout(struct spi_controller *ctlr, struct spi_transfer *xfer)

//4、同步SPI数据传输
int spi_sync_transfer(struct spi_device *spi, struct spi_transfer *xfers, unsigned int num_xfers)

//5、SPI同步写操作
int spi_write(struct spi_device *spi, const void *buf, size_t len)

//6、SPI同步读操作
int spi_read(struct spi_device *spi, void *buf, size_t len)

//7、SPI同步8位写然后8位读
ssize_t spi_w8r8(struct spi_device *spi, u8 cmd)

//8、SPI同步8位写然后16位读
ssize_t spi_w8r16(struct spi_device *spi, u8 cmd)

//9、SPI同步8位写入,然后16位大端读
ssize_t spi_w8r16be(struct spi_device *spi, u8 cmd)

//10、为给定的board注册SPI设备
int spi_register_board_info(struct spi_board_info const *info, unsigned n)

//11、注册一个SPI驱动
int __spi_register_driver(struct module *owner, struct spi_driver *sdrv)

//12、分配新的SPI设备
struct spi_device *spi_alloc_device(struct spi_controller *ctlr)

//13、向SPI核心添加使用spi_alloc_device分配的spi_device
int spi_add_device(struct spi_device *spi)

//14、实例化一个新的SPI设备
struct spi_device *spi_new_device(struct spi_controller *ctlr, struct spi_board_info *chip)

//15、注销单个SPI设备
void spi_unregister_device(struct spi_device *spi)

//16、报告transfer的完成情况
void spi_finalize_current_transfer(struct spi_controller *ctlr)

//17、获取TX开始时间戳的助手函数
void spi_take_timestamp_pre(struct spi_controller *ctlr, struct spi_transfer *xfer, size_t progress, bool irqs_off)

//18、获取TX结束时间戳的助手函数
void spi_take_timestamp_post(struct spi_controller *ctlr, struct spi_transfer *xfer, size_t progress, bool irqs_off)

//19、获取下一个排队的消息。(由驱动程序调用用于检查排队的消息)
struct spi_message *spi_get_next_queued_message(struct spi_controller *ctlr)

//20、由驱动程序调用,通知内核队列前面的消息已经完成,可以从队列中删除。
void spi_finalize_current_message(struct spi_controller *ctlr)

//21、注册辅助SPI设备(该函数只能从主SPI设备的probe函数中调用)
struct spi_device *spi_new_ancillary_device(struct spi_device *spi, u8 chip_select)

//22、统计SpiSerialBus资源的个数
int acpi_spi_count_resources(struct acpi_device *adev)

//23、中止SPI从控制器上正在进行的传输请求
int spi_slave_abort(struct spi_device *spi)

//24、分配一个SPI主控制器或者从控制器
struct spi_controller *__spi_alloc_controller(struct device *dev, unsigned int size, bool slave)

//25、带资源管理的__spi_alloc_controller()
struct spi_controller *__devm_spi_alloc_controller(struct device *dev, unsigned int size, bool slave)

//26、注册SPI主控制器或者从控制器
int spi_register_controller(struct spi_controller *ctlr)

//27、带资源管理的spi_register_controller()
int devm_spi_register_controller(struct device *dev, struct spi_controller *ctlr)

//28、注销SPI主控制器或从控制器
void spi_unregister_controller(struct spi_controller *ctlr)

//29、当单个传输超过一定大小时,将spi传输拆分为多个传输
int spi_split_transfers_maxsize(struct spi_controller *ctlr, struct spi_message *msg, size_t maxsize, gfp_t gfp)

//30、当单个传输超过一定数量的SPI字时,将SPI传输拆分为多个传输
int spi_split_transfers_maxwords(struct spi_controller *ctlr, struct spi_message *msg, size_t maxwords, gfp_t gfp)

//31、设置SPI模式和时钟速率
int spi_setup(struct spi_device *spi)

//32、异步SPI传输
int spi_async(struct spi_device *spi, struct spi_message *message)

//33、阻塞/同步SPI数据传输
int spi_sync(struct spi_device *spi, struct spi_message *message)

//34、具有独占总线使用的spi_sync()版本
int spi_sync_locked(struct spi_device *spi, struct spi_message *message)

//35、获得独占SPI总线使用的锁
int spi_bus_lock(struct spi_controller *ctlr)

//36、释放独占SPI总线使用的锁
int spi_bus_unlock(struct spi_controller *ctlr)

//37、SPI同步写然后读。
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx)

五、SPI驱动实例分析

SPI驱动分为两个部分:主机侧驱动设备侧驱动

(5-1)SPI主机侧驱动

(1)SPI主机侧驱动设计思路

一般情况下,SPI主机侧的驱动程序芯片原厂,会去实现,并会合并到自己厂家维护的linux内核版本中发布给其他基于该芯片设计的厂商。在实现SPi主机侧驱动的时候,可以基于平台设备驱动框架实现,然后使用module_platform_driver()或者其他模块函数导出,例如:module_init()。在平台驱动的.probe指向的函数中实现spi驱动:

  • 1、区分spi驱动是slave还是master,并创建对应的struct spi_controller,如果是slave,则使用spi_alloc_slave()创建,如果是master,则使用spi_allov_master()创建。
  • 2、实现spi寄存器相关的映射。
  • 3、设置spi时钟。
  • 4、创建spi中断服务函数(以中断线程化方式实现)。
  • 5、初始化spi_controller相关组成元素的信息。
  • 6、指定struct spi_controller操作的callback。
  • 7、spi控制器相关的状态获取何和保存。
  • 8、注册spi控制器。可使用devm_spi_register_controller()或者相关接口实现。

(2)、SPI主机侧驱动案例分析

本小节,以Rockchip的rk3568的SPI主机侧驱动为例。分析SPI主机侧驱动的实现步骤,rk3568的spi驱动位于/drivers/spi/spi-rockchip.c(以具体linux内核源码为准)文件中。该驱动以platform驱动框架为基础实现,对应的struct platform_driver实现如下:

static struct platform_driver rockchip_spi_driver = {
	.driver = {
		.name	= DRIVER_NAME,
		.pm = &rockchip_spi_pm,
		.of_match_table = of_match_ptr(rockchip_spi_dt_match),
	},
	.probe = rockchip_spi_probe,
	.remove = rockchip_spi_remove,
};

module_platform_driver(rockchip_spi_driver);

在源码的最后使用module_platform_driver()导出spi驱动。

接着看看rockchip_spi_dt_match设备匹配表,定义如下:


可见该spi驱动支持的芯片类型比较多。

再看看.probe对应的rockchip_spi_probe(),该函数实现如下(函数中内容较多):

static int rockchip_spi_probe(struct platform_device *pdev)
{
	int ret;
	struct rockchip_spi *rs;
	struct spi_controller *ctlr;
	struct resource *mem;
	struct device_node *np = pdev->dev.of_node;
	u32 rsd_nsecs;
	bool slave_mode;
	struct pinctrl *pinctrl = NULL;

	slave_mode = of_property_read_bool(np, "spi-slave");

	if (slave_mode)
		ctlr = spi_alloc_slave(&pdev->dev,
				sizeof(struct rockchip_spi));
	else
		ctlr = spi_alloc_master(&pdev->dev,
				sizeof(struct rockchip_spi));

	if (!ctlr)
		return -ENOMEM;

	platform_set_drvdata(pdev, ctlr);

	rs = spi_controller_get_devdata(ctlr);
	ctlr->slave = slave_mode;

	/* Get basic io resource and map it */
	mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	rs->regs = devm_ioremap_resource(&pdev->dev, mem);
	if (IS_ERR(rs->regs)) {
		ret =  PTR_ERR(rs->regs);
		goto err_put_ctlr;
	}

	rs->apb_pclk = devm_clk_get(&pdev->dev, "apb_pclk");
	if (IS_ERR(rs->apb_pclk)) {
		dev_err(&pdev->dev, "Failed to get apb_pclk\n");
		ret = PTR_ERR(rs->apb_pclk);
		goto err_put_ctlr;
	}

	rs->spiclk = devm_clk_get(&pdev->dev, "spiclk");
	if (IS_ERR(rs->spiclk)) {
		dev_err(&pdev->dev, "Failed to get spi_pclk\n");
		ret = PTR_ERR(rs->spiclk);
		goto err_put_ctlr;
	}

	ret = clk_prepare_enable(rs->apb_pclk);
	if (ret < 0) {
		dev_err(&pdev->dev, "Failed to enable apb_pclk\n");
		goto err_put_ctlr;
	}

	ret = clk_prepare_enable(rs->spiclk);
	if (ret < 0) {
		dev_err(&pdev->dev, "Failed to enable spi_clk\n");
		goto err_disable_apbclk;
	}

	spi_enable_chip(rs, false);

	ret = platform_get_irq(pdev, 0);
	if (ret < 0)
		goto err_disable_spiclk;

	ret = devm_request_threaded_irq(&pdev->dev, ret, rockchip_spi_isr, NULL,
			IRQF_ONESHOT, dev_name(&pdev->dev), ctlr);
	if (ret)
		goto err_disable_spiclk;

	rs->dev = &pdev->dev;
	rs->freq = clk_get_rate(rs->spiclk);
	rs->gpio_requested = false;

	if (!of_property_read_u32(pdev->dev.of_node, "rx-sample-delay-ns",
				  &rsd_nsecs)) {
		/* rx sample delay is expressed in parent clock cycles (max 3) */
		u32 rsd = DIV_ROUND_CLOSEST(rsd_nsecs * (rs->freq >> 8),
				1000000000 >> 8);
		if (!rsd) {
			dev_warn(rs->dev, "%u Hz are too slow to express %u ns delay\n",
					rs->freq, rsd_nsecs);
		} else if (rsd > CR0_RSD_MAX) {
			rsd = CR0_RSD_MAX;
			dev_warn(rs->dev, "%u Hz are too fast to express %u ns delay, clamping at %u ns\n",
					rs->freq, rsd_nsecs,
					CR0_RSD_MAX * 1000000000U / rs->freq);
		}
		rs->rsd = rsd;
	}

	rs->fifo_len = get_fifo_len(rs);
	if (!rs->fifo_len) {
		dev_err(&pdev->dev, "Failed to get fifo length\n");
		ret = -EINVAL;
		goto err_disable_spiclk;
	}

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

	ctlr->auto_runtime_pm = true;
	ctlr->bus_num = pdev->id;
	ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_LSB_FIRST | SPI_CS_HIGH;
	if (slave_mode) {
		ctlr->mode_bits |= SPI_NO_CS;
		ctlr->slave_abort = rockchip_spi_slave_abort;
	} else {
		ctlr->flags = SPI_MASTER_GPIO_SS;
	}
	ctlr->num_chipselect = ROCKCHIP_SPI_MAX_CS_NUM;
	ctlr->dev.of_node = pdev->dev.of_node;
	ctlr->bits_per_word_mask = SPI_BPW_MASK(16) | SPI_BPW_MASK(8) | SPI_BPW_MASK(4);
	ctlr->min_speed_hz = rs->freq / BAUDR_SCKDV_MAX;
	ctlr->max_speed_hz = min(rs->freq / BAUDR_SCKDV_MIN, MAX_SCLK_OUT);

	ctlr->set_cs = rockchip_spi_set_cs;
	ctlr->setup = rockchip_spi_setup;
	ctlr->cleanup = rockchip_spi_cleanup;
	ctlr->transfer_one = rockchip_spi_transfer_one;
	ctlr->max_transfer_size = rockchip_spi_max_transfer_size;
	ctlr->handle_err = rockchip_spi_handle_err;

	ctlr->dma_tx = dma_request_chan(rs->dev, "tx");
	if (IS_ERR(ctlr->dma_tx)) {
		/* Check tx to see if we need defer probing driver */
		if (PTR_ERR(ctlr->dma_tx) == -EPROBE_DEFER) {
			ret = -EPROBE_DEFER;
			goto err_disable_pm_runtime;
		}
		dev_warn(rs->dev, "Failed to request TX DMA channel\n");
		ctlr->dma_tx = NULL;
	}

	ctlr->dma_rx = dma_request_chan(rs->dev, "rx");
	if (IS_ERR(ctlr->dma_rx)) {
		if (PTR_ERR(ctlr->dma_rx) == -EPROBE_DEFER) {
			ret = -EPROBE_DEFER;
			goto err_free_dma_tx;
		}
		dev_warn(rs->dev, "Failed to request RX DMA channel\n");
		ctlr->dma_rx = NULL;
	}

	if (ctlr->dma_tx && ctlr->dma_rx) {
		rs->dma_addr_tx = mem->start + ROCKCHIP_SPI_TXDR;
		rs->dma_addr_rx = mem->start + ROCKCHIP_SPI_RXDR;
		ctlr->can_dma = rockchip_spi_can_dma;
	}

	switch (readl_relaxed(rs->regs + ROCKCHIP_SPI_VERSION)) {
	case ROCKCHIP_SPI_VER2_TYPE1:
	case ROCKCHIP_SPI_VER2_TYPE2:
		if (ctlr->can_dma && slave_mode)
			rs->cs_inactive = true;
		else
			rs->cs_inactive = false;
		break;
	default:
		rs->cs_inactive = false;
	}

	pinctrl = devm_pinctrl_get(&pdev->dev);
	if (!IS_ERR(pinctrl)) {
		rs->high_speed_state = pinctrl_lookup_state(pinctrl, "high_speed");
		if (IS_ERR_OR_NULL(rs->high_speed_state)) {
			dev_warn(&pdev->dev, "no high_speed pinctrl state\n");
			rs->high_speed_state = NULL;
		}
	}

	ret = devm_spi_register_controller(&pdev->dev, ctlr);
	if (ret < 0) {
		dev_err(&pdev->dev, "Failed to register controller\n");
		goto err_free_dma_rx;
	}

	return 0;

err_free_dma_rx:
	if (ctlr->dma_rx)
		dma_release_channel(ctlr->dma_rx);
err_free_dma_tx:
	if (ctlr->dma_tx)
		dma_release_channel(ctlr->dma_tx);
err_disable_pm_runtime:
	pm_runtime_disable(&pdev->dev);
err_disable_spiclk:
	clk_disable_unprepare(rs->spiclk);
err_disable_apbclk:
	clk_disable_unprepare(rs->apb_pclk);
err_put_ctlr:
	spi_controller_put(ctlr);

	return ret;
}

上述.probe实现的主要步骤如下:

  • 读取spi-slave属性获取模式,如果是slave模式,则调用spi_alloc_slave()分配struct spi_contoller内存,否则为master模式,则调用spi_alloc_master()同样分配一个struct spi_contoller内存。

  • 获取基本的IO资源并对其进行映射。

  • 获取时钟并enable时钟。

  • 调用platform_get_irq()获取中断号,接着调用devm_request_threaded_irq()创建中断处理函数,其中中断处理函数为rockchip_spi_isr()

  • 设置struct rockchip_spi结构中的组成元素。struct rockchip_spi表示具体的spi控制器。

  • 设置struct spi_controller 结构中的组成元素。

  • 最后调用devm_spi_register_controller()注册spi控制器。

(5-2)SPI设备侧驱动

(1)SPI设备侧驱动设计思路

对于SPI设备的驱动,主要围绕如何与该SPI设备进行数据通信或者实现控制。在SPI控制器驱动实现的情况下,SPI设备侧的实现思路:

  • 1、对SPI设备进行描述。

可以通过修改设备树的方式对SPI设备进行描述。

  • 2、创建struct spi_driver的具体实例作为设备侧驱动。

  • 3、SPI设备数据收发处理流程

SPI设备数据的收发主要涉及到两个数据结构:struct spi_messagestruct spi_transfer,还需要
几个用于传输的API:

//在使用spi_message之前需要对其进行初始化:
void spi_message_init(struct spi_message *m)

//spi_message初始化完成以后可使用spi_message_add_tail将spi_transfer添加到spi_message队列中:
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)
  • 4、根据具体驱动需求设计struct file_operations对应的ops的callback。

  • 5、在.probe中使用spi_register_driver()向SPI核心注册SPI驱动,以字符设备类方式导出用户空间SPI设备文件,分配中断等

  • 6、以驱动框架方式导出。

(2)、SPI设备侧驱动案例分析

当主机侧的SPI实现后,我们可以快速的使用SPI控制器与SPI设备进行通信了。本小节以icm20608这款常见的SPI接口的六轴传感器为例分析SPI设备侧驱动的具体实现步骤。

  • 首先使用struct spi_driver创建spi驱动实例icm20608_driver,指定其中的.probe.removedriver参数,如有必要可实现.id_table
static struct spi_driver icm20608_driver = {
	.probe  = icm20608_probe,
	.remove = icm20608_remove,
	.driver = {
			.owner = THIS_MODULE,
		   	.name = "ICM20608",
		   	.of_match_table = icm20608_of_match, 
		   },
};
  • 接着使用module_init()初始化模块,在模块初始化函数中调用spi_register_driver()icm20608_driver注册到linux内核。然后实现模块退出接口函数,在该函数中实现必要的退出清理操作。

  • 接着实现icm20608_probe()icm20608_remove和设备树匹配表icm20608_of_match

  • icm20608_probe()中以字符设备方式向用户暴露出设备文件。并实现对icm20608设备文件的struct file_operations中的callback。例如:.open.readrelease。因icm20608为六轴传感器,主要操作是读取数据,所以.write可以不用实现。

  • 在实现.read操作时,主要内容是使用SPI控制器发送相应控制数据到SPI设备。使用SPI控制器发送数据的代码如下:

int icm20608_readRegs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    int ret;
    unsigned char txdata[len];
    struct spi_message message;
    struct spi_transfer *transfer;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    /* 1、申请内存 */
    transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);

    /* 2、发送要读取的寄存地址 */
    txdata[0] = reg | 0x80;		    /* 写数据的时候寄存器地址bit8要置1 */
    transfer->tx_buf = txdata;	    /* 要发送的数据 */
    transfer->len = 1;				/* 1个字节 */

    /* 3、初始化spi_message */
    spi_message_init(&message);		

    /* 4、将spi_transfer添加到spi_message队列 */
    spi_message_add_tail(transfer, &message);

    /* 5、同步发送 */
    ret = spi_sync(spi, &message);	

    /* 6、释放内存 */
    kfree(transfer);

    return ret;
}

六、SPI驱动调试总结

在SPI驱动调试过程中,还是需要注意以下几点:

  • (1)确保自己系统的SPI成功运行。这是SPI设备能正常通信工作的前提条件。
  • (2)在进行SPI数据传输时,确认时序是否正确:发送引脚有正常的波形,CLK引脚有正常的时钟信号,CS引脚有拉低。
  • (3)确保SPI的4个引脚的引脚复用配置正确。

参考链接:https://docs.kernel.org/driver-api/spi.html?highlight=spi#c.spi_read

https://docs.kernel.org/spi/index.html

https://stackoverflow.com/questions/57801887/using-spi-bitbang-driver

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript中history对象常用方法【通俗易懂】

✨前言✨   本篇文章主要在于了解及使用JavaScript中history对象常用方法 &#x1f352;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f352;博主将持续更新学习记录收获&#xff0c;友友们有任何问题可以在评论区留言 &#x1f4cd…

【052】基于Springboot、Vuey电影购票管理系统(附完整源码、数据库)

**基于Springboot、Vue、Mysql的电影购票管理系统&#xff08;附源码、数据库&#xff09;&#xff0c;超级完整的项目&#xff0c;值得下载&#xff01;&#xff01; 链接在博客最底下**电影购票管理系统源码及数据库百度云链接&#xff1a; https://pan.baidu.com/s/1loetDV…

VC6.0 下载的dsw打不开解决

有位朋友发了个老项目给我&#xff0c;是十多年前的VC6.0写的&#xff0c;为此我下载了一个VC6。但当选择打开工作空间时&#xff0c;却没有反应&#xff0c;甚至会报错。提示如下&#xff1a; 根据提示内容&#xff0c;Google了一下&#xff0c;找到了这篇帖子&#xff1a;htt…

poi操作Excel给列设置下拉菜单(数据验证)

效果图&#xff1a; pom.xml文件增加依赖&#xff1a; <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.0.1</version></dependency> 12345Workbook实现类有三个&#xff1a;HSSFWork…

2024,「量子人工智能」比想象中来得更快

我们离量子人工智能的未来还有多远&#xff1f; 两种改变游戏规则的技术的蓄意碰撞有可能颠覆科技行业&#xff0c;并带来一个商业颠覆和创新的新时代。很少有行业能幸免于这场变革&#xff0c;它将创造全新的价值和风险。夸夸其谈&#xff1f;业界并不这么认为。未来&#xff…

从零开始 - 在Python中构建和训练生成对抗网络(GAN)模型

生成对抗网络&#xff08;GANs&#xff09;是一种强大的生成模型&#xff0c;可以合成新的逼真图像。通过完整的实现过程&#xff0c;读者将对GANs在幕后的工作原理有深刻的理解。本教程首先导入必要的库并加载将用于训练GAN的Fashion-MNIST数据集。然后&#xff0c;提供了构建…

2024年【通信安全员ABC证】复审考试及通信安全员ABC证操作证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 通信安全员ABC证复审考试根据新通信安全员ABC证考试大纲要求&#xff0c;安全生产模拟考试一点通将通信安全员ABC证模拟考试试题进行汇编&#xff0c;组成一套通信安全员ABC证全真模拟考试试题&#xff0c;学员可通过…

优化模型:matlab二次规划

1.二次规划 1.1 二次规划的定义 若某非线性规划的目标函数为自变量 x x x的二次函数&#xff0c;且约束条件全是线性的&#xff0c;则称这种规划模型为二次规划。 1.2 二次规划的数学模型 min ⁡ 1 2 x T H x f T x \min \frac{1}{2}\boldsymbol{x}^{\boldsymbol{T}}\bolds…

【Matlab】RF随机森林时序预测算法(附代码)

资源下载&#xff1a; https://download.csdn.net/download/vvoennvv/88692249 一&#xff0c;概述 随机森林的基本思想是利用多个决策树对时序数据进行预测&#xff0c;其中每个决策树都使用不同的随机抽样方式选择训练数据&#xff0c;以减小过拟合的风险。 随机森林时序预测…

【计算机毕业设计】SSM实验室设备管理

项目介绍 本项目为后台管理系统&#xff0c;分为管理员、老师、学生三种角色&#xff1b; 管理员角色包含以下功能&#xff1a; 信息管理&#xff1a;用户管理&#xff1b; 基础管理&#xff1a;实验室管理,实验室申请记录,设备管理,设备记录管理,耗材管理,耗材记录管理等功能…

【Java进阶篇】字符串常量、字符串常量池详解

字符串常量、字符串常量池详解 ✔️字符串常量池是如何实现的?✔️字符串常量从哪来的? ✔️字符串常量是什么时候进入到字符串常量池的? ✔️字符串常量池是如何实现的? 字符串常量池 (String Constant Pool) 是Java中一块特殊的内存区域&#xff0c;用于存储字符串常量。…

C语言——操作符

一、算数操作符 1、(加操作符) 用于将两个数相加&#xff0c;例&#xff1a;3 3结果为6 2、-(减操作符) 用于将两个数相减&#xff0c;例&#xff1a;3 - 3结果为0 3、*(乘操作符) 用于将两个数相乘&#xff0c;例&#xff1a;3 * 3结果为9 4、/(除操作符) 用于将两个…

HashMap使用-LeetCode做题总结 454. 四数相加 II

454. 四数相加 II 最初思路优化思路Java语法增强for的使用场景 最初思路 枚举&#xff0c;因为是要计算有多少个元组&#xff0c;所以每个元素肯定都要遍历到&#xff0c;所以干脆算出所有元组的和。 我想用四个for循环加&#xff0c;但是失败。 优化思路 参考力扣 四数相加为…

创建VLAN及VLAN间通信

任务1、任务2、任务3实验背景&#xff1a; 在一家微型企业中&#xff0c;企业的办公区域分为两个房间&#xff0c;一个小房间为老板办公室&#xff0c;一个大房间为开放办公室&#xff0c;财务部和销售部的员工共同使用这个办公空间。我们需要通过VLAN的划分&#xff0c;使老板…

聊聊我使用亚马逊鲲鹏系统注册买家号的心得

想和大家聊一下我最近用了个挺好用的工具&#xff0c;就是亚马逊鲲鹏系统。以前我总是烦恼要一个一个手动注册亚马逊账号&#xff0c;真是麻烦。但有了这个系统&#xff0c;简直是方便到不行&#xff01; 首先&#xff0c;它有个全自动批量注册账号的功能&#xff0c;你只需要提…

Python爬取今日头条热门文章

前言 今日头条文章收益是没有任何门槛&#xff0c;只要是你发布文章&#xff0c;每篇文章的阅读量超过1000就能有收益&#xff0c;阅读量越多收益越高。于是乎我就有了个大胆的想法。何不利用Python爬虫&#xff0c;爬取热门文章&#xff0c;然后完成自动化发布文章呢&#xf…

77 Python开发-批量FofaSRC提取POC验证

目录 本课知识点:学习目的:演示案例:Python开发-某漏洞POC验证批量脚本Python开发-Fofa搜索结果提取采集脚本Python开发-教育SRC报告平台信息提取脚本 涉及资源: 本课知识点: Request爬虫技术&#xff0c;lxml数据提取&#xff08;把一些可以用的或者有价值的数据进行提取和保…

十二星座、社交做人守信用程度指数。

双子座&#xff08;95&#xff05; &#xff09;&#xff1b;天蝎座&#xff08;92&#xff05; &#xff09;&#xff1b;处女座&#xff08;90&#xff05; &#xff09; 金牛座&#xff08;85&#xff05; &#xff09;&#xff1b;狮子座&#xff08;85&#xff05; &#…

07. HTTP接口请求重试怎么处理?

目录 1、前言 2、实现方式 2.1、循环重试 2.2、递归重试 2.3、Spring Retry 2.4、Resilience4j 2.5、http请求网络工具内置重试方式 2.6、自定义重试工具 2.7、并发框架异步重试 2.8、消息队列 3、小结 1、前言 HTTP接口请求重试是指在请求失败时&#xff0c;再次发…

[python]matplotlib

整体图示 .ipynb 转换md时候图片不能通知携带&#xff0c;所有图片失效&#xff0c;不过直接运行代码可以执行 figure figure,axes与axis import matplotlib.pyplot as plt figplt.figure() fig2plt.subplots() fig3,axsplt.subplots(2,2) plt.show()<Figure size 640x480 …