Начало » Микроконтроллеры » Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции

 
 

Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции

19.01.21   erbol   1 689   0  

Привет, датагорцы — любители Ассемблера!
В пункте 2.5.2 «Инструкции условного перехода» предыдущей части статьи мы переложили на язык инструкций AVR-8 и Cortex M-4 шутливые обязанности ЦПУ из первой части.
Не смотря на то, что полученный в итоге код успешно выполняет поставленную перед ним задачу — эмулирует в терминал печать букв, прихлопы и притопы — подобная структура программы, когда всё её содержимое размещается в одном файле, несёт в себе ряд недостатков.

Выясним, что это за недостатки и как их устранить.

Недостатки подхода «всё в одном»

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

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

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

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

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

Макросы

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

1. В самом верху ассемблер-файла или в отдельном хидер-файле нижеследующим образом оформляется макрос:
.macro  имя макроса
  /* Тело макроса, в котором
     размещается фрагмент кода */
.endm


2. В том месте программы, где должен располагаться фрагмент, составляющий тело макроса, прописывается имя макроса.

3. Компилятор, встретив в коде имя макроса, меняет его на содержимое тела макроса.

Более гибкой в использовании является форма макроса со входными аргументами, которая записывается несколько иначе:
.macro  имя макроса  аргумент 1, аргумент 2, … , аргумент n 
  /* Тело макроса, в котором размещается фрагмент кода, 
     использующий аргументы макроса */
.endm


При этом, в теле макроса необходимо, в соответствии с требованиями GCC, добавлять символ обратной косой черты «\» перед каждым упоминанием имени любого из аргументов.
Практические примеры оформления макросов обоих типов будут приведены чуть ниже.
Размещение макроса в том же файле, в котором он применяется, мало что даёт в плане сокращения объёма текста, поскольку фактически это будет перенос его фрагмента из одного места файла в другое. Большего эффекта можно добиться, если записывать макросы в специально созданный для этого хидер-файл macro.h, а затем подключать его, при необходимости, в ассемблер-файле посредством директивы .include.

Важно понимать, что применение макросов не уменьшает количество инструкций МК в коде (т.е. не приводит к экономии ПП), а лишь позволяет оптимизировать текст программы, визуально сворачивая несколько строк в одну. В связи с этим не стоит ими увлекаться, оформляя как макрос первый попавшийся участок кода. Гораздо разумнее использовать макросы лишь в случаях с фрагментами, которые повторяются от проекта к проекту, не зависимо от назначения этих проектов. При таком подходе можно с полным правом добавить файл macro.h в папку template, доведя общее количество шаблонных файлов до пяти.
Рассмотрим для обеих платформ примеры эффективного использования макросов.

Макросы для AVR-8

Наиболее очевидным и достойным кандидатом на свёртку для этих МК является процедура указания на вершину стека.
Оформим в файле macro.h макрос с именем stackPointerInit:
.macro stackPointerInit
   LDI  r16, lo8(RAMEND)
   OUT  SPL, r16
   LDI  r16, hi8(RAMEND)
   OUT  SPH, r16
.endm

Тогда, соответствующие участки кода в main.S будут выглядеть так:
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"
   
   main:
      /* Указать на вершину стека */ 
      stackPointerInit

Для демонстрации порядка оформления и результата применения макроса со входными аргументами представим, что нам нужно в цикле основной программы устанавливать в 1, а затем сбрасывать в 0:

а) Третий бит PB3 регистра ввода-вывода PORTB, макроопределения которых оформим в шаблонном хидер-файле (attiny85.h или atmega8.h):
PORTB = 0x38
PB3 = 3


б) Первый бит одно-байтной переменной с именем flags, прописанной предварительно в секции данных.

Задача, кстати, вполне реальная и будет реализована в проекте из следующей части статьи.
Вспомним, что пара инструкций LDS/STS обеспечивает доступ к регистрам как периферии, так и SRAM, а ANDI/ORI — осуществление операций И/ИЛИ, соответственно, между содержимым РОН и константой.

Учитывая вышеизложенное, добавим в macro.h к уже имеющемуся там stackPointerInit макросы установки setBit и сброса clearBit бита регистра:
.macro setBit arg_1, arg_2
  /* Считать в r16 значение регистра, определённого аргументом arg_1 */
  LDS   r16, \arg_1
  /* Установить в 1 бит r16, определённый аргументом arg_2 */
  ORI   r16, (1 << \arg_2)
  /* Записать число из r16 в регистр, определённый аргументом arg_1 */
  STS   \arg_1, r16
.endm

.macro clearBit arg_1, arg_2
  /* Считать в r16 значение регистра, определённого аргументом arg_1 */
  LDS   r16, \arg_1
  /* Сбросить в 0 бит r16, определённый аргументом arg_2 */
  ANDI  r16, ~(1 << \arg_2)
  /* Записать число из r16 в регистр, определённый аргументом arg_1 */
  STS   \arg_1, r16
.endm


Обратите внимание на следующие детали:

1. Поскольку в макросах setBit и сlearBit задействован r16, следует убедиться до их применения, что указанный РОН не содержит данные, актуальные для другого фрагмента программы.

2. В связи с использованием пары инструкций LDS/STS, в макроопределениях регистров периферии, по отношению к которым применяются макросы setBit и сlearBit, необходимо указывать абсолютные значения их адресов.

Осталось оформить описанную выше задачу в виде программы:
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

  .data
    flags: .byte 1

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
      /* Указать на вершину стека */   
      stackPointerInit
    main_loop:
      /* Установить и сбросить 3-й бит регистра PORTB */ 
      setBit   PORTB, PB3 
      clearBit PORTB, PB3
      /* Установить и сбросить 1-й бит переменной flags */ 
      setBit   flags, 1
      clearBit flags, 1
      /* Вернуться к началу цикла основной программы */
      RJMP  main_loop
.end

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

Макросы для Cortex M-4

Чтобы не плодить однотипные примеры, рассмотрим условия оформления и применения макросов лишь в отношении STM32F401, поскольку для nRF52832 они — абсолютно такие же.
В качестве примера макроса без аргументов можно оформить в macro.h строку инициализации указателя стека как макрос с таким же, как и для AVR-8, именем stackPointerInit, что ещё хотя бы чуть-чуть сблизит в вашем восприятии два ядра:
.macro  stackPointerInit
  .word  RAMEND
.endm


Продублируем для STM32F401 пример из предыдущего пункта с установкой и сбросом для:

а) Бита 12 регистра ODR порта ввода-вывода GPIOB, прописав в stm32f401.h их макроопределения:
GPIOB = 0x40020400
ODR = 0x14
ODR_12 = 12


б) Второго бита переменной flags, объявив её предварительно в секции данных.

Добавим в macro.h отвечающие поставленной задаче макросы setBit и сlearBit, используя модификации инструкций AND/ORR для проведения операций И/ИЛИ, соответственно, между РОН и константой:
.macro setBit arg_1, arg_2
  /* Считать в r3 значение регистра, определённого аргументом arg_1 */
  LDR  r2, =\arg_1
  LDR  r3, [r2]
  /* Установить в 1 бит r3, определённый аргументом arg_2 */
  ORR  r3, (1 << \arg_2)
  /* Записать число из r3 в регистр, определённый аргументом arg_1 */
  STR  r3, [r2]
.endm

.macro clearBit arg_1, arg_2
  /* Считать в r3 значение регистра, определённого аргументом arg_1 */
  LDR  r2, =\arg_1
  LDR  r3, [r2]
  /* Сбросить в 0 бит r3, определённый аргументом arg_2 */
  AND  r3, ~(1 << \arg_2)
  /* Записать число из r3 в регистр, определённый аргументом arg_1 */
  STR  r3, [r2]
.endm  

Тогда, программа в main.S будет иметь следующий вид:
.include "stm32f401.h"
.include "macro.h"

  .data
    flags: .byte 1

  .text
    .org 0
      stackPointerInit

    .org Reset_vector
      .word main  + 1 

    .global main
    main:
    main_loop:
      /* Установить и сбросить 12-й бит регистра ODR порта GPIOB */ 
      setBit   (GPIOB + ODR), ODR_12
      clearBit (GPIOB + ODR), ODR_12
      /* Установить и сбросить 2-й бит переменной flags */ 
      setBit   flags, 2
      clearBit flags, 2
      /* Вернуться к началу цикла основной программы */
      B    main_loop
.end

Так же, как и в случае с AVR-8, перед применением setBit и сlearBit следует убедиться, не содержат ли РОН r2 и r3 данные, которые могут использоваться другими участками программы.

Функции

В функцию обычно выносятся два типа фрагментов кода:

а) Многократно повторяющиеся внутри одного проекта. В частности, функция delay, присутствующая в программе прихлопов/притопов представляет из себя набор инструкций, замкнутых в цикл, из которого процессор выходит по истечении заданного времени.

б) Участки программы, ответственные за настройку и последующую работу модулей периферии, а также протоколов общения МК с внешними устройствами (I2c, SPI, UART и т.д.). В качестве примера такого случая можно привести функции uartInit и uartSendByte из предыдущей части статьи.

Использование функций позволяет, в отличие от макросов, не только визуально, но и реально сократить размер кода, а следовательно уменьшить объём занимаемой программой flash-памяти.
К оформлению функций предъявляются практически те же требования, что и в случае с основной функцией:
1. Начало функции обозначается посредством метки.
2. Функция, которая должна быть доступна из других файлов, объявляется глобальной с помощью директивы .global.

Ничто не мешает разместить функцию в main.S, прописав её ниже основной функции и до метки .end. Однако, в этом случае мало что меняется в плане читабельности и гибкости кода, в связи с чем лучше изначально взять за привычку структурировать и разносить по нескольким файлам даже небольшую программу. Среди прочего, такой подход позволяет, при необходимости, переносить без изменений файл с нужными функциями из одного проекта в другой. К примеру, файлы uart.o и delayAVR.o (delayCortex.o) из архива предыдущей части статьи довольно часто подключаются мною к новому проекту на этапе отладки написанной программы.

Структура ассемблер-файла, в который выносятся функции, определяется теми же условиями, что и в случае с main.S:

а) Файл разбивается на две секции — .data и .text. Если использование переменных внутри данного файла не предусмотрено, секцию данных можно исключить.

б) Нижнюю границу кода следует отмечать директивой .end.

в) Желательно завершать файл пустой строкой, во избежание замечаний со стороны компилятора.

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

Оптимизируем программу с прихлопами и притопами, оформив подходящие её участки как функции, а заодно выясним, как связана работа функций со спецрегистрами и стеком в случае с AVR-8 и Cortex M-4.
Но, прежде создадим в Notepad++ папку нового проекта с произвольным именем (например, cpuJobDuties, т.е. должностные обязанности ЦПУ) и скопируем в неё следующие файлы для выбранного вами МК:
а) Теперь уже пять, включая macro.h, шаблонных файлов из папки template.
б) Объектные файлы uart.o и delayAVR.o (или delayCortex.o) из архива предыдущей части статьи.

Функции для AVR-8

В предыдущей части статьи, лишь с целью продемонстрировать работу как можно большего числа инструкций, процедуры прихлопов и притопов были оформлены по-разному. Перепишем процедуру притопов с применением тех же инструкций, что и в случае с прихлопами:
 /* Процедура прихлопов */
 CLR   r17
 LDI   r18, CLAPS_MAX_NUM
doClap:
 INC   r17
 LDI   r16, 'c'
 RCALL uartSendByte	
 CP    r17, r18
 BRNE  doClap
 /* Процедура притопов */
 CLR   r17
 LDI   r18, SQUATS_MAX_NUM
doSquat:
 INC   r17	
 LDI   r16, 's'
 RCALL uartSendByte
 CP    r17, r18
 BRNE  doSquat


Учитывая, что и прихлопы, и притопы являются проявлениями одного и того же действия — физических упражнений — вынесем циклическую часть обоих процедур в функцию doExercise. Последняя, размещённая в main.S, будет выглядеть вместе с фрагментами кода, вызывающими её, следующим образом:
CLAPS_MAX_NUM  = 2
SQUATS_MAX_NUM  = 3

  main:	
  main_loop:
     /* Сделать прихлопы */
    CLR   r17
    LDI   r18, CLAPS_MAX_NUM
    LDI   r16, 'c'
    RCALL doExercise
    /* Сделать притопы */
    CLR   r17
    LDI   r18, SQUATS_MAX_NUM
    LDI   r16, 's'
    RCALL doExercise
    /* Перейти к метке main_loop */
    RJMP  main_loop

  doExercise:	
    /* Сделать упражнение */
    INC   r17
    RCALL uartSendByte
    CP    r17, r18
    BRNE  doExercise
    /* Вернуться из функции */
    RET
.end


Как видите:
а) Вызов функции doExercise и возврат из неё обеспечиваются инструкциями RCALL и RET, соответственно.
б) При оформлении функции не использована директива .global, поскольку она является локальной, т.е. находится в том же файле, из которого вызывается.

Прикинем пользу от применения функции в плане экономии памяти программ:
1. Четыре инструкции doExercise (INC, RCALL, CP и BRNE) присутствуют в коде лишь однажды, в то время как в предыдущей части статьи прописывались 4 раза (в doClap_A, doClap_B, doSquat_A и doSquat_B).
2. С другой стороны, добавились инструкция вызова функции RCALL, которая в полной программе будет применяться четырежды (по два раза — на прихлопах и притопах), и инструкция возврата RET, используемая 1 раз.
3. 4 х 4 — 4 — 1 = 11 инструкций.
4. Итого, 11 х 2 байта = 22 байта экономии. И это — результат, полученный только на четырёх инструкциях, а ведь в реальной функции их могут быть десятки.

Продолжим оптимизацию, для чего создадим в папке проекта файл exercises.S, в который перенесём функцию doExercise:
.text	
  doExercise:	
    /* Сохранить в стек текущие значения r17 — r18 */
    PUSH  r17
    PUSH  r18
   /* Сделать упражнение */
   doExercise_loop:
    INC   r17
    RCALL uartSendByte
    CP    r17, r18
    BRNE  doExercise_loop
    /* Восстановить значения r17 — r18 в обратном порядке */
    POP  r18
    POP  r17
    /* Вернуться из функции */
    RET
.end

В данном конкретном случае сохранение в стек и последующее восстановление текущих значений r17r18 не требуется, поскольку нам точно известно, что нигде более, кроме doExercise, они не используются. Поэтому, инструкции PUSH/POP прописаны только в качестве примера порядка их применения.

Заметьте, что в функцию дополнительно введена метка doExercise_loop, имя которой и использует теперь инструкция BRNE в качестве аргумента. Сделано это, чтобы инструкции PUSH не исполнялись несколько раз, попав в цикл.

Полный объём кода в main.S по-прежнему оставляет желать лучшего, в связи с чем добавим в exercises.S ещё одну функцию doBothExercises, в которую перенесём:
а) инструкции очистки r17 и загрузки чисел в r16 и r18,
б) инструкцию вызова функции doExercise,
в) инструкции, обеспечивающие секундную задержку и вывод в Terminal символа пробела.

Туда же, в exercises.S, отправим макроопределения максимального количества прихлопов и притопов:
CLAPS_MAX_NUM  = 2
SQUATS_MAX_NUM  = 3

  .text	
    .global doBothExercises
    doBothExercises:
      /* Сохранить в стек текущие значения r16 — r18 */
      PUSH  r16
      PUSH  r17
      PUSH  r18
      /* Сделать прихлопы */
      CLR   r17
      LDI   r18, CLAPS_MAX_NUM
      LDI   r16, 'c'
      RCALL doExercise			
      /* Сделать притопы */
      CLR   r17
      LDI   r18, SQUATS_MAX_NUM
      LDI   r16, 's'
      RCALL doExercise
      /* Вывести в Terminal символ пробела */
      LDI   r16, 32
      RCALL uartSendByte
      /* Обеспечить задержку в 1 секунду */
      RCALL delay	
      /* Восстановить значения r16 — r18 в обратном порядке */
      POP  r18
      POP  r17
      POP  r16		
      /* Вернуться из функции */
      RET
		
    doExercise:	
      /* Сделать упражнение */
      INC   r17
      RCALL uartSendByte
      CP    r17, r18
      BRNE  doExercise
      /* Вернуться из функции */
      RET
.end


Обратите внимание, что:
1. Функция doBothExercises объявлена глобальной, поскольку будет вызываться из другого файла (main.S), в то время как doExercise — по-прежнему локальная в связи с тем, что обращение к ней происходит лишь в exercises.S.

2. Функция doExercise вызывается из doBothExercises, т.е. является вложенной по отношению к последней.

3. Инструкции сохранения и восстановления значений r17r18 перенесены в doBothExercises и объединены с таковыми для r16. Дублировать их в doExercise не имеет никакого смысла.

4. Метка doExercise_loop в функции doExercise исключена, в связи с отсутствием инструкций PUSH.

В итоге, программа в main.S примет следующий окончательный вид:
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

LAMP_BRIGHTNESS_CURRENT_VALUE = 32
LAMP_BRIGHTNESS_THRESHOLD = 15

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
      stackPointerInit
      RCALL uartInit
    main_loop:
      LDI   r20, LAMP_BRIGHTNESS_CURRENT_VALUE
      CPI   r20, LAMP_BRIGHTNESS_THRESHOLD
      BRLO  type_B
     type_A:	
      LDI   r16, 'A'
      RCALL uartSendByte			
      RCALL doBothExercises
      RJMP  main_loop			
     type_B:	
      LDI   r16, 'B'
      RCALL uartSendByte			
      RCALL doBothExercises
      RJMP  main_loop						
.end

Можете сравнить полученный результат с финальной программой из пункта 2.5.2.1 предыдущей части статьи и сделать выводы о пользе применения макросов и функций.

Чтобы полноценно продемонстрировать влияние работы функций на состояние спецрегистров и стека, вполне достаточно кода, содержащего всего две функции, одна из которых является вложенной. Поэтому, сократим текст нашей программы до следующего минимума:
main.S
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
      stackPointerInit
    main_loop:
      RCALL doBothExercises
      RJMP  main_loop						
.end

exercises.S
CLAPS_MAX_NUM  = 2

  .text	
    .global doBothExercises
    doBothExercises:
      /* Сохранить в стек текущие значения r16 — r18 */
      PUSH  r16
      PUSH  r17
      PUSH  r18
      /* Сделать прихлопы */
      CLR   r17
      LDI   r18, CLAPS_MAX_NUM
      LDI   r16, 'c'
      RCALL doExercise			
      /* Восстановить значения r16 — r18 в обратном порядке */
      POP  r18
      POP  r17
      POP  r16		
      /* Вернуться из функции */
      RET
		
    doExercise:	
      /* Сделать упражнение */
      INC   r17
      CP    r17, r18
      BRNE  doExercise
      /* Вернуться из функции */
      RET
.end

Анимация одного полного цикла работы такой программы для ATtiny85 представлена на Видео 1.

Видео 1. Демонстрация работы программы для МК Attiny85.

Как видите, инструкции из файлов main.S и exercises.S объединены в ходе компиляции и последовательно загружены в память программ МК, начиная с её младшего адреса 0х0000. В левой части каждого кадра отображается состояние РОН, регистров SRAM из области стека и спецрегистров (SP, SREG и PC) после выполнения помеченной маркером инструкции.
Пройдёмся по кадрам видео, начиная с первого:

1. При исполнении инструкции RJMP в программный счётчик РС записывается значение адреса, являющееся аргументом инструкции.

2. По завершению процедуры указания на вершину стека (адрес 0х0008 ПП) составляющие (SPL и SPH) спецрегистра SP содержат значение старшего адреса SRAM – 0х025F.

3. Выполнение инструкции RCALL по адресу 0х000А приводит к следующим последствиям:
а) В РС загружается значение 0х000Е адреса первой инструкции функции doBothExercises.
б) Два байта адреса (0x00 и 0х0C) инструкции, которая должна была быть исполнена следующей, записываются в стек. При этом SPL дважды декрементируется и в итоге SP указывает на следующий свободный регистр стека (0х025D).

4. Каждая инструкция PUSH по адресам 0x000E – 0x0012 обуславливает сохранение в стек текущего значения соответствующего РОН, декремент SPL и увеличение на 2 значения PC.

5. Исполняя следующие три инструкции (адреса 0x0014 – 0x0018 ПП) ЦПУ очищает r17 и загружает в r18 и r16 значения максимального количества прихлопов (0х02) и ASCII кода символа «с» (0х63), соответственно. Поведение РС при этом — такое же, как и в предыдущем пункте.

6. Реакция МК на инструкцию RCALL по адресу 0х001А аналогична случаю из п.3:
а) Значение адреса следующей инструкции (байты 0х00 и 0х1С) записывается в стек, уменьшая при этом значение SP до 0х0258.
б) В РС помещается значение 0х0024 адреса первой инструкции функции doExercise.

7. Согласно инструкций по адресам 0х0024 — 0х0028 ПП процессор будет увеличивать на 1 число в r17, сравнивать числа в r17 и r18 и возвращаться к адресу 0х0024 до тех пор, пока значения чисел в r17 и r18 не сравняются. В этом случае:
а) При исполнении инструкции CP устанавливается в 1 первый бит Z регистра статуса SREG, т.е. значение последнего становится равным 0х02.
б) На инструкции BRNE обнуляется бит Z регистра SREG, а в РС загружается адрес 0x002A следующей ниже инструкции RET.

8. Выполнение инструкции RET по адресу 0х002А ПП приводит к считыванию из стека в РС значения 0х001С адреса возврата и увеличению значения SPL на 2 до 0x5A.

9. При исполнении каждой инструкции POP по адресам 0x0001C – 0x0020:
а) Из стека в соответствующий РОН считывается сохранённое там ранее значение.
б) Число в SPL увеличивается на 1.
в) Значения PC увеличивается на 2.

10. Так же, как и в пункте 8 инструкция RET по адресу 0х0020 ПП обуславливает считывание из стека в РС значения 0х000С адреса возврата и увеличение значения SPL на 2 до 0x5F.

11. По адресу 0x000C ПП осуществляется загрузка в РС значения адреса первой инструкции main_loop, куда и переходит ЦПУ, завершая один круг цикла основной функции.

Как видите картина мало чем отличается от описанной в Главе 7 первой части статьи.
Для Atmega8 всё будет происходить точно так же, за исключением начального значения указателя стека SP: 0x45F вместо 0x25F.

Функции для Cortex M-4

В отличие от AVR-8, процедуры прихлопов и притопов для Cortex M-4 в предыдущей части статьи были изначально оформлены с применением одинаковых инструкций:
  /* Процедура прихлопов */
  LDR  r3, =0
  MOV  r2, CLAPS_MAX_NUM
doClap:
  ADD  r3, 1
  MOV  r1, 'c'
  BL   uartSendByte
  CMP  r3, r2
  BNE  doClap
  /* Процедура притопов */
  LDR  r3, =0
  MOV  r2, SQUATS_MAX_NUM
doSquat:
  ADD  r3, 1
  MOV  r1, 's'
  BL   uartSendByte
  CMP  r3, r2
  BNE  doSquat

Поэтому, можно сразу переписать их как функцию, назвав её doExercise с учётом того, что и прихлопы и притопы являются упражнениями. Но, прежде необходимо прояснить одно важное отличие в механизме вызова функций и возврата из них для МК ARM по сравнению с AVR.
В простейшем случае указанные действия обеспечиваются применением инструкций BL и BX, соответственно:
  main:
  main_loop:  
    /* Вызвать функцию doExercise */
    BL doExercise
    /* Перейти к началу цикла основной функции */
    B  main_loop	

  doExercise:
    /* Вернуться из функции */
    BX LR
.end

При исполнении инструкции вызова BL значение адреса инструкции, подлежащей к исполнению следующей (в данном случае — B main_loop), сохраняется не в стек, как это происходит в AVR-8, а в регистр связей LR. В программный счётчик PC при этом загружается значение адреса первой инструкции вызываемой функции. При возврате из функции посредством инструкции BX значение адреса, хранимое в LR, считывается в PC и процессор переходит к исполнению инструкции, расположенной по указанному адресу (т.е. — той же B main_loop).

Всё это прекрасно работает ровно до тех пор, пока мы не используем вложенные функции.
Добавим в doExercise вызов вложенной функции uartSendByte и посмотрим, что из этого выйдет:
  main:
  main_loop:  
    /* Вызвать функцию doExercise */
    BL doExercise
    /* Перейти к началу цикла основной функции */
    B  main_loop	

  doExercise:
    /* Вызвать вложенную функцию */
    BL uartSendByte
    /* Вернуться из функции */
    BX LR
.end

При вызове doExercise из цикла основной функции значение адреса следующей инструкции (B main_loop) запишется, как говорилось выше, в LR, а в РС будет помещён адрес первой инструкции doExercise (BL uartSendByte), выполнение которой, в свою очередь, приведёт к повторной записи в LR значения адреса следующей инструкции теперь уже doExercise (BX LR).
Предыдущее значение LR (адреса инструкции B main_loop) будет, как вы понимаете, при этом затёрто. Вернувшись из uartSendByte в doExercise процессор исполнит инструкцию возврата BX, но вернётся при этом не в основную функцию, как это должно быть, а к самой BX, поскольку именно её адрес был записан в LR последним.

Говоря иначе, инструкция BX функции doExercise замкнёт процесс на себя и мы получим ситуацию из Главы 7 первой части статьи, когда в самый ответственный момент ЦПУ заклинило на притопах.
Чтобы избежать подобных проблем, следует на входе в функцию сохранять текущее значение LR в стек посредством инструкции PUSH, а перед возвратом из неё — восстанавливать с помощью инструкции POP:
  main:
  main_loop:  
    /* Вызвать функцию doExercise */
    BL   doExercise
    /* Перейти к началу цикла основной функции */
    B    main_loop	

  doExercise:
    /* Сохранить текущее значение LR в стек */
    PUSH {LR}
    /* Вызвать вложенную функцию */
    BL   uartSendByte
    /* Восстановить значение LR */
    POP  {LR}
    /* Вернуться из функции */
    BX   LR
.end

Воспользуемся ещё одной особенностью МК ARM, заключающейся в доступности программного счётчика PC для записи/чтения со стороны программиста. Учитывая, что при возврате из функции значение из LR так и так записывается в PC, заменим две последние инструкции функции:
/* Восстановить значение LR */
POP {LR}
/* Вернуться из функции */
BX  LR

на одну, но равнозначную по результату применения:
/* Загрузить в PC значение из LR */
POP {PC}

Тогда, процедуры прихлопов/притопов, оформленные как функция doExercise, и вызывающие её фрагменты цикла основной программы будут выглядеть так:
CLAPS_MAX_NUM = 2
SQUATS_MAX_NUM = 3

  main:
  main_loop:
    /* Сделать прихлопы */
    LDR  r3, =0
    MOV  r2, CLAPS_MAX_NUM
    MOV  r1, 'c'
    BL	 doExercise
    /* Сделать притопы */
    LDR  r3, =0
    MOV  r2, SQUATS_MAX_NUM
    MOV  r1, 's'
    BL   doExercise			
    /* Перейти к началу цикла основной функции */
    B    main_loop

  doExercise:
    /* Сохранить текущее значение LR в стек */
    PUSH {LR}
   /* Сделать упражнение */
   doExercise_loop:
    ADD  r3, 1
    BL   uartSendByte
    CMP  r3, r2
    BNE  doExercise_loop
    /* Вернуться из функции*/
    POP  {PC}	
.end

Обратите внимание на метку doExercise_loop, введённую в состав функции, чтобы инструкция PUSH не попала в цикл, исполняясь несколько раз. Именно имя указанной метки теперь является аргументом инструкции BNE.

По соображениям, высказанным в начале главы, перенесём функцию doExercise в созданный для этого в папке проекта файл exercises.S:
 .syntax unified
  .text
    .global doExercise
    doExercise:
      PUSH {LR}
      /* Сохранить текущие значения r2 и r3 */
      PUSH {r2}
      PUSH {r3}
     /* Сделать упражнение */
     doExercise_loop:	
      ADD  r3, 1
      BL   uartSendByte
      CMP  r3, r2
      BNE  doExercise_loop
      /* Восстановить в обратном порядке значения r2 и r3 */
      POP  {r3}
      POP  {r2}
      /* Вернуться из функции*/
      POP  {PC}
.end

Как видите:
а) В самом верху файла прописана директива .syntax unified, чтобы избавиться от необходимости предварять в коде каждое числовое значение символом «#».
б) Функция doExercise объявлена глобальной, поскольку вызываться будет теперь уже из другого файла.
в) Текущие значения используемых функцией РОН r2 и r3 сохраняются при входе в неё в стек и восстанавливаются в обратном порядке на выходе. И хотя в данном случае такая процедура совсем не обязательна в связи с тем, что для других участков программы значения r2 и r3 не актуальны, оставим её в качестве примера работы инструкций PUSH/POP.

Кстати, для ARM в применении упомянутых инструкций также есть свои нюансы.
Если вы сохраняете/считываете значения РОН по одному, как это сделано в коде выше, то необходимо придерживаться оговорённого ранее метода LIFO: последовательность чтения из стека — обратная последовательности записи в него. Однако, при использовании варианта инструкций со списком РОН процессор самостоятельно определяет, какое число в стеке относится к тому или иному РОН, т.е. снимает с вас ответственность за соблюдение последовательности записи/чтения. Учитывая изложенное, перепишем соответствующим образом содержимое exercises.S, добавив заодно к списку РОН ещё и LR с PC:
 .syntax unified
  .text
    .global doExercise
    doExercise:
      /* Сохранить текущие значения r2, r3 и LR */
      PUSH {r2—r3, LR}
     /* Сделать упражнение */
     doExercise_loop:	
      ADD  r3, 1
      BL   uartSendByte
      CMP  r3, r2
      BNE  doExercise_loop
      /* Восстановить значения r2, r3 и вернуться из функции */
      POP  {r2—r3, PC}
.end

С целью повысить компактность кода в main.S, отправим в exercises.S ещё и макроопределения максимального числа прихлопов/притопов, а также добавим функцию doBothExercises, в которую перенесём:
а) инструкции загрузки нуля в r3, максимального количества прихлопов/притопов в r2 и символов «с»/«s» в r1.
б) инструкцию вызова функции doExercise,
в) процедуры задержки и вывода в Terminal символа пробела.
г) инструкции сохранения и восстановления текущих значений r1r3.

.syntax unified

CLAPS_MAX_NUM = 2
SQUATS_MAX_NUM = 3

  .text
    .global doBothExercises
    doBothExercises:
      /* Сохранить в стек текущие значения r1-r3 и LR */
      PUSH {r1—r3, LR}
      /* Сделать прихлопы */
      LDR  r3, =0
      MOV  r2, CLAPS_MAX_NUM
      MOV  r1, 'c'
      BL   doExercise
      /* Сделать притопы */
      LDR  r3, =0
      MOV  r2, SQUATS_MAX_NUM
      MOV  r1, 's'
      BL   doExercise
      /* Вывести в Terminal символ пробела */
      MOV  r1, 32
      BL   uartSendByte
      /* Обеспечить задержку в 1 секунду */
      BL   delay
      /* Восстановить значения r1-r3 и вернуться из функции*/
      POP  {r1—r3, PC}

    doExercise:
      PUSH {LR}
     /* Сделать упражнение */
     doExercise_loop:
      ADD  r3, 1
      BL   uartSendByte
      CMP  r3, r2
      BNE  doExercise_loop
      /* Вернуться из функции*/
      POP  {PC}
.end

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

С учётом предпринятых мер, текст main.S сократится до:
.include "stm32f401.h" /* или "nrf52832.h" */
.include "macro.h"	

LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15

  .text
    .org 0
      stackPointerInit
    .org Reset_vector
      .word main + 1
  
    .global main 
    main:
      BL  uartInit
    main_loop:
      MOV r4, LAMP_BRIGHTNESS_CURRENT_VALUE
      CMP r4, LAMP_BRIGHTNESS_THRESHOLD
      BLT type_B
     type_A:
      MOV r1, 'A'
      BL  uartSendByte
      BL  doBothExercises
      B   main_loop
     type_B:
      MOV r1, 'B'
      BL  uartSendByte
      BL  doBothExercises			
      B   main_loop					
.end

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

Чтобы прояснить картину взаимной связи работы функций, спецрегистров и стека, сократим программу, как и в случае с AVR-8, до необходимого и достаточного минимума:
main.S
.include "stm32f401.h" /* или "nrf52832.h" */
.include "macro.h"	

  .text
    .org 0
      stackPointerInit
    .org Reset_vector
      .word main + 1
  
    .global main 
    main:
    main_loop:
      BL  doBothExercises
      B   main_loop					
.end

exercises.S
.syntax unified

CLAPS_MAX_NUM = 2

  .text
    .global doBothExercises
    doBothExercises:
      /* Сохранить в стек текущие значения r1-r3 и LR */
      PUSH {r1—r3, LR}
      /* Сделать прихлопы */
      LDR  r3, =0
      MOV  r2, CLAPS_MAX_NUM
      MOV  r1, 'c'
      BL   doExercise
      /* Восстановить значения r1-r3 и вернуться из функции*/
      POP  {r1—r3, PC}

    doExercise:
      PUSH {LR}
     /* Сделать упражнение */
     doExercise_loop:
      ADD  r3, 1
      CMP  r3, r2
      BNE  doExercise_loop
      /* Вернуться из функции*/
      POP  {PC}
.end

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

Видео 2. Демонстрация работы программы для SoC nRF52832.

Правая часть кадра Видео 2 является представлением нашей программы в ПП nRF52832. Как видите, код из файлов main.S и exercises.S собран при компиляции в единый список инструкций, исполнение текущей из которых помечается маркером.
В левой части картинки расположены два блока:
а) Оперативная память SRAM.
б) Так называемый файл регистров, который объединяет РОН и спецрегистры (указатель стека SP, регистр связей LR, программный счётчик PC и регистр статуса PSR). Заметьте, что к трём первым из указанных спецрегистров можно обращаться ещё и как к обычным РОН (r13, r14 и r15, соответственно).

Выясним, как меняется состояние того или иного регистра слева при исполнении очередной инструкции программы:

1. При сбросе/подаче питания МК программный счётчик РС обнуляется и ЦПУ, соответственно, начинает работу с адреса 0х00000000 ПП, загружая в SP значение 0х2000FFFF старшего адреса SRAM. Программный счётчик РС при этом увеличивается на 4.

2. Далее (адрес 0х0000004 ПП) осуществляется переход к первой и единственной в нашем случае инструкции основной функции BL с увеличением на 4 значения РС.

3. При исполнении инструкции BL (адрес 0х0000008 ПП) происходит следующее:
а) Значение адреса 0х0000000С следующей инструкции B записывается в LR.
б) Аргумент 0x00000010 инструкции BL загружается в PC и именно по этому адресу переходит процессор.

4. Согласно указаниям инструкции PUSH текущие значения r1r3 и LR сохраняются в стек. При этом, значение SP декрементируется до 0x2000FFEF, указывая на текущий свободный регистр стека, а число в PC вновь увеличивается на 4.

5. Исполнение следующих трёх инструкций (LDR и два MOV) приводит к загрузке нуля в r3, максимального числа прихлопов (0х02) в r2, ASCII кода (0х63) символа «с» в r1 и увеличению на 12 единиц значения PC.

6. По аналогии с п.3, при исполнении инструкции BL по адресу 0х00000020 ПП:
а) Значение 0х00000024 адреса следующей инструкции POP записывается в LR.
б) Аргумент 0x00000028 инструкции BL, т.е. адрес первой инструкции функции doExercise загружается в PC.

7. Исполняя инструкции doExercise, процессор сохраняет в стек текущее значение LR, уменьшая число в SP до 0x2000FFEB, а затем крутится в цикле doExercise_loop, увеличивая значение r3 и сравнивая полученное число со значением r2. При их равенстве произойдёт следующее:
а) На инструкции CMP установится в 1 тридцатый бит Z спецрегистра PSR, в связи с чем значение последнего изменится с нуля на 0x40000000.
б) При исполнении инструкции BNE в РС будет загружен, в отличие от первого круга doExercise_loop, не аргумент 0х0000002С инструкции BNE, а число 0х00000038, т.е. адрес следующей инструкции POP. Бит Z регистра PSR при этом обнулится.

8. Инструкция POP по адресу 0х00000038 ПП обуславливает копирование из стека в PC числа 0х00000024, обозначая переход по этому адресу. При этом значение SP уже инкрементируется до 0x2000FFEF.

9. По выполнению очередной инструкции POP (адрес 0х00000024) SP вновь укажет на старший адрес SRAM. При этом, будут считаны из стека:
а) в r1r3 – первоначальные их значения,
б) в РС – значение 0х0000000С адреса инструкции, подлежащей к исполнению следующей.

10. Исполняя инструкцию В по адресу 0х0000000С, процессор перейдёт к находящейся четырьмя байтами выше инструкции BL, замыкая тем самым один круг main_loop.

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

Для STM32F401 отличие заключается лишь в значении младшего адреса ПП: программа будет размещена, начиная с адреса 0х08000000.

Компиляция сложносоставного проекта

В случае компиляции проекта, состоящего из нескольких файлов, каждый отдельный ассемблер-файл преобразуется посредством утилиты as в объектный файл с расширением .o, после чего последние собираются утилитой ld в единый elf-файл с произвольным именем (обычно, это — имя проекта). На этом же этапе подключаются, при необходимости, сторонние объектные файлы, такие как uart.o или delayAVR.o (delayCortex.o), к примеру. Далее, как и в случае с проектом, состоящего из одного файла, elf-файл при помощи утилиты objcopy трансформируется в файл прошивки с расширением .hex, который и загружается в МК.
С учётом изложенного, содержимое Makefile для нашего проекта cpuJobDuties будет выглядеть так:
AVR-8
all:
  avr-as main.S -mmcu=mcu_name -o main.o
  avr-as exercises.S -mmcu=mcu_name -o exercises.o
  avr-ld main.o exercises.o uart.o delayAVR.o -T LinkerScript.ld -o cpuJobDuties.elf
  avr-objcopy cpuJobDuties.elf -j .text -j .data -O ihex cpuJobDuties.hex
  avrdude -p mcu_name -c usbasp -P usb -e -U flash:w:cpuJobDuties.hex:i

Cortex M-4
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -o main.o
  arm-none-eabi-as exercises.S -mcpu=cortex-m4 -o exercises.o
  arm-none-eabi-ld main.o exercises.o uart.o delayCortex.o -T LinkerScript.ld -o cpuJobDuties.elf
  arm-none-eabi-objcopy cpuJobDuties.elf -S -O ihex cpuJobDuties.hex
  openocd -f interface/stlink.cfg -f target/mcu_name.cfg -c "init" -c "reset init" -c "flash write_image erase cpuJobDuties.hex" -c "reset" -c "exit"

Напомню, что все надписи «mcu_name» в примерах выше должны быть заменены на «attiny85», «atmega8», «stm32f4x» или «nrf52», в зависимости от выбранного вами МК.
После компиляции папка проекта дополнится двумя объектными файлами (main.o и exercises.o), а также elf- и hex-файлы с именем cpuJobDuties.

Представим, что мы решили использовать функцию doExercise в другом проекте. Для этого достаточно:
1. Скопировать файл exercises.o папку нового проекта.
2. В требуемом месте кода нового проекта вызвать функцию (RCALL doExercise – для AVR, BL doExercise – для ARM).
3. Подключить exercises.o к сборке в соответствующей строке Makefile так, как мы сделали в примере выше это для uart.o и delayAVR.o (delayCortex.o).

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

Оформление и отладка проекта по частям

Осталось осветить вопрос о пользе структурирования программы в плане отладки.
Теперь уже зная все основные нюансы оформления и работы макросов и функций перепишем проект с самого начала так, как будто мы только что создали папку cpuJobDuties и скопировали в неё пять шаблонных файлов, а также файлы uart.o и delayAVR.o (или delayCortex.o).

Зная наперёд, что будем разносить программу по отдельным файлам, создадим в папке проекта файл exercises.S. Кроме того, сразу внесём в Makefile дополнения, оговорённые в предыдущей главе.
Поскольку каждый фрагмент кода в примерах выше подробно комментировался, в этот раз я опущу все комментарии, чтобы не занимать попусту экранное пространство.

Для начала пропишем и проверим работу ветвления, в зависимости от результата сравнения текущего значения яркости лампы с пороговым:
AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
       stackPointerInit
       RCALL uartInit
    main_loop:
       LDI   r20, LAMP_BRIGHTNESS_CURRENT_VALUE
       CPI   r20, LAMP_BRIGHTNESS_THRESHOLD
       BRLO  type_B
     type_A:	
       LDI   r16, 'A'
       RCALL uartSendByte
       RCALL delay
       RJMP  main_loop			
     type_B:	
       LDI   r16, 'B'
       RCALL uartSendByte
       RCALL delay
       RJMP  main_loop
.end

Cortex M-4
.include "stm32f401.h" /* или "nrf52832.h" */
.include "macro.h"	

LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15

  .text
    .org 0
      stackPointerInit
    .org Reset_vector
      .word main + 1
  
    .global main 
    main:
       BL  uartInit
    main_loop:
       MOV r4, LAMP_BRIGHTNESS_CURRENT_VALUE
       CMP r4, LAMP_BRIGHTNESS_THRESHOLD
       BLT type_B
     type_A:
       MOV r1, 'A'
       BL  uartSendByte
       BL  delay
       B   main_loop
     type_B:
       MOV r1, 'B'
       BL  uartSendByte
       BL  delay			
       B   main_loop					
.end

Если после компиляции и загрузки программы в МК вы получите периодический вывод в Terminal символа «А» или «В» в зависимости от того, больше или меньше пятнадцати значение LAMP_BRIGHTNESS_CURRENT_VALUE, значит с логикой программы всё в порядке.

Далее оформляем в exercises.S функцию doExercise, объявив её пока глобальной:
AVR-8
.text	
  .global doExercise
  doExercise:	
    INC   r17
    RCALL uartSendByte
    CP    r17, r18
    BRNE  doExercise
    RET
.end

Cortex M-4
.text	
  .global doExercise
  doExercise:	
    INC   r17
    RCALL uartSendByte
    CP    r17, r18
    BRNE  doExercise
    RET
.end

а в main.S проверяем работу только вновь созданной функции:
AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
      stackPointerInit
      RCALL uartInit
    main_loop:
      CLR   r17
      LDI   r18, 2
      LDI   r16, 'c'		
      RCALL doExercise
      LDI   r16, 32
      RCALL uartSendByte
      RCALL delay
      RJMP  main_loop
.end

Cortex M-4
.include "stm32f401.h" /* или "nrf52832.h" */
.include "macro.h"	

  .text
    .org 0
      stackPointerInit
    .org Reset_vector
      .word main + 1
  
    .global main 
    main:
      BL  uartInit
    main_loop:
      LDR r3, =0
      MOV r2, 2
      MOV r1, 'с'
      BL  doExercise
      MOV r1, 32
      BL  uartSendByte			
      BL  delay						
      B   main_loop					
.end

Меняя количество (значение r18 для AVR и r2 для Cortex M-4) и тип упражнения (значение r16 для AVR и r1 для Cortex M-4) убеждаемся, что в Terminal печатаются через пробел соответствующие символы в требуемом количестве, а следовательно функция doExercise работает так, как ей и положено. После этого, можно в exercises.S придать функции статус локальной и добавить макроопределения максимального количества прихлопов/притопов.

Следующим шагом добавляем в exercises.S глобальную функцию doBothExercises:
AVR-8
CLAPS_MAX_NUM  = 2
SQUATS_MAX_NUM  = 3

  .text	
  .global doBothExercices
  doBothExercices:
    CLR   r17
    LDI   r18, CLAPS_MAX_NUM
    LDI   r16, 'c'
    RCALL doExercise			
    CLR   r17
    LDI   r18, SQUATS_MAX_NUM
    LDI   r16, 's'
    RCALL doExercise
    LDI   r16, 32
    RCALL uartSendByte
    RCALL delay			
    RET

  doExercise:	
    INC   r17
    RCALL uartSendByte
    CP    r17, r18
    BRNE  doExercise
    RET
.end

Cortex M-4
.syntax unified

CLAPS_MAX_NUM  = 2
SQUATS_MAX_NUM  = 3

  .text
    .global doBothExercises
    doBothExercises:
      PUSH {r1-r3, LR}
      LDR  r3, =0
      MOV  r2, CLAPS_MAX_NUM
      MOV  r1, 'c'
      BL   doExercise
      LDR  r3, =0
      MOV  r2, SQUATS_MAX_NUM
      MOV  r1, 's'
      BL   doExercise			
      MOV  r1, 32
      BL   uartSendByte
      BL   delay		
      POP  {r1-r3, PC}

    doExercise:
      PUSH {LR}
     doExercise_loop:	
      ADD  r3, 1
      BL   uartSendByte
      CMP  r3, r2
      BNE  doExercise_loop
      POP  {PC}
.end

и проверяем корректность её работы, вызывая в main.S:
AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

  .text
    .org Reset_vector
      RJMP  main

    .global main
    main:
      stackPointerInit
      RCALL uartInit
    main_loop:
      RCALL doBothExercices
      RJMP  main_loop
.end

Cortex M-4
.include "stm32f401.h" /* или "nrf52832.h" */
.include "macro.h"	

  .text
    .org 0
      stackPointerInit
    .org Reset_vector
      .word main + 1
  
    .global main 
    main:
     BL uartInit
    main_loop:
     BL doBothExercises
     B  main_loop					
.end

Если в Terminal с периодичностью в секунду появляются через пробел два символа «с» и три «s», значит работа doBothExercises (включая вызов вложенной doExercise) соответствует нашим ожиданиям, а следовательно проверка отдельных фрагментов программы завершена и мы можем собрать её воедино.

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

Спасибо за внимание! 😉

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

🌻 Купон до 1000₽ для новичка на Aliexpress

Никогда не затаривался у китайцев? Пришло время начать!
Камрад, регистрируйся на Али по нашей ссылке. Ты получишь скидочный купон на первый заказ. Не тяни, условия акции меняются.

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

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



 

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

Нравится

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

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

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

 

 

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

 

Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК... В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы...
Схема на Датагоре. Новая статья Программа схемотехнического моделирования Micro-Cap 8. М.А. Амелина... Эта книжка будет весьма кстати всем, кто заинтересовался нашим релизом Micro-Cap 8 RUS. Программа...
Схема на Датагоре. Новая статья Elect_60: программа микроконтроллерного управления внешними устройствами от ПК... Многие наши коллеги желающие создать микроконтроллерное устройство, управляемое от ПК сталкиваются...
Схема на Датагоре. Новая статья Микроконтроллеры AVR в радиолюбительской практике. А. В. Белов... А. В. Белов Микроконтроллеры AVR в радиолюбительской практике Данная книга представляет собой...
Схема на Датагоре. Новая статья Грызем микроконтроллеры. Урок 2.... Предлагаю продолжить изучение микроконтроллеров… Второй урок будет посвящен по большей части...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 1. Начало пути... Приветствую всех сограждан и читателей журнала Датагор! Пользуясь кучей времени, предоставленной...
Схема на Датагоре. Новая статья Применение микроконтроллеров AVR. Схемы, алгоритмы, программы... Какой микроконтроллер выбрать? Где найти его описание? Где взять программу, обеспечивающую...
Схема на Датагоре. Новая статья Радиолюбительские конструкции на PIC микроконтроллерах, Кн.2, Н.И. Заец... Радиолюбительские конструкции на PIC микроконтроллерах, Кн.2, Солон-Пресс, 2005г. Автор: Н.И.Заец...
Схема на Датагоре. Новая статья Верховцев О.Г. Практические советы мастеру-любителю. Электротехника, электроника, материалы и их обработка.... Верховцев О.Г. Практические советы мастеру-любителю. Электротехника, электроника, материалы и их...
Схема на Датагоре. Новая статья PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен... PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен пер. с англ. Евстифеева А. В. — М.:...
Схема на Датагоре. Новая статья Electronics Workbench и Micro-Cap. Компьютерное моделирование аналоговых устройств. Кардашев Г. А.... Кардашев Г. А. Виртуальная электроника. Компьютерное моделирование аналоговых устройств. — М.:...
Схема на Датагоре. Новая статья Самоучитель по программированию PIC контроллеров для начинающих. Часть 1. Корабельников Е.А.... Самоучитель по программированию PIC контроллеров для начинающих. Часть 1. Корабельников Е.А....
 

Общаемся по статье 💬

Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции

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

 

Информация
Вы не можете участвовать в комментировании. Вероятные причины:
— Администратор остановил комментирование этой статьи.
— Вы не авторизовались на сайте. Войдите с паролем.
— Вы не зарегистрированы у нас. Зарегистрируйтесь.
— Вы зарегистрированы, но имеете низкий уровень доступа. Получите полный доступ.