基于小华例程3.2版本USB之usb_dev_msc工程深入代码详解

本篇日志记录单片机是如何使用USB识别flash成为一个U盘的。

USB中的MSC是指“Mass Storage Class”,即“大量存储设备类”。这是USB标准中定义的一种设备类别,它允许存储设备(如U盘、移动硬盘等)通过USB接口与计算机进行连接和通信。
MSC设备遵循特定的USB协议,使得它们能够被主机操作系统(如Windows、macOS、Linux)识别为可移动存储设备,从而无需安装额外的驱动程序即可使用。这种即插即用的特性使得MSC设备非常方便和实用。
当MSC设备连接到计算机时,它会模拟出类似硬盘驱动器的接口,让计算机能够像访问本地硬盘一样访问这些设备上的数据。因此,用户可以轻松地复制、删除或编辑这些设备上的文件。
总的来说,MSC为用户提供了一种简单、通用的方式来使用USB存储设备,极大地便利了数据交换和备份。

首先打开工程,进入main函数发现,跟CDC例程是一样的结构,但是usb_dev_init(&usb_dev, &stcPortIdentify, &user_desc, &usb_dev_msc_cbk, &user_cb);里面不再使用class_cdc_cbk,而是使用usb_dev_msc_cbk,也就是这个class类型决定了这个工程是一个msc设备,而不是其他。

对比CDC的结构体和msc的结构体:

// cdc 结构体
usb_dev_class_func  class_cdc_cbk = {
    &usb_dev_cdc_init,
    &usb_dev_cdc_deinit,
    &usb_dev_cdc_setup,
    NULL,
    &usb_dev_cdc_ctrlep_rxready,
    &usb_dev_cdc_getcfgdesc,
    &usb_dev_cdc_sof,
    &usb_dev_cdc_datain,
    &usb_dev_cdc_dataout,
    NULL,
    NULL,
};



// msc 结构体
usb_dev_class_func usb_dev_msc_cbk = {
    &usb_dev_msc_init,
    &usb_dev_msc_deinit,
    &usb_dev_msc_setup,
    NULL,
    NULL,
    &usb_dev_msc_getcfgdesc,
    NULL,
    &usb_dev_msc_datain,
    &usb_dev_msc_dataout,
    NULL,
    NULL,
};

msc设备少了一些回调函数,函数名功能差不多。下面来看各个回调函数是如何实现的,分别做什么用。

第一个初始化函数:

/**
 * 初始化存储设备接口配置
 * @brief 初始化USB存储设备的接口配置,为数据传输做准备。
 * @param [in] pdev 设备实例指针,用于指向当前USB设备。
 * @retval None 无返回值。
 */
void usb_dev_msc_init(void *pdev)
{
    // 先进行存储设备的去初始化操作,以确保资源被正确释放。
    usb_dev_msc_deinit(pdev);
    // 开启设备的IN端点,用于从设备向主机传输数据。
    usb_opendevep(pdev, MSC_IN_EP, MSC_EPIN_SIZE, EP_TYPE_BULK);
    // 开启设备的OUT端点,用于从主机向设备传输数据。
    usb_opendevep(pdev, MSC_OUT_EP, MSC_EPOUT_SIZE, EP_TYPE_BULK);
    // 初始化BOT协议,这是USB存储设备与主机通信的基本协议之一。
    msc_bot_init(pdev);
}

第二个函数deinit函数,(随着单片机应用的加深,之前认为init很重要,慢慢发现deini也很重要,单片机里面的很多操作都是对称的,比如初始化和去初始化,读和写,代开保护和关闭保护,开关中断,插入检测和拔出检测,数据的输入输出等)这个和初始化里面的操作都是对称的,也比较好理解。

/**
 * @brief  初始化存储设备接口配置
 * @param  [in] pdev        设备实例指针
 * @retval None
 */
void usb_dev_msc_deinit(void *pdev)
{
    // 关闭MSC的IN端点
    usb_shutdevep(pdev, MSC_IN_EP);
    // 关闭MSC的OUT端点
    usb_shutdevep(pdev, MSC_OUT_EP);
    // 销毁Bot协议层的初始化
    msc_bot_deinit(pdev);
}

接下来的函数,setup函数,一般简单的外设初始化中就包含了设置,复杂的外设初始化之后还需要再设置一次。或者有些是硬件初始化和数据初始化两个分开的操作。

/**
 * 处理MSC(Mass Storage Class)设置请求
 * 
 * @param pdev        设备实例指针
 * @param req         设置请求结构体指针
 * @retval status     操作状态,成功返回USB_DEV_OK,失败返回USB_DEV_FAIL
 */
uint8_t usb_dev_msc_setup(void *pdev, USB_SETUP_REQ *req)
{
    uint8_t u8Res = USB_DEV_OK;
    // 根据请求的类型处理
    switch (req->bmRequest & USB_REQ_TYPE_MASK) {
        case USB_REQ_TYPE_CLASS:
            // 根据请求的具体值处理
            switch (req->bRequest) {
                case BOT_GET_MAX_LUN:
                    // 获取最大LUN(逻辑单元编号)
                    if ((req->wValue == (uint16_t)0) &&
                        (req->wLength == (uint16_t)1) &&
                        ((req->bmRequest & 0x80U) == (uint8_t)0x80)) {
                        dev_msc_maxlun = msc_fops->GetMaxLun();
                        // 发送最大LUN信息
                        if (dev_msc_maxlun > 0U) {
                            usb_ctrldatatx(pdev, &dev_msc_maxlun, 1U);
                        } else {
                            usb_ctrlerr(pdev);
                            u8Res = USB_DEV_FAIL;
                        }
                    } else {
                        usb_ctrlerr(pdev);
                        u8Res = USB_DEV_FAIL;
                    }
                    break;
                case BOT_RESET:
                    // 执行设备重置
                    if ((req->wValue == (uint16_t)0) &&
                        (req->wLength == (uint16_t)0) &&
                        ((req->bmRequest & 0x80U) != (uint8_t)0x80)) {
                        msc_bot_rst(pdev);
                    } else {
                        usb_ctrlerr(pdev);
                        u8Res = USB_DEV_FAIL;
                    }
                    break;
                default:
                    // 处理未识别的类请求
                    usb_ctrlerr(pdev);
                    u8Res = USB_DEV_FAIL;
                    break;
            }
            break;
        case USB_REQ_TYPE_STANDARD:
            // 根据请求的具体值处理标准请求
            switch (req->bRequest) {
                case USB_REQ_GET_INTERFACE:
                    // 获取当前接口设置
                    usb_ctrldatatx(pdev, &dev_msc_altset, 1U);
                    break;
                case USB_REQ_SET_INTERFACE:
                    // 设置当前接口
                    dev_msc_altset = (uint8_t)(req->wValue);
                    break;
                case USB_REQ_CLEAR_FEATURE:
                    // 清除特性,涉及端点的关闭和重新打开
                    usb_flsdevep(pdev, (uint8_t)req->wIndex);
                    usb_shutdevep(pdev, (uint8_t)req->wIndex);
                    if ((((uint8_t)req->wIndex) & (uint16_t)0x80U) == (uint16_t)0x80) {
                        // 对IN端点进行操作
                        usb_opendevep(pdev, ((uint8_t)req->wIndex), MSC_EPIN_SIZE, EP_TYPE_BULK);
                    } else {
                        // 对OUT端点进行操作
                        usb_opendevep(pdev, ((uint8_t)req->wIndex), MSC_EPOUT_SIZE, EP_TYPE_BULK);
                    }
                    msc_bot_complete_clearfeature(pdev, (uint8_t)req->wIndex);
                    break;
                default:
                    // 忽略其他标准请求
                    break;
            }
            break;
        default:
            // 忽略其他类型的请求
            break;
    }
    return u8Res;
}

再下面一个函数,获取配置描述符,这个操作是跟PC链接之后,USB主机会主动跟从机获取这个信息。

/**
 * 获取配置描述符
 * @param [in] length 指向一个无符号短整型的指针,用于接收配置描述符的长度(单位:字节)
 * @retval 返回配置描述符的缓冲区指针
 */
uint8_t *usb_dev_msc_getcfgdesc(uint16_t *length)
{
    // 设置传入参数length的值为配置描述符的大小,并返回配置描述符的缓冲区指针
    *length = (uint16_t)sizeof(usb_dev_msc_cfgdesc);
    return usb_dev_msc_cfgdesc;
}

最后两个函数:处理数据的输入和输出。还要强调的是IN和OUT都是针对USB主机而言。这里实现很简单使用bot协议栈处理就行了。

/**
 * 处理数据输入(IN)到DATA
 * 
 * @param pdev        设备实例指针,用于标识特定的USB设备实例。
 * @param epnum       端点索引,标识进行数据传输的特定端点。
 * @retval None       该函数没有返回值。
 */
void usb_dev_msc_datain(void *pdev, uint8_t epnum)
{
    // 调用MSC(BOT)协议栈的DataIN处理函数,完成数据接收后的处理
    msc_bot_datain(pdev, epnum);
}

/**
 * 处理OUT方向的数据
 * @param pdev        设备实例指针,用于指向当前USB设备实例
 * @param epnum       端点索引,标识进行数据传输的端点号
 * @retval None       该函数没有返回值
 */
void usb_dev_msc_dataout(void *pdev, uint8_t epnum)
{
    // 调用MSC(BOT)协议栈的DataOut处理函数,继续处理OUT方向的数据传输
    msc_bot_dataout(pdev, epnum);
}

bot协议是一个啥东西呢?(USB的BOT(Bulk-Only Transport)协议是一种用于USB大量存储设备(如U盘和外部硬盘)的数据传输协议。BOT协议是USB Mass Storage Class(MSC)标准的一部分,它定义了数据如何在USB主机和大量存储设备之间传输。
BOT协议的主要特点如下:
1. **简洁性**:BOT协议设计简单,易于实现,使得USB存储设备能够快速被各种操作系统识别和使用。
2. **即插即用**:支持即插即用功能,用户将设备插入计算机后,操作系统通常会自动加载必要的驱动程序,使得设备能够立即使用。
3. **批量传输**:BOT协议使用USB的批量传输(Bulk Transfer)方式来传输数据。批量传输是一种无固定时序的数据传输方式,适合于大量数据的传输,因为它可以高效地利用USB总线的带宽。
4. **命令/数据分离**:BOT协议将命令(如读取、写入、格式化等)和数据传输分离,提高了数据传输的效率和灵活性。
5. **错误处理**:BOT协议包含了一套错误处理机制,以确保数据传输的可靠性和完整性。
由于BOT协议的简单性和高效性,它成为了USB存储设备中最常用的传输协议之一。然而,随着USB技术的发展,一些新的传输协议,如USB Attached SCSI (UAS) / USB Storage Transfer (UASP),提供了更高的性能和更复杂的特性,逐渐取代了BOT协议在一些高性能存储设备中的应用。
)作为USB的使用者,不用纠结这个协议,不知道大家看代码的时候有没有一种感觉,我必须要知道这个函数是干什么的,怎么实现的。其实大可不必,别人已经实现和封装好的,你会用就行了。除非你要自己编写USB协议栈。

看完初始化之后,很自然产生了一个疑问:既然是要读写大容量设备,那么如何读写呢?在程序里面是如何跟FLASH和SD的读写绑定在一起的呢?

先看一下程序的源文件结构:

 图中1表示USB的核心实现部分,这部分作为用户不用修改。图中2表示用户实现的部分,这部分是需要自己修改和实现的,2-a表示USB的描述,可以结合里面的内容根据自己喜欢自行定义。2-b这个函数就是USB和存储介质绑定的操作;2-c是用户自定义的回调函数,这个可以用来调试也可以实现一些实际需要的功能(比如检测USB插入了,那么某些操作就必须停下了),各个回调的时机是USB连上主机之后的状态改变。

接下来我们详细看看usb_dev_msc_msd文件,是如何实现flash和USB的绑定的,这个跟flash的按照地址读写有什么区别?

打开usb_dev_msc_msd.h,非常简单,仅仅定义了一个外部数组:extern const int8_t msc_inquirydata[]; 这个数组在usb_dev_msc_msd.c中定义,在下面这个函数中调用:其中msc_fops->pInquiry就绑定的这个数组。


/**
 * @brief 处理查询命令
 * @param [in] lun 逻辑单元号
 * @param [in] params 命令参数
 * @retval status 操作状态,返回0表示成功
 */
int8_t scsi_inquiry(uint8_t lun, uint8_t *params)
{
    const uint8_t *pPage; // 指向查询数据的指针
    uint16_t len; // 查询数据长度

    // 判断是否请求的是标准查询信息
    if ((params[1] & 0x01U) != 0U) {
        // 标准查询,使用预定义的Page00查询数据
        pPage = (const uint8_t *)MSC_Page00_Inquiry_Data;
        len   = LENGTH_INQUIRY_PAGE00;
    } else {
        // 非标准查询,从指定逻辑单元的查询数据开始处获取数据
        pPage = (uint8_t *)&msc_fops->pInquiry[lun * USB_DEV_INQUIRY_LENGTH];
        // 计算数据长度
        len   = (uint16_t)pPage[4] + (uint16_t)5;

        // 如果请求长度小于可用长度,则调整为请求长度
        if (params[4] <= len) {
            len = params[4];
        }
    }

    // 设置传输数据的长度
    MSC_BOT_DataLen = len;

    // 复制查询数据到MSC_BOT_Data缓冲区
    while (len != 0U) {
        len--;
        MSC_BOT_Data[len] = pPage[len];
    }

    return (int8_t)0; // 成功完成操作
}

在usb_dev_msc_mem.h中定义了一个结构体,USB_DEV_MSC_cbk_TypeDef

typedef struct {
    int8_t (* Init)(uint8_t lun);
    int8_t (* GetCapacity)(uint8_t lun, uint32_t *block_num, uint32_t *block_size);
    int8_t (* GetMaxLun)(void);
    int8_t (* IsReady)(uint8_t lun);
    int8_t (* Read)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
    int8_t (* Write)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
    int8_t (* IsWriteProtected)(uint8_t lun);
    int8_t *pInquiry;
} USB_DEV_MSC_cbk_TypeDef;

具体说明意思呢?看下面的注释:

/**
 * USB设备MSC(Mass Storage Class)回调函数类型定义
 * 该结构体定义了USB MSC类驱动所需的回调函数及其参数和返回类型。
 */
typedef struct {
    /**
     * 初始化函数
     * @param lun 逻辑单元号
     * @return 操作结果,0表示成功,其他值表示失败
     */
    int8_t (* Init)(uint8_t lun);

    /**
     * 获取容量函数
     * @param lun 逻辑单元号
     * @param block_num 指向变量的指针,用于接收块数量
     * @param block_size 指向变量的指针,用于接收块大小
     * @return 操作结果,0表示成功,其他值表示失败
     */
    int8_t (* GetCapacity)(uint8_t lun, uint32_t *block_num, uint32_t *block_size);

    /**
     * 获取最大逻辑单元号函数
     * @return 最大逻辑单元号
     */
    int8_t (* GetMaxLun)(void);

    /**
     * 检查逻辑单元是否准备就绪函数
     * @param lun 逻辑单元号
     * @return 操作结果,0表示就绪,其他值表示不就绪
     */
    int8_t (* IsReady)(uint8_t lun);

    /**
     * 读取函数
     * @param lun 逻辑单元号
     * @param buf 指向接收缓冲区的指针
     * @param blk_addr 要读取的块地址
     * @param blk_len 要读取的块数量
     * @return 操作结果,0表示成功,其他值表示失败
     */
    int8_t (* Read)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);

    /**
     * 写入函数
     * @param lun 逻辑单元号
     * @param buf 指向数据缓冲区的指针
     * @param blk_addr 写入的起始块地址
     * @param blk_len 要写入的块数量
     * @return 操作结果,0表示成功,其他值表示失败
     */
    int8_t (* Write)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);

    /**
     * 检查逻辑单元是否写保护函数
     * @param lun 逻辑单元号
     * @return 操作结果,0表示未写保护,非0表示写保护
     */
    int8_t (* IsWriteProtected)(uint8_t lun);

    /**
     * Inquiry数据指针
     * 用于SCSI Inquiry命令的响应数据。
     */
    int8_t *pInquiry;
} USB_DEV_MSC_cbk_TypeDef;

使用extern USB_DEV_MSC_cbk_TypeDef *msc_fops;定义一个结构体指针来方便其他操作。那么msc_fops绑定的是哪个结构体呢?继续看代码:

static USB_DEV_MSC_cbk_TypeDef flash_fops = {
    &msc_init,
    &msc_getcapacity,
    &msc_getmaxlun,
    &msc_ifready,
    &msc_read,
    &msc_write,
    &msc_ifwrprotected,
    (int8_t *)msc_inquirydata
};

/* Pointer to flash_fops */
USB_DEV_MSC_cbk_TypeDef *msc_fops = &flash_fops;

原来绑定的是一个静态函数数组结构体flash_fops,看到了static数组,我们应该感到很开心。因为出现这个就表示,已经要跟用户代码发生点关系了。根据《代码大全》中的表驱动经验,一般静态数组用来预设参数和函数,参数预设之后是用户可以选择的;函数预设之后,需要绑定用户实现的函数。这个结构体中一共有7个函数。这七个函数在这个结构体之前声明:

让我们来逐个看函数如何实现的。

 前面四个函数都是简单函数,初始化就是flash的初始化;获得容量就是我们提前定义好的宏,准备好和是否保护,这个都直接返回OK,就是默认都是可以操作的。

/**
 * @brief  initialize storage
 * @param  [in] lun          logic number
 * @retval status
 */
int8_t msc_init(uint8_t lun)
{
    BSP_W25QXX_Init();
    return LL_OK;
}

/**
 * @brief  Get Storage capacity
 * @param  [in] lun          logic number
 * @param  [in] block_num    sector number
 * @param  [in] block_size   sector size
 * @retval status
 */
int8_t msc_getcapacity(uint8_t lun, uint32_t *block_num, uint32_t *block_size)
{
    *block_size = 512U;
    *block_num  = W25Q64_MAX_ADDR / 512U;
    return LL_OK;
}

/**
 * @brief  Check if storage is ready
 * @param  [in] lun          logic number
 * @retval status
 */
int8_t  msc_ifready(uint8_t lun)
{
    USB_STATUS_REG |= (uint8_t)0X10;
    return LL_OK;
}

/**
 * @brief  Check if storage is write protected
 * @param  [in] lun          logic number
 * @retval status
 */
int8_t msc_ifwrprotected(uint8_t lun)
{
    return LL_OK;
}

继续看读的操作:本例程中只有一个外部存储就是flash,lun的值是主机设定的,.通过程序中的MSC_BOT_CBW_TypeDef 结构体进行设置。这个结构体是用于USB大量存储设备类(USB Mass Storage Class, MSC)中的BOT(Bulk-Only Transport)协议的命令块包裹(Command Block Wrapper, CBW)的。CBW是主机发送给设备的一个命令包,用于请求设备执行特定的操作,如读取、写入或格式化等。


/**
 * 从存储设备读取数据
 * @param lun 逻辑单元号
 * @param buf 用于存储读取数据的缓冲区
 * @param blk_addr 起始扇区地址
 * @param blk_len 要读取的扇区数量
 * @retval 返回操作状态,0表示成功,非0表示失败
 */
int8_t msc_read(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
    int8_t res = (int8_t)0; // 初始化返回值为成功

    USB_STATUS_REG |= 0x02U; // 设置USB状态寄存器,准备读取操作

    // 检查逻辑单元号是否为1,进行不同的操作
    if (lun == 1U) {
        // 如果之前有错误,则更新USB状态寄存器
        if (0 != res) {
            USB_STATUS_REG |= 0x08U;
        }
    } else {
        // 对于非逻辑单元1,调用BSP函数读取数据
        (void)BSP_W25QXX_Read(blk_addr * 512U, buf, (uint32_t)blk_len * 512U);
    }
    return res; // 返回操作结果
}

在USB使用flash模拟的U盘中,逻辑单元号码(Logical Unit Number,简称LUN)是从0开始的。LUN是用于标识存储设备中的逻辑单元的编号,每个逻辑单元可以被视作一个独立的存储设备。在USB大量存储设备类(USB Mass Storage Class,简称USB MSC)中,每个USB存储设备可以包含一个或多个LUN。
例如,一个USB闪存驱动器通常只有一个LUN,其LUN编号为0。但是,一些复杂的存储设备,如外置硬盘盒或多功能存储设备,可能会包含多个物理驱动器或分区,每个分区都会被分配一个不同的LUN编号,例如0、1、2等。
在USB MSC协议中,LUN编号是通过命令来访问和管理的。例如,CBW(Command Block Wrapper)命令中会包含目标LUN的信息,告诉存储设备要访问哪个逻辑单元。同样,在存储设备返回的数据中,如CSW(Command Status Wrapper)响应中,也会包含相关的LUN信息。
因此,逻辑单元号码是从0开始的,而不是1。在处理USB存储设备时,操作系统和应用程序会根据LUN编号来访问和操作相应的逻辑单元。

这段代码中,存在疑问项目是为什么lun==1的时候,是这个操作?:在USB存储设备中,逻辑单元号(LUN)是用来标识不同的存储设备或分区的一种机制。每个逻辑单元可以被视作一个独立的存储设备,可以独立地被主机操作系统访问和管理。
在您提供的代码片段中,`msc_read` 函数是一个模拟USB存储设备的函数,它接受逻辑单元号、缓冲区、起始扇区地址和要读取的扇区数量作为参数。这个函数的目的是从指定逻辑单元的指定扇区读取数据,并将其存储在提供的缓冲区中。
代码中的逻辑单元号检查 `if (lun == 1U)` 是用来判断是否针对逻辑单元1进行操作。如果逻辑单元号是1,代码会执行特定的操作,这可能是处理错误状态或更新USB状态寄存器。对于非逻辑单元1的情况,代码会调用一个名为 `BSP_W25QXX_Read` 的BSP(Board Support Package)函数来执行实际的读取操作。
这种逻辑单元号检查可能是为了在处理逻辑单元1时执行一些特殊的处理,比如重置错误状态或执行一些设备特定的操作。对于非逻辑单元1的情况,则调用BSP函数来执行标准的读取操作。
需要注意的是,具体的操作逻辑和代码实现可能会根据实际的存储设备和开发环境而有所不同。因此,上述代码的解释是基于一般的USB存储设备操作和BSP函数的使用习惯。
继续看flash的写操作函数:因为flash可以按照字节读取,但是写入的话跟扇区有关系。

/**
 * @brief 无需校验的写入函数
 * 
 * 该函数用于向W25QXX(一种SPI Flash)芯片写入数据,无需进行写入校验。函数将数据按页写入,自动处理跨页写入的情况。
 * 
 * @param pBuffer 指向需要写入数据的缓冲区的指针。
 * @param WriteAddr 写入的起始地址。
 * @param NumByteToWrite 需要写入的数据字节数。
 */
static void W25QXX_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
    uint16_t pageremain; // 用于计算剩余可写入的页字节数

    // 计算从当前地址开始到页末的剩余字节数
    pageremain = (uint16_t)(256U - WriteAddr % 256U);
    // 如果剩余可写入字节小于等于需写入字节,则将pageremain更新为需写入字节数
    if (NumByteToWrite <= pageremain) {
        pageremain = NumByteToWrite;
    }

    // 循环,直到所有数据都被写入
    for (;;) {
        // 调用BSP_W25QXX_Write函数写入数据,写入字节数为pageremain
        (void)BSP_W25QXX_Write(WriteAddr, pBuffer, pageremain);

        // 如果本次写入的字节数等于需写入的总字节数,则退出循环
        if (NumByteToWrite == pageremain) {
            break;
        } else { // 如果本次写入的字节数小于需写入的总字节数,则更新写入参数继续写入
            pBuffer += pageremain; // 更新数据缓冲区指针
            WriteAddr += pageremain; // 更新写入地址

            NumByteToWrite -= pageremain; // 更新剩余待写入字节数
            // 根据剩余待写入字节数确定下一次写入的字节数
            if (NumByteToWrite > 256U) {
                pageremain = 256U; // 如果剩余字数多于一页,则下一次写入256字节
            } else {
                pageremain = NumByteToWrite; // 如果剩余字数不足一页,则下一次写入剩余的字数
            }
        }
    }
}

/**
 * @brief 写入SPI Flash函数
 * 
 * 该函数用于将数据写入SPI Flash指定地址。函数首先会检查待写入的扇区是否为空白,
 * 如果非空白,则先擦除扇区,再写入数据。函数支持跨扇区写入。
 * 
 * @param pbuf 指向待写入数据的缓冲区的指针
 * @param WriteAddr 写入的起始地址
 * @param NumByteToWrite 待写入的字节数
 */
static void SpiFlashWrite(uint8_t *pbuf, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
    uint32_t secpos;  // 计算待写入地址所在的扇区位置
    uint16_t secoff;  // 计算待写入地址在扇区内的偏移
    uint16_t secremain; // 计算当前扇区剩余可写入的字节数
    uint16_t i;
    uint8_t *pu8CbBuf; // 定义一个指针,用于备份扇区数据
    pu8CbBuf = u8CopybackBuf; // 指向备份缓冲区
    secpos = WriteAddr / W25Q64_SECTOR_SIZE; // 计算写入地址所在的扇区
    secoff = (uint16_t)(WriteAddr % W25Q64_SECTOR_SIZE); // 计算写入地址在扇区内的偏移
    secremain = (uint16_t)(W25Q64_SECTOR_SIZE - secoff); // 计算当前扇区剩余字节数
    if (NumByteToWrite <= secremain) {
        /* 如果待写入字节数小于等于当前扇区剩余字节数,则调整为等于剩余字节数 */
        secremain = NumByteToWrite;
    }
    for (;;) {
        (void)BSP_W25QXX_Read(secpos * W25Q64_SECTOR_SIZE, pu8CbBuf, W25Q64_SECTOR_SIZE); // 从Flash读取整个扇区的数据到备份缓冲区
        /* 检查扇区是否为空白 */
        for (i = 0U; i < secremain; i++) {
            if (pu8CbBuf[secoff + i] != 0XFFU) {
                break; // 如果发现有非全0xFF的字节,则退出循环
            }
        }
        if (i < secremain) {
            /* 如果扇区非空白,需要擦除 */
            (void)BSP_W25QXX_EraseSector(secpos * W25Q64_SECTOR_SIZE); // 擦除当前扇区
            /* 先备份需要写入的数据 */
            for (i = 0U; i < secremain; i++) {
                pu8CbBuf[i + secoff] = pbuf[i]; // 将待写入数据复制到备份缓冲区
            }
            /* 擦除后写回数据 */
            W25QXX_Write_NoCheck(pu8CbBuf, secpos * W25Q64_SECTOR_SIZE, (uint16_t)W25Q64_SECTOR_SIZE); // 写入整个扇区的数据

        } else {
            /* 如果扇区为空白,直接写入数据 */
            W25QXX_Write_NoCheck(pbuf, WriteAddr, secremain); // 直接写入数据到指定地址
        }
        if (NumByteToWrite == secremain) {
            break; // 如果已经写入完所有指定字节,则退出循环
        } else {
            /* 准备写入下一个扇区 */
            secpos++;
            secoff = 0U;

            pbuf += secremain; // 更新待写入数据的指针位置
            WriteAddr += secremain; // 更新下一个写入地址
            NumByteToWrite -= secremain; // 更新剩余待写入字节数
            if (NumByteToWrite > W25Q64_SECTOR_SIZE) {
                secremain = (uint16_t)W25Q64_SECTOR_SIZE; // 如果剩余字节数多于一个扇区大小,则设置下一个扇区大小为整个扇区
            } else {
                secremain = NumByteToWrite; // 如果剩余字节数不足一个扇区,则设置下一个扇区大小为剩余字节数
            }
        }
    }
}

结合注释,应该能看懂是什么意思。注意的是:在NAND闪存(如SD卡)中,数据的改写通常需要先进行擦除操作。这是因为NAND闪存的工作原理是使用电荷来存储数据,当电荷流失时,数据会丢失。为了保证数据的持久性,NAND闪存通常采用页内写入和块擦除的方式。
1. **页内写入**:在NAND闪存中,每个页(Page)可以被分成多个块(Block),每个块可以存储一定数量的数据。在页内写入时,如果一个页内有多个块已经有数据,只需要擦除需要改写数据的块,然后将新数据写入该块。
2. **块擦除**:由于NAND闪存的写入操作需要先进行擦除,所以在需要改写数据时,即使只改写一个扇区(块),也需要先擦除整个块。这是因为擦除操作是按块进行的,不能单独擦除一个扇区。
因此,如果你想要改写一个扇区内的数据,即使这个扇区内只有一个块需要改写,你也必须先擦除整个块,然后再进行写入操作。这是因为擦除操作会清除块内的所有数据,所以在写入新数据之前,你需要确保块是空的或者已经被成功擦除。

msc_write函数就非常简单了,因为写里面遇到的问题,上面两个函数都处理完了。

/**
 * @brief  Write data to storage devices
 * @param  [in] lun          logic number
 * @param  [in] buf          data buffer be written
 * @param  [in] blk_addr     sector address
 * @param  [in] blk_len      sector count
 * @retval status
 */
int8_t msc_write(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
    int8_t res = (int8_t)0;

    USB_STATUS_REG |= 0X01U;
    if (lun == 1U) {
        if (0 != res) {
            USB_STATUS_REG |= 0X04U;
        }
    } else {
        SpiFlashWrite(buf, blk_addr * 512U, blk_len * 512U);
    }
    return res;
}

对于用户移植而言,重要的就是source文件夹下面的几个文件,写好外部存储的读写接口就可以了。如果后面遇到问题,不要怀疑USB,而要检查自己写的读写接口是否满足了要求。

有个彩蛋:在移植fat32中,我发现读写IO,跟USB的一模一样!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值