esp-idf-psram-msc/main/tusb_msc_psram.c
2024-06-13 13:11:43 +02:00

607 lines
19 KiB
C

/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "common/tusb_common.h"
#include "diskio.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_check.h"
#include "esp_vfs_fat.h"
#include "diskio_impl.h"
#include "esp_partition.h"
#include "tusb_msc_psram.h"
#include "vfs_fat_internal.h"
#include "tinyusb.h"
#include "class/msc/msc_device.h"
#include "tusb_msc_storage.h"
#include "esp_vfs_fat.h"
static const char *TAG = "tinyusb_msc_psram";
typedef struct {
bool is_fat_mounted;
const char *base_path;
uint8_t* memory;
size_t sectors;
uint8_t pdrv;
esp_err_t (*mount)(BYTE pdrv);
esp_err_t (*unmount)(void);
tusb_msc_callback_t callback_mount_changed;
tusb_msc_callback_t callback_premount_changed;
int max_files;
} tinyusb_msc_psram_handle_s; /*!< MSC object */
/* handle of tinyusb driver connected to application */
static tinyusb_msc_psram_handle_s *s_storage_handle;
static bool _psram_read (uint32_t lba, uint32_t offset,size_t size, uint8_t *dest) {
/*!< sector read function */
if(!s_storage_handle || !s_storage_handle->memory){
return false;
}
memcpy(dest, s_storage_handle->memory + lba * FF_MAX_SS + offset, size);
return true;
}
static bool _psram_write (uint32_t lba, uint32_t offset,size_t size, const uint8_t *src) {
/*!< sector write function */
if(!s_storage_handle || !s_storage_handle->memory){
return false;
}
memcpy(s_storage_handle->memory + lba * FF_MAX_SS + offset, src, size);
return true;
}
static DSTATUS _diskio_psram_status (unsigned char pdrv);
static DSTATUS _diskio_psram_init (unsigned char pdrv) {
/*!< disk initialization function */
return _diskio_psram_status(pdrv);
}
static DSTATUS _diskio_psram_status (unsigned char pdrv) {
/*!< disk status check function */
if(s_storage_handle && s_storage_handle->memory && s_storage_handle->pdrv == pdrv) {
return 0;
}
return STA_NOINIT;
}
static DRESULT _diskio_psram_read (unsigned char pdrv, unsigned char* buff, uint32_t sector, unsigned count) {
/*!< sector read function */
if(!s_storage_handle || !s_storage_handle->memory || pdrv!=s_storage_handle->pdrv){
return RES_NOTRDY;
}
if(_psram_read(sector, 0, FF_MAX_SS*count, buff)){
return RES_OK;
}
return RES_NOTRDY;
}
static DRESULT _diskio_psram_write (unsigned char pdrv, const unsigned char* buff, uint32_t sector, unsigned count) {
/*!< sector write function */
if(!s_storage_handle || !s_storage_handle->memory || pdrv!=s_storage_handle->pdrv){
return RES_NOTRDY;
}
if(_psram_write(sector, 0, FF_MAX_SS*count, buff)){
return RES_OK;
}
return RES_NOTRDY;
}
static DRESULT _diskio_psram_ioctl(unsigned char pdrv, unsigned char cmd,
void *buff) {
/*!< function to get info about disk and do some misc operations */
if (!s_storage_handle || !s_storage_handle->memory ||
pdrv != s_storage_handle->pdrv) {
return RES_NOTRDY;
}
switch (cmd) {
case CTRL_SYNC:
/*nothing to do*/
break;
case GET_SECTOR_COUNT:
((unsigned int *)buff)[0] = s_storage_handle->sectors;
break;
case GET_SECTOR_SIZE:
((unsigned int *)buff)[0] = FF_MAX_SS;
break;
case GET_BLOCK_SIZE:
((unsigned int *)buff)[0] =
1; // byte access. todo: check if 4 makes sense
break;
case CTRL_TRIM:
/*nothing to do*/
break;
default:
return RES_PARERR;
}
return RES_OK;
}
static ff_diskio_impl_t *s_diskio_psram;
static esp_err_t _mount_psram(BYTE pdrv)
{
ff_diskio_register(pdrv, s_diskio_psram);
s_storage_handle->pdrv = pdrv;
return ESP_OK;
}
static esp_err_t _unmount_psram(void)
{
if (s_storage_handle->pdrv == 0xff) {
ESP_LOGE(TAG, "Invalid state");
return ESP_ERR_INVALID_STATE;
}
char drv[3] = {(char)('0' + s_storage_handle->pdrv), ':', 0};
f_mount(0, drv, 0);
ff_diskio_unregister(s_storage_handle->pdrv);
return ESP_OK;
}
static esp_err_t msc_psram_read_sector(uint32_t lba,
uint32_t offset,
size_t size,
void *dest)
{
assert(s_storage_handle);
if (_psram_read(lba, offset, size, dest)) {
return ESP_OK;
}
return ESP_FAIL;
}
static esp_err_t msc_psram_write_sector(uint32_t lba,
uint32_t offset,
size_t size,
const void *src)
{
assert(s_storage_handle);
if (s_storage_handle->is_fat_mounted) {
ESP_LOGE(TAG, "can't write, FAT mounted");
return ESP_ERR_INVALID_STATE;
}
size_t sector_size = tinyusb_msc_psram_get_sector_size();
size_t temp = 0;
size_t addr = 0; // Address of the data to be read, relative to the beginning of the partition.
ESP_RETURN_ON_FALSE(!__builtin_umul_overflow(lba, sector_size, &temp), ESP_ERR_INVALID_SIZE, TAG, "overflow lba %lu sector_size %u", lba, sector_size);
ESP_RETURN_ON_FALSE(!__builtin_uadd_overflow(temp, offset, &addr), ESP_ERR_INVALID_SIZE, TAG, "overflow addr %u offset %lu", temp, offset);
if (addr % sector_size != 0 || size % sector_size != 0) {
ESP_LOGE(TAG, "Invalid Argument lba(%lu) offset(%lu) size(%u) sector_size(%u)", lba, offset, size, sector_size);
return ESP_ERR_INVALID_ARG;
}
if (_psram_write(lba, offset, size, src)) {
return ESP_OK;
}
return ESP_FAIL;
}
static esp_err_t _mount(char *drv, FATFS *fs)
{
void *workbuf = NULL;
const size_t workbuf_size = 4096;
esp_err_t ret;
// Try to mount partition
FRESULT fresult = f_mount(fs, drv, 1);
if (fresult != FR_OK) {
ESP_LOGW(TAG, "f_mount failed (%d)", fresult);
if (!((fresult == FR_NO_FILESYSTEM || fresult == FR_INT_ERR))) {
ret = ESP_FAIL;
goto fail;
}
workbuf = ff_memalloc(workbuf_size);
if (workbuf == NULL) {
ret = ESP_ERR_NO_MEM;
goto fail;
}
size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size(
CONFIG_WL_SECTOR_SIZE,
4096);
ESP_LOGW(TAG, "formatting card, allocation unit size=%d", alloc_unit_size);
const MKFS_PARM opt = {(BYTE)FM_FAT, 0, 0, 0, alloc_unit_size};
fresult = f_mkfs("", &opt, workbuf, workbuf_size); // Use default volume
if (fresult != FR_OK) {
ret = ESP_FAIL;
ESP_LOGE(TAG, "f_mkfs failed (%d)", fresult);
goto fail;
}
free(workbuf);
workbuf = NULL;
fresult = f_mount(fs, drv, 0);
if (fresult != FR_OK) {
ret = ESP_FAIL;
ESP_LOGE(TAG, "f_mount failed after formatting (%d)", fresult);
goto fail;
}
}
return ESP_OK;
fail:
if (workbuf) {
free(workbuf);
}
return ret;
}
esp_err_t tinyusb_msc_psram_mount(const char *base_path)
{
esp_err_t ret = ESP_OK;
assert(s_storage_handle);
if (s_storage_handle->is_fat_mounted) {
return ESP_OK;
}
tusb_msc_callback_t cb = s_storage_handle->callback_premount_changed;
if (cb) {
tinyusb_msc_event_t event = {
.type = TINYUSB_MSC_EVENT_PREMOUNT_CHANGED,
.mount_changed_data = {
.is_mounted = s_storage_handle->is_fat_mounted
}
};
cb(&event);
}
if (!base_path) {
base_path = CONFIG_TINYUSB_MSC_MOUNT_PATH;
}
// connect driver to FATFS
BYTE pdrv = 0xFF;
ESP_RETURN_ON_ERROR(ff_diskio_get_drive(&pdrv), TAG,
"The maximum count of volumes is already mounted");
char drv[3] = {(char)('0' + pdrv), ':', 0};
ESP_GOTO_ON_ERROR((s_storage_handle->mount)(pdrv), fail, TAG, "Failed pdrv=%d", pdrv);
FATFS *fs = NULL;
ret = esp_vfs_fat_register(base_path, drv, s_storage_handle->max_files, &fs);
if (ret == ESP_ERR_INVALID_STATE) {
ESP_LOGD(TAG, "it's okay, already registered with VFS");
} else if (ret != ESP_OK) {
ESP_LOGE(TAG, "esp_vfs_fat_register failed (0x%x)", ret);
goto fail;
}
ESP_GOTO_ON_ERROR(_mount(drv, fs), fail, TAG, "Failed _mount");
s_storage_handle->is_fat_mounted = true;
s_storage_handle->base_path = base_path;
cb = s_storage_handle->callback_mount_changed;
if (cb) {
tinyusb_msc_event_t event = {
.type = TINYUSB_MSC_EVENT_MOUNT_CHANGED,
.mount_changed_data = {
.is_mounted = s_storage_handle->is_fat_mounted
}
};
cb(&event);
}
return ret;
fail:
if (fs) {
esp_vfs_fat_unregister_path(base_path);
}
ff_diskio_unregister(pdrv);
s_storage_handle->is_fat_mounted = false;
ESP_LOGW(TAG, "Failed to mount storage (0x%x)", ret);
return ret;
}
esp_err_t tinyusb_msc_psram_unmount(void)
{
if (!s_storage_handle) {
return ESP_FAIL;
}
if (!s_storage_handle->is_fat_mounted) {
return ESP_OK;
}
tusb_msc_callback_t cb = s_storage_handle->callback_premount_changed;
if (cb) {
tinyusb_msc_event_t event = {
.type = TINYUSB_MSC_EVENT_PREMOUNT_CHANGED,
.mount_changed_data = {
.is_mounted = s_storage_handle->is_fat_mounted
}
};
cb(&event);
}
esp_err_t err = (s_storage_handle->unmount)();
if (err) {
return err;
}
err = esp_vfs_fat_unregister_path(s_storage_handle->base_path);
s_storage_handle->base_path = NULL;
s_storage_handle->is_fat_mounted = false;
cb = s_storage_handle->callback_mount_changed;
if (cb) {
tinyusb_msc_event_t event = {
.type = TINYUSB_MSC_EVENT_MOUNT_CHANGED,
.mount_changed_data = {
.is_mounted = s_storage_handle->is_fat_mounted
}
};
cb(&event);
}
return err;
}
uint32_t tinyusb_msc_psram_get_sector_count(void)
{
assert(s_storage_handle);
return s_storage_handle->sectors;
}
uint32_t tinyusb_msc_psram_get_sector_size(void)
{
return FF_MAX_SS;
}
esp_err_t tinyusb_msc_storage_init_psram(const tinyusb_msc_psram_config_t *config)
{
assert(!s_storage_handle);
assert(!s_diskio_psram);
assert(config);
assert(config->memory);
s_diskio_psram = (ff_diskio_impl_t *)malloc(sizeof(ff_diskio_impl_t));
ESP_RETURN_ON_FALSE(s_diskio_psram, ESP_ERR_NO_MEM, TAG, "could not allocate new handle for diskio_psram");
s_diskio_psram->init = &_diskio_psram_init;
s_diskio_psram->status = &_diskio_psram_status;
s_diskio_psram->read = &_diskio_psram_read;
s_diskio_psram->write = &_diskio_psram_write;
s_diskio_psram->ioctl = &_diskio_psram_ioctl;
s_storage_handle = (tinyusb_msc_psram_handle_s *)malloc(sizeof(tinyusb_msc_psram_handle_s));
ESP_RETURN_ON_FALSE(s_storage_handle, ESP_ERR_NO_MEM, TAG, "could not allocate new handle for storage");
s_storage_handle->mount = &_mount_psram;
s_storage_handle->unmount = &_unmount_psram;
s_storage_handle->is_fat_mounted = false;
s_storage_handle->base_path = NULL;
s_storage_handle->memory = config->memory;
s_storage_handle->sectors = config->memory_size / FF_MAX_SS;
// In case the user does not set mount_config.max_files
// and for backward compatibility with versions <1.4.2
// max_files is set to 2
const int max_files = config->mount_config.max_files;
s_storage_handle->max_files = max_files > 0 ? max_files : 2;
/* Callbacks setting up*/
if (config->callback_mount_changed) {
tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED, config->callback_mount_changed);
} else {
tinyusb_msc_unregister_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED);
}
if (config->callback_premount_changed) {
tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_PREMOUNT_CHANGED, config->callback_premount_changed);
} else {
tinyusb_msc_unregister_callback(TINYUSB_MSC_EVENT_PREMOUNT_CHANGED);
}
return ESP_OK;
}
void tinyusb_msc_psram_deinit(void)
{
assert(s_storage_handle);
free(s_storage_handle);
s_storage_handle = NULL;
}
esp_err_t tinyusb_msc_register_callback(tinyusb_msc_event_type_t event_type,
tusb_msc_callback_t callback)
{
assert(s_storage_handle);
switch (event_type) {
case TINYUSB_MSC_EVENT_MOUNT_CHANGED:
s_storage_handle->callback_mount_changed = callback;
return ESP_OK;
case TINYUSB_MSC_EVENT_PREMOUNT_CHANGED:
s_storage_handle->callback_premount_changed = callback;
return ESP_OK;
default:
ESP_LOGE(TAG, "Wrong event type");
return ESP_ERR_INVALID_ARG;
}
}
esp_err_t tinyusb_msc_unregister_callback(tinyusb_msc_event_type_t event_type)
{
assert(s_storage_handle);
switch (event_type) {
case TINYUSB_MSC_EVENT_MOUNT_CHANGED:
s_storage_handle->callback_mount_changed = NULL;
return ESP_OK;
case TINYUSB_MSC_EVENT_PREMOUNT_CHANGED:
s_storage_handle->callback_premount_changed = NULL;
return ESP_OK;
default:
ESP_LOGE(TAG, "Wrong event type");
return ESP_ERR_INVALID_ARG;
}
}
bool tinyusb_msc_psram_in_use_by_usb_host(void)
{
assert(s_storage_handle);
return !s_storage_handle->is_fat_mounted;
}
/* TinyUSB MSC callbacks
********************************************************************* */
/** SCSI ASC/ASCQ codes. **/
/** User can add and use more codes as per the need of the application **/
#define SCSI_CODE_ASC_MEDIUM_NOT_PRESENT 0x3A /** SCSI ASC code for 'MEDIUM NOT PRESENT' **/
#define SCSI_CODE_ASC_INVALID_COMMAND_OPERATION_CODE 0x20 /** SCSI ASC code for 'INVALID COMMAND OPERATION CODE' **/
#define SCSI_CODE_ASCQ 0x00
// Invoked when received SCSI_CMD_INQUIRY
// Application fill vendor id, product id and revision with string up to 8, 16, 4 characters respectively
void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4])
{
(void) lun;
const char vid[] = "TinyUSB";
const char pid[] = "Flash Storage";
const char rev[] = "0.1";
memcpy(vendor_id, vid, strlen(vid));
memcpy(product_id, pid, strlen(pid));
memcpy(product_rev, rev, strlen(rev));
}
// Invoked when received Test Unit Ready command.
// return true allowing host to read/write this LUN e.g SD card inserted
bool tud_msc_test_unit_ready_cb(uint8_t lun)
{
(void) lun;
bool result = false;
if (s_storage_handle->is_fat_mounted) {
tud_msc_set_sense(lun, SCSI_SENSE_NOT_READY, SCSI_CODE_ASC_MEDIUM_NOT_PRESENT, SCSI_CODE_ASCQ);
result = false;
} else {
if (tinyusb_msc_psram_unmount() != ESP_OK) {
ESP_LOGW(TAG, "tud_msc_test_unit_ready_cb() unmount Fails");
}
result = true;
}
return result;
}
// Invoked when received SCSI_CMD_READ_CAPACITY_10 and SCSI_CMD_READ_FORMAT_CAPACITY to determine the disk size
// Application update block count and block size
void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size)
{
(void) lun;
uint32_t sec_count = tinyusb_msc_psram_get_sector_count();
uint32_t sec_size = tinyusb_msc_psram_get_sector_size();
*block_count = sec_count;
*block_size = (uint16_t)sec_size;
}
// Invoked when received Start Stop Unit command
// - Start = 0 : stopped power mode, if load_eject = 1 : unload disk storage
// - Start = 1 : active mode, if load_eject = 1 : load disk storage
bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject)
{
(void) lun;
(void) power_condition;
if (load_eject && !start) {
if (tinyusb_msc_psram_mount(s_storage_handle->base_path) != ESP_OK) {
ESP_LOGW(TAG, "tud_msc_start_stop_cb() mount Fails");
}
}
return true;
}
// Invoked when received SCSI READ10 command
// - Address = lba * BLOCK_SIZE + offset
// - Application fill the buffer (up to bufsize) with address contents and return number of read byte.
int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize)
{
esp_err_t err = msc_psram_read_sector(lba, offset, bufsize, buffer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "msc_psram_read_sector failed: 0x%x", err);
return 0;
}
return bufsize;
}
// Invoked when received SCSI WRITE10 command
// - Address = lba * BLOCK_SIZE + offset
// - Application write data from buffer to address contents (up to bufsize) and return number of written byte.
int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize)
{
esp_err_t err = msc_psram_write_sector(lba, offset, bufsize, buffer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "msc_psram_write_sector failed: 0x%x", err);
return 0;
}
return bufsize;
}
/**
* Invoked when received an SCSI command not in built-in list below.
* - READ_CAPACITY10, READ_FORMAT_CAPACITY, INQUIRY, TEST_UNIT_READY, START_STOP_UNIT, MODE_SENSE6, REQUEST_SENSE
* - READ10 and WRITE10 has their own callbacks
*
* \param[in] lun Logical unit number
* \param[in] scsi_cmd SCSI command contents which application must examine to response accordingly
* \param[out] buffer Buffer for SCSI Data Stage.
* - For INPUT: application must fill this with response.
* - For OUTPUT it holds the Data from host
* \param[in] bufsize Buffer's length.
*
* \return Actual bytes processed, can be zero for no-data command.
* \retval negative Indicate error e.g unsupported command, tinyusb will \b STALL the corresponding
* endpoint and return failed status in command status wrapper phase.
*/
int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void *buffer, uint16_t bufsize)
{
int32_t ret;
switch (scsi_cmd[0]) {
case SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL:
/* SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL is the Prevent/Allow Medium Removal
command (1Eh) that requests the library to enable or disable user access to
the storage media/partition. */
ret = 0;
break;
default:
ESP_LOGW(TAG, "tud_msc_scsi_cb() invoked: %d", scsi_cmd[0]);
tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, SCSI_CODE_ASC_INVALID_COMMAND_OPERATION_CODE, SCSI_CODE_ASCQ);
ret = -1;
break;
}
return ret;
}
// Invoked when device is unmounted
void tud_umount_cb(void)
{
if (tinyusb_msc_psram_mount(s_storage_handle->base_path) != ESP_OK) {
ESP_LOGW(TAG, "tud_umount_cb() mount Fails");
}
}
// Invoked when device is mounted (configured)
void tud_mount_cb(void)
{
tinyusb_msc_psram_unmount();
}
/*********************************************************************** TinyUSB MSC callbacks*/