Add test cases for sf commands to verify various SPI flash operations
such as erase, write and read. It also adds qspi lock unlock cases.
This test relies on boardenv_* configurations to run it for different
SPI flash family such as single SPI, QSPI, and OSPI.

Signed-off-by: Love Kumar <love.ku...@amd.com>
---
 test/py/tests/test_spi.py | 626 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 626 insertions(+)
 create mode 100644 test/py/tests/test_spi.py

diff --git a/test/py/tests/test_spi.py b/test/py/tests/test_spi.py
new file mode 100644
index 000000000000..a0b5c075b6ce
--- /dev/null
+++ b/test/py/tests/test_spi.py
@@ -0,0 +1,626 @@
+# SPDX-License-Identifier: GPL-2.0
+# (C) Copyright 2023, Advanced Micro Devices, Inc.
+
+import pytest
+import random
+import re
+import u_boot_utils
+
+"""
+Note: This test relies on boardenv_* containing configuration values to define
+spi minimum and maximum frequnecies at which the flash part can operate on and
+these tests run at 5 different spi frequnecy randomised values in the range.
+It also defines the SPI bus number containing the SPI-flash chip, SPI
+chip-select, SPI mode, SPI flash part name and timeout parameters. If minimum
+and maximum frequency is not defined, it will run on freq 0 by default.
+
+Without the boardenv_* configuration, this test will be automatically skipped.
+
+It also relies on configuration values for supported flashes for qspi lock and
+unlock cases. It will run qspi lock-unlock cases only for the supported flash
+parts.
+
+Example:
+env__spi_device_test = {
+    'bus': 0,
+    'chip_select': 0,
+    'min_freq': 10000000,
+    'max_freq': 100000000,
+    'mode': 0,
+    'part_name': 'n25q00a',
+    'timeout': 100000,
+}
+
+env__qspi_lock_unlock = {
+    'supported_flash': 'mt25qu512a, n25q00a, n25q512ax3',
+}
+"""
+
+def setup_spi(u_boot_console):
+    f = u_boot_console.config.env.get('env__spi_device_test', None)
+    if not f:
+        pytest.skip('No env file to read for SPI family device test')
+
+    bus = f.get('bus', 0)
+    cs = f.get('chip_select', 0)
+    mode = f.get('mode', 0)
+    part_name = f.get('part_name', None)
+    timeout = f.get('timeout', None)
+
+    if not part_name:
+        pytest.skip('No env file to read SPI family flash part name')
+
+    return bus, cs, mode, part_name, timeout
+
+# Find out minimum and maximum frequnecies that SPI device can operate
+def spi_find_freq_range(u_boot_console):
+    f = u_boot_console.config.env.get('env__spi_device_test', None)
+    if not f:
+        pytest.skip('No env file to read for SPI family device test')
+
+    min_f = f.get('min_freq', None)
+    max_f = f.get('max_freq', None)
+
+    if not min_f:
+        min_f = 0
+    if not max_f:
+        max_f = 0
+    if max_f < min_f:
+        max_f = min_f
+
+    if min_f == 0 and max_f == 0:
+        iterations = 1
+    else:
+        iterations = 5
+
+    return min_f, max_f, iterations
+
+# Find out SPI family flash memory parameters
+def spi_pre_commands(u_boot_console, freq):
+    bus, cs, mode, part_name, timeout = setup_spi(u_boot_console)
+
+    output = u_boot_console.run_command(f'sf probe {bus}:{cs} {freq} {mode}')
+    if not 'SF: Detected' in output:
+        pytest.skip('No SPI device available')
+
+    if not part_name in output:
+        pytest.fail('SPI flash part name not recognized')
+
+    m = re.search('page size (.+?) Bytes', output)
+    if m:
+        try:
+            page_size = int(m.group(1))
+        except ValueError:
+            pytest.fail('SPI page size not recognized')
+
+    m = re.search('erase size (.+?) KiB', output)
+    if m:
+        try:
+            erase_size = int(m.group(1))
+        except ValueError:
+            pytest.fail('SPI erase size not recognized')
+
+        erase_size *= 1024
+
+    m = re.search('total (.+?) MiB', output)
+    if m:
+        try:
+            global total_size
+            total_size = int(m.group(1))
+        except ValueError:
+            pytest.fail('SPI total size not recognized')
+
+        total_size *= 1024 * 1024
+
+    m = re.search('Detected (.+?) with', output)
+    if m:
+        try:
+            flash_part = m.group(1)
+            assert flash_part == part_name
+        except:
+            pytest.fail('SPI flash part not recognized')
+
+    return page_size, erase_size, total_size, flash_part, timeout
+
+# Read the whole SPI flash twice, random_size till full flash size, random
+# till page size
+def spi_read_twice(u_boot_console, page_size, total_size, timeout):
+    expected_read = 'Read: OK'
+
+    for size in random.randint(4, page_size), random.randint(4, total_size), 
total_size:
+        addr = u_boot_utils.find_ram_base(u_boot_console)
+        size = size & ~3
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf read %x 0 %x' % (addr + total_size, size)
+            )
+            assert expected_read in output
+        output = u_boot_console.run_command('crc32 %x %x' % (addr + 
total_size, size))
+        m = re.search('==> (.+?)$', output)
+        if not m:
+            pytest.fail('CRC32 failed')
+        expected_crc32 = m.group(1)
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf read %x 0 %x' % (addr + total_size + 10, size)
+            )
+            assert expected_read in output
+        output = u_boot_console.run_command(
+            'crc32 %x %x' % (addr + total_size + 10, size)
+        )
+        assert expected_crc32 in output
+
+@pytest.mark.buildconfigspec('cmd_sf')
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_memory')
+def test_spi_read_twice(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+        spi_read_twice(u_boot_console, page_size, total_size, timeout)
+        i = i + 1
+
+# This test check crossing boundary for dual/parallel configurations
+def spi_erase_block(u_boot_console, erase_size, total_size):
+    expected_erase = 'Erased: OK'
+    for start in range(0, total_size, erase_size):
+        output = u_boot_console.run_command('sf erase %x %x' % (start, 
erase_size))
+        assert expected_erase in output
+
+@pytest.mark.buildconfigspec('cmd_sf')
+def test_spi_erase_block(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+        spi_erase_block(u_boot_console, erase_size, total_size)
+        i = i + 1
+
+# Random write till page size, random till size and full size
+def spi_write_twice(u_boot_console, page_size, erase_size, total_size, 
timeout):
+    addr = u_boot_utils.find_ram_base(u_boot_console)
+    expected_write = 'Written: OK'
+    expected_read = 'Read: OK'
+    expected_erase = 'Erased: OK'
+
+    old_size = 0
+    for size in (
+        random.randint(4, page_size),
+        random.randint(page_size, total_size),
+        total_size,
+    ):
+        offset = random.randint(4, page_size)
+        offset = offset & ~3
+        size = size & ~3
+        size = size - old_size
+        output = u_boot_console.run_command('crc32 %x %x' % (addr + 
total_size, size))
+        m = re.search('==> (.+?)$', output)
+        if not m:
+            pytest.fail('CRC32 failed')
+
+        expected_crc32 = m.group(1)
+        if old_size % page_size:
+            old_size = int(old_size / page_size)
+            old_size *= page_size
+
+        if size % erase_size:
+            erasesize = int(size / erase_size + 1)
+            erasesize *= erase_size
+
+        eraseoffset = int(old_size / erase_size)
+        eraseoffset *= erase_size
+
+        timeout = 100000000
+        start = 0
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf erase %x %x' % (eraseoffset, erasesize)
+            )
+            assert expected_erase in output
+
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf write %x %x %x' % (addr + total_size, old_size, size)
+            )
+            assert expected_write in output
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf read %x %x %x' % (addr + total_size + offset, old_size, 
size)
+            )
+            assert expected_read in output
+        output = u_boot_console.run_command(
+            'crc32 %x %x' % (addr + total_size + offset, size)
+        )
+        assert expected_crc32 in output
+        old_size = size
+
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_sf')
+@pytest.mark.buildconfigspec('cmd_memory')
+def test_spi_write_twice(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+        spi_write_twice(u_boot_console, page_size, erase_size, total_size, 
timeout)
+        i = i + 1
+
+def spi_write_continues(u_boot_console, page_size, total_size, timeout):
+    spi_erase_block(u_boot_console)
+    expected_write = 'Written: OK'
+    expected_read = 'Read: OK'
+    addr = u_boot_utils.find_ram_base(u_boot_console)
+
+    output = u_boot_console.run_command('crc32 %x %x' % (addr + 0x10000, 
total_size))
+    m = re.search('==> (.+?)$', output)
+    if not m:
+        pytest.fail('CRC32 failed')
+    expected_crc32 = m.group(1)
+
+    old_size = 0
+    for size in (
+        random.randint(4, page_size),
+        random.randint(page_size, total_size),
+        total_size,
+    ):
+        size = size & ~3
+        size = size - old_size
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf write %x %x %x' % (addr + 0x10000 + old_size, old_size, 
size)
+            )
+            assert expected_write in output
+        old_size += size
+
+    with u_boot_console.temporary_timeout(timeout):
+        output = u_boot_console.run_command(
+            'sf read %x %x %x' % (addr + 0x10000 + total_size, 0, total_size)
+        )
+        assert expected_read in output
+
+    output = u_boot_console.run_command(
+        'crc32 %x %x' % (addr + 0x10000 + total_size, total_size)
+    )
+    assert expected_crc32 in output
+
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_sf')
+@pytest.mark.buildconfigspec('cmd_memory')
+def test_spi_write_continues(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+        spi_write_continues(u_boot_console, page_size, total_size, timeout)
+        i = i + 1
+
+def spi_erase_all(u_boot_console, total_size, timeout):
+    expected_erase = 'Erased: OK'
+    start = 0
+    with u_boot_console.temporary_timeout(timeout):
+        output = u_boot_console.run_command('sf erase 0 ' + 
str(hex(total_size)))
+        assert expected_erase in output
+
+@pytest.mark.buildconfigspec('cmd_sf')
+def test_spi_erase_all(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+        spi_erase_all(u_boot_console, total_size, timeout)
+        i = i + 1
+
+# Flash operations: erase/write/read
+def flash_ops(
+    u_boot_console, ops, start, size, offset=0, exp_ret=0, exp_str='', 
not_exp_str=''
+):
+
+    f = u_boot_console.config.env.get('env__spi_device_test', None)
+    if not f:
+        timeout = 1000000
+
+    timeout = f.get('timeout', 1000000)
+
+    if ops == 'erase':
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command('sf erase %x %x' % (start, 
size))
+    else:
+        with u_boot_console.temporary_timeout(timeout):
+            output = u_boot_console.run_command(
+                'sf %s %x %x %x' % (ops, offset, start, size)
+            )
+
+    if exp_str:
+        assert exp_str in output
+    if not_exp_str:
+        assert not_exp_str not in output
+
+    ret_code = u_boot_console.run_command('echo $?')
+    if exp_ret >= 0:
+        assert ret_code.endswith(str(exp_ret))
+
+    return output, ret_code
+
+# Unlock the flash before making it fail
+def qspi_unlock_exit(u_boot_console, addr, size):
+    u_boot_console.run_command('sf protect unlock %x %x' % (addr, size))
+    assert False, 'FAIL: Flash lock is unable to protect the data!'
+
+# QSPI lock-unlock operations
+def qspi_lock_unlock(u_boot_console, lock_addr, lock_size):
+    addr = u_boot_utils.find_ram_base(u_boot_console)
+    expected_erase = 'Erased: OK'
+    expected_write = 'Written: OK'
+    expected_erase_errors = [
+        'Erase operation failed',
+        'Attempted to modify a protected sector',
+        'Erased: ERROR',
+        'is protected and cannot be erased',
+        'ERROR: flash area is locked',
+    ]
+    expected_write_errors = [
+        'ERROR: flash area is locked',
+        'Program operation failed',
+        'Attempted to modify a protected sector',
+        'Written: ERROR',
+    ]
+
+    # Find the protected/un-protected region
+    if lock_addr < (total_size // 2):
+        sect_num = (lock_addr + lock_size) // erase_size
+        x = 1
+        while x < sect_num:
+            x *= 2
+        prot_start = 0
+        prot_size = x * erase_size
+        unprot_start = prot_start + prot_size
+        unprot_size = total_size - unprot_start
+    else:
+        sect_num = (total_size - lock_addr) // erase_size
+        x = 1
+        while x < sect_num:
+            x *= 2
+        prot_start = total_size - (x * erase_size)
+        prot_size = total_size - prot_start
+        unprot_start = 0
+        unprot_size = prot_start
+
+    # Check erase/write operation before locking
+    flash_ops(u_boot_console, 'erase', prot_start, prot_size, 0, 0, 
expected_erase)
+    flash_ops(u_boot_console, 'write', prot_start, prot_size, addr, 0, 
expected_write)
+
+    # Locking the flash
+    u_boot_console.run_command('sf protect lock %x %x' % (lock_addr, 
lock_size))
+    output = u_boot_console.run_command('echo $?')
+    assert output.endswith('0')
+
+    # Check erase/write operation after locking
+    output, ret_code = flash_ops(u_boot_console, 'erase', prot_start, 
prot_size, 0, -1)
+    if not any(error in output for error in expected_erase_errors) or 
ret_code.endswith(
+        '0'
+    ):
+        qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+    output, ret_code = flash_ops(
+        u_boot_console, 'write', prot_start, prot_size, addr, -1
+    )
+    if not any(error in output for error in expected_write_errors) or 
ret_code.endswith(
+        '0'
+    ):
+        qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+    # Check locked sectors
+    sect_lock_start = random.randrange(prot_start, (prot_start + prot_size), 
erase_size)
+    if prot_size > erase_size:
+        sect_lock_size = random.randrange(
+            erase_size, (prot_start + prot_size - sect_lock_start), erase_size
+        )
+    else:
+        sect_lock_size = erase_size
+    sect_write_size = random.randint(1, sect_lock_size)
+
+    output, ret_code = flash_ops(
+        u_boot_console, 'erase', sect_lock_start, sect_lock_size, 0, -1
+    )
+    if not any(error in output for error in expected_erase_errors) or 
ret_code.endswith(
+        '0'
+    ):
+        qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+    output, ret_code = flash_ops(
+        u_boot_console, 'write', sect_lock_start, sect_write_size, addr, -1
+    )
+    if not any(error in output for error in expected_write_errors) or 
ret_code.endswith(
+        '0'
+    ):
+        qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+    # Check unlocked sectors
+    if unprot_size != 0:
+        sect_unlock_start = random.randrange(
+            unprot_start, (unprot_start + unprot_size), erase_size
+        )
+        if unprot_size > erase_size:
+            sect_unlock_size = random.randrange(
+                erase_size, (unprot_start + unprot_size - sect_unlock_start), 
erase_size
+            )
+        else:
+            sect_unlock_size = erase_size
+        sect_write_size = random.randint(1, sect_unlock_size)
+
+        output, ret_code = flash_ops(
+            u_boot_console, 'erase', sect_unlock_start, sect_unlock_size, 0, -1
+        )
+        if expected_erase not in output or ret_code.endswith('1'):
+            qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+        output, ret_code = flash_ops(
+            u_boot_console, 'write', sect_unlock_start, sect_write_size, addr, 
-1
+        )
+        if expected_write not in output or ret_code.endswith('1'):
+            qspi_unlock_exit(u_boot_console, lock_addr, lock_size)
+
+    # Unlocking the flash
+    u_boot_console.run_command('sf protect unlock %x %x' % (lock_addr, 
lock_size))
+    output = u_boot_console.run_command('echo $?')
+    assert output.endswith('0')
+
+    # Check erase/write operation after un-locking
+    flash_ops(u_boot_console, 'erase', prot_start, prot_size, 0, 0, 
expected_erase)
+    flash_ops(u_boot_console, 'write', prot_start, prot_size, addr, 0, 
expected_write)
+
+    # Check previous locked sectors
+    sect_lock_start = random.randrange(prot_start, (prot_start + prot_size), 
erase_size)
+    if prot_size > erase_size:
+        sect_lock_size = random.randrange(
+            erase_size, (prot_start + prot_size - sect_lock_start), erase_size
+        )
+    else:
+        sect_lock_size = erase_size
+    sect_write_size = random.randint(1, sect_lock_size)
+
+    flash_ops(
+        u_boot_console, 'erase', sect_lock_start, sect_lock_size, 0, 0, 
expected_erase
+    )
+    flash_ops(
+        u_boot_console,
+        'write',
+        sect_lock_start,
+        sect_write_size,
+        addr,
+        0,
+        expected_write,
+    )
+
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_sf')
+@pytest.mark.buildconfigspec('cmd_memory')
+def test_qspi_lock_unlock(u_boot_console):
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    flashes = u_boot_console.config.env.get('env__qspi_lock_unlock', False)
+    if not flashes:
+        pytest.skip('No supported flash list for lock/unlock provided')
+
+    i = 0
+    while i < loop:
+        page_size, erase_size, total_size, flash_part, timeout = 
spi_pre_commands(
+                u_boot_console, random.randint(min_f, max_f))
+
+        flashes_list = flashes.get('supported_flash', None).split(',')
+        flashes_list = [x.strip() for x in flashes_list]
+        if flash_part not in flashes_list:
+            pytest.skip('Detected flash does not support lock/unlock')
+
+        # For lower half of memory
+        lock_addr = random.randint(0, (total_size // 2) - 1)
+        lock_size = random.randint(1, ((total_size // 2) - lock_addr))
+        qspi_lock_unlock(u_boot_console, lock_addr, lock_size)
+
+        # For upper half of memory
+        lock_addr = random.randint((total_size // 2), total_size - 1)
+        lock_size = random.randint(1, (total_size - lock_addr))
+        qspi_lock_unlock(u_boot_console, lock_addr, lock_size)
+
+        # For entire flash
+        lock_addr = random.randint(0, total_size - 1)
+        lock_size = random.randint(1, (total_size - lock_addr))
+        qspi_lock_unlock(u_boot_console, lock_addr, lock_size)
+
+        i = i + 1
+
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_sf')
+@pytest.mark.buildconfigspec('cmd_memory')
+def test_spi_negative(u_boot_console):
+    expected_erase = 'Erased: OK'
+    expected_write = 'Written: OK'
+    expected_read = 'Read: OK'
+    min_f, max_f, loop = spi_find_freq_range(u_boot_console)
+    page_size, erase_size, total_size, flash_part, timeout = spi_pre_commands(
+            u_boot_console, random.randint(min_f, max_f))
+    addr = u_boot_utils.find_ram_base(u_boot_console)
+    i = 0
+    while i < loop:
+        # Erase negative test
+        start = random.randint(0, total_size)
+        esize = erase_size
+
+        # If erasesize is not multiple of flash's erase size
+        while esize % erase_size == 0:
+            esize = random.randint(0, total_size - start)
+
+        error_msg = 'Erased: ERROR'
+        flash_ops(
+            u_boot_console, 'erase', start, esize, 0, 1, error_msg, 
expected_erase
+        )
+
+        # If eraseoffset exceeds beyond flash size
+        eoffset = random.randint(total_size, (total_size + int(0x1000000)))
+        error_msg = 'Offset exceeds device limit'
+        flash_ops(
+            u_boot_console, 'erase', eoffset, esize, 0, 1, error_msg, 
expected_erase
+        )
+
+        # If erasesize exceeds beyond flash size
+        esize = random.randint((total_size - start), (total_size + 
int(0x1000000)))
+        error_msg = 'ERROR: attempting erase past flash size'
+        flash_ops(
+            u_boot_console, 'erase', start, esize, 0, 1, error_msg, 
expected_erase
+        )
+
+        # If erase size is 0
+        esize = 0
+        error_msg = 'ERROR: Invalid size 0'
+        flash_ops(
+            u_boot_console, 'erase', start, esize, 0, 1, error_msg, 
expected_erase
+        )
+
+        # If erasesize is less than flash's page size
+        esize = random.randint(0, page_size)
+        start = random.randint(0, (total_size - page_size))
+        error_msg = 'Erased: ERROR'
+        flash_ops(
+            u_boot_console, 'erase', start, esize, 0, 1, error_msg, 
expected_erase
+        )
+
+        # Write/Read negative test
+        # if Write/Read size exceeds beyond flash size
+        offset = random.randint(0, total_size)
+        size = random.randint((total_size - offset), (total_size + 
int(0x1000000)))
+        error_msg = 'Size exceeds partition or device limit'
+        flash_ops(
+            u_boot_console, 'write', offset, size, addr, 1, error_msg, 
expected_write
+        )
+        flash_ops(
+            u_boot_console, 'read', offset, size, addr, 1, error_msg, 
expected_read
+        )
+
+        # if Write/Read offset exceeds beyond flash size
+        offset = random.randint(total_size, (total_size + int(0x1000000)))
+        size = random.randint(0, total_size)
+        error_msg = 'Offset exceeds device limit'
+        flash_ops(
+            u_boot_console, 'write', offset, size, addr, 1, error_msg, 
expected_write
+        )
+        flash_ops(
+            u_boot_console, 'read', offset, size, addr, 1, error_msg, 
expected_read
+        )
+
+        # if Write/Read size is 0
+        offset = random.randint(0, 2)
+        size = 0
+        error_msg = 'ERROR: Invalid size 0'
+        flash_ops(
+            u_boot_console, 'write', offset, size, addr, 1, error_msg, 
expected_write
+        )
+        flash_ops(
+            u_boot_console, 'read', offset, size, addr, 1, error_msg, 
expected_read
+        )
+
+        i = i + 1
-- 
2.25.1

Reply via email to