Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Программирование микроконтроллеров PIC. Часть 3. Структура программы на ассемблере

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

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

Ниже показана типичная структура программы на ассемблере.

;--- Шапка программы ---
   list p=16f628a
   __config b'11111100110001'
   CBLOCK 0x20
     variable1   ; первая запись
     variable2   ; вторая запись
     variable3   ; третья запись
     variable4   ; четвёртая запись
   ENDC
     Const1  equ  .1
     Const2  equ  .5
     TRISB   equ  06h (1-й банк)
     PORTB   equ  06h (0-й банк)
     Status  equ  03h
     Z       equ  02h
скобка
;--- Тело программы ---
         org 0h
  ; можно выполнить 3 команды
  ; основной программы
    goto  start
         org 4h
  ; подпрограмма обработки
  ; прерываний
start ; продолж. основной программы
      ; инициализация
      ; решение задачи
     end
скобка

1. Шапка программы.

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

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

__config значение — эта директива указывает значение слова конфигурации контроллера. Слово конфигурации определяет основные параметры контроллера (какой будет использоваться генератор, будет ли установлена защита памяти программ и т.д.).

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

CBLOCK число — блок поименованных констант. Число — определяет константу, которой будет соответствовать первая запись (имя). Вторая запись будет соответствовать константе число+1, третья — константе число+2 и так далее. То есть, в нашем примере, везде, где компилятор увидит запись variable1, он вместо этой записи подставит число 20h (другая запись 0x20), вместо variable2 будет подставлено 21h и т.д.

С помощью этой директивы очень удобно присваивать имена сразу целому блоку пользовательских регистров (в которых программа будет хранить свои переменные или константы).

Пользовательские регистры контроллера PIC16F628A начинаются по адресу 20h. В нашем примере, поименовав константы, начиная со значения 20h, мы фактически назначили регистру по адресу 20h имя variable1, регистру по адресу 21h имя variable2 и т.д. Допустим, мы используем регистр по адресу 20h в качестве счётчика. Для того, чтобы увеличить значение счётчика нам нужно прибавить 1 к значению, записанному в этом регистре. На ассемблере это можно записать так: incf 20h. Но, у нас константе 20h соответствует имя variable1. То есть, мы можем записать incf variable1 и компилятор сам заменит variable1 на 20h. Согласитесь, вторая запись гораздо более наглядна.

То есть, с помощью директивы CBLOCK можно фактически определить блоки регистров (присвоить имена регистрам, расположенным по определённым адресам), которые будут использоваться в программе как переменные.

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

equ — эта директива сообщает компилятору, что запись, расположенная перед этой директивой соответствует числу, стоящему после этой директивы (компилятор будет заменять запись на число). В нашем примере, записи Const1 будет соответствовать число 1 (точка перед числом обозначает десятичную систему исчисления, а буква h — шестнадцатиричную), записи Const2 — число 5, записи TRISB — число 6 и т.д. С помощью этой директивы обычно задают константы, адреса используемых в программе регистров и присваивают имена отдельным битам в регистре.

Допустим в программе мы несколько раз в разных местах оперируем константой, значение которой равно 1 (например загружаем её в аккумулятор командой movlw .1). Если нам понадобится перекомпилировать программу с другим значением этой константы, то надо будет искать все куски кода, где мы её используем и в каждом случае отдельно менять код. Если же мы директивой equ указали, что записи Const1 соответствует константа 1, то теперь мы везде можем писать movlw Const1 и для изменения константы нам нужно всего лишь изменить её значение в шапке программы.

Аналогичным образом именуются используемые регистры. Например, мы хотим работать с портом B. Открываем документацию и смотрим, какие нам понадобятся регистры для работы с ним и по каким адресам они расположены. Мы видим, что для работы с портом B нам понадобится регистр, расположенный в нулевом банке по адресу 06h, который в доке называется PORTB и регистр, расположенный в первом банке по адресу 06h, который в доке называется TRISB. Пока компилятор не знает, что такое PORTB и TRISB, — мы можем обращаться к ним только по адресу. Например, если мы напишем movwf 06h, находясь в нулевом банке, то контроллер запишет данные из аккумулятора в регистр PORTB, а если та же команда будет выполнена, когда мы находимся в первом банке, то данные из аккумулятора будут записаны в регистр TRISB. Если мы в шапке программы пропишем, что имя PORTB соответствует числу 06h (PORTB equ 06h), то теперь в программе, для записи данных из аккумулятора в порт B, код может выглядеть так: movwf PORTB и компилятор будет знать, что запись PORTB нужно заменить на 06h. Точно так же можно поименовать и биты, указав, что, например, бит Z в регистре Status — это второй бит и везде, где мы пишем Z, компилятору нужно заменить эту запись на число 02h.

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

2. Тело программы.

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

ORG число — эта директива как раз предназначена для компилятора. Число в данном случае — это адрес, начиная с которого в памяти программ контроллера располагается код, следующий за директивой ORG. При старте, счётчик команд (указатель на текущую выполняемую команду) всегда устанавливается на нулевой адрес, поэтому логично, что мы должны разместить программу так, чтобы первая инструкция оказалась в памяти программ по нулевому адресу. Для этого в начале тела программы пишут ORG 0.

При возникновении прерывания счётчик так же всегда устанавливается по какому-то определённому адресу, например для PIC16F628A это будет адрес 0004h. Следовательно, если мы используем в программе прерывания, то перед подпрограммой обработки прерываний нужно написать ORG 04h, чтобы первая инструкция этой подпрограммы располагалась по адресу 0004h. Кроме того, в этом случае, необходимо обеспечить, чтобы основная программа не пересекалась с подпрограммой обработки прерывания.

Помните, мы указали, что программа должна располагаться в памяти программ, начиная с нулевого адреса. Соответственно, первая её инструкция будет по нулевому адресу, вторая по адресу 0001h, третья — по адресу 0002h, четвёртая по адресу 0003h, а пятая — по адресу 0004h. Опа! Начиная с пятой инструкции мы оказались в той области памяти, где должна располагаться подпрограмма обработки прерываний. Чтобы такого пересечения не произошло, подпрограмму обработки прерываний обходят командой goto. То есть, начиная с нулевого адреса до адреса 0002h можно разместить основную программу, а потом нужно сделать переход в ту область памяти, которая располагается за подпрограммой обработки прерывания и оставшуюся часть основной программы расположить там. В нашем примере (рисунок наверху) такой переход выполняется командой goto start.

Тело программы также можно поделить на 2 части — инициализацию и, собственно, выполнение какой-то задачи.

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

Пусть мы (для примера) хотим организовать мигание светодиодом, подключенным к 7-й ноге PIC16F628A. Для этого нам нужно, чтобы седьмая нога работала как выход. Седьмая нога — это канал RB0 порта B. Открываем доку и смотрим, что нужно сделать, чтобы настроить RB0 как выход. Мы видим, что направлением работы порта B управляет регистр TRISB, который находится по адресу 86h. 86h — это первый банк, смещение 06h от начала банка. Для того, чтобы RB0 работал как выход, нужно установить нулевой бит регистра TRISB в 0. То есть инициализация будет заключаться в следующем:

bcf PORTB, 0    ; устанавливаем начальное состояние
                ; (уровень, который установится на RB0
                ; после переключения направления работы
                ; на выход, 0 - низкий уровень, 1 - высокий
bsf Status, RP0 ; переходим в первый банк (нужный
                ; нам регистр TRISB находится там), для чего
                ; устанавливаем в 1 бит RP0 регистра Status
bcf TRISB, 0    ; устанавливаем в 0 нулевой бит регистра TRISB
                ; (устанавливаем направление работы RB0 - на выход)
bcf Status, RP0 ; возвращаемся в нулевой банк

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

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

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

  1. Часть 1. Необходимые инструменты и программы. Основы MPLAB
  2. Часть 2. Что такое микроконтроллер и как с ним работать
  3. Часть 3. Структура программы на ассемблере
  4. Часть 4. Разработка рабочей части программы. Алгоритмы
  5. Часть 5. Ассемблер. Организация циклов и ветвлений
  6. Часть 6. Как перевести контроллер в режим программирования и залить в него прошивку

Добавить комментарий