Это не учебное пособие. Тут не будет рассказов об инструкциях языка и его особенностях. Тут будет рассказ о потраченном времени на простые вещи - выяснение того, что нужно сделать по минимуму, чтобы программа и данные оказались в контроллере на своем месте и симулятор ПРОТЕУС показал вам текст объектного файла. Это заметка о моей попытке освоения ассемблера ATMEL. Сначала немного о себе. Давно, в 80-х прошлого столетия, писал я как-то программу дизассемблера для того, чтобы с ее помощью раскусить чужую программу расчета стоимости перевозки грузов по сети ж.д. СССР. Время было безденежное, а программа та стоила не дешево. Пришлось изучить язык INTEL8080 и задача была решена.
К чему бы это? А к тому, что не очень давно пришлось программировать для микроконтроллеров ATMEL.
Решил, что конечно быстро все вспомню, а не тут то было. Почитал один, другой, третий учебник, а сесть и написать программу не могу. В них вроде бы все верно, не подкопаешься. Как у Ленина, по форме правильно, а по сути издевательство. Только вот со временем понятно становится, что написано правильно, но не для того, чтобы что-то сразу сделать, а для того, чтобы уже со знанием дела искать детали. Понял только, что инструкций в контроллерах Atmel куда как меньше и они победнее INTEL8080.
Поэтому захотелось попробовать изложить свое видение о том, как войти в ассемблер имея только общие представления о микроконтроллерах.
Микроконтроллер – электронное полупроводниковое устройство, содержащее все необходимые компоненты для вычисления и управления неким объектом посредством электрических сигналов. Иначе говоря, нужна только батарейка и этот самый контроллер.
Ассемблер – язык самого низкого уровня программирования, жестко привязанный к архитектуре (внутреннему устройству) микроконтроллера. Каждый ассемблер имеет свой собственный синтаксис (слова, знаки препинания). На язык микроконтроллера слова ассемблера переводит в команды контроллера компилятор.
Компилятор – программа- переводчик языка ассемблер в машинные коды в форме загрузочного модуля.
Машинные коды – двоичные цифры, обычно для компактности представляются в 16-ричном буквенно-цифровом формате (цифры 0…9 и буквы A…F), сформированные в байт (группу из 8 бит, единиц или нулей), которые микроконтроллер распознает и выполняет по ним предписанные действия. Байты в свою очередь состоят из полубайтов (16-ричных цифр 0…F). Эти полубайты называют нибблами.
Загрузочный модуль это последовательность машинных кодов, готовых для размещения в памяти программ (флэш) микроконтроллера и сформированных по особым правилам. Правила простые - чаще всего это набор строк по 16…256 байт, заканчивающиеся контрольной суммой для проверки правильности записи и чтения (так называемый HEX- формат IBM). Описание загрузки этого модуля в контроллер вынесем за пределы этой заметки.
Теперь о том, что надо представлять себе о микроконтроллере прежде, чем сесть и что-то писать. Только самое основное. И не вообще, а о контроллерах компании Atmel.
В контроллерах можно выделить 3 области памяти – 1) память программ (флэш, постоянная память), память данных (ОЗУ), 2) оперативная память, держит данные пока есть напряжение и 3) область загрузчика, тоже небольшая флэш. На последней зацикливаться не будем. Дебри пока не нужны. Кроме того, надо помнить, что микроконтроллер может иметь развитую периферию со своими местами хранения оперативной информации по типу ОЗУ – область регистров. Через них программа пользователя получает доступ к результатам работы периферийных устройств – счетчиков, аналого-цифровых преобразователей, интерфейсов обмена данными и т.п.
Оперативная память. Ее размер говорит о том, какие объемы информации можно обработать. Pазмер может быть очень маленьким – сотни байт и весьма большим – сотни килобайт. Часть оперативной памяти управляющее устройство (УУ) берет себе, ее вашей программой ЗАНИМАТЬ ЕЕ НЕЛЬЗЯ без спросу, хоть она и оперативная память. Почему?
Начало ОЗУ занимают рабочие регистры УУ, с их помощью производятся операции вычисления и пересылки данных внутри оперативной памяти и из памяти программ в оперативную память и обратно. Остаток ОЗУ отдан под нужды программистов.
Еще есть один сегмент памяти, - регистры периферии УУ. Это физически тоже оперативная память, имеет свое адресное пространство и является буфером через который программа пользователя получает данные, обработанные УУ и участвует в процессе обработки.
У этих регистров есть свои грабли. Ранние контроллеры Атмел не имели развитой периферии (это обычно те, которые Атмел выпускал в начале своей деятельности), к ним обращаются по одним, «старым» правилам, командами IN, OUT. К регистрам контроллеров, выпущенных позднее эти команды не применимы. Точнее, они сохранены, но только для регистров не выше некоторого адресного пространства, например, $03FF.
Память программ (флэш). Тут придется начинать рассказ о других граблях. Самое время упомянуть о прерываниях.
Прерывание – прекращение выполнения основной программы и передача управления программе обслуживания событий. Событий может быть множество, например счетчик УУ досчитал до какого-то числа и надо его запомнить. Или зажечь лампочку. Для того, чтобы произвести внеочередное действие, управление передается так называемому «обработчику прерываний» - короткой подпрограмме выполняющей нужную команду. В каждом типе контроллера свое количество и размещение векторов прерываний.
Вектор прерывания – адрес передачи управления по событию. Размещен жестко на определенном месте в начале флэш памяти. Векторов может быть многие десятки. Назначение и размещение оговаривается в описании микроконтроллера.
Теперь практический вопрос. Можно разместить свою программу в области векторов? Ответ ДА, но не всегда. А тогда, когда вы не программируете прерываний.
Не забывайте, что бы вы не написали, но когда ВКЛЮЧАЕТЕ микроконтроллер, первая команда, которую он выполнит, должна находиться по адресу 0000. Если у вас там размещено что попало – ждите головную боль.
Поэтому первая команда, которую вы должны запрограммировать по адресу 0000, это JMP RESET – перейти к процедуре «наведения порядка» в дебрях микропроцессора.
А можно обойтись без этого? Да, но если вы понимаете что будет дальше. А дальше должна быть запрограммирована основная операция – адресация указателя стека (штабеля, по-англицки). Правильный адрес указателя – дно оперативной памяти.
Стек – область ОЗУ для хранения адресов возврата из подпрограмм и прерываний.
Указатель стека - двухбайтный регистр, содержащий адрес возврата из подпрограмм и прерываний. При работе программы, имеющей и подпрограммы и прерывания, указатель каждый раз сдвигается на 2 байта вверх по памяти, сохраняя адрес, откуда пришел вызов подпрограммы. По окончании работы подпрограммы программный счетчик получает этот адрес для возврата в основную программу, а указатель стека сдвигается на 2 байта вниз.
Что дают эти сведения, точнее, что может случиться, если вы забыли про стек? И стек и ваши данные находятся в одном и том же сегменте оперативной памяти, но по разную сторону - стек на дне, данные навеврху, если, конечно, вы не желали сами себе проблем. Если вы запрограммировали хранение большого массива данных, в добавок разрешили прерывания от периферийных модулей (USART, I2C, DAC, счетчиков и пр.), да к тому же возможен вызов вложенных подпрограмм, вот и причина встретиться вашему массиву данных с указателем стека. Указателю не досуг проверять, есть там ваши данные или их нет, он обязан сохранить адрес возврата из прерывания или подпрограммы. Этот адрес может заместить ваши данные. Редкая ситуация. Даже скорее надуманная, но забывать нельзя.
Еще один дурацкий вопрос можно задать. А можно обойтись вообще без всего этого, что написано выше, и программа будет работать, как хотелось? Можно и так. Вот какие НО вы должны будете учесть.
1) такая программа не должна содержать вызовы подпрограмм.
2) Программа не должна использовать прерываний
3) Программа не должна завешаться аварийно, когда после последней операции УУ не знает, куда пойти дальше.
Перечисленные условия предполагают, что поток команд по окончании либо начнется с начала либо завершится командой перехода к фрагменту программы по заранее предусмотренной метке.
Теперь вы можете разместить свою программу где угодно в памяти флэш. И если вдруг она работает, но симулятор ПРОТЕУС вместо SOURCE выдает пустую форму - посмотрите, куда положили текст, поискав директиву ".ORG".
Следующий вопрос, как это, «разместить в памяти»?
В ассемблере есть оператор (директива) ORG. Именно с его помощью можно отдать распоряжение компилятору, где и что разместить. Этот оператор действует как в памяти флэш для указания адреса размещения программы, так и в оперативной памяти, для указания места хранения переменных.
Приведу несколько примеров. Хочу разместить свою программу прямо в начале флеш. Пусть первая команда исполняемой программы, тоже для примера, будет командой загрузки константы $A5 в регистр R16. Тогда пишу:
.cseg ; .cseg оператор(директива), указывающий, что операции и данные помещаются
; в сегмент команд, точка с запятой отделяет комментарий
.org $0000 ; .org указатель адреса размещения. $ - признак 16-ричного формата числа
; обе команды cseg и org предваряются точкой. Обязательно. Это правило.
begin:
ldi R16, $A5
…
далее следует остальная часть вашей программы. Слово «begin:» в начале, это метка, на которую можно сделать ссылку из любой части программы для передачи управления, предусмотренной вашим алгоритмом, чтобы можно было все начать с начала.
Этот пример показывает, как разместить программу прямо в начале флэш-памяти. На практике такого не встретите. Напомню, что эта программа не должна содержать вызовы подпрограмм или прерывания, потому что не была проведена процедура инициации стека, а следовательно возврат из подпрограмм будет невозможен.
Может быть тут возникнет вопрос, а почему? А вот почему. Указатель стека при помещении в стек адреса возврата, смещается на два байта вверх от дна памяти. На два байта потому, что процессоры Атмел адресуются к командам во флеш-памяти словами по 2 байта.
В примере выше мы не программировали указатель стека. А значит, он указывает на ячейку памяти с адресом $0000. При вызове подпрограммы указатель укажет на несуществующую область памяти $0000 – 2, в которую он должен был бы поместить адрес возврата из подпрограммы. Куда деться программному счетчику процессора?
Теперь перейдем от «неправильных» примеров в правильным с учетом архитектуры процессоров..
Вы, например, заранее не знаете, понадобятся ли вам векторы прерывании и какие именно. Тогда вы должны обратиться к описанию контроллера, к разделу ПРЕРЫВАНИЯ и найти самый последний вектор с указанием его адреса во флеш-памяти. Добавьте 2 байта и можете начать размещение вашей программы для случая программирования любого прерывания. В начале можно впасть в другую крайность – перечислить в начале своей программы все векторы прерывания, даже если вы их не собираетесь использовать. Вреда не будет. Но и пользы никакой.
Пример.
Для процессора AtMega328P последнее прерывание имеет номер 26 и размещено по адресу 0x0032 - написание по правилам языка СИ, или $32 по допустимому в ассемблере сокращенному написанию чисел с основанием 16.
Тогда вы можете писать в тексте программы только мимнимально необходимое, предварив ее следующими операторами:
;
.cseg ; все следующие действия будут в командном сегменте
.org 0 ; и начнутся с адреса 0000
Jmp begin
;
; тут можно активировать любой вектор из перечня
;
.org $34 ; а тут начало свободного пространства
Begin: ; метка для адресации (передачи управления)
ldi r16, high(RAMEND); Установить указатель стека на дно оперативной памяти
out SPH,r16 ; stack reset
ldi r16, low(RAMEND)
out SPL,r16
…
текст программы.
...
Jmp Begin
;
Если вы знаете, что будете использовать какое-то определенное прерывание, можете только его и указать. Например, будете использовать прерывание по переполнению счетчика-таймера №0. Для процессора Mega328P это вектор №17 с адресом размещения $20.
Напишем фрагмент программы с комментариями для такого случая:
;
.cseg ; разместить в командном сегменте
.org 0 ; по адресу 0000
Jmp Start ; команду перехода к процедуре с меткой «Start»
;
.org $20 ; разместить вектор прерывания TIMER0 OVF Timer/Overflow
Jmp TIMER0_OVF ; с передачей управления на метку обработчика этого прерывания
;
.org $34 ; разместить по адресу $34 начало программы
Start: ; и назначить для нее метку Start (см. команду .org 0 выше потексту)
…
Программа
…
Jmp Start ; повторение цикла
;
TIMER0_OVF: ; метка обработчика прерывания
; тут можно что-то сделать
Reti ; команда возврата из прерывания
;
Зная эти правила вы можете написать работоспособную программу.
Подводя итог можно заключить, что в общем случае текст программы содержит 3 области
- область обьявления прерываний
-область программы
-область обработчиков прерываний и подпрограмм
Для указания места размещения программы используют оператор .cseg (указание на флэш-память). Указать конкретный адрес используемых вектроеов прерывания и размещения программы нужно опеатором .org {адрес}. Для того, чтобы вектор прерывания нашел своего обработчика, программа могла найти нужное место для передачи управления подпрограмме, используют «LABEL:» - метки с двоеточием для организации ссылок,
Перейдем к оформлению оперативной памяти в вашей программе. В начале мы должны поинтересоваться в руководстве на процессор, а где начинается область ОЗУ, доступная пользователю. Для AtMega328P она начинается с адреса $100. Пользователь может разместить тут свои переменные. Если вы попытаетесь забыть, начнете с 0000, компилятор напомнит, эта область НЕ ВАША.
В тексте программы описание ваших переменных может размещаться там, где вам удобно. Мне удобнее, когда я вижу переменные в верхней части программы. Поэтому у меня текст разбит на области:
- объявление векторов прерывания
-объявление переменных
-текст программы
-текст подпрограмм
-обработчики прерывания
Объявление переменных начинается с команд указания вида памяти (флэш/озу) и указателя адреса. Мы уже узнали, что можем пользоваться ОЗУ с адреса $100.
Напишем:
;
.dseg ; указание компилятору, пусть размещает переменные в ОЗУ
.org $100 ; начиная с адреса 100 (HEX)
Voltage: .byte 2 ; тут у меня будет лежать 2 байта измеренного напряжения
Current: .byte 2 ; тут будет лежать 2 байта измеренного тока
И т.д.
;
В этом примере мы заняли для хранения переменных всего 4 байта в начале разрешенной области с адреса $100. Как этот текст приладить к примерам, приведенным выше?
Хотя бы так :
;
.cseg ; разместить в командном сегменте
.org 0 ; по адресу 0000
Jmp Start ; команду перехода к процедуре с меткой «Start»
;
.org $20 ; активировать вектор прерывания TIMER0 OVF Timer/Overflow
Jmp TIMER0_OVF ; а это метка обработчика этого прерывания
;
.dseg ; перейти к распределению памяти ОЗУ (data seg)
.org $100 ; начать размещение с адреса $100
Voltage: .byte 2 ; тут будет лежать 2 байта измеренного напряжения
Current: .byte 2 ; тут будет лежать 2 байта измеренного тока
;
.cseg ; вернуться к управлению памятью флэш (командный сегмент)
.org $34 ; начало программы разместить по адресу $34
Start: ; и назначить для нее метку Start (см. команду .org 0 выше по тексту)
…
Тут продолжается текст программы
…
Jmp Start ; конец программы, переход к началу цикла
;
TIMER0_OVF: ; метка обработчика прерывания для вектора $20
; тут можно что-то сделать
Reti ; команда возврата из прерывания
;
Небольшой совет начинающим програмистам. Чтобы не лазать по описаниям в поисках места размещения регистров периферии, я приспособился везде писать инструкции обращения к порта IN и OUT. Компилятор сообщает об ошибке адресации там, где надо было применить ST/LD STS / LDS. И вот почему.
Если вы напишете инструкции обмена с портами из комплекта ST/LD STS/LDS для регистров, размещенных в начале адресного пространства, компилятор их благополучно проглотит, но делать не будет НИЧЕГО. И сообщать о своем безделии не намерен. Поиск этой идиотической причины глюков вашей программы может занять время. Именно поэтому, там где не уверен, лучше написать инструкцию "старую" - IN/OUT и получить замечание, чем искать ошибку, где ее и быть то не должно. Эта рекомендация относится, в первую очередь, к пользователям монстра Atmel Studio, которая нынче переименована в Microchip Studio, но как и прежде еле шевелится и зависает.
Вот еще дополнение, которое может помочь начинающим программистам-ассемблерщикам. Если вам захотелось разместить константы в сегменте кода (иначе говоря, в тексте программы), вас может озадачить поведение процессора при попытке перенесения данных в ОЗУ. Их не видно. А оказывается, что вы не обратили внимание, что адресация в командном сегменте производится по словам, а не по байтам, как в сегменте данных.
Чтобы взять данные из сегмента команд и поместить их в оперативную память, надо удвоить аргумент метки из сегмента команд, например:
ldi zl,low(_7SegDecoder*2) ;это метка в памяти программ
ldi zh,high(_7SegDecoder*2) ; тут забираем
;
ldi xl,low(Coder2_7) ; это метка в ОЗУ, попросту ваша переменная
ldi xh,high(Coder2_7) ; а сюда положим
(операции загрузки, выгрузки регистров и циклические процедуры опущены)
В этой заметке я хотел описать свои проблемы, с которыми я столкнулся в самом начале при создании ассемблерной программы, когда не знал с чего начать, как разместить в программе команды, и переменные, чтобы они попали в нужное процессору место. Если смогу написать что-то поприличнее, напишу.
После опубликования пришел в ужас - что делал, чтобы выглядело как фрагмент текста программы, превратилось в кашу. Бог знает что тут надо делать.