SD Driver: reinit sd card on error (#2493)
* SD Driver: reinit sd card on error * SD Driver: cleanup fatfs bindings * Storage: optimized glue * Storage: move fatfs initialization to appropriate subsystems, minor code cleanup * SD Driver: minor code cleanup Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									6ec62f48f9
								
							
						
					
					
						commit
						f7024cff78
					
				| @ -73,29 +73,34 @@ uint32_t storage_data_get_timestamp(StorageData* storage) { | |||||||
| 
 | 
 | ||||||
| /****************** storage glue ******************/ | /****************** storage glue ******************/ | ||||||
| 
 | 
 | ||||||
| bool storage_has_file(const File* file, StorageData* storage_data) { | static StorageFile* storage_get_file(const File* file, StorageData* storage) { | ||||||
|     bool result = false; |     StorageFile* storage_file_ref = NULL; | ||||||
| 
 | 
 | ||||||
|     StorageFileList_it_t it; |     StorageFileList_it_t it; | ||||||
|     for(StorageFileList_it(it, storage_data->files); !StorageFileList_end_p(it); |     for(StorageFileList_it(it, storage->files); !StorageFileList_end_p(it); | ||||||
|         StorageFileList_next(it)) { |         StorageFileList_next(it)) { | ||||||
|         const StorageFile* storage_file = StorageFileList_cref(it); |         StorageFile* storage_file = StorageFileList_ref(it); | ||||||
| 
 | 
 | ||||||
|         if(storage_file->file->file_id == file->file_id) { |         if(storage_file->file->file_id == file->file_id) { | ||||||
|             result = true; |             storage_file_ref = storage_file; | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return result; |     return storage_file_ref; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool storage_path_already_open(FuriString* path, StorageFileList_t array) { | bool storage_has_file(const File* file, StorageData* storage) { | ||||||
|  |     return storage_get_file(file, storage) != NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool storage_path_already_open(FuriString* path, StorageData* storage) { | ||||||
|     bool open = false; |     bool open = false; | ||||||
| 
 | 
 | ||||||
|     StorageFileList_it_t it; |     StorageFileList_it_t it; | ||||||
| 
 | 
 | ||||||
|     for(StorageFileList_it(it, array); !StorageFileList_end_p(it); StorageFileList_next(it)) { |     for(StorageFileList_it(it, storage->files); !StorageFileList_end_p(it); | ||||||
|  |         StorageFileList_next(it)) { | ||||||
|         const StorageFile* storage_file = StorageFileList_cref(it); |         const StorageFile* storage_file = StorageFileList_cref(it); | ||||||
| 
 | 
 | ||||||
|         if(furi_string_cmp(storage_file->path, path) == 0) { |         if(furi_string_cmp(storage_file->path, path) == 0) { | ||||||
| @ -108,43 +113,15 @@ bool storage_path_already_open(FuriString* path, StorageFileList_t array) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void storage_set_storage_file_data(const File* file, void* file_data, StorageData* storage) { | void storage_set_storage_file_data(const File* file, void* file_data, StorageData* storage) { | ||||||
|     StorageFile* founded_file = NULL; |     StorageFile* storage_file_ref = storage_get_file(file, storage); | ||||||
| 
 |     furi_check(storage_file_ref != NULL); | ||||||
|     StorageFileList_it_t it; |     storage_file_ref->file_data = file_data; | ||||||
| 
 |  | ||||||
|     for(StorageFileList_it(it, storage->files); !StorageFileList_end_p(it); |  | ||||||
|         StorageFileList_next(it)) { |  | ||||||
|         StorageFile* storage_file = StorageFileList_ref(it); |  | ||||||
| 
 |  | ||||||
|         if(storage_file->file->file_id == file->file_id) { |  | ||||||
|             founded_file = storage_file; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     furi_check(founded_file != NULL); |  | ||||||
| 
 |  | ||||||
|     founded_file->file_data = file_data; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void* storage_get_storage_file_data(const File* file, StorageData* storage) { | void* storage_get_storage_file_data(const File* file, StorageData* storage) { | ||||||
|     const StorageFile* founded_file = NULL; |     StorageFile* storage_file_ref = storage_get_file(file, storage); | ||||||
| 
 |     furi_check(storage_file_ref != NULL); | ||||||
|     StorageFileList_it_t it; |     return storage_file_ref->file_data; | ||||||
| 
 |  | ||||||
|     for(StorageFileList_it(it, storage->files); !StorageFileList_end_p(it); |  | ||||||
|         StorageFileList_next(it)) { |  | ||||||
|         const StorageFile* storage_file = StorageFileList_cref(it); |  | ||||||
| 
 |  | ||||||
|         if(storage_file->file->file_id == file->file_id) { |  | ||||||
|             founded_file = storage_file; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     furi_check(founded_file != NULL); |  | ||||||
| 
 |  | ||||||
|     return founded_file->file_data; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void storage_push_storage_file(File* file, FuriString* path, StorageData* storage) { | void storage_push_storage_file(File* file, FuriString* path, StorageData* storage) { | ||||||
|  | |||||||
| @ -60,7 +60,7 @@ struct StorageData { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| bool storage_has_file(const File* file, StorageData* storage_data); | bool storage_has_file(const File* file, StorageData* storage_data); | ||||||
| bool storage_path_already_open(FuriString* path, StorageFileList_t files); | bool storage_path_already_open(FuriString* path, StorageData* storage_data); | ||||||
| 
 | 
 | ||||||
| void storage_set_storage_file_data(const File* file, void* file_data, StorageData* storage); | void storage_set_storage_file_data(const File* file, void* file_data, StorageData* storage); | ||||||
| void* storage_get_storage_file_data(const File* file, StorageData* storage); | void* storage_get_storage_file_data(const File* file, StorageData* storage); | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ static void storage_path_change_to_real_storage(FuriString* path, StorageType re | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| FS_Error storage_get_data(Storage* app, FuriString* path, StorageData** storage) { | static FS_Error storage_get_data(Storage* app, FuriString* path, StorageData** storage) { | ||||||
|     StorageType type = storage_get_type_by_path(path); |     StorageType type = storage_get_type_by_path(path); | ||||||
| 
 | 
 | ||||||
|     if(storage_type_is_valid(type)) { |     if(storage_type_is_valid(type)) { | ||||||
| @ -111,7 +111,7 @@ bool storage_process_file_open( | |||||||
|     file->error_id = storage_get_data(app, path, &storage); |     file->error_id = storage_get_data(app, path, &storage); | ||||||
| 
 | 
 | ||||||
|     if(file->error_id == FSE_OK) { |     if(file->error_id == FSE_OK) { | ||||||
|         if(storage_path_already_open(path, storage->files)) { |         if(storage_path_already_open(path, storage)) { | ||||||
|             file->error_id = FSE_ALREADY_OPEN; |             file->error_id = FSE_ALREADY_OPEN; | ||||||
|         } else { |         } else { | ||||||
|             if(access_mode & FSAM_WRITE) { |             if(access_mode & FSAM_WRITE) { | ||||||
| @ -268,7 +268,7 @@ bool storage_process_dir_open(Storage* app, File* file, FuriString* path) { | |||||||
|     file->error_id = storage_get_data(app, path, &storage); |     file->error_id = storage_get_data(app, path, &storage); | ||||||
| 
 | 
 | ||||||
|     if(file->error_id == FSE_OK) { |     if(file->error_id == FSE_OK) { | ||||||
|         if(storage_path_already_open(path, storage->files)) { |         if(storage_path_already_open(path, storage)) { | ||||||
|             file->error_id = FSE_ALREADY_OPEN; |             file->error_id = FSE_ALREADY_OPEN; | ||||||
|         } else { |         } else { | ||||||
|             storage_push_storage_file(file, path, storage); |             storage_push_storage_file(file, path, storage); | ||||||
| @ -357,7 +357,7 @@ static FS_Error storage_process_common_remove(Storage* app, FuriString* path) { | |||||||
|     FS_Error ret = storage_get_data(app, path, &storage); |     FS_Error ret = storage_get_data(app, path, &storage); | ||||||
| 
 | 
 | ||||||
|     do { |     do { | ||||||
|         if(storage_path_already_open(path, storage->files)) { |         if(storage_path_already_open(path, storage)) { | ||||||
|             ret = FSE_ALREADY_OPEN; |             ret = FSE_ALREADY_OPEN; | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -618,8 +618,10 @@ static const FS_Api fs_api = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| void storage_ext_init(StorageData* storage) { | void storage_ext_init(StorageData* storage) { | ||||||
|  |     fatfs_init(); | ||||||
|  | 
 | ||||||
|     SDData* sd_data = malloc(sizeof(SDData)); |     SDData* sd_data = malloc(sizeof(SDData)); | ||||||
|     sd_data->fs = &USERFatFS; |     sd_data->fs = &fatfs_object; | ||||||
|     sd_data->path = "0:/"; |     sd_data->path = "0:/"; | ||||||
|     sd_data->sd_was_present = true; |     sd_data->sd_was_present = true; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,8 +3,6 @@ | |||||||
| 
 | 
 | ||||||
| #include <stm32wbxx_ll_cortex.h> | #include <stm32wbxx_ll_cortex.h> | ||||||
| 
 | 
 | ||||||
| #include <fatfs.h> |  | ||||||
| 
 |  | ||||||
| #define TAG "FuriHal" | #define TAG "FuriHal" | ||||||
| 
 | 
 | ||||||
| void furi_hal_init_early() { | void furi_hal_init_early() { | ||||||
| @ -74,10 +72,6 @@ void furi_hal_init() { | |||||||
| #endif | #endif | ||||||
|     furi_hal_bt_init(); |     furi_hal_bt_init(); | ||||||
|     furi_hal_compress_icon_init(); |     furi_hal_compress_icon_init(); | ||||||
| 
 |  | ||||||
|     // FatFS driver initialization
 |  | ||||||
|     MX_FATFS_Init(); |  | ||||||
|     FURI_LOG_I(TAG, "FATFS OK"); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void furi_hal_switch(void* address) { | void furi_hal_switch(void* address) { | ||||||
|  | |||||||
| @ -1,39 +1,12 @@ | |||||||
| /**
 |  | ||||||
|   ****************************************************************************** |  | ||||||
|   * @file   fatfs.c |  | ||||||
|   * @brief  Code for fatfs applications |  | ||||||
|   ****************************************************************************** |  | ||||||
|   * @attention |  | ||||||
|   * |  | ||||||
|   * <h2><center>© Copyright (c) 2020 STMicroelectronics. |  | ||||||
|   * All rights reserved.</center></h2> |  | ||||||
|   * |  | ||||||
|   * This software component is licensed by ST under Ultimate Liberty license |  | ||||||
|   * SLA0044, the "License"; You may not use this file except in compliance with |  | ||||||
|   * the License. You may obtain a copy of the License at: |  | ||||||
|   *                             www.st.com/SLA0044 |  | ||||||
|   * |  | ||||||
|   ****************************************************************************** |  | ||||||
|   */ |  | ||||||
| 
 |  | ||||||
| #include "fatfs.h" | #include "fatfs.h" | ||||||
| 
 | 
 | ||||||
| uint8_t retUSER; /* Return value for USER */ | /** logical drive path */ | ||||||
| char USERPath[4]; /* USER logical drive path */ | char fatfs_path[4]; | ||||||
| FATFS USERFatFS; /* File system object for USER logical drive */ | /** File system object */ | ||||||
| FIL USERFile; /* File object for USER */ | FATFS fatfs_object; | ||||||
| 
 | 
 | ||||||
| /* USER CODE BEGIN Variables */ | void fatfs_init(void) { | ||||||
| 
 |     FATFS_LinkDriver(&sd_fatfs_driver, fatfs_path); | ||||||
| /* USER CODE END Variables */ |  | ||||||
| 
 |  | ||||||
| void MX_FATFS_Init(void) { |  | ||||||
|     /*## FatFS: Link the USER driver ###########################*/ |  | ||||||
|     retUSER = FATFS_LinkDriver(&USER_Driver, USERPath); |  | ||||||
| 
 |  | ||||||
|     /* USER CODE BEGIN Init */ |  | ||||||
|     /* additional user code for init */ |  | ||||||
|     /* USER CODE END Init */ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
| @ -42,13 +15,5 @@ void MX_FATFS_Init(void) { | |||||||
|   * @retval Time in DWORD |   * @retval Time in DWORD | ||||||
|   */ |   */ | ||||||
| DWORD get_fattime(void) { | DWORD get_fattime(void) { | ||||||
|     /* USER CODE BEGIN get_fattime */ |  | ||||||
|     return 0; |     return 0; | ||||||
|     /* USER CODE END get_fattime */ |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /* USER CODE BEGIN Application */ |  | ||||||
| 
 |  | ||||||
| /* USER CODE END Application */ |  | ||||||
| 
 |  | ||||||
| /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ |  | ||||||
|  | |||||||
| @ -1,49 +1,19 @@ | |||||||
| /**
 | #pragma once | ||||||
|   ****************************************************************************** | 
 | ||||||
|   * @file   fatfs.h | #include "fatfs/ff.h" | ||||||
|   * @brief  Header for fatfs applications | #include "fatfs/ff_gen_drv.h" | ||||||
|   ****************************************************************************** | #include "user_diskio.h" | ||||||
|   * @attention |  | ||||||
|   * |  | ||||||
|   * <h2><center>© Copyright (c) 2020 STMicroelectronics. |  | ||||||
|   * All rights reserved.</center></h2> |  | ||||||
|   * |  | ||||||
|   * This software component is licensed by ST under Ultimate Liberty license |  | ||||||
|   * SLA0044, the "License"; You may not use this file except in compliance with |  | ||||||
|   * the License. You may obtain a copy of the License at: |  | ||||||
|   *                             www.st.com/SLA0044 |  | ||||||
|   * |  | ||||||
|   ****************************************************************************** |  | ||||||
|   */ |  | ||||||
| 
 | 
 | ||||||
| /* Define to prevent recursive inclusion -------------------------------------*/ |  | ||||||
| #ifndef __fatfs_H |  | ||||||
| #define __fatfs_H |  | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
| extern "C" { | extern "C" { | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| #include "fatfs/ff.h" | /** File system object */ | ||||||
| #include "fatfs/ff_gen_drv.h" | extern FATFS fatfs_object; | ||||||
| #include "user_diskio.h" /* defines USER_Driver as external */ |  | ||||||
| 
 | 
 | ||||||
| /* USER CODE BEGIN Includes */ | /** Init file system driver */ | ||||||
|  | void fatfs_init(void); | ||||||
| 
 | 
 | ||||||
| /* USER CODE END Includes */ |  | ||||||
| 
 |  | ||||||
| extern uint8_t retUSER; /* Return value for USER */ |  | ||||||
| extern char USERPath[4]; /* USER logical drive path */ |  | ||||||
| extern FATFS USERFatFS; /* File system object for USER logical drive */ |  | ||||||
| extern FIL USERFile; /* File object for USER */ |  | ||||||
| 
 |  | ||||||
| void MX_FATFS_Init(void); |  | ||||||
| 
 |  | ||||||
| /* USER CODE BEGIN Prototypes */ |  | ||||||
| 
 |  | ||||||
| /* USER CODE END Prototypes */ |  | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| #endif /*__fatfs_H */ |  | ||||||
| 
 |  | ||||||
| /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ |  | ||||||
|  | |||||||
| @ -164,7 +164,7 @@ | |||||||
| 
 | 
 | ||||||
| /* USER CODE BEGIN Volumes */ | /* USER CODE BEGIN Volumes */ | ||||||
| #define _STR_VOLUME_ID 0 /* 0:Use only 0-9 for drive ID, 1:Use strings for drive ID */ | #define _STR_VOLUME_ID 0 /* 0:Use only 0-9 for drive ID, 1:Use strings for drive ID */ | ||||||
| #define _VOLUME_STRS "RAM", "NAND", "CF", "SD1", "SD2", "USB1", "USB2", "USB3" | #define _VOLUME_STRS "SD" | ||||||
| /* _STR_VOLUME_ID switches string support of volume ID.
 | /* _STR_VOLUME_ID switches string support of volume ID.
 | ||||||
| /  When _STR_VOLUME_ID is set to 1, also pre-defined strings can be used as drive | /  When _STR_VOLUME_ID is set to 1, also pre-defined strings can be used as drive | ||||||
| /  number in the path name. _VOLUME_STRS defines the drive ID strings for each | /  number in the path name. _VOLUME_STRS defines the drive ID strings for each | ||||||
|  | |||||||
| @ -1,116 +0,0 @@ | |||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Sample code of OS dependent controls for FatFs                         */ |  | ||||||
| /* (C)ChaN, 2014                                                          */ |  | ||||||
| /*   Portions COPYRIGHT 2017 STMicroelectronics                           */ |  | ||||||
| /*   Portions Copyright (C) 2014, ChaN, all right reserved                */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| 
 |  | ||||||
| /**
 |  | ||||||
|   ****************************************************************************** |  | ||||||
|   * @attention |  | ||||||
|   * |  | ||||||
|   * Copyright (c) 2017 STMicroelectronics. All rights reserved. |  | ||||||
|   * |  | ||||||
|   * This software component is licensed by ST under BSD 3-Clause license, |  | ||||||
|   * the "License"; You may not use this file except in compliance with the |  | ||||||
|   * License. You may obtain a copy of the License at: |  | ||||||
|   *                       opensource.org/licenses/BSD-3-Clause |  | ||||||
|   * |  | ||||||
|   ****************************************************************************** |  | ||||||
| **/ |  | ||||||
| 
 |  | ||||||
| #include "fatfs/ff.h" |  | ||||||
| 
 |  | ||||||
| #if _FS_REENTRANT |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Create a Synchronization Object                                        */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* This function is called in f_mount() function to create a new
 |  | ||||||
| /  synchronization object, such as semaphore and mutex. When a 0 is returned, |  | ||||||
| /  the f_mount() function fails with FR_INT_ERR. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| int ff_cre_syncobj(/* 1:Function succeeded, 0:Could not create the sync object */ |  | ||||||
|                    BYTE vol, /* Corresponding volume (logical drive number) */ |  | ||||||
|                    _SYNC_t* sobj /* Pointer to return the created sync object */ |  | ||||||
| ) { |  | ||||||
|     int ret; |  | ||||||
| 
 |  | ||||||
|     //osSemaphoreDef(SEM);
 |  | ||||||
|     //*sobj = osSemaphoreCreate(osSemaphore(SEM), 1);
 |  | ||||||
|     *sobj = furi_mutex_alloc(FuriMutexTypeNormal); |  | ||||||
|     ret = (*sobj != NULL); |  | ||||||
| 
 |  | ||||||
|     return ret; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Delete a Synchronization Object                                        */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* This function is called in f_mount() function to delete a synchronization
 |  | ||||||
| /  object that created with ff_cre_syncobj() function. When a 0 is returned, |  | ||||||
| /  the f_mount() function fails with FR_INT_ERR. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| int ff_del_syncobj(/* 1:Function succeeded, 0:Could not delete due to any error */ |  | ||||||
|                    _SYNC_t sobj /* Sync object tied to the logical drive to be deleted */ |  | ||||||
| ) { |  | ||||||
|     furi_mutex_free(sobj); |  | ||||||
|     return 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Request Grant to Access the Volume                                     */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* This function is called on entering file functions to lock the volume.
 |  | ||||||
| /  When a 0 is returned, the file function fails with FR_TIMEOUT. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| int ff_req_grant(/* 1:Got a grant to access the volume, 0:Could not get a grant */ |  | ||||||
|                  _SYNC_t sobj /* Sync object to wait */ |  | ||||||
| ) { |  | ||||||
|     int ret = 0; |  | ||||||
| 
 |  | ||||||
|     if(furi_mutex_acquire(sobj, _FS_TIMEOUT) == FuriStatusOk) { |  | ||||||
|         ret = 1; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return ret; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Release Grant to Access the Volume                                     */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* This function is called on leaving file functions to unlock the volume.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| void ff_rel_grant(_SYNC_t sobj /* Sync object to be signaled */ |  | ||||||
| ) { |  | ||||||
|     furi_mutex_release(sobj); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| #if _USE_LFN == 3 /* LFN with a working buffer on the heap */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Allocate a memory block                                                */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* If a NULL is returned, the file function fails with FR_NOT_ENOUGH_CORE.
 |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| void* ff_memalloc(/* Returns pointer to the allocated memory block */ |  | ||||||
|                   UINT msize /* Number of bytes to allocate */ |  | ||||||
| ) { |  | ||||||
|     return ff_malloc(msize); /* Allocate a new memory block with POSIX API */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| /* Free a memory block                                                    */ |  | ||||||
| /*------------------------------------------------------------------------*/ |  | ||||||
| 
 |  | ||||||
| void ff_memfree(void* mblock /* Pointer to the memory block to free */ |  | ||||||
| ) { |  | ||||||
|     ff_free(mblock); /* Discard the memory block with POSIX API */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #endif |  | ||||||
| @ -1,50 +1,10 @@ | |||||||
| /* USER CODE BEGIN Header */ |  | ||||||
| /**
 |  | ||||||
|  ****************************************************************************** |  | ||||||
|  * @file    user_diskio.c |  | ||||||
|  * @brief   This file includes a diskio driver skeleton to be completed by the user. |  | ||||||
|  ****************************************************************************** |  | ||||||
|  * @attention |  | ||||||
|  * |  | ||||||
|  * <h2><center>© Copyright (c) 2020 STMicroelectronics. |  | ||||||
|  * All rights reserved.</center></h2> |  | ||||||
|  * |  | ||||||
|  * This software component is licensed by ST under Ultimate Liberty license |  | ||||||
|  * SLA0044, the "License"; You may not use this file except in compliance with |  | ||||||
|  * the License. You may obtain a copy of the License at: |  | ||||||
|  *                             www.st.com/SLA0044 |  | ||||||
|  * |  | ||||||
|  ****************************************************************************** |  | ||||||
|  */ |  | ||||||
| /* USER CODE END Header */ |  | ||||||
| 
 |  | ||||||
| #ifdef USE_OBSOLETE_USER_CODE_SECTION_0 |  | ||||||
| /* 
 |  | ||||||
|  * Warning: the user section 0 is no more in use (starting from CubeMx version 4.16.0) |  | ||||||
|  * To be suppressed in the future.  |  | ||||||
|  * Kept to ensure backward compatibility with previous CubeMx versions when  |  | ||||||
|  * migrating projects.  |  | ||||||
|  * User code previously added there should be copied in the new user sections before  |  | ||||||
|  * the section contents can be deleted. |  | ||||||
|  */ |  | ||||||
| /* USER CODE BEGIN 0 */ |  | ||||||
| /* USER CODE END 0 */ |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| /* USER CODE BEGIN DECL */ |  | ||||||
| 
 |  | ||||||
| /* Includes ------------------------------------------------------------------*/ |  | ||||||
| #include "user_diskio.h" | #include "user_diskio.h" | ||||||
| #include <furi_hal.h> | #include <furi_hal.h> | ||||||
| #include "sector_cache.h" | #include "sector_cache.h" | ||||||
| /* Private typedef -----------------------------------------------------------*/ |  | ||||||
| /* Private define ------------------------------------------------------------*/ |  | ||||||
| 
 | 
 | ||||||
| /* Private variables ---------------------------------------------------------*/ |  | ||||||
| /* Disk status */ |  | ||||||
| static volatile DSTATUS Stat = STA_NOINIT; | static volatile DSTATUS Stat = STA_NOINIT; | ||||||
| 
 | 
 | ||||||
| static DSTATUS User_CheckStatus(BYTE lun) { | static DSTATUS driver_check_status(BYTE lun) { | ||||||
|     UNUSED(lun); |     UNUSED(lun); | ||||||
|     Stat = STA_NOINIT; |     Stat = STA_NOINIT; | ||||||
|     if(sd_get_card_state() == SdSpiStatusOK) { |     if(sd_get_card_state() == SdSpiStatusOK) { | ||||||
| @ -54,32 +14,20 @@ static DSTATUS User_CheckStatus(BYTE lun) { | |||||||
|     return Stat; |     return Stat; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* USER CODE END DECL */ | static DSTATUS driver_initialize(BYTE pdrv); | ||||||
|  | static DSTATUS driver_status(BYTE pdrv); | ||||||
|  | static DRESULT driver_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count); | ||||||
|  | static DRESULT driver_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count); | ||||||
|  | static DRESULT driver_ioctl(BYTE pdrv, BYTE cmd, void* buff); | ||||||
| 
 | 
 | ||||||
| /* Private function prototypes -----------------------------------------------*/ | Diskio_drvTypeDef sd_fatfs_driver = { | ||||||
| DSTATUS USER_initialize(BYTE pdrv); |     driver_initialize, | ||||||
| DSTATUS USER_status(BYTE pdrv); |     driver_status, | ||||||
| DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count); |     driver_read, | ||||||
| #if _USE_WRITE == 1 |     driver_write, | ||||||
| DRESULT USER_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count); |     driver_ioctl, | ||||||
| #endif /* _USE_WRITE == 1 */ |  | ||||||
| #if _USE_IOCTL == 1 |  | ||||||
| DRESULT USER_ioctl(BYTE pdrv, BYTE cmd, void* buff); |  | ||||||
| #endif /* _USE_IOCTL == 1 */ |  | ||||||
| 
 |  | ||||||
| Diskio_drvTypeDef USER_Driver = { |  | ||||||
|     USER_initialize, |  | ||||||
|     USER_status, |  | ||||||
|     USER_read, |  | ||||||
| #if _USE_WRITE |  | ||||||
|     USER_write, |  | ||||||
| #endif /* _USE_WRITE == 1 */ |  | ||||||
| #if _USE_IOCTL == 1 |  | ||||||
|     USER_ioctl, |  | ||||||
| #endif /* _USE_IOCTL == 1 */ |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /* Private functions ---------------------------------------------------------*/ |  | ||||||
| static inline bool sd_cache_get(uint32_t address, uint32_t* data) { | static inline bool sd_cache_get(uint32_t address, uint32_t* data) { | ||||||
|     uint8_t* cached_data = sector_cache_get(address); |     uint8_t* cached_data = sector_cache_get(address); | ||||||
|     if(cached_data) { |     if(cached_data) { | ||||||
| @ -101,24 +49,73 @@ static inline void sd_cache_invalidate_all() { | |||||||
|     sector_cache_init(); |     sector_cache_init(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static bool sd_device_read(uint32_t* buff, uint32_t sector, uint32_t count) { | ||||||
|  |     bool result = false; | ||||||
|  | 
 | ||||||
|  |     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); | ||||||
|  |     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; | ||||||
|  | 
 | ||||||
|  |     if(sd_read_blocks(buff, sector, count, SD_TIMEOUT_MS) == SdSpiStatusOK) { | ||||||
|  |         FuriHalCortexTimer timer = furi_hal_cortex_timer_get(SD_TIMEOUT_MS * 1000); | ||||||
|  | 
 | ||||||
|  |         /* wait until the read operation is finished */ | ||||||
|  |         result = true; | ||||||
|  |         while(sd_get_card_state() != SdSpiStatusOK) { | ||||||
|  |             if(furi_hal_cortex_timer_is_expired(timer)) { | ||||||
|  |                 result = false; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_hal_sd_spi_handle = NULL; | ||||||
|  |     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static bool sd_device_write(uint32_t* buff, uint32_t sector, uint32_t count) { | ||||||
|  |     bool result = false; | ||||||
|  | 
 | ||||||
|  |     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); | ||||||
|  |     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; | ||||||
|  | 
 | ||||||
|  |     if(sd_write_blocks(buff, sector, count, SD_TIMEOUT_MS) == SdSpiStatusOK) { | ||||||
|  |         FuriHalCortexTimer timer = furi_hal_cortex_timer_get(SD_TIMEOUT_MS * 1000); | ||||||
|  | 
 | ||||||
|  |         /* wait until the Write operation is finished */ | ||||||
|  |         result = true; | ||||||
|  |         while(sd_get_card_state() != SdSpiStatusOK) { | ||||||
|  |             if(furi_hal_cortex_timer_is_expired(timer)) { | ||||||
|  |                 sd_cache_invalidate_all(); | ||||||
|  | 
 | ||||||
|  |                 result = false; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     furi_hal_sd_spi_handle = NULL; | ||||||
|  |     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /**
 | /**
 | ||||||
|   * @brief  Initializes a Drive |   * @brief  Initializes a Drive | ||||||
|   * @param  pdrv: Physical drive number (0..) |   * @param  pdrv: Physical drive number (0..) | ||||||
|   * @retval DSTATUS: Operation status |   * @retval DSTATUS: Operation status | ||||||
|   */ |   */ | ||||||
| DSTATUS USER_initialize(BYTE pdrv) { | static DSTATUS driver_initialize(BYTE pdrv) { | ||||||
|     /* USER CODE BEGIN INIT */ |  | ||||||
| 
 |  | ||||||
|     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); |     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); | ||||||
|     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; |     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; | ||||||
| 
 | 
 | ||||||
|     DSTATUS status = User_CheckStatus(pdrv); |     DSTATUS status = driver_check_status(pdrv); | ||||||
| 
 | 
 | ||||||
|     furi_hal_sd_spi_handle = NULL; |     furi_hal_sd_spi_handle = NULL; | ||||||
|     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); |     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); | ||||||
| 
 | 
 | ||||||
|     return status; |     return status; | ||||||
|     /* USER CODE END INIT */ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
| @ -126,11 +123,9 @@ DSTATUS USER_initialize(BYTE pdrv) { | |||||||
|   * @param  pdrv: Physical drive number (0..) |   * @param  pdrv: Physical drive number (0..) | ||||||
|   * @retval DSTATUS: Operation status |   * @retval DSTATUS: Operation status | ||||||
|   */ |   */ | ||||||
| DSTATUS USER_status(BYTE pdrv) { | static DSTATUS driver_status(BYTE pdrv) { | ||||||
|     /* USER CODE BEGIN STATUS */ |  | ||||||
|     UNUSED(pdrv); |     UNUSED(pdrv); | ||||||
|     return Stat; |     return Stat; | ||||||
|     /* USER CODE END STATUS */ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
| @ -141,11 +136,10 @@ DSTATUS USER_status(BYTE pdrv) { | |||||||
|   * @param  count: Number of sectors to read (1..128) |   * @param  count: Number of sectors to read (1..128) | ||||||
|   * @retval DRESULT: Operation result |   * @retval DRESULT: Operation result | ||||||
|   */ |   */ | ||||||
| DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | static DRESULT driver_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | ||||||
|     /* USER CODE BEGIN READ */ |  | ||||||
|     UNUSED(pdrv); |     UNUSED(pdrv); | ||||||
|     DRESULT res = RES_ERROR; |  | ||||||
| 
 | 
 | ||||||
|  |     bool result; | ||||||
|     bool single_sector = count == 1; |     bool single_sector = count == 1; | ||||||
| 
 | 
 | ||||||
|     if(single_sector) { |     if(single_sector) { | ||||||
| @ -154,32 +148,33 @@ DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); |     result = sd_device_read((uint32_t*)buff, (uint32_t)(sector), count); | ||||||
|     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; |  | ||||||
| 
 | 
 | ||||||
|     if(sd_read_blocks((uint32_t*)buff, (uint32_t)(sector), count, SD_TIMEOUT_MS) == |     if(!result) { | ||||||
|        SdSpiStatusOK) { |         uint8_t counter = sd_max_mount_retry_count(); | ||||||
|         FuriHalCortexTimer timer = furi_hal_cortex_timer_get(SD_TIMEOUT_MS * 1000); |  | ||||||
| 
 | 
 | ||||||
|         /* wait until the read operation is finished */ |         while(result == false && counter > 0 && hal_sd_detect()) { | ||||||
|         res = RES_OK; |             SdSpiStatus status; | ||||||
|         while(sd_get_card_state() != SdSpiStatusOK) { | 
 | ||||||
|             if(furi_hal_cortex_timer_is_expired(timer)) { |             if((counter % 2) == 0) { | ||||||
|                 res = RES_ERROR; |                 // power reset sd card
 | ||||||
|                 break; |                 status = sd_init(true); | ||||||
|             } |             } else { | ||||||
|         } |                 status = sd_init(false); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|     furi_hal_sd_spi_handle = NULL; |             if(status == SdSpiStatusOK) { | ||||||
|     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); |                 result = sd_device_read((uint32_t*)buff, (uint32_t)(sector), count); | ||||||
|  |             } | ||||||
|  |             counter--; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if(single_sector && res == RES_OK) { |     if(single_sector && result == true) { | ||||||
|         sd_cache_put(sector, (uint32_t*)buff); |         sd_cache_put(sector, (uint32_t*)buff); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return res; |     return result ? RES_OK : RES_ERROR; | ||||||
|     /* USER CODE END READ */ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
| @ -190,41 +185,36 @@ DRESULT USER_read(BYTE pdrv, BYTE* buff, DWORD sector, UINT count) { | |||||||
|   * @param  count: Number of sectors to write (1..128) |   * @param  count: Number of sectors to write (1..128) | ||||||
|   * @retval DRESULT: Operation result |   * @retval DRESULT: Operation result | ||||||
|   */ |   */ | ||||||
| #if _USE_WRITE == 1 | static DRESULT driver_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { | ||||||
| DRESULT USER_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { |  | ||||||
|     /* USER CODE BEGIN WRITE */ |  | ||||||
|     /* USER CODE HERE */ |  | ||||||
|     UNUSED(pdrv); |     UNUSED(pdrv); | ||||||
|     DRESULT res = RES_ERROR; |     bool result; | ||||||
| 
 | 
 | ||||||
|     sd_cache_invalidate_range(sector, sector + count); |     sd_cache_invalidate_range(sector, sector + count); | ||||||
| 
 | 
 | ||||||
|     furi_hal_spi_acquire(&furi_hal_spi_bus_handle_sd_fast); |     result = sd_device_write((uint32_t*)buff, (uint32_t)(sector), count); | ||||||
|     furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_sd_fast; |  | ||||||
| 
 | 
 | ||||||
|     if(sd_write_blocks((uint32_t*)buff, (uint32_t)(sector), count, SD_TIMEOUT_MS) == |     if(!result) { | ||||||
|        SdSpiStatusOK) { |         uint8_t counter = sd_max_mount_retry_count(); | ||||||
|         FuriHalCortexTimer timer = furi_hal_cortex_timer_get(SD_TIMEOUT_MS * 1000); |  | ||||||
| 
 | 
 | ||||||
|         /* wait until the Write operation is finished */ |         while(result == false && counter > 0 && hal_sd_detect()) { | ||||||
|         res = RES_OK; |             SdSpiStatus status; | ||||||
|         while(sd_get_card_state() != SdSpiStatusOK) { |  | ||||||
|             if(furi_hal_cortex_timer_is_expired(timer)) { |  | ||||||
|                 sd_cache_invalidate_all(); |  | ||||||
| 
 | 
 | ||||||
|                 res = RES_ERROR; |             if((counter % 2) == 0) { | ||||||
|                 break; |                 // power reset sd card
 | ||||||
|             } |                 status = sd_init(true); | ||||||
|         } |             } else { | ||||||
|  |                 status = sd_init(false); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|     furi_hal_sd_spi_handle = NULL; |             if(status == SdSpiStatusOK) { | ||||||
|     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); |                 result = sd_device_write((uint32_t*)buff, (uint32_t)(sector), count); | ||||||
| 
 |             } | ||||||
|     return res; |             counter--; | ||||||
|     /* USER CODE END WRITE */ |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return result ? RES_OK : RES_ERROR; | ||||||
| } | } | ||||||
| #endif /* _USE_WRITE == 1 */ |  | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|   * @brief  I/O control operation   |   * @brief  I/O control operation   | ||||||
| @ -233,9 +223,7 @@ DRESULT USER_write(BYTE pdrv, const BYTE* buff, DWORD sector, UINT count) { | |||||||
|   * @param  *buff: Buffer to send/receive control data |   * @param  *buff: Buffer to send/receive control data | ||||||
|   * @retval DRESULT: Operation result |   * @retval DRESULT: Operation result | ||||||
|   */ |   */ | ||||||
| #if _USE_IOCTL == 1 | static DRESULT driver_ioctl(BYTE pdrv, BYTE cmd, void* buff) { | ||||||
| DRESULT USER_ioctl(BYTE pdrv, BYTE cmd, void* buff) { |  | ||||||
|     /* USER CODE BEGIN IOCTL */ |  | ||||||
|     UNUSED(pdrv); |     UNUSED(pdrv); | ||||||
|     DRESULT res = RES_ERROR; |     DRESULT res = RES_ERROR; | ||||||
|     SD_CardInfo CardInfo; |     SD_CardInfo CardInfo; | ||||||
| @ -280,8 +268,4 @@ DRESULT USER_ioctl(BYTE pdrv, BYTE cmd, void* buff) { | |||||||
|     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); |     furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_fast); | ||||||
| 
 | 
 | ||||||
|     return res; |     return res; | ||||||
|     /* USER CODE END IOCTL */ |  | ||||||
| } | } | ||||||
| #endif /* _USE_IOCTL == 1 */ |  | ||||||
| 
 |  | ||||||
| /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ |  | ||||||
|  | |||||||
| @ -1,48 +1,14 @@ | |||||||
| /* USER CODE BEGIN Header */ | #pragma once | ||||||
| /**
 |  | ||||||
|  ****************************************************************************** |  | ||||||
|   * @file    user_diskio.h |  | ||||||
|   * @brief   This file contains the common defines and functions prototypes for   |  | ||||||
|   *          the user_diskio driver. |  | ||||||
|   ****************************************************************************** |  | ||||||
|   * @attention |  | ||||||
|   * |  | ||||||
|   * <h2><center>© Copyright (c) 2020 STMicroelectronics. |  | ||||||
|   * All rights reserved.</center></h2> |  | ||||||
|   * |  | ||||||
|   * This software component is licensed by ST under Ultimate Liberty license |  | ||||||
|   * SLA0044, the "License"; You may not use this file except in compliance with |  | ||||||
|   * the License. You may obtain a copy of the License at: |  | ||||||
|   *                             www.st.com/SLA0044 |  | ||||||
|   * |  | ||||||
|   ****************************************************************************** |  | ||||||
|   */ |  | ||||||
| /* USER CODE END Header */ |  | ||||||
| 
 |  | ||||||
| /* Define to prevent recursive inclusion -------------------------------------*/ |  | ||||||
| #ifndef __USER_DISKIO_H |  | ||||||
| #define __USER_DISKIO_H |  | ||||||
| 
 | 
 | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
| extern "C" { | extern "C" { | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| /* USER CODE BEGIN 0 */ |  | ||||||
| 
 |  | ||||||
| /* Includes ------------------------------------------------------------------*/ |  | ||||||
| #include "sd_spi_io.h" | #include "sd_spi_io.h" | ||||||
| #include "fatfs/ff_gen_drv.h" | #include "fatfs/ff_gen_drv.h" | ||||||
| /* Exported types ------------------------------------------------------------*/ |  | ||||||
| /* Exported constants --------------------------------------------------------*/ |  | ||||||
| /* Exported functions ------------------------------------------------------- */ |  | ||||||
| extern Diskio_drvTypeDef USER_Driver; |  | ||||||
| 
 | 
 | ||||||
| /* USER CODE END 0 */ | extern Diskio_drvTypeDef sd_fatfs_driver; | ||||||
| 
 | 
 | ||||||
| #ifdef __cplusplus | #ifdef __cplusplus | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 |  | ||||||
| #endif /* __USER_DISKIO_H */ |  | ||||||
| 
 |  | ||||||
| /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/ |  | ||||||
|  | |||||||
| @ -4,8 +4,6 @@ | |||||||
| 
 | 
 | ||||||
| #include <stm32wbxx_ll_cortex.h> | #include <stm32wbxx_ll_cortex.h> | ||||||
| 
 | 
 | ||||||
| #include <fatfs.h> |  | ||||||
| 
 |  | ||||||
| #define TAG "FuriHal" | #define TAG "FuriHal" | ||||||
| 
 | 
 | ||||||
| void furi_hal_init_early() { | void furi_hal_init_early() { | ||||||
| @ -81,10 +79,6 @@ void furi_hal_init() { | |||||||
|     furi_hal_nfc_init(); |     furi_hal_nfc_init(); | ||||||
|     furi_hal_rfid_init(); |     furi_hal_rfid_init(); | ||||||
| #endif | #endif | ||||||
| 
 |  | ||||||
|     // FatFS driver initialization
 |  | ||||||
|     MX_FATFS_Init(); |  | ||||||
|     FURI_LOG_I(TAG, "FATFS OK"); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void furi_hal_switch(void* address) { | void furi_hal_switch(void* address) { | ||||||
|  | |||||||
| @ -44,7 +44,7 @@ static bool flipper_update_init() { | |||||||
| 
 | 
 | ||||||
|     furi_hal_spi_config_init(); |     furi_hal_spi_config_init(); | ||||||
| 
 | 
 | ||||||
|     MX_FATFS_Init(); |     fatfs_init(); | ||||||
|     if(!hal_sd_detect()) { |     if(!hal_sd_detect()) { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sergey Gavrilov
						Sergey Gavrilov