MCU로 뭔가 만들다 보면 전원이 나가도 설정 값이 영구적으로 저장되면 좋겠다 싶은 경우가 있다. 그럴때는 외부 EEPROM 같은 걸 사용 할 수도 있겠지만, 내장 Flash 메모리 용량이 넉넉하면 여기를 활용 할 수 있다.

STM32L4 Flash Memory

  • STM32L432에는 256kb의 Flash Memory가 달려있다.
  • 256kb는 2kb 크기의 128개 Page들로 쪼개져 있다.
  • 저장 하려는 위치는 “Erase”가 되있어야 한다. “Erase” 작업은 Page단위로 진행 되기 때문에 2kb가 다 지워 지게 된다.
  • 한번에 최소 64 Bit를 기록해야 한다.

Flash 주소 Linker Script

Flash Memory에 뭔가를 저장 하려면 먼저 해당 Page를 다 날려야 하기 때문에 잘 못 하면 Code나 Data영역이 날아가 버려서 프로그램이 뻗어 버릴 수 있다. 그런 문제를 방지하기 위해서 저장 전용으로 사용할 Page를 확보 해야 한다. 귀찮으면 대충 마지막 페이지인 127번 Page를 사용하면 된다. 게으른 방법 같지만 사실 이 방법도 상당히 괜찮은 방법이다. 어차피 마지막 페이지까지 Code, Data 영역으로 사용할 정도로 프로그램 이미지가 크다면 외부 저장 모듈이 꼭 필요하다.

무작정 마지막 Page를 사용하도록 Hard Coding 하는 방법 보다 좀 더 있어 보이게 하고 싶으면 Linker Script를 건드리는 방법이 있다. Code와 Data 등을 다 Link한 후에 2kb Align된 Section을 하나 추가하고 Symbol을 성정해서 C 코드에서 그 Symbol 주소를 기반으로 Page 번호를 계산하면 된다.

아무튼 이 방법을 사용하기 위해서는 Linker Script에 아래와 같은 내용을 거의 맨 마지막 Section으로 추가하면 된다.

  .conf :
  ALIGN(0x800) 
  {
    _user_config = .;
    _flash_origin = ORIGIN(FLASH);
    . = . + 0x800;
  } >FLASH

일단 컴파일후 생성될 Binary에 .conf라는 Section이 하나 추가 된다. .conf Section은 2kb(0x800)단위로 Align되고. C 코드에서 주소 값으로 사용할 _user_config라는 Symbol을 정의한다. C 언어 입장에서는 _user_config라는 Symbol이 .conf Section의 시작 주소가 된다.

C에서 Linker Script Symbol 사용

먼저 전역변수 설정하는 부분에 아래와 같이 정의해주자.

extern uint8_t _user_config[2048];
extern uint8_t _flash_origin[128][2048];

여기에서는 _user_config를 uint8_t Type으로 한 Page 크기 만큼의 배열로 정의 했지만, Type은 별로 중요하지 않고 _user_config라는 Symbol이 .conf Page의 시작주소라는 의미가 중요하다. 그리고 더 중요한 점은 extern Keyword로 외부에서 정의된 Symbol이란 걸 꼭 강조 해줘야 한다.

마찬 가지고 _flash_origin은 Flash Memory의 시작주소라는 이미가 중요하다.

이런 정보를 넣고 Compile, Link를 끝내고 Symbol Table을 보면 .conf Section과 그 안에 정의된 Symbol 정보를 볼 수 있다.

$ objdump -t prj.elf  | grep '.conf'
08002000 l    d  .conf  00000000 .conf
08000000 g       .conf  00000000 _flash_origin
08002000 g       .conf  00000000 _user_config

Linker Script에 .conf Section을 0x800으로 Align하라고 해서 0x800의 배수인 0x8002000에 만들어 졌다. Page로 따지면 5번째 Page인 4번 Page이다.

Flash에 값 저장하기(HAL)

  FLASH_EraseInitTypeDef flash_erase;
  flash_erase.TypeErase = FLASH_TYPEERASE_PAGES;
  flash_erase.Banks = FLASH_BANK_1;
  flash_erase.Page = ((uint32_t)_user_config - (uint32_t)_flash_origin)/0x800;
  flash_erase.NbPages = 1;

  uint64_t a __attribute__((aligned(8))) = 0x0807060504030302 ;
  HAL_StatusTypeDef ret;
  uint32_t ecode;

  ret = HAL_FLASH_Unlock();
  ret = HAL_FLASHEx_Erase(&flash_erase, &ecode);
  ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, (uint32_t)_user_config, a);
  ecode = HAL_FLASH_GetError();
  ret = HAL_FLASH_Lock();

  ret = ret;
  ecode = ecode;

HAL함수들을 사용했기 때문에 Erase, Program(저장)은 아주 간단하지만, 한 가지 주의할 점은 flash에 저장할 값이 들어있는 a변수는 64bit로 Align 해줘야 한다.

저장을 할 때는 주소를 사용하지만 Erase를 할때는 Page 번호를 사용한다. Page 번호는 Linker Script에서 생성한 Symbol 정보를 기반으로 쉽게 계산 할 수 있다.
((uint32_t)_user_config - (uint32_t)_flash_origin)/0x800

HAL_FLASHEx_Erase()을 사용해서 해당 Page를 지우고, HAL_FLASH_Program()으로 64 Bit만큼 기록을 해주게 된다.

아래와 같이 gdb를 사용해서 실제로 값이 저장되기 전/후를 확인 해서 진짜로 HAL 함수가 잘 동작했는지 확인해 볼 수 있다.

(gdb) file prj.elf 
...
(gdb) load
...
(gdb) break main
...
(gdb) c
Continuing.

Breakpoint 4, main () at Src/main.c:77
77        flash_erase.TypeErase = FLASH_TYPEERASE_PAGES;
(gdb) n
78        flash_erase.Banks = FLASH_BANK_1;
(gdb) n
79        flash_erase.Page = ((uint32_t)_user_config - (uint32_t)_flash_origin)/0x800;
(gdb) n
80        flash_erase.NbPages = 1;
(gdb) n
82        uint64_t a __attribute__((aligned(8))) = 0x0807060504030302 ;
(gdb) n
86        ret = HAL_FLASH_Unlock();
(gdb) print/x _user_config
$16 = {0xff <repeats 2048 times>} !!! 저장전
(gdb) n
87        ret = HAL_FLASHEx_Erase(&flash_erase, &ecode);
(gdb) n
88        ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, (uint32_t)_user_config, a);
(gdb) n
89        ecode = HAL_FLASH_GetError();
(gdb) print/x _user_config
$17 = {0x2, 0x3, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0xff <repeats 2040 times>} !!! 저장후
(gdb) n
90        ret = HAL_FLASH_Lock();
(gdb)

ECC Bits

Flash Memory에 관해서 잘 모르지만, wikipedia를 보면 “1”로 설정 되있는 Bit은 언제나 “0”으로 프로그램 할 수 있는 것 처럼 되있는데 STM32L432에서 테스트 해보니 안된다.

아마도 64 Bit 단위로 기록 할 때, 추가로 8 Bit ECC를 기록하기 때문에 ECC Bit 중에 “0” -> “1” 이 필요한 상황이 생기면 에러가 나는 것 같다. 그런 상황을 비켜 갈 수 있는 값을 저장하는 건 가능 할 것 같지만 별로 확인해볼 가치는 없는 것 같다.

하지만 64 Bit을 모두 “0”으로 바꾸는 거는 된다고 메뉴얼에 적혀있는 걸로 봐서는 64 Bit이 모두 “0”일 때는 ECC 8 Bit도 모두 “0”인 가봄.

2kb Memory

저장하는 값이 하나면 상관 없는데 여러가지 값을 저장하는 경우 하나의 값만 변경해도 2kb를 전부 지워우고 다시 저장해야하기 때문에 지우기 전에 모든 값을 RAM 영역에 임시로 저장해야 된다. 최악의 경우에는 2kb 만큼의 RAM이 항상 비어 있어야 하는데 L432kc의 RAM이 64kb 밖에 안되는 상황에서 2kb는 좀 크다.