diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index aa2c2318e0..0462770382 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -55,6 +55,11 @@ ifneq (,$(filter at24mac,$(USEMODULE))) USEMODULE += at24cxxx endif +ifneq (,$(filter at25xxx,$(USEMODULE))) + FEATURES_REQUIRED += periph_spi + USEMODULE += xtimer +endif + ifneq (,$(filter at30tse75x,$(USEMODULE))) USEMODULE += xtimer FEATURES_REQUIRED += periph_i2c diff --git a/drivers/Makefile.include b/drivers/Makefile.include index a2d442cae3..e9d78958ae 100644 --- a/drivers/Makefile.include +++ b/drivers/Makefile.include @@ -32,6 +32,10 @@ ifneq (,$(filter at24mac,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/at24mac/include endif +ifneq (,$(filter at25xxx,$(USEMODULE))) + USEMODULE_INCLUDES += $(RIOTBASE)/drivers/at25xxx/include +endif + ifneq (,$(filter at86rf2xx,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/at86rf2xx/include endif diff --git a/drivers/at25xxx/Makefile b/drivers/at25xxx/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/drivers/at25xxx/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/at25xxx/at25xxx.c b/drivers/at25xxx/at25xxx.c new file mode 100644 index 0000000000..33509b9cf0 --- /dev/null +++ b/drivers/at25xxx/at25xxx.c @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2019 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_at25xxx + * @{ + * + * @file + * @brief Driver for the AT25xxx family of SPI-EEPROMs. + * This also includes M95xxx, 25AAxxx, 25LCxxx, + * CAT25xxx & BR25Sxxx. + * + * @author Benjamin Valentin + * @} + */ + +#include +#include +#include + +#include "at25xxx.h" +#include "at25xxx_constants.h" +#include "at25xxx_params.h" +#include "byteorder.h" + +#include "xtimer.h" +#define POLL_DELAY_US (1000) + +#ifndef min +#define min(a, b) ((a) > (b) ? (b) : (a)) +#endif + +#define PAGE_SIZE (dev->params.page_size) +#define ADDR_LEN (AT25XXX_PARAM_ADDR_LEN) +#define ADDR_MSK ((1UL << ADDR_LEN) - 1) + +#ifndef AT25XXXX_SET_BUF_SIZE +/** + * @brief Adjust to configure buffer size + */ +#define AT225XXXX_SET_BUF_SIZE (64) +#endif + +static inline int getbus(const at25xxx_t *dev) +{ + return spi_acquire(dev->params.spi, dev->params.cs_pin, SPI_MODE_0, dev->params.spi_clk); +} + +static inline uint32_t _pos(uint8_t cmd, uint32_t pos) +{ + /* first byte is CMD, then addr with MSB first */ + pos = htonl((pos & ADDR_MSK) | ((uint32_t)cmd << ADDR_LEN)); + pos >>= 8 * sizeof(pos) - (ADDR_LEN + 8); + return pos; +} + +static inline bool _write_in_progress(const at25xxx_t *dev) +{ + return spi_transfer_reg(dev->params.spi, dev->params.cs_pin, CMD_RDSR, 0) & SR_WIP; +} + +static inline bool _write_enabled(const at25xxx_t *dev) +{ + return spi_transfer_reg(dev->params.spi, dev->params.cs_pin, CMD_RDSR, 0) & SR_WEL; +} + +static inline int _wait_until_eeprom_ready(const at25xxx_t *dev) +{ + uint8_t tries = 10; + while (_write_in_progress(dev) && --tries) { + spi_release(dev->params.spi); + xtimer_usleep(POLL_DELAY_US); + getbus(dev); + } + + return tries == 0 ? -ETIMEDOUT : 0; +} + +static ssize_t _write_page(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len) +{ + /* write no more than to the end of the current page to prevent wrap-around */ + size_t remaining = PAGE_SIZE - (pos & (PAGE_SIZE - 1)); + len = min(len, remaining); + pos = _pos(CMD_WRITE, pos); + + /* wait for previous write to finish - may take up to 5 ms */ + int res = _wait_until_eeprom_ready(dev); + if (res) { + return res; + } + + /* set write enable and wait for status change */ + spi_transfer_byte(dev->params.spi, dev->params.cs_pin, false, CMD_WREN); + + unsigned tries = 1000; + while (!_write_enabled(dev) && --tries) {} + + if (tries == 0) { + return -ETIMEDOUT; + } + + /* write the data */ + spi_transfer_bytes(dev->params.spi, dev->params.cs_pin, true, &pos, NULL, 1 + ADDR_LEN / 8); + spi_transfer_bytes(dev->params.spi, dev->params.cs_pin, false, data, NULL, len); + + return len; +} + +int at25xxx_write(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len) +{ + int res = 0; + const uint8_t *d = data; + + if (pos + len > dev->params.size) { + return -ERANGE; + } + + getbus(dev); + + while (len) { + ssize_t written = _write_page(dev, pos, d, len); + if (written < 0) { + res = written; + break; + } + + len -= written; + pos += written; + d += written; + } + + spi_release(dev->params.spi); + + return res; +} + +void at25xxx_write_byte(const at25xxx_t *dev, uint32_t pos, uint8_t data) +{ + at25xxx_write(dev, pos, &data, sizeof(data)); +} + +int at25xxx_read(const at25xxx_t *dev, uint32_t pos, void *data, size_t len) +{ + if (pos + len > dev->params.size) { + return -ERANGE; + } + + getbus(dev); + + /* wait for previous write to finish - may take up to 5 ms */ + int res = _wait_until_eeprom_ready(dev); + if (res) { + return res; + } + + pos = _pos(CMD_READ, pos); + spi_transfer_bytes(dev->params.spi, dev->params.cs_pin, true, &pos, NULL, 1 + ADDR_LEN / 8); + spi_transfer_bytes(dev->params.spi, dev->params.cs_pin, false, NULL, data, len); + + spi_release(dev->params.spi); + + return 0; +} + +uint8_t at25xxx_read_byte(const at25xxx_t *dev, uint32_t pos) +{ + uint8_t b; + at25xxx_read(dev, pos, &b, sizeof(b)); + return b; +} + +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; + } + + memset(data, val, sizeof(data)); + + getbus(dev); + + while (len) { + size_t written = _write_page(dev, pos, data, min(len, sizeof(data))); + len -= written; + pos += written; + total += written; + } + + spi_release(dev->params.spi); + + return 0; +} + +int at25xxx_clear(const at25xxx_t *dev, uint32_t pos, size_t len) +{ + return at25xxx_set(dev, pos, 0, len); +} + +int at25xxx_init(at25xxx_t *dev, const at25xxx_params_t *params) +{ + dev->params = *params; + spi_init_cs(dev->params.spi, dev->params.cs_pin); + + if (dev->params.wp_pin != GPIO_UNDEF) { + gpio_init(dev->params.wp_pin, GPIO_OUT); + gpio_set(dev->params.wp_pin); + } + + if (dev->params.hold_pin != GPIO_UNDEF) { + gpio_init(dev->params.hold_pin, GPIO_OUT); + gpio_set(dev->params.hold_pin); + } + + /* check if the SPI configuration is valid */ + if (getbus(dev) != SPI_OK) { + return -1; + } + spi_release(dev->params.spi); + return 0; +} diff --git a/drivers/at25xxx/include/at25xxx_constants.h b/drivers/at25xxx/include/at25xxx_constants.h new file mode 100644 index 0000000000..4a3ca3c40b --- /dev/null +++ b/drivers/at25xxx/include/at25xxx_constants.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_at25xxx + * @{ + * + * @file + * @brief Commands for the AT25xxx family of SPI-EEPROMs + * + * @author Benjamin Valentin + * @} + */ + +#ifndef AT25XXX_CONSTANTS_H +#define AT25XXX_CONSTANTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define CMD_WREN (0x6) /**< Write Enable */ +#define CMD_WRDI (0x4) /**< Write Disable */ +#define CMD_RDSR (0x5) /**< Read Status Register */ +#define CMD_WRSR (0x1) /**< Write Status Register */ +#define CMD_READ (0x3) /**< Read from Memory Array */ +#define CMD_WRITE (0x2) /**< Write to Memory Array */ + +#define SR_WIP (0x01) /**< Write In Progress */ +#define SR_WEL (0x02) /**< Write Enable Latch */ +#define SR_BP0 (0x04) /**< Block Protect 0 */ +#define SR_BP1 (0x08) /**< Block Protect 1 */ +#define SR_SRWD (0x80) /**< Status Register Write Disable */ + +#ifdef __cplusplus +} +#endif + +#endif /* AT25XXX_CONSTANTS_H */ diff --git a/drivers/at25xxx/include/at25xxx_params.h b/drivers/at25xxx/include/at25xxx_params.h new file mode 100644 index 0000000000..9b1a8db6c0 --- /dev/null +++ b/drivers/at25xxx/include/at25xxx_params.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_at25xxx + * @{ + * + * @file + * @brief Default configuration for the M95M01 EEPROM + * + * @author Benjamin Valentin + * @} + */ + +#ifndef AT25XXX_PARAMS_H +#define AT25XXX_PARAMS_H + +#include "board.h" +#include "at25xxx.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters for the AT25XXX driver + * @{ + */ +#ifndef AT25XXX_PARAM_SPI +#define AT25XXX_PARAM_SPI (SPI_DEV(0)) +#endif +#ifndef AT25XXX_PARAM_SPI_CLK +#define AT25XXX_PARAM_SPI_CLK (SPI_CLK_5MHZ) +#endif +#ifndef AT25XXX_PARAM_CS +#define AT25XXX_PARAM_CS (GPIO_PIN(0, 0)) +#endif +#ifndef AT25XXX_PARAM_WP +#define AT25XXX_PARAM_WP (GPIO_UNDEF) +#endif +#ifndef AT25XXX_PARAM_HOLD +#define AT25XXX_PARAM_HOLD (GPIO_UNDEF) +#endif +#ifndef AT25XXX_PARAM_SIZE +#define AT25XXX_PARAM_SIZE (128 * 1024UL) /* EEPROM size, in bytes */ +#endif +#ifndef AT25XXX_PARAM_ADDR_LEN +#define AT25XXX_PARAM_ADDR_LEN (24) /* Address length, in bits */ +#endif +#ifndef AT25XXX_PARAM_PAGE_SIZE +#define AT25XXX_PARAM_PAGE_SIZE (256) /* Page size, in bytes */ +#endif + +#ifndef AT25XXX_PARAMS +#define AT25XXX_PARAMS { .spi = AT25XXX_PARAM_SPI, \ + .spi_clk = AT25XXX_PARAM_SPI_CLK, \ + .cs_pin = AT25XXX_PARAM_CS, \ + .wp_pin = AT25XXX_PARAM_WP, \ + .hold_pin = AT25XXX_PARAM_HOLD, \ + .size = AT25XXX_PARAM_SIZE, \ + .page_size = AT25XXX_PARAM_PAGE_SIZE } +#endif + +/** + * @brief AT25XXX configuration + */ +static const at25xxx_params_t at25xxx_params[] = +{ + AT25XXX_PARAMS +}; + +#ifdef __cplusplus +} +#endif + +#endif /* AT25XXX_PARAMS_H */ diff --git a/drivers/include/at25xxx.h b/drivers/include/at25xxx.h new file mode 100644 index 0000000000..e4bd598234 --- /dev/null +++ b/drivers/include/at25xxx.h @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup drivers_at25xxx AT25xxx family of SPI-EEPROMs + * @ingroup drivers_misc + * + * @brief This driver also support M95xxx, 25AAxxx, 25LCxxx, CAT25xxx & BR25Sxxx families + * @{ + * + * @file + * @brief Driver for the AT25xxx series of EEPROMs + * + * @author Benjamin Valentin + * @} + */ + +#ifndef AT25XXX_H +#define AT25XXX_H + +#include +#include + +#include "periph/spi.h" +#include "periph/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief struct holding all params needed for device initialization + */ +typedef struct { + spi_t spi; /**< SPI bus the device is connected to */ + spi_clk_t spi_clk; /**< SPI clock speed to use */ + spi_cs_t cs_pin; /**< GPIO pin connected to chip select */ + gpio_t wp_pin; /**< GPIO pin connected to the write-protect pin */ + gpio_t hold_pin; /**< GPIO pin connected to the hold pin */ + uint32_t size; /**< Size of the EEPROM in bytes */ + uint16_t page_size; /**< Page Size of the EEPROM in bytes */ +} at25xxx_params_t; + +/** + * @brief struct that represents an AT25XXX device + */ +typedef struct { + at25xxx_params_t params; /**< parameters */ +} at25xxx_t; + +/** + * @brief Initialize an AT25XXX device handle with AT25XXX parameters + * + * @param[in, out] dev AT25XXX device handle + * @param[in] params AT25XXX parameters + * + * @return 0 on success, -1 on failure + * + */ +int at25xxx_init(at25xxx_t *dev, const at25xxx_params_t *params); + +/** + * @brief Read a byte at a given position @p pos + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * + * @return read byte + */ +uint8_t at25xxx_read_byte(const at25xxx_t *dev, uint32_t pos); + +/** + * @brief Sequentially read @p len bytes from a given position @p pos + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * @param[out] data read buffer + * @param[in] len requested length to be read + * + * @return 0 on success + * @return -ERANGE if pos + len > EEPROM size + */ +int at25xxx_read(const at25xxx_t *dev, uint32_t pos, void *data, size_t len); + +/** + * @brief Write a byte at a given position @p pos + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * @param[in] data value to be written + */ +void at25xxx_write_byte(const at25xxx_t *dev, uint32_t pos, uint8_t data); + +/** + * @brief Sequentially write @p len bytes from a given position @p pos + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * @param[in] data write buffer + * @param[in] len requested length to be written + * + * @return 0 on success + * @return -ERANGE if pos + len > EEPROM size + */ +int at25xxx_write(const at25xxx_t *dev, uint32_t pos, const void *data, size_t len); + +/** + * @brief Set @p len bytes from a given position @p pos to the + * value @p val + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * @param[in] val value to be set + * @param[in] len requested length to be written + * + * @return 0 on success + * @return -ERANGE if pos + len > EEPROM size + */ +int at25xxx_set(const at25xxx_t *dev, uint32_t pos, uint8_t val, size_t len); + +/** + * @brief Set @p len bytes from position @p pos to 0 + * + * This is a wrapper around @see at25xxx_set. + * + * @param[in] dev AT25XXX device handle + * @param[in] pos position in EEPROM memory + * @param[in] len requested length to be written + * + * @return 0 on success + * @return -ERANGE if pos + len > EEPROM size + */ +int at25xxx_clear(const at25xxx_t *dev, uint32_t pos, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* AT25XXX_H */ diff --git a/tests/driver_at25xxx/Makefile b/tests/driver_at25xxx/Makefile new file mode 100644 index 0000000000..ea3e739bb6 --- /dev/null +++ b/tests/driver_at25xxx/Makefile @@ -0,0 +1,6 @@ +include ../Makefile.tests_common + +USEMODULE += at25xxx +USEMODULE += embunit + +include $(RIOTBASE)/Makefile.include diff --git a/tests/driver_at25xxx/README.md b/tests/driver_at25xxx/README.md new file mode 100644 index 0000000000..08ec6be842 --- /dev/null +++ b/tests/driver_at25xxx/README.md @@ -0,0 +1,8 @@ +# About +This is a manual test application for the AT25XXX EEPROM driver. + +# Usage +This test application will initialize the EEPROM with its default parameters. + +After initialization data will be written to the EEPROM and read back to be +checked for integrity. diff --git a/tests/driver_at25xxx/main.c b/tests/driver_at25xxx/main.c new file mode 100644 index 0000000000..b4f1ebeff0 --- /dev/null +++ b/tests/driver_at25xxx/main.c @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Test application for the AT25XXX EEPROM driver + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include + +#include "embUnit.h" + +#include "at25xxx.h" +#include "at25xxx_params.h" + +static at25xxx_t dev; + +static void test_normal_write(void) +{ + const char data_in_a[] = "Hello EEPROM!"; + const char data_in_b[] = "This is a test."; + char data_out[32]; + + TEST_ASSERT_EQUAL_INT(0, at25xxx_write(&dev, 0, data_in_a, sizeof(data_in_a))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, 0, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_STRING(data_in_a, data_out); + + TEST_ASSERT_EQUAL_INT(0, at25xxx_write(&dev, 0, data_in_b, sizeof(data_in_b))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, 0, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_STRING(data_in_b, data_out); +} + +static void test_page_write(void) +{ + const char data_in_a[] = "Hello EEPROM!"; + const char data_in_b[] = "This is a test."; + char data_out[32]; + + TEST_ASSERT_EQUAL_INT(0, at25xxx_write(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_in_a, sizeof(data_in_a))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_STRING(data_in_a, data_out); + + TEST_ASSERT_EQUAL_INT(0, at25xxx_write(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_in_b, sizeof(data_in_b))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_STRING(data_in_b, data_out); +} + +static void test_page_clear(void) +{ + const char data_in_a[] = "Hello EEPROM!"; + char data_out[32]; + char data_clr[32]; + + memset(data_clr, 0, sizeof(data_clr)); + + TEST_ASSERT_EQUAL_INT(0, at25xxx_write(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_in_a, sizeof(data_in_a))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_STRING(data_in_a, data_out); + + TEST_ASSERT_EQUAL_INT(0, at25xxx_clear(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, sizeof(data_out))); + TEST_ASSERT_EQUAL_INT(0, at25xxx_read(&dev, AT25XXX_PARAM_PAGE_SIZE - 5, data_out, sizeof(data_out))); + TEST_ASSERT_EQUAL_INT(0, memcmp(data_out, data_clr, sizeof(data_clr))); +} + +static Test *tests_EEPROM_tests(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_normal_write), + new_TestFixture(test_page_write), + new_TestFixture(test_page_clear), + }; + + EMB_UNIT_TESTCALLER(EEPROM_tests, NULL, NULL, fixtures); + + return (Test *)&EEPROM_tests; +} + +int main(void) +{ + puts("AT25XXX EEPROM driver test application\n"); + + at25xxx_init(&dev, &at25xxx_params[0]); + + printf("EEPROM size: %u kiB\n", (unsigned) (dev.params.size / 1024)); + printf("EEPROM page size: %d bytes\n", AT25XXX_PARAM_PAGE_SIZE); + printf("EEPROM address length: %d bits\n\n", AT25XXX_PARAM_ADDR_LEN); + + TESTS_START(); + TESTS_RUN(tests_EEPROM_tests()); + TESTS_END(); + + return 0; +}