키보드를 만듭시다. 어때요~ 참 쉽죠? (14)

나빌레라의 이미지

NOR 플래시 메모리에서 펌웨어가 동작 중인데 펌웨어 데이터가 저장되 있는 플래시 메모리 영역에 데이터를 업데이트 하면 현재 그 업데이트 동작을 수행하고 있는 펌웨어 코드를 지워버리거나 덮어서 써 버리는 문제가 생긴다. 두 개 정도 해결 방법을 생각해 봤다.

내가 설명할 두 가지 방법 외에도 여러 해결 방법이 있을 것이다. 현재 동작하고 있는 있는 코드가 손상되지 않기만 하면 된다.

특정 코드만 램 코드로 만드는 방법

호스트에서 데이터를 받아서 플래시 메모리 페이지를 지우고 다시 데이터를 쓰는 일련의 과정을 추적해서 해당 동작에 관련된 코드를 모두 램 영역에 리로케이션 하는 방법이 첫 번째 해법이다. 코드는 램에서 돌고 플래시 메모리는 업데이트하면 되므로 동작 중인 코드가 손상되지는 않는다. 하지만 이 방법은 문제가 많다.

먼저 램 사이즈가 20KB로 매우 작다는 점이다. 이 20KB를 커널, 사용자 태스크가 스택 및 static 데이터 용으로 어느정도 쓰고 있다. 그러므로 남는 램에 코드를 올려야 하는데 램이 모자를 수도 있다.

램이 충분하다 해도, 동작 중에 스택 오버플로우나 메모리 연산 버그 같은 것이 램코드 영역을 침범해서 코드 자체가 깨질 가능성도 있다. 누가 키보드 펌웨어를 해킹할 리는 없겠지만 아무튼 코드가 런타임에 손상될 수 있는건 잠재적으로 위험한 것이다.

빌드 프로세스가 복잡해 진다. 램 코드로 편입될 코드가 양이 적건 많건 간에 코드를 용도에 따라 분리해야 하는 추가 작업이 필요하다. 그리고 이것에 맞춰 링커 옵션도 수정해야 한다. 링커가 수정되었으니 ARM reset exception handler에서 처리하는 리로케이션 및 로딩 작업도 더 복잡하게 바뀌어야 한다.

여러모로 손이 많이 가고 변경 사항이 많이 필요하다.

부트 로더를 만드는 방법

두 번째 방법은 부트 로더를 만드는 방법이다. 펌웨어를 부트 로더와 메인 펌웨어로 분리한다. 그리고 부트 로더는 기존 펌웨어가 있던 자리에서 동작한다. 예를 들어 플래시 메모리 페이지 0 ~ 20 위치 정도에서 동작하는 거다. 그래서 STM32F103의 부트 롬이 보기에는 부트 로더가 그냥 펌웨어인 것이다. 부트 로더는 특정 이벤트가 발생하지 않으면 부팅과 동시에서 제어권을 메인 펌웨어로 바로 넘긴다. 메인 펌웨어는 부트 로더 이후 위치인 플래시 메모리 페이지 21 ~ 63에 위치한다.

만약 부팅 과정에 부트 로더가 대기하고 있는 이벤트가 발생하면 부트 로더는 메인 펌웨어로 제어를 넘기는 것이 아니라 자체 다운로드 프로세스를 진행한다. 그래서 호스트에서 펌웨어 바이너리가 오면 메인 펌웨어 영역인 플래시 메모리 페이지 21 ~ 63을 수정하는 것이다. 현재 동작하는 펌웨어 명령어는 부트 로더 영역에 있으므로 메인 펌웨어를 업그레이드해도 현재 동작하는 명령어는 손상되지 않는다.

램 코드를 사용하는 방법보다 단순하고 쉽게 만들 수 있다. 다만 단점이 있다면 플래시 메모리를 많이 사용한다는 것이다. 예를 들어 램 코드를 사용한다면 펌웨어를 최대 63KB 크기까지 만들 수 있는데 비해 부트 로더를 쓰면 펌웨어 바이너리 크기는 (63KB - 부트 로더 바이너리 크기)로 제한된다. 예를 들어 부트 로더가 20KB라면 메인 펌웨어는 43KB까지 밖에 못 쓴다.

다행히 내가 만든 펌웨어 최종 크기는 20KB가 안된다. 부트로더도 17KB 정도다. 그래서 크기는 아직은 별 문제가 안된다. 만약 미래에 기능이 막 붙으면서 바이너리 사이즈가 커진다면 그 때가서 문제 해결하지 뭐.

단점이 또 있는데, 부트 로더를 수정 할 수 없다는 것이다. 부트 로더를 수정하려면 UART로 연결해서 펌웨어를 다운로드해야 한다. MSC로는 메인 펌웨어만 수정할 수 있고 부트 로더는 고칠 수 없다. 부트 로더를 고칠 일이 얼마나 있겠냐 싶어서 이 문제도 일단 무시하기로 했다.

부트 로더가 기다리는 이벤트

부트 로더로 동작할지 메인 펌웨어로 동작할지를 결정하는 동작을 정해야 한다. 보통 하드웨어적으로 점퍼를 쇼트한다거나 스위치를 바꾼다거나 하는 방식으로 동작을 정하는 것이 일반적이다. 다행인 것은 키보드는 스위치가 매우 많은 디바이스라는 것이다. 그래서 하드웨어 트리거를 뭐로 할지 고민할 필요가 없다. 키보드 키 스위치 중에 하나 골라 쓰면 되니까.

그래서 결정한 것이 ESC 키다. 사실 ESC 키가 아니라 하드웨어 키 맵 상에 (Row 0, Col 0)에 맵핑된 키를 누르고 USB를 꼽으면 부트 로더로 동작하게끔 만들었다.

    if (CheckBootMode())
    {
        debug_printf("BootLoader..\n");
        App_msc_Init();
    }
    else
    {
        debug_printf("Jump to the MainFW..\n");
        Jump(MAIN_FW_BADDR);
    }

CheckBootMode() 함수에서 트리거 키가 눌렸는지 검사하고 트리거 키가 눌렸으면 부트 로더로 동작한다. 그게 아니라면 메인 펌웨어로 점프한다. 그러면 CheckBootMode() 함수를 어떻게 구현했는지 보자.

static bool CheckBootMode(void)
{
    return KeyHw_IsPressed(0, 0);
}
 
bool KeyHw_IsPressed(uint32_t col, uint32_t row)
{
    bool isPressed = false;
 
    SetAllRowToLow();
    SetAllColToInput();
 
    SetRowToHigh(row);
    HAL_Delay(1);
    isPressed = GetColInput(col);
 
    return isPressed;
}

CheckBootMode() 함수는 하드웨어 키 매트릭스의 (0, 0) 위치를 검사한다. 기본 키맵에는 ESC가 맵핑되어 있다. 사용자가 키맵 에디터로 키맵을 바꾼다면 다른 키다. 논리적으로 어떤 키 코드가 맵핑됐는지는 상관없고 통상적으로 우리가 알고 있는 그 ESC 키 위치의 스위치를 누르고 있으면 CheckBootMode() 함수는 true를 리턴한다.

메인 펌웨어로 점프

메인 펌웨어로 점프하는 코드는 내가 생각했던 것보다 복잡했다. 나는 그냥 메인 펌웨어 시작 위치 잡아서 함수 포인터로 만들어 점프하면 될 줄 알았는데 해야 할 일이 조금 더 많았다.

static void Jump(uint32_t address)
{
    typedef void (*pFunction)(void);
 
    pFunction Jump_To_Application;
 
    // variable that will be loaded with the start address of the application
    uint32_t* JumpAddress;
    const uint32_t* ApplicationAddress = (uint32_t *)address;
 
    // get jump address from application vector table
    JumpAddress = (uint32_t *)ApplicationAddress[1];
 
    // load this address into function pointer
    Jump_To_Application = (pFunction)JumpAddress;
 
    // Disable all interrupts
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = NVIC->IABR[i];
    }
 
    // Set interrupt vector table
    SCB->VTOR = address; /* Vector Table Relocation in Internal FLASH. */
 
    // Set stack pointer as in application's vector table
    __set_MSP((uint32_t)(ApplicationAddress[0]));
 
    // Go
    Jump_To_Application();
}

이전 글에서 언급했듯, Cortex-M 시리즈용 펌웨어 바이너리의 첫 4바이트는 스택 포인터다. 그 다음 위치가 reset exception handler의 시작 주소값이다. 그래서 ApplicationAddress[0]는 MCP에 설정하고, ApplicationAddress[1]는 함수 포인터에 저장했다가 마지막에 호출 형식으로 점프한다. 점프하기 전에 부트 로더에서 절정한 인터럽트를 모두 끈다. 메인 펌웨어가 필요한 인터럽트는 메인 펌웨어 초기화 하면서 다시 켤테니까. 그리고 하드웨어 레지스터를 설정해서 벡터 테이블 주소를 바꾼다. 그래야 메인 펌웨어 동작 중에 인터럽트가 발생 했을 때 개발자가 의도한 위치로 인터럽트 컨택스트가 점프한다.

MSC와 HID를 분리

MSC 기능은 이제 부트 로더에만 필요하다. 메인 펌웨어는 HID만 필요하다. 그래서 부트 로더는 MSC 전용 펌웨어가 되고 메인 펌웨어는 HID 전용 펌웨어가 된다. 그리고 부트 로더는 RTOS 기능이 필요없다. MSC 기능만 가지고 있고 파일을 받아서 플래시에 기록하는 기능만 있어야 한다. 물론 메인 펌웨어와 부트 로더 모두 HAL을 포함해야 하기에 결과로 나오는 펌웨어 바이너리 크기는 크게 차이 나지 않는다.

다음글에서 MSC와 HID를 분리하고 부트 로더에서는 RTOS 코드를 뺄 것이다. 그리고 벡터 테이블 주소도 다르니 링커 스크립트에서 리로케이션 기준 주소도 바꿔야 할 듯 하다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.