내용 요약
문제: 시스템 전원 차단(Power-off) 또는 정전 발생 시 비휘발성 메모리(EEPROM/Flash)에 저장 중이던 설정 데이터가 깨지거나 배드 섹터 발생.
원인: 물리적인 Flash Memory는 지우기/쓰기 횟수 한계(Endurance Cycles)가 존재, 전원 차단 시 원자적 쓰기(Atomic Write)가 보장되지 않아 데이터 비트가 비정상 매팅됨.
해결: 듀얼 뱅크 구조의 백업 버퍼링 및 데이터 검증 알고리즘(CRC-16)을 적용 및 소프트웨어 Wear-Leveling 적용 섹터 가용 수명 균등 배분.
비휘발성 메모리 데이터 무결성(Data Integrity) 유실 및 수명 고갈 증상
임베디드 시스템 설계 시 장치의 설정값, 로그, 캘리브레이션 데이터를 저장하기 위해 EEPROM이나 MCU 내부의 Data Flash 영역을 활용합니다. 양산 후 장치에서 예기치 않은 타이밍에 시스템 전원이 차단(Power-down)되거나 배터리가 분리되는 전력 손실(Brown-out) 상황이 발생할 경우, 저장해 둔 메모리 블록 전체가 0x00 또는 0xFF로 초기화되거나 심볼이 깨지는 데이터 오염(Data Corruption) 현상이 보고되기도 합니다.
더불어, 매 루프마다 고정된 특정 섹터의 물리 주소에 데이터를 무조건 덮어쓰는 구조로 펌웨어를 빌드하면, 해당 블록의 지우기/쓰기 한계 수명(Endurance Life Cycle)을 순식간에 초과하게 됩니다. 이 경우 컴파일러나 HAL 라이브러리 상에서 에러 플래그를 반환하지 않더라도, 실제 물리 셀 구조가 파괴되어 하드웨어적인 Flash Program Error 또는 Erase Error를 유발하고 결국 시스템이 정지하는 락업 사태로 이어집니다.
플래시 메모리(Flash Memory) 물리 아키텍처와 Endurance 한계의 원인 분석
Flash Memory는 물리적으로 트랜지스터 내 플로팅 게이트(Floating Gate)에 전자를 가두거나 방출하는 방식으로 데이터를 기록합니다. 구조적 특성상 1에서 0으로 바꾸는 쓰기 연산(Program)은 바이트 또는 워드 단위로 가능하지만, 0에서 1로 상태를 되돌리는 연산(Erase)은 반드시 섹터(Sector) 또는 블록(Block) 단위로만 수행되어야 합니다.
대부분의 MCU 내장 플래시 메모리는 섹터당 약 10,000회에서 100,000회의 지우기(Max Erase Cycles) 제한을 가집니다. 만약 매번 같은 주소 공간에 쓰기 작업을 반복하면 다음과 같은 결함이 발생합니다.
- Oxide Degradation (산화막 마모): 반복적인 고전압 인가로 인해 플로팅 게이트를 감싸고 있는 절연 산화막이 마모되어 전자를 가두는 능력을 상실합니다. 이는 장치 전원이 차단되었을 때 전하가 누설되어 데이터가 지워지는 현상(Data Retention Failure)을 유발합니다.
- Non-Atomic Write (비원자적 기록): 데이터를 지우고 새로 쓰는 도중 전원이 차단되면 물리 레벨에서의 상태 바인딩이 미완성 상태로 남아, 다음 부팅 시점에 HardFault나 유효하지 않은 메모리 참조 규격 오류를 발생시킵니다.
따라서 하드웨어 섹터를 순차적으로 순환하며 기록하는 Wear-Leveling(웨어 레벨링) 기법과 전원 차단 직전을 감안한 방어적 백업 시퀀스 설계가 필수적입니다.
데이터 왜곡 및 수명 단축을 유발하는 잘못된 플래시 제어 C 코드 예시 (Bad Case)
아래 소스코드는 웨어 레벨링과 백업 메커니즘을 배제한 채, 고정된 물리 주소에 매번 지우기(Erase)와 쓰기(Program)를 직접 연산하여 하드웨어 수명을 단축 시키고 전원 차단 시 무결성을 파괴하는 전형적인 구조입니다.
#include "stm32f4xx_hal.h"
#define FLASH_TARGET_ADDRESS 0x08060000 // Fixed physical flash memory address
#define DATA_SIZE 4
uint32_t g_settings_data[DATA_SIZE] = {0x01, 0x02, 0x03, 0x04};
/* BAD CASE: High risk of hardware wearout and data loss during power failure */
void Save_System_Settings_Bad(void)
{
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t SectorError = 0;
HAL_FLASH_Unlock();
/* 1. Erase target sector every time before write - Accelerates physical wearout */
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;
EraseInitStruct.Sector = FLASH_SECTOR_7;
EraseInitStruct.NbSectors = 1;
if (HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK)
{
/* Error Handling */
HAL_FLASH_Lock();
return;
}
/* 2. Direct sequential write without verification or backup buffers */
for (int i = 0; i < DATA_SIZE; i++)
{
// If power is cut here, previously erased data is completely lost
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_TARGET_ADDRESS + (i * 4), g_settings_data[i]);
}
HAL_FLASH_Lock();
}
가용 수명 연장 및 무결성을 보장하는 방어적 웨어 레벨링 및 백업 C 코드 (Good Case)
문제를 해결하기 위해서는 데이터를 순차적으로 채워나가는 가상 포인터 기반의 Wear-Leveling 아키텍처를 도입하고, 데이터 팩 뒤에 CRC-16 체크섬과 카운터 마커를 매핑하여 전원이 끊겨도 이전 상태의 유효 데이터(Valid Data)를 원자적으로 복구할 수 있도록 격리해야 합니다.
#include "stm32f4xx_hal.h"
#define SECTOR_START_ADDR 0x08060000
#define SECTOR_SIZE 0x20000 // 128KB Sector size example
#define SLOT_SIZE 32 // 4 bytes Header + 16 bytes Data + 4 bytes CRC + 8 bytes Padding
#define MAX_SLOTS (SECTOR_SIZE / SLOT_SIZE)
typedef struct {
uint32_t sequence_id; // Incremental counter for Wear-Leveling tracking
uint32_t data[4]; // Actual configuration payload
uint32_t crc32; // Integrity verification checksum
} FlashSlotTypeDef;
/* Simple software checksum for flash data block validation */
uint32_t Calculate_Block_CRC32(uint32_t *pData, uint32_t length)
{
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < length; i++) {
crc ^= pData[i];
for (int j = 0; j < 32; j++) {
if (crc & 1) crc = (crc >> 1) ^ 0xEDB88320;
else crc >>= 1;
}
}
return crc;
}
/* GOOD CASE: Pure software Wear-Leveling implementation with data integrity check */
void Save_System_Settings_Good(uint32_t *p_new_data)
{
FlashSlotTypeDef current_slot;
uint32_t target_address = SECTOR_START_ADDR;
uint32_t max_seq = 0;
uint32_t next_slot_idx = 0;
HAL_FLASH_Unlock();
/* 1. Scan memory window to find the active slot and locate the next empty slot */
for (uint32_t i = 0; i < MAX_SLOTS; i++)
{
uint32_t addr = SECTOR_START_ADDR + (i * SLOT_SIZE);
uint32_t seq = *(volatile uint32_t*)addr;
if (seq == 0xFFFFFFFF) // Empty slot found
{
if (next_slot_idx == 0) next_slot_idx = i;
break;
}
else if (seq > max_seq)
{
max_seq = seq;
next_slot_idx = i + 1; // Mark next index
}
}
/* 2. Check sector overflow - Trigger Erase only when all slots are exhausted */
if (next_slot_idx >= MAX_SLOTS)
{
FLASH_EraseInitTypeDef EraseInitStruct;
uint32_t SectorError = 0;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS;
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3;
EraseInitStruct.Sector = FLASH_SECTOR_7;
EraseInitStruct.NbSectors = 1;
if (HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) == HAL_OK) {
next_slot_idx = 0;
} else {
HAL_FLASH_Lock();
return;
}
}
/* 3. Prepare structured payload with incremental metadata tracking */
current_slot.sequence_id = max_seq + 1;
for(int i = 0; i < 4; i++) {
current_slot.data[i] = p_new_data[i];
}
current_slot.crc32 = Calculate_Block_CRC32(current_slot.data, 4);
/* 4. Sequential allocation to divide physical cell stress evenly */
target_address = SECTOR_START_ADDR + (next_slot_idx * SLOT_SIZE);
uint32_t *p_payload = (uint32_t*)¤t_slot;
for (uint32_t i = 0; i < (sizeof(FlashSlotTypeDef) / 4); i++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, target_address + (i * 4), p_payload[i]);
}
HAL_FLASH_Lock();
}
핵심 수정 포인트 설명
- 순차 할당 구조를 통한 물리 스트레스 분산 (Wear-Leveling): 고정된 주소에 덮어쓰지 않고, 섹터 내 빈 슬롯(0xFFFFFFFF)을 찾아 순차적으로 데이터를 기록하므로 단일 주소 집중 마모 현상이 해소됩니다.
- 지우기 주기(Erase Cycle) 최소화 격리: 매회 플래시 메모리를 지우지 않고, 지정된 가용 슬롯 영역이 꽉 찬 순간(Sector Overflow)에만 물리 지우기를 1회 수행하여 하드웨어 가용 수명을 극대화합니다.
- 시퀀스 ID 및 CRC32 데이터 검증 동기화: 구조체 내부에 순차 증가 카운터(sequence_id)와 무결성 검증값(crc32)을 함께 매핑하여, 기록 도중 전원이 차단되어 손상된 슬롯은 스킵하고 직전 부팅 루프의 안전한 데이터를 복구해 냅니다.
플래시 수명 고갈 및 데이터 유실 디버깅 가이드 (Debugging Tips)
- SFR(Special Function Register) 플래시 에러 플래그 확인: 플래시 기록 명령 수행 직후 디버거(ST-LINK, J-Link)의 레지스터 윈도우 뷰어를 활성화하여 FLASH_SR 레지스터를 추적하십시오. WRPERR(Write Protection Error) 또는 PGAERR(Programming Alignment Error) 비트가 셋되었다면 링커 마운트 경계선 정렬 버그이거나 하드웨어 물리 마모 상태입니다.
- 메모리 윈도우 뷰어(Memory Window Viewer) 바이트 스캔: 기록 연산 직전과 직후에 타겟 물리 메모리 주소 영역을 강제 모니터링하십시오. 전체 비트가 정상적으로 기록되었는지, 혹은 비원자적 결합으로 인해 불완전한 상태 비트(0x80, 0x00 등이 뒤섞인 상태)로 마감되었는지 실시간으로 크로스 체킹해야 합니다.
- PVD (Programmable Voltage Detector) 인터럽트 연동 계측: 전원 관리 모듈 레지스터를 설정하여 전압 강하가 시작되는 시점에 내부 PVD 인터럽트 벡터를 활성화하십시오. 전압이 임계점 이하로 완전히 떨어지기 전, 잔류 커패시터 전력 공급 시간(수 ms)을 확보하여 동작 중인 플래시 쓰기 루프를 정지시키고 현재 구동 중인 원자적 슬롯까지만 안전 바인딩 마감 처리를 유도할 수 있습니다.
'Firmware & RTOS > Troubleshooting' 카테고리의 다른 글
| [MCU Flash] 플래시 메모리 데이터 깨짐 원인: Erase-before-Write 시퀀스 누락 (0) | 2026.06.25 |
|---|---|
| [임베디드 C] UART/SPI 노이즈 데이터 왜곡을 방지하는 C언어 CRC-16-CCITT 알고리즘 검증 (0) | 2026.06.24 |
| [통신 데이터] 내비게이션/센서 연동 시 빅엔디안 vs 리틀엔디안 바이트 스와핑(C언어) (가장 추천) (0) | 2026.06.22 |
| [MCU 저전력] Deep Sleep 웨이크업(Wake-up) 직후 클록(HSE/PLL) 안정화 대기 루프 구현 (0) | 2026.06.21 |
| [임베디드 저전력] Sleep/Stop 모드 진입 시 전류 소모 안 줄어드는 원인 (Pending Flag 클리어) (0) | 2026.06.20 |