Merge pull request #14038 from benpicco/mtd_pagewise
mtd: add page addressed operations to allow access > 4GiB on SD cards
This commit is contained in:
commit
9b2aa40e03
@ -58,8 +58,8 @@
|
||||
/* the external pointer to the system MTD device */
|
||||
mtd_dev_t* mtd0 = 0;
|
||||
|
||||
mtd_dev_t _flash_dev;
|
||||
mtd_desc_t _flash_driver;
|
||||
static mtd_dev_t _flash_dev;
|
||||
static mtd_desc_t _flash_driver;
|
||||
|
||||
#ifdef MCU_ESP8266
|
||||
|
||||
@ -77,6 +77,8 @@ extern uint32_t spi_flash_get_id(void);
|
||||
static int _flash_init (mtd_dev_t *dev);
|
||||
static int _flash_read (mtd_dev_t *dev, void *buff, uint32_t addr, uint32_t size);
|
||||
static int _flash_write (mtd_dev_t *dev, const void *buff, uint32_t addr, uint32_t size);
|
||||
static int _flash_write_page (mtd_dev_t *dev, const void *buff, uint32_t page,
|
||||
uint32_t offset, uint32_t size);
|
||||
static int _flash_erase (mtd_dev_t *dev, uint32_t addr, uint32_t size);
|
||||
static int _flash_power (mtd_dev_t *dev, enum mtd_power_state power);
|
||||
|
||||
@ -120,6 +122,7 @@ void spi_flash_drive_init (void)
|
||||
_flash_driver.init = &_flash_init;
|
||||
_flash_driver.read = &_flash_read;
|
||||
_flash_driver.write = &_flash_write;
|
||||
_flash_driver.write_page = &_flash_write_page;
|
||||
_flash_driver.erase = &_flash_erase;
|
||||
_flash_driver.power = &_flash_power;
|
||||
|
||||
@ -528,6 +531,16 @@ static int _flash_write (mtd_dev_t *dev, const void *buff, uint32_t addr, uint32
|
||||
return (spi_flash_write(_flash_beg + addr, buff, size) == ESP_OK) ? 0 : -EIO;
|
||||
}
|
||||
|
||||
static int _flash_write_page (mtd_dev_t *dev, const void *buff, uint32_t page, uint32_t offset,
|
||||
uint32_t size)
|
||||
{
|
||||
uint32_t addr = _flash_beg + page * _flashchip->page_size + offset;
|
||||
uint32_t remaining = _flashchip->page_size - offset;
|
||||
size = MIN(size, remaining);
|
||||
|
||||
return (spi_flash_write(addr, buff, size) == ESP_OK) ? (int) size : -EIO;
|
||||
}
|
||||
|
||||
static int _flash_erase (mtd_dev_t *dev, uint32_t addr, uint32_t size)
|
||||
{
|
||||
DEBUG("%s dev=%p addr=%08x size=%u\n", __func__, dev, addr, size);
|
||||
|
||||
@ -103,6 +103,42 @@ int _read(const at24cxxx_t *dev, uint32_t pos, void *data, size_t len)
|
||||
return check;
|
||||
}
|
||||
|
||||
static
|
||||
int _write_page(const at24cxxx_t *dev, uint32_t pos, const void *data, size_t len)
|
||||
{
|
||||
int check;
|
||||
uint8_t polls = DEV_MAX_POLLS;
|
||||
uint8_t dev_addr;
|
||||
uint16_t _pos;
|
||||
uint8_t flags = 0;
|
||||
|
||||
if (DEV_EEPROM_SIZE > 2048) {
|
||||
/* 2 bytes word address length if more than 11 bits are
|
||||
used for addressing */
|
||||
/* append page address bits to device address (if any) */
|
||||
dev_addr = (DEV_I2C_ADDR | ((pos & 0xFF0000) >> 16));
|
||||
_pos = (pos & 0xFFFF);
|
||||
flags = I2C_REG16;
|
||||
}
|
||||
else {
|
||||
/* append page address bits to device address (if any) */
|
||||
dev_addr = (DEV_I2C_ADDR | ((pos & 0xFF00) >> 8));
|
||||
_pos = pos & 0xFF;
|
||||
}
|
||||
|
||||
while (-ENXIO == (check = i2c_write_regs(DEV_I2C_BUS, dev_addr,
|
||||
_pos, data, len, flags))) {
|
||||
if (--polls == 0) {
|
||||
break;
|
||||
}
|
||||
xtimer_usleep(AT24CXXX_POLL_DELAY_US);
|
||||
}
|
||||
|
||||
DEBUG("[at24cxxx] i2c_write_regs(): %d; polls: %d\n", check, polls);
|
||||
|
||||
return check;
|
||||
}
|
||||
|
||||
static
|
||||
int _write(const at24cxxx_t *dev, uint32_t pos, const void *data, size_t len)
|
||||
{
|
||||
@ -111,31 +147,9 @@ int _write(const at24cxxx_t *dev, uint32_t pos, const void *data, size_t len)
|
||||
|
||||
while (len) {
|
||||
size_t clen = MIN(len, DEV_PAGE_SIZE - MOD_POW2(pos, DEV_PAGE_SIZE));
|
||||
uint8_t polls = DEV_MAX_POLLS;
|
||||
uint8_t dev_addr;
|
||||
uint16_t _pos;
|
||||
uint8_t flags = 0;
|
||||
if (DEV_EEPROM_SIZE > 2048) {
|
||||
/* 2 bytes word address length if more than 11 bits are
|
||||
used for addressing */
|
||||
/* append page address bits to device address (if any) */
|
||||
dev_addr = (DEV_I2C_ADDR | ((pos & 0xFF0000) >> 16));
|
||||
_pos = (pos & 0xFFFF);
|
||||
flags = I2C_REG16;
|
||||
}
|
||||
else {
|
||||
/* append page address bits to device address (if any) */
|
||||
dev_addr = (DEV_I2C_ADDR | ((pos & 0xFF00) >> 8));
|
||||
_pos = pos & 0xFF;
|
||||
}
|
||||
while (-ENXIO == (check = i2c_write_regs(DEV_I2C_BUS, dev_addr,
|
||||
_pos, cdata, clen, flags))) {
|
||||
if (--polls == 0) {
|
||||
break;
|
||||
}
|
||||
xtimer_usleep(AT24CXXX_POLL_DELAY_US);
|
||||
}
|
||||
DEBUG("[at24cxxx] i2c_write_regs(): %d; polls: %d\n", check, polls);
|
||||
|
||||
check = _write_page(dev, pos, cdata, clen);
|
||||
|
||||
if (!check) {
|
||||
len -= clen;
|
||||
pos += clen;
|
||||
@ -243,6 +257,24 @@ int at24cxxx_write(const at24cxxx_t *dev, uint32_t pos, const void *data,
|
||||
return check;
|
||||
}
|
||||
|
||||
int at24cxxx_write_page(const at24cxxx_t *dev, uint32_t page, uint32_t offset,
|
||||
const void *data, size_t len)
|
||||
{
|
||||
int check;
|
||||
|
||||
assert(offset < DEV_PAGE_SIZE);
|
||||
|
||||
/* write no more than to the end of the current page to prevent wrap-around */
|
||||
size_t remaining = DEV_PAGE_SIZE - offset;
|
||||
len = MIN(len, remaining);
|
||||
|
||||
i2c_acquire(DEV_I2C_BUS);
|
||||
check = _write_page(dev, page * DEV_PAGE_SIZE + offset, data, len);
|
||||
i2c_release(DEV_I2C_BUS);
|
||||
|
||||
return check ? check : (int) len;
|
||||
}
|
||||
|
||||
int at24cxxx_set(const at24cxxx_t *dev, uint32_t pos, uint8_t val,
|
||||
size_t len)
|
||||
{
|
||||
|
||||
@ -56,6 +56,12 @@ static int _mtd_at24cxxx_write(mtd_dev_t *mtd, const void *src, uint32_t addr,
|
||||
return at24cxxx_write(DEV(mtd), addr, src, size) == AT24CXXX_OK ? 0 : -EIO;
|
||||
}
|
||||
|
||||
static int mtd_at24cxxx_write_page(mtd_dev_t *mtd, const void *src, uint32_t page,
|
||||
uint32_t offset, uint32_t size)
|
||||
{
|
||||
return at24cxxx_write_page(DEV(mtd), page, offset, src, size);
|
||||
}
|
||||
|
||||
static int _mtd_at24cxxx_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size)
|
||||
{
|
||||
return at24cxxx_clear(DEV(mtd), addr, size) == AT24CXXX_OK ? 0 : -EIO;
|
||||
@ -72,6 +78,7 @@ const mtd_desc_t mtd_at24cxxx_driver = {
|
||||
.init = _mtd_at24cxxx_init,
|
||||
.read = _mtd_at24cxxx_read,
|
||||
.write = _mtd_at24cxxx_write,
|
||||
.write_page = mtd_at24cxxx_write_page,
|
||||
.erase = _mtd_at24cxxx_erase,
|
||||
.power = _mtd_at24cxxx_power
|
||||
};
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
#include "at25xxx.h"
|
||||
#include "at25xxx_constants.h"
|
||||
#include "at25xxx_params.h"
|
||||
#include "bitarithm.h"
|
||||
#include "byteorder.h"
|
||||
|
||||
#include "xtimer.h"
|
||||
@ -81,12 +82,14 @@ static inline int _wait_until_eeprom_ready(const at25xxx_t *dev)
|
||||
return tries == 0 ? -ETIMEDOUT : 0;
|
||||
}
|
||||
|
||||
static ssize_t _write_page(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len)
|
||||
static int _at25xxx_write_page(const at25xxx_t *dev, uint32_t page, uint32_t offset, const void *data, size_t len)
|
||||
{
|
||||
assert(offset < PAGE_SIZE);
|
||||
|
||||
/* write no more than to the end of the current page to prevent wrap-around */
|
||||
size_t remaining = PAGE_SIZE - (pos & (PAGE_SIZE - 1));
|
||||
size_t remaining = PAGE_SIZE - offset;
|
||||
len = min(len, remaining);
|
||||
pos = _pos(CMD_WRITE, pos);
|
||||
uint32_t pos = _pos(CMD_WRITE, page * PAGE_SIZE + offset);
|
||||
|
||||
/* wait for previous write to finish - may take up to 5 ms */
|
||||
int res = _wait_until_eeprom_ready(dev);
|
||||
@ -111,6 +114,17 @@ static ssize_t _write_page(const at25xxx_t *dev, uint32_t pos, const void *data,
|
||||
return len;
|
||||
}
|
||||
|
||||
int at25xxx_write_page(const at25xxx_t *dev, uint32_t page, uint32_t offset, const void *data, size_t len)
|
||||
{
|
||||
int res;
|
||||
|
||||
getbus(dev);
|
||||
res = _at25xxx_write_page(dev, page, offset, data, len);
|
||||
spi_release(dev->params.spi);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int at25xxx_write(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len)
|
||||
{
|
||||
int res = 0;
|
||||
@ -120,18 +134,32 @@ int at25xxx_write(const at25xxx_t *dev, uint32_t pos, const void *data, size_t l
|
||||
return -ERANGE;
|
||||
}
|
||||
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(PAGE_SIZE);
|
||||
const uint32_t page_mask = PAGE_SIZE - 1;
|
||||
|
||||
uint32_t page = pos >> page_shift;
|
||||
uint32_t offset = pos & page_mask;
|
||||
|
||||
getbus(dev);
|
||||
|
||||
while (len) {
|
||||
ssize_t written = _write_page(dev, pos, d, len);
|
||||
ssize_t written = _at25xxx_write_page(dev, page, offset, d, len);
|
||||
|
||||
if (written < 0) {
|
||||
res = written;
|
||||
break;
|
||||
}
|
||||
|
||||
len -= written;
|
||||
pos += written;
|
||||
d += written;
|
||||
|
||||
if (len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
d += written;
|
||||
page += (offset + written) >> page_shift;
|
||||
offset = (offset + written) & page_mask;
|
||||
}
|
||||
|
||||
spi_release(dev->params.spi);
|
||||
@ -177,7 +205,6 @@ uint8_t at25xxx_read_byte(const at25xxx_t *dev, uint32_t pos)
|
||||
int at25xxx_set(const at25xxx_t *dev, uint32_t pos, uint8_t val, size_t len)
|
||||
{
|
||||
uint8_t data[AT225XXXX_SET_BUF_SIZE];
|
||||
size_t total = 0;
|
||||
|
||||
if (pos + len > dev->params.size) {
|
||||
return -ERANGE;
|
||||
@ -185,13 +212,20 @@ int at25xxx_set(const at25xxx_t *dev, uint32_t pos, uint8_t val, size_t len)
|
||||
|
||||
memset(data, val, sizeof(data));
|
||||
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(PAGE_SIZE);
|
||||
const uint32_t page_mask = PAGE_SIZE - 1;
|
||||
|
||||
uint32_t page = pos >> page_shift;
|
||||
uint32_t offset = pos & page_mask;
|
||||
|
||||
getbus(dev);
|
||||
|
||||
while (len) {
|
||||
size_t written = _write_page(dev, pos, data, min(len, sizeof(data)));
|
||||
len -= written;
|
||||
pos += written;
|
||||
total += written;
|
||||
size_t written = _at25xxx_write_page(dev, page, offset, data, min(len, sizeof(data)));
|
||||
len -= written;
|
||||
page += (offset + written) >> page_shift;
|
||||
offset = (offset + written) & page_mask;
|
||||
}
|
||||
|
||||
spi_release(dev->params.spi);
|
||||
|
||||
@ -57,6 +57,15 @@ static int mtd_at25xxx_write(mtd_dev_t *dev, const void *buff, uint32_t addr, ui
|
||||
return at25xxx_write(mtd_at25xxx_->at25xxx_eeprom, addr, buff, size);
|
||||
}
|
||||
|
||||
static int mtd_at25xxx_write_page(mtd_dev_t *dev, const void *src, uint32_t page, uint32_t offset,
|
||||
uint32_t size)
|
||||
{
|
||||
DEBUG("[mtd_at25xxx] write_page: page:%" PRIu32 " offset:%" PRIu32 " size:%" PRIu32 "\n",
|
||||
page, offset, size);
|
||||
mtd_at25xxx_t *mtd_at25xxx_ = (mtd_at25xxx_t*)dev;
|
||||
return at25xxx_write_page(mtd_at25xxx_->at25xxx_eeprom, page, offset, src, size);
|
||||
}
|
||||
|
||||
static int mtd_at25xxx_erase(mtd_dev_t *dev, uint32_t addr, uint32_t size)
|
||||
{
|
||||
DEBUG("[mtd_at25xxx] mtd_at25xxx_erase: addr:%" PRIu32 " size:%" PRIu32 "\n", addr, size);
|
||||
@ -78,6 +87,7 @@ const mtd_desc_t mtd_at25xxx_driver = {
|
||||
.init = mtd_at25xxx_init,
|
||||
.read = mtd_at25xxx_read,
|
||||
.write = mtd_at25xxx_write,
|
||||
.write_page = mtd_at25xxx_write_page,
|
||||
.erase = mtd_at25xxx_erase,
|
||||
.power = mtd_at25xxx_power,
|
||||
};
|
||||
|
||||
@ -129,6 +129,23 @@ int at24cxxx_write_byte(const at24cxxx_t *dev, uint32_t pos, uint8_t data);
|
||||
int at24cxxx_write(const at24cxxx_t *dev, uint32_t pos, const void *data,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* @brief Sequentially write @p len bytes to a given @p page.
|
||||
* The function will write up to the page boundary and then return
|
||||
* the number of bytes written up to that.
|
||||
*
|
||||
* @param[in] dev AT24CXXX device handle
|
||||
* @param[in] page page of EEPROM memory
|
||||
* @param[in] offset offset from the start of the page, must be < page size
|
||||
* @param[in] data write buffer
|
||||
* @param[in] len requested length to be written
|
||||
*
|
||||
* @return number of bytes written on success
|
||||
* @return error on failure
|
||||
*/
|
||||
int at24cxxx_write_page(const at24cxxx_t *dev, uint32_t page, uint32_t offset,
|
||||
const void *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Set @p len bytes from a given position @p pos to the
|
||||
* value @p val
|
||||
|
||||
@ -109,6 +109,22 @@ void at25xxx_write_byte(const at25xxx_t *dev, uint32_t pos, uint8_t data);
|
||||
*/
|
||||
int at25xxx_write(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Sequentially write @p len bytes to a given @p page.
|
||||
* The function will write up to the page boundary and then return.
|
||||
*
|
||||
* @param[in] dev AT25XXX device handle
|
||||
* @param[in] page page of EEPROM memory
|
||||
* @param[in] offset offset from the start of the page, must be < page size
|
||||
* @param[in] data write buffer
|
||||
* @param[in] len requested length to be written
|
||||
*
|
||||
* @return number of bytes written on success
|
||||
* @return error on failure
|
||||
*/
|
||||
int at25xxx_write_page(const at25xxx_t *dev, uint32_t page, uint32_t offset,
|
||||
const void *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Set @p len bytes from a given position @p pos to the
|
||||
* value @p val
|
||||
|
||||
@ -101,6 +101,27 @@ struct mtd_desc {
|
||||
uint32_t addr,
|
||||
uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Read from the Memory Technology Device (MTD) using
|
||||
* pagewise addressing.
|
||||
*
|
||||
* @p offset should not exceed the page size
|
||||
*
|
||||
* @param[in] dev Pointer to the selected driver
|
||||
* @param[out] buff Pointer to the data buffer to store read data
|
||||
* @param[in] page Page number to start reading from
|
||||
* @param[in] offset Byte offset from the start of the page
|
||||
* @param[in] size Number of bytes
|
||||
*
|
||||
* @return number of bytes read on success
|
||||
* @return < 0 value on error
|
||||
*/
|
||||
int (*read_page)(mtd_dev_t *dev,
|
||||
void *buff,
|
||||
uint32_t page,
|
||||
uint32_t offset,
|
||||
uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Write to the Memory Technology Device (MTD)
|
||||
*
|
||||
@ -120,6 +141,27 @@ struct mtd_desc {
|
||||
uint32_t addr,
|
||||
uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Write to the Memory Technology Device (MTD) using
|
||||
* pagewise addressing.
|
||||
*
|
||||
* @p offset should not exceed the page size
|
||||
*
|
||||
* @param[in] dev Pointer to the selected driver
|
||||
* @param[out] buff Pointer to the data to be written
|
||||
* @param[in] page Page number to start writing to
|
||||
* @param[in] offset Byte offset from the start of the page
|
||||
* @param[in] size Number of bytes
|
||||
*
|
||||
* @return bytes written on success
|
||||
* @return < 0 value on error
|
||||
*/
|
||||
int (*write_page)(mtd_dev_t *dev,
|
||||
const void *buff,
|
||||
uint32_t page,
|
||||
uint32_t offset,
|
||||
uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Erase sector(s) over the Memory Technology Device (MTD)
|
||||
*
|
||||
@ -136,6 +178,21 @@ struct mtd_desc {
|
||||
uint32_t addr,
|
||||
uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Erase sector(s) of the Memory Technology Device (MTD)
|
||||
*
|
||||
* @param[in] dev Pointer to the selected driver
|
||||
* @param[in] sector the first sector number to erase
|
||||
|
||||
* @param[in] count Number of sectors to erase
|
||||
*
|
||||
* @return 0 on success
|
||||
* @return < 0 value on error
|
||||
*/
|
||||
int (*erase_sector)(mtd_dev_t *dev,
|
||||
uint32_t sector,
|
||||
uint32_t count);
|
||||
|
||||
/**
|
||||
* @brief Control power of Memory Technology Device (MTD)
|
||||
*
|
||||
@ -176,6 +233,29 @@ int mtd_init(mtd_dev_t *mtd);
|
||||
*/
|
||||
int mtd_read(mtd_dev_t *mtd, void *dest, uint32_t addr, uint32_t count);
|
||||
|
||||
/**
|
||||
* @brief Read data from a MTD device with pagewise addressing
|
||||
*
|
||||
* The MTD layer will take care of splitting up the transaction into multiple
|
||||
* reads if it is required by the underlying storage media.
|
||||
*
|
||||
* @p offset must be smaller than the page size
|
||||
*
|
||||
* @param mtd the device to read from
|
||||
* @param[out] dest the buffer to fill in
|
||||
* @param[in] page Page number to start reading from
|
||||
* @param[in] offset offset from the start of the page (in bytes)
|
||||
* @param[in] size the number of bytes to read
|
||||
*
|
||||
* @return 0 on success
|
||||
* @return < 0 if an error occurred
|
||||
* @return -ENODEV if @p mtd is not a valid device
|
||||
* @return -ENOTSUP if operation is not supported on @p mtd
|
||||
* @return -EOVERFLOW if @p addr or @p count are not valid, i.e. outside memory
|
||||
* @return -EIO if I/O error occurred
|
||||
*/
|
||||
int mtd_read_page(mtd_dev_t *mtd, void *dest, uint32_t page, uint32_t offset, uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Write data to a MTD device
|
||||
*
|
||||
@ -199,6 +279,31 @@ int mtd_read(mtd_dev_t *mtd, void *dest, uint32_t addr, uint32_t count);
|
||||
*/
|
||||
int mtd_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uint32_t count);
|
||||
|
||||
/**
|
||||
* @brief Write data to a MTD device with pagewise addressing
|
||||
*
|
||||
* The MTD layer will take care of splitting up the transaction into multiple
|
||||
* writes if it is required by the underlying storage media.
|
||||
*
|
||||
* @p offset must be smaller than the page size
|
||||
*
|
||||
*
|
||||
* @param mtd the device to write to
|
||||
* @param[in] src the buffer to write
|
||||
* @param[in] page Page number to start writing to
|
||||
* @param[in] offset byte offset from the start of the page
|
||||
* @param[in] size the number of bytes to write
|
||||
*
|
||||
* @return 0 on success
|
||||
* @return < 0 if an error occurred
|
||||
* @return -ENODEV if @p mtd is not a valid device
|
||||
* @return -ENOTSUP if operation is not supported on @p mtd
|
||||
* @return -EOVERFLOW if @p addr or @p count are not valid, i.e. outside memory,
|
||||
* @return -EIO if I/O error occurred
|
||||
* @return -EINVAL if parameters are invalid
|
||||
*/
|
||||
int mtd_write_page(mtd_dev_t *mtd, const void *src, uint32_t page, uint32_t offset, uint32_t size);
|
||||
|
||||
/**
|
||||
* @brief Erase sectors of a MTD device
|
||||
*
|
||||
@ -217,6 +322,22 @@ int mtd_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uint32_t count);
|
||||
*/
|
||||
int mtd_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t count);
|
||||
|
||||
/**
|
||||
* @brief Erase sectors of a MTD device
|
||||
*
|
||||
* @param mtd the device to erase
|
||||
* @param[in] sector the first sector number to erase
|
||||
* @param[in] num the number of sectors to erase
|
||||
*
|
||||
* @return 0 if erase successful
|
||||
* @return < 0 if an error occurred
|
||||
* @return -ENODEV if @p mtd is not a valid device
|
||||
* @return -ENOTSUP if operation is not supported on @p mtd
|
||||
* @return -EOVERFLOW if @p addr or @p sector are not valid, i.e. outside memory
|
||||
* @return -EIO if I/O error occurred
|
||||
*/
|
||||
int mtd_erase_sector(mtd_dev_t *mtd, uint32_t sector, uint32_t num);
|
||||
|
||||
/**
|
||||
* @brief Set power mode on a MTD device
|
||||
*
|
||||
|
||||
@ -18,8 +18,12 @@
|
||||
* @author Vincent Dupont <vincent@otakeys.com>
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "bitarithm.h"
|
||||
#include "mtd.h"
|
||||
|
||||
int mtd_init(mtd_dev_t *mtd)
|
||||
@ -42,12 +46,62 @@ int mtd_read(mtd_dev_t *mtd, void *dest, uint32_t addr, uint32_t count)
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
if (mtd->driver->read) {
|
||||
return mtd->driver->read(mtd, dest, addr, count);
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(mtd->page_size);
|
||||
const uint32_t page_mask = mtd->page_size - 1;
|
||||
|
||||
return mtd_read_page(mtd, dest, addr >> page_shift, addr & page_mask, count);
|
||||
}
|
||||
|
||||
int mtd_read_page(mtd_dev_t *mtd, void *dest, uint32_t page, uint32_t offset,
|
||||
uint32_t count)
|
||||
{
|
||||
if (!mtd || !mtd->driver) {
|
||||
return -ENODEV;
|
||||
}
|
||||
else {
|
||||
return -ENOTSUP;
|
||||
|
||||
if (mtd->driver->read_page == NULL) {
|
||||
/* TODO: remove when all backends implement read_page */
|
||||
if (mtd->driver->read) {
|
||||
return mtd->driver->read(mtd, dest, mtd->page_size * page + offset, count);
|
||||
} else {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
/* Implementation assumes page size is <= INT_MAX and a power of two. */
|
||||
/* We didn't find hardware yet where this is not true. */
|
||||
assert(mtd->page_size <= INT_MAX);
|
||||
assert(bitarithm_bits_set(mtd->page_size) == 1);
|
||||
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(mtd->page_size);
|
||||
const uint32_t page_mask = mtd->page_size - 1;
|
||||
|
||||
page += offset >> page_shift;
|
||||
offset = offset & page_mask;
|
||||
|
||||
char *_dst = dest;
|
||||
|
||||
while (count) {
|
||||
int read_bytes = mtd->driver->read_page(mtd, _dst, page, offset, count);
|
||||
|
||||
if (read_bytes < 0) {
|
||||
return read_bytes;
|
||||
}
|
||||
|
||||
count -= read_bytes;
|
||||
|
||||
if (count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
_dst += read_bytes;
|
||||
page += (offset + read_bytes) >> page_shift;
|
||||
offset = (offset + read_bytes) & page_mask;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mtd_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uint32_t count)
|
||||
@ -56,12 +110,62 @@ int mtd_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uint32_t count)
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
if (mtd->driver->write) {
|
||||
return mtd->driver->write(mtd, src, addr, count);
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(mtd->page_size);
|
||||
const uint32_t page_mask = mtd->page_size - 1;
|
||||
|
||||
return mtd_write_page(mtd, src, addr >> page_shift, addr & page_mask, count);
|
||||
}
|
||||
|
||||
int mtd_write_page(mtd_dev_t *mtd, const void *src, uint32_t page, uint32_t offset,
|
||||
uint32_t count)
|
||||
{
|
||||
if (!mtd || !mtd->driver) {
|
||||
return -ENODEV;
|
||||
}
|
||||
else {
|
||||
return -ENOTSUP;
|
||||
|
||||
if (mtd->driver->write_page == NULL) {
|
||||
/* TODO: remove when all backends implement write_page */
|
||||
if (mtd->driver->write) {
|
||||
return mtd->driver->write(mtd, src, mtd->page_size * page + offset, count);
|
||||
} else {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
/* Implementation assumes page size is <= INT_MAX and a power of two. */
|
||||
/* We didn't find hardware yet where this is not true. */
|
||||
assert(mtd->page_size <= INT_MAX);
|
||||
assert(bitarithm_bits_set(mtd->page_size) == 1);
|
||||
|
||||
/* page size is always a power of two */
|
||||
const uint32_t page_shift = bitarithm_msb(mtd->page_size);
|
||||
const uint32_t page_mask = mtd->page_size - 1;
|
||||
|
||||
page += offset >> page_shift;
|
||||
offset = offset & page_mask;
|
||||
|
||||
const char *_src = src;
|
||||
|
||||
while (count) {
|
||||
int written = mtd->driver->write_page(mtd, _src, page, offset, count);
|
||||
|
||||
if (written < 0) {
|
||||
return written;
|
||||
}
|
||||
|
||||
count -= written;
|
||||
|
||||
if (count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
_src += written;
|
||||
page += (offset + written) >> page_shift;
|
||||
offset = (offset + written) & page_mask;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mtd_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t count)
|
||||
@ -70,12 +174,42 @@ int mtd_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t count)
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
if (mtd->driver->erase) {
|
||||
return mtd->driver->erase(mtd, addr, count);
|
||||
uint32_t sector_size = mtd->pages_per_sector * mtd->page_size;
|
||||
|
||||
if (count % sector_size) {
|
||||
return -EOVERFLOW;
|
||||
}
|
||||
else {
|
||||
return -ENOTSUP;
|
||||
|
||||
if (addr % sector_size) {
|
||||
return -EOVERFLOW;
|
||||
}
|
||||
|
||||
return mtd_erase_sector(mtd, addr / sector_size, count / sector_size);
|
||||
}
|
||||
|
||||
int mtd_erase_sector(mtd_dev_t *mtd, uint32_t sector, uint32_t count)
|
||||
{
|
||||
if (!mtd || !mtd->driver) {
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
if (sector >= mtd->sector_count) {
|
||||
return -EOVERFLOW;
|
||||
}
|
||||
|
||||
if (mtd->driver->erase_sector == NULL) {
|
||||
/* TODO: remove when all backends implement erase_sector */
|
||||
if (mtd->driver->erase) {
|
||||
uint32_t sector_size = mtd->pages_per_sector * mtd->page_size;
|
||||
return mtd->driver->erase(mtd,
|
||||
sector * sector_size,
|
||||
count * sector_size);
|
||||
} else {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
return mtd->driver->erase_sector(mtd, sector, count);
|
||||
}
|
||||
|
||||
int mtd_power(mtd_dev_t *mtd, enum mtd_power_state power)
|
||||
|
||||
@ -29,22 +29,6 @@
|
||||
#include <inttypes.h>
|
||||
#include <errno.h>
|
||||
|
||||
static int mtd_sdcard_init(mtd_dev_t *mtd);
|
||||
static int mtd_sdcard_read(mtd_dev_t *mtd, void *dest, uint32_t addr,
|
||||
uint32_t size);
|
||||
static int mtd_sdcard_write(mtd_dev_t *mtd, const void *src, uint32_t addr,
|
||||
uint32_t size);
|
||||
static int mtd_sdcard_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size);
|
||||
static int mtd_sdcard_power(mtd_dev_t *mtd, enum mtd_power_state power);
|
||||
|
||||
const mtd_desc_t mtd_sdcard_driver = {
|
||||
.init = mtd_sdcard_init,
|
||||
.read = mtd_sdcard_read,
|
||||
.write = mtd_sdcard_write,
|
||||
.erase = mtd_sdcard_erase,
|
||||
.power = mtd_sdcard_power,
|
||||
};
|
||||
|
||||
static int mtd_sdcard_init(mtd_dev_t *dev)
|
||||
{
|
||||
DEBUG("mtd_sdcard_init\n");
|
||||
@ -79,6 +63,28 @@ static int mtd_sdcard_read(mtd_dev_t *dev, void *buff, uint32_t addr,
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
static int mtd_sdcard_read_page(mtd_dev_t *dev, void *buff, uint32_t page,
|
||||
uint32_t offset, uint32_t size)
|
||||
{
|
||||
DEBUG("mtd_sdcard_read_page: page:%" PRIu32 " offset:%" PRIu32 " size:%" PRIu32 "\n",
|
||||
page, offset, size);
|
||||
|
||||
if (offset) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
mtd_sdcard_t *mtd_sd = (mtd_sdcard_t*)dev;
|
||||
sd_rw_response_t err;
|
||||
int res = sdcard_spi_read_blocks(mtd_sd->sd_card, page,
|
||||
buff, SD_HC_BLOCK_SIZE,
|
||||
size / SD_HC_BLOCK_SIZE, &err);
|
||||
|
||||
if (err != SD_RW_OK) {
|
||||
return -EIO;
|
||||
}
|
||||
return res * SD_HC_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
static int mtd_sdcard_write(mtd_dev_t *dev, const void *buff, uint32_t addr,
|
||||
uint32_t size)
|
||||
{
|
||||
@ -95,6 +101,28 @@ static int mtd_sdcard_write(mtd_dev_t *dev, const void *buff, uint32_t addr,
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
static int mtd_sdcard_write_page(mtd_dev_t *dev, const void *buff, uint32_t page,
|
||||
uint32_t offset, uint32_t size)
|
||||
{
|
||||
DEBUG("mtd_sdcard_write_page: page:%" PRIu32 " offset:%" PRIu32 " size:%" PRIu32 "\n",
|
||||
page, offset, size);
|
||||
|
||||
if (offset) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
mtd_sdcard_t *mtd_sd = (mtd_sdcard_t*)dev;
|
||||
sd_rw_response_t err;
|
||||
int res = sdcard_spi_write_blocks(mtd_sd->sd_card, page,
|
||||
buff, SD_HC_BLOCK_SIZE,
|
||||
size / SD_HC_BLOCK_SIZE, &err);
|
||||
|
||||
if (err != SD_RW_OK) {
|
||||
return -EIO;
|
||||
}
|
||||
return res * SD_HC_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
static int mtd_sdcard_erase(mtd_dev_t *dev,
|
||||
uint32_t addr,
|
||||
uint32_t size)
|
||||
@ -121,3 +149,13 @@ static int mtd_sdcard_power(mtd_dev_t *dev, enum mtd_power_state power)
|
||||
(make use of sdcard_spi_params_t.power pin) */
|
||||
return -ENOTSUP; /* currently not supported */
|
||||
}
|
||||
|
||||
const mtd_desc_t mtd_sdcard_driver = {
|
||||
.init = mtd_sdcard_init,
|
||||
.read = mtd_sdcard_read,
|
||||
.read_page = mtd_sdcard_read_page,
|
||||
.write = mtd_sdcard_write,
|
||||
.write_page = mtd_sdcard_write_page,
|
||||
.erase = mtd_sdcard_erase,
|
||||
.power = mtd_sdcard_power,
|
||||
};
|
||||
|
||||
@ -56,6 +56,8 @@
|
||||
|
||||
#define MBIT_AS_BYTES ((1024 * 1024) / 8)
|
||||
|
||||
#define MIN(a, b) ((a) > (b) ? (b) : (a))
|
||||
|
||||
/**
|
||||
* @brief JEDEC memory manufacturer ID codes.
|
||||
*
|
||||
@ -75,14 +77,6 @@ static int mtd_spi_nor_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uin
|
||||
static int mtd_spi_nor_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size);
|
||||
static int mtd_spi_nor_power(mtd_dev_t *mtd, enum mtd_power_state power);
|
||||
|
||||
const mtd_desc_t mtd_spi_nor_driver = {
|
||||
.init = mtd_spi_nor_init,
|
||||
.read = mtd_spi_nor_read,
|
||||
.write = mtd_spi_nor_write,
|
||||
.erase = mtd_spi_nor_erase,
|
||||
.power = mtd_spi_nor_power,
|
||||
};
|
||||
|
||||
static void mtd_spi_acquire(const mtd_spi_nor_t *dev)
|
||||
{
|
||||
spi_acquire(dev->params->spi, dev->params->cs,
|
||||
@ -464,7 +458,7 @@ static int mtd_spi_nor_read(mtd_dev_t *mtd, void *dest, uint32_t addr, uint32_t
|
||||
DEBUG("mtd_spi_nor_read: %p, %p, 0x%" PRIx32 ", 0x%" PRIx32 "\n",
|
||||
(void *)mtd, dest, addr, size);
|
||||
const mtd_spi_nor_t *dev = (mtd_spi_nor_t *)mtd;
|
||||
size_t chipsize = mtd->page_size * mtd->pages_per_sector * mtd->sector_count;
|
||||
uint32_t chipsize = mtd->page_size * mtd->pages_per_sector * mtd->sector_count;
|
||||
if (addr > chipsize) {
|
||||
return -EOVERFLOW;
|
||||
}
|
||||
@ -521,6 +515,35 @@ static int mtd_spi_nor_write(mtd_dev_t *mtd, const void *src, uint32_t addr, uin
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mtd_spi_nor_write_page(mtd_dev_t *mtd, const void *src, uint32_t page, uint32_t offset,
|
||||
uint32_t size)
|
||||
{
|
||||
const mtd_spi_nor_t *dev = (mtd_spi_nor_t *)mtd;
|
||||
|
||||
DEBUG("mtd_spi_nor_write_page: %p, %p, 0x%" PRIx32 ", 0x%" PRIx32 ", 0x%" PRIx32 "\n",
|
||||
(void *)mtd, src, page, offset, size);
|
||||
|
||||
uint32_t remaining = mtd->page_size - offset;
|
||||
size = MIN(remaining, size);
|
||||
|
||||
be_uint32_t addr_be = byteorder_htonl(page * mtd->page_size + offset);
|
||||
|
||||
mtd_spi_acquire(dev);
|
||||
|
||||
/* write enable */
|
||||
mtd_spi_cmd(dev, dev->params->opcode->wren);
|
||||
|
||||
/* Page program */
|
||||
mtd_spi_cmd_addr_write(dev, dev->params->opcode->page_program, addr_be, src, size);
|
||||
|
||||
/* waiting for the command to complete before returning */
|
||||
wait_for_write_complete(dev, 0);
|
||||
|
||||
mtd_spi_release(dev);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
static int mtd_spi_nor_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size)
|
||||
{
|
||||
DEBUG("mtd_spi_nor_erase: %p, 0x%" PRIx32 ", 0x%" PRIx32 "\n",
|
||||
@ -619,3 +642,12 @@ static int mtd_spi_nor_power(mtd_dev_t *mtd, enum mtd_power_state power)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const mtd_desc_t mtd_spi_nor_driver = {
|
||||
.init = mtd_spi_nor_init,
|
||||
.read = mtd_spi_nor_read,
|
||||
.write = mtd_spi_nor_write,
|
||||
.write_page = mtd_spi_nor_write_page,
|
||||
.erase = mtd_spi_nor_erase,
|
||||
.power = mtd_spi_nor_power,
|
||||
};
|
||||
|
||||
@ -95,15 +95,15 @@ DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
|
||||
return RES_PARERR;
|
||||
}
|
||||
|
||||
uint32_t nread = count * fatfs_mtd_devs[pdrv]->page_size;
|
||||
int res = mtd_read(fatfs_mtd_devs[pdrv], buff,
|
||||
sector * fatfs_mtd_devs[pdrv]->page_size,
|
||||
nread);
|
||||
uint32_t sector_size = fatfs_mtd_devs[pdrv]->page_size
|
||||
* fatfs_mtd_devs[pdrv]->pages_per_sector;
|
||||
|
||||
int res = mtd_read_page(fatfs_mtd_devs[pdrv], buff,
|
||||
sector, 0, count * sector_size);
|
||||
|
||||
if (res != 0) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
assert((nread / fatfs_mtd_devs[pdrv]->page_size) == count);
|
||||
return RES_OK;
|
||||
}
|
||||
|
||||
@ -127,23 +127,21 @@ DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count)
|
||||
}
|
||||
|
||||
/* erase memory before writing to it */
|
||||
int res = mtd_erase(fatfs_mtd_devs[pdrv],
|
||||
sector * fatfs_mtd_devs[pdrv]->page_size,
|
||||
count * fatfs_mtd_devs[pdrv]->page_size);
|
||||
int res = mtd_erase_sector(fatfs_mtd_devs[pdrv], sector, count);
|
||||
|
||||
if (res != 0) {
|
||||
return RES_ERROR; /* erase failed! */
|
||||
}
|
||||
|
||||
uint32_t nwrite = count * fatfs_mtd_devs[pdrv]->page_size;
|
||||
res = mtd_write(fatfs_mtd_devs[pdrv], buff,
|
||||
sector * fatfs_mtd_devs[pdrv]->page_size,
|
||||
nwrite);
|
||||
uint32_t sector_size = fatfs_mtd_devs[pdrv]->page_size
|
||||
* fatfs_mtd_devs[pdrv]->pages_per_sector;
|
||||
|
||||
res = mtd_write_page(fatfs_mtd_devs[pdrv], buff,
|
||||
sector, 0, count * sector_size);
|
||||
|
||||
if (res != 0) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
assert((nwrite / fatfs_mtd_devs[pdrv]->page_size) == count);
|
||||
return RES_OK;
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,8 @@ static int _dev_read(const struct lfs_config *c, lfs_block_t block,
|
||||
DEBUG("lfs_read: c=%p, block=%" PRIu32 ", off=%" PRIu32 ", buf=%p, size=%" PRIu32 "\n",
|
||||
(void *)c, block, off, buffer, size);
|
||||
|
||||
return mtd_read(mtd, buffer, ((fs->base_addr + block) * c->block_size) + off, size);
|
||||
return mtd_read_page(mtd, buffer, (fs->base_addr + block) * mtd->pages_per_sector,
|
||||
off, size);
|
||||
}
|
||||
|
||||
static int _dev_write(const struct lfs_config *c, lfs_block_t block,
|
||||
@ -84,17 +85,8 @@ static int _dev_write(const struct lfs_config *c, lfs_block_t block,
|
||||
DEBUG("lfs_write: c=%p, block=%" PRIu32 ", off=%" PRIu32 ", buf=%p, size=%" PRIu32 "\n",
|
||||
(void *)c, block, off, buffer, size);
|
||||
|
||||
const uint8_t *buf = buffer;
|
||||
uint32_t addr = ((fs->base_addr + block) * c->block_size) + off;
|
||||
for (const uint8_t *part = buf; part < buf + size; part += c->prog_size,
|
||||
addr += c->prog_size) {
|
||||
int ret = mtd_write(mtd, part, addr, c->prog_size);
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return mtd_write_page(mtd, buffer, (fs->base_addr + block) * mtd->pages_per_sector,
|
||||
off, size);
|
||||
}
|
||||
|
||||
static int _dev_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
@ -104,12 +96,7 @@ static int _dev_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
|
||||
DEBUG("lfs_erase: c=%p, block=%" PRIu32 "\n", (void *)c, block);
|
||||
|
||||
int ret = mtd_erase(mtd, ((fs->base_addr + block) * c->block_size), c->block_size);
|
||||
if (ret >= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return mtd_erase_sector(mtd, fs->base_addr + block, 1);
|
||||
}
|
||||
|
||||
static int _dev_sync(const struct lfs_config *c)
|
||||
|
||||
@ -72,7 +72,8 @@ static int _dev_read(const struct lfs_config *c, lfs_block_t block,
|
||||
DEBUG("lfs_read: c=%p, block=%" PRIu32 ", off=%" PRIu32 ", buf=%p, size=%" PRIu32 "\n",
|
||||
(void *)c, block, off, buffer, size);
|
||||
|
||||
return mtd_read(mtd, buffer, ((fs->base_addr + block) * c->block_size) + off, size);
|
||||
return mtd_read_page(mtd, buffer, (fs->base_addr + block) * mtd->pages_per_sector,
|
||||
off, size);
|
||||
}
|
||||
|
||||
static int _dev_write(const struct lfs_config *c, lfs_block_t block,
|
||||
@ -84,17 +85,8 @@ static int _dev_write(const struct lfs_config *c, lfs_block_t block,
|
||||
DEBUG("lfs_write: c=%p, block=%" PRIu32 ", off=%" PRIu32 ", buf=%p, size=%" PRIu32 "\n",
|
||||
(void *)c, block, off, buffer, size);
|
||||
|
||||
const uint8_t *buf = buffer;
|
||||
uint32_t addr = ((fs->base_addr + block) * c->block_size) + off;
|
||||
for (const uint8_t *part = buf; part < buf + size; part += c->prog_size,
|
||||
addr += c->prog_size) {
|
||||
int ret = mtd_write(mtd, part, addr, c->prog_size);
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return mtd_write_page(mtd, buffer, (fs->base_addr + block) * mtd->pages_per_sector,
|
||||
off, size);
|
||||
}
|
||||
|
||||
static int _dev_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
@ -104,12 +96,7 @@ static int _dev_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
|
||||
DEBUG("lfs_erase: c=%p, block=%" PRIu32 "\n", (void *)c, block);
|
||||
|
||||
int ret = mtd_erase(mtd, ((fs->base_addr + block) * c->block_size), c->block_size);
|
||||
if (ret >= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return mtd_erase_sector(mtd, fs->base_addr + block, 1);
|
||||
}
|
||||
|
||||
static int _dev_sync(const struct lfs_config *c)
|
||||
|
||||
@ -7,6 +7,7 @@ BOARD_INSUFFICIENT_MEMORY := \
|
||||
msb-430 \
|
||||
msb-430h \
|
||||
nucleo-f031k6 \
|
||||
nucleo-l031k6 \
|
||||
nucleo-f042k6 \
|
||||
stm32f030f4-demo \
|
||||
telosb \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user