summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCYFS <[email protected]>2026-07-02 11:36:16 +0800
committerRbb666 <[email protected]>2026-07-04 16:45:24 +0800
commite97530fea8fd3ec60de637cd08c1ee3c6c00f193 (patch)
tree9c157d05f80aa33ee959bef54aabf01f81e588bd
parentece7b4fe9fee73ae3458192984671d1edfab58e1 (diff)
[components][spi] add utest cases
-rw-r--r--bsp/gd32/arm/gd32527I-eval/.ci/attachconfig/ci.attachconfig.yml18
-rw-r--r--bsp/gd32/arm/gd32527I-eval/board/linker_scripts/link.ld7
-rw-r--r--bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml20
-rw-r--r--components/drivers/spi/SConscript6
-rw-r--r--components/drivers/spi/utest/Kconfig112
-rw-r--r--components/drivers/spi/utest/SConscript16
-rw-r--r--components/drivers/spi/utest/spi_flash_tc.c431
-rw-r--r--components/drivers/spi/utest/spi_loopback_tc.c452
-rw-r--r--components/utilities/utest/Kconfig1
9 files changed, 1063 insertions, 0 deletions
diff --git a/bsp/gd32/arm/gd32527I-eval/.ci/attachconfig/ci.attachconfig.yml b/bsp/gd32/arm/gd32527I-eval/.ci/attachconfig/ci.attachconfig.yml
new file mode 100644
index 0000000000..09a4c3c475
--- /dev/null
+++ b/bsp/gd32/arm/gd32527I-eval/.ci/attachconfig/ci.attachconfig.yml
@@ -0,0 +1,18 @@
+utest.spi:
+ kconfig:
+ - CONFIG_BSP_USING_SPI=y
+ - CONFIG_BSP_USING_SPI5=y
+ - CONFIG_RT_USING_UTESTCASES=y
+ - CONFIG_RT_CONSOLEBUF_SIZE=256
+ - CONFIG_RT_UTEST_SPI_LOOPBACK=y
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_BUS_NAME="spi5"
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_DEV_NAME="spi_loop"
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_DATA_SIZE=512
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_MAX_HZ=10000000
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_API_SMOKE=y
+ - CONFIG_RT_UTEST_SPI_FLASH=y
+ - CONFIG_RT_UTEST_SPI_FLASH_SPI_DEV_NAME="spi50"
+ - CONFIG_RT_UTEST_SPI_FLASH_BUS_NAME="spi5"
+ - CONFIG_RT_UTEST_SPI_FLASH_CS_PIN=136
+ - CONFIG_RT_UTEST_SPI_FLASH_DEV_NAME="GD25Q16B"
+ - CONFIG_RT_UTEST_SPI_FLASH_MAX_HZ=10000000
diff --git a/bsp/gd32/arm/gd32527I-eval/board/linker_scripts/link.ld b/bsp/gd32/arm/gd32527I-eval/board/linker_scripts/link.ld
index 6fbeea4e92..2ef9902e0e 100644
--- a/bsp/gd32/arm/gd32527I-eval/board/linker_scripts/link.ld
+++ b/bsp/gd32/arm/gd32527I-eval/board/linker_scripts/link.ld
@@ -39,6 +39,13 @@ SECTIONS
__vsymtab_end = .;
. = ALIGN(4);
+ /* section information for utest */
+ . = ALIGN(4);
+ __rt_utest_tc_tab_start = .;
+ KEEP(*(UtestTcTab))
+ __rt_utest_tc_tab_end = .;
+ . = ALIGN(4);
+
/* section information for initial. */
. = ALIGN(4);
__rt_init_start = .;
diff --git a/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml b/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml
index 5492a642cf..6b62ab697a 100644
--- a/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml
+++ b/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml
@@ -202,6 +202,26 @@ utest.pin:
- CONFIG_RT_USING_UTESTCASES=y
- CONFIG_RT_CONSOLEBUF_SIZE=256
- CONFIG_RT_UTEST_PIN=y
+utest.spi:
+ <<: *scons
+ kconfig:
+ - CONFIG_BSP_USING_SPI=y
+ - CONFIG_BSP_USING_SPI1=y
+ - CONFIG_BSP_USING_SPI2=y
+ - CONFIG_RT_USING_UTESTCASES=y
+ - CONFIG_RT_CONSOLEBUF_SIZE=256
+ - CONFIG_RT_UTEST_SPI_LOOPBACK=y
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_BUS_NAME="spi1"
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_DEV_NAME="spi_loop"
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_DATA_SIZE=512
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_MAX_HZ=10000000
+ - CONFIG_RT_UTEST_SPI_LOOPBACK_API_SMOKE=y
+ - CONFIG_RT_UTEST_SPI_FLASH=y
+ - CONFIG_RT_UTEST_SPI_FLASH_SPI_DEV_NAME="spi20"
+ - CONFIG_RT_UTEST_SPI_FLASH_BUS_NAME="spi2"
+ - CONFIG_RT_UTEST_SPI_FLASH_CS_PIN=28
+ - CONFIG_RT_UTEST_SPI_FLASH_DEV_NAME="W25Q64"
+ - CONFIG_RT_UTEST_SPI_FLASH_MAX_HZ=10000000
# ------ component CI ------
component.cherryusb_cdc:
kconfig:
diff --git a/components/drivers/spi/SConscript b/components/drivers/spi/SConscript
index a48767cb25..29499e7e4d 100644
--- a/components/drivers/spi/SConscript
+++ b/components/drivers/spi/SConscript
@@ -1,5 +1,6 @@
from building import *
from gcc import *
+import os
import rtconfig
cwd = GetCurrentDir()
@@ -46,4 +47,9 @@ src += src_device
group = DefineGroup('DeviceDrivers', src, depend = ['RT_USING_SPI'], CPPPATH = CPPPATH, LOCAL_CFLAGS = LOCAL_CFLAGS)
+list = os.listdir(cwd)
+for item in list:
+ if os.path.isfile(os.path.join(cwd, item, 'SConscript')):
+ group = group + SConscript(os.path.join(item, 'SConscript'))
+
Return('group')
diff --git a/components/drivers/spi/utest/Kconfig b/components/drivers/spi/utest/Kconfig
new file mode 100644
index 0000000000..1638916ab5
--- /dev/null
+++ b/components/drivers/spi/utest/Kconfig
@@ -0,0 +1,112 @@
+menu "SPI Test"
+
+config RT_UTEST_SPI_LOOPBACK
+ bool "SPI MOSI/MISO Loopback Test"
+ select RT_USING_SPI
+ default n
+ help
+ This test validates full-duplex loopback data with sendrecv8,
+ transfer and transfer_message over length and clock matrices.
+ The hardware transfer mode, such as polling, interrupt or DMA, is
+ selected by the BSP SPI driver configuration.
+
+if RT_UTEST_SPI_LOOPBACK
+
+config RT_UTEST_SPI_LOOPBACK_BUS_NAME
+ string "SPI bus name for loopback test"
+ default "spi2"
+
+config RT_UTEST_SPI_LOOPBACK_DEV_NAME
+ string "Temporary SPI device name for loopback test"
+ default "spi_loop"
+
+config RT_UTEST_SPI_LOOPBACK_DATA_SIZE
+ int "SPI loopback maximum test data size"
+ range 1 65535
+ default 512
+
+config RT_UTEST_SPI_LOOPBACK_MAX_HZ
+ int "SPI loopback maximum clock frequency (Hz)"
+ range 1 100000000
+ default 10000000
+ help
+ The test automatically builds the clock matrix from standard SPI
+ frequencies up to this value, and always includes this maximum value.
+
+config RT_UTEST_SPI_LOOPBACK_API_SMOKE
+ bool "Enable SPI TX-only/RX-only API smoke test"
+ default n
+ help
+ Enable this to smoke-test TX-only and RX-only transfer APIs by return
+ length. These transfers do not validate MOSI/MISO loopback data and
+ may be unsupported or unstable on some BSP SPI controllers.
+
+config RT_UTEST_SPI_LOOPBACK_TIMEOUT
+ int "SPI loopback test timeout (s)"
+ range 1 3600
+ default 60
+
+endif
+
+config RT_UTEST_SPI_FLASH
+ bool "SPI Flash SFUD Read/Write Test"
+ select RT_USING_SPI
+ select RT_USING_SFUD
+ select RT_SFUD_USING_SFDP
+ select RT_SFUD_USING_FLASH_INFO_TABLE
+ select RT_USING_HEAP
+ default n
+
+if RT_UTEST_SPI_FLASH
+
+config RT_UTEST_SPI_FLASH_SPI_DEV_NAME
+ string "SPI device name for SFUD flash"
+ default "spi20"
+ help
+ The SPI device that connects to the flash, such as spi20. If it does
+ not exist yet, the test attaches it to the configured SPI bus and CS pin.
+
+config RT_UTEST_SPI_FLASH_BUS_NAME
+ string "SPI bus name for flash device attach"
+ default "spi2"
+ help
+ The SPI bus used when the test needs to attach the SPI flash device.
+
+config RT_UTEST_SPI_FLASH_CS_PIN
+ int "SPI flash CS pin index"
+ range -1 32767
+ default -1
+ help
+ Use the RT-Thread pin index used by the BSP, such as a GET_PIN() value
+ on STM32. -1 means PIN_NONE.
+
+config RT_UTEST_SPI_FLASH_DEV_NAME
+ string "SFUD flash model/device name"
+ default "W25Q64"
+ help
+ This is the SFUD flash device name used to find or register the flash,
+ and is usually set to the board flash model name, such as W25Q64 or
+ W25Q128. SFUD still verifies the actual chip by JEDEC ID and SFDP or
+ its flash information table when probing.
+
+config RT_UTEST_SPI_FLASH_MAX_HZ
+ int "SPI flash test maximum clock frequency (Hz)"
+ range 1 50000000
+ default 10000000
+ help
+ The SPI clock used while probing and testing the flash. Keep this
+ conservative for board-level utests because wiring, pinmux and flash
+ parts may vary.
+
+config RT_UTEST_SPI_FLASH_TEST_OFFSET
+ hex "SPI flash test offset"
+ default 0x0
+
+config RT_UTEST_SPI_FLASH_TEST_SIZE
+ int "SPI flash test data size"
+ range 1 4096
+ default 256
+
+endif
+
+endmenu
diff --git a/components/drivers/spi/utest/SConscript b/components/drivers/spi/utest/SConscript
new file mode 100644
index 0000000000..8b7a049e5e
--- /dev/null
+++ b/components/drivers/spi/utest/SConscript
@@ -0,0 +1,16 @@
+Import('rtconfig')
+from building import *
+
+cwd = GetCurrentDir()
+src = []
+CPPPATH = [cwd, cwd + '/..', cwd + '/../sfud/inc']
+
+if GetDepend(['RT_UTEST_SPI_LOOPBACK']):
+ src += ['spi_loopback_tc.c']
+
+if GetDepend(['RT_UTEST_SPI_FLASH']):
+ src += ['spi_flash_tc.c']
+
+group = DefineGroup('utestcases', src, depend = ['RT_USING_UTESTCASES', 'RT_USING_SPI'], CPPPATH = CPPPATH)
+
+Return('group')
diff --git a/components/drivers/spi/utest/spi_flash_tc.c b/components/drivers/spi/utest/spi_flash_tc.c
new file mode 100644
index 0000000000..184e51b368
--- /dev/null
+++ b/components/drivers/spi/utest/spi_flash_tc.c
@@ -0,0 +1,431 @@
+/*
+ * Copyright (c) 2006-2026, RT-Thread Development Team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Change Logs:
+ * Date Author Notes
+ * 2026-07-02 CYFS add SPI flash SFUD read/write utest
+ */
+
+#include <rtthread.h>
+#include <rtdevice.h>
+#include "utest.h"
+
+#ifdef RT_UTEST_SPI_FLASH
+
+#undef DBG_TAG
+#undef DBG_LVL
+#include "dev_spi_flash_sfud.h"
+
+#define SPI_FLASH_SPI_DEV_NAME RT_UTEST_SPI_FLASH_SPI_DEV_NAME
+#define SPI_FLASH_DEV_NAME RT_UTEST_SPI_FLASH_DEV_NAME
+#define SPI_FLASH_BUS_NAME RT_UTEST_SPI_FLASH_BUS_NAME
+#define SPI_FLASH_CS_PIN RT_UTEST_SPI_FLASH_CS_PIN
+#define SPI_FLASH_MAX_HZ RT_UTEST_SPI_FLASH_MAX_HZ
+#define SPI_FLASH_TEST_OFFSET RT_UTEST_SPI_FLASH_TEST_OFFSET
+#define SPI_FLASH_TEST_SIZE RT_UTEST_SPI_FLASH_TEST_SIZE
+
+static rt_spi_flash_device_t spi_flash_probe_dev = RT_NULL;
+static sfud_flash_t sfud_dev = RT_NULL;
+static struct rt_spi_device spi_flash_attached_dev;
+static rt_bool_t spi_flash_attached;
+static rt_uint32_t test_addr;
+static rt_uint32_t backup_addr;
+static rt_uint32_t backup_size;
+static rt_uint8_t *backup_buf = RT_NULL;
+static rt_uint8_t *write_buf = RT_NULL;
+static rt_uint8_t *read_buf = RT_NULL;
+
+static rt_uint32_t spi_flash_align_down(rt_uint32_t value, rt_uint32_t align)
+{
+ return value - value % align;
+}
+
+static rt_uint32_t spi_flash_align_up(rt_uint32_t value, rt_uint32_t align)
+{
+ rt_uint64_t aligned = value;
+
+ aligned = (aligned + align - 1) / align * align;
+ return (rt_uint32_t)aligned;
+}
+
+static rt_err_t spi_flash_restore_backup(void)
+{
+ sfud_err result;
+
+ if (sfud_dev == RT_NULL || backup_buf == RT_NULL || backup_size == 0)
+ {
+ return RT_EOK;
+ }
+
+ result = sfud_erase_write(sfud_dev, backup_addr, backup_size, backup_buf);
+ if (result != SFUD_SUCCESS)
+ {
+ LOG_E("Restore SPI flash data failed, addr: 0x%08x, size: %u.", backup_addr, backup_size);
+ return -RT_ERROR;
+ }
+
+ return RT_EOK;
+}
+
+static void spi_flash_fill_pattern(rt_uint8_t *buf, rt_size_t size)
+{
+ rt_size_t i;
+
+ for (i = 0; i < size; i++)
+ {
+ buf[i] = (rt_uint8_t)(0xA5 ^ (i * 13 + (i >> 2)));
+ }
+}
+
+static void spi_flash_free_buffers(void)
+{
+ if (backup_buf != RT_NULL)
+ {
+ rt_free(backup_buf);
+ backup_buf = RT_NULL;
+ }
+ if (write_buf != RT_NULL)
+ {
+ rt_free(write_buf);
+ write_buf = RT_NULL;
+ }
+ if (read_buf != RT_NULL)
+ {
+ rt_free(read_buf);
+ read_buf = RT_NULL;
+ }
+}
+
+static void spi_flash_delete_probe_dev(void)
+{
+ sfud_dev = RT_NULL;
+ if (spi_flash_probe_dev != RT_NULL)
+ {
+ rt_sfud_flash_delete(spi_flash_probe_dev);
+ spi_flash_probe_dev = RT_NULL;
+ }
+}
+
+static void spi_flash_detach_device(void)
+{
+ if (spi_flash_attached)
+ {
+ rt_spi_bus_detach_device_cspin(&spi_flash_attached_dev);
+ spi_flash_attached = RT_FALSE;
+ }
+}
+
+static rt_bool_t spi_flash_is_all_ff(const rt_uint8_t *buf, rt_size_t size)
+{
+ rt_size_t i;
+
+ for (i = 0; i < size; i++)
+ {
+ if (buf[i] != 0xFF)
+ {
+ return RT_FALSE;
+ }
+ }
+
+ return RT_TRUE;
+}
+
+static sfud_flash_t spi_flash_find_existing(void)
+{
+ rt_device_t flash_dev;
+ rt_device_t spi_dev;
+
+ flash_dev = rt_device_find(SPI_FLASH_DEV_NAME);
+ if (flash_dev != RT_NULL && flash_dev->type == RT_Device_Class_Block)
+ {
+ rt_spi_flash_device_t spi_flash_dev = (rt_spi_flash_device_t)flash_dev;
+
+ if (spi_flash_dev->user_data != RT_NULL)
+ {
+ LOG_I("Use existing SFUD flash device %s.", SPI_FLASH_DEV_NAME);
+ return (sfud_flash_t)spi_flash_dev->user_data;
+ }
+ }
+
+ spi_dev = rt_device_find(SPI_FLASH_SPI_DEV_NAME);
+ if (spi_dev != RT_NULL && spi_dev->type == RT_Device_Class_SPIDevice)
+ {
+ struct rt_spi_device *rt_spi_dev = (struct rt_spi_device *)spi_dev;
+ rt_spi_flash_device_t spi_flash_dev = (rt_spi_flash_device_t)rt_spi_dev->user_data;
+
+ if (spi_flash_dev != RT_NULL && spi_flash_dev->user_data != RT_NULL)
+ {
+ sfud_flash_t flash = (sfud_flash_t)spi_flash_dev->user_data;
+
+ LOG_I("Use existing SFUD flash %s on SPI device %s.",
+ flash->name, SPI_FLASH_SPI_DEV_NAME);
+ return flash;
+ }
+ }
+
+ return RT_NULL;
+}
+
+static rt_err_t spi_flash_attach_device(void)
+{
+ rt_base_t cs_pin = SPI_FLASH_CS_PIN;
+ rt_device_t spi_dev;
+ rt_device_t bus;
+ rt_err_t result;
+
+ spi_dev = rt_device_find(SPI_FLASH_SPI_DEV_NAME);
+ if (spi_dev != RT_NULL)
+ {
+ if (spi_dev->type != RT_Device_Class_SPIDevice)
+ {
+ LOG_E("%s exists but is not an SPI device.", SPI_FLASH_SPI_DEV_NAME);
+ return -RT_ERROR;
+ }
+ return RT_EOK;
+ }
+
+ bus = rt_device_find(SPI_FLASH_BUS_NAME);
+ if (bus == RT_NULL || bus->type != RT_Device_Class_SPIBUS)
+ {
+ LOG_E("SPI bus %s not found.", SPI_FLASH_BUS_NAME);
+ return -RT_ERROR;
+ }
+
+ if (cs_pin < 0)
+ {
+ cs_pin = PIN_NONE;
+ }
+
+ LOG_I("Try to attach SPI flash device %s to bus %s, CS pin %d.",
+ SPI_FLASH_SPI_DEV_NAME, SPI_FLASH_BUS_NAME, (int)cs_pin);
+
+ rt_memset(&spi_flash_attached_dev, 0, sizeof(spi_flash_attached_dev));
+ result = rt_spi_bus_attach_device_cspin(&spi_flash_attached_dev,
+ SPI_FLASH_SPI_DEV_NAME,
+ SPI_FLASH_BUS_NAME,
+ cs_pin,
+ RT_NULL);
+ if (result == RT_EOK)
+ {
+ spi_flash_attached = RT_TRUE;
+ LOG_I("Attach SPI flash device %s to bus %s success.",
+ SPI_FLASH_SPI_DEV_NAME, SPI_FLASH_BUS_NAME);
+ }
+ else
+ {
+ LOG_E("Attach SPI flash device %s to bus %s failed, result: %d.",
+ SPI_FLASH_SPI_DEV_NAME, SPI_FLASH_BUS_NAME, result);
+ }
+
+ return result;
+}
+
+static rt_err_t spi_flash_read_jedec_id(rt_uint8_t id[3])
+{
+ rt_device_t spi_dev;
+ struct rt_spi_device *rt_spi_dev;
+ struct rt_spi_configuration cfg;
+ rt_uint8_t cmd = 0x9F;
+ rt_err_t result;
+
+ spi_dev = rt_device_find(SPI_FLASH_SPI_DEV_NAME);
+ if (spi_dev == RT_NULL || spi_dev->type != RT_Device_Class_SPIDevice)
+ {
+ return -RT_ERROR;
+ }
+
+ rt_spi_dev = (struct rt_spi_device *)spi_dev;
+ rt_memset(&cfg, 0, sizeof(cfg));
+ cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB;
+ cfg.data_width = 8;
+ cfg.max_hz = SPI_FLASH_MAX_HZ;
+
+ result = rt_spi_configure(rt_spi_dev, &cfg);
+ if (result != RT_EOK && result != -RT_EBUSY)
+ {
+ return result;
+ }
+
+ return rt_spi_send_then_recv(rt_spi_dev, &cmd, sizeof(cmd), id, 3);
+}
+
+static void spi_flash_probe_info(void)
+{
+ rt_uint8_t mf_id = sfud_dev->chip.mf_id;
+ rt_uint8_t type_id = sfud_dev->chip.type_id;
+ rt_uint8_t capacity_id = sfud_dev->chip.capacity_id;
+
+ LOG_I("SPI flash %s JEDEC ID %02x %02x %02x, capacity %u bytes, erase granularity %u bytes.",
+ sfud_dev->name, mf_id, type_id, capacity_id, sfud_dev->chip.capacity, sfud_dev->chip.erase_gran);
+
+ uassert_true(sfud_dev->chip.capacity > 0);
+ uassert_true(sfud_dev->chip.erase_gran > 0);
+ uassert_true(sfud_dev->chip.write_mode != 0);
+ uassert_true(sfud_dev->chip.erase_gran_cmd != 0);
+}
+
+static void spi_flash_erase_write_read(void)
+{
+ sfud_err result;
+
+ rt_memset(read_buf, 0, SPI_FLASH_TEST_SIZE);
+ result = sfud_erase(sfud_dev, backup_addr, backup_size);
+ uassert_int_equal(result, SFUD_SUCCESS);
+
+ result = sfud_read(sfud_dev, test_addr, SPI_FLASH_TEST_SIZE, read_buf);
+ uassert_int_equal(result, SFUD_SUCCESS);
+ uassert_true(spi_flash_is_all_ff(read_buf, SPI_FLASH_TEST_SIZE));
+
+ spi_flash_fill_pattern(write_buf, SPI_FLASH_TEST_SIZE);
+ result = sfud_write(sfud_dev, test_addr, SPI_FLASH_TEST_SIZE, write_buf);
+ uassert_int_equal(result, SFUD_SUCCESS);
+
+ rt_memset(read_buf, 0, SPI_FLASH_TEST_SIZE);
+ result = sfud_read(sfud_dev, test_addr, SPI_FLASH_TEST_SIZE, read_buf);
+ uassert_int_equal(result, SFUD_SUCCESS);
+ uassert_buf_equal(write_buf, read_buf, SPI_FLASH_TEST_SIZE);
+}
+
+static rt_err_t utest_tc_init(void)
+{
+ rt_uint32_t erase_gran;
+
+ sfud_dev = spi_flash_find_existing();
+ if (sfud_dev == RT_NULL && spi_flash_attach_device() != RT_EOK)
+ {
+ LOG_E("Attach SPI flash device %s on bus %s failed.",
+ SPI_FLASH_SPI_DEV_NAME, SPI_FLASH_BUS_NAME);
+ return -RT_ERROR;
+ }
+
+ if (sfud_dev == RT_NULL)
+ {
+ rt_uint8_t jedec_id[3] = {0};
+
+ if (spi_flash_read_jedec_id(jedec_id) == RT_EOK)
+ {
+ LOG_I("SPI flash raw JEDEC ID: %02x %02x %02x at %u Hz.",
+ jedec_id[0], jedec_id[1], jedec_id[2], (unsigned int)SPI_FLASH_MAX_HZ);
+ }
+ else
+ {
+ LOG_W("Read SPI flash raw JEDEC ID failed before SFUD probe.");
+ }
+
+ LOG_I("Try to probe SFUD flash device %s by SPI device %s.",
+ SPI_FLASH_DEV_NAME, SPI_FLASH_SPI_DEV_NAME);
+ {
+ struct rt_spi_configuration cfg;
+
+ rt_memset(&cfg, 0, sizeof(cfg));
+ cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB;
+ cfg.data_width = 8;
+ cfg.max_hz = SPI_FLASH_MAX_HZ;
+ spi_flash_probe_dev = rt_sfud_flash_probe_ex(SPI_FLASH_DEV_NAME,
+ SPI_FLASH_SPI_DEV_NAME,
+ &cfg,
+ RT_NULL);
+ }
+ if (spi_flash_probe_dev != RT_NULL)
+ {
+ sfud_dev = (sfud_flash_t)spi_flash_probe_dev->user_data;
+ }
+ }
+
+ if (sfud_dev == RT_NULL)
+ {
+ LOG_E("SFUD flash on SPI device %s not found.", SPI_FLASH_SPI_DEV_NAME);
+ spi_flash_detach_device();
+ return -RT_ERROR;
+ }
+
+ if (SPI_FLASH_TEST_SIZE <= 0)
+ {
+ LOG_E("SPI flash test size must be greater than 0.");
+ spi_flash_detach_device();
+ return -RT_ERROR;
+ }
+
+ erase_gran = sfud_dev->chip.erase_gran;
+ if (sfud_dev->chip.capacity == 0 || erase_gran == 0)
+ {
+ LOG_E("Invalid SPI flash geometry, capacity: %u, erase granularity: %u.",
+ sfud_dev->chip.capacity, erase_gran);
+ goto __error;
+ }
+
+ test_addr = SPI_FLASH_TEST_OFFSET;
+ if (test_addr >= sfud_dev->chip.capacity ||
+ SPI_FLASH_TEST_SIZE > sfud_dev->chip.capacity - test_addr)
+ {
+ LOG_E("SPI flash test range is out of bounds, offset: 0x%08x, size: %u, capacity: %u.",
+ test_addr, SPI_FLASH_TEST_SIZE, sfud_dev->chip.capacity);
+ goto __error;
+ }
+
+ backup_addr = spi_flash_align_down(test_addr, erase_gran);
+ backup_size = spi_flash_align_up(test_addr + SPI_FLASH_TEST_SIZE - backup_addr, erase_gran);
+ if (backup_size > sfud_dev->chip.capacity - backup_addr)
+ {
+ LOG_E("SPI flash backup range is out of bounds, offset: 0x%08x, size: %u, capacity: %u.",
+ backup_addr, backup_size, sfud_dev->chip.capacity);
+ goto __error;
+ }
+
+ backup_buf = (rt_uint8_t *)rt_malloc(backup_size);
+ write_buf = (rt_uint8_t *)rt_malloc(SPI_FLASH_TEST_SIZE);
+ read_buf = (rt_uint8_t *)rt_malloc(SPI_FLASH_TEST_SIZE);
+ if (backup_buf == RT_NULL || write_buf == RT_NULL || read_buf == RT_NULL)
+ {
+ LOG_E("Allocate SPI flash test buffer failed.");
+ goto __nomem;
+ }
+
+ if (sfud_read(sfud_dev, backup_addr, backup_size, backup_buf) != SFUD_SUCCESS)
+ {
+ LOG_E("Backup SPI flash data failed, addr: 0x%08x, size: %u.", backup_addr, backup_size);
+ goto __error;
+ }
+
+ LOG_I("SPI flash test uses %s on %s at offset 0x%08x, size %u.",
+ sfud_dev->name, SPI_FLASH_SPI_DEV_NAME, test_addr, SPI_FLASH_TEST_SIZE);
+
+ return RT_EOK;
+
+__nomem:
+ spi_flash_free_buffers();
+ spi_flash_delete_probe_dev();
+ spi_flash_detach_device();
+ return -RT_ENOMEM;
+
+__error:
+ spi_flash_free_buffers();
+ spi_flash_delete_probe_dev();
+ spi_flash_detach_device();
+ return -RT_ERROR;
+}
+
+static rt_err_t utest_tc_cleanup(void)
+{
+ rt_err_t result;
+
+ result = spi_flash_restore_backup();
+
+ spi_flash_free_buffers();
+ spi_flash_delete_probe_dev();
+ spi_flash_detach_device();
+
+ return result;
+}
+
+static void testcase(void)
+{
+ UTEST_UNIT_RUN(spi_flash_probe_info);
+ UTEST_UNIT_RUN(spi_flash_erase_write_read);
+}
+
+UTEST_TC_EXPORT(testcase, "components.drivers.spi.flash", utest_tc_init, utest_tc_cleanup, 30);
+
+#endif /* RT_UTEST_SPI_FLASH */
diff --git a/components/drivers/spi/utest/spi_loopback_tc.c b/components/drivers/spi/utest/spi_loopback_tc.c
new file mode 100644
index 0000000000..6f9b5505e2
--- /dev/null
+++ b/components/drivers/spi/utest/spi_loopback_tc.c
@@ -0,0 +1,452 @@
+/*
+ * Copyright (c) 2006-2026, RT-Thread Development Team
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Change Logs:
+ * Date Author Notes
+ * 2026-07-02 CYFS add SPI MOSI/MISO loopback utest
+ */
+
+#include <rtthread.h>
+#include <rtdevice.h>
+#include "utest.h"
+
+#ifdef RT_UTEST_SPI_LOOPBACK
+
+#ifndef RT_UTEST_SPI_LOOPBACK_MAX_HZ
+#define RT_UTEST_SPI_LOOPBACK_MAX_HZ 10000000
+#endif
+
+#define SPI_LOOPBACK_BUS_NAME RT_UTEST_SPI_LOOPBACK_BUS_NAME
+#define SPI_LOOPBACK_DEV_NAME RT_UTEST_SPI_LOOPBACK_DEV_NAME
+#define SPI_LOOPBACK_DATA_SIZE RT_UTEST_SPI_LOOPBACK_DATA_SIZE
+#define SPI_LOOPBACK_MAX_HZ RT_UTEST_SPI_LOOPBACK_MAX_HZ
+#define SPI_LOOPBACK_TIMEOUT RT_UTEST_SPI_LOOPBACK_TIMEOUT
+#define SPI_LOOPBACK_CLK_ENTRY(max_hz) ((SPI_LOOPBACK_MAX_HZ > (max_hz)) ? (max_hz) : 0)
+#define SPI_LOOPBACK_LEN_ENTRY(length) ((SPI_LOOPBACK_DATA_SIZE > (length)) ? (length) : 0)
+#define SPI_LOOPBACK_CLK_TABLE \
+{ \
+ SPI_LOOPBACK_CLK_ENTRY(100000), \
+ SPI_LOOPBACK_CLK_ENTRY(400000), \
+ SPI_LOOPBACK_CLK_ENTRY(1000000), \
+ SPI_LOOPBACK_CLK_ENTRY(2000000), \
+ SPI_LOOPBACK_CLK_ENTRY(5000000), \
+ SPI_LOOPBACK_CLK_ENTRY(10000000), \
+ SPI_LOOPBACK_CLK_ENTRY(50000000), \
+ SPI_LOOPBACK_MAX_HZ, \
+}
+#define SPI_LOOPBACK_LEN_TABLE \
+{ \
+ SPI_LOOPBACK_LEN_ENTRY(1), \
+ SPI_LOOPBACK_LEN_ENTRY(8), \
+ SPI_LOOPBACK_LEN_ENTRY(31), \
+ SPI_LOOPBACK_LEN_ENTRY(32), \
+ SPI_LOOPBACK_LEN_ENTRY(64), \
+ SPI_LOOPBACK_LEN_ENTRY(256), \
+ SPI_LOOPBACK_LEN_ENTRY(512), \
+ SPI_LOOPBACK_DATA_SIZE, \
+}
+
+static struct rt_spi_device spi_loopback_dev;
+static rt_bool_t spi_loopback_attached;
+static rt_uint8_t tx_buf[SPI_LOOPBACK_DATA_SIZE];
+static rt_uint8_t rx_buf[SPI_LOOPBACK_DATA_SIZE];
+static const rt_uint32_t spi_loopback_clk_table[] = SPI_LOOPBACK_CLK_TABLE;
+static const rt_size_t spi_loopback_len_table[] = SPI_LOOPBACK_LEN_TABLE;
+
+static void spi_loopback_fill_pattern(rt_uint8_t *buf, rt_size_t size, rt_uint8_t seed)
+{
+ rt_size_t i;
+
+ for (i = 0; i < size; i++)
+ {
+ buf[i] = (rt_uint8_t)(seed + i * 17 + (i >> 1));
+ }
+}
+
+static rt_err_t spi_loopback_configure(rt_uint32_t max_hz)
+{
+ rt_err_t result;
+ struct rt_spi_configuration cfg;
+
+ rt_memset(&cfg, 0, sizeof(cfg));
+ cfg.data_width = 8;
+ cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB | RT_SPI_NO_CS;
+ cfg.max_hz = max_hz;
+
+ spi_loopback_dev.config.usage_freq = 0;
+ result = rt_spi_configure(&spi_loopback_dev, &cfg);
+ if (result == -RT_EBUSY)
+ {
+ result = RT_EOK;
+ }
+
+ return result;
+}
+
+static rt_bool_t spi_loopback_check_length(rt_size_t length)
+{
+ if (length == 0)
+ {
+ return RT_FALSE;
+ }
+
+ if (length > sizeof(tx_buf))
+ {
+ return RT_FALSE;
+ }
+
+ return RT_TRUE;
+}
+
+static void spi_loopback_validate_length_table(void)
+{
+ rt_size_t i;
+ rt_bool_t valid = RT_TRUE;
+
+ for (i = 0; i < sizeof(spi_loopback_len_table) / sizeof(spi_loopback_len_table[0]); i++)
+ {
+ rt_size_t length = spi_loopback_len_table[i];
+
+ if (length == 0)
+ {
+ continue;
+ }
+
+ if (length > sizeof(tx_buf))
+ {
+ LOG_E("SPI loopback length matrix[%u] = %u exceeds data size %u.",
+ (unsigned int)i, (unsigned int)length, (unsigned int)sizeof(tx_buf));
+ valid = RT_FALSE;
+ continue;
+ }
+ }
+
+ uassert_true(valid);
+}
+
+static rt_bool_t spi_loopback_check_buffer(const rt_uint8_t *tx, const rt_uint8_t *rx,
+ rt_size_t length, const char *stage,
+ rt_size_t clk_index, rt_size_t len_index)
+{
+ rt_size_t i;
+
+ if (rt_memcmp(tx, rx, length) == 0)
+ {
+ uassert_buf_equal(tx, rx, length);
+ return RT_TRUE;
+ }
+
+ for (i = 0; i < length; i++)
+ {
+ if (tx[i] != rx[i])
+ {
+ LOG_E("SPI loopback %s mismatch: clk[%u], len[%u], offset %u, tx=0x%02x, rx=0x%02x.",
+ stage,
+ (unsigned int)clk_index,
+ (unsigned int)len_index,
+ (unsigned int)i,
+ tx[i],
+ rx[i]);
+ break;
+ }
+ }
+
+ uassert_buf_equal(tx, rx, length);
+ return RT_FALSE;
+}
+
+static rt_bool_t spi_loopback_transfer_full_duplex(rt_size_t length, rt_uint8_t seed,
+ rt_size_t clk_index, rt_size_t len_index)
+{
+ rt_ssize_t result;
+
+ if (!spi_loopback_check_length(length))
+ {
+ return RT_FALSE;
+ }
+
+ spi_loopback_fill_pattern(tx_buf, length, seed);
+ rt_memset(rx_buf, 0, length);
+
+ result = rt_spi_transfer(&spi_loopback_dev, tx_buf, rx_buf, length);
+ uassert_int_equal(result, (rt_ssize_t)length);
+ if (result != (rt_ssize_t)length)
+ {
+ return RT_FALSE;
+ }
+
+ return spi_loopback_check_buffer(tx_buf, rx_buf, length, "transfer", clk_index, len_index);
+}
+
+static rt_bool_t spi_loopback_sendrecv8(rt_uint8_t tx_data, rt_size_t clk_index)
+{
+ rt_uint8_t rx_data = 0;
+ rt_err_t result;
+
+ result = rt_spi_sendrecv8(&spi_loopback_dev, tx_data, &rx_data);
+ uassert_int_equal(result, RT_EOK);
+ if (result != RT_EOK)
+ {
+ return RT_FALSE;
+ }
+
+ if (rx_data != tx_data)
+ {
+ LOG_E("SPI loopback byte mismatch: clk[%u], tx=0x%02x, rx=0x%02x. Check the selected bus MOSI/MISO pins.",
+ (unsigned int)clk_index,
+ tx_data,
+ rx_data);
+ }
+ uassert_int_equal(rx_data, tx_data);
+
+ return (rx_data == tx_data) ? RT_TRUE : RT_FALSE;
+}
+
+static rt_bool_t spi_loopback_transfer_message(rt_size_t length, rt_uint8_t seed,
+ rt_size_t clk_index, rt_size_t len_index)
+{
+ struct rt_spi_message msg1;
+ struct rt_spi_message msg2;
+ struct rt_spi_message *result;
+ rt_size_t first_len;
+
+ if (!spi_loopback_check_length(length))
+ {
+ return RT_FALSE;
+ }
+
+ if (length < 2)
+ {
+ return RT_TRUE;
+ }
+
+ first_len = length / 2;
+ spi_loopback_fill_pattern(tx_buf, length, seed);
+ rt_memset(rx_buf, 0, length);
+ rt_memset(&msg1, 0, sizeof(msg1));
+ rt_memset(&msg2, 0, sizeof(msg2));
+
+ msg1.send_buf = tx_buf;
+ msg1.recv_buf = rx_buf;
+ msg1.length = first_len;
+ msg1.cs_take = 1;
+ msg1.cs_release = 0;
+ msg1.next = &msg2;
+
+ msg2.send_buf = tx_buf + first_len;
+ msg2.recv_buf = rx_buf + first_len;
+ msg2.length = length - first_len;
+ msg2.cs_take = 0;
+ msg2.cs_release = 1;
+ msg2.next = RT_NULL;
+
+ result = rt_spi_transfer_message(&spi_loopback_dev, &msg1);
+ uassert_null(result);
+ if (result != RT_NULL)
+ {
+ return RT_FALSE;
+ }
+
+ return spi_loopback_check_buffer(tx_buf, rx_buf, length, "message", clk_index, len_index);
+}
+
+static rt_bool_t spi_loopback_data_length_matrix(rt_size_t clk_index)
+{
+ rt_size_t i;
+ rt_size_t tested = 0;
+ rt_bool_t passed = RT_TRUE;
+
+ for (i = 0; i < sizeof(spi_loopback_len_table) / sizeof(spi_loopback_len_table[0]); i++)
+ {
+ rt_size_t length = spi_loopback_len_table[i];
+
+ if (length == 0)
+ {
+ continue;
+ }
+
+ if (!spi_loopback_check_length(length))
+ {
+ continue;
+ }
+
+ tested++;
+ LOG_I("SPI loopback data matrix: clk[%u], len[%u] = %u bytes.",
+ (unsigned int)clk_index, (unsigned int)i, (unsigned int)length);
+
+ if (!spi_loopback_transfer_full_duplex(length, (rt_uint8_t)(0x35 + clk_index + i), clk_index, i))
+ {
+ passed = RT_FALSE;
+ break;
+ }
+
+ if (!spi_loopback_transfer_message(length, (rt_uint8_t)(0x5A + clk_index + i), clk_index, i))
+ {
+ passed = RT_FALSE;
+ break;
+ }
+ }
+
+ uassert_true(tested > 0);
+ return passed;
+}
+
+#ifdef RT_UTEST_SPI_LOOPBACK_API_SMOKE
+static void spi_loopback_transfer_tx_only_smoke(rt_size_t length, rt_uint8_t seed)
+{
+ rt_ssize_t result;
+
+ if (!spi_loopback_check_length(length))
+ {
+ return;
+ }
+
+ spi_loopback_fill_pattern(tx_buf, length, seed);
+
+ result = rt_spi_transfer(&spi_loopback_dev, tx_buf, RT_NULL, length);
+ uassert_int_equal(result, (rt_ssize_t)length);
+}
+
+static void spi_loopback_transfer_rx_only_smoke(rt_size_t length)
+{
+ rt_ssize_t result;
+
+ if (!spi_loopback_check_length(length))
+ {
+ return;
+ }
+
+ rt_memset(rx_buf, 0, length);
+
+ result = rt_spi_transfer(&spi_loopback_dev, RT_NULL, rx_buf, length);
+ uassert_int_equal(result, (rt_ssize_t)length);
+}
+
+static void spi_loopback_api_smoke_length_matrix(rt_size_t clk_index)
+{
+ rt_size_t i;
+ rt_size_t tested = 0;
+
+ for (i = 0; i < sizeof(spi_loopback_len_table) / sizeof(spi_loopback_len_table[0]); i++)
+ {
+ rt_size_t length = spi_loopback_len_table[i];
+
+ if (!spi_loopback_check_length(length))
+ {
+ continue;
+ }
+
+ tested++;
+ LOG_I("SPI transfer API smoke: clk[%u], len[%u] = %u bytes, tx-only/rx-only.",
+ (unsigned int)clk_index, (unsigned int)i, (unsigned int)length);
+
+ spi_loopback_transfer_tx_only_smoke(length, (rt_uint8_t)(0xC3 + clk_index + i));
+ spi_loopback_transfer_rx_only_smoke(length);
+ }
+
+ uassert_true(tested > 0);
+}
+#endif /* RT_UTEST_SPI_LOOPBACK_API_SMOKE */
+
+static void spi_loopback_clock_matrix(void)
+{
+ rt_size_t i;
+ rt_size_t tested = 0;
+
+ spi_loopback_validate_length_table();
+
+ for (i = 0; i < sizeof(spi_loopback_clk_table) / sizeof(spi_loopback_clk_table[0]); i++)
+ {
+ rt_err_t result;
+ rt_uint32_t request_hz = spi_loopback_clk_table[i];
+ rt_uint32_t actual_hz;
+
+ if (request_hz == 0)
+ {
+ continue;
+ }
+
+ tested++;
+ LOG_I("SPI loopback clock matrix[%u]: request %u Hz.", (unsigned int)i, request_hz);
+
+ result = spi_loopback_configure(request_hz);
+ uassert_int_equal(result, RT_EOK);
+ if (result != RT_EOK)
+ {
+ continue;
+ }
+
+ actual_hz = spi_loopback_dev.config.usage_freq;
+ if (actual_hz != 0)
+ {
+ LOG_I("SPI loopback clock matrix[%u]: actual %u Hz.", (unsigned int)i, actual_hz);
+ }
+
+ if (!spi_loopback_sendrecv8((rt_uint8_t)(0xA5 ^ i), i))
+ {
+ LOG_E("Skip longer transfers for clock[%u] because the byte loopback check failed.",
+ (unsigned int)i);
+ continue;
+ }
+
+#ifdef RT_UTEST_SPI_LOOPBACK_API_SMOKE
+ if (spi_loopback_data_length_matrix(i))
+ {
+ spi_loopback_api_smoke_length_matrix(i);
+ }
+#else
+ (void)spi_loopback_data_length_matrix(i);
+#endif
+ }
+
+ uassert_true(tested > 0);
+}
+
+static rt_err_t utest_tc_init(void)
+{
+ rt_device_t bus;
+
+ LOG_I("SPI loopback test: connect MOSI and MISO on bus %s.", SPI_LOOPBACK_BUS_NAME);
+ LOG_I("SPI loopback test: use the selected bus pinmux, not another SPI header.");
+ LOG_I("SPI loopback test: max clock %u Hz, max data %u bytes.",
+ (unsigned int)SPI_LOOPBACK_MAX_HZ, (unsigned int)SPI_LOOPBACK_DATA_SIZE);
+
+ bus = rt_device_find(SPI_LOOPBACK_BUS_NAME);
+ if (bus == RT_NULL || bus->type != RT_Device_Class_SPIBUS)
+ {
+ LOG_E("SPI bus %s not found.", SPI_LOOPBACK_BUS_NAME);
+ return -RT_ERROR;
+ }
+
+ rt_memset(&spi_loopback_dev, 0, sizeof(spi_loopback_dev));
+ if (rt_spi_bus_attach_device(&spi_loopback_dev, SPI_LOOPBACK_DEV_NAME, SPI_LOOPBACK_BUS_NAME, RT_NULL) != RT_EOK)
+ {
+ LOG_E("Attach SPI loopback device %s failed.", SPI_LOOPBACK_DEV_NAME);
+ return -RT_ERROR;
+ }
+ spi_loopback_attached = RT_TRUE;
+
+ return RT_EOK;
+}
+
+static rt_err_t utest_tc_cleanup(void)
+{
+ if (spi_loopback_attached)
+ {
+ rt_spi_bus_detach_device(&spi_loopback_dev);
+ spi_loopback_attached = RT_FALSE;
+ }
+
+ return RT_EOK;
+}
+
+static void testcase(void)
+{
+ UTEST_UNIT_RUN(spi_loopback_clock_matrix);
+}
+
+UTEST_TC_EXPORT(testcase, "components.drivers.spi.loopback", utest_tc_init, utest_tc_cleanup, SPI_LOOPBACK_TIMEOUT);
+
+#endif /* RT_UTEST_SPI_LOOPBACK */
diff --git a/components/utilities/utest/Kconfig b/components/utilities/utest/Kconfig
index 742ec521e2..af23e9e789 100644
--- a/components/utilities/utest/Kconfig
+++ b/components/utilities/utest/Kconfig
@@ -19,6 +19,7 @@ menu "RT-Thread Utestcases"
rsource "../../../components/drivers/pin/utest/Kconfig"
rsource "../../../components/drivers/serial/utest/Kconfig"
rsource "../../../components/drivers/smp_call/utest/Kconfig"
+ rsource "../../../components/drivers/spi/utest/Kconfig"
endmenu
rsource "../../../components/dfs/utest/Kconfig"