В первой части статьи был рассмотрен процесс разработки программ для микроконтроллеров AVR в инструментальной среде Eclipse. В конце первой части представлена общая схема разработки программ для ARM в Eclipse с помощью GNU Tool chain.

Используя практику многосерийных фильмов, напомню окончание первой части статьи и сразу же продолжу изложение материала.

Итак, процесс разработки программ в Eclipse для ARM микроконтроллеров с помощью GNU Tool chain изображен на следующей схеме.

Из схемы видно, что исходные файлы на языках С и Assembler компилируются с помощью arm-none-eabi-gcc. Компилятор arm-none-eabi-gcc может осуществлять вызов ассемблера arm-none-eabi-as, компоновщика arm-none-eabi-ld, поэтому в нашем Makefile вместо них будет указан arm-none-eabi-gcc.

В результате компиляции получается один или несколько объектных файлов. Далее компоновщик arm-none-eabi-ld выполняет компоновку объектных модулей в памяти микроконтроллера. Для микроконтроллеров с архитектурой ARM код программы может находиться в разных областях памяти( FLASH, SRAM и т.д. ).

На размещение кода и данных внутри микроконтроллера можно влиять путем создания файла сценария компоновки. Сценарий компоновки передает линкеру адрес и размер доступных в микроконтроллере объемов памяти, предписывает компоновщику размещение секций программы по конкретным адресам .

Чтобы написать скрипт компоновки необходимо иметь перед глазами карту памяти используемого микроконтроллера. Для большинства микроконтроллеров ARM можно найти готовый файл сценария.

После компоновки создается один файл исполняемого образа программы в формате elf .

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

Для этого необходимо выполнить преобразование утилитой arm-none-eabi-objcopy в один из доступных для прошивки форматов ( bin , intel hex, motorola S-records и т.д. ).

Из полученного после компоновки elf- файла можно получить листинг кода программы на языке ассемблера с помощью утилиты arm-none-eabi-objdump.

Makefile на нашей схеме изображен отдельно , в нем находиться предписание для утилиты make для сборки всего проекта.

При использовании для разработки программ языка С в большинстве случаев применяется стартовый код, — так называемый C — startup код, который выполняет начальную инициализацию микроконтроллера и передает управление основной функции main программы на С.

Стартовый код написан на языке ассемблера. Его вовсе не обязательно разрабатывать самостоятельно, поскольку для всех поддерживаемых GNU Tool chain микроконтроллеров он входит в состав инструментальной среды. В большинстве случаев стартовый код поставляется в виде откомпилированного объектного модуля, который в процессе компоновки вашего проекта присоединяется к нему.

Однако мы рассмотрим более детально стартовый код для различных архитектур ядра ARM.

Далее изображен пример стартового файла crt0.S микроконтроллера компании NXP LPC2138 с архитектурой ядра ARM7. Будем рассматривать этот код частями сверху-вниз.

В этой части кода выполняются объявления. Директива ассемблера (GNU-ASM) .global делает указанный после директивы символ глобальным, это значит, что данный символ можно будет использовать в других модулях программы.

Директивы ассемблера .set и .equ аналогичны директиве #define в языке C и выполняют установку нового значения символу. При компиляции проекта везде в тексте программы символ будет заменен на его значение.

Рассмотрим следующий фрагмент кода:

Директива .text указывает на начало сегмента кода, все следующие за ней директивы будут размещены в секции «text».

Далее при помощи директивы .arm указывается, что будет использован набор команд ARM.
Для процессоров ARM7 всего существует два набора команд — ARM и Thumb. Второй режим включает набор 16-разрядных команд и предназначен для увеличения плотности кода программы.

Объявление .section .init, «ax» декларирует секцию внутри сегмента кода с названием init и с помощью флагов «ax» указывает на то , что раздел является перемещаемым и содержит исполняемый код.

Директива .code 32 указывает на 32-разрядный код , а директива .align указывает на выравнивание кода по 16-ти разрядам (двум байтам). Метка _boot является глобальной (.global) и указывает на начало функции (.func _boot ).

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

В этом варианте обработчики содержат бесконечный цикл. При необходимости можно организовать функцию-обработчик соответствующего вектора прерывания.

Теперь объявляем три глобальные метки, по которым можно перейти на следующий код. Код программы всегда начинается с функции _start.

Далее выполняем инициализацию указателей вершин стеков для всех режимов работы процессора ARM7.

Величина стека задана в скрипте компоновщика( далее будет описан ).

В расположенном выше фрагменте кода мы перешли в режим «Undefined Instruction», запретив прерывания установкой I_BIT и F_BIT. Потом загрузили в SP адрес вершины стека из R0. Вычли из текущего значения в R0 размер стека режима «Undefined Instruction» и получили в R0 адрес вершины стека для следующего режима.
Аналогичным образом выполняется инициализация для всех остальных режимов.

Следующая инициализация необходима, если код предназначен для загрузки в FLASH память микроконтроллера.

Заполняем секцию .bss нолями:

И , наконец, выполняем передачу управления функции main.

Подсчет размера функции _start и ее окончание.

Бесконечный цикл, переход на который можно совершать из ненужных векторов прерываний или ошибочных ситуаций.

Для процессорного ядра Cortex-M3 стартовый код вообще является необязательным. В «Кортексах» вся программа может быть написана на языке программирования С.

При компиляции описанного выше стартового кода необходимо указать компилятору использовать режим ARM, а точнее ничего не указывать, так как по-умолчанию код компилируется для режима ARM.
Раздельная компиляция ARM и Thumb исходных кодов, находящихся в разных файлах, является специфической особенностью компилятора GCC.

Для того, чтобы откомпилировать исходный текст программы в режиме Thumb необходимо указать компилятору ключ -mthumb. Если в проект входят файлы, откомпилированные в ARM и Thumb режимах и между ними выполняется вызов функций , то необходимо также указать компоновщику ключ -mthumb-interwork, который позволит осуществлять переход между режимами ARM и Thumb во время выполнения кода.

При сборке программы из объектных модулей компоновщик использует скрипт компоновки, в котором указывается размещение секций внутри памяти микроконтроллера. Если этот скрипт явно не указывать , то будет использоваться встроенный по-умолчанию в компоновщик скрипт.

Для подключения собственного файла скрипта необходимо указать компоновщику с помощью опции -T . название скрипта. Имя файла и расширение для компоновщика не имеют значения. Пример :

Компоновщик вырабатывает исполняемый файл с настраиваемыми адресами. Эти адреса устанавливаются каждый раз при загрузке модуля в память. Каждый раздел описывается двумя адресами — виртуальным адресом (VMA) и загружаемым в память адресом (LMA). Первый предназначен для внутреннего использования, а второй указывает на расположение памяти, куда будет загружен раздел. Процесс определения адреса модуля называется его размещением.

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

Рассмотрим пример сценария компоновщика для микроконтроллера LPC2138 ( ARM7 ).

Ключевое слово MEMORY используется для присвоения имен блокам адресного пространства. В данном примере указывается местоположение и размер памяти микроконтроллера. Внутренней FLASH памяти присваивается название rom, а внутреннее ОЗУ микроконтроллера поименовано как ram. Атрибуты памяти (rx) указывают на то, что содержимое памяти может читаться и выполняться, аналогичным образом атрибуты (rw) указывают на возможность чтения и записи в данной области памяти.

Ключевое слово SECTIONS указывает на то, что далее будет описана карта распределения памяти для скомпонованного объектного модуля. Точка указывает на текущий адрес, то есть строка
. = 0x00000000 устанавливает текущий адрес в абсолютный со значением 0x00000000. После установки текущего адреса он будет автоматически увеличиваться при добавлении элементов в выходной файл.

Оператор .text : { } помещает начало секции .text выходного файла по текущему адресу. В скобках указываются элементы, которые будут включены в раздел .text выходного файла. Можно указать конкретные имена входных файлов ( компонуемых объектных файлов), в данном примере указаны все ( оператор * ) имена входных файлов.

Строка >rom указывает на то, что данный раздел будет помещен в именованную область памяти rom (то есть в FLASH память ).

Наименования основных секций для компоновщика являются предопределеннымы. В секции .text находиться код программы, также там могут находиться константы (.rodata). Секция .data предназначена для размещения данных , а в секции .bss находятся инициализированные нулевым значением данные. Туда попадают,например, статические переменные в языке С, которые только объявлены, но должны содержать нулевое начальное значение. Поэтому заполнение секции .bss нулевыми значениями производиться в стартовом коде перед вызовом функции main.

Именованные разделы размещаются в том порядке, в котором они указаны. Раздел .bss в нашем примере будет находиться сразу после раздела .data, поскольку для него не указан начальный адрес.

Компоновщику, по большому счету, все-равно на каком языке программирования написан программный модуль, скомпилированный в объектный и используемый в качестве входного . Вы можете использовать при компоновке объектные модули, написанные на языках программирования С и Assembler, как это делается при подключении стартового кода. В большинстве случаев такая необходимость не возникает, поскольку объема памяти и производительности современных микроконтроллеров достаточно для применения языка С.

Все программы из пакета ARM Tool chain( в качестве ARM Tool chain может выступать пакет Sourcery G++, Yagarto и т.д ) имеют интерфейс командной строки. Можно последовательно вызывать из командной строки компилятор, компоновщик, утилиты для преобразования и прошивки исполняемого файла. А можно записать всю последовательность вызовов в Makefile, запустить утилиту make и наблюдать за результатами выполнения команд, перечисленных в Makefile.

В отличии от командного файла Makefile не просто содержит последовательность выполняемых команд, он описывает так называемые зависимости.

Общая форма записи в Makefile на концептуальном уровне выглядит так :

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

Если, например, нам необходимо откомпилировать исходный файл main.c, потом скомпоновать объектный модуль main.o и стартовый код crt0.o в исполняемый образ main.out, далее преобразовать main.out в формат intel-hex , а также получить ассемблерный листинг программы, то Makefile будет содержать такие записи :

Цели all и clean называются псевдоцелями, это просто символьные метки,которые используются для запуска цепочки зависимостей из Makefile. Директива .PHONY: all clean перечисляет все псевдоцели, которые могут вызываться указанием названия цели в командной строке утилиты make.

Если мы запустим утилиту make с параметром all в командной строке, то активируется цель all. Утилита make проанализирует зависимость цели all — файлы main.hex и main.lst. Если эти файлы уже существуют, то на этом работа утилиты прекратится. При первом запуске у нас в наличии есть только исходные файлы crt0.s и main.c.

Содержимое main.c :

Далее анализируется файл main.hex, для его получения необходим main.out. В свою очередь для получения main.out необходимы объектные файлы crt0.o и main.o, которые можно получить из исходных файлов компиляцией. Все действия будут выполнены в обратном порядке для удовлетворения зависимостей. Сначала будут откомпилированы файлы crt0.s и main.c, потом скомпонованы в main.out, а уж после этого из main.out будут получены main.hex и main.lst.

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

Поэкспериментируйте с описанными выше файлами, поместите файлы Makefile, main.c, script.link, crt0.s в один каталог , запустите командную строку, перейдите с помощью команды cd в созданный каталог и наберите в командной строке make all. Потренироваться в написании скрипта компоновки можно путем его редактирования и просмотра результатов компоновки в файле листинга программы main.lst. Команда make clean, набранная из командной строки, удалит все созданные в результате выполнения make all файлы.

Настало время вернуться к Eclipse. Создадим новый проект с названием ARM7_project и добавим к нему все описанные выше файлы. Можете загрузить готовый проект.

Предоставленный код выполняет мерцание светодиодом, подключенным на первый вывод микроконтроллера LPC2138 ( PORT0.21 ), светодиод зажигается при высоком уровне на выводе PORT0.21 и гаснет при низком. Для понимания логики работы этого простого примера рекомендую почитать книгу Т. Мартин «Микроконтроллеры ARM7. Семейство LPC2000 компании Philips. Вводный курс».

Для отладки кода внутри кристалла микроконтроллера используем адаптер FTDI JTAG. Если Вы внимательно читали мои предыдущие статьи на этом сайте, то настраивать Eclipse для использования отладки Вы должны уметь.

В Fedora 13 Electronic Lab окно проекта, созданное в Eclipse Helios под Windows 7 , выглядело следующим образом.

В свежей версии FEL обновлять openocd до версии 0.4.0 не пришлось, так эта версия установлена изначально. Единственное, что пришлось установить, — это Sourcery G++. Путь к каталогу с утилитами из пакета Sourcery G++ в переменной PATH после перезагрузки потерялся, поэтому пришлось явно указать в Makefile расположение необходимых утилит.

Для отладки в Eclipse под FEL 13 вместо знакомого нам плагина ZylinCDT теперь используется GDB Hardware Debugging Plug-in.

На следующих рисунках изображены необходимые для запуска отладки в Eclipse под Fedora 13 (FEL) настройки.

Строка запуска openocd выглядела следующим образом :

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

Viewed 31079 times by 8092 viewers

Last modified: 06/02/2020

Author

Comments

В инициализационном файле crt0, когда происходит переход к функции main, каким образом цпу поменяет режим??как он узнает,что нужно перходить именно в режим ARM?зачем = перед всеми глобальными переменными ( типа ldr r10,=main )?.
.global main является глобальной.Те комманды
ldr r10,=main
mov lr,pc
bx r10
заставят компилятор перейти к функции main,что в файле main.c,Те int main(void) тоже является меткой??

    каким образом цпу поменяет режим??как он узнает,что нужно перходить именно в режим ARM?

    В GCC для процессоров ARM компиляция исходных файлов для режима ARM и Thumb выполняется раздельно. По-умолчанию используется система команд ARM, для компиляции в Thumb необходимо
    установить опции компилятора -mthumb и -mthumb-interwork( для перехода между режимами ARM и Thumb )

    зачем = перед всеми глобальными переменными ( типа ldr r10,=main )?

    Почитайте тут ( страница 99 )

    .global main является глобальной.Те комманды
    ldr r10,=main
    mov lr,pc
    bx r10
    заставят компилятор перейти к функции main,что в файле main.c,Те int main(void) тоже является меткой??

    int main(void); — объявление функции main, которое просто указывает компилятору, что далее по тексту будет реализация функции,
    .global main — показывает компоновщику, что реализация main будет в другом объектном модуле ( равносильно extern в языке С ) (
    страница 53 в руководстве по GNU-AS, указанному выше )

А как Вы знаете,какие адреса для памяти нужно задавать в скрипте для компоновищика

MEMORY
{
rom(rx) : ORIGIN = 0x00000000, LENGTH = 512k
ram(rw) : ORIGIN = 0x40000000, LENGTH = 96k
}

Я вот работаю с arm966e-s, и в результате
monitor flash write_image erase unlock ./main.hex 0×08000000 ihex
получаю,что gdb при работе с openocd пишет,что на данном адресе нет flash

А как Вы знаете,какие адреса для памяти нужно задавать в скрипте для компоновищика

Нужно смотреть в документации карту памяти конкретного микроконтроллера. Просмотрите документацию на ваш микроконтроллер, могут быть отличия от карты памяти, приведенной для ядра arm966e-s

monitor flash write_image erase unlock ./main.hex 0×08000000 ihex
получаю,что gdb при работе с openocd пишет,что на данном адресе нет flash

Попробуйте выяснить размер и расположение flash- памяти с помощью команд

flash banks
flash list
flash probe (num)

Если память не заблокирована , то ключ unlock нужно убрать, иначе могут быть ошибки.
Поищите готовый скрипт компоновщика для вашей модели ( семейства) МК

В скрипте линковщика Вы отводите паямять под flash и под sram. На команду

monitor flash write_image ./main.out

получаю примерно то,что писал выше.Сектор .text записывается нормально. Но дело в том что по адресу 0х0400,куда я записываю .data, .bss (т.е. в SRAM), получаю сообщение об ошибке. Есть ли какие нибудь способы указать openocd.что данный сектор не flash, a sram?или указать это в скрипте линковщика

Карта памяти чипа

Четче
Получается,что используются одна команда openocd — flash, для загрузки в sram и flash

    Напишите тип микроконтроллера, текст Вашего скрипта компоновки и параметры при запуске в командной строке openocd

В скрипте компоновщика явно указывается в какую область памяти нужно положить секцию:

ENTRY(_boot)
STACK_SIZE = 0x400;
MEMORY
{
rom(rx) : ORIGIN = 0x00000000, LENGTH = 512k
ram(rw) : ORIGIN = 0x40000000, LENGTH = 32k
}
SECTIONS
{
. = 0x00000000;
.text : {
KEEP(*(.init))
*(.text)
*(.rodata)
}> rom
_etext = .;
. = 0x40000000;
.data : {
_data = .;
*(.data)
} > ram
_edata = .;
.bss : {
__bss_start = .;
*(.bss)
} >ram
__bss_end__ = .;
.stack : {
. += STACK_SIZE;
PROVIDE (_stack = .);
}>ram
_end = .;
}

Знак больше после фигурной скобки в конце секции и потом название типа памяти

CPU: STR912FW44X

Запуск openocd

подключение к openocd через gdb. Стирание и снятие блокировки провожу раздельно,иначе,если это делать через write_image,все виснет. (Вопрос:разве должно стирание происходить так долго(13.6 с)? )

ENTRY(_boot)

STACK_SIZE = 0x400;

MEMORY

{

rom(rx) : ORIGIN = 0x00000000, LENGTH = 512k

ram(rw) : ORIGIN = 0x04000000, LENGTH = 32k

}

SECTIONS

{

. = 0x00000000;

.text : {

KEEP(*(.init))

*(.text)

*(.rodata)

}>rom

_etext = .;

. = 0x04000000;

.data : {

_data = .;

*(.data)

}>ram

_edata = .;

.bss : {

__bss_start = .;

*(.bss)

}>ram

__bss_end__ = .;

.stack : {

. += STACK_SIZE;

PROVIDE (_stack = .);

}>ram

_end = .;

}

Попробуйте сначала прошить контроллер силами openocd :


openocd -f olimex-arm-usb-ocd.cfg -f str912.cfg -c init -c targets -c "halt" -c "flash write_image erase unlock ./main.out 0x00000000 elf" -c "reset run" -c shutdown

Я решил проблему,используя load_image. flash write_image отказывается прошивать sram.

Хмм,у меня такой вопрос:как быстро у вас работает jtag? операцию записи он проводит c 0,2 кб\с. Стирание 512K происходит за 13 сек. В чем дело?

Хмм,у меня такой вопрос:как быстро у вас работает jtag?


wrote 1024 bytes from file CMSIS_example.out in 0.240986s (4.150 kb/s)

операцию записи он проводит c 0,2 кб\с. Стирание 512K происходит за 13 сек. В чем дело?

Частота работы JTAG у меня 1000 кГц :

Info : clock speed 1000 kHz

А у Вас 16 кГц :

Info : RCLK (adaptive clock speed) not supported — fallback to 16 kHz

Обратите внимание на аналогичную строчку для LPC2138 в статье :

Info : RCLK (adaptive clock speed) not supported — fallback to 500 kHz

Попробуйте увеличить значение частоты в файле str912.cfg :

# jtag speed. We need to stick to 16kHz until we've finished reset.
jtag_rclk 16

    Спасибо. Еще вопрос: в gdb команда continue продолжает выполнение команды до брэйкпоинта. Я устанавливаю метку break main,значит программа должна остановиться на этой метке, но это не происходит,а gdb не то что бы виснет,но после continue ничего не выполняет. Есть только строчка «Note: automatically using hardware breakpoints for read-only addresses». Означает ли это что точка останова не поставлена?

Я устанавливаю метку break main,значит программа должна остановиться на этой метке, но это не происходит,а gdb не то что бы виснет,но после continue ничего не выполняет.

Скорее всего Вы устанавливаете точку останова не имея привязки к своему коду.
С прошитым контроллером попробуйте заново запустить openocd . Далее gdb :

# cd ПУТЬ_К_ELF_ФАЙЛУ
# arm-none-eabi-gdb ./main.out
(gdb) target remote localhost:3333

В ответ gdb должен выдать адрес, с которого стартует Ваша программа и реальное содержимое по этому адресу
Далее:

(gdb) break main
(gdb) continue

Команда load в gdb загружает в отладчик(gdb) исполняемый файл чтобы тот мог с ним работать,устанавливать точки останова ,но не в процессор. Каким образом вы,уже работая в Eclipse, прошиваете чип?ведь судя по скринам с Eclipse,приведёнными вами выше, с конфигурацией openocd и gdb в среде, происходит только их настройка,а команд для прошивки (flash write_image или в моем случае это load_image) я что-то не увидел.

Команда load image( в примере установлена галочка напротив «load image» ) выполняет программирование микроконтроллера, это команда gdb, а write_image( и load_image ) — команда openocd, которую можно выполнить без привлечения gdb

Вот смотрите. Как вы считаете,если я сконфигурирую Run в Commands таким образом

monitor soft_reset_halt
monitor load_image ./main.out 0
monitor soft_reset_halt
monitor arm7_9 dbgrq enable
monitor gdb_breakpoint_override hard
tbreak main
continue

будет ли целесообразно каждый раз при начале отдебаживания прошивать контроллер

Если исходный код программы изменен, то каким образом, по Вашему мнению, не прошивая контроллер можно запустить внутрисхемную отладку? Пользуйтесь симулятором, если не хотите каждый раз прошивать заново память контроллера или загружайте Вашу программу в ОЗУ, а не в FLASH. Если же программа не изменилась, то отладку можно запустить и без команды load( write_image, load_image) , то есть без повторной загрузки образа в память микроконтроллера.

Попробуйте заменить Openocd-команду load_image на gdb -команду load в настройках Eclipse ( или установить галочку «load image» , как у меня в примере ).
Эта команда более универсальна, с ее помощью можно загрузить прошивку как в Flash-память , так и в RAM (с соответствующим загрузке в RAM скриптом компоновки), можно вообще не загружать, если программа не изменялась ( стр. 209 в документации на gdb ).

monitor soft_reset_halt
load
monitor arm7_9 dbgrq enable
monitor gdb_breakpoint_override hard
tbreak main
continue

Подскажите,правильно ли я делаю:
мне нужно разместить одну сишную функцию в sram, и я делаю так

_data:
ldr pc,_FMI_Config
_FMI_Config: .word FMI_Config

Вроде как FMI_Config должен разместиться вначале sram.

Comments are closed.