В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Ассемблер для микроконтроллера с нуля. Часть 7. Компиляция, отладка, загрузка

📆8 сентября 2021   ✒️erbol   🔎5.256   💬2  

Привет датагорцам и гостям нашего кибер-города! В предыдущих частях материала по Ассемблеру основное внимание уделялось оформлению кода, в то время как компиляция, компоновка, отладка и загрузка программы в МК были упомянуты вскользь [1–5], либо вовсе не рассматривались.
Пришло время остановиться на указанных и сопутствующих [6–9] вопросах подробнее.

Введение

Продублируем для удобства Рисунок 35 из первой части статьи и обсудим его детали, оставленные ранее без внимания.

Ассемблер для микроконтроллера с нуля. Часть 7. Компиляция, отладка, загрузка
Рисунок 1. Этапы программирования МК.

Примем за результат первого этапа проект lightControl из пятой части статьи.

В реализации последующих этапов принимают участие те или иные программные средства (утилиты компилятора GCC, загрузчики), работу которых мы регулируем посредством опций в соответствующей строке Makefile, например:
а) в случае с утилитой компиляции as обуславливаем посредством опции:
-mmcu/mcpu — тип микроконтроллера/ядра,
-o — имя объектного файла,

б) предписываем утилите компоновки ld с помощью опции:
-o — дать соответствующее имя выходному elf-файлу,
 — придерживаться сценария, оговорённого в LinkerScript.ld.

С некоторыми, не использовавшимися ранее, опциями мы познакомимся ниже, полный же их перечень и назначение каждой можно увидеть с помощью опции --help:
/* AVR-8 */
avr-as --help
avr-ld --help
avrdude --help
/* Cortex M-4 */
arm-none-eabi-as --help
arm-none-eabi-ld --help
openocd --help

Нюансы работы упомянутых и других программ, используемых от компиляции кода до загрузки его в МК, и будут интересовать нас в рамках настоящей части статьи.

Поскольку суть дальнейших рассуждений едина для всех рассматриваемых нами МК, там, где это возможно, ограничимся в примерах кода ATmega8.

Компиляция

На этапе 2 из Рисунка 1 посредством утилиты as [1] осуществляется компиляция ассемблер-файла в объектный, причём происходит это в два приёма:
а) Препроцессинг, в ходе которого, среди прочего, производится:
• подключение файлов, оговорённых директивой .include,
• замена макросов и макроопределений на скрывающиеся за ними инструкции и числа, а также логических конструкций типа (1 << n) — на обычные числа.

б) Непосредственно компиляция, основное назначение которой — перевод ассемблерных инструкций программы в их двоичное представление или энкодинг, алгоритм которого для AVR-8 и Cortex M-4 отражён в [7] и [8], соответственно.

На Рисунке 2 представлены выдержки из указанных документов для инструкции сравнения содержимого РОН с константой.

Рисунок 2. Двоичный код инструкций CPI для AVR-8 и CMP для Cortex M-4.

Выясним, как именно реализуется энкодинг на следующих примерах:
/* AVR-8 */
CPI  r20, 117
/* Cortex M-4 */
CMP  r1, 239

Для ATtiny85 и ATmega8, согласно шаблона в верхней части Рисунка 2, двоичное представление инструкции из примера выше составят:
• Уникальный код 0011 инструкции CPI.
• Старший полу-байт 0111 числа 117 (01110101 в двоичном формате).
• Четыре младших бита номера РОН, уменьшенного на 16. Такое вычитание производится с целью уместиться в выделенное пространство. Для r20 будет это будет 20 — 16 = 4 или 0100 в двоичном представлении.
• Младший полу-байт 0101 числа 117.

В итоге получим 0011011101000101 или 0×3745 в шестнадцатеричном представлении.

В случае с STM32F401 и nRF52832, как следует из шаблона в нижней части Рисунка 2, составляющими бинарного варианта инструкции сравнения будут:
• Уникальный код 00101 инструкции CMP.
• Номер РОН в двоичном представлении, размещённый в трёх битах — 001.
• Число 239 в двоичном представлении или 11101111.

Как результат — 0010100111101111 или 0×29EF в шестнадцатеричном формате.

Следует отметить, что для рассматриваемых нами МК принят так называемый little-endian (т.е. от младшего байта к старшему) порядок размещения информации в памяти программ, в связи с чем в hex-файлах полученные нами числа 0×3745 и 0×29EF будут представлены как 0×4537 и 0xEF29, соответственно.

Из не применявшихся нами ранее следует особо отметить опцию -g утилиты as, обеспечивающую наличие в объектных и, как следствие, elf-файле информации, без которой не возможна полноценная отладка:
/* AVR-8 */
avr-as filename.S -mmcu=mcuname -g -o main.o
/* Cortex M-4 */
arm-none-eabi-as filename.S -mcpu=cpuname -g -o filename.o

Дизассемблирование

Открыв любой объектный файл, вы увидите ничего не говорящий набор символов, придать смысл которому можно c помощью утилиты дизассемблирования objdump:
/* AVR-8 */
avr-objdump -d filename.o > filename.dmp
/* Cortex M-4 */
arm-none-eabi-objdump -d filename.o > filename.dmp
где, filename — имя объектного файла.

На Рисунке 3 приведены фрагменты из файлов main.dmp и led.dmp нашего проекта.

Рисунок 3. Некоторые дамп-файлы проекта lighControl для МК ATMega8.

Как видите, основная информация сгруппирована в три колонки, обозначенные заглавными буквами красного цвета:
1. Колонка A фактически указывает на адрес инструкции в памяти программ МК. Значение адреса изменяется после исполнения каждой инструкции в соответствии с длиной последней. По большей части это — 2 байта, но бывают и исключения , как в случае с инструкциями LDS/STS, длина которых — 4 байта.

2. B колонке B содержится ассемблерный код вашей программы после компиляции. На примере макроса stackPointerInit (адреса 1e–24 в файле main.dmp) можете убедиться, что препроцессор заменил макроопределения регистров (SPL/SPH) и старшего адреса SRAM (RAMEND) на их численные значения.

3. Содержимое колонки С — шестнадцатеричное представление кода из колонки B. Именно эти данные перекочёвывают на дальнейших этапах из Рисунка 1 в файл прошивки и, в конечном итоге, микроконтроллер.

Помимо вышеуказанной, объектные файлы содержат и иную информацию (параметры секций, таблицу символов и др.), увидеть которую можно, заменив при запуске утилиты objdump опцию -d на -DSx.

Приглядевшись внимательно к содержимому дамп-файлов, вы обнаружите некоторые странности:
• Адресация во всех файлах начинается с одного и того же значения — 0, хотя, исходя из простой логики, две разные инструкции не могут находиться по одному и тому же адресу в памяти программ и нумерация адресов в led.dmp, timer.dmp и adc.dmp должна быть продолжением таковой для main.dmp.
• Аргументы инструкций RJMP и RCALL не отражают корректный адрес перехода. В частности, вызов функций adcInit, ledsInit и timerInit (адреса 0×26–0×2a в файле main.dmp) отправляет процессор по адресу, значение которого равно сумме текущего значения программного счётчика PC и 0, т. е. к следующей инструкции в этом же файле.

Дело в том, что утилита as при компиляции одного S-файла совершенно не подозревает о наличии других, в связи с чем и возникают вышеприведённые несоответствия, устранение которых происходит при компоновке, т. е. на этапе 3 из Рисунка 1.

Компоновка

После завершения компиляции всех ассемблер-файлов проекта, следующая строка Makefile запускает утилиту компоновки ld [2] с результатом в виде отладочного файла lightControl.elf. Применим к последнему утилиту objdump, чтобы получить доступный для чтения дамп-файл, выдержки из которого представлены на Рисунке 4.

Рисунок 4. Фрагменты файла lightControl.dmp для МК ATmega8.

Как видите:
1. Все объектные файлы собраны воедино в том порядке, в котором их имена расположены в строке Makefile, вызывающей утилиту ld.

2. Нумерация адресов стала, как и положено, сквозной.

3. Аргументы инструкций перехода теперь уже отражают корректные адреса, в частности:
• Вектор прерывания по сбросу, расположенный по адресу 0, указывает на адрес, вычисляемый как сумма текущего значений программного счётчика PC и смещения (2 + 28 = 30 = 0×1e), т. е. на первую инструкцию функции main.
• Аргумент инструкции RCALL, расположенной по адресу 0×26, направляет процессор к адресу 0×28 + 8 = 0×30 или к первой инструкции функции ledsInit.

Управление компоновкой через скрипт-файл

Для начала проясним, не вдаваясь в подробности, как именно использует утилита ld в своей работе записи, произведённые в LinkerScript.ld.

Всё основывается на системе команд, из доступного набора которых в нашем случае применяются две:
MEMORY определяет тип, начальный адрес и размер памяти МК, в которую, в конечном итоге, должны быть помещены те или иные данные из входных файлов. К примеру, в рамках проекта lightControl предполагается наличие флэш-памяти с начальным адресом 0×00 и размером 8 килобайт.
SECTIONS указывает компоновщику, в каких именно секциях входных o-файлов находится информация для формирования соответствующей секции выходного elf-файла. В частности, запись
.text : /* секция выходного файла */
  {
    *(.text); /* секции входных файлов */
  } > ROM
обязывает ld группировать данные из секций .text входных файлов в одноимённую секцию выходного для последующего размещения в памяти программ.

Тот факт, что утилита ld компонует объектные файлы согласно очерёдности их имён в командной строке, накладывает как минимум пару ограничений:
1. Если вы по невнимательности пропишете на первом месте в командной строке компиляции любой другой объектный файл вместо main.o, то по адресам векторов прерываний будут помещены инструкции из указанного файла. Такая программа, будучи загруженной в МК, может и будет функционировать, но результаты её работы сильно удивят вас при генерации первого же прерывания.

2. Строгий порядок имён входных файлов в строке вызова компоновщика затрудняет оптимизацию Makefile, которой мы займёмся позже.

Ситуацию легко разрешить, если:
а) дополнить структуру программы под-секциями (sub-sections),
б) указать в LinkerScript.ld имена секций и под-секций в удовлетворяющем нас порядке.

Договоримся, в качестве примера, группировать:
• вектора прерываний — в под-секции .vectors,
• обработчики прерываний — в под-секции .handlers,
• код функции main — в одноимённой под-секции,
• весь остальной код — по-прежнему в секции .text.

Имена «text» и «data» секций являются зарезервированными, поэтому не требуют наличия предварительной директивы .section. Во всех остальных случаях, включая под-секции, указанная директива должна прописываться явно:
.section .text.имя под-секции

Учитывая изложенное выше, структура ассемблер-файлов нашего проекта примет следующий вид:
main.S
.section .text.vectors
  .org Reset_vector
    RJMP  main
  .org TIMER1_OVF_vector * 2
    RJMP  TIMER1_OVF_Handler
  .org ADC_vector * 2
    RJMP  ADC_Handler

.section .text.main
  .global main
  main:
    ...
  main_loop:
    RJMP  main_loop

led.S
.text
  ledsInit:
    ...
    RET

  yellowLedToggle:
    ...
    RET

timer.S
.section .text.handlers
  TIMER1_OVF_Handler:
    ...
    RETI

.text
  timerInit:
  ...
  RET

adc.S
.section .text.handlers
  ADC_Handler:
    ...
    RETI

.text
  adcInit:
    ...
    RET

  adcStartConversion:
    ...
    RET

Определим в LinkerScript.ld порядок размещения данных из секций и под-секций входных файлов в секции .text выходного файла:
.text : /* секция выходного файла */
  {
    /* секции и под-секции входных файлов */
    *(.text.vectors);
    *(.text.handlers);
    *(.text.main);
    *(.text);
  } > ROM

Теперь, независимо от очерёдности имён объектных файлов в строке вызова утилиты ld, данные в файле lightControl.elf будут сгруппированы в установленном нами порядке.

Рисунок 5. Фрагменты файла lightControl.dmp для случая с под-секциями.


Подключение к проекту сторонних объектных файлов и библиотек

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

В качестве примера, создадим на диске C папку library, куда скопируем из архива статьи файлы uart.S и uart.h с функциями, поддерживающими работу одноимённого протокола, а затем откомпилируем их:
avr-as uart.S -mmcu=atmega8 -o uart.o

Теперь предположим, что мы решили выводить в Terminal значение регистра ADCH при каждом прерывании АЦП, для чего необходимо:
1. Вставить:
• в main — инструкцию вызова функции инициализации UART
RCALL  uartInit

• в обработчик прерывания АЦП — фрагмент передачи требуемых данных посредством uartSendByte
LDS    r16, ADCH
RCALL  uartSendByte

2. Добавить uart.o в строку Makefile, запускающую утилиту ld
avr-ld led.o timer.o adc.o main.o c:/library/uart.o -T LinkerScript.ld -o lightControl.elf

С течением времени количество объектных файлов, подобных uart.o, будет расти и встанет вопрос иx компактного хранения, в чём поможет ещё одна утилита gcc — архиватор ar.

Вернёмся в папку library и наберём в консоли следующую команду
avr-ar -rcs libAVR.a uart.o
по исполнению которой появится файл статической библиотеки libAVR.a, куда будет добавлен наш объектный файл.

К имени библиотеки предъявляется лишь одно требование — обязательный префикс «lib».

В последующем вы может пополнять библиотеку и другими объектными файлами с помощью всё той же команды, заменив, естественно, имя o-файла. Увидеть текущее её содержимое можно посредством опции -t утилиты ar:
avr-ar -t libAVR.a

Теперь, для подключения uart.o совсем не обязательно упоминать его имя в строке Makefile вызова утилиты ld, а достаточно:
а) указать путь к папке локации библиотеки посредством опции -L,
б) с помощью опции -l обозначить имя библиотеки без префикса «lib».
avr-ld led.o timer.o adc.o main.o -L c:/library -lAVR -T LinkerScript.ld -o lightControl.elf

Дизассемблировав посредством objdump полученный таким образом файл lightControl.elf, вы сможете убедиться, что код функций UART благополучно извлечён из библиотеки и помещён в соответствующий раздел выходного файла.

Дополнительным преимуществом использования библиотеки является то, что код содержащихся в ней объектных файлов подключается только при вызове соответствующей функции. Проще говоря, закомментировав или удалив в проекте функции UART, мы предотвращаем включение их кода в состав elf- и hex-файлов даже, если сама библиотека упомянута в строке вызова ld.

Отладка программы

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

Тем не менее, избежать ошибок полностью не удаётся даже опытному программисту.

Некоторые из них определяются компилятором и легко устраняются, как например:
Грамматические, обусловленные явными ляпами в именах инструкций, регистров, функций и т. п.
Синтаксические, связанные с нарушением правил записи инструкции, в частности, когда нарушен порядок расположения аргументов инструкции.

Однако, другие могут быть компилятору уже не под силу, в том числе:
Семантические, встречающиеся в конструкциях выражений. В рамках ассемблер-кода подобная ошибка чаще всего случается при записи числа в форме логических операций, когда вы забываете про скобки или выбираете не тот логический оператор в конструкциях типа ~((1 << m) | (1 << n)).
Логические, примером которых служит неверный выбор инструкции условного перехода при организации ветвления программы.
Ошибки тайминга, когда недочёты в расчётах времени тех или иных процессов приводят к нарушению обмена данными между структурными единицами программы либо между МК и внешними устройствами.
Технологические, обусловленные, в свою очередь, ошибками в мануалах и даташитах (такое, к сожалению, тоже случается) либо неправильной трактовкой их содержания с вашей стороны.
Алгоритмические, вызванные, как следует из названия, недостатками алгоритма программы.
Системные, порождаемые некорректностью задач, определенных для устройства в целом или его отдельных узлов.

Выявление и устранение именно таких ошибок и является целью этапа 4 из Рисунка 1 — отладки.

Способы и инструменты отладки

В основе отладки лежит анализ текущего значения регистров (РОН, SRAM, ввода-вывода) в ходе ручного управления работой МК по исполнению кода c возможностью остановки в заданном месте программы. Осуществить такое управление можно как на реальном микроконтроллере, так и с помощью программы, симулирующей его работу.

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

В рамках GCC симуляция реализуется посредством связки утилиты gdb [3] и встроенного в неё (sim) либо внешнего (simulAVR, QEMU и др.) симулятора. Следует отметить, что указанные симуляторы слабо, а в некоторых случаях вовсе не поддерживаются Windows, поэтому пользователям данной ОС имеет смысл обратить внимание на другие инструменты, например систему Proteus VSM, которая предоставляет широкий спектр возможностей, включая:
• оформление и компиляция кода для ряда МК, в т. ч. на базе ядер AVR и ARM,
• пошаговая отладка программ, написанных как внутри Proteus, так и вовне и переданных в систему в виде hex-файла,
• симуляция работы не только микроконтроллеров разного типа, но и целого устройства, для чего имеется обширная графическая элементная база, начиная с резистора и заканчивая дисплеем, осциллографом и логическим анализатором.
• проектирование и трассировка печатной платы.

Таким образом, вы можете в рамках одной среды приобрести немало полезных знаний и навыков, а главное — определиться, стоит ли заниматься встраиваемыми системами и, если да, с какого именно МК начать.
Для полноценной отладки «в железе» вам понадобится следующий минимальный набор инструментов:
1. Отладчик — устройство, непосредственно взаимодействующее с МК по одному из общепринятых протоколов (например, debugWire, SWD, JTAG) и обеспечивающее обмен информацией между микроконтроллером и отладочным софтом на вашем компьютере. Помимо основной функции, отладчик можно применять и для загрузки программы в МК, однако не всякий программатор годится для использования в качестве отладчика, в т. ч. используемый нами USBasp.

2. Осциллограф для контроля за формой сигнала на выводах МК и таймингом процессов,

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


Аппаратная отладка средствами GCC осуществляется через обмен данными между вышеупомянутой утилитой gdb, выступающей в качестве клиента, и сервером, роль которого играет отладочная программа (к примеру, AVaRICE — для AVR-8 или OpenOCD [4] — для Cortex M-4), обеспечивающая, с другой стороны, связь с отладчиком, а через него и с микроконтроллером.

Примеры отладки

Учитывая упомянутые выше ограничения (слабая поддержка симуляции в Windows, отсутствие функции отладки для USBasp), рассмотрим аппаратную отладку:
• в GCC — для nRF52832,
• посредством UART — для Atmega8.

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

Для обоих МК отладку будем осуществлять так, как если бы писали программу с нуля:
а) По очереди подключая ассемблер-файлы к проекту, отладим код содержащихся в них функций.

б) Проверим корректность обмена данными между функциональными модулями.

в) Убедимся в удовлетворительной работе устройства в целом.

Отладка средствами GCC для nRF52832

Подключив устройство на базе nRF5283 через st-link v2 к компьютеру, откроем в Windows программу cmd и посредством команды
openocd -f interface/stlink.cfg -f target/nrf52.cfg

запустим gdb-сервер с портом по адресу 3333:

Рисунок 6. Запуск gdb-сервера через OpenOCD для МК nRF52832.

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

Предположив, что в первую очередь мы оформили функции управления светодиодами, пройдём из Notepad++ к проекту lightControl для nRF52832 и приведём содержимое main.S и Makefile к следующему виду:
main.S
.include "nrf52832.h"
.include "macro.h"
  .data

  .text
    .org 0
      /* Указать на вершину стека */
      stackPointerInit
    .org Reset_vector
      .word main + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
    main_loop:
      /* Изменить состояние жёлтого светодиода на противоположное */
      BL  yellowLedToggle
      /* Перейти к метке main_loop */
      B   main_loop
.end

Makefile
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -g -o main.o
  arm-none-eabi-as led.S -mcpu=cortex-m4 -g -o led.o
  arm-none-eabi-ld main.o led.o -T LinkerScript.ld -o lightControl.elf

Обратите внимание, что в Makefile:
а) Добавлена опция -g в строке вызова утилиты as, что обусловит отражение отладочной информации в объектных, а после компоновки и elf-файле.

б) Отсутствуют строки создания и загрузки в МК hex-файла, поскольку для отладки достаточно elf-файла.

Сохранив изменения, перейдём в консоль и с помощью соответствующих команд, сопровождаемых нажатием клавиши «Enter»:
• откомпилируем программу
make
...

• запустим в качестве клиента утилиту gdb с указанием имени elf-файла
arm-none-eabi-gdb lightControl.elf
...
Reading symbols from lightControl.elf...done.
(gdb)

• обеспечим соединение между сервером и клиентом
(gdb)target extended-remote: 3333
Remote debugging using : 3333
...
(gdb)

• и загрузим elf-файл в МК
(gdb)load
Loading section .text, size 0x60 lma 0x0
Start address 0x0, load size 96
Transfer rate: 413 bytes/sec, 96 bytes/write.
(gdb)

Далее необходимо определить строки программы, в которых требуется её остановка, чтобы, при необходимости, считать или изменить текущее состояние того или иного регистра. В нашем случае достаточно одной точки останова на строке возврата цикла main_loop. Чтобы узнать номер указанной строки можно заглянуть в main.S либо вывести его содержимое на печать посредством команды list, указав примерные границы интересующего нас фрагмента (например, с 13 до 22 строки):
(gdb) list main.S: 13, 22
13 main:
14   /* Настроить выводы управления светодиодами */
15   BL  ledsInit
16 main_loop:
17   /* Изменить состояние жёлтого светодиода на противоположное */
18   BL  yellowLedToggle
19   /* Перейти к метке main_loop */
20   B  main_loop
21  .end
22
(gdb)

С помощью команды break назначим в строке 20 точку останова, которой будет присвоен определённый порядковый номер:
(gdb) break main.S: 20
Breakpoint 1 at 0x10: file main.S, line 20.
(gdb)

Помимо приведённой, в отношении точек останова могут применяться следующие команды:
/* вывести информацию обо всех имеющихся точках останова */
info break
/* приостановить действие точки останова с номером n */
disable n
/* возобновить действие точки останова с номером n */
enable n
/* удалить точку останова с номером n */
delete n
/* удалить все точки останова */
delete

Сбросим микроконтроллер:
(gdb) monitor reset init
...
(gdb)

А затем командой continue запустим работу программы до первой точки останова:
(gdb) continue
...
Breakpoint 1, main_loop () at main.S:20
20  B  main_loop
(gdb)

Если при этом не загорится жёлтый светодиод, следует искать огрехи в led.S. Для такого простого кода это — скорее всего семантические или технологические ошибки.

Следующая команда continue вновь запустит, а затем остановит программу на строке 20 main.S, но уже с погашением жёлтого светодиода.

При желании, вы можете пошагово проверить работу кода с помощью команды step:
(gdb) step
halted: PC: 0x0000000c
18 BL    yellowLedToggle
(gdb) step
halted: PC: 0x00000044
yellowLedToggle () at led.S:31
31 PUSH  {r0-r1, LR}
(gdb) step
halted: PC: 0x00000046
33 LDR   r0, =(GPIO + GPIO_OUT)
(gdb)

Чтобы проверить корректность настроек для пина управления зелёным светодиодом достаточно, перейти в хидер-файл led.h и временно изменить значение макроопределения YELLOW_LED_PIN с 24 на 25, а затем, вернувшись к gdb-сеансу в консоли, посредством команды:
• make — перекомпилировать программу,
• load — обновить прошивку,
• monitor reset init — сбросить МК,
• continue — убедиться в реальном переключении состояния вывода 25.

Чтение/запись текущего значения регистров мы будем осуществлять командам print/set, соответственно, которые в зависимости от типа регистра прописываются следующим образом:
/* Команды чтения/записи */
/* РОН */
print $rn
set $rn = x 
где n – номер РОН, х — записываемое значение 
/* регистра ввода-ввода */
ptint *A
set *A = x 
где A – адрес регистра ввода-вывода, х — записываемое значение 
/* переменной в SRAM */
print varName
set varName = x 
где varName – имя переменной, х — записываемое значение 

Например, заставить мигать жёлтый светодиод можно, попеременно записывая 0 и 1 в 24-й бит регистра OUT модуля GPIO по адресу 0×50000504:
(gdb) set *0x50000504 = (1 << 24)
(gdb) set *0x50000504 = (0 << 24)

Будем считать, что вслед за led.S мы оформили timer.S, поэтому добавим в Makefile соответствующую информацию:
Makefile
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -g -o main.o
  arm-none-eabi-as led.S -mcpu=cortex-m4 -g -o led.o
  arm-none-eabi-as timer.S -mcpu=cortex-m4 -g -o timer.o
  arm-none-eabi-ld main.o led.o timer.o -T LinkerScript.ld -o lightControl.elf

Проверить корректность настроек таймера можно, убедившись в срабатывании его прерывания. Для визуализации этого факта воспользуемся тем, что функции управления АЦП как бы ещё не написаны и временно заменим в обработчике прерывания таймера строку, вызывающую функцию adcStartConversion
/* Запустить измерение АЦП */
BL  adcStartConversion

на фрагмент, изменяющий состояние зелёного светодиода
/* Изменить состояние зелёного светодиода на противоположное */
LDR  r0, =(GPIO + GPIO_OUT)
LDR  r1, [r0]
EOR  r1, (1 << 25)
STR  r1, [r0]

Такой приём, помимо визуализации, даст нам возможность подобрать, не утруждая себя расчётами, будущую частоту сэмплирования АЦП через период сигнала на выводе 25.

Учитывая, что в указанном обработчике уже имеется вызов функции yellowLedToggle, удалим, перейдя в main.S, аналогичную строку в цикле main_loop и, кроме того, добавим:
• вектор прерывания таймера,
• вызов функции timerInit.

main.S
.include "nrf52832.h"
.include "macro.h"
  .data

  .text
    .org 0
    /* Указать на вершину стека */
      stackPointerInit
    .org Reset_vector
      .word main + 1
    .org TIMER2_vector
      .word TIMER2_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      /* Перейти к метке main_loop */
      B  main_loop
.end

Вернувшись к gdb-сеансу в консоли, вновь откомпилируем и загрузим в МК программу посредством команд make и load, соответственно.

Для проверки работы таймера требуется непрерывная работа программы, в связи с чем удалим имеющиеся точки останова:
(gdb) delete
(gdb)

В отсутствие точек останова команда continue замкнёт процессор на пустом цикле main_loop, вывести из которого его можно будет лишь прервав gdb-сеанс сочетанием клавиш Ctrl–c. Поэтому, в таких случаях мы будем использовать для запуска команду monitor reset run, а остановки — monitor halt.

Если в коде файла timer.S нет ошибок, то запустив его исполнение
(gdb) monitor reset run
(gdb

вы должны получить на выводах 24 и 25 периодический сигнал с полу-периодом в 491.6 мс и 32.8 мс, соответственно, как следует из Раздела 4.2 пятой части статьи.

Не останавливая работу МК, можно с помощью команды set «на лету» менять содержимое регистра PRESCALER модуля TIMER2 адресу 0×4000A510, пока не будет подобран подходящий период сигнала на выводе 25, а значит и частота измерений АЦП в последующем. К примеру, следующая последовательность команд
(gdb) set *0x4000A510 = 1
(gdb) set *0x4000A510 = 2
(gdb) set *0x4000A510 = 3
(gdb) set *0x4000A510 = 4

обуславливает на упомянутом выводе сигнал с полу-периодом 8.2, 16.4, 32.8 и 65.5 миллисекунд, соответственно.

Остановим работу программы
(gdb) monitor halt
target halted due to debug-request, current mode: Thread 
...
(gdb)

и займёмся подключением и отладкой кода управления ШИМ.

В первую очередь пополним соответствующим образом Makefile:
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -g -o main.o
  arm-none-eabi-as led.S -mcpu=cortex-m4 -g -o led.o
  arm-none-eabi-as timer.S -mcpu=cortex-m4 -g -o timer.o
  arm-none-eabi-as pwm.S -mcpu=cortex-m4 -g -o pwm.o
  arm-none-eabi-ld main.o led.o timer.o pwm.o -T LinkerScript.ld -o lightControl.elf

Затем перейдём к main.S и добавим в:
• main — инициализацию модуля ШИМ.
• main_loop — строки перезапуска последовательности PWM0, скопировав их из обработчика прерывания АЦП, что позволит нам менять период и скважность ШИМ-сигнала в ходе последующей отладки.

main.S
main:
  /* Настроить выводы управления светодиодами */
  BL  ledsInit
  /* Настроить таймер */
  BL  timernit
  /* Настроить модуль PWM */
  BL  pwmInit
main_loop:
  /* Перезапустить последовательность PWM0 */
  LDR r0, =(PWM0 + PWM_TASKS_SEQSTART_0)
  LDR r1, =1
  STR r1, [r0]
  /* Перейти к метке main_loop */
  B   main_loop

Поскольку теперь вывод 25 будет управляться модулем ШИМ, удалим из обработчика прерывания таймера внесённые туда чуть ранее строки переключения состояния зелёного светодиода, а затем сохраним все изменения и обновим прошивку МК, набрав из консоли последовательно команды make и load.

Для того, чтобы вручную регулировать параметры ШИМ-сигнала, нам вновь понадобится остановка на строке возврата к началу цикла основной функции, поэтому выясним её текущий номер (у меня она — 28-я) и командами:
• break — назначим точку останова,
• monitor reset init — сбросим МК,
• continue — запустим работу программы.

В отсутствие ошибок в pwm.S на выводе 25 nRF52832 генерируется ШИМ-сигнал, период и скважность которого определяются значениями регистра COUNTERTOP модуля PWM0 (адрес — 0×4001C508) и переменной pwmSeq, соответственно. Меняя их при каждой остановке через команду set, можно наблюдать с помощью осциллографа текущие параметры сигнала на указанном выводе. В частности, последовательность команд
(gdb) set pwmSeq = 100
(gdb) continue
...
(gdb) set pwmSeq = 8192
(gdb) continue
...
(gdb) set pwmSeq = 16283
(gdb) continue
...
(gdb)

приводит к следующим изменениям на выводе управления зелёным светодиодом

Рисунок 7. Изменение параметров ШИМ-сигнала через значение переменной pwmSeq.


Чтобы увидеть взаимодействие модулей GPIO, таймера и ШИМ в динамике, достаточно при очередной остановке посредством команды:
а) disable — приостановить действие точки останова,
б) monitor reset run — запустить непрерывную работу программы, останавливая её, при необходимости, командой monitor halt.

Наиболее вероятной, помимо семантических и технологических, ошибкой в управлении работой модуля ШИМ nRF52832 является установка 15-го бита переменной pwmSeq в состояние, отличное от первоначального, что приводит к смене полярности ШИМ-сигнала. Поскольку в функции pwmInit мы определили значение указанного бита равным нулю, следует избегать в последующем записи в pwmSeq числа, большего, чем 16383.

Осталось «написать» функции файла adc.S, что в нашем случае означает приведение программы к её первоначальному виду. Чтобы не отматывать назад все произведённые выше изменения в коде, просто ещё раз скопируем из архива пятой части статьи файлы main.S, led.S и timer.S в папку debug, поверх уже имеющихся там.

На данном этапе отладки необходимо, помимо поиска ошибок, убедиться в правильном подборе настроек АЦП (источник опорного напряжения, усиление и др.). Особое внимание следует уделить тому, чтобы разрешение АЦП и разрядность счётчика модуля ШИМ имели одно и то же значение (в нашем случае это — 14), в противном случае неизбежны искажения при передаче данных из переменной adcValue в pwmSeq.

Показателем корректности кода adc.S будут:
• срабатывание прерывания АЦП по завершению измерения,
• изменение значения переменной pwmSeq в диапазоне от 0 до 16383 при повороте ручки потенциометра, подключённого к выводу 2 nRF52832.

Добавив в Makefile данные по файлам adc
arm-none-eabi-as adc.S -mcpu=cortex-m4 -g -o adc.o
arm-none-eabi-ld main.o led.o timer.o pwm.o adc.o -T LinkerScript.ld -o lightControl.elf

откомпилируем из консоли программу и загрузим обновлённый elf-файл в МК, применив команды make и load, соответственно.

Для начала, отладим код в «ручном» режиме, в связи с чем организуем точку останова в обработчике SAADC_Handler на строке 20, перезапускающей последовательность PWM0:
(gdb) break adc.S: 20
...
(gdb)

Кроме того, заблокируем (команда disable) или удалим (команда delete) назначенную ранее точку останова в main.S, необходимости в которой в данном случае нет.

Сбросим МК и запустим работу программы:
(gdb) monitor reset init
...
(gdb) continue
Continuing.
Breakpoint 1, SAADC_Handler () at adc.S:20
20 LDR  r0, =(PWM0 + PWM_TASKS_SEQSTART_0)
(gdb)

Перед тем, как подойти к строке 20 обработчика прерывания АЦП, процессор копирует через РОН r1 значение переменной adcValue в pwmSeq, следовательно их значения в точке останова должны быть одинаковыми. Убедиться, так ли это на самом деле, можно, меняя перед каждой командой continue положение ручки потенциометра, а после остановки процессора — выводя в консоль командой print текущие значения r1, adcValue и pwmSeq:
(gdb) continue
...
(gdb) print adcValue&16383
$16 = 16380
(gdb) print $r1
$17 = 16380
(gdb) print pwmSeq
$18 = 16380
(gdb) continue
...
(gdb) print adcValue&16383
$19 = 7184
(gdb) print $r1
$20 = 7184
(gdb) print pwmSeq
$21 = 7184
(gdb) continue
...
(gdb) print adcValue&16383
$22 = 0
(gdb) print $r1
$23 = 0
(gdb) print pwmSeq
$24 = 0

Как видите, в команде print для переменной adcValue применена логическая операция И с числом 16383, чтобы обнулить её незначащие биты 15—31.

Параллельно со значениями регистров в консоли должна меняться и яркость зелёного светодиода или скважность сигнала на выводе 25, в чём можно удостовериться с помощью осциллографа.

В заключение отладки необходимо удалить все точки останова и, запустив через команду monitor reset run непрерывную работу программы, убедиться в удовлетворительной работе всего устройства в целом. После этого, можно завершить формирование Makefile, добавив строки создания и загрузки в МК hex-файла.
arm-none-eabi-objcopy lightControl.elf -S -O ihex lightControl.hex
openocd -f interface/stlink.cfg -f target/nrf52.cfg -c "init" -c "reset init" -c "flash write_image erase lightControl.hex" -c "reset" -c "exit"


Отладка с помощью UART для ATmega8

В случаях, когда выбранные вами микроконтроллер или программатор не поддерживают аппаратную отладку (например, ATmega8 и USBasp), можно использовать ещё один вариант проверки кода, суть которого заключается в том, чтобы зациклить процессор в требуемом месте программы, определив в качестве условия выхода из цикла поступление по UART определённого символа.

Данный способ менее гибок, чем классическая отладка, поскольку:
1. Не останавливает реально процессор, а лишь заставляет ходить по кругу. При этом, тактируемая параллельно ему периферия (АЦП, таймер и др.) продолжает свою работу.

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

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

Соединим устройство на базе Atmega8 с компьютером через программатор UASBasp. Кроме того, обеспечим связь между микроконтроллером и программой Terminal через USB-UART адаптер так, как это было показано в Разделе 2.2 второй части статьи.

Для организации обмена данными воспользуемся ранее созданной библиотекой libAVR.a из папки library, которая, помимо уже знакомых вам uartUnit и uartSendByte, содержит функции:
uartReceiveByte, ответственную за приём одного байта,
breakPoint, которая, по сути, является циклом, ожидающим поступления символа «s» для завершения своей работы.

Начнём с проверки кода функций управления светодиодами, для чего перепишем соответствующим образом файлы main.S и Makefile:
main.S
.include "atmega8.h" 
.include "macro.h"
  .data

  .text
    .org Reset_vector
      RJMP   main

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
    main_loop:			
      /* Изменить состояние жёлтого светодиода на противоположное */
      RCALL  yellowLedToggle
      /* Зациклить процессор */
      RCALL  breakPoint
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end

Как видите, в main, помимо вызова функции ledsInit, добавлена инициализация протокола UART, а в main_loop — зацикливание процессора посредство функции breakPoint.

Makefile
all:
  avr-as main.S -mmcu=atmega8 -o main.o
  avr-as led.S -mmcu=atmega8 -o led.o
  avr-ld main.o led.o -L c:/library -lAVR -T LinkerScript.ld -o lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i

Обратите внимание, что применение опции -g утилиты as в данном случае не требуется, поскольку в МК будет загружаться hex-, а не elf-файл.

Компиляция и загрузка программы в МК командой «make» из консоли приведёт ко включению жёлтого светодиода на выводе PB0 при условии, что в коде led.S нет ошибок. При этом, отправляя символ «s» из Terminal, вы можете менять состояние указанного вывода на противоположное.


Рисунок 8. Передача символа «s» для выхода из цикла breakPoint.

Временно заменив в led.h значение макроопределения GREEN_LED на PB0, можно таким же образом убедиться в корректности настроек для зелёного светодиода после повторной компиляции и загрузки кода в МК.

Для отладки кода timer.S внесём дополнения в Makefile
all:
  avr-as main.S -mmcu=atmega8 -o main.o
  avr-as led.S -mmcu=atmega8 -o led.o
  avr-as timer.S -mmcu=atmega8 -o timer.o
  avr-ld main.o led.o timer.o -L c:/library -lAVR -T LinkerScript.ld -o lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i
,
а затем по очереди проверим:
• генерацию ШИМ-сигнала на выводе PB1,
• срабатывание прерывания таймера.

На первом этапе приведём main.S к следующему виду:
.include "atmega8.h"
.include "macro.h"
  .data

  .text
    .org Reset_vector
      RJMP   main

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end

Как видите, в коде пока отсутствуют строки, обуславливающие работу прерывания таймера — вектор и инструкция SEI глобального разрешения прерываний.

Далее, меняя значение макроопределения OCR1A_VALUE в timer.h, а затем компилируя и загружая программу в МК, вы можете посредством осциллографа проверить параметры ШИМ-сигнала на выводе PB1, взяв за основу расчёты из Раздела 2.2.3 пятой части статьи. В качестве примера, на рисунке 9 представлены осциллограммы для значений 10, 127 и 245 упомянутого макроопределения.

Рисунок 9. Параметры ШИМ-сигнала на выводе PB1 в зависимости от значения OCR1A_VALUE.

На втором этапе включим в main.S вектор прерывания таймера, а также разрешим прерывания глобально:
.include "atmega8.h"
.include "macro.h"
  .data

  .text
    .org Reset_vector
      RJMP   main
    .org TIMER1_OVF_vector * 2
      RJMP   TIMER1_OVF_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end

Помимо этого, в timer.s временно заменим строку обработчика прерывания, запускающую измерение АЦП
/* Запустить измерение АЦП */
RCALL  adcStartConversion

на фрагмент, переключающий пин PB1 в противоположное состояние
/* Изменить состояние зелёного светодиода на противоположное */
LDS  r16, PORTB
LDI  r17, (1 << PB1)
EOR  r16, r17
STS  PORTB, r16
,
а также закомментируем в функции timerInit строку, разрешающую ШИМ-сигнал
setBit  TCCR1A, COM1A1

Вышеприведённые меры позволят нам подобрать будущую частоту измерений аналогового напряжения через период сигнала на выводе PB1, исключив при этом влияние ШИМ на состояние последнего.

Далее, устанавливая ту или иную комбинацию битов CS регистра TCCR1B в timerInit, а затем компилируя и загружая код в микроконтроллер, определим оптимальную частоту сэмплирования АЦП, исходя из осциллограммы для вывода управления зелёным светодиодом.

Внесём недостающие записи в Makefile
all:
  avr-as main.S -mmcu=atmega8 -o main.o
  avr-as led.S -mmcu=atmega8 -o led.o
  avr-as timer.S -mmcu=atmega8 -o timer.o
  avr-as adc.S -mmcu=atmega8 -o adc.o
  avr-ld main.o led.o timer.o adc.o -L c:/library -lAVR -T LinkerScript.ld -o lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i

и займёмся отладкой кода adc.S, реализовать которую проще всего, если оставить в main.S лишь фрагменты, имеющие отношение к UART и АЦП, добавив в main_loop запуск измерений:
.include "atmega8.h"
.include "macro.h"
  .data

  .text
    .org   Reset_vector
      RJMP   main
    .org ADC_vector * 2
      RJMP   ADC_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить АЦП */
      RCALL  adcInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Запустить измерение АЦП */
      adcStartConversion
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end

Как вы помните:
• скважность ШИМ-сигнала определяет значение старшего байта ADCH регистра данных АЦП,
• для отправки числа в Terminal, необходимо поместить его в РОН r16.

Учитывая вышеизложенное, перепишем обработчик прерывания АЦП следующим образом:
ADC_Handler:
  LDS    r17, ADCL
  LDS    r16, ADCH
  RCALL  uartSendByte
  RCALL  breakPoint
  RETI

Откомпилировав и загрузив программу в МК, мы получим остановку при каждом переходе процессора в обработчик с отражением в поле принимаемых данных Terminal текущего значения ADCH. Изменяя положение ручки потенциометра, подключённого к выводу PC2 микроконтроллера, и выводя процессор из цикла посредством отправки символа «s» из Terminal, необходимо убедиться, что значение ADCH меняется в диапазоне от 0 до 255.

Рисунок 10. Зависимость значения регистра ADCH от уровня аналогового напряжения на пине PC2.

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

Формирование и загрузка в МК hex-файла

После устранения ошибок в программе наступает очередь пятого этапа из Рисунка 1, в ходе которого утилита objcopy, опираясь на данные из elf-файла и задаваемые нами опции, формирует hex-файл в формате согласно [9].

Для рассматриваемых нами МК, hex-файл разбит на строки, именуемые записями (record), которые состоят из следующих полей:

Рисунок 11. Формат строки hex-файла.

Коротко, о назначении каждого поля.

RECORD MARK выражается символом «:» и обозначает начало строки.

RECLEN отражает количество байт в поле INFO/DATA.

LOAD OFFSET определяет адрес во flash-памяти МК, начиная с которого будут загружаться данные из поля INFO/DATA.

RECTYPE означает тип основной информации, содержащейся в данной строке, и может принимать следующие значения:
• 00 — данные для загрузки в память программ микроконтроллера,
• 01 — конец файла,
• 02 — расширенный адрес сегмента,
• 03 — начальный адрес сегмента,
• 04 — расширенный линейный адрес,
• 05 — начальный линейный адрес.

INFO/DATA дублирует шестнадцатеричный код вашей программы из соответствующих строк elf-файла.

CHKSUM — байт контрольной суммы, значение которого подбирается так, чтобы сумма байтов данной строки, включая и сам CHKSUM, давала ноль в младшем байте.

На рисунке 12 из проекта lightControl для ATmega8 представлены:
а) фрагменты elf-файла с кодом, загружаемым по адресам с 0×00 по 0×1e flash-памяти МК.
б) первые две и последняя строки hex-файла с выделенными, для наглядности, полями строк.

Рисунок 12. Выдержки из а) elf— и б) hex-файла проекта lightControl для МК ATmega8.

Как видите,
1. Для первой строки hex-файла:
• Значения полей RECLEN и RECTYPE составляют 0×10 (16 — в десятичном исчислении) и 0×00, соответственно, следовательно она содержит 16 байт данных для загрузки в память программ МК, начиная с адреса 0×0000, как следует из значения поля LOAD OFFSET.
• Поле INFO/DATA начинает шестнадцатеричный код инструкции перехода к main, выделенный рамкой жёлтого цвета в обеих частях рисунка. Значения следующих пятнадцати байтов поля данных равны нулю, поскольку вектора прерываний с соответствующими адресами в проекте не используются.
• Сумма значений вышеперечисленных байтов с числом 0×22 из поля CHKSUM равна 0×100, т. е. даёт ноль в младшем байте.

2. Вторая строка также несёт в себе 16 байт данных, предназначенных для записи во flash-память микроконтроллера, в т. ч. по адресу:
• 0×0010 — выделенный на рисунке рамкой зелёного цвета шестнадцатеричный код инструкции перехода к обработчику прерывания таймера,
• с 0×0012 по 0×001b — нули, поскольку соответствующие вектора прерываний в проекте не задействованы.
• 0×001c и 0×001e — обрамлённые на рисунке красным цветом коды инструкции перехода к обработчику прерывания АЦП и первой инструкции макроса stackPointerInit, соответственно.

Значение байта контрольной суммы — 0xF1, что в сумме по второй строке даёт число 0×400 с нулевым значением младшего байта.

3. Значение 01 поля RECTYPE последней строки свидетельствует об отсутствии ниже какой-либо информации для загрузки в МК. Сумма значений 0×01 и 0xFF байтов строки — 0×100.

На последнем, шестом этапе из Рисунка 1 осуществляется перевод шестнадцатеричного кода hex-файла в бинарный с загрузкой последнего в микроконтроллер. В рамках настоящей статьи этот процесс реализовывался посредством запуска из консоли утилит avrdude или OpenOCD. Однако при желании вы можете использовать для прошивки и другие программные инструменты, в частности AVRDUDE_PROG 3.3, ST-LINK utility или nRFgo Studio в случае с AVR-8, STM32 или nRF52, соответственно.

Протокол обмена, применяемый для загрузки программы, определяется возможностями микроконтроллера и программатора. В нашем случае это:
SPI — для ATtiny85 и ATmega8,
SWD — для STM32F401 и nRF52832.

Оптимизация Makefile

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

Имеются претензии и к Makefile, наиболее существенные из которых следующие:
1. Текущая структура файла является причиной того, что после каждой команды «make» запускаются все процедуры подряд, в то время как в реальности загрузка исполняемого файла в МК осуществляется лишь после неоднократной компиляции и компоновки программы с целью выявить и устранить ошибки в коде. Кроме того, все без исключения генерируемые файлы (o, elf и hex) обновляются даже, если ни в один исходный ассемблер-файл не вносятся какие-либо изменения, т. е. попусту расходуются ресурсы вашего компьютера.

2. Переход на другой проект или МК неизбежно повлечёт за собой необходимость внесения изменений в части имён и состава исходных и генерируемых файлов, а также опций утилит. Делать это придётся непосредственно в строках вызова той или иной утилиты, что сопряжено с риском случайно что-либо не то изменить или удалить.

3. Число строк вызова утилиты as (читай — объём работ по их оформлению) на данный момент определяется количеством ассемблер-файлов, а ведь их может быть десятки.

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

Общие сведения

Задача Makefile — заставить определённым образом работать программу make [6], что достигается посредством правил (rules), имеющих следующий вид:
цель: зависимость
  рецепт

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

Коротко о составляющих правила:
• Цель (target) указывает на начало правила. Кроме того, в определённых случаях цель может означать результат работы рецепта и правила в целом, о чём — чуть ниже.
• Зависимость (prerequisite), так же как и цель, имеет двойное назначение. Во-первых, она может указывать на одноимённую цель другого правила, оформленного ниже текущего, а во-вторых — определять имя входного файла для рецепта данного правила. В случае, если зависимостей несколько, их отделяют знаком пробела. Не редки случаи, когда зависимости отсутствуют вовсе.
• Каждый из рецептов (recipe), которых может быть несколько, обязательно предваряется знаком табуляции и определяет действия make в рамках данного правила. Чаще всего это — вызов утилит gcc.

Если вызывающая её из консоли команда не имеет аргументов, make придерживается следующего порядка в обработке Makefile:
1. Дойдя до первого сверху правила определяет, есть ли у него зависимость. При отсутствии последней, программа исполняет рецепты данного правила и завершает на этом работу.

2. Если зависимость имеется, выясняется, не совпадает ли её имя с именем цели другого правила, прописанного ниже. В случае отрицательного ответа работа, опять-таки, завершается после исполнения рецептов правила.

3. В случае совпадения имён из двух правил согласно п. 2, make, не исполняя рецепты первого правила, переходит ко второму и повторяет проверки из п. п. 1-2 уже для него.

4. Цепь проверок и переходов продолжается до тех пор, пока не будет обнаружено правило без зависимости либо с именем зависимости, не совпадающим с именем цели ни одного из правил ниже. Тогда, программа исполняет рецепты этого правила и в обратном порядке возвращается к предыдущим, исполняя по ходу уже их рецепты.

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

Оптимизация структуры Makefile

С целью проверить вышеописанный алгоритм, а также обособить этап загрузки, заменим единственное правило Makefile проекта lightControl на четыре:
copy: link
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex

link: compile
  avr-ld main.o led.o timer.o adc.o -T LinkerScript.ld -o lightControl.elf

compile:
  avr-as main.S -mmcu=atmega8 -o main.o	
  avr-as led.S -mmcu=atmega8 -o led.o
  avr-as timer.S -mmcu=atmega8 -o timer.o
  avr-as adc.S -mmcu=atmega8 -o adc.o

upload:	
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i

Теперь, по команде «make» одноимённая программа, добравшись по результатам проверок до не имеющего зависимостей правила с целью compile, запустит компиляцию ассемблер-файлов, а после её завершения двинется в обратный путь, исполняя рецепты правила с целью link, а затем — copy.

Загрузку hex-файла в МК можно запустить только, если набрать в консоли команду «make upload».

До этого момента мы использовали форму правила c произвольными именами цели и зависимости, причём:
• Цель лишь указывала на начало правила. Кстати, такие цели принято называть «фальшивыми» (phony targets).
• Зависимость, если таковая имелась, использовалась только для поиска другого правила с одноимённой целью.

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

Чтобы понять, о чём идёт речь, перепишем Makefile следующим образом:
all: lightControl.hex

lightControl.hex: lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex

lightControl.elf: led.o timer.o adc.o main.o
  avr-ld main.o led.o timer.o adc.o -T LinkerScript.ld -o lightControl.elf

main.o: main.S
  avr-as main.S -mmcu=atmega8 -o main.o

led.o: led.S
  avr-as led.S -mmcu=atmega8 -o led.o

timer.o: timer.S
  avr-as timer.S -mmcu=atmega8 -o timer.o

adc.o: adc.S
  avr-as adc.S -mmcu=atmega8 -o adc.o

upload:
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i

Как видите,
а) Сверху добавлено пустое правило с фальшивой целью all и зависимостью lightControl.hex.

б) В правилах, обуславливающих компиляцию, компоновку и преобразование elf-файла в hex:
• цель, помимо указания на начало правила, ещё и определяет имя и тип файла, который должен получиться в результате исполнения данного правила,
• зависимость может, по-прежнему, служить для перехода к другому правилу, но теперь также указывает утилите из рецепта данного правила, какой файл нужно использовать в качестве входного.

в) Правило компиляции раздроблено на четыре, по одному — на каждый S-файл.

г) Загрузка программы в МК по-прежнему запускается правилом с фальшивой целью upload.

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

Присутствие имён файлов в качестве цели и зависимости правила даёт следующие важные преимущества:
1. Становятся доступными такие фишки make, как шаблонные и неявные правила, автоматические переменные, а также некоторые директивы и функции, применение которых позволяет минимизировать объём Makefile и сделать его универсальным.

2. Включается заложенный разработчиками make механизм, когда правило пропускается без исполнения, если уже имеющийся файл-цель создан позже файла-зависимости. Попробуйте, предварительно удалив из папки проекта ранее созданные объектные и исполнительные файлы, последовательно дважды набрать в консоли команду «make», не меняя между командами содержимое ассемблер-файлов. В первый раз будут, как и положено, сгенерированы объектные, elf- и hex-файлы. Но, при второй попытке в консоли появится сообщение «Nothing to be done for 'all'», означающее, что исполнять all и следующие за ним правила, а значит и обновлять сгенерированные ранее файлы не имеет смысла, поскольку исходные файлы не корректировались. Если же внести изменения только в определённый, например main.S, файл, то при втором проходе обновятся лишь зависимые от него main.o, lightControl.elf и lightControl.hex. Таким образом, мы снимаем поставленный в начале главы вопрос нерационального использования ресурсов компьютера.

Переменные

Переменные играют ту же роль, что и макроопределения в ассемблер-файле — дают условное имя фрагменту Makefile. Так же, как и препроцессор as в случае с макроопределениями, make меняет любую встреченную на своём пути переменную на скрытую за ней сущность.

Оформляется переменная следующим образом:
ИМЯ_ПЕРЕМЕННОЙ = значение переменной

Допускается дробление сложно-составных значений переменной посредством символа «+»:
ИМЯ ПЕРЕМЕННОЙ = составляющая 1 значения переменной
ИМЯ ПЕРЕМЕННОЙ += составляющая 2 значения переменной
...
ИМЯ ПЕРЕМЕННОЙ += составляющая n значения переменной

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

В тексте правила объявленная переменная прописывается в круглых скобках и предваряется специальным символом «$»:
$(ИМЯ ПЕРЕМЕННОЙ)

Конкретный состав переменных и степень дробления их значений — дело вашей фантазии. Тем не менее, позволю себе заметить, что объявление в виде отдельных переменных нижеследующих параметров проекта существенно повышает гибкость Makefile:
• имя выходных файлов,
• тип МК или ядра,
• пути к файлам проекта,
• имена и опции утилит.

Прописывать переменные можно непосредственно в верхней части Makefile, однако при таком подходе последний теряет в универсальности. Поэтому, создадим для этих целей в папке проекта вспомогательный файл variables.mk, чтобы затем подключить его из Makefile посредством директивы include, которая, в отличие от своей коллеги из as, не предваряется точкой.

Тогда, один из возможных вариантов использования переменных будет выглядеть так:
variables.mk
# Основные параметры проекта
FILE          = lightControl
MCU           = atmega8
# Список объектных файлов
OBJECTS       = main.o led.o timer.o adc.o
# Утилиты GCC и их опции
PREAMBLE      = avr-
COMPILER      = $(PREAMBLE)as
  CC_OPTIONS  = -mmcu=$(MCU)
  CC_OPTIONS  += -o
LINKER        = $(PREAMBLE)ld
  L_OPTIONS   = -T LinkerScript.ld 
  L_OPTIONS   += -o
COPIER        = $(PREAMBLE)objcopy
  CP_OPTIONS  = -j .text
  CP_OPTIONS  += -j .data
  CP_OPTIONS  += -O ihex
# Загрузчик и его опции
UPLOADER      = avrdude
  U_OPTIONS   = -p $(MCU)
  U_OPTIONS   += -c usbasp
  U_OPTIONS   += -P usb
  U_OPTIONS   += -e
  U_OPTIONS   += -U flash:w:$(FILE).hex:i

Как видите:
1. В первые строки variables.mk вынесены переменные имени исполняемых файлов и типа МК. Обусловлено это тем, что окончательный вариант Makefile позволит в большинстве случаев переносить его из проекта в проект, меняя только значения указанных переменных.

2. Отдельными переменными оформлены:
• список объектных файлов,
• преамбула утилит gcc, чтобы упростить адаптацию Makefile под проекты для МК с ядром ARM.

3. Значения всех остальных переменных — фрагменты рецептов последнего Makefile.

Помимо обычных переменных, создаваемых пользователем, разработчики встроили в make и так называемые автоматические переменные, которые применяются для обозначения цели и зависимостей правила, представленных в виде имён файлов. В частности, автоматическая переменная:
$@ означает цель,
$< — первую из всех имеющихся зависимостей,
$^ — набор всех зависимостей.

Перепишем наш Makefile с учётом информации, изложенной выше:
include variables.mk

all: $(FILE).hex

$(FILE).hex: $(FILE).elf
  $(COPIER) $< $(CP_OPTIONS) $@

$(FILE).elf: $(OBJECTS)
  $(LINKER) $^ $(L_OPTIONS) $@

main.o: main.S
  $(COMPILER) $< $(CC_OPTIONS) $@

led.o: led.S
  $(COMPILER) $< $(CC_OPTIONS) $@

timer.o: timer.S
  $(COMPILER) $< $(CC_OPTIONS) $@

adc.o: adc.S
  $(COMPILER) $< $(CC_OPTIONS) $@

upload:
  $(UPLOADER) $(U_OPTIONS)

Если при взгляде на этот секретный шифр у вас возникли мысли о подвохе, просто замените все переменные их значениями и вы получите предыдущую версию Makefile.

Для проверки работы файла variables.mk и обновлённого Makefile вам придётся удалить из папки проекта сгенерированные ранее объектные файлы, чтобы не сработало упомянутое выше правило с сообщением «Nothing to be done for 'all'».

Шаблонные правила

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

Оформляется шаблонное правило посредством специального символа «%» и в общем случае выглядит так:
%.ext1: %.ext2
рецепт

Встретив такое правило, make для каждого файла с расширением ext2 из папки (или под-папок) проекта исполняет рецепт, используя указанный файл в качестве входного. При этом генерируется выходной файл с таким же именем и расширением ext1.

Оформим в виде шаблонных правила компиляции и компоновки из нашего Makefile:
include variables.mk

all: $(FILE).hex

$(FILE).hex: $(FILE).elf
  $(COPIER) $< $(CP_OPTIONS) $@

%.elf: $(OBJECTS)
  $(LINKER) $^ $(L_OPTIONS) $@

%.o: %.S
  $(COMPILER) $< $(CC_OPTIONS) $@

upload:	
  $(UPLOADER) $(U_OPTIONS)

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

Удалив старые объектные и исполнительные файлы, а затем запустив компиляцию, вы обнаружите ещё одну выгоду от использования шаблонов: после генерации elf-файла запускается неявное правило, которое автоматически удаляет не нужные нам, но захламляющие папку проекта объектные файлы.

Функции

У нас всё ещё остаётся параметр Makefile, требующий ручной работы, объём которой может быть значительным — список объектных файлов из переменной OBJECTS. Кроме того, мы до сих пор не сняли вопрос с файловой структурой проекта. Устранить эти досадные недостатки позволят функции make.

Прежде всего, исключим зависимость содержимого el-файла от очерёдности имён объектных файлов в правиле компоновки, для чего дополним структуру ассемблер-файлов под-секциями и внесём изменения в LinkerScript.ld так, как мы это делали в Разделе 4.1 настоящей части статьи.

Далее, создадим в папке проекта под-папки src и inc, куда перенесём исходные и хидер-файлы, соответственно. Кроме того, договоримся хранить исполняемые файлы в под-папке exe, обязанности по созданию которой возложим на make.

В файле variables.mk:
1. Добавим раздел «Папки хранения файлов» с переменными для обозначения имён вышеупомянутых под-папок:
SRC_DIR  = src
INC_DIR  = inc
EXE_DIR  = exe

2. Укажем утилите as новый путь к хидер-файлам, приплюсовав к переменной CC_OPTIONS опцию -I с именем соответствующей папки:
CC_OPTIONS  += -I $(INC_DIR)

3. Автоматизируем формирование значения переменной OBJECTS из раздела «Список объектных файлов», для чего:
а) с помощью функции wildcard определим полный список S-файлов в под-папке src:
SOURCES  = $(wildcard $(SRC_DIR)/*.S)

б) через функцию patsubst обяжем make давать объектным файлам те же имена, что и у исходных. Замечу, что вы можете выбрать одну из двух форм записи указанной функции:
# Полная форма 
OBJECTS  = $(patsubst %.S,%.o,$(SOURCES))
# Краткая форма 
OBJECTS  = $(SOURCES:.S=.o)

4. Учтём в переменной U_OPTIONS изменения в локации hex-файла:
U_OPTIONS += -U flash:w:$(EXE_DIR)/$(FILE).hex:i

Окончательно, файл variables.mk будет выглядеть так:
# Основные параметры проекта
FILE          = lightControl
MCU           = atmega8
# Папки хранения файлов
SRC_DIR       = src
INC_DIR       = inc
EXE_DIR       = exe
# Список объектных файлов
SOURCES       = $(wildcard $(SRC_DIR)/*.S)
OBJECTS       = $(SOURCES:.S=.o)
# Утилиты GCC и их опции
PREAMBLE      = avr-
COMPILER      = $(PREAMBLE)as
  CC_OPTIONS  = -mmcu=$(MCU)
  CC_OPTIONS  += -I $(INC_DIR)
  CC_OPTIONS  += -o
LINKER        = $(PREAMBLE)ld
  L_OPTIONS   = -T LinkerScript.ld 
  L_OPTIONS   += -o
COPIER        = $(PREAMBLE)objcopy
  CP_OPTIONS  = -j .text
  CP_OPTIONS  += -j .data
  CP_OPTIONS  += -O ihex
# Загрузчик и его опции
UPLOADER      = avrdude
  U_OPTIONS   = -p $(MCU)
  U_OPTIONS   += -c usbasp
  U_OPTIONS   += -P usb
  U_OPTIONS   += -e
  U_OPTIONS += -U flash:w:$(EXE_DIR)/$(FILE).hex:i

Осталось добавить в правило компоновки командную строку создания под-папки exe посредством утилиты mkdir и указать путь к этой папке в правиле преобразования elf-файла в hex, после чего Makefile примет следующий вид:

include variables.mk

all: $(FILE).hex

$(FILE).hex: $(EXE_DIR)/$(FILE).elf
  $(COPIER) $< $(CP_OPTIONS) $(EXE_DIR)/$@

%.elf: $(OBJECTS)
  mkdir -p $(EXE_DIR)
  $(LINKER) $^ $(L_OPTIONS) $@

%.o: %.S
  $(COMPILER) $< $(CC_OPTIONS) $@

upload:
  $(UPLOADER) $(U_OPTIONS)


Перед вами — универсальная версия Makefile, которую можно переносить из проекта в проект, не зависимо от типа МК, а также количества и имён ассемблер-файлов, при соблюдении, разумеется, файловой структуры (т.е. хранении исходных и хидер-файлов в под-папках src и inc, соответственно). Корректировки придётся вносить только в файл variables.mk.
Чтобы подтвердить справедливость этого утверждения, можете глянуть на содержимое обоих файлов в проекте lightControl для nRF52832 из архива статьи.


Файлы

🎁assembler-7.zip  22.58 Kb ⇣ 45

Полезные ссылки

[1] Using as
[2] Using ld
[3] Debugging with GDB
[4] Open On-Chip Debugger: OpenOCD User’s Guide
[5] AVRDUDE — AVR Downloader/Uploader
[6] GNU make
[7] AVR Instruction Set Manual
[8] ARMv7-M Architecture Reference Manual
[9] Hexadecimal Object File Format Specification

Заключение

На этом цикл статей «Ассемблер для микроконтроллера с нуля» завершается. Основная цель работы — дать читателю понимание того, что механизм работы микроконтроллера мало чем отличается для разных платформ и реализовать его программирование можно в рамках одной среды и компилятора. При этом, я постарался с достаточной подробностью осветить весь процесс: от оформления инструкции в текстовом файле до её загрузки в микроконтроллер и исполнения.

Спасибо за внимание и до новых встреч!

Камрад, рассмотри датагорские рекомендации

🌼 Полезные и проверенные железяки, можно брать

Опробовано в лаборатории редакции или читателями.




 

Читательское голосование

Нравится

Статью одобрили 29 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Программирование на языке С для AVR и PIC микроконтроллеров. Шпак Ю.А.... Программирование на языке С для AVR и PIC микроконтроллеров. Шпак Ю.А. Издательство "МК -...
Схема на Датагоре. Новая статья ATmega8A, ATmega8, ATmega8L datasheet даташит... Фото чипа atmega8a в корпусе dip28 Представляю вам даташит на микроконтроллер ATmega8 фирмы Atmel,...
Схема на Датагоре. Новая статья PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен... PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен пер. с англ. Евстифеева А. В. — М.:...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции... Привет, датагорцы — любители Ассемблера! В пункте 2.5.2 «Инструкции условного перехода» предыдущей...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 4. Система адресации памяти, назначение выводов, тактирование и прерывания МК... Привет датагорцам! Сегодня мы остановимся на следующих вопросах касательно рассматриваемых нами...
Схема на Датагоре. Новая статья Применение микроконтроллеров AVR. Схемы, алгоритмы, программы... Какой микроконтроллер выбрать? Где найти его описание? Где взять программу, обеспечивающую...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI... В проекте из предыдущей части нашей ассемблерной эпопеи мы подключали к микроконтроллеру светодиод...
Схема на Датагоре. Новая статья Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL, Евстифеев А.В.... Издательство: Додэка XXI [М.], 560 стр. 2005 г. Книга посвящена вопросам практического применения...
Схема на Датагоре. Новая статья 10 практических устройств на AVR-микроконтроллерах. Книга 1. А.В. Кравченко... 10 практических устройств на AVR-микроконтроллерах. Книга 1. А.В. Кравченко Издательство: Москва,...
Схема на Датагоре. Новая статья Радиолюбительские конструкции на PIC микроконтроллерах, Кн.3, Н.И. Заец... Радиолюбительские конструкции на PIC микроконтроллерах, Кн.3 Автор: Н.И. Заец Третья книга...
Схема на Датагоре. Новая статья Разработка встроенных систем с помощью микроконтроллеров PIС. Уилмсхерст Т.... Разработка встроенных систем с помощью микроконтроллеров PIС. Уилмсхерст Т. Год издания: 2008...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК... В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы...
 

Комментарии, вопросы, ответы, дополнения, отзывы

 

<
Читатель Датагора

galrad

<
Читатель Датагора

erbol

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой