F-11033: propagate program/erase timeouts from the T10xx flash HAL

hal_flash_status_wait() returns -1 when the NOR does not settle its
status bits within the poll budget, but hal_flash_write() and
hal_flash_erase() discarded the result and returned 0 unconditionally:
a stuck program or erase reported success, and the update flow
continued as if the flash held the new image.

Capture every wait result and return it to the caller on the first
failure. No state-restore command is needed: this driver works through
the memory-mapped QPI window, where reads are plain loads and the
controller issues the read command per access, so there is no device
command state to recover.

unit-t10xx-flash-status runs the real extracted functions against a
mock QPI status model (offset 0 reports the DQ status byte: toggling
while busy, 0x44 after a program, 0x4C after an erase). A stuck
device burns the full 200 ms / 1.1 s poll budget with a no-op udelay,
so the timeout path runs in milliseconds.

Verification:
- Built: powerpc-linux-gnu-gcc -fsyntax-only -Wall with
  TARGET_nxp_t1024: clean.
- Tested: unit-t10xx-flash-status 4/4; pre-fix both timeout tests got
  ret == 0 from a stuck device, now -1. Success paths (write lands in
  the model, erase completes) unchanged.
- Pitfalls: the first failing page/sector now aborts the rest of the
  operation, which is the desired behavior (the caller aborts the
  update); no callers depended on the unconditional 0.
- Style: cstyle-check.sh flag count on hal/nxp_t10xx.c unchanged from
  the pre-change file; the new test trips only the uncrustify
  pointer-alignment class the sibling unit tests trip.
- Message: F-11033: prefix, no co-author trailers.
- Unverified: no T10xx board execution.
pull/870/head
Daniele Lacamera 2026-08-24 18:33:40 +02:00
parent f573e97e76
commit 1ae15e3abb
3 changed files with 276 additions and 3 deletions

View File

@ -3249,6 +3249,7 @@ static int hal_flash_status_wait(uint32_t sector, uint16_t mask, uint32_t timeou
int hal_flash_write(uint32_t address, const uint8_t *data, int len)
{
uint32_t i, pos, sector, offset, xfer, nwords;
int ret = 0;
/* adjust for flash base */
if (address >= FLASH_BASE_ADDR)
@ -3288,7 +3289,9 @@ int hal_flash_write(uint32_t address, const uint8_t *data, int len)
FLASH_IO16_WRITE(sector, offset, 0);
FLASH_IO16_WRITE(sector, offset, word);
FLASH_IO8_WRITE(sector, offset, AMD_CMD_WRITE_BUFFER_CONFIRM);
hal_flash_status_wait(sector, 0x44, 200*1000);
ret = hal_flash_status_wait(sector, 0x44, 200*1000);
if (ret != 0)
return ret;
address++;
pos++;
len--;
@ -3317,7 +3320,9 @@ int hal_flash_write(uint32_t address, const uint8_t *data, int len)
/* Typical 410us */
/* poll for program completion - max 200ms */
hal_flash_status_wait(sector, 0x44, 200*1000);
ret = hal_flash_status_wait(sector, 0x44, 200*1000);
if (ret != 0)
return ret;
address += xfer;
len -= xfer;
@ -3328,6 +3333,7 @@ int hal_flash_write(uint32_t address, const uint8_t *data, int len)
int hal_flash_erase(uint32_t address, int len)
{
uint32_t sector;
int ret = 0;
/* adjust for flash base */
if (address >= FLASH_BASE_ADDR)
@ -3350,7 +3356,9 @@ int hal_flash_erase(uint32_t address, int len)
/* Typical is 200ms (max 1100ms) */
/* poll for erase completion - max 1.1 sec */
hal_flash_status_wait(sector, 0x4C, 1100*1000);
ret = hal_flash_status_wait(sector, 0x4C, 1100*1000);
if (ret != 0)
return ret;
address += FLASH_SECTOR_SIZE;
len -= FLASH_SECTOR_SIZE;

View File

@ -113,6 +113,7 @@ TESTS+=unit-zynq-ext-write
TESTS+=unit-versal-qspi-dma
TESTS+=unit-versal-ext-write
TESTS+=unit-t10xx-qe-firmware
TESTS+=unit-t10xx-flash-status
TESTS+=unit-aurix-erased-fill
TESTS+=unit-aurix-erased-fill-invert
TESTS+=unit-t2080-fman-loader
@ -1005,6 +1006,19 @@ stm32g4_write_extract.h: ../../hal/stm32g4.c
unit-stm32g4-write: unit-stm32g4-write.c stm32g4_write_extract.h
gcc -o $@ unit-stm32g4-write.c $(CFLAGS) $(LDFLAGS)
# unit-t10xx-flash-status runs the real hal_flash_write()/hal_flash_erase()
# and hal_flash_status_wait() from hal/nxp_t10xx.c against a mock QPI
# status model (F-11033: a timed-out program/erase used to report
# success because the wait result was discarded).
t10xx_flash_status_extract.h: ../../hal/nxp_t10xx.c
sed -n '/^static void hal_flash_unlock_sector/,/^}/p' $< > $@
sed -n '/^static int hal_flash_status_wait/,/^}/p' $< >> $@
sed -n '/^int hal_flash_write/,/^}/p' $< >> $@
sed -n '/^int hal_flash_erase/,/^}/p' $< >> $@
unit-t10xx-flash-status: unit-t10xx-flash-status.c t10xx_flash_status_extract.h
gcc -o $@ unit-t10xx-flash-status.c $(CFLAGS) $(LDFLAGS)
# unit-ecc-raw-der runs the real wolfCrypt raw-to-DER conversion and
# verification (F-11024: the wolfHSM verify path in src/image.c passed
# minimal field sizes with field-start pointers to

View File

@ -0,0 +1,251 @@
/* unit-t10xx-flash-status.c
*
* Regression test for F-11033: hal_flash_write() and hal_flash_erase()
* in hal/nxp_t10xx.c discarded the return value of
* hal_flash_status_wait(), so a program or erase that timed out (the
* NOR stuck mid-operation) still reported success to the caller.
*
* The real functions are extracted by the Makefile and run against a
* mock QPI status model: offset 0 of a sector reports the DQ status
* byte (toggling while busy, 0x44 after a program, 0x4C after an
* erase). udelay() is a no-op, so a stuck device burns the full poll
* budget (200 ms / 1.1 s worth of iterations) in milliseconds.
*
* 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 <string.h>
/* 16-bit QPI configuration, as used by the T10xx targets. */
#define FLASH_BASE_ADDR 0
#define FLASH_PAGE_SIZE 1024
#define FLASH_SECTOR_SIZE (128 * 1024)
#define FLASH_CFI_WIDTH 16
#define AMD_CMD_ERASE_START 0x80
#define AMD_CMD_ERASE_SECTOR 0x30
#define AMD_CMD_UNLOCK_START 0xAA
#define AMD_CMD_UNLOCK_ACK 0x55
#define AMD_CMD_WRITE_TO_BUFFER 0x25
#define AMD_CMD_WRITE_BUFFER_CONFIRM 0x29
#define AMD_STATUS_TOGGLE 0x40
#define FLASH_UNLOCK_ADDR1 0x555
#define FLASH_UNLOCK_ADDR2 0x2AA
void udelay(uint32_t us);
/* ---- mock QPI status model ------------------------------------------- */
typedef enum t10xx_mock_state {
ST_IDLE = 0,
ST_PROGRAMMING,
ST_ERASING,
ST_PROG_DONE,
ST_ERASE_DONE,
} t10xx_mock_state;
#define MOCK_SECTOR_BYTES 64
#define NSECTORS 4
static uint8_t g_flash[NSECTORS * MOCK_SECTOR_BYTES];
static int g_state[NSECTORS];
static int g_stuck[NSECTORS];
static int g_unlocks;
static void mock_reset(int stuck)
{
int s;
memset(g_flash, 0, sizeof(g_flash));
for (s = 0; s < NSECTORS; s++) {
g_state[s] = ST_IDLE;
g_stuck[s] = stuck;
}
g_unlocks = 0;
}
/* The real macros dereference the memory-mapped flash window; the test
* routes them through the model so command writes drive the state.
* 16-bit variant: every index is a word index (byte offset = 2 * n),
* and the 8-bit write replicates the byte across the 16-bit word. */
static uint8_t mock_read8(uint32_t sector, uint32_t n)
{
uint32_t byte_off = 2 * n;
if (n == 0 &&
((g_state[sector] == ST_PROGRAMMING) ||
(g_state[sector] == ST_ERASING))) {
if (!g_stuck[sector]) {
/* Complete on the first status poll after the command. */
g_state[sector] = (g_state[sector] == ST_PROGRAMMING) ?
ST_PROG_DONE : ST_ERASE_DONE;
return (g_state[sector] == ST_PROG_DONE) ? 0x44 : 0x4C;
}
return AMD_STATUS_TOGGLE; /* busy, never completes */
}
if (n == 0) {
return 0x4C; /* idle: satisfies program (0x44) and erase (0x4C) */
}
if (byte_off < MOCK_SECTOR_BYTES) {
return g_flash[sector * MOCK_SECTOR_BYTES + byte_off];
}
return 0;
}
static uint16_t mock_read16(uint32_t sector, uint32_t n)
{
uint32_t byte_off = 2 * n;
return (uint16_t)(g_flash[sector * MOCK_SECTOR_BYTES + byte_off] |
((uint16_t)g_flash[sector * MOCK_SECTOR_BYTES +
byte_off + 1] << 8));
}
static void mock_write8(uint32_t sector, uint32_t n, uint8_t val)
{
uint32_t byte_off = 2 * n;
if (n == FLASH_UNLOCK_ADDR1 && val == AMD_CMD_UNLOCK_START) {
g_unlocks++;
return;
}
if (n == FLASH_UNLOCK_ADDR2 && val == AMD_CMD_UNLOCK_ACK) {
return;
}
if (n == 0 && val == AMD_CMD_ERASE_SECTOR) {
g_state[sector] = ST_ERASING;
return;
}
if (val == AMD_CMD_WRITE_BUFFER_CONFIRM) {
g_state[sector] = ST_PROGRAMMING;
return;
}
if (byte_off < MOCK_SECTOR_BYTES) {
g_flash[sector * MOCK_SECTOR_BYTES + byte_off] = val;
g_flash[sector * MOCK_SECTOR_BYTES + byte_off + 1] = val;
}
}
static void mock_write16(uint32_t sector, uint32_t n, uint16_t val)
{
uint32_t byte_off = 2 * n;
if (byte_off + 1 < MOCK_SECTOR_BYTES) {
g_flash[sector * MOCK_SECTOR_BYTES + byte_off] = (uint8_t)(val & 0xFF);
g_flash[sector * MOCK_SECTOR_BYTES + byte_off + 1] =
(uint8_t)(val >> 8);
}
}
#define FLASH_IO8_READ(sec, n) mock_read8((sec), (n))
#define FLASH_IO8_WRITE(sec, n, val) mock_write8((sec), (n), (val))
#define FLASH_IO16_READ(sec, n) mock_read16((sec), (n))
#define FLASH_IO16_WRITE(sec, n, val) mock_write16((sec), (n), (val))
/* The real functions from hal/nxp_t10xx.c (extracted by the Makefile). */
static void hal_flash_unlock_sector(uint32_t sector);
#include "t10xx_flash_status_extract.h"
void udelay(uint32_t us)
{
(void)us; /* no real delay: a stuck device burns the poll budget fast */
}
START_TEST (test_flash_write_success)
{
uint8_t data[8];
int i;
int ret;
mock_reset(0);
for (i = 0; i < 8; i++)
data[i] = (uint8_t)(0xA0 + i);
ret = hal_flash_write(0, data, sizeof(data));
ck_assert_int_eq(ret, 0);
ck_assert_int_gt(g_unlocks, 0);
ck_assert_mem_eq(g_flash, data, 8);
}
END_TEST
START_TEST (test_flash_write_timeout_returns_error)
{
uint8_t data[8] = { 0 };
int ret;
/* Device stuck mid-program: the status never settles. */
mock_reset(1);
ret = hal_flash_write(0, data, sizeof(data));
ck_assert_int_eq(ret, -1);
}
END_TEST
START_TEST (test_flash_erase_success)
{
int ret;
mock_reset(0);
ret = hal_flash_erase(0, FLASH_SECTOR_SIZE);
ck_assert_int_eq(ret, 0);
ck_assert_int_gt(g_unlocks, 0);
}
END_TEST
START_TEST (test_flash_erase_timeout_returns_error)
{
int ret;
/* Device stuck mid-erase: the status never settles. */
mock_reset(1);
ret = hal_flash_erase(0, FLASH_SECTOR_SIZE);
ck_assert_int_eq(ret, -1);
}
END_TEST
Suite *t10xx_flash_status_suite(void)
{
Suite *s = suite_create("t10xx flash status");
TCase *tc = tcase_create("flash-op-timeout");
tcase_add_test(tc, test_flash_write_success);
tcase_add_test(tc, test_flash_write_timeout_returns_error);
tcase_add_test(tc, test_flash_erase_success);
tcase_add_test(tc, test_flash_erase_timeout_returns_error);
tcase_set_timeout(tc, 30);
suite_add_tcase(s, tc);
return s;
}
int main(void)
{
int fails;
Suite *s = t10xx_flash_status_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
fails = srunner_ntests_failed(sr);
srunner_free(sr);
return fails;
}