跳到正文
Qiita · Unity 开发社区· 来源· hk816·· 4 小时前AI 评分1

基于 Unity 框架的 SSD ECC 多段错误恢复测试设计

SSDのデータを死守せよ!「ECC多段エラーリカバリ」のテスト設計(Unity編)

正文 · 原文

0

Go to list of users who liked

0

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🚀 メモリコントローラのエラー訂正・データ信頼性向上(ECC)機能

組み込みソフトウェアにおいて、ハードウェアの限界をソフトウェアでカバーする「エラーリカバリ」のコードは、最もバグが潜みやすく、かつ最もテストが難しい領域です。
特にSSDのECC(エラー訂正)機能は、「最初は読めなかったが、読み出し電圧を変えたら読めた」「訂正限界ギリギリだから別ブロックへ引っ越す」といった複雑な状態遷移を伴います。

実機でこの「ギリギリのエラー状態」を意図的に作り出すのは至難の業ですが、Unity(単体テストフレームワーク)とモックを駆使すれば、PC上でハードウェアの極限状態を100%シミュレートできます。

今回は、前回の「ECC・多段エラーリカバリ」のC言語実装に対し、エンタープライズ品質を証明するためのテストコードを公開します!


💡 テスト設計のポイント

エラーリカバリのテストでは、正常系はもちろんのこと、ハードウェアが「最悪の振る舞い」をした際にファームウェアがどう持ち堪えるか(フェイルソフト)の証明が求められます。

  1. 閾値の境界値テスト(Scrubbing発動の証明)
    エラー訂正(CORRECTED)が成功した場合でも、訂正ビット数が「閾値未満(例: 39bit)」なら通常終了し、「閾値ジャスト(40bit)」なら寿命のサインとして予防保全(Scrubbing)が発動することを厳密に検証します。
  2. Read Retryの状態遷移と副作用の排除(正常系/異常系)
    「最初はUNC(訂正不可)、2回目のリトライで成功」というシナリオをモックに注入します。この時、正しくループを抜けることだけでなく、「次のReadに悪影響を与えないよう、確実にハードウェアの電圧(Vth)設定が0(デフォルト)にリセットされているか」をコール履歴から証明します。
  3. 最終防衛線(RAIN)とフェイルソフトの検証(異常系)
    リトライ上限(5回)をすべて失敗し、NAND間パリティ(RAIN)にフォールバックするシナリオを検証します。RAINで復元できた場合は「GBB(後発不良)登録」が確実に行われること、RAINすら失敗した場合はハングアップ(無限ループ)せずに安全に false を返して終了することを証明します。

💻 テストコード

#include "unity.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

/* テスト対象のCファイルを直接インクルード(ホワイトボックステスト用) */
#include "ftl_ecc_recovery.c"

/* --- モック用制御・状態記録変数 --- */
#define MOCK_MAX_READS 10U
static ECC_Result_t g_mock_ecc_seq[MOCK_MAX_READS];
static uint32_t     g_mock_ecc_idx = 0U;

static uint32_t g_call_read = 0U;
static uint32_t g_call_set_retry = 0U;
static uint32_t g_last_retry_level = 0xFFFFFFFFU; /* 初期値(無効値) */

static uint32_t g_call_scrub = 0U;
static uint32_t g_call_gbb = 0U;
static uint32_t g_call_rain = 0U;
static bool     g_mock_rain_success = true;

/* --- 外部HAL/FW関数のダミー実装 (モック) --- */
ECC_Result_t HAL_NandReadPageWithEcc(uint32_t pba, uint8_t* p_buffer) {
    g_call_read++;
    ECC_Result_t res = g_mock_ecc_seq[g_mock_ecc_idx];
    if (g_mock_ecc_idx < (MOCK_MAX_READS - 1U)) {
        g_mock_ecc_idx++;
    }
    return res;
}

void HAL_SetReadRetryLevel(uint32_t retry_index) {
    g_call_set_retry++;
    g_last_retry_level = retry_index;
}

void FTL_TriggerScrubbing(uint32_t pba) {
    g_call_scrub++;
}

bool FTL_RecoverByRain(uint32_t pba, uint8_t* p_buffer) {
    g_call_rain++;
    return g_mock_rain_success;
}

void FTL_MarkBlockAsGbb(uint32_t pba) {
    g_call_gbb++;
}

/* --- テストのセットアップ --- */
void setUp(void) {
    g_mock_ecc_idx = 0U;
    g_call_read = 0U;
    g_call_set_retry = 0U;
    g_last_retry_level = 0xFFFFFFFFU;
    g_call_scrub = 0U;
    g_call_gbb = 0U;
    g_call_rain = 0U;
    g_mock_rain_success = true;
    
    memset(g_mock_ecc_seq, 0, sizeof(g_mock_ecc_seq));
}
void tearDown(void) {}

/* =========================================================
 *  テストケース
 * ========================================================= */

/* 1. 異常系: NULLポインタ入力によるガード検証 */
void test_FTL_RobustReadWithEcc_NullPointer_ShouldReturnFalse(void) {
    bool result = FTL_RobustReadWithEcc(100U, NULL);
    TEST_ASSERT_FALSE_MESSAGE(result, "バッファポインタがNULLの場合はfalseを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_read, "HWへのReadアクセスを行わないこと");
}

/* 2. 正常系: エラーなし(SUCCESS)の一発読み出し */
void test_FTL_RobustReadWithEcc_Success_ShouldReturnTrue(void) {
    uint8_t buffer[4096];
    g_mock_ecc_seq[0].status = FTL_ECC_SUCCESS;
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_TRUE_MESSAGE(result, "一発成功時はtrueを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_read, "Readは1度だけ呼ばれること");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_scrub, "Scrubbing(退避)は発動しないこと");
}

/* 3. 境界値: 訂正ビット数が閾値未満 (Scrub発動せず) */
void test_FTL_RobustReadWithEcc_CorrectedBelowThreshold_ShouldNotScrub(void) {
    uint8_t buffer[4096];
    g_mock_ecc_seq[0].status = FTL_ECC_CORRECTED;
    g_mock_ecc_seq[0].corrected_bits = FTL_SCRUB_THRESHOLD_BITS - 1U; /* 39ビット(閾値未満) */
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_TRUE_MESSAGE(result, "訂正成功なのでtrueを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_scrub, "閾値未満の劣化ではまだデータ退避(Scrub)を行わないこと");
}

/* 4. 境界値: 訂正ビット数が閾値到達 (Scrub発動の証明) */
void test_FTL_RobustReadWithEcc_CorrectedAtThreshold_ShouldTriggerScrub(void) {
    uint8_t buffer[4096];
    g_mock_ecc_seq[0].status = FTL_ECC_CORRECTED;
    g_mock_ecc_seq[0].corrected_bits = FTL_SCRUB_THRESHOLD_BITS; /* 40ビット(閾値到達) */
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_TRUE_MESSAGE(result, "訂正成功なのでtrueを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_scrub, "閾値到達時は寿命のサインと見なし、Scrubbingを発動すること");
}

/* 5. 状態遷移: Read Retry 成功と副作用(Vthリセット)の検証 */
void test_FTL_RobustReadWithEcc_RetrySuccess_ShouldScrubAndResetVth(void) {
    uint8_t buffer[4096];
    
    /* 1回目(通常)、2回目(Retry=1)はUNC、3回目(Retry=2)でSUCCESSするシナリオ */
    g_mock_ecc_seq[0].status = FTL_ECC_UNCORRECTABLE;
    g_mock_ecc_seq[1].status = FTL_ECC_UNCORRECTABLE;
    g_mock_ecc_seq[2].status = FTL_ECC_SUCCESS;
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_TRUE_MESSAGE(result, "Retryで救出できた場合はtrueを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(3U, g_call_read, "Read関数が計3回呼ばれること");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_scrub, "Retryが発動したブロックは劣化が確定しているため、必ずScrubすること");
    
    /* 【セキュリティ・副作用検証】 */
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_last_retry_level, "Retryループを抜けた後、次のReadを壊さないようVthが必ず0(リセット)されること");
}

/* 6. 異常系: RAINによる最終復元と不良ブロック(GBB)登録 */
void test_FTL_RobustReadWithEcc_RetryExhausted_RainSuccess_ShouldMarkGbb(void) {
    uint8_t buffer[4096];
    
    /* 全てのRead(初回 + リトライ最大5回 = 計6回)がUNCになるシナリオ */
    for (uint32_t i = 0U; i <= FTL_MAX_READ_RETRIES; i++) {
        g_mock_ecc_seq[i].status = FTL_ECC_UNCORRECTABLE;
    }
    g_mock_rain_success = true; /* RAIN(パリティ復元)は成功する */
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_TRUE_MESSAGE(result, "RAINで救出できた場合はtrueを返すこと");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U + FTL_MAX_READ_RETRIES, g_call_read, "最大リトライ回数まで全て試行されること");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_rain, "Retry限界到達後にRAINが発動すること");
    
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_scrub, "RAIN復旧ブロックのデータは即時退避すること");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(1U, g_call_gbb, "RAINまで発動したブロックは完全破損と見なし、GBB登録すること");
}

/* 7. 異常系: RAINすら失敗する完全破損(Unrecovered Error)時のフェイルソフト検証 */
void test_FTL_RobustReadWithEcc_AllFails_ShouldReturnFalse(void) {
    uint8_t buffer[4096];
    
    for (uint32_t i = 0U; i <= FTL_MAX_READ_RETRIES; i++) {
        g_mock_ecc_seq[i].status = FTL_ECC_UNCORRECTABLE;
    }
    g_mock_rain_success = false; /* RAINも失敗(論理的にも復元不可能) */
    
    bool result = FTL_RobustReadWithEcc(100U, buffer);
    
    TEST_ASSERT_FALSE_MESSAGE(result, "全てのリカバリ手段が失敗した場合は、ハングアップせず安全にfalseを返すこと(フェイルソフト)");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_scrub, "復元に失敗したゴミデータを退避(Scrub)してはならない");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0U, g_call_gbb, "データが失われたため、通常のGBB登録シーケンスではなく上位エラーハンドラへ委譲すること");
}

0

Go to list of users who liked

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme

What you can do with signing up

0

Go to list of users who liked

0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

来源:Qiita · Unity 开发社区 · qiita.com