키보드를 만듭시다. 어때요~ 참 쉽죠? (13)
키맵 다운로더를 만들었으면 펌웨어 다운로더는 쉽게 만들 수 있다. 조금만 확장하면 된다. 그러나 펌웨어 다운로드 기능은 키맵과는 다르게 고려해야할 사항이 몇개 더 있다.
펌웨어 바이너리 위치
STM32F103은 전원이 들어오면 내장 부트롬에서 내장 플래시 메모리 시작 주소로 컨택스트를 점프한다. 즉, 사용자 펌웨어는 내장 플래시 0번 섹터에 0번 바이트에 있는 명령어가 바로 실행된다는 것이다. 그래서 펌웨어를 다운로드한다면 펌웨어 바이너리는 0번 섹터에서부터 쓰기(write)를 하면된다.
플래시 메모리 공간은 중국 싸구려 제품 특유의 형편없는 QA를 고려해 64KB만 쓰기로 했다. 그 64KB 중에서 마지막 1KB는 키맵 데이터용이다. 그러므로 펌웨어는 63KB를 쓸 수 있다.
펌웨어 파일 식별
펌웨어 파일도 키맵 파일처럼 매직 문자를 시작 위치에 넣어서 사용하는 방법을 고려해 볼 수 있다. 아니면 전통적인 방법인 파일 이름을 보고 구분하는 방법도 여전히 유효하다. 매직 문자를 시작 위치에 넣어서 펌웨어 파일을 식별하는 방법을 쓴다면 한 가지 고려해야할 사항이 있다. 매직 문자열 자체는 ARM 인스트럭션이 아니기 때문에 실제 플래시 메모리에 기록할 때는 매직 문자열을 빼고 기록해야 한다. 즉, 1KB 섹터 사이즈로 디바이스로 들어오는 데이터를 앞에 4바이트씩 계속 밀면서 데이터를 써야 한다는 것이다. 이것을 코드로 구현하려면 버퍼를 쓰는 수 밖에 없다. 버퍼도 써야하고 오프셋 계산도 해야 한다. 귀찮다.
이 시점에서 한 가지 알아두면 좋은 지식이 있다. 상식이 아니라 지식이다. 상식처럼 보편적으로 알 필요는 없고 특정 분야에서 일하는 개발자만 알면 되는 것이라 지식이라 칭했다. ARM Cortex-M 계열 프로세서는 메모리 주소 오프셋 0x00000000에 스택 포인터의 초기값을 설정해 놓도록 스펙을 정해 놨다. 정확히 꼭 메모리 주소가 0x00000000일 필요는 없고 레지스터에서 벡터 테이블 시작 주소 설정과 관련되있긴 하다. 그래서 엄밀히는 벡터 테이블 시작 주소라고 해야 맞다. 다만, 일반적으로 0x00000000이므로 대충 그렇게 알아두자. 그래서 전원이 들어오고 나면 ARM 코어는 0x00000000에서 4바이트를 읽어서 이 값을 MSP(Main Stack Pointer) 레지스터에 쓴다. 이 값이 스택 초기 주소가 되는 것이다.
펌웨어 다운로더 얘기하다가 갑자기 스택 얘기를 왜 꺼낸 것이냐면, 특별한 이유가 없는한 업그레이드 용으로 빌드하는 펌웨어나 현재 키보드 컨트롤러에서 돌고 있는 펌웨어나 이 스택 초기 주소값은 같다. 항상 같다. 그리고 웬만하면 항상 같도록 유지보수를 하면된다. 그렇다면 현재 돌고 있는 펌웨어는 자기 자신의 스택 초기 주소값을 알고 있으므로 FAT16의 write 함수를 통해서 들어오는 데이터의 첫 번째 섹터의 첫 4바이트가 스택 초기 주소값과 같다면 이것을 펌웨어 바이너리로 간주해도 된다는 말이다.
이러면 어떤 장점이 생기냐면, 펌웨어는 호스트에서 다운받은 새 펌웨어 바이너리를 그냥 그대로 플래시 메모리에 기록하면 된다. 수정을 할 필요도 없고 데이터 몇 개를 건너 뛸 필요도 없다. 그래서 구현 코드가 매우 간단해 진다.
펌웨어 다운로드 절차
펌웨어 다운로드 절차 역시 키맵 다운로드 절차와 크게 다르지 않다. 다만 키맵은 데이터 크기를 1KB로 정해 놨기 때문에 딱 한 섹터 쓰면(write) 끝이지만 펌웨어는 20KB 정도로 크기 때문에 20 섹터 정도를 써야 한다는 것이다. 그 차이 외에는 구현상 차이가 거의 없다.
extern uint32_t _estack; uint32_t firstWordOfMainFw = (uint32_t)&_estack; if (*checkMainFw == firstWordOfMainFw) { debug_printf("MainFW download\n"); tempflashStartPage = MAIN_FW_PAGENUM; startCopy = true; }
호스트에서 데이터가 들어올 때 첫 섹터의 첫 4바이트가 estack 값과 같다면 펌웨어 다운로드 절차를 진행한다. estack 값은 링커 스크립트에 지정되어 있는 값으로 보통 STM32F103의 내장 SRAM의 마지막 주소값이다. 스택은 높은 주소에서 낮은 주소로 숫자를 줄이면서 쓰기 때문에 마지막 주소값을 쓴다.
일반적으로 업그레이드하는 펌웨어도 코드들 수정하지 링커 스크립트 값을 조정하는 경우는 거의 없으므로 이 값은 앞으로 사용할 펌웨어가 모두 같은 값을 쓴다고 가정한다. 그리고 개발자가 그렇게 되도록 계속 관리해야 한다. 흔히 말하는 호환성 문제라는 것이 이런데서 생기는 것이다.
FAT16의 write 콜백 함수는 섹터 데이터가 들어올 때마다 호출되므로 20섹터 정도를 기록하려면 마지막 섹터 오프셋 정보를 함수 로컬 변수에 저장하면 안된다. 그래서 static 변수로 선언한 xxxTempxxx에 섹터 오프셋 번호를 저장해 놓고 이 변수의 값은 호출 때마다 증가한다.
문제점
기본 절차나 코드는 문제 없는데 그냥 다짜고자 플래시 메모리 0번 페이지부터 펌웨어를 쓰면(write) 문제가 생긴다. 어떤 문제일까? 힌트를 주자면 NOR 플래시 메모리는 그 자체에서 명령어 실행이 가능한 메모리라는 것이다. 생각해 보았는가.
문제는 뭐냐면 FAT16의 write() 함수 자체도 NOR 플래시에서 실행 중이라는 것이다. 여기서 무릎을 탁 치며 아! 하는 사람이 있을테고 여전히 모르는 사람이 있을 것이다. 여기 쯤에서 문제를 파악한 사람은 임베디드 소프트웨어 엔지니어로 진로를 잡아 보는 걸 고려해 보기 바란다. 자질이 있다.
예를 들어, FAT16의 write() 함수가 내장 플래시 메모리 페이지 2에 저장되어 있다고 치자. 그리고 호스트에서 업데이트 펌웨어 데이터가 쭉 쭉 들어오고 있다. 페이지 0을 지우고 데이터를 기록한다. 페이지 1을 지우고 데이터를 기록한다. 페이지 2를 지우고... 이 시점에서 ARM 코어는 아무것도 못한다. 현재 돌고 있는 명령어가 지워졌기 때문이다. 명령어가 플래시 메모리 위에서 직접 실행되고 있기 때문에 플래시 메모리의 데이터를 지우면 실행할 명령어가 없어진다.
이 문제를 해결하는 방법은 두 가지 정도가 있다. 다음 글에서 두 가지를 고려해보고 둘 중 하나를 골라 구현한 이야기를 하겠다.
댓글 달기