🚀 メモリコントローラのエラー訂正・データ信頼性向上(ECC)機能
組み込みソフトウェアにおいて、ハードウェアの限界をソフトウェアでカバーする「エラーリカバリ」のコードは、最もバグが潜みやすく、かつ最もテストが難しい領域です。
特にSSDのECC(エラー訂正)機能は、「最初は読めなかったが、読み出し電圧を変えたら読めた」「訂正限界ギリギリだから別ブロックへ引っ越す」といった複雑な状態遷移を伴います。
実機でこの「ギリギリのエラー状態」を意図的に作り出すのは至難の業ですが、Unity(単体テストフレームワーク)とモックを駆使すれば、PC上でハードウェアの極限状態を100%シミュレートできます。
今回は、前回の「ECC・多段エラーリカバリ」のC言語実装に対し、エンタープライズ品質を証明するためのテストコードを公開します!
💡 テスト設計のポイント
エラーリカバリのテストでは、正常系はもちろんのこと、ハードウェアが「最悪の振る舞い」をした際にファームウェアがどう持ち堪えるか(フェイルソフト)の証明が求められます。
-
閾値の境界値テスト(Scrubbing発動の証明)
エラー訂正(CORRECTED)が成功した場合でも、訂正ビット数が「閾値未満(例: 39bit)」なら通常終了し、「閾値ジャスト(40bit)」なら寿命のサインとして予防保全(Scrubbing)が発動することを厳密に検証します。 -
Read Retryの状態遷移と副作用の排除(正常系/異常系)
「最初はUNC(訂正不可)、2回目のリトライで成功」というシナリオをモックに注入します。この時、正しくループを抜けることだけでなく、「次のReadに悪影響を与えないよう、確実にハードウェアの電圧(Vth)設定が0(デフォルト)にリセットされているか」をコール履歴から証明します。 -
最終防衛線(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登録シーケンスではなく上位エラーハンドラへ委譲すること");
}