mirror of https://github.com/wolfSSL/wolfBoot.git
420 lines
13 KiB
C
420 lines
13 KiB
C
/* unit-ls1028a-xspi-write.c
|
|
*
|
|
* Regression test for F-7977: hal_flash_write() and ext_flash_write() in
|
|
* hal/nxp_ls1028a.c issue xspi_write_en() once before xspi_flash_write(),
|
|
* but xspi_flash_write() issues one Page Program command per 256-byte
|
|
* chunk when len > XSPI_IP_BUF_SIZE. NOR flash clears its
|
|
* write-enable latch after each program operation, so with a single WEN
|
|
* only the first page of the request is actually programmed; the
|
|
* remaining Page Programs are rejected by the flash while the wrapper
|
|
* still reports success.
|
|
*
|
|
* The fix issues Write Enable for every page inside xspi_flash_write(),
|
|
* after the prior program has completed and before the Page Program
|
|
* command.
|
|
*
|
|
* The LS1028A XSPI driver is plain C register access apart from a few
|
|
* bare `asm volatile("...")` barrier statements (DDR init, delay), so
|
|
* the test includes the HAL file via nxp_ls1028a_host.c (generated by
|
|
* the Makefile, identical except those statements are blanked out),
|
|
* rebases XSPI_BASE onto host memory, and emulates the FlexSPI
|
|
* controller at the XSPI_IPCMD_START() boundary. The emulated NOR keeps
|
|
* a write-enable latch exactly like real NOR: set by WEN, consumed by
|
|
* each Page Program / Sector Erase, and a program or erase attempted
|
|
* without WEN is rejected.
|
|
*
|
|
* Copyright (C) 2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfBoot.
|
|
*
|
|
* wolfBoot is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfBoot is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
|
*/
|
|
|
|
#include <check.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#define TARGET_nxp_ls1028a
|
|
#define EXT_FLASH
|
|
/* The HAL checks for the aarch64 build (ARCH_AARCH64 also selects the
|
|
* 64-bit hal.h prototypes); the DTS load address is a board setting the
|
|
* host test does not use. */
|
|
#define ARCH_AARCH64
|
|
#define WOLFBOOT_LOAD_DTS_ADDRESS 0
|
|
|
|
#include "nxp_ls1028a.h"
|
|
|
|
/* Rebase the FlexSPI register block onto host memory. */
|
|
static uint32_t g_xspi_regs[0x1000 / sizeof(uint32_t)];
|
|
|
|
#undef XSPI_BASE
|
|
#define XSPI_BASE ((uintptr_t)g_xspi_regs)
|
|
|
|
/* Command log (the sequence the driver asks the controller to run). */
|
|
#define XSPI_LOG_MAX 64
|
|
enum {
|
|
XSPI_CMD_WEN,
|
|
XSPI_CMD_PP,
|
|
XSPI_CMD_SE,
|
|
XSPI_CMD_RDSR,
|
|
};
|
|
static struct {
|
|
int cmd;
|
|
uint32_t addr;
|
|
uint32_t len;
|
|
int rejected;
|
|
} g_xspi_log[XSPI_LOG_MAX];
|
|
static int g_xspi_log_n;
|
|
|
|
/* Emulated NOR flash plus the write-enable latch real NOR keeps. */
|
|
static uint8_t g_nor[2 * 128 * 1024];
|
|
static int g_wel;
|
|
/* RDSR polls remaining before the emulated device reports WIP clear.
|
|
* A program or erase arms it; Write Enable and program/erase issued
|
|
* while it is armed are ignored, exactly as the device does. */
|
|
#define NOR_BUSY_POLLS 2
|
|
static int g_busy;
|
|
|
|
/* The FlexSPI drains the TX FIFO while the command runs, so the flash
|
|
* receives the bytes in the order the driver hands them to the FIFO.
|
|
* xspi_emu_tfd_write() (called in place of the xspi_writereg() body in
|
|
* the generated host copy) captures that stream. */
|
|
static uint8_t g_tfd_stream[XSPI_IP_BUF_SIZE];
|
|
static uint32_t g_tfd_stream_len;
|
|
|
|
static void xspi_emu_tfd_write(uint32_t *addr, uint32_t val)
|
|
{
|
|
uint32_t i;
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
if (g_tfd_stream_len < sizeof(g_tfd_stream))
|
|
g_tfd_stream[g_tfd_stream_len++] = (uint8_t)(val >> (8 * i));
|
|
}
|
|
*(volatile uint32_t *)(addr) = val;
|
|
}
|
|
|
|
static void xspi_emu_start(void)
|
|
{
|
|
uint32_t ipcr1 = XSPI_IPCR1;
|
|
uint32_t seq = ipcr1 >> 16;
|
|
uint32_t len = ipcr1 & 0xFFFF;
|
|
uint32_t addr = XSPI_IPCR0;
|
|
uint32_t i;
|
|
int e;
|
|
|
|
/* Status polls are not operations: answer them without taking a
|
|
* log slot, so the assertions below stay about WEN/PP/SE order. */
|
|
if (seq == LUT_INDEX_RDSR) {
|
|
/* WIP stays set for a few polls after a program or erase */
|
|
XSPI_RFD(0) = g_busy ? FLASH_SR_WIP_MSK : 0;
|
|
if (g_busy)
|
|
g_busy--;
|
|
XSPI_INTR |= XSPI_IPCMDDONE;
|
|
g_tfd_stream_len = 0;
|
|
return;
|
|
}
|
|
|
|
e = g_xspi_log_n < XSPI_LOG_MAX ? g_xspi_log_n : XSPI_LOG_MAX - 1;
|
|
if (g_xspi_log_n < XSPI_LOG_MAX)
|
|
g_xspi_log_n++;
|
|
g_xspi_log[e].addr = addr;
|
|
g_xspi_log[e].len = len;
|
|
g_xspi_log[e].rejected = 0;
|
|
|
|
if (seq == LUT_INDEX_WRITE_EN) {
|
|
g_xspi_log[e].cmd = XSPI_CMD_WEN;
|
|
if (g_busy)
|
|
g_xspi_log[e].rejected = 1; /* WREN ignored while WIP set */
|
|
else
|
|
g_wel = 1;
|
|
}
|
|
else if (seq == LUT_INDEX_PP) {
|
|
g_xspi_log[e].cmd = XSPI_CMD_PP;
|
|
if (g_wel) {
|
|
/* Real NOR behavior: the write pointer wraps to the start
|
|
* of the physical page it started in, so a Page Program
|
|
* crossing the boundary clobbers the beginning of that
|
|
* page. */
|
|
{
|
|
uint32_t base = addr & ~(uint32_t)(FLASH_PAGE_SIZE - 1);
|
|
|
|
for (i = 0; i < len; i++)
|
|
g_nor[base + ((addr + i) & (FLASH_PAGE_SIZE - 1))] =
|
|
g_tfd_stream[i];
|
|
}
|
|
g_wel = 0; /* the program consumes the latch */
|
|
g_busy = NOR_BUSY_POLLS;
|
|
}
|
|
else {
|
|
g_xspi_log[e].rejected = 1; /* NOR ignores PP without WEN */
|
|
}
|
|
}
|
|
else if (seq == LUT_INDEX_SE) {
|
|
g_xspi_log[e].cmd = XSPI_CMD_SE;
|
|
if (g_wel) {
|
|
memset(&g_nor[addr], 0xFF, len);
|
|
g_wel = 0;
|
|
g_busy = NOR_BUSY_POLLS;
|
|
}
|
|
else {
|
|
g_xspi_log[e].rejected = 1;
|
|
}
|
|
}
|
|
|
|
XSPI_INTR |= XSPI_IPCMDDONE;
|
|
g_tfd_stream_len = 0; /* the FIFO was drained by the command */
|
|
}
|
|
|
|
/* The controller is told to run the sequence programmed in IPCR1/0
|
|
* only through XSPI_IPCMD_START(); the emulation runs there. */
|
|
#undef XSPI_IPCMD_START
|
|
#define XSPI_IPCMD_START() xspi_emu_start()
|
|
|
|
/* The TX FIFO wait and "reset" both evaluate the TXWE mask; evaluating
|
|
* it re-arms the TXWE status bit so the waits cannot spin on the host
|
|
* (the real controller raises it when the FIFO has room). */
|
|
#undef XSPI_INTR_IPTXWE_MASK
|
|
#define XSPI_INTR_IPTXWE_MASK ({ g_xspi_regs[0x14 / 4] |= 0x1 << 6; 0x1 << 6; })
|
|
|
|
/* Software reset completes immediately on the host. */
|
|
static void xspi_emu_sw_reset(void)
|
|
{
|
|
XSPI_MCR0 = 0;
|
|
}
|
|
#undef XSPI_SWRESET
|
|
#define XSPI_SWRESET() xspi_emu_sw_reset()
|
|
|
|
/* Pull in the code under test. nxp_ls1028a_host.c is generated from
|
|
* hal/nxp_ls1028a.c by the Makefile (see there for the single
|
|
* mechanical difference). */
|
|
#include "nxp_ls1028a_host.c"
|
|
|
|
static void setup(void)
|
|
{
|
|
memset(g_xspi_regs, 0, sizeof(g_xspi_regs));
|
|
memset(g_nor, 0xFF, sizeof(g_nor));
|
|
g_xspi_log_n = 0;
|
|
g_wel = 0;
|
|
g_busy = 0;
|
|
g_tfd_stream_len = 0;
|
|
XSPI_INTR = 0x1 << 6; /* TX FIFO write enable */
|
|
}
|
|
|
|
static void teardown(void)
|
|
{
|
|
}
|
|
|
|
static void fill(uint8_t *buf, size_t len, uint8_t base)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < len; i++)
|
|
buf[i] = (uint8_t)(base + i);
|
|
}
|
|
|
|
/* No command in the log may have been rejected by the flash. */
|
|
static int any_rejected(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < g_xspi_log_n; i++) {
|
|
if (g_xspi_log[i].rejected)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* A single-page write: one WEN, one Page Program, data lands. */
|
|
START_TEST(test_write_single_page)
|
|
{
|
|
uint8_t data[512];
|
|
size_t i;
|
|
|
|
fill(data, sizeof(data), 0x10);
|
|
|
|
ck_assert_int_eq(hal_flash_write(0, data, 256), 256);
|
|
|
|
ck_assert_int_eq(g_xspi_log_n, 2);
|
|
ck_assert_int_eq(g_xspi_log[0].cmd, XSPI_CMD_WEN);
|
|
ck_assert_int_eq(g_xspi_log[1].cmd, XSPI_CMD_PP);
|
|
ck_assert_uint_eq(g_xspi_log[1].addr, 0);
|
|
ck_assert_uint_eq(g_xspi_log[1].len, 256);
|
|
ck_assert_int_eq(any_rejected(), 0);
|
|
for (i = 0; i < 256; i++)
|
|
ck_assert_uint_eq(g_nor[i], data[i]);
|
|
}
|
|
END_TEST
|
|
|
|
/* Multi-page write: the pre-fix single WEN left the latch clear for
|
|
* every Page Program after the first, so the flash rejected them and
|
|
* the NOR kept 0xFF while hal_flash_write() returned success. */
|
|
START_TEST(test_write_multipart)
|
|
{
|
|
uint8_t data[1024];
|
|
size_t i;
|
|
|
|
fill(data, sizeof(data), 0x20);
|
|
|
|
ck_assert_int_eq(hal_flash_write(0, data, 600), 600);
|
|
|
|
/* WEN before each of the three Page Programs */
|
|
ck_assert_int_eq(g_xspi_log_n, 6);
|
|
for (i = 0; i < 3; i++) {
|
|
ck_assert_int_eq(g_xspi_log[2 * i].cmd, XSPI_CMD_WEN);
|
|
ck_assert_int_eq(g_xspi_log[2 * i + 1].cmd, XSPI_CMD_PP);
|
|
}
|
|
ck_assert_uint_eq(g_xspi_log[1].len, 256);
|
|
ck_assert_uint_eq(g_xspi_log[3].addr, 256);
|
|
ck_assert_uint_eq(g_xspi_log[3].len, 256);
|
|
ck_assert_uint_eq(g_xspi_log[5].addr, 512);
|
|
ck_assert_uint_eq(g_xspi_log[5].len, 88);
|
|
ck_assert_int_eq(any_rejected(), 0);
|
|
for (i = 0; i < 600; i++)
|
|
ck_assert_uint_eq(g_nor[i], data[i]);
|
|
for (i = 600; i < sizeof(data); i++)
|
|
ck_assert_uint_eq(g_nor[i], 0xFF);
|
|
}
|
|
END_TEST
|
|
|
|
/* Multi-page write starting at an unaligned address. The first
|
|
* chunk must stop at the physical page boundary (156 bytes), not run
|
|
* a full 256-byte Page Program across it. */
|
|
START_TEST(test_write_unaligned)
|
|
{
|
|
uint8_t data[1024];
|
|
size_t i;
|
|
|
|
fill(data, sizeof(data), 0x30);
|
|
|
|
ck_assert_int_eq(hal_flash_write(100, data, 600), 600);
|
|
|
|
ck_assert_int_eq(g_xspi_log_n, 6);
|
|
for (i = 0; i < 3; i++) {
|
|
ck_assert_int_eq(g_xspi_log[2 * i].cmd, XSPI_CMD_WEN);
|
|
ck_assert_int_eq(g_xspi_log[2 * i + 1].cmd, XSPI_CMD_PP);
|
|
}
|
|
ck_assert_uint_eq(g_xspi_log[1].addr, 100);
|
|
ck_assert_uint_eq(g_xspi_log[1].len, 156);
|
|
ck_assert_uint_eq(g_xspi_log[3].addr, 256);
|
|
ck_assert_uint_eq(g_xspi_log[3].len, 256);
|
|
ck_assert_uint_eq(g_xspi_log[5].addr, 512);
|
|
ck_assert_uint_eq(g_xspi_log[5].len, 188);
|
|
ck_assert_int_eq(any_rejected(), 0);
|
|
for (i = 0; i < 600; i++)
|
|
ck_assert_uint_eq(g_nor[100 + i], data[i]);
|
|
for (i = 0; i < 100; i++)
|
|
ck_assert_uint_eq(g_nor[i], 0xFF);
|
|
for (i = 700; i < sizeof(data); i++)
|
|
ck_assert_uint_eq(g_nor[i], 0xFF);
|
|
}
|
|
END_TEST
|
|
|
|
/* A write that crosses a page boundary must not clobber the start of
|
|
* the next page: NOR wraps the write pointer at the boundary, so a
|
|
* 256-byte Page Program starting 16 bytes short of it would overwrite
|
|
* the first 16 bytes of the page. */
|
|
START_TEST(test_write_crossing_page_no_wrap)
|
|
{
|
|
uint8_t data[256];
|
|
size_t i;
|
|
|
|
/* Pre-seed the whole flash with a pattern. */
|
|
memset(g_nor, 0x5A, sizeof(g_nor));
|
|
fill(data, sizeof(data), 0x60);
|
|
|
|
ck_assert_int_eq(hal_flash_write(240, data, 256), 256);
|
|
|
|
/* No Page Program may cross a physical page boundary. */
|
|
for (i = 0; i < g_xspi_log_n; i++) {
|
|
if (g_xspi_log[i].cmd == XSPI_CMD_PP)
|
|
ck_assert_int_le(
|
|
(int)(g_xspi_log[i].addr % FLASH_PAGE_SIZE) +
|
|
(int)g_xspi_log[i].len, FLASH_PAGE_SIZE);
|
|
}
|
|
ck_assert_int_eq(g_xspi_log_n, 4); /* WEN+PP x2 */
|
|
ck_assert_uint_eq(g_xspi_log[1].addr, 240);
|
|
ck_assert_uint_eq(g_xspi_log[1].len, 16);
|
|
ck_assert_uint_eq(g_xspi_log[3].addr, 256);
|
|
ck_assert_uint_eq(g_xspi_log[3].len, 240);
|
|
ck_assert_int_eq(any_rejected(), 0);
|
|
|
|
/* The written bytes. */
|
|
for (i = 0; i < 256; i++)
|
|
ck_assert_uint_eq(g_nor[240 + i], data[i]);
|
|
|
|
/* The page start that a wrapping program would clobber. */
|
|
for (i = 0; i < 240; i++)
|
|
ck_assert_uint_eq(g_nor[i], 0x5A);
|
|
for (i = 496; i < sizeof(g_nor); i++)
|
|
ck_assert_uint_eq(g_nor[i], 0x5A);
|
|
}
|
|
END_TEST
|
|
|
|
/* The EXT_FLASH wrapper has the same single-WEN bug. */
|
|
START_TEST(test_ext_write_multipart)
|
|
{
|
|
uint8_t data[1024];
|
|
size_t i;
|
|
|
|
fill(data, sizeof(data), 0x40);
|
|
|
|
ck_assert_int_eq(ext_flash_write(0, data, 600), 600);
|
|
|
|
ck_assert_int_eq(g_xspi_log_n, 6);
|
|
for (i = 0; i < 3; i++) {
|
|
ck_assert_int_eq(g_xspi_log[2 * i].cmd, XSPI_CMD_WEN);
|
|
ck_assert_int_eq(g_xspi_log[2 * i + 1].cmd, XSPI_CMD_PP);
|
|
}
|
|
ck_assert_int_eq(any_rejected(), 0);
|
|
for (i = 0; i < 600; i++)
|
|
ck_assert_uint_eq(g_nor[i], data[i]);
|
|
}
|
|
END_TEST
|
|
|
|
/* Note: hal_flash_erase() already issues a Write Enable per sector
|
|
* inside its loop, so it is not affected by this finding (and its SE
|
|
* command encoding is a separate matter, not covered here).
|
|
*
|
|
* Suite for the F-7977 write path: */
|
|
Suite *ls1028a_xspi_suite(void)
|
|
{
|
|
Suite *s = suite_create("ls1028a-xspi-write");
|
|
TCase *tc = tcase_create("ls1028a-xspi-write");
|
|
|
|
tcase_add_checked_fixture(tc, setup, teardown);
|
|
tcase_add_test(tc, test_write_single_page);
|
|
tcase_add_test(tc, test_write_multipart);
|
|
tcase_add_test(tc, test_write_unaligned);
|
|
tcase_add_test(tc, test_write_crossing_page_no_wrap);
|
|
tcase_add_test(tc, test_ext_write_multipart);
|
|
|
|
suite_add_tcase(s, tc);
|
|
return s;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int fails;
|
|
Suite *s = ls1028a_xspi_suite();
|
|
SRunner *sr = srunner_create(s);
|
|
|
|
srunner_run_all(sr, CK_NORMAL);
|
|
fails = srunner_ntests_failed(sr);
|
|
srunner_free(sr);
|
|
|
|
return fails;
|
|
}
|