Skip to content
Snippets Groups Projects
Commit a76d8ebb authored by schneider's avatar schneider
Browse files

combine MSC and FAT libary access to external flash

parent 8a053217
No related branches found
No related tags found
No related merge requests found
......@@ -67,13 +67,17 @@ CMSIS_ROOT=$(LIBS_DIR)/CMSIS
# Source files for this test (add path to VPATH below)
SRCS = main.c
SRCS += mscmem.c
SRCS += ../lib/card10/mx25lba.c
# Where to find source files for this test
VPATH = .
VPATH = .
# Where to find header files for this test
IPATH = .
IPATH += ../lib/card10
VPATH += ../lib/card10
# Enable assertion checking for development
PROJ_CFLAGS+=-DMXC_ASSERT_ENABLE
......
......@@ -48,266 +48,54 @@
#include "mscmem.h"
#include <string.h>
#include <stdio.h>
#include "mx25.h"
#include "mx25lba.h"
/***** Definitions *****/
#define SPIXF_DISK 1
#define RAM_DISK 0
#define LBA_SIZE 512 /* Size of "logical blocks" in bytes */
#define LBA_SIZE_SHIFT 9 /* The shift value used to convert between addresses and block numbers */
/***** Global Data *****/
/***** File Scope Variables *****/
static int initialized = 0;
static int running = 0;
#if SPIXF_DISK
#define MX25_BAUD 5000000 /* SPI clock rate to communicate with the MX25 */
#define MX25_SECTOR_SIZE 4096 /* Number of bytes in one sector of the MX25 */
#define MX25_SECTOR_SIZE_SHIFT 12 /* The shift value used to convert between addresses and block numbers */
#define MX25_NUM_SECTORS 2048 /* Total number of sectors in the MX25 */
#define LBA_PER_SECTOR (MX25_SECTOR_SIZE >> LBA_SIZE_SHIFT)
#define INVALID_SECTOR MX25_NUM_SECTORS /* Use a sector number past the end of memory to indicate invalid */
/***** File Scope Variables *****/
static uint32_t sectorNum = INVALID_SECTOR;
static uint8_t sector[MX25_SECTOR_SIZE];
static int sectorDirty = 0;
/***** Function Prototypes *****/
static uint32_t getSectorNum(uint32_t lba);
static uint32_t getSectorAddr(uint32_t lba);
static uint32_t getSector(uint32_t num);
/******************************************************************************/
static uint32_t getSectorNum(uint32_t lba)
{
/* Absolute_address = lba * LBA_SIZE */
/* Sector_num = Absolute_address / MX25_SECTOR_SIZE */
/* Sector_num = lba * 512 / 4096 */
return lba >> (MX25_SECTOR_SIZE_SHIFT - LBA_SIZE_SHIFT);
}
/******************************************************************************/
static uint32_t getSectorAddr(uint32_t lba)
{
/* eight 512 byte blocks in each sector */
return (lba & (LBA_PER_SECTOR - 1)) << LBA_SIZE_SHIFT;
}
/******************************************************************************/
static uint32_t getSector(uint32_t num)
{
/* New sector requested? */
if(sectorNum != num) {
/* Is the current sector real? */
if(sectorNum != INVALID_SECTOR) {
/* Was it written to after it was read from memory? */
if(sectorDirty) {
/* Erase the old data. */
MX25_Erase(sectorNum << MX25_SECTOR_SIZE_SHIFT, MX25_Erase_4K);
/* Write the new */
MX25_Program_Page(sectorNum << MX25_SECTOR_SIZE_SHIFT, sector, MX25_SECTOR_SIZE, SPIXFC_WIDTH_4);
/* Mark data as clean */
sectorDirty = 0;
}
}
/* Requesting a new valid sector? */
if(num != INVALID_SECTOR) {
MX25_Read(num << MX25_SECTOR_SIZE_SHIFT, sector, MX25_SECTOR_SIZE, SPIXFC_WIDTH_4);
sectorDirty = 0;
sectorNum = num;
}
}
return 0;
}
//static int initialized = 0;
//static int running = 0;
/******************************************************************************/
int mscmem_init()
{
if(!initialized) {
MX25_Init();
MX25_Reset();
MX25_Quad(1);
initialized = 1;
}
return 0;
return mx25_init();
}
/******************************************************************************/
uint32_t mscmem_size(void)
{
/* Get number of 512 byte chunks the MX25 contains. */
return (MX25_SECTOR_SIZE >> LBA_SIZE_SHIFT) * MX25_NUM_SECTORS;
return mx25_size();
}
/******************************************************************************/
int mscmem_read(uint32_t lba, uint8_t* buffer)
{
uint32_t addr;
/* Convert to MX25 sector number. */
uint32_t sNum = getSectorNum(lba);
if(getSector(sNum)) {
/* Failed to write/read from MX25 */
return 1;
}
/* Get the offset into the current sector */
addr = getSectorAddr(lba);
memcpy(buffer, sector + addr, LBA_SIZE);
return 0;
return mx25_read(lba, buffer);
}
/******************************************************************************/
int mscmem_write(uint32_t lba, uint8_t* buffer)
{
uint32_t addr;
/* Convert to MX25 sector number. */
uint32_t sNum = getSectorNum(lba);
if(getSector(sNum)) {
/* Failed to write/read from MX25 */
return 1;
}
/* Get the offset into the current sector */
addr = getSectorAddr(lba);
memcpy(sector + addr, buffer, LBA_SIZE);
sectorDirty = 1;
return 0;
return mx25_write(lba, buffer);
}
/******************************************************************************/
int mscmem_start()
{
/* Turn on the MX25 if it is not already. */
if(!initialized) {
mscmem_init();
}
/* Check if the initialization succeeded. If it has, start running. */
if(initialized) {
running = 1;
}
/* Start should return fail (non-zero) if the memory cannot be initialized. */
return !initialized;
return mx25_start();
}
/******************************************************************************/
int mscmem_stop()
{
/* TODO - could shut down XIPF interface here. */
/* Flush the currently cached sector if necessary. */
if(getSector(INVALID_SECTOR)) {
return 1;
}
running = 0;
return 0;
return mx25_stop();
}
/******************************************************************************/
int mscmem_ready()
{
return running;
return mx25_ready();
}
#elif RAM_DISK
#define NUM_PAGES 0x100
static uint8_t mem[NUM_PAGES][LBA_SIZE];
/******************************************************************************/
int mscmem_init()
{
if(!initialized) {
initialized = 1;
#if (ERASE_MEMORY_ON_INIT)
memset(mem, 0, sizeof(mem));
#endif
}
return 0;
}
/******************************************************************************/
uint32_t mscmem_size(void)
{
return NUM_PAGES;
}
/******************************************************************************/
int mscmem_read(uint32_t lba, uint8_t* buffer)
{
if(lba >= NUM_PAGES) {
return 1;
}
memcpy(buffer, mem[lba], LBA_SIZE);
return 0;
}
/******************************************************************************/
int mscmem_write(uint32_t lba, uint8_t* buffer)
{
if(lba >= NUM_PAGES) {
return 1;
}
memcpy(mem[lba], buffer, LBA_SIZE);
return 0;
}
/******************************************************************************/
int mscmem_start()
{
/* Not much to do for this implementation. The RAM is always ready. */
if(!initialized) {
mscmem_init();
}
/* Check if the RAM has been initialized. If it has, start running. */
if(initialized) {
running = 1;
}
/* Start should return fail (non-zero) if the memory cannot be initialized. */
return !initialized;
}
/******************************************************************************/
int mscmem_stop()
{
/* Nothing to do for this implementation. All data is written as it is */
/* received so there are no pending writes that need to be flushed. */
running = 0;
return 0;
}
/******************************************************************************/
int mscmem_ready()
{
return running;
}
#else
#error "You must assign either RAM_DISK or SPIXF_DISK to 1."
#endif
/*******************************************************************************
* Copyright (C) 2017 Maxim Integrated Products, Inc., All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Maxim Integrated
* Products, Inc. shall not be used except as stated in the Maxim Integrated
* Products, Inc. Branding Policy.
*
* The mere transfer of this software does not imply any licenses
* of trade secrets, proprietary technology, copyrights, patents,
* trademarks, maskwork rights, or any other form of intellectual
* property whatsoever. Maxim Integrated Products, Inc. retains all
* ownership rights.
*
* Description: Communications Device Class ACM (Serial Port) over USB
* $Id: descriptors.h 31172 2017-10-05 19:05:57Z zach.metzinger $
*
*******************************************************************************
*/
/**
* @file mscmem.h
* @brief Memory routines used by the USB Mass Storage Class example.
* See the msc_mem_t structure in msc.h for function details.
* @details Functions are provided for using the internal RAM of the
* device or the external SPI flash memory. Use the SPIXF_DISK
* and RAM_DISK defines to select the desired memory at compile
* time.
*/
#include "mscmem.h"
#include <string.h>
#include <stdio.h>
#include "mx25.h"
/***** Definitions *****/
#define LBA_SIZE 512 /* Size of "logical blocks" in bytes */
#define LBA_SIZE_SHIFT 9 /* The shift value used to convert between addresses and block numbers */
/***** Global Data *****/
/***** File Scope Variables *****/
static int initialized = 0;
static int running = 0;
#define MX25_BAUD 5000000 /* SPI clock rate to communicate with the MX25 */
#define MX25_SECTOR_SIZE 4096 /* Number of bytes in one sector of the MX25 */
#define MX25_SECTOR_SIZE_SHIFT 12 /* The shift value used to convert between addresses and block numbers */
#define MX25_NUM_SECTORS 2048 /* Total number of sectors in the MX25 */
#define LBA_PER_SECTOR (MX25_SECTOR_SIZE >> LBA_SIZE_SHIFT)
#define INVALID_SECTOR MX25_NUM_SECTORS /* Use a sector number past the end of memory to indicate invalid */
/***** File Scope Variables *****/
static uint32_t sectorNum = INVALID_SECTOR;
static uint8_t sector[MX25_SECTOR_SIZE];
static int sectorDirty = 0;
/***** Function Prototypes *****/
static uint32_t getSectorNum(uint32_t lba);
static uint32_t getSectorAddr(uint32_t lba);
static uint32_t getSector(uint32_t num);
/******************************************************************************/
static uint32_t getSectorNum(uint32_t lba)
{
/* Absolute_address = lba * LBA_SIZE */
/* Sector_num = Absolute_address / MX25_SECTOR_SIZE */
/* Sector_num = lba * 512 / 4096 */
return lba >> (MX25_SECTOR_SIZE_SHIFT - LBA_SIZE_SHIFT);
}
/******************************************************************************/
static uint32_t getSectorAddr(uint32_t lba)
{
/* eight 512 byte blocks in each sector */
return (lba & (LBA_PER_SECTOR - 1)) << LBA_SIZE_SHIFT;
}
/******************************************************************************/
static uint32_t getSector(uint32_t num)
{
/* New sector requested? */
if(sectorNum != num) {
/* Is the current sector real? */
if(sectorNum != INVALID_SECTOR) {
/* Was it written to after it was read from memory? */
if(sectorDirty) {
/* Erase the old data. */
MX25_Erase(sectorNum << MX25_SECTOR_SIZE_SHIFT, MX25_Erase_4K);
/* Write the new */
MX25_Program_Page(sectorNum << MX25_SECTOR_SIZE_SHIFT, sector, MX25_SECTOR_SIZE, SPIXFC_WIDTH_4);
/* Mark data as clean */
sectorDirty = 0;
}
}
/* Requesting a new valid sector? */
if(num != INVALID_SECTOR) {
MX25_Read(num << MX25_SECTOR_SIZE_SHIFT, sector, MX25_SECTOR_SIZE, SPIXFC_WIDTH_4);
sectorDirty = 0;
sectorNum = num;
}
}
return 0;
}
/******************************************************************************/
int mx25_init()
{
if(!initialized) {
MX25_Init();
MX25_Reset();
MX25_Quad(1);
initialized = 1;
}
return 0;
}
/******************************************************************************/
uint32_t mx25_size(void)
{
/* Get number of 512 byte chunks the MX25 contains. */
return (MX25_SECTOR_SIZE >> LBA_SIZE_SHIFT) * MX25_NUM_SECTORS;
}
/******************************************************************************/
int mx25_read(uint32_t lba, uint8_t* buffer)
{
uint32_t addr;
/* Convert to MX25 sector number. */
uint32_t sNum = getSectorNum(lba);
if(getSector(sNum)) {
/* Failed to write/read from MX25 */
return 1;
}
/* Get the offset into the current sector */
addr = getSectorAddr(lba);
memcpy(buffer, sector + addr, LBA_SIZE);
return 0;
}
/******************************************************************************/
int mx25_write(uint32_t lba, uint8_t* buffer)
{
uint32_t addr;
/* Convert to MX25 sector number. */
uint32_t sNum = getSectorNum(lba);
if(getSector(sNum)) {
/* Failed to write/read from MX25 */
return 1;
}
/* Get the offset into the current sector */
addr = getSectorAddr(lba);
memcpy(sector + addr, buffer, LBA_SIZE);
sectorDirty = 1;
return 0;
}
/******************************************************************************/
int mx25_start()
{
/* Turn on the MX25 if it is not already. */
if(!initialized) {
mscmem_init();
}
/* Check if the initialization succeeded. If it has, start running. */
if(initialized) {
running = 1;
}
/* Start should return fail (non-zero) if the memory cannot be initialized. */
return !initialized;
}
/******************************************************************************/
int mx25_stop()
{
/* TODO - could shut down XIPF interface here. */
/* Flush the currently cached sector if necessary. */
if(getSector(INVALID_SECTOR)) {
return 1;
}
running = 0;
return 0;
}
/******************************************************************************/
int mx25_sync()
{
/* Flush the currently cached sector if necessary. */
if(getSector(INVALID_SECTOR)) {
return 1;
}
return 0;
}
/******************************************************************************/
int mx25_ready()
{
return running;
}
/*******************************************************************************
* Copyright (C) 2017 Maxim Integrated Products, Inc., All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Maxim Integrated
* Products, Inc. shall not be used except as stated in the Maxim Integrated
* Products, Inc. Branding Policy.
*
* The mere transfer of this software does not imply any licenses
* of trade secrets, proprietary technology, copyrights, patents,
* trademarks, maskwork rights, or any other form of intellectual
* property whatsoever. Maxim Integrated Products, Inc. retains all
* ownership rights.
*
* Description: Communications Device Class ACM (Serial Port) over USB
* $Id: descriptors.h 31172 2017-10-05 19:05:57Z zach.metzinger $
*
*******************************************************************************
*/
/**
* @file mx25.h
* @brief External flash access, adapted from USB MSC example
*/
#ifndef __MX25_H__
#define __MX25_H__
#include <stdint.h>
int mx25_init(void);
int mx25_start(void);
int mx25_stop(void);
uint32_t mx25_size(void);
int mx25_read(uint32_t lba, uint8_t* buffer);
int mx25_write(uint32_t lba, uint8_t* buffer);
int mx25_ready(void);
#endif /* __MX25_H__ */
......@@ -10,9 +10,14 @@
#include "diskio.h" /* FatFs lower layer API */
/* Definitions of physical drive number for each drive */
#define DEV_FLASH 0 /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_FLASH 0 /* Example: Map MMC/SD card to physical drive 1 */
#define DEV_SD 1 /* Example: Map MMC/SD card to physical drive 1 */
#define SDHC 0
#define SECTOR_SIZE 512UL
#if SDHC
/* # of times to check for a card, should be > 1 to detect both SD and MMC */
#define INIT_CARD_RETRIES 10
......@@ -24,6 +29,15 @@ static DRESULT mmc_get_csd(void *buff);
/* Globals */
unsigned int init_done = 0;
#endif
extern int mx25_init(void);
extern int mx25_start(void);
extern int mx25_stop(void);
extern uint32_t mx25_size(void);
extern int mx25_read(uint32_t lba, uint8_t* buffer);
extern int mx25_write(uint32_t lba, uint8_t* buffer);
extern int mx25_sync(void);
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
......@@ -33,11 +47,23 @@ DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS status = 0;
#if 0
if (!SDHC_Card_Inserted()) {
init_done = 0;
status = STA_NOINIT | STA_NODISK;
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */
#endif
DSTATUS status = 0;
if(pdrv == 0) {
return STA_NOINIT;
}
#if SDHC
if(pdrv == 1) {
if (!SDHC_Card_Inserted()) {
init_done = 0;
status = STA_NOINIT | STA_NODISK;
}
}
#endif
return status;
......@@ -70,13 +96,21 @@ DSTATUS disk_initialize (
}
#endif
#if 0
if (SDHC_Card_Inserted() && (SDHC_Lib_InitCard(INIT_CARD_RETRIES) == E_NO_ERROR)) {
/* Card initialized and ready for work */
init_done = 1;
status = 0;
} else {
status = STA_NOINIT;
if(pdrv == 0) {
if(mx25_start()) {
status = RES_OK;
}
}
#if SDHC
if(pdrv == 1) {
if (SDHC_Card_Inserted() && (SDHC_Lib_InitCard(INIT_CARD_RETRIES) == E_NO_ERROR)) {
/* Card initialized and ready for work */
init_done = 1;
status = 0;
} else {
status = STA_NOINIT;
}
}
#endif
......@@ -98,11 +132,21 @@ DRESULT disk_read (
{
DRESULT status = RES_ERROR;
#if 0
if (SDHC_Lib_Read(buff, sector, count, SDHC_LIB_SINGLE_DATA) != E_NO_ERROR) {
status = RES_ERROR;
} else {
if(pdrv == 0) {
int sector_offset;
status = RES_OK;
for(sector_offset = 0; sector_offset < count; sector_offset++) {
if(mx25_read(sector + sector_offset, (uint8_t*)buff + SECTOR_SIZE * sector_offset) == 1) {
status = RES_ERROR;
break;
}
}
}
#if SDHC
if(pdrv == 1) {
if (SDHC_Lib_Read(buff, sector, count, SDHC_LIB_SINGLE_DATA) == E_NO_ERROR) {
status = RES_OK;
}
}
#endif
......@@ -124,11 +168,24 @@ DRESULT disk_write (
{
DRESULT status = RES_ERROR;
#if 0
if (SDHC_Lib_Write(sector, (void *)buff, count, SDHC_LIB_SINGLE_DATA) != E_NO_ERROR) {
status = RES_ERROR;
} else {
if(pdrv == 0) {
int sector_offset;
status = RES_OK;
for(sector_offset = 0; sector_offset < count; sector_offset++) {
if(mx25_write(sector + sector_offset, (uint8_t*)buff + SECTOR_SIZE * sector_offset) == 1) {
status = RES_ERROR;
break;
}
}
}
#if SDHC
if(pdrv == 1) {
if (SDHC_Lib_Write(sector, (void *)buff, count, SDHC_LIB_SINGLE_DATA) != E_NO_ERROR) {
status = RES_ERROR;
} else {
status = RES_OK;
}
}
#endif
......@@ -147,30 +204,55 @@ DRESULT disk_ioctl (
void *buff /* Buffer to send/receive control data */
)
{
DRESULT status;
switch(cmd) {
case CTRL_SYNC:
/* Mandatory */
status = ctrl_sync(buff);
break;
case GET_SECTOR_COUNT:
/* Mandatory */
status = get_sector_count(buff);
break;
case GET_BLOCK_SIZE:
/* Mandatory */
status = get_block_size(buff);
break;
case MMC_GET_CSD:
/* Optional */
status = mmc_get_csd(buff);
break;
default:
status = RES_PARERR;
break;
DRESULT status = RES_PARERR;
if(pdrv == 0) {
switch(cmd) {
case CTRL_SYNC:
/* Mandatory */
status = mx25_sync();
break;
case GET_SECTOR_COUNT:
/* Mandatory */
*((DWORD *)buff) = mx25_size() / SECTOR_SIZE;
status = RES_OK;
break;
case GET_BLOCK_SIZE:
/* Mandatory */
*((DWORD *)buff) = SECTOR_SIZE;
status = RES_OK;
break;
default:
status = RES_PARERR;
break;
}
}
#if SDHC
if(pdrv == 1) {
switch(cmd) {
case CTRL_SYNC:
/* Mandatory */
status = ctrl_sync(buff);
break;
case GET_SECTOR_COUNT:
/* Mandatory */
status = get_sector_count(buff);
break;
case GET_BLOCK_SIZE:
/* Mandatory */
status = get_block_size(buff);
break;
case MMC_GET_CSD:
/* Optional */
status = mmc_get_csd(buff);
break;
default:
status = RES_PARERR;
break;
}
}
#endif
return status;
}
......@@ -216,6 +298,7 @@ DWORD get_fattime(void) {
}
}
#if SDHC
static DRESULT ctrl_sync(void *buff)
{
return RES_OK;
......@@ -225,7 +308,6 @@ static DRESULT get_sector_count(void *buff)
{
DRESULT status = RES_ERROR;
#if 0
mxc_sdhc_csd_regs_t csd;
if (init_done) {
......@@ -236,7 +318,6 @@ static DRESULT get_sector_count(void *buff)
} else {
status = RES_NOTRDY;
}
#endif
return status;
}
......@@ -245,7 +326,6 @@ static DRESULT get_block_size(void *buff)
{
DRESULT status = RES_ERROR;
#if 0
mxc_sdhc_csd_regs_t csd;
if (init_done) {
if (SDHC_Lib_GetCSD(&csd) == E_NO_ERROR) {
......@@ -255,7 +335,6 @@ static DRESULT get_block_size(void *buff)
} else {
status = RES_NOTRDY;
}
#endif
return status;
}
......@@ -264,7 +343,6 @@ static DRESULT mmc_get_csd(void *buff)
{
DRESULT status = RES_ERROR;
#if 0
if (init_done) {
if (SDHC_Lib_GetCSD(buff) == E_NO_ERROR) {
status = RES_OK;
......@@ -272,7 +350,7 @@ static DRESULT mmc_get_csd(void *buff)
} else {
status = RES_NOTRDY;
}
#endif
return status;
}
#endif
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment