【STM32】基于SPI协议读写SD,详解!

文章目录

    • 0 前言
    • 1 SD卡的种类和简介
      • 1.1 SD卡的种类
      • 1.2 SD卡的整体结构
      • 1.3 SD卡运行机制——指令和响应
    • 2 SD卡的通信总线
      • 2.1 SDIO
      • 2.2 SPI
    • 3 硬件连接
    • 4 代码实践【重点】
      • 4.1 HAL库移植
      • 4.2 标准库移植
      • 4.3 遇到的问题和解决方案
    • 5 扩展阅读

0 前言

  因为项目需要,使用stm32读写sd卡,这一块网上的资料很多,但是比较杂乱。有些是不能跑,有些是代码可以跑,但是相关的注释或者配置方法、流程不够清晰明确,于是花了几天时间,研究了几个成功案例之后,总结出一个相对明确的流程。【基于STM32F103C8T6

网上有各种流传的例程,经过测试确实可以用,但是魔改得有点多,个人觉得不是很便于理解,所以想着能不能从最开始的FATFS包来自己手动移植一个,最好是这个流程完全可复制,操作也非常简单,就像一个插件一样,基本实现模块化。

1 SD卡的种类和简介

  既然要读写SD卡,那首先要对SD卡的底层有一定的了解,这样才能够真正理解后面的代码。

1.1 SD卡的种类

  首先需要明确的是,SD卡指的是那种大的卡,一般用在相机里面,如下图所示:

在这里插入图片描述在这里插入图片描述

而这种卡:

在这里插入图片描述在这里插入图片描述
一般叫microSD卡TF卡,二者其实相差不大,只是引脚略微不同,其实读写都是一样的,也可以考虑买一个TF卡转SD卡的卡套,来适应两种接口。

  相比于这个SD卡的名字,另一个SD卡的标准显得更加重要。所谓的标准,差别主要体现在容量上面,这个需要在使用前明确。目前仍然有很多老年手机不支持大容量的TF卡,其本质就是因为不支持更高的标准。常见的SD卡标准如下图所示。
在这里插入图片描述
这个标准SD卡和TF卡是一样的,只是名字不同。

参考链接

  另外,根据这个链接, 实际上SD的通信协议也有多个版本,最早支持的版本是1.x,在SDHC之后,基本都是使用2.0版本,来兼容FAT32格式(原来都是FAT和FAT16),这两个协议的区别在驱动方面主要体现在指令上(2.0版本的指令更多,且兼容1.x版本的指令),这个后面有相关介绍,先埋个伏笔。

1.2 SD卡的整体结构

  理解了SD卡的种类,再来看看结构,主要是以下这张图
在这里插入图片描述
简单来说,就是除了存储单元外,还有好几个寄存器用于存放卡相关的信息,这些信息可以通过一些特定的指令读写。

1.3 SD卡运行机制——指令和响应

  SD卡的核心就是存储,那外部的主机如何对这个进行读写呢?就是通过指令。主机发送一条指令,然后SD卡会发送响应,让主机知道指令执行情况。
  每一条指令都是6个字节(48bit),其结构如下所示:
在这里插入图片描述

其中,Command占6位,所以一共有64个指令,从0-63,依次叫CMD0,CMD1,。。。CMD63,但是因为一次性是发送一个字节,也就是8位,所以会加上前面的两位,即0x40+CMDx才是指令。
  紧接着的是32位指令执行的参数,一般是存储地址或者寄存器值等,不是所有指令都有参数,对于没有参数的指令,直接传0即可。
  最后是校验值,这里采用的是循环校验,计算有点复杂,这个其实在后续的代码中,都是把部分常用的指令对应的校验计算出来给他传过去,并没有现场计算。

  指令发出之后,主机要等待SD卡的响应,其响应有很多类型,长度也各自不一样。短的响应只有一个字节,长的响应可以有多个字节。大部分的指令都是R1类型,即只有一个字节,R2表示响应有两个字节,还有一种类型是R1b,即在R1的基础上,后面紧跟着busy信号,可能有多个字节,一般不怎么使用。R1响应的结构各个位都有单独的含义,如下图所示。
在这里插入图片描述
可以看到,第6-1位都是错误,为1表示错误(“有效”),为0表示没有错误;第0位表示卡是否处于空闲状态,一般是发送进入IDLE指令(CMD0)之后会响应,也就是0x01。

  以上就是SD卡使用的基本讨论,即写入一个6字节的指令,然后读取响应的1-2个字节,并判断指令执行状态。时序图如下所示。
在这里插入图片描述
  接下来就是重点:SD卡数据的读写。和上面一样,读写数据之前,需要先发送一个指令,然后再读入或写入数据。对应的指令主要是这几个:
在这里插入图片描述
分别有读单块、读多块、写单块、写多块四个指令。其中,读写多块貌似需要使用到ACMD指令,所以用得比较少,可以通过多次调用读写单块的函数达到读写多块的目的。【一般SD卡一块(block)是512 Byte】
  根据官方的手册,读数据的流程大概是这样:
在这里插入图片描述
即先发送读的指令,然后等待sd卡响应指令后(根据上图,读单块和多块的响应都是R1类型),再读取数据块。
类似地,写指令的操作流程时序如下所示。
在这里插入图片描述
和上面不一样的是,在数据写入完毕后,还会有一个响应(Data Response),表示数据写入的情况,由SD卡传输给主机,是一个字节,其格式如下所示

在这里插入图片描述

  但是,这个时序图中并没有对“Data Block”部分进行展开叙述,但其实其内部结构同样重要,这里根据官方的描述和可行代码自行绘制了这张图:
在这里插入图片描述
其中,First Byte类似于一个启动符号,告知后面有数据来了,然后是一个block的数据,一般是512字节,最后是两位校验码。
  对于读数据,首先要读第一个字节,判断是不是0xFE,如果是,表示后面是数据,要把后面的数据给收了,收完512字节之后,最后的两位校验码可以忽略;对于写数据,是在发完写指令之后,手动写入0xFE,作为写数据的第一个字节,然后再写入512字节数据,最后两位校验码一般直接传0xFF即可。

2 SD卡的通信总线

  上面介绍的是SD卡的运行机制,从上面的结构图可以看出,这个运行机制到MCU控制端还需要一个通信协议,来约定这些数据该如何传输。常见的SD卡通信协议主要有两种:SPI模式SD模式(SDIO),其中两种通信协议下的引脚定义如下图所示。
在这里插入图片描述

在SPI协议中,SD卡扮演的角色是Slave,即从机,故其中MOSI和MISO中“M”指的控制数据读写的芯片,如MCU等;“S”从机是指SD卡。

参考链接

关于引脚的理解:以SPI为例,MCU对SD卡的控制指令都是通过CMD引脚串行传输的,所以CMD引脚是MOSI;而SD卡返回的数据是通过D0传输,所以D0是MISO。而SDIO数据传输可以选定多个引脚,常见的有只使用D0,和使用D0~D3四个引脚,并行传输。

2.1 SDIO

  在STM32F10x系列型号中,只有大容量的芯片才支持这个协议,没有实践过,这里只放一个网上的教程:

  • SDIO—SD卡读写测试

值得一提的是,不同协议其实只是传输方式不一样,底层的那些逻辑是差不多的,当然有些指令SPI协议不支持,只支持SDIO协议。

2.2 SPI

  • 概述
      SPI是四线协议:SCK(同步时钟),MOSI(主机到从机的数据),MISO(从机到主机的数据),CS(片选)。和IIC类似,也是一个串行协议,因为有时钟信号,所以是一个同步传输的协议(UART是异步协议)。但是,值得一提的是,因为收发数据是两根线,所以SPI是全双工协议,而IIC因为只有SCK和SDA,所以是半双工协议。

  • 运行模式
      SPI比较特殊的地方在于,它的电平和采样边沿可以额外设置,也就是设置不同的传输模式,这个设置由两个变量来确定:CPOL(Clock Polarity)、CPHA(Clock Phase),这两个变量分别可以设置0或1,因此组合起来有四种模式:

    • 0 0 CLK空闲时为低电平,CLK上升沿(第一个边沿)采样数据。
    • 0 1 CLK空闲时为低电平,CLK下降沿(第二个边沿)采样数据。
    • 1 0 CLK空闲时为高电平,CLK下降沿(第一个边沿)采样数据。
    • 1 1 CLK空闲时为高电平,CLK上升沿(第二个边沿)采样数据。
  • 数据同步
      由于SPI是全双工协议,且时钟只能是主设备发出,所以在主设备看来,不管是发送还是接收数据,都必须提供时钟,加上数据发送和接收是分开的两根线,所以数据在发送时也需要接收,或者说,接收时因为需要时钟,所以其实接收缓冲区也会新增数据,只是用不用的问题。
      那问题来了,如果我要收一个数据,必须发一个数据,那对方因为该数据误操作了怎么办?所以在接收数据时,要发送一个对从机设备来说无效的数据,也就是所谓的dummy data,这样就不会误响应了。

  • 代码配置
      网上有很多流传的软件SPI,即在理解SPI协议的基础上,使用IO口实现这个时序,但是这样一方面是代码比较麻烦,另外就是时钟配置难以掌握,所以这种只适用于硬件SPI没有或者被用完的情况,在有硬件SPI外设的前提下,还是用硬件比较方便。
      这里以一个标准库下的SPI外设初始化为例理解一下SPI配置的方法:

    void SD_SPI_Init(void)
    {
         
        GPIO_InitTypeDef GPIO_InitStructure;
    	
    	//使能时钟——宏定义实现
        ENABLE_SD_SPI_GPIO_CLK();
        ENABLE_SD_SPI_CLK();
    	
    	//GPIO初始化
        GPIO_InitStructure.GPIO_Pin = SD_SPI_MOSI_PIN | SD_SPI_SCK_PIN;   //MOSI & SCK: AFIO,Output
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(SD_SPI_GPIO_PORT, &GPIO_InitStructure);
    
        GPIO_InitStructure.GPIO_Pin = SD_SPI_MISO_PIN;                    //MISO: Input
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //输入
        GPIO_Init(SD_SPI_GPIO_PORT, &GPIO_InitStructure);
    
    	//SPI外设初始化
        SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
        SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//设置SPI工作模式:设置为主SPI
        SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//设置SPI的数据大小:SPI发送接收8位帧结构
        SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		//选择了串行时钟的稳态:时钟悬空高
        SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	//数据捕获于第二个时钟沿
        SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
        SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;		//定义波特率预分频的值:波特率预分频值为256
        SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
        SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
        SPI_Init(SD_SPI, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
    
        SPI_Cmd(SD_SPI, ENABLE); //使能SPI外设
    
        SD_SPI_ReadWriteByte(0xff);//启动传输
    }
    

    重点是GPIO口输出和输入要分别配置。

3 硬件连接

  SD卡电路设计如下图所示,在画电路板时,记得在几个sd卡的引脚上加上上拉电阻:

在这里插入图片描述

CD引脚全称是Card Detect,用于检测卡是否插入,在一些开发板的原理图中有类似的做法,但是软件其实也可以判断出来,所以必要性不强

4 代码实践【重点】

  在使用SD卡时,建议在充分理解上述展示的SD卡运行原理后先实现存储的访问,比如先写入一段,然后再去读取,串口输出读取的内容,对比一下是否一致。然后再考虑加上FATFS,实现基本的读写文件功能。
  很显然,我其实并没有按照这个流程学习,而是先找了网上的一个可运行的代码(已经带了FATFS),然后在此基础上不断尝试新的操作,在这个尝试的过程中对SD卡运行原理有了比较深刻的认识。

  言归正传,如果以实用为主,建议直接使用HAL库,如果愿意折腾,可以自己尝试在标准库实现,建议在HAL库的基础上再去移植标准库。由于这两个步骤我都实践了一遍,后文都有介绍。

参考链接:

  • 国外的一个教程,基于CubeIDE实现
  • 从大容量移植到中等容量的成功案例

4.1 HAL库移植

  这部分内容基本参考自上面的教程,只做了一些小的修改,让这个部分集成度更高。

  • 首先先设置一些系统参数,不设置其实问题也不大,但是设置全面,不留风险是编程开发的一个好习惯:
    在这里插入图片描述

    在这里插入图片描述

  • 然后使能SPI外设

    在这里插入图片描述
    这里简单介绍一下NSS,所谓硬件NSS类似于串口的硬件流控一样,即通过实际的引脚来实现片选,这样就可以直接调用SPI的函数来进行控制,而所谓软件(即下面 NSS Signal Type: Software)即是额外再初始化一个引脚来控制。

    这里其实个人觉得两者是差不多的,只是硬件是芯片指定的引脚,而软件则可以随便指定,相对自由一些。代码上其实差别不大,只是一个调SPI库的函数,一个调GPIO库的函数。但是网上相关的代码基本都是使用软件形式,所以这里也跟风一下。

  • 然后再添加FATFS,这里只改动两个设置:

    • USE_LFN:Enable with static working buffer on the BSS
    • MAX_SS:4096

    如下图所示
    在这里插入图片描述

  • 项目配置那块,需要把堆栈加大

    在这里插入图片描述
    分文件显示,模块化更容易理解:
    在这里插入图片描述

    私以为将不同外设分为不同文件是一个很好的习惯

最后,生成代码即可,代码方面主要修改3个文件:

fat_sd_card.c【额外添加的一个文件】

#define TRUE  1
#define FALSE 0
#define bool BYTE

#include "fatfs_sd_card.h"

static volatile DSTATUS Stat = STA_NOINIT;  /* Disk Status */
static uint8_t CardType;                    /* Type 0:MMC, 1:SDC, 2:Block addressing */
static uint8_t PowerFlag = 0;               /* Power flag */

/***************************************
 * SPI functions
 **************************************/

/* slave select */
static void SELECT(void)
{
   
    HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_RESET);
    HAL_Delay(1);
}

/* slave deselect */
static void DESELECT(void)
{
   
    HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_SET);
    HAL_Delay(1);
}

/* SPI transmit a byte */
static void SPI_TxByte(uint8_t data)
{
   
    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_Transmit(HSPI_SDCARD, &data, 1, SPI_TIMEOUT);
}

/* SPI transmit buffer */
static void SPI_TxBuffer(uint8_t* buffer, uint16_t len)
{
   
    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_Transmit(HSPI_SDCARD, buffer, len, SPI_TIMEOUT);
}

/* SPI receive a byte */
static uint8_t SPI_RxByte(void)
{
   
    uint8_t dummy, data;
    dummy = 0xFF;

    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_TransmitReceive(HSPI_SDCARD, &dummy, &data, 1, SPI_TIMEOUT);

    return data;
}

/* SPI receive a byte via pointer */
static void SPI_RxBytePtr(uint8_t* buff)
{
   
    *buff = SPI_RxByte();
}

/***************************************
 * SD functions
 **************************************/

/* wait SD ready */
static uint8_t SD_ReadyWait(void)
{
   
    uint8_t res;

    /* timeout 500ms */
    int32_t Timer2 = 0xffffff;

    /* if SD goes ready, receives 0xFF */
    do
    {
   
        res = SPI_RxByte();
        Timer2--;
    }
    while((res != 0xFF) && Timer2 > 0);

    return res;
}

/* power on */
static void SD_PowerOn(void)
{
   
    uint8_t args[6];
    uint32_t cnt = 0x1FFF;

    /* transmit bytes to wake up */
    DESELECT();
    for(int i = 0; i < 10; i++)
    {
   
        SPI_TxByte(0xFF);
    }

    /* slave select */
    SELECT();

    /* make idle state */
    args[0] = CMD0;   /* CMD0:GO_IDLE_STATE */
    args[1] = 0;
    args[2] = 0;
    args[3] = 0;
    args[4] = 0;
    args[5] = 0x95;   /* CRC */

    SPI_TxBuffer(args, sizeof(args));

    /* wait response */
    while((SPI_RxByte() != 0x01) && cnt)
    {
   
        cnt--;
    }

    DESELECT();
    SPI_TxByte(0XFF);

    PowerFlag = 1;
}

/* power off */
static void SD_PowerOff(void)
{
   
    PowerFlag = 0;
}

/* check power flag */
static uint8_t SD_CheckPower(void)
{
   
    return PowerFlag;
}

/* receive data block */
static bool SD_RxDataBlock(BYTE* buff, UINT len)
{
   
    uint8_t token;

    /* timeout 200ms */
    int32_t Timer1 = 0xffffff;

    /* loop until receive a response or timeout */
    do
    {
   
        token = SPI_RxByte();
        Timer1--;
    }
    while((token == 0xFF) && Timer1 > 0);

    /* invalid response */
    if(token != 0xFE) return FALSE;

    /* receive data */
    do
    {
   
        SPI_RxBytePtr(buff++);
    }
    while(len--);

    /* discard CRC */
    SPI_RxByte();
    SPI_RxByte();

    return TRUE;
}

/* transmit data block */
#if _USE_WRITE == 1
static bool SD_TxDataBlock(const uint8_t* buff, BYTE token)
{
   
    uint8_t resp;
    uint8_t i = 0;

    /* wait SD ready */
    if(SD_ReadyWait() != 0xFF) return FALSE;

    /* transmit token */
    SPI_TxByte(token);

    /* if it's not STOP token, transmit data */
    if(token != 0xFD)
    {
   
        SPI_TxBuffer((uint8_t*)buff, 512);

        /* discard CRC */
        SPI_RxByte();
        SPI_RxByte();

        /* receive response */
        while(i <= 64)
        {
   
            resp = SPI_RxByte();

            /* transmit 0x05 accepted */
            if((resp & 0x1F) == 0x05) break;
            i++;
        }

        /* recv buffer clear */
        while(SPI_RxByte() == 0);
    }

    /* transmit 0x05 accepted */
    if((resp & 0x1F) == 0x05) return TRUE;

    return FALSE;
}
#endif /* _USE_WRITE */

/* transmit command */
static BYTE SD_SendCmd(BYTE cmd, uint32_t arg)
{
   
    uint8_t crc, res;

    /* wait SD ready */
    if(SD_ReadyWait() != 0xFF) return 0xFF;

    /* transmit command */
    SPI_TxByte(cmd);          /* Command */
    SPI_TxByte((uint8_t)(arg >> 24));   /* Argument[31..24] */
    SPI_TxByte((uint8_t)(arg >> 16));   /* Argument[23..16] */
    SPI_TxByte((uint8_t)(arg >> 8));  /* Argument[15..8] */
    SPI_TxByte((uint8_t)arg);       /* Argument[7..0] */

    /* prepare CRC */
    if(cmd == CMD0) crc = 0x95; /* CRC for CMD0(0) */
    else if(cmd == CMD8) crc = 0x87;  /* CRC for CMD8(0x1AA) */
    else crc = 1;

    /* transmit CRC */
    SPI_TxByte(crc);

    /* Skip a stuff byte when STOP_TRANSMISSION */
    if(cmd == CMD12) SPI_RxByte();

    /* receive response */
    uint8_t n = 10;
    do
    {
   
        res = SPI_RxByte();
    }
    while((res & 0x80) && --n);

    return res;
}

/***************************************
 * user_diskio.c functions
 **************************************/

/* initialize SD */
DSTATUS SD_disk_initialize(BYTE drv)
{
   
    uint8_t n, type, ocr[4];

    /* single drive, drv should be 0 */
    if(drv) return STA_NOINIT;

    /* no disk */
    if(Stat & STA_NODISK) return Stat;

    /* power on */
    SD_PowerOn();

    /* slave select */
    SELECT()<

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

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

相关文章

ZooKeeper单机、集群模式搭建教程

单点配置 ZooKeeper在启动的时候&#xff0c;默认会读取/conf/zoo.cfg配置文件&#xff0c;该文件缺失会报错。因此&#xff0c;我们需要在将容器/conf/挂载出来&#xff0c;在制定的目录下&#xff0c;添加zoo.cfg文件。 zoo.cfg logback.xml 配置文件的信息可以从二进制包…

如何在Mysql中生成0-23完整的小时数据

目录 1. 创建表2. 插入0-23小时的数据3. 查询并合并数据 在数据分析中&#xff0c;我们经常需要对特定时间段内的数据进行统计和分析。 例如&#xff0c;在名片进线的场景中&#xff0c;我们可能需要了解一天内每小时的名片进线数量。 然而&#xff0c;由于某些时间点可能没有数…

厦门凯酷全科技有限公司正规吗?

在这个短视频风起云涌的时代&#xff0c;抖音作为电商领域的黑马&#xff0c;正以惊人的速度改变着消费者的购物习惯与品牌的市场策略。在这场变革中&#xff0c;厦门凯酷全科技有限公司凭借其专业的抖音电商服务&#xff0c;在众多服务商中脱颖而出&#xff0c;成为众多品牌信…

SpringBoot配置类

在Spring Boot中&#xff0c;配置类是一种特殊的类&#xff0c;用于定义和配置Spring应用程序的各种组件、服务和属性。这些配置类通常使用Java注解来声明&#xff0c;并且可以通过Spring的依赖注入机制来管理和使用。 Spring 容器初始化时会加载被Component、Service、Reposi…

ADS项目笔记 1. 低噪声放大器LNA天线一体化设计

在传统射频结构的设计中&#xff0c;天线模块和有源电路部分相互分离&#xff0c;两者之间通过 50 Ω 传输线级联&#xff0c;这种设计需要在有源电路和天线之间建立无源网络&#xff0c;包括天线模块的输入匹配网络以及有源电路的匹配网络。这些无源网络不仅增加了系统的插入损…

Vue2+ElementUI:用计算属性实现搜索框功能

前言&#xff1a; 本文代码使用vue2element UI。 输入框搜索的功能&#xff0c;可以在前端通过计算属性过滤实现&#xff0c;也可以调用后端写好的接口。本文介绍的是通过计算属性对表格数据实时过滤&#xff0c;后附完整代码&#xff0c;代码中提供的是死数据&#xff0c;可…

【目标检测】用YOLOv8-Segment训练语义分割数据集(保姆级教学)

前言 这篇教程会手把手带你用 YOLOv8-Segment 搭建一个属于自己的分割任务项目。从环境配置到数据集准备&#xff0c;再到模型训练和测试&#xff0c;所有步骤都有详细说明&#xff0c;适合初学者使用。你将学会如何安装必要的软件&#xff0c;标注自己的数据&#xff0c;并使…

Elasticsearch:管理和排除 Elasticsearch 内存故障

作者&#xff1a;来自 Elastic Stef Nestor 随着 Elastic Cloud 提供可观察性、安全性和搜索等解决方案&#xff0c;我们将使用 Elastic Cloud 的用户范围从完整的运营团队扩大到包括数据工程师、安全团队和顾问。作为 Elastic 支持代表&#xff0c;我很乐意与各种各样的用户和…

前深度学习时代-经典的推荐算法

参考自《深度学习推荐系统》—— 王喆&#xff0c;用于学习记录。 1.协同过滤 “协同过滤”就是协同大家的反馈、评价和意见一起对海量的信息进行过滤&#xff0c;从中筛选出目标用户可能感兴趣的信息的推荐过程。 基于用户相似度进行推荐的协同过滤算法 UserCF 用户相似度…

两行命令搭建深度学习环境(Docker/torch2.5.1+cu118/命令行美化+插件),含完整的 Docker 安装步骤

深度学习环境的配置过于繁琐&#xff0c;所以我制作了两个基础的镜像&#xff0c;希望可以帮助大家节省时间&#xff0c;你可以选择其中一种进行安装&#xff0c;版本说明&#xff1a; base 版本基于 pytorch/pytorch:2.5.1-cuda11.8-cudnn9-devel&#xff0c;默认 python 版本…

WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇

WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇 WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇&#xff08;本文&#xff09; WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前言…

AI在电商平台中的创新应用:提升销售效率与用户体验的数字化转型

1. 引言 AI技术在电商平台的应用已不仅仅停留在基础的数据分析和自动化推荐上。随着人工智能的迅速发展&#xff0c;越来越多的电商平台开始将AI技术深度融合到用户体验、定价策略、供应链优化、客户服务等核心业务中&#xff0c;从而显著提升运营效率和用户满意度。在这篇文章…

Blossom:开源私有部署的markdown笔记软件

在信息化、数字化时代&#xff0c;我们每个人的生活和工作都离不开笔记和知识管理。从简单的待办事项&#xff0c;到复杂的项目计划&#xff0c;再到存储大量个人知识的工具&#xff0c;如何选择一个高效、便捷且符合个人需求的笔记软件&#xff0c;成了许多人的难题。最近在逛…

Linux debian系统安装ClamTk开源图形用户界面(GUI)杀毒软件

一、ClamTk简介 ClamTk 是一个基于 ClamAV 的开源图形用户界面&#xff08;GUI&#xff09;杀毒软件。它使用 GTK2-Perl 脚本构建而成&#xff0c;支持32位与64位操作系统。ClamTk 提供了一个直观的用户界面&#xff0c;使得用户无需深入了解命令行即可完成大部分操作。它具备…

Linux 进程信号的产生

目录 0.前言 1. 通过终端按键产生信号 1.1 CtrlC&#xff1a;发送 SIGINT 信号 1.2 Ctrl\&#xff1a;发送 SIGQUIT 信号 1.3 CtrlZ&#xff1a;发送 SIGTSTP 信号 2.调用系统命令向进程发信号 3.使用函数产生信号 3.1 kill 函数 3.2 raise 函数 3.3 abort 函数 4.由软件条件产…

【大数据学习 | HBASE高级】hive操作hbase

一般在查询hbase的数据的时候我们可以直接使用hbase的命令行或者是api进行查询就行了&#xff0c;但是在日常的计算过程中我们一般都不是为了查询&#xff0c;都是在查询的基础上进行二次计算&#xff0c;所以使用hbase的命令是没有办法进行数据计算的&#xff0c;并且对于hbas…

微信小程序 https://thirdwx.qlogo.cn 不在以下 downloadFile 合法域名列表中

授权登录后&#xff0c;拿到用户头像进行加载&#xff0c;但报错提示&#xff1a; https://thirdwx.qlogo.cn 不在以下 downloadFile 合法域名列表中 解决方法一&#xff08;未完全解决&#xff0c;临时处理&#xff09;&#xff1a;在微信开发者工具将不校验...勾上就可以访问…

rk3399开发环境使用Android 10初体验蓝牙功能

版本 日期 作者 变更表述 1.0 2024/11/10 于忠军 文档创建 零. 前言 由于Bluedroid的介绍文档有限&#xff0c;以及对Android的一些基本的知识需要了(Android 四大组件/AIDL/Framework/Binder机制/JNI/HIDL等)&#xff0c;加上需要掌握的语言包括Java/C/C等&#xff0…

1. Django中的URL调度器 (项目创建与简单测试)

1. 创建 Django 项目 运行以下命令创建一个名为 blog_project 的 Django 项目&#xff1a; django-admin startproject blog_project2. 创建博客应用 Django 中&#xff0c;项目可以包含多个应用。创建一个名为 blog 的应用&#xff1a; cd blog_project python manage.py …

数据结构(初阶4)---循环队列详解

循环队列 1.循环队列的结构  1).逻辑模式 2.实现接口  1).初始化  2).判断空和满  3).增加  4).删除  5).找头  6).找尾 3.循环队列的特点 1.循环队列的结构 1).逻辑模式 与队列是大同小异的&#xff0c; 其中还是有一个指向队列头的head指针&#xff0c; 也有一个指向尾…