Non-Volatile Memory (NVM)란 비휘발성 메모리, 즉 전원을 꺼도 다음에 다시 전원을 넣었을 때 지워지지 않는 메모리를 말한다. 대표적으로 플래시 메모리, EEPROM 등이 있는데, 펌웨어에서는 사용자의 설정이나 캘리브레이션 데이터 저장 등의 용도로 필요하다. TV의 펌웨어를 만든다고 했을 때, 전원을 켤 때마다 채널이 1번에 가있다면 얼마나 불편하겠는가. 사용자가 선택한 마지막 채널을 기억했다가 다음에 그 채널부터 시작하는 게 당연하겠다.
먼저 EEPROM으로 말하자면 가장 다루기 쉬운 NVM 장치다. AVR에는 대부분 EEPROM이 1KB 정도 있어, 데이터 저장하기가 간단했는데, ARM 코어를 사용하는 MCU들은 대부분 EEPROM을 지원하지 않는다. EEPROM은 1 바이트 단위로 쓰고 지울 수 있어 다루기 쉽고, 썼다 지웠다 할 수 있는 횟수가(Endurance) 10만 회 정도로 많다. EEPROM은 전기적으로 지우는데, EPROM 중에는 자외선으로 지우는 것도 있다. 요즘은 찾아보기 어렵지만. 어쨌든 TG11에는 EEPROM이 없다.
다음으로 Flash Memory가 있는데, 플래시 메모리는 데이터 보존 방식에 따라 NOR형, NAND형으로 나뉜다. NOR형은 쓰기와 읽기는 바이트 또는 워드 단위로 할 수 있지만, 지우기는 블록단위로만 가능하다. NAND형은 읽기/쓰기가 페이지 단위로만 가능한 대신 기록 속도가 NOR형에 비해 빠르다. NAND는 랜덤 액세스가 안되고 페이지 단위로만 읽을 수 있기 때문에 코드 영역으로 사용할 수 없고 데이터 저장만 가능하다. 그래서 USB 메모리는 NAND 플래시로 만든다.
NOR의 장점은 읽는 속도가 빠르고, 읽기에 랜덤 액세스 접근이 되므로, 코드 영역으로 사용할 수 있다는 점이다. 보통 MCU에 내장된 플래시는 거의 NOR형 플래시다. NAND 플래시에 코드를 저장하는 경우는 바로 실행할 수가 없기 때문에, 코드를 램으로 복사한 후에 실행해야 한다. 하드디스크나 마찬가지다. 그래서 SSD도 NAND 플래시로 만든다. PC나 스마트폰들은 하드디스크나 NAND 메모리를 쓰기 때문에 RAM이 많이 필요한 이유가 그 때문이다. TG11에 내장된 플래시도 NOR 플래시고 EEPROM이 따로 없기 때문에 여기다가 코드도 저장하고 데이터도 저장해야 한다.
일반적인 NOR 플래시의 구조를 보면 뱅크(bank)가 있고, 그 안에 여러 개의 섹터(Sector)가 있고, 또 각 섹터 안에 여러 개의 페이지가 존재하는 형식이다. NOR 플래시의 쓰기 기능은 바이트 또는 워드 단위로 쓸 수 있지만, BIT 1을 0으로 만들 수만 있고, 0을 1로 되돌리지는 못하기 때문에 쓰기 전에 반드시 지우는 작업을 해야 한다. 지우는 작업은 섹터 또는 페이지 단위로 이루어지며 섹터 또는 페이지 내부가 모두 BIT 1로 채워져 전부 0xFF가 된다. 1바이트를 기록하기 위해서는.. (1) 해당 바이트가 포함된 페이지 전체를 RAM으로 복사하고 (2) RAM에서 해당 바이트를 수정하고 (3) 플래시의 해당 페이지를 지우고 (4) RAM의 데이터를 플래시로 기록한다. 그냥 막 쓰던 EEPROM에 비해 복잡하고 느리고 사용하기 까다롭다.
또 하나의 문제는 플래시의 특정 페이지가 쓰기 모드로 변경되면 그 페이지가 포함된 뱅크 전체가 데이터를 읽을 수 없는 상태가 된다는 점이다. 그래서 코드와 데이터는 같은 뱅크에 배치해서는 안된다. 데이터를 쓰려고 하는 순간 코드를 읽을 수 없는 상태가 되기 때문에... 그렇지만 뱅크가 여러 개로 나뉜 경우는 몇 메가바이트씩 하는 큰 플래시의 이야기고 MCU에 내장된 조그만 플래시는 보통 전체가 하나의 뱅크로 되어 있다. 그렇다면 EEPROM도 없는데 데이터를 어디다 저장한단 말인가.
시리얼 등으로 펌웨어를 업데이트하는 경우를 생각해 보자. 요즘은 TG11처럼 별도의 부트로더가 있어 펌웨어 업데이트를 지원해주는 칩이 많지만, 예전에는 전부 직접 만들어야만 했다. 업데이트를 위해 플래시를 지워버리면 프로그램이 없어지는데, 어떻게 시리얼로 새 펌웨어를 받아와 어떻게 기록하겠는가.
이 둘은 각각 데이터와 코드를 플래시에 쓰는 문제이며 처리 방법은 동일하다. 핵심은 코드를 램에 복사하고 램에서 실행한다는 것이다. 말은 간단해도 구현은 간단하지 않다. 그냥 막 복사한다고 다 되는 것이 아니다. 컴파일된 코드에 포함된 메모리 어드레스들이 플래시 영역을 가리키고 있어, 점프하는 순간 망한다. 그래서 컴파일하고 링크할 때부터 이 함수는 램의 어느 부분에서 실행되는 함수라고 지정한 후 컴파일하고, 프로그램 시작 시 또는 그 함수를 사용하기 전에 해당 코드를 램으로 복사해야 한다. 링크 시에 참조되는 scatter 파일에서 처리하는데, 링커마다 사용법이 조금씩 다르다.
플래시 관련 라이브러리 및 함수는 모두 램으로 옮겨야 하고, 시리얼로 받아와서 기록하는 경우 시리얼 관련 함수들도 옮겨야 한다. 작업 중에 인터럽트가 걸리지 않게 막아야 하고, 필요한 인터럽트 라면 그 인터럽트 핸들러와 핸들러가 호출하는 함수들도 모두 램으로 옮겨야 한다. 정말 만만치 않은 작업이다.
다행히도 TG11은 이러한 과정을 쉽게 처리할 수 있도록 라이브러리 및 환경을 잘 만들어 놓았다. EEPROM도 없고 데이터 기록을 위한 전용 뱅크도 준비해 두지 않았으니 당연하다고 해야 할까. MSC 드라이버가 이를 지원한다.
먼저 "EM_MSC_RUN_FROM_RAM"을 define 해야 한다. 그러면 램으로 복사가 필요한 함수들은 자동으로 램으로 배치되고, 복사하는 루틴도 알아서 수행된다. 내가 만드는 함수 중에 램으로 올려야 하는 함수는 MSC_RAMFUNC_DEFINITION_BEGIN과 MSC_RAMFUNC_DEFINITION_END 매크로로 둘러싸면 된다.
#include <em_msc.h>
#define NVM_POSIT__CHANNEL (0x10000UL - 2048) // 64k flash 기준
MSC_RAMFUNC_DEFINITION_BEGIN
void nvm_update_storage__tv_channel(uint16_t ch)
{
MSC_ErasePage(NVM_CHANNEL_I);
MSC_WriteWord(NVM_CHANNEL_I, (void *)&ch, 1);
}
MSC_RAMFUNC_DEFINITION_END
저렇게만 하면 된다. 그러면 해당 함수는 자동으로 RAM에 올라가고 RAM에서 실행된다. 이 예제에서는 64KB 플래시의 마지막 페이지에 데이터를 저장하고 있는데, 메모리 맵을 보면 0x0FE00000 번지에 데이터 저장용으로 할당된 페이지가 하나 있다. 쓰라고 해 놓았으니, 그걸 사용하는 것을 추천드리며, 따로 동떨어져 있는 것으로 보아 뱅크가 다를지도 모른다. 즉, 굳이 코드를 램에 올려 실행 안 해도 괜찮을 수 있다. 이번에 진행한 프로젝트는 데이터 저장용으로 3페이지를 사용해 어차피 램에 올려야 하기에 따로 테스트해보지 않았는데, 이 부분은 조만간 테스트해 본 후에 본문을 수정하기로 하겠다. (당황스러운 테스트 결과는 본문 마지막에 따로 추가했습니다.)
#if defined(_EFM32_GECKO_FAMILY) \
|| defined(_SILICON_LABS_32B_SERIES_2) \
|| defined(EM_MSC_RUN_FROM_RAM)
#define MSC_RAMFUNC_DECLARATOR SL_RAMFUNC_DECLARATOR
#define MSC_RAMFUNC_DEFINITION_BEGIN SL_RAMFUNC_DEFINITION_BEGIN
#define MSC_RAMFUNC_DEFINITION_END SL_RAMFUNC_DEFINITION_END
#else
#define MSC_RAMFUNC_DECLARATOR
#define MSC_RAMFUNC_DEFINITION_BEGIN
#define MSC_RAMFUNC_DEFINITION_END
#endif
"em_msc.h" 파일을 보면 위와 같은 부분이 있는데, "_EFM32_GECKO_FAMILY", "_SILICON_LABS_32B_SERIES_2", "EM_MSC_RUN_FROM_RAM" 셋 중에 하나는 define이 되어 있어야 한다. TG11은 Tiny Gecko 시리즈니 당연히 _EFM32_GECKO_FAMILY가 define 되어 있겠지..라고 생각하면 오산이다. "gecko_sdk_xxx" 폴더 아래를 뒤져보면 device 정보를 정의해 놓은 파일이 있는데, Start Kit를 쓴다면 "efm32tg11b540f64gq48.h" 파일이다. 아무튼 사용하는 칩의 이름으로 된 헤더 파일을 참조해보면...
/** Part family */
#define _EFM32_TINY_FAMILY 1 /**< Tiny Gecko MCU Family */
#define _EFM_DEVICE /**< Silicon Labs EFM-type MCU */
#define _SILICON_LABS_32B_SERIES_1 /**< Silicon Labs series number */
#define _SILICON_LABS_32B_SERIES 1 /**< Silicon Labs series number */
#define _SILICON_LABS_32B_SERIES_1_CONFIG_1 /**< Series 1, Configuration 1 */
#define _SILICON_LABS_32B_SERIES_1_CONFIG 1 /**< Series 1, Configuration 1 */
#define _SILICON_LABS_GECKO_INTERNAL_SDID_103 /**< Silicon Labs internal use only, may change any time */
#define _SILICON_LABS_GECKO_INTERNAL_SDID 103 /**< Silicon Labs internal use only, may change any time */
#define _SILICON_LABS_32B_PLATFORM_2 /**< Silicon Labs platform name */
#define _SILICON_LABS_32B_PLATFORM 2 /**< Silicon Labs platform name */
#define _SILICON_LABS_32B_PLATFORM_2_GEN_1 /**< @deprecated Platform 2, generation 1 */
#define _SILICON_LABS_32B_PLATFORM_2_GEN 1 /**< @deprecated Platform 2, generation 1 */
TG11은 "_EFM32_GECKO_FAMILY"가 아닌 "_EFM32_TINY_FAMILY"로 되어 있다. 원래 다들 그런 건지 TG11만 왕따 당하는 건지는 다른 Gecko를 써보지 않아 모르지만 "_EFM32_GECKO_FAMILY"는 선언되어 있지 않고, "_SILICON_LABS_32B_SERIES_2"는 해당 사항이 없으니 직접 "EM_MSC_RUN_FROM_RAM"을 선언해 줘야 한다.
"em_msc.h" 파일의 위쪽에 직접 기입하는 방법은 절대 추천하지 않는다. 코드를 복사해 왔다면 공유하는 라이브러리가 아닌 이 프로젝트 전용으로 쓰는 헤더 파일이겠지만, 그래도 그렇게는 사용하지 마시라. SDK와 자동생성 파일은 함부로 수정하는 게 아니다.
"EM_MSC_RUN_FROM_RAM"을 define 하고 "em_msc.h"가 그것을 참조하도록 하는 방법은 두 가지가 있다. 컴파일 옵션의 "-D"를 사용하는 방법이 그중 하나다. "-DEM_MSC_RUN_FROM_RAM" 혹은 "-DEM_MSC_RUN_FROM_RAM=1"을 넣으면 되는데, 직접 넣는 게 아니라 프로젝트 설정의 "Proprocessor"의 "Define Symbols"에 넣어주면 적용된다.
또 하나의 방법은 헤더 파일을 만들어 그 안에 define 하고, 만든 헤더 파일을 프로젝트 내의 모든 파일에 강제로 include 시키는 방법이다. 필자의 경우 이 방법이 관리가 편하고, 다음 프로젝트 진행 시 이식성도 좋아 주로 사용하는 편이다. 컴파일 옵션의 "-include" 혹은 "--include"로 사용하며 역시 직접 넣는 게 아니고, 프로젝트 설정 "Includes"의 "Include files"에 헤더 파일을 등록하면 된다.
프로젝트를 많이 진행하다 보면 자신만의 라이브러리 같은 게 쌓이는데, 매번 바뀌는 하드웨어에 맞춰 수정해야 한다. 그러다 보면 편한 적용 방법을 찾게 되고 대부분 #if, #ifdef 따위의 preprocessor 지시자를 이용하는 방법이라, 나만의 설정들을 define 하는 헤더 파일 하나쯤은 만들게 된다. 필자의 경우 __platform.h라는 헤더 파일은 무조건 강제 include 시키고, 그 안에서 __config.h, __target.h 등 나름의 구분 방법에 따른 헤더 파일들을 include 하고 있다. "EM_MSC_RUN_FROM_RAM" 같은 경우는 __config.h 안에 선언해 두었다.
자, 이렇게 간단하지만 꼭 필요한 데이터 저장 방식에 대해 알아보았다. TG11의 플래시는 썼다 지웠다를 10,000회 보장한다. 하루에 1번씩 기록한다면 30년 정도는 보장된다. TV로 돌아가 보자. 채널 데이터를 언제 기록할 것인가? 채널을 바꿀 때마다 기록한다면 하루에 100번 이상 기록해야 할지도 모른다. 100일이면 끝장난다. 100,000번 보장되는 SLC 플래시나 EEPROM이라도 3년밖에 못쓴다. 그래서 기록이 빈번한 경우에는 여러 가지 기법이 필요하다. 플래시에 기록하되 기록 횟수까지 같이 기록하고 그 횟수가 일정 횟수에 도달하면 저장하는 페이지를 다른 곳으로 옮기는 방법이 있겠고. 한번 페이지를 지운 후 기록은 순차적으로 하는 방법도 있다. 채널 정보가 2바이트라고 하면 2KB의 페이지에 처음엔 옵셋 0x0000번지에 채널을 기록하고 다음에는 0x0002번지, 그다음엔 0x0004번지에 기록하고,... 1024번 기록해서 더 이상 자리가 없으면 페이지를 지우고 0x0000번지부터 다시 기록한다. 읽어올 때는 0xFFFF가 나타나기 직전의 2바이트가 마지막 기록이므로 그걸 읽어온다. 1000배 더 오래 쓸 수 있게 되었다.
채널을 바꿀 때마다 기록하지 않고 전원을 끌 때 기록하는 방법도 있다. 이 경우 코드를 뽑는 경우와 같이 갑자기 전원이 차단되면 데이터를 기록하지 못한다. 또는 그냥 SRAM에 두고 전원을 꺼도 PC의 절전모드처럼 SRAM만 살려두는 방법을 쓸 수도 있겠다. MCU의 경우는 꺼지는 척만 하고 Deep Sleep모드에 들어간다든지... 하지만 이 경우도 코드를 뽑으면 다 날아간다. 선택과 고민은 여러분의 몫이다.
테스트 결과 |
전혀 예상치 못했던 결과가 나왔습니다. TG11 시리즈는 굳이 관련 함수들을 램에 올릴 필요가 없다는 점입니다. (어쩐지 기본적으로 설정이 안되어 있더라니...). User Data 영역은 물론이고 메인 플래시 영역까지 막 지우고 막 써도 됩니다. 코드를 덮어쓰면 안되는건 당연하지만요. 쓰기 모드에 진입해도 플래시 상태가 변하지 않길래 레퍼런스를 다시 읽어보니 • During a write or erase, flash read accesses will be stalled, effectively halting code execution from flash. Code execution continues upon write/erase completion. Code residing in RAM may be executed during a write/erase operation. 라고 해놓았네요, 지우기/쓰기 동작시에는 비정상 데이터가 읽히는게 아니라 아예 읽을 수가 없도록 되어 있습니다. 읽기 요청이 들어오면 시간을 끌다가 지우기/쓰기가 끝나면 읽어서 돌려준다는 건데요. 저도 이런 경우는 처음이라... 결국 펌웨어 업데이트를 직접 하거나, 데이터를 지우기/쓰기 중에도 인터럽트 등의 작업을 지연없이 계속해야하는 경우가 아니면 "EM_MSC_RUN_FROM_RAM" 선언을 할 필요가 없겠습니다. |
'EFM32 예제로 배워보기' 카테고리의 다른 글
[장치.05] ADC - Scan Mode (0) | 2022.05.07 |
---|---|
[장치.04] ADC - Single Mode (0) | 2022.05.04 |
[기본.04] 모델명 (Part Number) 읽는 법과 Errata (0) | 2022.04.28 |
[기본.03] Cortex-M0+의 NVIC 시스템 (0) | 2022.04.25 |
[장치.02] Periodic Timer와 PRS (0) | 2022.04.24 |