During a sector erase (but not a chip erase), the embeded erase program can be suspended. Once suspended, the sectors not selected for erasure may be read and programmed. Autoselect mode is allowed during erase suspend mode. Presumably, CFI queries are similarly allowed so this commit allows them as well. Since guest firmware can use status bits DQ7, DQ6, DQ3, and DQ2 to determine the current state of sector erasure, these bits are properly implemented. Signed-off-by: Stephen Checkoway <stephen.checkoway@oberlin.edu> Message-Id: <20190426162624.55977-10-stephen.checkoway@oberlin.edu> Acked-by: Thomas Huth <thuth@redhat.com> Acked-by: Philippe Mathieu-Daudé <philmd@redhat.com> Tested-by: Philippe Mathieu-Daudé <philmd@redhat.com> [PMD: Rebased] Signed-off-by: Philippe Mathieu-Daudé <philmd@redhat.com>
682 lines
23 KiB
C
682 lines
23 KiB
C
/*
|
|
* QTest testcase for parallel flash with AMD command set
|
|
*
|
|
* Copyright (c) 2019 Stephen Checkoway
|
|
*
|
|
* This work is licensed under the terms of the GNU GPL, version 2 or later.
|
|
* See the COPYING file in the top-level directory.
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "libqtest.h"
|
|
|
|
/*
|
|
* To test the pflash_cfi02 device, we run QEMU with the musicpal machine with
|
|
* a pflash drive. This enables us to test some flash configurations, but not
|
|
* all. In particular, we're limited to a 16-bit wide flash device.
|
|
*/
|
|
|
|
#define MP_FLASH_SIZE_MAX (32 * 1024 * 1024)
|
|
#define BASE_ADDR (0x100000000ULL - MP_FLASH_SIZE_MAX)
|
|
|
|
#define UNIFORM_FLASH_SIZE (8 * 1024 * 1024)
|
|
#define UNIFORM_FLASH_SECTOR_SIZE (64 * 1024)
|
|
|
|
/* Use a newtype to keep flash addresses separate from byte addresses. */
|
|
typedef struct {
|
|
uint64_t addr;
|
|
} faddr;
|
|
#define FLASH_ADDR(x) ((faddr) { .addr = (x) })
|
|
|
|
#define CFI_ADDR FLASH_ADDR(0x55)
|
|
#define UNLOCK0_ADDR FLASH_ADDR(0x555)
|
|
#define UNLOCK1_ADDR FLASH_ADDR(0x2AA)
|
|
|
|
#define CFI_CMD 0x98
|
|
#define UNLOCK0_CMD 0xAA
|
|
#define UNLOCK1_CMD 0x55
|
|
#define SECOND_UNLOCK_CMD 0x80
|
|
#define AUTOSELECT_CMD 0x90
|
|
#define RESET_CMD 0xF0
|
|
#define PROGRAM_CMD 0xA0
|
|
#define SECTOR_ERASE_CMD 0x30
|
|
#define CHIP_ERASE_CMD 0x10
|
|
#define UNLOCK_BYPASS_CMD 0x20
|
|
#define UNLOCK_BYPASS_RESET_CMD 0x00
|
|
#define ERASE_SUSPEND_CMD 0xB0
|
|
#define ERASE_RESUME_CMD SECTOR_ERASE_CMD
|
|
|
|
typedef struct {
|
|
int bank_width;
|
|
|
|
/* Nonuniform block size. */
|
|
int nb_blocs[4];
|
|
int sector_len[4];
|
|
|
|
QTestState *qtest;
|
|
} FlashConfig;
|
|
|
|
static char image_path[] = "/tmp/qtest.XXXXXX";
|
|
|
|
/*
|
|
* The pflash implementation allows some parameters to be unspecified. We want
|
|
* to test those configurations but we also need to know the real values in
|
|
* our testing code. So after we launch qemu, we'll need a new FlashConfig
|
|
* with the correct values filled in.
|
|
*/
|
|
static FlashConfig expand_config_defaults(const FlashConfig *c)
|
|
{
|
|
FlashConfig ret = *c;
|
|
|
|
if (ret.bank_width == 0) {
|
|
ret.bank_width = 2;
|
|
}
|
|
if (ret.nb_blocs[0] == 0 && ret.sector_len[0] == 0) {
|
|
ret.sector_len[0] = UNIFORM_FLASH_SECTOR_SIZE;
|
|
ret.nb_blocs[0] = UNIFORM_FLASH_SIZE / UNIFORM_FLASH_SECTOR_SIZE;
|
|
}
|
|
|
|
/* XXX: Limitations of test harness. */
|
|
assert(ret.bank_width == 2);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Return a bit mask suitable for extracting the least significant
|
|
* status/query response from an interleaved response.
|
|
*/
|
|
static inline uint64_t device_mask(const FlashConfig *c)
|
|
{
|
|
return (uint64_t)-1;
|
|
}
|
|
|
|
/*
|
|
* Return a bit mask exactly as long as the bank_width.
|
|
*/
|
|
static inline uint64_t bank_mask(const FlashConfig *c)
|
|
{
|
|
if (c->bank_width == 8) {
|
|
return (uint64_t)-1;
|
|
}
|
|
return (1ULL << (c->bank_width * 8)) - 1ULL;
|
|
}
|
|
|
|
static inline void flash_write(const FlashConfig *c, uint64_t byte_addr,
|
|
uint64_t data)
|
|
{
|
|
/* Sanity check our tests. */
|
|
assert((data & ~bank_mask(c)) == 0);
|
|
uint64_t addr = BASE_ADDR + byte_addr;
|
|
switch (c->bank_width) {
|
|
case 1:
|
|
qtest_writeb(c->qtest, addr, data);
|
|
break;
|
|
case 2:
|
|
qtest_writew(c->qtest, addr, data);
|
|
break;
|
|
case 4:
|
|
qtest_writel(c->qtest, addr, data);
|
|
break;
|
|
case 8:
|
|
qtest_writeq(c->qtest, addr, data);
|
|
break;
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
|
|
static inline uint64_t flash_read(const FlashConfig *c, uint64_t byte_addr)
|
|
{
|
|
uint64_t addr = BASE_ADDR + byte_addr;
|
|
switch (c->bank_width) {
|
|
case 1:
|
|
return qtest_readb(c->qtest, addr);
|
|
case 2:
|
|
return qtest_readw(c->qtest, addr);
|
|
case 4:
|
|
return qtest_readl(c->qtest, addr);
|
|
case 8:
|
|
return qtest_readq(c->qtest, addr);
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Convert a flash address expressed in the maximum width of the device as a
|
|
* byte address.
|
|
*/
|
|
static inline uint64_t as_byte_addr(const FlashConfig *c, faddr flash_addr)
|
|
{
|
|
/*
|
|
* Command addresses are always given as addresses in the maximum
|
|
* supported bus size for the flash chip. So an x8/x16 chip in x8 mode
|
|
* uses addresses 0xAAA and 0x555 to unlock because the least significant
|
|
* bit is ignored. (0x555 rather than 0x554 is traditional.)
|
|
*
|
|
* In general we need to multiply by the maximum device width.
|
|
*/
|
|
return flash_addr.addr * c->bank_width;
|
|
}
|
|
|
|
/*
|
|
* Return the command value or expected status replicated across all devices.
|
|
*/
|
|
static inline uint64_t replicate(const FlashConfig *c, uint64_t data)
|
|
{
|
|
/* Sanity check our tests. */
|
|
assert((data & ~device_mask(c)) == 0);
|
|
return data;
|
|
}
|
|
|
|
static inline void flash_cmd(const FlashConfig *c, faddr cmd_addr,
|
|
uint8_t cmd)
|
|
{
|
|
flash_write(c, as_byte_addr(c, cmd_addr), replicate(c, cmd));
|
|
}
|
|
|
|
static inline uint64_t flash_query(const FlashConfig *c, faddr query_addr)
|
|
{
|
|
return flash_read(c, as_byte_addr(c, query_addr));
|
|
}
|
|
|
|
static inline uint64_t flash_query_1(const FlashConfig *c, faddr query_addr)
|
|
{
|
|
return flash_query(c, query_addr) & device_mask(c);
|
|
}
|
|
|
|
static void unlock(const FlashConfig *c)
|
|
{
|
|
flash_cmd(c, UNLOCK0_ADDR, UNLOCK0_CMD);
|
|
flash_cmd(c, UNLOCK1_ADDR, UNLOCK1_CMD);
|
|
}
|
|
|
|
static void reset(const FlashConfig *c)
|
|
{
|
|
flash_cmd(c, FLASH_ADDR(0), RESET_CMD);
|
|
}
|
|
|
|
static void sector_erase(const FlashConfig *c, uint64_t byte_addr)
|
|
{
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, SECOND_UNLOCK_CMD);
|
|
unlock(c);
|
|
flash_write(c, byte_addr, replicate(c, SECTOR_ERASE_CMD));
|
|
}
|
|
|
|
static void wait_for_completion(const FlashConfig *c, uint64_t byte_addr)
|
|
{
|
|
/* If DQ6 is toggling, step the clock and ensure the toggle stops. */
|
|
const uint64_t dq6 = replicate(c, 0x40);
|
|
if ((flash_read(c, byte_addr) & dq6) ^ (flash_read(c, byte_addr) & dq6)) {
|
|
/* Wait for erase or program to finish. */
|
|
qtest_clock_step_next(c->qtest);
|
|
/* Ensure that DQ6 has stopped toggling. */
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==, flash_read(c, byte_addr));
|
|
}
|
|
}
|
|
|
|
static void bypass_program(const FlashConfig *c, uint64_t byte_addr,
|
|
uint16_t data)
|
|
{
|
|
flash_cmd(c, UNLOCK0_ADDR, PROGRAM_CMD);
|
|
flash_write(c, byte_addr, data);
|
|
/*
|
|
* Data isn't valid until DQ6 stops toggling. We don't model this as
|
|
* writes are immediate, but if this changes in the future, we can wait
|
|
* until the program is complete.
|
|
*/
|
|
wait_for_completion(c, byte_addr);
|
|
}
|
|
|
|
static void program(const FlashConfig *c, uint64_t byte_addr, uint16_t data)
|
|
{
|
|
unlock(c);
|
|
bypass_program(c, byte_addr, data);
|
|
}
|
|
|
|
static void chip_erase(const FlashConfig *c)
|
|
{
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, SECOND_UNLOCK_CMD);
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, CHIP_ERASE_CMD);
|
|
}
|
|
|
|
static void erase_suspend(const FlashConfig *c)
|
|
{
|
|
flash_cmd(c, FLASH_ADDR(0), ERASE_SUSPEND_CMD);
|
|
}
|
|
|
|
static void erase_resume(const FlashConfig *c)
|
|
{
|
|
flash_cmd(c, FLASH_ADDR(0), ERASE_RESUME_CMD);
|
|
}
|
|
|
|
/*
|
|
* Test flash commands with a variety of device geometry.
|
|
*/
|
|
static void test_geometry(const void *opaque)
|
|
{
|
|
const FlashConfig *config = opaque;
|
|
QTestState *qtest;
|
|
qtest = qtest_initf("-M musicpal,accel=qtest"
|
|
" -drive if=pflash,file=%s,format=raw,copy-on-read"
|
|
/* Device geometry properties. */
|
|
" -global driver=cfi.pflash02,"
|
|
"property=num-blocks0,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=sector-length0,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=num-blocks1,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=sector-length1,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=num-blocks2,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=sector-length2,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=num-blocks3,value=%d"
|
|
" -global driver=cfi.pflash02,"
|
|
"property=sector-length3,value=%d",
|
|
image_path,
|
|
config->nb_blocs[0],
|
|
config->sector_len[0],
|
|
config->nb_blocs[1],
|
|
config->sector_len[1],
|
|
config->nb_blocs[2],
|
|
config->sector_len[2],
|
|
config->nb_blocs[3],
|
|
config->sector_len[3]);
|
|
FlashConfig explicit_config = expand_config_defaults(config);
|
|
explicit_config.qtest = qtest;
|
|
const FlashConfig *c = &explicit_config;
|
|
|
|
/* Check the IDs. */
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, AUTOSELECT_CMD);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0)), ==, replicate(c, 0xBF));
|
|
if (c->bank_width >= 2) {
|
|
/*
|
|
* XXX: The ID returned by the musicpal flash chip is 16 bits which
|
|
* wouldn't happen with an 8-bit device. It would probably be best to
|
|
* prohibit addresses larger than the device width in pflash_cfi02.c,
|
|
* but then we couldn't test smaller device widths at all.
|
|
*/
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(1)), ==,
|
|
replicate(c, 0x236D));
|
|
}
|
|
reset(c);
|
|
|
|
/* Check the erase blocks. */
|
|
flash_cmd(c, CFI_ADDR, CFI_CMD);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x10)), ==, replicate(c, 'Q'));
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x11)), ==, replicate(c, 'R'));
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x12)), ==, replicate(c, 'Y'));
|
|
|
|
/* Num erase regions. */
|
|
int nb_erase_regions = flash_query_1(c, FLASH_ADDR(0x2C));
|
|
g_assert_cmphex(nb_erase_regions, ==,
|
|
!!c->nb_blocs[0] + !!c->nb_blocs[1] + !!c->nb_blocs[2] +
|
|
!!c->nb_blocs[3]);
|
|
|
|
/* Check device length. */
|
|
uint32_t device_len = 1 << flash_query_1(c, FLASH_ADDR(0x27));
|
|
g_assert_cmphex(device_len, ==, UNIFORM_FLASH_SIZE);
|
|
|
|
/* Check that erase suspend to read/write is supported. */
|
|
uint16_t pri = flash_query_1(c, FLASH_ADDR(0x15)) +
|
|
(flash_query_1(c, FLASH_ADDR(0x16)) << 8);
|
|
g_assert_cmpint(pri, >=, 0x2D + 4 * nb_erase_regions);
|
|
g_assert_cmpint(flash_query(c, FLASH_ADDR(pri + 0)), ==, replicate(c, 'P'));
|
|
g_assert_cmpint(flash_query(c, FLASH_ADDR(pri + 1)), ==, replicate(c, 'R'));
|
|
g_assert_cmpint(flash_query(c, FLASH_ADDR(pri + 2)), ==, replicate(c, 'I'));
|
|
g_assert_cmpint(flash_query_1(c, FLASH_ADDR(pri + 6)), ==, 2); /* R/W */
|
|
reset(c);
|
|
|
|
const uint64_t dq7 = replicate(c, 0x80);
|
|
const uint64_t dq6 = replicate(c, 0x40);
|
|
const uint64_t dq3 = replicate(c, 0x08);
|
|
const uint64_t dq2 = replicate(c, 0x04);
|
|
|
|
uint64_t byte_addr = 0;
|
|
for (int region = 0; region < nb_erase_regions; ++region) {
|
|
uint64_t base = 0x2D + 4 * region;
|
|
flash_cmd(c, CFI_ADDR, CFI_CMD);
|
|
uint32_t nb_sectors = flash_query_1(c, FLASH_ADDR(base + 0)) +
|
|
(flash_query_1(c, FLASH_ADDR(base + 1)) << 8) + 1;
|
|
uint32_t sector_len = (flash_query_1(c, FLASH_ADDR(base + 2)) << 8) +
|
|
(flash_query_1(c, FLASH_ADDR(base + 3)) << 16);
|
|
g_assert_cmphex(nb_sectors, ==, c->nb_blocs[region]);
|
|
g_assert_cmphex(sector_len, ==, c->sector_len[region]);
|
|
reset(c);
|
|
|
|
/* Erase and program sector. */
|
|
for (uint32_t i = 0; i < nb_sectors; ++i) {
|
|
sector_erase(c, byte_addr);
|
|
|
|
/* Check that DQ3 is 0. */
|
|
g_assert_cmphex(flash_read(c, byte_addr) & dq3, ==, 0);
|
|
qtest_clock_step_next(c->qtest); /* Step over the 50 us timeout. */
|
|
|
|
/* Check that DQ3 is 1. */
|
|
uint64_t status0 = flash_read(c, byte_addr);
|
|
g_assert_cmphex(status0 & dq3, ==, dq3);
|
|
|
|
/* DQ7 is 0 during an erase. */
|
|
g_assert_cmphex(status0 & dq7, ==, 0);
|
|
uint64_t status1 = flash_read(c, byte_addr);
|
|
|
|
/* DQ6 toggles during an erase. */
|
|
g_assert_cmphex(status0 & dq6, ==, ~status1 & dq6);
|
|
|
|
/* Wait for erase to complete. */
|
|
wait_for_completion(c, byte_addr);
|
|
|
|
/* Ensure DQ6 has stopped toggling. */
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==,
|
|
flash_read(c, byte_addr));
|
|
|
|
/* Now the data should be valid. */
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==, bank_mask(c));
|
|
|
|
/* Program a bit pattern. */
|
|
program(c, byte_addr, 0x55);
|
|
g_assert_cmphex(flash_read(c, byte_addr) & 0xFF, ==, 0x55);
|
|
program(c, byte_addr, 0xA5);
|
|
g_assert_cmphex(flash_read(c, byte_addr) & 0xFF, ==, 0x05);
|
|
byte_addr += sector_len;
|
|
}
|
|
}
|
|
|
|
/* Erase the chip. */
|
|
chip_erase(c);
|
|
/* Read toggle. */
|
|
uint64_t status0 = flash_read(c, 0);
|
|
/* DQ7 is 0 during an erase. */
|
|
g_assert_cmphex(status0 & dq7, ==, 0);
|
|
uint64_t status1 = flash_read(c, 0);
|
|
/* DQ6 toggles during an erase. */
|
|
g_assert_cmphex(status0 & dq6, ==, ~status1 & dq6);
|
|
/* Wait for erase to complete. */
|
|
qtest_clock_step_next(c->qtest);
|
|
/* Ensure DQ6 has stopped toggling. */
|
|
g_assert_cmphex(flash_read(c, 0), ==, flash_read(c, 0));
|
|
/* Now the data should be valid. */
|
|
|
|
for (int region = 0; region < nb_erase_regions; ++region) {
|
|
for (uint32_t i = 0; i < c->nb_blocs[region]; ++i) {
|
|
uint64_t byte_addr = i * c->sector_len[region];
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==, bank_mask(c));
|
|
}
|
|
}
|
|
|
|
/* Unlock bypass */
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, UNLOCK_BYPASS_CMD);
|
|
bypass_program(c, 0 * c->bank_width, 0x01);
|
|
bypass_program(c, 1 * c->bank_width, 0x23);
|
|
bypass_program(c, 2 * c->bank_width, 0x45);
|
|
/*
|
|
* Test that bypass programming, unlike normal programming can use any
|
|
* address for the PROGRAM_CMD.
|
|
*/
|
|
flash_cmd(c, FLASH_ADDR(3 * c->bank_width), PROGRAM_CMD);
|
|
flash_write(c, 3 * c->bank_width, 0x67);
|
|
wait_for_completion(c, 3 * c->bank_width);
|
|
flash_cmd(c, FLASH_ADDR(0), UNLOCK_BYPASS_RESET_CMD);
|
|
bypass_program(c, 4 * c->bank_width, 0x89); /* Should fail. */
|
|
g_assert_cmphex(flash_read(c, 0 * c->bank_width), ==, 0x01);
|
|
g_assert_cmphex(flash_read(c, 1 * c->bank_width), ==, 0x23);
|
|
g_assert_cmphex(flash_read(c, 2 * c->bank_width), ==, 0x45);
|
|
g_assert_cmphex(flash_read(c, 3 * c->bank_width), ==, 0x67);
|
|
g_assert_cmphex(flash_read(c, 4 * c->bank_width), ==, bank_mask(c));
|
|
|
|
/* Test ignored high order bits of address. */
|
|
flash_cmd(c, FLASH_ADDR(0x5555), UNLOCK0_CMD);
|
|
flash_cmd(c, FLASH_ADDR(0x2AAA), UNLOCK1_CMD);
|
|
flash_cmd(c, FLASH_ADDR(0x5555), AUTOSELECT_CMD);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0)), ==, replicate(c, 0xBF));
|
|
reset(c);
|
|
|
|
/*
|
|
* Program a word on each sector, erase one or two sectors per region, and
|
|
* verify that all of those, and only those, are erased.
|
|
*/
|
|
byte_addr = 0;
|
|
for (int region = 0; region < nb_erase_regions; ++region) {
|
|
for (int i = 0; i < config->nb_blocs[region]; ++i) {
|
|
program(c, byte_addr, 0);
|
|
byte_addr += config->sector_len[region];
|
|
}
|
|
}
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, SECOND_UNLOCK_CMD);
|
|
unlock(c);
|
|
byte_addr = 0;
|
|
const uint64_t erase_cmd = replicate(c, SECTOR_ERASE_CMD);
|
|
for (int region = 0; region < nb_erase_regions; ++region) {
|
|
flash_write(c, byte_addr, erase_cmd);
|
|
if (c->nb_blocs[region] > 1) {
|
|
flash_write(c, byte_addr + c->sector_len[region], erase_cmd);
|
|
}
|
|
byte_addr += c->sector_len[region] * c->nb_blocs[region];
|
|
}
|
|
|
|
qtest_clock_step_next(c->qtest); /* Step over the 50 us timeout. */
|
|
wait_for_completion(c, 0);
|
|
byte_addr = 0;
|
|
for (int region = 0; region < nb_erase_regions; ++region) {
|
|
for (int i = 0; i < config->nb_blocs[region]; ++i) {
|
|
if (i < 2) {
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==, bank_mask(c));
|
|
} else {
|
|
g_assert_cmphex(flash_read(c, byte_addr), ==, 0);
|
|
}
|
|
byte_addr += config->sector_len[region];
|
|
}
|
|
}
|
|
|
|
/* Test erase suspend/resume during erase timeout. */
|
|
sector_erase(c, 0);
|
|
/*
|
|
* Check that DQ 3 is 0 and DQ6 and DQ2 are toggling in the sector being
|
|
* erased as well as in a sector not being erased.
|
|
*/
|
|
byte_addr = c->sector_len[0];
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq3, ==, 0);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
status0 = flash_read(c, byte_addr);
|
|
status1 = flash_read(c, byte_addr);
|
|
g_assert_cmpint(status0 & dq3, ==, 0);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
|
|
/*
|
|
* Check that after suspending, DQ6 does not toggle but DQ2 does toggle in
|
|
* an erase suspended sector but that neither toggle (we should be
|
|
* getting data) in a sector not being erased.
|
|
*/
|
|
erase_suspend(c);
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq6, ==, status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
g_assert_cmpint(flash_read(c, byte_addr), ==, flash_read(c, byte_addr));
|
|
|
|
/* Check that after resuming, DQ3 is 1 and DQ6 and DQ2 toggle. */
|
|
erase_resume(c);
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
status0 = flash_read(c, byte_addr);
|
|
status1 = flash_read(c, byte_addr);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
wait_for_completion(c, 0);
|
|
|
|
/* Repeat this process but this time suspend after the timeout. */
|
|
sector_erase(c, 0);
|
|
qtest_clock_step_next(c->qtest);
|
|
/*
|
|
* Check that DQ 3 is 1 and DQ6 and DQ2 are toggling in the sector being
|
|
* erased as well as in a sector not being erased.
|
|
*/
|
|
byte_addr = c->sector_len[0];
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
status0 = flash_read(c, byte_addr);
|
|
status1 = flash_read(c, byte_addr);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
|
|
/*
|
|
* Check that after suspending, DQ6 does not toggle but DQ2 does toggle in
|
|
* an erase suspended sector but that neither toggle (we should be
|
|
* getting data) in a sector not being erased.
|
|
*/
|
|
erase_suspend(c);
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq6, ==, status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
g_assert_cmpint(flash_read(c, byte_addr), ==, flash_read(c, byte_addr));
|
|
|
|
/* Check that after resuming, DQ3 is 1 and DQ6 and DQ2 toggle. */
|
|
erase_resume(c);
|
|
status0 = flash_read(c, 0);
|
|
status1 = flash_read(c, 0);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
status0 = flash_read(c, byte_addr);
|
|
status1 = flash_read(c, byte_addr);
|
|
g_assert_cmpint(status0 & dq3, ==, dq3);
|
|
g_assert_cmpint(status0 & dq6, ==, ~status1 & dq6);
|
|
g_assert_cmpint(status0 & dq2, ==, ~status1 & dq2);
|
|
wait_for_completion(c, 0);
|
|
|
|
qtest_quit(qtest);
|
|
}
|
|
|
|
/*
|
|
* Test that
|
|
* 1. enter autoselect mode;
|
|
* 2. enter CFI mode; and then
|
|
* 3. exit CFI mode
|
|
* leaves the flash device in autoselect mode.
|
|
*/
|
|
static void test_cfi_in_autoselect(const void *opaque)
|
|
{
|
|
const FlashConfig *config = opaque;
|
|
QTestState *qtest;
|
|
qtest = qtest_initf("-M musicpal,accel=qtest"
|
|
" -drive if=pflash,file=%s,format=raw,copy-on-read",
|
|
image_path);
|
|
FlashConfig explicit_config = expand_config_defaults(config);
|
|
explicit_config.qtest = qtest;
|
|
const FlashConfig *c = &explicit_config;
|
|
|
|
/* 1. Enter autoselect. */
|
|
unlock(c);
|
|
flash_cmd(c, UNLOCK0_ADDR, AUTOSELECT_CMD);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0)), ==, replicate(c, 0xBF));
|
|
|
|
/* 2. Enter CFI. */
|
|
flash_cmd(c, CFI_ADDR, CFI_CMD);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x10)), ==, replicate(c, 'Q'));
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x11)), ==, replicate(c, 'R'));
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0x12)), ==, replicate(c, 'Y'));
|
|
|
|
/* 3. Exit CFI. */
|
|
reset(c);
|
|
g_assert_cmphex(flash_query(c, FLASH_ADDR(0)), ==, replicate(c, 0xBF));
|
|
|
|
qtest_quit(qtest);
|
|
}
|
|
|
|
static void cleanup(void *opaque)
|
|
{
|
|
unlink(image_path);
|
|
}
|
|
|
|
/*
|
|
* XXX: Tests are limited to bank_width = 2 for now because that's what
|
|
* hw/arm/musicpal.c has.
|
|
*/
|
|
static const FlashConfig configuration[] = {
|
|
/* One x16 device. */
|
|
{
|
|
.bank_width = 2,
|
|
},
|
|
/* Nonuniform sectors (top boot). */
|
|
{
|
|
.bank_width = 2,
|
|
.nb_blocs = { 127, 1, 2, 1 },
|
|
.sector_len = { 0x10000, 0x08000, 0x02000, 0x04000 },
|
|
},
|
|
/* Nonuniform sectors (bottom boot). */
|
|
{
|
|
.bank_width = 2,
|
|
.nb_blocs = { 1, 2, 1, 127 },
|
|
.sector_len = { 0x04000, 0x02000, 0x08000, 0x10000 },
|
|
},
|
|
};
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
int fd = mkstemp(image_path);
|
|
if (fd == -1) {
|
|
g_printerr("Failed to create temporary file %s: %s\n", image_path,
|
|
strerror(errno));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
if (ftruncate(fd, UNIFORM_FLASH_SIZE) < 0) {
|
|
int error_code = errno;
|
|
close(fd);
|
|
unlink(image_path);
|
|
g_printerr("Failed to truncate file %s to %u MB: %s\n", image_path,
|
|
UNIFORM_FLASH_SIZE, strerror(error_code));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
close(fd);
|
|
|
|
qtest_add_abrt_handler(cleanup, NULL);
|
|
g_test_init(&argc, &argv, NULL);
|
|
|
|
size_t nb_configurations = sizeof configuration / sizeof configuration[0];
|
|
for (size_t i = 0; i < nb_configurations; ++i) {
|
|
const FlashConfig *config = &configuration[i];
|
|
char *path = g_strdup_printf("pflash-cfi02"
|
|
"/geometry/%dx%x-%dx%x-%dx%x-%dx%x"
|
|
"/%d",
|
|
config->nb_blocs[0],
|
|
config->sector_len[0],
|
|
config->nb_blocs[1],
|
|
config->sector_len[1],
|
|
config->nb_blocs[2],
|
|
config->sector_len[2],
|
|
config->nb_blocs[3],
|
|
config->sector_len[3],
|
|
config->bank_width);
|
|
qtest_add_data_func(path, config, test_geometry);
|
|
g_free(path);
|
|
}
|
|
|
|
qtest_add_data_func("pflash-cfi02/cfi-in-autoselect", &configuration[0],
|
|
test_cfi_in_autoselect);
|
|
int result = g_test_run();
|
|
cleanup(NULL);
|
|
return result;
|
|
}
|