Флеш память

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

Записать то мы запишем, а как достать? Для этого сначала надо туда что-либо положить.
Поэтому добавляй в конце программы, в пределах сегмента.CSEG метку, например, data и после нее, используя оператор.db, вписывай свои данные.

Оператор DB означает что мы на каждую константу используем по байту. Есть еще операторы задающий двубайтные константы DW (а также DD и DQ).

1 data:.db 12,34,45,23

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

Одна тонкость — дело в том, что адрес метки подставляет компилятор, а он считает его адресом перехода для программного счетчика. А он, если ты помнишь, адресует двубайтные слова — ведь длина команды у нас может быть либо 2 либо 4ре байта.

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

Для загрузки данных из памяти программ используется команда из группы Load Program Memory

Например, LPM Rn,Z

Она заносит в регистр Rn число из ячейки на которую указывает регистровая пара Z. Напомню, что Z это два регистра, R30 (ZL) и R31 (ZH). В R30 заносится младший байт адреса, а в R31 старший.

В коде выглядит это так:

12345678910111213 LDI ZL,low(data*2); заносим младший байт адреса, в регистровую пару Z LDI ZH,high(data*2); заносим старший байт адреса, в регистровую пару Z; умножение на два тут из-за того, что адрес указан в; в двубайтных словах, а нам надо в байтах.; Поэтому и умножаем на два; После загрузки адреса можно загружать число из памяти LPM R16, Z; в регистре R16 после этой команды будет число 12,; взятое из памяти программ.; где то в конце программы, но в сегменте.CSEG data:.db 12,34,45,23

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

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

123456789101112131415161718192021222324252627282930313233 .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,DelayM3: DEC R17 NOP BRNE M3 RJMP Start; Зациклим программу

Сразу напрашивается повторяющийся участок кода вынести за скобки.

1234 LDI R17,DelayM2: DEC R17 NOP BRNE M2

Для этих целей есть группа команд перехода к подпрограмме CALL (ICALL, RCALL, CALL)
И команда возврата из подпрограммы RET

В результате получается такой код:

1234567891011121314151617181920212223242526272829 .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,DelayM1: 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×035D, а контроллер сделал прыжок на адрес Wait.

Наша процедура спокойно выполняется, как ей и положено, а по команде RET процессор достанет из стека наш заныченный адрес 0×000007 и прыгнет сразу же на команду OUT UDR,R16

Таким образом, где бы мы не вызвали нашу процедуру Wait - мы всегда вернемся к тому же месту откуда вызвали, точнее на шаг вперед. Так как при переходах в стеке сохраняется адрес возврата. А если испортить стек? Взять и засунуть туда еще что нибудь? Подправь процедуру Wait и добавь туда немного бреда, например, такого

12345678 Wait: LDI R17,DelayM1: 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):

123456789 LDI R17,low(M1) PUSH R17 LDI R17,High(M1) PUSH R17; потом дофига дофига другого кода... для отвлечения; внимания, а затем, в нужном месте, ВНЕЗАПНО RET

И происходил переход на метку M1, своего рода извратский аналог RJMP M1. А точнее IJMP, только вместо Z пары мы используем данные адреса загруженные в стек из любого другого регистра, иногда пригождается. Но без особой нужды таким извратом заниматься не рекомендую — запутывает программу будь здоров.

Но побалуйся обязательно, чтобы во всей красе прочувствовать стековые переходы.

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

Иногда подпрограммы ошибочно называют функциями. Отличие подпрограммы от функции в том, что функция всегда имеет какое то значение на входе и выдает ответ на выходе, как в математике. Ассемблерная подпрограмма же не имеет таких механизмов и их приходится изобретать самому. Например, передавая в РОН или в ячейках ОЗУ.

Подпрограммы vs Макросы
Но не стоит маникально все повторяющиеся участки заворачивать в подпрограммы. Дело в том, что переход и возврат добавляют две команды, а еще у нас идет прогрузка стека на 2 байта. Что тоже не есть гуд. И если заменяется три-четыре команды, то овчинка с CALL-RET не стоит выделки и лучше запихать все в макрос.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: