diff --git a/drivers/include/mtd_spi_nor.h b/drivers/include/mtd_spi_nor.h index 3852946e0715..4bada7453aa6 100644 --- a/drivers/include/mtd_spi_nor.h +++ b/drivers/include/mtd_spi_nor.h @@ -12,8 +12,29 @@ * @ingroup drivers_storage * @brief Driver for serial NOR flash memory technology devices attached via SPI * - * @{ * + * @section mtd_spi_nor_overview Overview + * Various manufacturers such as ISSI or Macronix offer small SPI NOR Flash which usually + * come in 8-pin packages and are used for persistent data storage. + * This driver adds support for these devices with support for RIOT's MTD subsystem. + * + * @section mtd_spi_nor_usage Usage + * + * The basic functions of all flash devices can be used by just including the + * mtd_spi_nor module. + * + * USEMODULE += mtd_spi_nor + * + * For ISSI and Macronix devices, some data integrity features are provided + * to check if program or erase operations were successful. + * These features can be activated by specifying the "SPI_NOR_F_CHECK_INTEGRITY" flag in + * the "flag" parameter of the mtd_spi_nor_params_t structure. + * + * \n + * Some examples of how to work with the MTD SPI NOR driver can be found in the test for the + * driver or in some board definitions such as for the nRF52840DK. + * + * @{ * @file * @brief Interface definition for the serial flash memory driver * @@ -30,6 +51,7 @@ #include "periph/spi.h" #include "periph/gpio.h" #include "mtd.h" +#include "kernel_defines.h" #ifdef __cplusplus extern "C" @@ -38,6 +60,9 @@ extern "C" /** * @brief SPI NOR flash opcode table + * + * Manufacturer specific opcodes have a short form of the manufacturer as a prefix, + * for example "MX" for "Macronix". */ typedef struct { uint8_t rdid; /**< Read identification (JEDEC ID) */ @@ -53,7 +78,10 @@ typedef struct { uint8_t chip_erase; /**< Chip erase */ uint8_t sleep; /**< Deep power down */ uint8_t wake; /**< Release from deep power down */ - /* TODO: enter 4 byte address mode for large memories */ + uint8_t mx_rdscur; /**< Read security register (Macronix specific) */ + uint8_t issi_rderp; /**< Read extended read parameters register (ISSI specific) */ + uint8_t issi_clerp; /**< Clear extended read parameters register (ISSI specific) */ + uint8_t issi_wrdi; /**< Write disable (ISSI specific) */ } mtd_spi_nor_opcode_t; /** @@ -96,6 +124,22 @@ typedef struct __attribute__((packed)) { */ #define SPI_NOR_F_SECT_64K (4) +/** + * @brief Enable data integrity checks after program/erase operations + * for devices that support it. + */ +#define SPI_NOR_F_CHECK_INTEGRETY (256) + +/** + * @brief Enable functionality that is specific for Macronix devices. + */ +#define SPI_NOR_F_MANUF_MACRONIX (0xC2) + +/** + * @brief Enable functionality that is specific for ISSI devices. + */ +#define SPI_NOR_F_MANUF_ISSI (0x9D) + /** * @brief Compile-time parameters for a serial flash device */ @@ -170,7 +214,8 @@ extern const mtd_desc_t mtd_spi_nor_driver; * The numbers were taken from Micron M25P16, but the same opcodes can * be found in Macronix MX25L25735E, and multiple other data sheets for * different devices, as well as in the Linux kernel, so they seem quite - * sensible for default values. */ + * sensible for default values. + */ extern const mtd_spi_nor_opcode_t mtd_spi_nor_opcode_default; /** diff --git a/drivers/mtd_spi_nor/include/mtd_spi_nor_defines.h b/drivers/mtd_spi_nor/include/mtd_spi_nor_defines.h new file mode 100644 index 000000000000..16df103134f9 --- /dev/null +++ b/drivers/mtd_spi_nor/include/mtd_spi_nor_defines.h @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2024 Technische Universität Hamburg + * + * 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_mtd_spi_nor + * @{ + * + * @file + * @brief Definitions for the MTD SPI NOR Flash driver + * + * @author Christopher Büchse + * + */ + +#ifndef MTD_SPI_NOR_DEFINES_H +#define MTD_SPI_NOR_DEFINES_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Common Status Bits from the Status Register of SPI NOR Flashs + * @{ + */ +/** + * @brief Write In Progress Flag (R) + * + * 0 - Device is ready + * 1 - Write cycle in progress and device is busy + */ +#define SPI_NOR_STATUS_WIP 0x01u + +/** + * @brief Write Enable Latch Flag (R/W) + * + * 0 - Device is not write enabled + * 1 - Device is write enabled + */ +#define SPI_NOR_STATUS_WEL 0x02u + +/** + * @brief Block Protection Bit 0 Flag (R/W) + * + * 0 - Specific blocks are not write-protected + * 1 - Specific blocks are write-protected + */ +#define SPI_NOR_STATUS_BP0 0x04u + +/** + * @brief Block Protection Bit 1 Flag (R/W) + * + * 0 - Specific blocks are not write-protected + * 1 - Specific blocks are write-protected + */ +#define SPI_NOR_STATUS_BP1 0x08u + +/** + * @brief Block Protection Bit 2 Flag (R/W) + * + * 0 - Specific blocks are not write-protected + * 1 - Specific blocks are write-protected + */ +#define SPI_NOR_STATUS_BP2 0x10u + +/** + * @brief Block Protection Bit 3 Flag (R/W) + * + * 0 - Specific blocks are not write-protected + * 1 - Specific blocks are write-protected + */ +#define SPI_NOR_STATUS_BP3 0x20u + +/** + * @brief Quad Enable Flag (R/W) + * + * 0 - Quad output function disabled + * 1 - Quad output function enabled + */ +#define SPI_NOR_STATUS_QE 0x40u + +/** + * @brief Status Register Write Disable Flag (R/W) + * + * 0 - Status Register is not write protected + * 1 - Status Register is write protected + */ +#define SPI_NOR_STATUS_SRWD 0x80u + +/** @} */ + +/** + * @name Macronix Style Security Register Bits + * @note These flags were taken from the MX25L51245G datasheet, but probably apply + * to other devices from Macronix as well. + * @{ + */ +/** + * @brief Secured OTP Flag + * + * 0 - OTP area not factory locked + * 1 - OTP area factory locked + */ +#define MX_SECURITY_SOTP 0x01u + +/** + * @brief Lock-down Secured OTP Flag + * + * 0 - OTP area not (user) locked + * 1 - OTP area locked (can not be programmed/erased) + */ +#define MX_SECURITY_LDSO 0x02u + +/** + * @brief Program Suspend Flag + * + * 0 - Program is not suspended + * 1 - Program suspended + */ +#define MX_SECURITY_PSB 0x04u + +/** + * @brief Erase Suspend Flag + * + * 0 - Erase is not suspended + * 1 - Erase is suspended + */ +#define MX_SECURITY_ESB 0x08u + +/** + * @brief Reserved + */ +#define MX_SECURITY_XXXXX 0x10u + +/** + * @brief Program Fail Flag + * + * 0 - Program Operation succeeded + * 1 - Program Operation failed or region is protected + */ +#define MX_SECURITY_PFAIL 0x20u + +/** + * @brief Erase Fail Flag + * + * 0 - Erase Operation succeeded + * 1 - Erase Operation failed or region is protected + */ +#define MX_SECURITY_EFAIL 0x40u + +/** + * @brief Write Protection Selection Flag + * + * 0 - Normal Write Protect mode + * 1 - Advanced Sector Protection mode + */ +#define MX_SECURITY_WPSEL 0x80u +/** @} */ + +/** + * @name ISSI Style Security Register Bits from Extended Read Register (ERP) + * @note These flags were taken from the IS25LE01G datasheet, but probably + * apply to other devices from ISSI as well. + * @{ + */ + +/** + * @brief Reserved + */ +#define IS_ERP_XXXXX 0x01u + +/** + * @brief Protection Error Flag (R) + * + * 0 - No protection error + * 1 - Protection Error occurred in program or erase + */ +#define IS_ERP_PROT_E 0x02u + +/** + * @brief Program Error Flag (R) + * + * 0 - Program Operation succeeded + * 1 - Program Operation failed or region is protected + */ +#define IS_ERP_P_ERR 0x04u + +/** + * @brief Erase Error Flag (R) + * + * 0 - Erase Operation succeeded + * 1 - Erase Operation failed or region is protected + */ +#define IS_ERP_E_ERR 0x08u + +/** + * @brief Data Learning Pattern Flag (R/W) + * + * 0 - DLP is disabled + * 1 - DLP is enabled + */ +#define IS_ERP_DLPEN 0x10u + +/** + * @brief Output Driver Strength Bit 0 (R/W) + */ +#define IS_ERP_ODS0 0x20u + +/** + * @brief Output Driver Strength Bit 1 (R/W) + */ +#define IS_ERP_ODS1 0x40u + +/** + * @brief Output Driver Strength Bit 2 (R/W) + */ +#define IS_ERP_ODS2 0x80u +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* MTD_SPI_NOR_DEFINES_H */ +/** @} */ diff --git a/drivers/mtd_spi_nor/mtd_spi_nor.c b/drivers/mtd_spi_nor/mtd_spi_nor.c index 869f22871028..9abc52447614 100644 --- a/drivers/mtd_spi_nor/mtd_spi_nor.c +++ b/drivers/mtd_spi_nor/mtd_spi_nor.c @@ -35,6 +35,8 @@ #include "time_units.h" #include "thread.h" +#include "include/mtd_spi_nor_defines.h" + #if IS_USED(MODULE_ZTIMER) #include "ztimer.h" #elif IS_USED(MODULE_XTIMER) @@ -64,7 +66,7 @@ #define MTD_4K (4096ul) #define MTD_4K_ADDR_MASK (0xFFF) -#define MBIT_AS_BYTES ((1024 * 1024) / 8) +#define MBIT_AS_BYTES ((1024UL * 1024UL) / 8) /** * @brief JEDEC memory manufacturer ID codes. @@ -75,8 +77,8 @@ #define JEDEC_BANK(n) ((n) << 8) typedef enum { - SPI_NOR_JEDEC_ATMEL = 0x1F | JEDEC_BANK(1), - SPI_NOR_JEDEC_MICROCHIP = 0xBF | JEDEC_BANK(1), + SPI_NOR_JEDEC_ATMEL = 0x1FU | JEDEC_BANK(1), + SPI_NOR_JEDEC_MICROCHIP = 0xBFU | JEDEC_BANK(1), } jedec_manuf_t; /** @} */ @@ -205,7 +207,8 @@ static void mtd_spi_cmd_read(const mtd_spi_nor_t *dev, uint8_t opcode, void *des * @param[out] src write buffer * @param[in] count number of bytes to write after the opcode has been sent */ -static void __attribute__((unused)) mtd_spi_cmd_write(const mtd_spi_nor_t *dev, uint8_t opcode, const void *src, uint32_t count) +static void __attribute__((unused)) mtd_spi_cmd_write(const mtd_spi_nor_t *dev, uint8_t opcode, + const void *src, uint32_t count) { TRACE("mtd_spi_cmd_write: %p, %02x, %p, %" PRIu32 "\n", (void *)dev, (unsigned int)opcode, src, count); @@ -231,7 +234,7 @@ static void mtd_spi_cmd(const mtd_spi_nor_t *dev, uint8_t opcode) static bool mtd_spi_manuf_match(const mtd_jedec_id_t *id, jedec_manuf_t manuf) { - return manuf == ((id->bank << 8) | id->manuf); + return manuf == (uint32_t)((id->bank << 8) | id->manuf); } /** @@ -313,7 +316,7 @@ static uint32_t mtd_spi_nor_get_size(const mtd_jedec_id_t *id) /* ID 2 is used to encode the product version, usually 1 or 2 */ (id->device[1] & ~0x3) == 0) { /* capacity encoded as power of 32k sectors */ - return (32 * 1024) << (0x1F & id->device[0]); + return (32 * (uint32_t)1024) << (0x1F & id->device[0]); } if (mtd_spi_manuf_match(id, SPI_NOR_JEDEC_MICROCHIP)) { switch (id->device[1]) { @@ -353,6 +356,36 @@ static void delay_us(unsigned us) #endif } +static inline void wait_for_write_enable_cleared(const mtd_spi_nor_t *dev) +{ + do { + uint8_t status; + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status, sizeof(status)); + + TRACE("mtd_spi_nor: wait device status = 0x%02x, waiting !WEL-Flag\n", + (unsigned int)status); + if ((status & SPI_NOR_STATUS_WEL) == 0) { + break; + } + thread_yield(); + } while (1); +} + +static inline void wait_for_write_enable_set(const mtd_spi_nor_t *dev) +{ + do { + uint8_t status; + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status, sizeof(status)); + + TRACE("mtd_spi_nor: wait device status = 0x%02x, waiting WEL-Flag\n", + (unsigned int)status); + if (status & SPI_NOR_STATUS_WEL) { + break; + } + thread_yield(); + } while (1); +} + static inline void wait_for_write_complete(const mtd_spi_nor_t *dev, uint32_t us) { unsigned i = 0, j = 0; @@ -369,8 +402,9 @@ static inline void wait_for_write_complete(const mtd_spi_nor_t *dev, uint32_t us uint8_t status; mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status, sizeof(status)); - TRACE("mtd_spi_nor: wait device status = 0x%02x\n", (unsigned int)status); - if ((status & 1) == 0) { /* TODO magic number */ + TRACE("mtd_spi_nor: wait device status = 0x%02x, waiting !WIP-Flag\n", + (unsigned int)status); + if ((status & SPI_NOR_STATUS_WIP) == 0) { break; } i++; @@ -402,6 +436,49 @@ static inline void wait_for_write_complete(const mtd_spi_nor_t *dev, uint32_t us DEBUG("\n"); } +static inline int check_data_integrity(const mtd_spi_nor_t *dev, int ret) +{ + /* skip the test if the device definitiuon does not include the integrity test */ + if (!(dev->params->flag & SPI_NOR_F_CHECK_INTEGRETY)) { + return ret; + } + + int new_ret = ret; + uint8_t buffer; + + /* read the first byte of the JEDEC ID to distinguish between manufacturers */ + mtd_spi_cmd_read(dev, dev->params->opcode->rdid, &buffer, sizeof(buffer)); + + if (buffer == SPI_NOR_F_MANUF_MACRONIX) { + /* Macronix specific: Read security register */ + mtd_spi_cmd_read(dev, dev->params->opcode->mx_rdscur, &buffer, sizeof(buffer)); + if (buffer & MX_SECURITY_PFAIL || buffer & MX_SECURITY_EFAIL) { + DEBUG("mtd_spi_nor: ERR: program/erase failed! scur = %02x\n", buffer); + + new_ret = -EIO; + } + } else if (buffer == SPI_NOR_F_MANUF_ISSI) { + /* ISSI specific: Read extended read register for data integrity flags */ + mtd_spi_cmd_read(dev, dev->params->opcode->issi_rderp, &buffer, sizeof(buffer)); + if (buffer & IS_ERP_P_ERR || buffer & IS_ERP_E_ERR) { + DEBUG("mtd_spi_nor: ERR: program/erase failed! erp = %02x\n", buffer); + + new_ret = -EIO; + + /* clear the extended read parameters register manually */ + mtd_spi_cmd(dev, dev->params->opcode->issi_clerp); + + /* ISSI flashs do not reset the WEL-Flag when an operation was not *completed* */ + mtd_spi_cmd(dev, dev->params->opcode->issi_wrdi); + } + } else { + DEBUG("mtd_spi_nor: ERR: unknown manufacturer ID = %02x, skip data integrity test\n", + buffer); + } + + return new_ret; +} + static void _init_pins(mtd_spi_nor_t *dev) { DEBUG("mtd_spi_nor_init: init pins\n"); @@ -487,7 +564,8 @@ static int mtd_spi_nor_init(mtd_dev_t *mtd) mtd_spi_nor_t *dev = (mtd_spi_nor_t *)mtd; DEBUG("mtd_spi_nor_init: -> spi: %lx, cs: %lx, opcodes: %p\n", - (unsigned long)_get_spi(dev), (unsigned long)dev->params->cs, (void *)dev->params->opcode); + (unsigned long)_get_spi(dev), (unsigned long)dev->params->cs, + (void *)dev->params->opcode); /* CS, WP, Hold */ _init_pins(dev); @@ -506,7 +584,8 @@ static int mtd_spi_nor_init(mtd_dev_t *mtd) return -EIO; } DEBUG("mtd_spi_nor_init: Found chip with ID: (%d, 0x%02x, 0x%02x, 0x%02x)\n", - dev->jedec_id.bank, dev->jedec_id.manuf, dev->jedec_id.device[0], dev->jedec_id.device[1]); + dev->jedec_id.bank, dev->jedec_id.manuf, + dev->jedec_id.device[0], dev->jedec_id.device[1]); /* derive density from JEDEC ID */ if (mtd->sector_count == 0) { @@ -614,6 +693,7 @@ static int mtd_spi_nor_write_page(mtd_dev_t *mtd, const void *src, uint32_t page uint32_t remaining = mtd->page_size - offset; size = MIN(remaining, size); + int ret = size; uint32_t addr = page * mtd->page_size + offset; @@ -622,19 +702,29 @@ static int mtd_spi_nor_write_page(mtd_dev_t *mtd, const void *src, uint32_t page /* write enable */ mtd_spi_cmd(dev, dev->params->opcode->wren); + /* Wait for WEL-Flag to be set */ + wait_for_write_enable_set(dev); + /* Page program */ mtd_spi_cmd_addr_write(dev, dev->params->opcode->page_program, addr, src, size); /* waiting for the command to complete before returning */ wait_for_write_complete(dev, 0); + /* (optionally) check if the operation was successful */ + ret = check_data_integrity(dev, ret); + + /* Wait for WEL-Flag to be cleared */ + wait_for_write_enable_cleared(dev); + mtd_spi_release(dev); - return size; + return ret; } static int mtd_spi_nor_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size) { + int ret = 0; DEBUG("mtd_spi_nor_erase: %p, 0x%" PRIx32 ", 0x%" PRIx32 "\n", (void *)mtd, addr, size); mtd_spi_nor_t *dev = (mtd_spi_nor_t *)mtd; @@ -664,6 +754,9 @@ static int mtd_spi_nor_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size) /* write enable */ mtd_spi_cmd(dev, dev->params->opcode->wren); + /* Wait for WEL-Flag to be set */ + wait_for_write_enable_set(dev); + if (size == total_size) { mtd_spi_cmd(dev, dev->params->opcode->chip_erase); size -= total_size; @@ -703,10 +796,17 @@ static int mtd_spi_nor_erase(mtd_dev_t *mtd, uint32_t addr, uint32_t size) /* waiting for the command to complete before continuing */ wait_for_write_complete(dev, us); + + /* (optionally) check if the operation was successful */ + ret = check_data_integrity(dev, ret); + + /* Wait for WEL-Flag to be cleared */ + wait_for_write_enable_cleared(dev); + } mtd_spi_release(dev); - return 0; + return ret; } const mtd_desc_t mtd_spi_nor_driver = { diff --git a/drivers/mtd_spi_nor/mtd_spi_nor_configs.c b/drivers/mtd_spi_nor/mtd_spi_nor_configs.c index 65d674b0226d..e66a1e656495 100644 --- a/drivers/mtd_spi_nor/mtd_spi_nor_configs.c +++ b/drivers/mtd_spi_nor/mtd_spi_nor_configs.c @@ -40,6 +40,10 @@ const mtd_spi_nor_opcode_t mtd_spi_nor_opcode_default = { .chip_erase = 0xc7, .sleep = 0xb9, .wake = 0xab, + .mx_rdscur = 0x2b, + .issi_rderp = 0x81, + .issi_clerp = 0x82, + .issi_wrdi = 0x04, }; const mtd_spi_nor_opcode_t mtd_spi_nor_opcode_default_4bytes = { @@ -56,6 +60,10 @@ const mtd_spi_nor_opcode_t mtd_spi_nor_opcode_default_4bytes = { .chip_erase = 0xc7, .sleep = 0xb9, .wake = 0xab, + .mx_rdscur = 0x2b, + .issi_rderp = 0x81, + .issi_clerp = 0x82, + .issi_wrdi = 0x04, }; /** @} */ diff --git a/pkg/littlefs/fs/littlefs_fs.c b/pkg/littlefs/fs/littlefs_fs.c index 6f23c9067651..fcf31d1ca099 100644 --- a/pkg/littlefs/fs/littlefs_fs.c +++ b/pkg/littlefs/fs/littlefs_fs.c @@ -84,7 +84,11 @@ static int _dev_write(const struct lfs_config *c, lfs_block_t block, (void *)c, block, off, buffer, size); uint32_t page = (fs->base_addr + block) * fs->sectors_per_block * mtd->pages_per_sector; - return mtd_write_page_raw(mtd, buffer, page, off, size); + int ret = mtd_write_page_raw(mtd, buffer, page, off, size); + if (ret == -EIO) { + ret = LFS_ERR_CORRUPT; + } + return ret; } static int _dev_erase(const struct lfs_config *c, lfs_block_t block) @@ -95,7 +99,11 @@ static int _dev_erase(const struct lfs_config *c, lfs_block_t block) DEBUG("lfs_erase: c=%p, block=%" PRIu32 "\n", (void *)c, block); uint32_t sector = (fs->base_addr + block) * fs->sectors_per_block; - return mtd_erase_sector(mtd, sector, fs->sectors_per_block); + int ret = mtd_erase_sector(mtd, sector, fs->sectors_per_block); + if (ret == -EIO) { + ret = LFS_ERR_CORRUPT; + } + return ret; } static int _dev_sync(const struct lfs_config *c) diff --git a/pkg/littlefs2/fs/littlefs2_fs.c b/pkg/littlefs2/fs/littlefs2_fs.c index d2968aa6f655..44685704fada 100644 --- a/pkg/littlefs2/fs/littlefs2_fs.c +++ b/pkg/littlefs2/fs/littlefs2_fs.c @@ -84,7 +84,11 @@ static int _dev_write(const struct lfs_config *c, lfs_block_t block, (void *)c, block, off, buffer, size); uint32_t page = (fs->base_addr + block) * fs->sectors_per_block * mtd->pages_per_sector; - return mtd_write_page_raw(mtd, buffer, page, off, size); + int ret = mtd_write_page_raw(mtd, buffer, page, off, size); + if (ret == -EIO) { + ret = LFS_ERR_CORRUPT; + } + return ret; } static int _dev_erase(const struct lfs_config *c, lfs_block_t block) @@ -95,7 +99,12 @@ static int _dev_erase(const struct lfs_config *c, lfs_block_t block) DEBUG("lfs_erase: c=%p, block=%" PRIu32 "\n", (void *)c, block); uint32_t sector = (fs->base_addr + block) * fs->sectors_per_block; - return mtd_erase_sector(mtd, sector, fs->sectors_per_block);} + int ret = mtd_erase_sector(mtd, sector, fs->sectors_per_block); + if (ret == -EIO) { + ret = LFS_ERR_CORRUPT; + } + return ret; +} static int _dev_sync(const struct lfs_config *c) { diff --git a/tests/drivers/mtd_spi_nor/Makefile b/tests/drivers/mtd_spi_nor/Makefile new file mode 100644 index 000000000000..c21ee50315bb --- /dev/null +++ b/tests/drivers/mtd_spi_nor/Makefile @@ -0,0 +1,11 @@ +BOARD ?= nrf52840dk + +include ../Makefile.drivers_common + +USEMODULE += embunit +USEMODULE += mtd_spi_nor + +#CFLAGS += -DTHREAD_STACKSIZE_MAIN=2048 +#CFLAGS += -DISR_STACKSIZE=1024 + +include $(RIOTBASE)/Makefile.include diff --git a/tests/drivers/mtd_spi_nor/Makefile.ci b/tests/drivers/mtd_spi_nor/Makefile.ci new file mode 100644 index 000000000000..72db76ccb5cc --- /dev/null +++ b/tests/drivers/mtd_spi_nor/Makefile.ci @@ -0,0 +1,3 @@ +BOARD_INSUFFICIENT_MEMORY := \ + atmega8 \ + # diff --git a/tests/drivers/mtd_spi_nor/README.md b/tests/drivers/mtd_spi_nor/README.md new file mode 100644 index 000000000000..f4a463b2273e --- /dev/null +++ b/tests/drivers/mtd_spi_nor/README.md @@ -0,0 +1,58 @@ +# Test Program for the MTD SPI NOR Flash Driver + +This program performs a number of tests with the MTD SPI NOR Flash driver, +including reading, writing and erasing of the chip. + +This test will destroy the data present on the chip! + +# Usage + +## nRF52840DK +The test is designed to work with the built-in MX25F6435F flash of the +Nordic Semiconductor nRF52840DK development board. +Currently the nRF52840DK Flash definitions do not have the data integrity options enabled. +To enable these features for the built-in flash, the "SPI_NOR_F_MANUF_CHECK_INTEGRITY" +flag has to be added to the NRF52840DK_NOR_FLAGS define in the +boards/nrf52840dk/include/board.h file. + +To run the test you simply have to compile and run the test: +``` +make flash term +``` +The tests should conclude with "OK (3 tests)". +``` + +Likewise, the tests should conclude with "OK (3 tests)". + +## nRF52840DK with different flash chips +The default SPI Device for using different Flash chips is SPI(0), which is present on +the Arduino Uno style headers. The default pin for Chip Select is P1.5. The SPI Device and +Chip Select pin can be changed with the CFLAGS FLASH_SPI_DEV and FLASH_SPI_CS: +``` +CFLAGS+="-DFLASH_SPI_DEV=0 -DFLASH_SPI_CS='GPIO_PIN(1,5)'" ... +``` + +The Device under Test can be changed with CFLAGS as well: +``` +... CFLAGS+="-DTEST_IS25LE01G" ... +``` + +If the Flash supports the data integrity features, it is automatically enabled in the +mtd_spi_nor_params_t in the flash_dut.c file. + +A full test call would therefore be for example: +``` +CFLAGS+="-DFLASH_SPI_DEV=1 -DFLASH_SPI_CS='GPIO_PIN(1,5)' -DTEST_IS25LE01G" make flash term +``` + +# Tested/Supported Chips + +## ISSI + +- IS25LP128 (no data integrity check features) +- IS25LE01G + +## Macronix + +- M25F6435F (built-in on nRF52840DK) +- M25F12873F diff --git a/tests/drivers/mtd_spi_nor/flash_dut.c b/tests/drivers/mtd_spi_nor/flash_dut.c new file mode 100644 index 000000000000..f216d770a498 --- /dev/null +++ b/tests/drivers/mtd_spi_nor/flash_dut.c @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 Technische Universität Hamburg + * + * 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. + */ +/** + * @{ + * + * @file + * @brief Definitions for different Flash chips that can be tested + * + * @author Christopher Büchse + */ + +#include "timex.h" +#include "mtd_spi_nor.h" + +/* The default CS pin and SPI device is for the built-in flash of the nRF52840DK */ +#ifndef FLASH_SPI_CS +#define FLASH_SPI_CS GPIO_PIN(1, 5) +#endif + +#ifndef FLASH_SPI_DEV +#define FLASH_SPI_DEV 0 +#endif + +#ifdef TEST_IS25LP128 + +#define IS25LP128_PAGE_SIZE (256) +#define IS25LP128_PAGES_PER_SECTOR (16) +#define IS25LP128_SECTOR_COUNT (4096) +#define IS25LP128_FLAGS (SPI_NOR_F_SECT_4K | SPI_NOR_F_SECT_32K | \ + SPI_NOR_F_SECT_64K) +#define IS25LP128_SPI_CLK SPI_CLK_1MHZ +#define IS25LP128_SPI_MODE SPI_MODE_0 + +static const mtd_spi_nor_params_t _is25lp128_flash_nor_params = { + .opcode = &mtd_spi_nor_opcode_default, + .wait_chip_erase = 30LU * US_PER_SEC, + .wait_32k_erase = 100LU *US_PER_MS, + .wait_sector_erase = 70LU * US_PER_MS, + .wait_chip_wake_up = 100LU * US_PER_MS, + .clk = IS25LP128_SPI_CLK, + .flag = IS25LP128_FLAGS, + .spi = SPI_DEV(FLASH_SPI_DEV), + .mode = IS25LP128_SPI_MODE, + .cs = FLASH_SPI_CS, + .wp = GPIO_UNDEF, + .hold = GPIO_UNDEF, +}; + +static mtd_spi_nor_t _is25lp128_nor_dev = { + .base = { + .driver = &mtd_spi_nor_driver, + .page_size = IS25LP128_PAGE_SIZE, + .pages_per_sector = IS25LP128_PAGES_PER_SECTOR, + .sector_count = IS25LP128_SECTOR_COUNT, + }, + .params = &_is25lp128_flash_nor_params, +}; + +MTD_XFA_ADD(_is25lp128_nor_dev, 10); + +#elif defined TEST_IS25LE01G + +#define IS25LE01G_PAGE_SIZE (256) +#define IS25LE01G_PAGES_PER_SECTOR (16) +#define IS25LE01G_SECTOR_COUNT (32768) +#define IS25LE01G_FLAGS (SPI_NOR_F_SECT_4K | SPI_NOR_F_SECT_32K | \ + SPI_NOR_F_SECT_64K | SPI_NOR_F_CHECK_INTEGRETY) +#define IS25LE01G_SPI_CLK SPI_CLK_10MHZ +#define IS25LE01G_SPI_MODE SPI_MODE_0 + +static const mtd_spi_nor_params_t _is25le01g_flash_nor_params = { + .opcode = &mtd_spi_nor_opcode_default, + .wait_chip_erase = 90LU * US_PER_SEC, + .wait_32k_erase = 140LU * US_PER_MS, + .wait_sector_erase = 100LU * US_PER_MS, + .wait_chip_wake_up = 35LU * US_PER_MS, + .clk = IS25LE01G_SPI_CLK, + .flag = IS25LE01G_FLAGS, + .spi = SPI_DEV(FLASH_SPI_DEV), + .mode = IS25LE01G_SPI_MODE, + .cs = FLASH_SPI_CS, + .wp = GPIO_UNDEF, + .hold = GPIO_UNDEF, +}; + +static mtd_spi_nor_t _is25le01g_nor_dev = { + .base = { + .driver = &mtd_spi_nor_driver, + .page_size = IS25LE01G_PAGE_SIZE, + .pages_per_sector = IS25LE01G_PAGES_PER_SECTOR, + .sector_count = IS25LE01G_SECTOR_COUNT, + }, + .params = &_is25le01g_flash_nor_params, +}; + +MTD_XFA_ADD(_is25le01g_nor_dev, 10); + +#elif defined TEST_MX25L12873F + +#define MX25L12873F_PAGE_SIZE (256) +#define MX25L12873F_PAGES_PER_SECTOR (16) +#define MX25L12873F_SECTOR_COUNT (4096) +#define MX25L12873F_FLAGS (SPI_NOR_F_SECT_4K | SPI_NOR_F_SECT_32K | \ + SPI_NOR_F_SECT_64K | SPI_NOR_F_CHECK_INTEGRETY) +#define MX25L12873F_SPI_CLK SPI_CLK_10MHZ +#define MX25L12873F_SPI_MODE SPI_MODE_0 + +static const mtd_spi_nor_params_t _mx25l12873f_flash_nor_params = { + .opcode = &mtd_spi_nor_opcode_default, + .wait_chip_erase = 50LU * US_PER_SEC, + .wait_32k_erase = 150LU *US_PER_MS, + .wait_sector_erase = 40LU * US_PER_MS, + .wait_chip_wake_up = 35LU * US_PER_MS, + .clk = MX25L12873F_SPI_CLK, + .flag = MX25L12873F_FLAGS, + .spi = SPI_DEV(FLASH_SPI_DEV), + .mode = MX25L12873F_SPI_MODE, + .cs = FLASH_SPI_CS, + .wp = GPIO_UNDEF, + .hold = GPIO_UNDEF, +}; + +static mtd_spi_nor_t _mx25l12873f_nor_dev = { + .base = { + .driver = &mtd_spi_nor_driver, + .page_size = MX25L12873F_PAGE_SIZE, + .pages_per_sector = MX25L12873F_PAGES_PER_SECTOR, + .sector_count = MX25L12873F_SECTOR_COUNT, + }, + .params = &_mx25l12873f_flash_nor_params, +}; + +MTD_XFA_ADD(_mx25l12873f_nor_dev, 10); + +#endif /* TEST_MX25L12873F */ +/** @} */ diff --git a/tests/drivers/mtd_spi_nor/main.c b/tests/drivers/mtd_spi_nor/main.c new file mode 100644 index 000000000000..522f72bef932 --- /dev/null +++ b/tests/drivers/mtd_spi_nor/main.c @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 Technische Universität Hamburg + * + * 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. + */ + +/** + * @{ + * + * @file + * @brief Test for the MTD SPI NOR Driver + * + * @author Christopher Büchse + */ + +#include +#include + +#include "busy_wait.h" +#include "embUnit.h" + +#include "mtd_spi_nor.h" +#include "../../drivers/mtd_spi_nor/include/mtd_spi_nor_defines.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +#define ENABLE_TRACE 0 +#define TRACE(...) DEBUG(__VA_ARGS__) + +#define TEST_MTD mtd_dev_get(MTD_NUMOF-1) /* Make sure to get the last device */ + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static inline spi_t _get_spi(const mtd_spi_nor_t *dev) +{ + return dev->params->spi; +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static void mtd_spi_cmd(const mtd_spi_nor_t *dev, uint8_t opcode) +{ + if (IS_ACTIVE(ENABLE_TRACE)) { + TRACE("mtd_spi_cmd: %p, %02x\n", + (void *)dev, (unsigned int)opcode); + } + spi_transfer_byte(_get_spi(dev), dev->params->cs, false, opcode); +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static void mtd_spi_cmd_read(const mtd_spi_nor_t *dev, uint8_t opcode, void *dest, uint32_t count) +{ + if (IS_ACTIVE(ENABLE_TRACE)) { + TRACE("mtd_spi_cmd_read: %p, %02x, %p, %" PRIu32 "\n", + (void *)dev, (unsigned int)opcode, dest, count); + } + spi_transfer_regs(_get_spi(dev), dev->params->cs, opcode, NULL, dest, count); +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static void mtd_spi_cmd_write(const mtd_spi_nor_t *dev, uint8_t opcode, + const void *src, uint32_t count) +{ + if (IS_ACTIVE(ENABLE_TRACE)) { + TRACE("mtd_spi_cmd_write: %p, %02x, %p, %" PRIu32 "\n", + (void *)dev, (unsigned int)opcode, src, count); + } + spi_transfer_regs(_get_spi(dev), dev->params->cs, opcode, + (void *)src, NULL, count); +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static inline void wait_for_write_enable_set(const mtd_spi_nor_t *dev) +{ + do { + uint8_t status; + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status, sizeof(status)); + + if (IS_ACTIVE(ENABLE_TRACE)) { + TRACE("mtd_spi_nor: wait device status = 0x%02x, waiting WEL-Flag\n", + (unsigned int)status); + } + if (status & SPI_NOR_STATUS_WEL) { + break; + } + thread_yield(); + } while (1); +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static void mtd_spi_acquire(const mtd_spi_nor_t *dev) +{ + spi_acquire(_get_spi(dev), dev->params->cs, + dev->params->mode, dev->params->clk); +} + +/* This function was copied from drivers/mtd_spi_nor/mtd_spi_nor.c */ +static void mtd_spi_release(const mtd_spi_nor_t *dev) +{ + spi_release(_get_spi(dev)); +} + +static void setup(void) +{ +} + +static void teardown(void) +{ +} + +static void test_mtd_init(void) +{ + DEBUG("test_mtd_init: Initializing the Device\n"); + + int ret = mtd_init(TEST_MTD); + TEST_ASSERT_EQUAL_INT(0, ret); + + /* make sure the Block Protect bits are not set before we start the tests */ + mtd_spi_nor_t *dev = (mtd_spi_nor_t *)TEST_MTD; + + uint8_t status_reg; + const uint8_t bp_flags = SPI_NOR_STATUS_BP0 | SPI_NOR_STATUS_BP1 | \ + SPI_NOR_STATUS_BP2 | SPI_NOR_STATUS_BP3; + + mtd_init(TEST_MTD); + + /* Revert everything back to the original state */ + mtd_spi_acquire(dev); + + /* check that the Status Register is writable */ + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status_reg, sizeof(status_reg)); + if (status_reg & SPI_NOR_STATUS_SRWD) { + status_reg &= !SPI_NOR_STATUS_SRWD; + } + + /* Send WREN command to write to the Status Register */ + mtd_spi_cmd(dev, dev->params->opcode->wren); + wait_for_write_enable_set(dev); + + /* Mask the status bits and Block Protection Flags */ + status_reg &= !(SPI_NOR_STATUS_WEL | SPI_NOR_STATUS_WIP); + status_reg &= !bp_flags; + + mtd_spi_cmd_write(dev, 0x01, &status_reg, sizeof(status_reg)); /* Opcode: WRSR */ + busy_wait_us(15000); /* writing the SR can take up to 15ms */ + mtd_spi_release(dev); +} + +static void test_mtd_erase(void) +{ + DEBUG("test_mtd_erase: Erasing the first sector\n"); + + const uint32_t sector_size = TEST_MTD->page_size*TEST_MTD->pages_per_sector; + uint8_t buffer[TEST_MTD->page_size]; + + int ret = mtd_erase(TEST_MTD, 0, sector_size); + TEST_ASSERT_EQUAL_INT(0, ret); + + return; + + /* read back the sector and check that it is blank */ + for (uint32_t page = 0; page < TEST_MTD->pages_per_sector; page++) { + ret = mtd_read_page(TEST_MTD, buffer, page, 0, TEST_MTD->page_size); + TEST_ASSERT_EQUAL_INT(0, ret); + + uint8_t sum = 0xFF; + for (uint32_t i = 0; i < TEST_MTD->page_size; i++) { + sum &= buffer[i]; + } + TEST_ASSERT_EQUAL_INT(0xFF, sum); + } +} + +static void test_mtd_block_protect(void) +{ + int ret; + const char test_data[16] = "BLOCK TEST"; + const uint8_t bp_flags = SPI_NOR_STATUS_BP0 | SPI_NOR_STATUS_BP1 | \ + SPI_NOR_STATUS_BP2 | SPI_NOR_STATUS_BP3; + + uint8_t status_reg; + mtd_spi_nor_t *dev = (mtd_spi_nor_t *)TEST_MTD; + + if (!(dev->params->flag & SPI_NOR_F_CHECK_INTEGRETY)) { + DEBUG("test_mtd_block_protect: No security features enabled, skip test.\n"); + return; + } + + DEBUG("test_mtd_block_protect: Checking the Block Protect Functions\n"); + + mtd_spi_acquire(dev); + + /* check that the Status Register is writable */ + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status_reg, sizeof(status_reg)); + if (status_reg & SPI_NOR_STATUS_SRWD) { + status_reg &= !SPI_NOR_STATUS_SRWD; + } + + /* Mask the status bits */ + status_reg &= !(SPI_NOR_STATUS_WEL | SPI_NOR_STATUS_WIP); + + /* Send WREN command to write to the Status Register */ + mtd_spi_cmd(dev, dev->params->opcode->wren); + wait_for_write_enable_set(dev); + + /* read Status Register again */ + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status_reg, sizeof(status_reg)); + TEST_ASSERT_EQUAL_INT(0, status_reg & SPI_NOR_STATUS_SRWD); + TEST_ASSERT_EQUAL_INT(SPI_NOR_STATUS_WEL, status_reg & SPI_NOR_STATUS_WEL); + + /* protect all blocks */ + status_reg |= bp_flags; + mtd_spi_cmd_write(dev, 0x01, &status_reg, sizeof(status_reg)); /* Opcode WRSR */ + busy_wait_us(15000); /* writing the SR can take up to 15ms */ + + /* confirm that the Block Protection was actually set */ + mtd_spi_cmd_read(dev, dev->params->opcode->rdsr, &status_reg, sizeof(status_reg)); + TEST_ASSERT_EQUAL_INT(bp_flags, status_reg & bp_flags); + + mtd_spi_release(dev); + + /* Perform a write test to check if the P_FAIL flag is correctly handled */ + ret = mtd_write_page_raw(TEST_MTD, test_data, 0x00000000, 0x00, sizeof(test_data)); + TEST_ASSERT_EQUAL_INT(-EIO, ret); + + /* wait for one second between the tests */ + for (uint16_t ms = 1000; ms; ms--) { + busy_wait_us((unsigned int)US_PER_MS); + } + + /* Perform an erase test to check if the E_FAIL flag is correctly handled */ + ret = mtd_erase(TEST_MTD, 0x00000000, dev->base.page_size * dev->base.pages_per_sector); + TEST_ASSERT_EQUAL_INT(-EIO, ret); + + /* Revert everything back to the original state */ + mtd_spi_acquire(dev); + + /* Send WREN command to write to the Status Register */ + mtd_spi_cmd(dev, dev->params->opcode->wren); + wait_for_write_enable_set(dev); + + /* Mask the status bits and Block Protection Flags */ + status_reg &= !(SPI_NOR_STATUS_WEL | SPI_NOR_STATUS_WIP); + status_reg &= !bp_flags; + + mtd_spi_cmd_write(dev, 0x01, &status_reg, sizeof(status_reg)); /* Opcode: WRSR */ + busy_wait_us(15000); /* writing the SR can take up to 15ms */ + mtd_spi_release(dev); +} + +Test *tests_mtd_spi_nor_tests(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_mtd_init), + new_TestFixture(test_mtd_erase), + + new_TestFixture(test_mtd_block_protect), + }; + + EMB_UNIT_TESTCALLER(mtd_mtd_spi_nor_tests, setup, teardown, fixtures); + + return (Test *)&mtd_mtd_spi_nor_tests; +} + +int main(void) +{ + DEBUG("tests/mtd_spi_nor: %u MTD devices present, selected device: %u\n", + (uint8_t)MTD_NUMOF, (uint8_t)(MTD_NUMOF-1)); + + TESTS_START(); + TESTS_RUN(tests_mtd_spi_nor_tests()); + TESTS_END(); + return 0; +} +/** @} */