Миро Самек. Построение простых систем на ARM-контроллерах с использованием инструментов GNU (перевод)


Часть 2

В этой части мы начнём разбираться с кодом, упоминавшимся в первой части и доступном на сайте Embedded.com , который содержит версии для языков "Си" и С++ примера "Blinky", мигающего четырьмя светодиодами отладочной платы AT91SAM7S-EK фирмы Atmel.

[Примеры кода в переводе даются по исходным текстам, скачаным с сайта "Quantum Leaps, LLC" , и, в отдельных случаях, в целях улучшения читаемости могут быть переформатированы и/или сокращены.

Текст в квадратных скобках, выделенный цветом аналогично этому фрагменту, содержит примечания, добавленные переводчиком.

Текст, отмеченный словом "примечание", принадлежит автору статьи.]

Вариант для "Си" расположен в подкаталоге "c_blinky", а вариант для С++ - в "cpp_blinky". Приложение "Blinky" незамысловато, но спроектировано аккуратно и использует все подходы и возможности, описанные в данной статье. Проект использует инструменты "CodeSourcery G++ GNU" для процессора ARM [3].

В этой части будет описан универсальный стартовый код и низкоуровневая инициализация для простой (bare-metal) системы на процессоре ARM. Рекомендуется ознакомиться с документом "IAR Compiler Reference Guide" [7], а именно с разделами "System startup and termination" и "Customizing system initialization".


2.1 Стартовый код

Ассемблерная реализация инициализации для простых (bare-metal) систем на ARM располагается в файле "startup.s", одинаковом для проектов на "Си" и C++. Код проектировался как универсальный и должен работать на любом контроллере ARM без изменений.

Вся инициализация процессора и отладочной платы, проводимая до начала функции "main()", должна обрабатываться процедурой "low_level_init()", которая обычно может быть написана на C/C++, но при необходимости и на ассемблере.

 
Листинг 2.1 Стартовый код (startup.s)

      /***********************************************************************
      * The startup code must be linked at the start of ROM, which is NOT
      * necessarily address zero.
      */
(1)         .text
(2)         .code   32

(3)         .global _vectors
(4)         .func   _vectors

      _vectors:

      /* Vector table
      * NOTE: used only very briefly until RAM is remapped to address zero
      */
(5)         B       _reset          /* Reset                 */
(6)         B       .               /* Undefined Instruction */
            B       .               /* Software Interrupt    */
            B       .               /* Prefetch Abort        */
            B       .               /* Data Abort            */
            B       .               /* Reserved              */
            B       .               /* IRQ                   */
            B       .               /* FIQ                   */

            .size   _vectors, . - _vectors
            .endfunc

      /* The copyright notice embedded prominently at the beginning of ROM */
(7)         .string "Copyright (c) YOUR COMPANY. All Rights Reserved."
(8)         .align 4                /* re-align to the word boundary */


      /***********************************************************************
      * _reset
      */
            .func   _reset

(9)   _reset:

      /* Call the platform-specific low-level initialization routine
      *
      * NOTE: The ROM is typically NOT at its linked address before the remap,
      * so the branch to low_level_init() must be relative (position
      * independent code). The low_level_init() function must continue to
      * execute in ARM state. Also, the function low_level_init() cannot rely
      * on uninitialized data being cleared and cannot use any initialized
      * data, because the .bss and .data sections have not been initialized yet.
      */
(10)        LDR     r0,=_reset        /* reset address is the 1st argument */
(11)        LDR     r1,=_cstartup     /* return address is the 2nd argument */
(12)        MOV     lr,r1             /* set the return addr. after the remap */
(13)        LDR     sp,=__stack_end__ /* set the temporary stack pointer */
(14)        B       low_level_init    /* relative branch enables remap */

      /* NOTE: after the return from low_level_init() the ROM is remapped
      * to its linked address so the rest of the code executes at its linked
      * address.
      */

(15)  _cstartup:
      /* Relocate .fastcode section (copy from ROM to RAM) */
(16)        LDR     r0,=__fastcode_load
            LDR     r1,=__fastcode_start
            LDR     r2,=__fastcode_end
      1:
            CMP     r1,r2
            LDMLTIA r0!,{r3}
            STMLTIA r1!,{r3}
            BLT     1b

      /* Relocate the .data section (copy from ROM to RAM) */
(17)        LDR     r0,=__data_load
            LDR     r1,=__data_start
            LDR     r2,=_edata
      1:
            CMP     r1,r2
            LDMLTIA r0!,{r3}
            STMLTIA r1!,{r3}
            BLT     1b

      /* Clear the .bss section (zero init) */
(18)        LDR     r1,=__bss_start__
            LDR     r2,=__bss_end__
            MOV     r3,#0
      1:
            CMP     r1,r2
            STMLTIA r1!,{r3}
            BLT     1b

      /* Fill the .stack section */
(19)        LDR     r1,=__stack_start__
            LDR     r2,=__stack_end__
            LDR     r3,=STACK_FILL
      1:
            CMP     r1,r2
            STMLTIA r1!,{r3}
            BLT     1b

      /* Initialize stack pointers for all ARM modes */
(20)        MSR     CPSR_c,#(IRQ_MODE | I_BIT | F_BIT)
            LDR     sp,=__irq_stack_top__       /* set the IRQ stack pointer */

            MSR     CPSR_c,#(FIQ_MODE | I_BIT | F_BIT)
            LDR     sp,=__fiq_stack_top__       /* set the FIQ stack pointer */

            MSR     CPSR_c,#(SVC_MODE | I_BIT | F_BIT)
            LDR     sp,=__svc_stack_top__       /* set the SVC stack pointer */

            MSR     CPSR_c,#(ABT_MODE | I_BIT | F_BIT)
            LDR     sp,=__abt_stack_top__       /* set the ABT stack pointer */

            MSR     CPSR_c,#(UND_MODE | I_BIT | F_BIT)
            LDR     sp,=__und_stack_top__       /* set the UND stack pointer */

(21)        MSR     CPSR_c,#(SYS_MODE | I_BIT | F_BIT)
            LDR     sp,=__c_stack_top__         /* set the C stack pointer */

      /* Invoke the static C++ constructors (harmless in C) */
(22)        LDR     r12,=__libc_init_array
            MOV     lr,pc           /* set the return address */
            BX      r12             /* the target code can be ARM or THUMB */

      /* Enter the C/C++ code */
(23)        LDR     r12,=main
            MOV     lr,pc           /* set the return address */
            BX      r12             /* the target code can be ARM or THUMB */

(24)        SWI     0xFFFFFF        /* cause exception if main() ever returns */

            .size   _reset, . - _reset
            .endfunc

            .end
										

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

(1) Директива ".text" сообщает ассемблеру, что код надо добавлять в конец кодового сегмента "text".

(2) Директива ".code 32" выбирает набор 32-битных инструкций ARM (значение 16 выбирает набор 16-битный инструкций THUMB). Таким образом, ядро начинает работу в состоянии ARM.

(3) Директива ".global" делает метку "_vectors" видимой компоновщику.

(4) Директива ".func" создаёт для функции "_vectors" отладочную информацию (тело функции должно заканчиваться директивой ".endfunc").

(5) Сразу после сброса ядро ARM начинает исполнение инструкции по адресу 0x00000000, который в процессе начальной загрузки находится в программной памяти. Позднее программная память может быть переадресована посредством процедуры реорганизации памяти. Таким образом, программный код компонуется под результирующий адрес [после процедуры реорганизации], а не под адрес в момент начальной загрузки.

Динамическое изменение карты расположения памяти имеет, как минимум, два следствия. Во-первых, несколько начальных инструкций должны быть позиционно независимыми. Это значит, что может использоваться только адресация относительно счётчика команд "PC". Во-вторых, первоначальная таблица векторов используется очень недолго и заменяется на таблицу, построенную в оперативной памяти.

(6) Начальная таблица векторов состоит только из набора бесконечных циклов (передающих управление на самих себя) и используется до момента её замены на таблицу векторов, построенную в оперативной памяти. Если в процессе замены возникнет исключение, отладочная плата, скорее всего, перейдёт в состояние, из которого процессор не сможет выйти самостоятельно, поэтому рабочие устройства должны иметь схему (такую как внешний сторожевой таймер с независимым источником тактирования), позволяющую информировать пользователя об этом событии.

(7) В начале программной памяти полезно иметь заметную строку с информацией об авторстве.

(8) После включённой в код текстовой строки [произвольной длины] необходимо производить выравнивание по границе машинного слова. [Правильнее ставить процедуру выравнивания перед исполняемым кодом, ведь выравнивается-то именно он.]

(9) На эту метку происходит переход после сброса.

(10) Регистры "r0" и "r1" используются как аргументы при вызове функции "low_level_init()". В регистр "r0" записывается адрес обработчика сброса, который может быть полезен при построении таблицы векторов в оперативной памяти.

(11) В регистр "r1" записывается [рабочий, т.е. времени исполнения] адрес стартового кода для языка "Си", который одновременно является адресом возврата из функции "low_level_init()". Контроллерам некоторых моделей (таких как AT91x40 с модулем EBI) указанный адрес может потребоваться для прямого перехода после процедуры реорганизации памяти.

(12) Регистр связи (link) загружается адресом возврата. Отметим, что это [рабочий, т.е. времени исполнения] адрес метки "_cstartup" после процедуры реорганизации, а не адрес очередной машинной инструкции [каким он видится в точке 12] (таким образом, загружать адрес инструкцией "LDR lr, pc" некорректно).

(13) Временный указатель стека устанавливается в конец сегмента стека. Инструменты GNU учитывают тот факт, что стек растёт в направлении младших адресов памяти.

(Примечание. Указатель стека, инициализированный на данном шаге, может содержать адрес памяти "несуществующей" до завершения процедуры реорганизации. В семействе AT91SAM7S такой проблемы нет, так как оперативная память всегда доступна по адресу компоновки (0x00200000). Но в других моделях (например, AT91x40) оперативной памяти по указанному адресу нет до момента переадресации EBI. В последнем случае функцию "low_level_init()" следует писать на ассемблере, чтобы гарантировать неиспользование указателя стека до завершения процедуры реорганизации памяти.)

(14) Функция "low_level_init()" вызывается инструкцией относительного перехода. Отметим, что инструкция "branch-with-link - BL" намеренно НЕ используется, потому что код (может быть) вызван не с адреса времени исполнения, а до проведения процедуры реорганизации памяти. Вместо этого, адрес возврата прямо загружается предыдущей инструкцией ("MOV lr, r1").

(Функция "low_level_init()" может быть написана на языке C/C++ с учётом следующих ограничений. Она должна исполняться в состоянии ядра ARM и не должна затрагивать инициализацию секции ".data" или ".bss". Кроме того, если требуется реорганизация памяти, то производиться эта операция должна внутри функции "low_level_init()", потому что после возврата ею управления код перестаёт быть позиционно независимым.)

(15) Метка "_cstartup" отмечает начало инициализации "Си".

(16) Секция ".fastcode" используется для кода, выполняемого из оперативной памяти. Здесь эта секция копируется с адреса расположения в программной памяти по адресу выполнения в оперативной памяти (см. третью часть статьи).

(17) Секция ".data" используется для инициализированных переменных. Эта секция копируется с адреса расположения в программной памяти по рабочему адресу в оперативной памяти (см. третью часть статьи).

(18) Секция ".bss" используется для неинициализированных переменных, которые в соответствии со стандартом "Си" должны быть обнулены (см. третью часть статьи).

(19) Секция ".stack" служит для хранения стеков. Секция заполняется специальным значением, облегчающим наблюдение за использованием стека в отладчике.

(20) Инициализируются все указатели стеков переключаемых регистровых банков.

(21) Последним инициализируется указатель стека режима User/System. Весь последующий код инициализации исполняется в режиме System.

(22) Библиотечная функция "__libc_init_array()" вызывает все статические конструкторы C++ (см. третью часть статьи). Указанная функция вызывается инструкцией "BX", что позволяет переключить ядро в состояне THUMB. В "Си" данная функция ничего не делает.

(23) Функция "main()" вызывается инструкций "BX", позволяющей переключить ядро в состояние THUMB.

(24) В простых (bare-metal) проектах функция "main()" никогда не возвращает управление, потому что операционная система отсутствует. Если всё же "main()" вернёт управление, произойдёт вызов исключения "Software Interrupt", в котором пользователь может уточнить способ обработки такой ситуации.


2.2 Низкоуровневая инициализаци

Низкоуровневая инициализация, проводимая функцией "low_level_init()", всегда сильно зависит от конкретной модели процессора и особенностей процедуры реорганизации памяти. Ранее говорилось, что функция "low_level_init()" может быть написана на "Си" или C++, но должна быть скомпилирована под набор инструкций ARM, не может проводить инициализацию секций ".data" и ".bss" или вызывать статические конструкторы C++.

 
Листинг 2.2 Низкоуровневая инициализация контроллера AT91SAM7S (low_level_init.c)

(1)   #include "bsp.h"

(2)   void low_level_init(void (*reset_addr)(), void (*return_addr)()) {
(3)         extern uint8_t __ram_start;
(4)         static uint32_t const LDR_PC_PC = 0xE59FF000U;
(5)         static uint32_t const MAGIC = 0xDEADBEEFU;
            AT91PS_PMC pPMC;

            /* Set flash wait sate FWS and FMCN */
(6)         AT91C_BASE_MC->MC_FMR = ((AT91C_MC_FMCN) & ((MCK + 500000)/1000000 << 16))
                             | AT91C_MC_FWS_1FWS;

            /* Disable the watchdog */
(7)         AT91C_BASE_WDTC->WDTC_WDMR = AT91C_WDTC_WDDIS;

(8)         /* Enable the Main Oscillator: ....*/
            /* Set the PLL and Divider: ....*/
            /* Select Master Clock and CPU Clock select the PLL_clock/2 ....*/

            /* setup the primary vector table in RAM */
(9)         *(uint32_t volatile *)(&__ram_start + 0x00) = LDR_PC_PC | 0x18;
            *(uint32_t volatile *)(&__ram_start + 0x04) = LDR_PC_PC | 0x18;
            *(uint32_t volatile *)(&__ram_start + 0x08) = LDR_PC_PC | 0x18;
            *(uint32_t volatile *)(&__ram_start + 0x0C) = LDR_PC_PC | 0x18;
            *(uint32_t volatile *)(&__ram_start + 0x10) = LDR_PC_PC | 0x18;
(10)        *(uint32_t volatile *)(&__ram_start + 0x14) = MAGIC;
            *(uint32_t volatile *)(&__ram_start + 0x18) = LDR_PC_PC | 0x18;
            *(uint32_t volatile *)(&__ram_start + 0x1C) = LDR_PC_PC | 0x18;

            /* setup the secondary vector table in RAM */
(11)        *(uint32_t volatile *)(&__ram_start + 0x20) = (uint32_t)reset_addr;
            *(uint32_t volatile *)(&__ram_start + 0x24) = 0x04U;
            *(uint32_t volatile *)(&__ram_start + 0x28) = 0x08U;
            *(uint32_t volatile *)(&__ram_start + 0x2C) = 0x0CU;
            *(uint32_t volatile *)(&__ram_start + 0x30) = 0x10U;
            *(uint32_t volatile *)(&__ram_start + 0x34) = 0x14U;
            *(uint32_t volatile *)(&__ram_start + 0x38) = 0x18U;
            *(uint32_t volatile *)(&__ram_start + 0x3C) = 0x1CU;

            /* check if the Memory Controller has been remapped already */
(12)        if (MAGIC != (*(uint32_t volatile *)0x14)) {
                  /* perform Memory Controller remapping */
(13)              AT91C_BASE_MC->MC_RCR = 1;
            }
(14)  }
										

Листинг 2.2 показывает низкоуровневую инициализацию микроконтроллера AT91SAM7S, написанную на языке "Си". Очень важно понимать, что инициализация другого контроллера, например, из семейства AT91x40, имеющего EBI, может сильно отличаться из-за особенностей операции реорганизации памяти. Ниже поясняются ключевые моменты кода.

(1) Компилятор GNU gcc отвечает стандартам языка "Си" и поддерживает стандартизованные документом C-99 типы данных, использование которых рекомендуется. [В "bsp.h" происходит подключение файла "stdint.h", который и объявляет указанные типы данных.]

(2) Функции "low_level_init()" передаются следующи аргументы: "reset_addr" - рабочий адрес обработчика сигнала сброса и "return_addr" - рабочий адрес возврата из процедуры "low_level_init()".

(Примечание. При обращении к "low_level_init()" из кода C++ функция должна объявляться как "extern "C"", чтобы учесть порядок аргументов.)

(3) Метка "__ram_start" отмечает рабочий адрес оперативной памяти. В контроллере AT91SAM7S оперативная память всегда доступна по указанному адресу, таким образом "__ram_start" отмечает и адрес оперативной памяти до проведения процедуры реорганизации (см. третью часть статьи).

(4) Константа "LDR_PC_PC" представляет собой объектный код инструкции ARM "LDR pc, [pc,...]", которая используется при построении таблицы векторов в оперативной памяти.

(5) Константа "MAGIC" используется как признак проведения операции реорганизации памяти.

(6) Для ускорения инициализации количество циклов ожидания флэш-памяти уменьшается со значения по умолчанию после сброса.

(7) Сторожевой AT91 таймер запрещается на время проведения инициализации. Приложение может разрешить таймер в функции "main()".

(8) Для ускорения инициализации проводится настройка тактирования процессора и периферии.

(9) У вычислительного ядра должна быть рабочая таблица векторов всё время, поэтому она заводится в оперативной памяти до проведения процедуры реорганизации. Структура таблицы показана ниже:

 

      0x00000000  LDR   pc,[pc,#0x18]     /* Reset                 */
      0x00000004  LDR   pc,[pc,#0x18]     /* Undefined Instruction */
      0x00000008  LDR   pc,[pc,#0x18]     /* Software Interrupt    */
      0x0000000С  LDR   pc,[pc,#0x18]     /* Prefetch Abort        */
      0x00000010  LDR   pc,[pc,#0x18]     /* Data Abort            */
      0x00000014  LDR   pc,[pc,#0x18]     /* Reserved              */
      0x00000018  LDR   pc,[pc,#0x18]     /* IRQ vector            */
      0x0000001С  LDR   pc,[pc,#0x18]     /* FIQ vector            */
										

Все записи таблицы загружают счётчик команд (PC) адресом во вторичной таблице, следующей в памяти сразу же за первой. Например, запись по адресу 0x00000000, соответствующая исключению "Reset", загружает в PC исполнительный адрес 0x00000000 (+8 из-за очереди) + 0x18 = 0x00000020, который расположен сразу после таблицы векторов.

(Примечание. Некоторые контроллеры ARM, такие как семейство NXP LPC, перемещают в начало адресного пространства только малую часть оперативной памяти, но, в любом случае, не менее чем 0x40 байт памяти (в случае LPC - именно 0x40), что достаточно для размещения обеих таблиц.)

(10) Записи в таблице векторов, относящиеся к неиспользуемым исключениям, заполняются константой MAGIC, которая пишется в оперативную память до процедуры реорганизации.

(11) Вторичная таблица адресов переходов содержит переход на метку "reset_addr" по адресу 0x00000020 и бесконечные циклы для всех остальных исключений. Например, запись для исключения "Prefetch Abort" по адресу 0x0000000C вновь вызывает загрузку счётчика команд адресом 0x0000000C [загружая PC содержимым памяти по адресу 0x0000000C + 0x08 + 0x18, то есть значением 0x0000000C], зацикливающим процессор.

Это временное решение работает до того момента, как приложение запишет во вторичную таблицу адреса своих обработчиков. До завершения этого процесса никакие исключения не могут быть обработаны.

(Примечание. Использование вторичной таблицы адресов переходов имеет несколько плюсов. Во-первых, очень легко программно менять обработчики исключений, просто прописывая их адреса во вторичной таблице, а не занимаясь построением инструкции относительного перехода для первичной таблицы. Во-вторых, загрузка значения в счётчик команд позволяет использовать всё 32-битное адресное пространство для размещения обработчика исключения, в то время как относительный переход ограничен +/- 25 битным смещением относительно текущего значения PC.)

(12) В память по абсолютному адресу 0x00000014 записывается (с последующей проверкой) константа MAGIC. До процедуры реорганизации адрес 0x00000014 находится в программной памяти и содержит инструкцию "B", отличающуюся от константы MAGIC. После реорганизации по адресу 0x00000014 располагается оперативная память.

(13) Если слово по адресу 0x00000014 не содержит константу MAGIC, значит операция записи не прошла. Это, в свою очередь, означает, что оперативная память не была перемещена на адрес 0x00000000 (то есть по адресу 0x00000000 по-прежнему расположена программная память) и реорганизацию следует повторить.

(Премечание. В контроллере AT91SAM7 невозможно выяснить результат реорганизации памяти проверкой какого-либо регистра. Метод записи по младшим адресам памяти может использоваться для надёжного тестирования результатов операции. Такой защитный приём очень удобен при проведении сброса в отладчике. Частичный сброс, проводимый отладчиком, обычно не отменяет результатов предшествующей операции реорганизации памяти и не требует повторного её проведения.)

(14) Возврат из "low_level_init()" происходит по адресу, записанному стартовым кодом в регистр "lr". Начиная с этого момента, код начинает исполняться по рабочим адресам.

ПредпросмотрAttachmentSize
bare_metal_arm_systems_html.zip327.73 КБ
blinky_files.zip173.94 КБ