МЕТОДИЧНІ ВКАЗІВКИ
Укладачі:
Вінничук Ігор Станіславович
Зюков.Сергій Володимирович
Відповідальний за випуск Григорків В.С.
Літературний редактор Лупул О.В.
Друкарня видавництва Чернівецького національного університету
58012, Чернівці, вул. Коцюбинського, 2
Флеш память
Память EEPROM маленькая, всего считанные байты, а иногда нужно сохранить кучу данных, например, послание инопланетянам или таблицу синусов, чтобы не тратить время на ее расчет. Да мало ли что нужно заранее заныкать в памяти. Поэтому данные можно забивать в память программ, в те самые килобайты флеша, что имеет контроллер на борту.
Записать то мы запишем, а как достать? Для этого сначала надо туда что-либо положить.
Поэтому добавляй в конце программы, в пределах сегмента .CSEG метку, например, data и после нее, используя оператор .db, вписывай свои данные.
Оператор DB означает что мы на каждую константу используем по байту. Есть еще операторы задающий двубайтные константы DW (а также DD и DQ).
Теперь, метка data указывает на адрес первого байта массива, остальные байты находятся смещением, просто добавляя к адресу единичку.
Одна тонкость — дело в том, что адрес метки подставляет компилятор, а он считает его адресом перехода для программного счетчика. А он, если ты помнишь, адресует двубайтные слова — ведь длина команды у нас может быть либо 2 либо 4ре байта.
А данные у нас лежат побайтово и контроллер при обращении к ним адресует их тоже побайтово. Адрес в словах меньше в два раза чем адрес в байтах и это надо учитывать, умножая адрес на два.
Для загрузки данных из памяти программ используется команда из группы Load Program Memory
Например, LPM Rn,Z
Она заносит в регистр Rn число из ячейки на которую указывает регистровая пара Z. Напомню, что Z это два регистра, R30 (ZL) и R31 (ZH). В R30 заносится младший байт адреса, а в R31 старший.
В коде выглядит это так:
| LDI ZL,low(data*2) ; заносим младший байт адреса, в регистровую пару Z
LDI ZH,high(data*2) ; заносим старший байт адреса, в регистровую пару Z
; умножение на два тут из-за того, что адрес указан в
; в двубайтных словах, а нам надо в байтах.
; Поэтому и умножаем на два
; После загрузки адреса можно загружать число из памяти
LPM R16, Z ; в регистре R16 после этой команды будет число 12,
; взятое из памяти программ.
; где то в конце программы, но в сегменте .CSEG
data: .db 12,34,45,23
|
AVR. Учебный курс. Подпрограммы и прерывания 
Автор DI HALT
Опубликовано 07 июля 2008
Рубрики: AVR. Учебный курс
Метки: Assembler, AVR, Программирование
Подпрограммы
Когда один и тот же участок кода часто повторяется, то разумно как то его вынести и использовать многократно. Это дает просто колоссальный выйгрыш по обьему кода и удобству программирования.
Вот, например, кусок кода, передающий в регистр UDR байты с некоторой выдержкой, выдержка делается за счет вращения бесконечного цикла:
| .CSEG
LDI R16,Low(RAMEND) ; Инициализация стека
OUT SPL,R16 ; Обязательно!!!
LDI R16,High(RAMEND)
OUT SPH,R16
.equ Byte = 50
.equ Delay = 20
LDI R16,Byte ; Загрузили значение
Start: OUT UDR,R16 ; Выдали его в порт
LDI R17,Delay ; Загрузили длительность задержки
M1: DEC R17 ; Уменьшили на 1
NOP ; Пустая операция
BRNE M1 ; Длительность не равна 0? Переход если не 0
OUT UDR,R16 ; Выдали значение в порт
LDI R17,Delay ; Аналогично
M2: DEC R17
NOP
BRNE M2
OUT UDR,R16
LDI R17,Delay
M3: DEC R17
NOP
BRNE M3
RJMP Start ; Зациклим программу
|
Сразу напрашивается повторяющийся участок кода вынести за скобки.
| LDI R17,Delay
M2: DEC R17
NOP
BRNE M2
|
Для этих целей есть группа команд перехода к подпрограмме CALL (ICALL, RCALL, CALL)
И команда возврата из подпрограммы RET
В результате получается такой код:
| .CSEG
LDI R16,Low(RAMEND) ; Инициализация стека
OUT SPL,R16 ; Обязательно!!!
LDI R16,High(RAMEND)
OUT SPH,R16
.equ Byte = 50
.equ Delay = 20
LDI R16,Byte ; Загрузили значение
Start: OUT UDR,R16 ; Выдали его в порт
RCALL Wait
OUT UDR,R16
RCALL Wait
OUT UDR,R16
RCALL Wait
OUT UDR,R16
RCALL Wait
RJMP Start ; Зациклим программу.
Wait: LDI R17,Delay
M1: DEC R17
NOP
BRNE M1
RET
|
Как видишь, программа резко сократилась в размерах. Теперь скопируй это в студию, скомпилируй и запусти на трассировку. Я хочу показать как работает команда RCALL и RET и при чем тут стек.
Вначале программа, как обычно, инициализирует стек. Потом загружает наши данные в регистры R16 и выдает первый байт в UDR… А потом по команде RCALL перейдет по адресу который мы присвоили нашей процедуре, поставив метку Wait в ее начале. Это понятно и логично, гораздо интересней то, что произойдет в этот момент со стеком.
До выполнения RCALL
Увеличить
Адрес команды RCALL в памяти, по данным PC = 0×000006, адрес следующей команды (OUT UDR,R16), очевидно, будет 0×000007. Указатель стека SP = 0×045F - конец памяти, где ему и положено быть в этот момент.
После RCALL
Увеличить
Смотри, в стек пихнулось число 0×000007, указатель сместился на два байта и стал 0×045D, а контроллер сделал прыжок на адрес Wait.
Наша процедура спокойно выполняется, как ей и положено, а по команде RET процессор достанет из стека наш заныченный адрес 0×000007 и прыгнет сразу же на команду OUT UDR,R16
Таким образом, где бы мы не вызвали нашу процедуру Wait - мы всегда вернемся к тому же месту откуда вызвали, точнее на шаг вперед. Так как при переходах в стеке сохраняется адрес возврата. А если испортить стек? Взять и засунуть туда еще что нибудь? Подправь процедуру Wait и добавь туда немного бреда, например, такого
| Wait: LDI R17,Delay
M1: DEC R17
NOP
BRNE M1
PUSH R17 ; Ой, я не специально!
RET
|
Перекомпиль и посмотри что будет =) Заметь, компилятор тебе даже слова не скажет. Мол все путем, дерзай :)
До команды PUSH R17 в стеке будет адрес возврата 00 07, так как в регистре R17 ,в данный момент, ноль, и этот ноль попадет в стек, то там будет уже 00 00 07.
А потом идет команда RET… Она глупая, ей все равно! RET тупо возьмет два первых верхних байта из стека и запихает их в Programm Counter.
И куда мы перейдем? Правильно — по адресу 00 00, в самое начало проги, а не туда откуда мы ушли по RCALL. А будь в R17 не 00, а что нибудь другое и попади это что-то в стек, то мы бы перешли вообще черт знает куда с непредсказуемыми последствиями. Это и называется срыв стека.
Но это не значит, что в подпрограммах нельзя пользоваться стеком в своих грязных целях. Можно!!! Но делать это надо с умом. Класть туда данные и доставать их перед выходом. Следуя железному правилу “Сколько положил в стек - столько и достань!”, чтобы на выходе из процедуры для команды RET лежал адрес возврата, а не черти что.
Мозговзрывной кодинг
Да, а еще тут возможны стековые извраты. Кто сказал, что мы должны вернуться именно туда откуда были вызываны? =))) А если условия изменились и по итогам вычислений в процедуре нам ВНЕЗАПНО туда стало не надо? Никто не запрещает тебе нужным образом подправить данные в стеке, а потом сделать RET и процессор, как миленький, забросит тебя туда куда надо. Легко!
Более того, я когда учился в универе и сдавал лабы по ассемблеру, то лихо взрывал мозги нашему преподу такими конструкциями (там, правда, был 8080, но разница не велика, привожу пример для AVR):
| LDI R17,low(M1)
PUSH R17
LDI R17,High(M1)
PUSH R17
; потом дофига дофига другого кода... для отвлечения
; внимания, а затем, в нужном месте, ВНЕЗАПНО
RET
|
И происходил переход на метку M1, своего рода извратский аналог RJMP M1. А точнее IJMP, только вместо Z пары мы используем данные адреса загруженные в стек из любого другого регистра, иногда пригождается. Но без особой нужды таким извратом заниматься не рекомендую — запутывает программу будь здоров.
Но побалуйся обязательно, чтобы во всей красе прочувствовать стековые переходы.
Отлаженные и выверенные подпрограммы кода можно запихать в отдельный модуль и таскать их из проекта в проект, не изобретая каждый раз по велосипеду.
Иногда подпрограммы ошибочно называют функциями. Отличие подпрограммы от функции в том, что функция всегда имеет какое то значение на входе и выдает ответ на выходе, как в математике. Ассемблерная подпрограмма же не имеет таких механизмов и их приходится изобретать самому. Например, передавая в РОН или в ячейках ОЗУ.
Подпрограммы vs Макросы
Но не стоит маникально все повторяющиеся участки заворачивать в подпрограммы. Дело в том, что переход и возврат добавляют две команды, а еще у нас идет прогрузка стека на 2 байта. Что тоже не есть гуд. И если заменяется три-четыре команды, то овчинка с CALL-RET не стоит выделки и лучше запихать все в макрос.