Миро Самек. Построение простых систем на ARM-контроллерах с использованием инструментов GNU (перевод)
- переводы |
- Miro Samek |
- GNU |
- Bare-Metal |
- arm
Часть 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". Начиная с этого момента, код начинает исполняться по рабочим адресам.
- блог пользователя teap0t
- 148193 просмотра
Новые записи в блогах
- Устранение дребезга контактов на основе вертикальных счетчиков
- Диагностика Imprecise Bus Faults в микроконтроллерах Cortex-M3/M4/M4F
- Self-powered камера
- Фоновый модулятор: беспроводная связь из ничего (перевод)
- Texas Instruments Analog Applications Journal SLYT612 "Снижение искажений в аналоговых КМОП ключах" (перевод)
- USB MSD. Часть 6. Команды SCSI (перевод)
- USB MSD. Часть 3. USB класс накопителей данных (перевод)
- Texas Instruments Application Report SBAA042 "Кодовые схемы, используемые в аналогово-цифровых преобразователях" (перевод)
- 10 принципов правильного интерфейса
- Релиз SDK на русский микропроцессор КРОЛИК
Recent comments
5 лет 19 недель назад
5 лет 34 недели назад
5 лет 44 недели назад
5 лет 46 недель назад
6 лет 2 недели назад
6 лет 26 недель назад
6 лет 27 недель назад
6 лет 30 недель назад
6 лет 41 неделя назад
7 лет 21 неделя назад