среда, 21 августа 2013 г.

Одинаковый бинарник для прошивки ARM Cortex-M3 через штатные средства и собственный загрузчик

Понадобилось сделать такой фокус на LPC1788. Дано: приложение и загрузчик, для его прошивки в рабочем устройстве. Загрузчик традиционно находится в начале адресного пространства (с 0 по 0x4000, например, в моём случае). Всё остальное занимает приложение. Нужно сделать возможным прошивать приложение через ISP или JTAG и так же без изменений с помощью собственного загрузчика.

Сначала в упор не понимал как это сделать, но на electronix.ru подсказали. Пишу для закрепления себе, возможно ещё кому-то пригодится. Способ должен быть рабочий для всех МК с ядром Cortex-M3/M4, но проверено только на LPC17xx.

И так, загрузчику выделены первые 0x4000 байт. Это жёстко ограничено в скрипте линкера. Таблица векторов прерываний загрузчика традиционно лежит по адресу 0. Теперь чтобы иметь возможность запустить из него приложение нужно чтобы в приложении: а) весь код находился начиная с 0x4000 адреса, б) таблица векторов приложения должна быть своя.

Переход на адрес приложения из загрузчика делается просто модификацией программного счётчика и вершины стека. В AN10866 от NXP это сделано с помощью инлайн ассемблера:
static void boot(size_t app_start)
{
    __asm volatile (
            "LDR SP, [R0]\n"
            "LDR PC, [R0, #4]\n"
    );
}
Перед переходом не забываем отключить прерывания, остановить PLL, сбросить конвейер. Примерно так это выглядит у меня:
void boot_application_start(size_t start_address)
{
    //__disable_irq();
    //__disable_fault_irq();

    /* Disable all interrupts */
    NVIC->ICER[0] = 0xFFFFFFFF;
    NVIC->ICER[1] = 0x00000001;
    /* Clear all pending interrupts */
    NVIC->ICPR[0] = 0xFFFFFFFF;
    NVIC->ICPR[1] = 0x00000001;
    /* Clear all interrupt priority */
    for(uint8_t tmp = 0; tmp < 32u; tmp++)
    {
        NVIC->IP[tmp] = 0x00;
    }

    // relocate vector table. depricated here since we set this address dynamically for
    // app and bootloader in resetisr @See ResetISR
    //SCB->VTOR = (start_address & 0x1FFFFF80);
    LPC_SC->CCLKSEL = 0x01; // set sysclk (12MHz) as clock source
    LPC_SC->PLL0CON = 0; // disable PLL
    LPC_SC->PLL0FEED = 0xAA;
    LPC_SC->PLL0FEED = 0x55;
    __asm volatile (
            "dmb\n"
            "dsb\n"
            "isb\n"
    );
    boot(start_address);
}
Вместо глобального запрета прерываний отключаются все по очереди, очищается очередь прерываний, переключаемся на встроенный тактовый генератор, отключаем PLL0, барьеры памяти, сброс конвейера и переход на приложение. Перемещение указателя на таблицу векторов отключено, см. дальше почему. В PC помещается адрес + 4 т.к. в таблице векторов Cortex-M3 первый элемент - вершина стека, а следующий - вектор сброса,  т.е. отсюда начинает исполнение программа после сброса процессора.

Если приложение собрано так что начальный адрес его 0x4000 (в скрипте линкера), то после перехода на его адрес необходимо скорректировать местоположение таблицы векторов (SCB->VTOR). Если используется CMSIS, то в SystemInit() это будет задано на 0. Поэтому надо либо убрать это из CMSIS и тогда ставить указатель на таблицу ещё в загрузчике, либо в самом приложении после SystemInit() переставлять таблицу куда надо. Но это неудобно т.к. требуется иметь 2 версии приложения: одну собранную без смещений и другую со смещениями для прошивки через загрузчик.

Теперь выход как сделать это удобнее. В приложении нужно иметь 2 копии таблицы векторов: одна основная по адресу 0x4000, вторая - копия, по адресу 0, в которой нужен только адрес вершины стека и указатель на вектор сброса. Основная таблица содержит все вектора прерываний как обычно и располагается, например, в секции isr_vector:
__attribute__ ((section(".isr_vector")))
void (* const g_pfnVectors[])(void) =
{
  // Core Level - CM3
  (void *)&_vStackTop,    // The initial stack pointer
  ResetISR,               // The reset handler
  NMI_Handler,            // The NMI handler

....
Копия располагается в другой секции, например, isr_vector2 и содержит только вершину стека и вектор сброса:
__attribute__ ((section(".isr_vector2")))
void (* const g_pfnVectors2[])(void) =
{
  // Core Level - CM3
  (void *)&_vStackTop,    // The initial stack pointer
  ResetISR
};
Для приложения и загрузчика делаются разные скрипты линкера.
У загрузчика есть регион памяти от 0 до 0x4000. Его .isr_vector располагается в начале, т.е. в 0 адресе как и положено:


MEMORY
{
   FLASH (rx) : ORIGIN = 0x0 LENGTH = 0x4000


.....

  .text :
  {
    KEEP(*(.isr_vector))
  
    ....
   
    *(.text*)
    *(.rodata*)

  } > FLASH
А у приложения регион памяти тоже от 0 и до [всего памяти - размер загрузчика], при этом первые 0x4000 байт зарезервированы, линкер не может туда размещать ничего кроме копии таблицы векторов (.isr_vectors2):
MEMORY
{
   FLASH (rx) : ORIGIN = 0x0000 LENGTH = 0x7C000
   ....  
  .text :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector2))
    . = ALIGN(4);
   
    . = 0x0FF;
    KEEP(*(.bootloader));
      
    . = 0x4000; /* first 0x4000 bytes - bootloader, keep this region */
    KEEP(*(.isr_vector));
   
    *(.text*)
    *(.rodata*)

  } > FLASH
Тут первые 0xFF байт отведены под копию таблицы векторов. Фактически оттуда нужно 8 байт - вершина стека и вектор сброса, но так же можно размещать ещё разные вещи вроде номера ревизии, контрольной суммы и пр. Следующее пространство до 0x4000 не используется (для защиты чтения у NXP там есть место куда надо магическое число записать, но тут это не важно и этот код удалён) и только начиная с 0x4000 располагается основная таблица векторов и само приложение.

Таким образом если приложение записано с помощью ISP или JTAG напрямую, то процессор ищет в адресе 0 вершину стека, а в адресе 0x4 - адрес первой инструкции. И таки находит в копии таблицы векторов, т.о. приложение работает. Когда же у нас в 0 адресе есть загрузчик, то сначала выполняется он. Если нужно прошить приложение в загрузчике, то он берёт бинарник приложения и просто пропускает первые 0x4000 байт из файла (канала связи или откуда ещё берётся приложение) и прошивает его в память со смещением 0x4000. Затем переходит на его адрес т.е. на основную таблицу векторов.

Маленький штрих в конце. Чтобы в приложении не задавать вручную таблицу векторов можно прямо в стартапе после SystemInit() записывать в SCB->VTOR адрес массива g_pfnVectors:
SCB->VTOR = (unsigned long)&g_pfnVectors & 0x3FFFFF80;
Таким образом у приложения g_pfnVectors (основная таблица) расположен всегда на 0x4000 линкером. У загрузчика на 0. Это будет работать и для приложения и для загрузчика т.к. для них разные скрипты линкера размещают таблицы в разных адресах - тех самых, на которые надо устанавливать SCB->VTOR.

7 комментариев:

  1. Олег, здравствуйте!
    Тема чрезвычайно интересная. Под LPC17xx есть загрузчик, у меня в изделии он работает на LPC1768. Но сейчас пришлось добавить TFT матрицу, и GUI. Перешел на LPC1788. А там уже не работает - очень много отличий у этих двух контроллеров. Придется разбираться, переделывать... Раз вы уже выложили отдельные ключевые места, может быть повесите весь загрузчик? Например на Электрониксе? Он много кому нужен.
    Спасибо. Хорошего дня!

    ОтветитьУдалить
    Ответы
    1. Добрый день, Иван.
      Ок, попробую на выходных вычленить оттуда "пропритарные части" :)
      1788 кстати не так сильно отличается от 1788. Периферия местами отличается (GPIO, например), а ядро тоже самое. Этот загрузчик на 1788 я тоже взял из ранее сделанного загрузчика на 1768. Править пришлось совсем не много.

      Удалить
  2. Поглядел ещё раз пристально на него и нашёл единственое что интересно может быть - сам код перехода к приложению. С этим самая засада была у меня и на 68 и на 88 процах. Этот код в посте есть. С одной оговоркой. По неизвестным причинам как-то этот когда сломался и перестал работать и сделал переход на адрес приложения через сишный указатель на функцию:

    typedef void (*app_func)(uint32_t);
    app_func app = (app_func)(*(volatile uint32_t *) (start_addr + 4u));
    __asm volatile ("MSR psp, %0" : : "r" (*(volatile uint32_t *)start_addr));
    app(start_addr);

    Но говорят так стек оказывает неверно инициализирован. У меня отладчика нет сейчас поэтому не могу проверить Но так работает, попробуйте, может поможет. А с ассемблерным я уже не помню как и почему он то работал то переставал по не понятным причинам.

    ОтветитьУдалить
    Ответы
    1. Олег, я продолжаю биться с загрузчиком. В моем случае загрузка идет по USB. Нормально работает запись во флеш, но не работает передача управления пользовательской программе.
      Интересно: на Электрониксе кто-то вам уже писал:
      "Этот метод не работает, точнее работает неправильно. При заходе в main() стэк оказывается сдвинут на 0x680 по сравнению с запуском приложения без бутлоадера. "
      Так вот. Уменя ровно то же самое. Если интересно, взгляните:
      __asm void boot_jump( uint32_t address ){
      LDR SP, [R0] ;Load new stack pointer address
      LDR PC, [R0, #4] ;Load new program counter address
      }

      void execute_user_code(void)
      {
      uint8_t tmp;

      NVIC->ICER[0] = 0xFFFFFFFF; //Disable all interrupts
      NVIC->ICER[1] = 0x00000001;

      NVIC->ICPR[0] = 0xFFFFFFFF; //Clear all pending interrupts
      NVIC->ICPR[1] = 0x00000001;
      for(tmp = 0; tmp < 32; tmp++) // Clear all interrupt priority
      {
      NVIC->IP[tmp] = 0x00;
      }

      LPC_SC->CCLKSEL = 0x01; // set sysclk (12MHz) as clock source
      LPC_SC->PLL0CON = 0; // disable PLL
      LPC_SC->PLL0FEED = 0xAA;
      LPC_SC->PLL0FEED = 0x55;

      SCB->VTOR = (0x2000) & 0x1FFFFF80;
      boot_jump(0x2000);
      }

      Удалить
    2. А по-правде говоря я не проверял что там со стеком :) Даже если сдвинут это лишь потеря памяти, работает он так же. Так у Вас оно ни так ни сяк не работает вообще?

      Удалить
    3. Сам себя обманул :-)
      У меня в проекте там задан адрес старта 0x2000. При этом я собственными руками писал scatter file для линкера, где указа адрес старта 0x0000. И поставил галочку "верить скеттеру" :-)
      Грамотные ребята сразу просекли. где сидит баба Яга
      http://www.lpcware.com/content/forum/lpc1788-bootloader-cant-jump-user-code-correctly

      Удалить