В цій статті, як я і обіцяв, покажу деякі важливі функції асемблера, що дозволяють оптимізувати код програми, а також зробити програму легшою для розуміння. А на початку, дам довідкову інформацію по усім командам асемблера. Скачати файл в форматі Microsoft Word або PDF.
Як бачимо, усі команди розділені на групи, в залежності від особливостей виконуваних ними дій. Реально, запам'ятовувати усі команди не потрібно, тому що часто використовуваних команд не так багато (реально 30-35), а решта є або схожими за змістом, або замінюються якоюсь інакшою командою. Це одна з маркетингових фішок ATMEL - ось, у наших МК аж 118 команд (у деяких і більше), у мк інших фірм може бути набагато менше команд, наприклад у МК серії PIC (фірми Microchip) быля 35 команд.
Логічні операції
З першої групи логічних операцій ми використовували команди ORI та ANDI для накладання бітових масок. Якщо потрібно встановити усі біти регістра в 1 або навпаки в 0 зручно використовувати команди:
SER [РРЗП] - Ця команда встановлює усі біти регістра в 1 (аналог ORI [РРЗП],0b11111111 або LDI [РРЗП],0b11111111 )
CLR [РРЗП] - Ця команда встановлює усі біти регістра в 0 (аналог ANDI [РРЗП],0b00000000 або LDI [РРЗП],0b00000000 )
Також ми вже знайомі з командою СОМ [РРЗП], яка інвертує усі бти в регістрі (в таблиці вона називається - перевід у зворотній код, тобто 0b11111111-РРЗП)
Ще часто використовуються команди OR[РРЗП1],[РРЗП2] та AND[РРЗП1],[РРЗП2], які виконують операції "логічного АБО" та "логічного І" між значеннями двох РРЗП.
УВАГА!!! ЯК Я УЖЕ ГОВОРИВ, РЕЗУЛЬТАТ ЗАПИСУЄТЬСЯ В ПЕРШИЙ ОПЕРАНД (РРЗП1), міняючи його початкове значення!!!
Команда TST [РРЗП] використовується для перевірки значення РРЗП на 0 чи від'ємне значення. Фактично, вона міняє тільки флаги Z, N, V в SREG не змінюючи значення РРЗП. Може використовуватись для організації умовних переходів сукупно з командами переходу.
В принципі, решта логічних команд не так часто застосовуються.
Арифметичні операції
Арифметичні операції використовуються для математичних обчислень і, в принципі поки що нам не потрібні. Ми вже знайомі з операціями декременту та інкременту РРЗП DEC та INC які віднімають та додають одиницю до значення РРЗП відповідно. Слід також звернути увагу, що операція віднімання від РРЗП константи (SUBI) існує, а от додавання до РРЗП константи - немає. Чому АТМЕЛ не зробили "ще одну потужну інструкцію" лишається загадкою :-).
Операції з розрядами
Зато тут АТМЕЛ постарався на славу! Команди SBR і CBR що встановлюють і обнулюють біти в РРЗП - практично аналогічні ORI та ANDI
Команди ВСLR і BSET очищують і встановлюють любий біт у регістрі SREG, але АТМЕЛ вирішили, що то ж треба памятати номера усіх бітів, і наробили рівно 16 додаткових інструкцій!!! для кожного біта SREG типу CLC, SEC для флагу С; СLZ і SEZ для Z і так далі.
В принципі реально часто будуть використовуватись операції:
SEI - команда вмикає дозвіл переривань
СLI - команда забороняє переривання
але це пізніше, коли будемо розглядати переривання.
Ще можна застосовувати операції
CBI [РВВ] , [номер біта] - очищує (замінює на 0) біт у регістрі вводу-виводу (такі як PORTx DDRx)
SBI [РВВ] , [номер біта] - встановлює (замінює на 1) біт у регістрі вводу-виводу (такі як PORTx DDRx)
Єдине що - ці дві команди виконуються за 2 такти а не за один, але є досить зручними.
Команди порівняння
Команди порівняння використовуються сумісно з командами умовних переходів. Фактично вони здійснюють віднімання від першого операнда другого але результат нікуди не записують, тільки міняють флаги в SREG Тобто якщо перший операнд більший від другого то флаги не встановлюються, якщо рівні - встановлюється флаг Z, якщо перший менший від другого - встановлюється флаг N. Потім має іти команда умовного переходу яка залежно від стану флагів здійснить перехід.
КОМАНДИ ПОРІВННЯННЯ НЕ ЗМІНЮЮТЬ ЗНАЧЕННЯ ОПЕРАНДІВ!!!
Найчастіше використовуються наступні команди:
Команда CP[РРЗП1],[РРЗП2] порівнює значення двох РРЗП
Команда СPI[РРЗП],[Константа] порівнює значення РРЗП з константою
Операції зсуву
Операції зсуву використовуються досить часто, їх принцип зрозумілий з довідкової таблиці. Біти в регістрі РРЗП - операнді зсуваються вліво чи вправо.
Операції пересилки даних
Це операції, що дозволяють переміщувати значення між РРЗП, Регістрами вводу-виводу, ОЗУ. Поки що ми використовували команди LDI, IN, OUT.
ще одна команда з цыэъ групи, яку ми розглянемо це
MOV[РРЗП1],[РРЗП2] - копіює значення РРЗП2 в РРЗП1.
Ця команда часто використовується для роботи з "молодшими" РРЗП R0...R15. В них не можна записати константу напряму, командою LDI, тому при потребі доводиться робити таку конструкцію:
ldi r17,224
mov r1,r17
тобто завантажуємо константу у якісь з "старших" РРЗП, а потім копіюємо значення в "молодший".
Команди LD і ST розглядатимемемо пізніше, як і PUSH і POP. В серйозних проектах ці команди зустрічаються дуже часто..
Команди керування системою
Серед цих команд ми знайомі з NOP - це "пуста команда", яка нічого не робить, але займає час одного такту. Решта команд поки що не використовуємо.
Команди безумовної передачі керування
З цієї групи ми знайомі з командою RJMP, яка передає керування до мітки.
Також найчастіше використовуються команди:
RCALL[Мітка] - виконує перехід до підпрограми
Що ж таке підпрограма? Підпрограма, це частина програми, що виконує певну функцію. Особливістю підпрограми є те, що до неї можна звертатися з основної програми не один раз, і після завершення виконання підпрограми керування передається на рядок, наступний після виклику підпрограми. Ось тут то ми і згадуємо про нашу затримку, яку використовували 2 рази, нагородивши купу міток в програмі. Саме затримку можна винести у підпрограму (бо це ж завершена функція) і просто двічі викликати її з основної програми!!!
Також тут важливою є команда
RET - Кінець підпрограми, повернення до основної програми.
Підпрограма повинна закінчуватись командою RET саме ця команда дає процесору знати, що підпрограма закінчилася і потрібно повернутися до виконання основної програми.
Інші команди цієї групи ми розглянемо пізніше, зараз вони нам не потрібні.
Команди умовних переходів
З цієї групи ми знайомі з командами BRNE і BREQ, що здійснюють перехід залежно від стану Z флага в регістрі SREG. Тут знову "новаторство" АТМЕЛ - є команди BRBS і BRBC що працюють з будь-яким бітом SREG і купа дублюючих команд по кожному біту окремо. Що ж, треба признати, що воно досить зручно.
У довіднику чомусь не вказані ще 4 команди переходу. Можливо, не усі AVR їх підтримують, але наш АТТіні 26 точно підтримує -
У довіднику чомусь не вказані ще 4 команди переходу. Можливо, не усі AVR їх підтримують, але наш АТТіні 26 точно підтримує -
SBRC[РРЗП],[Номер біта] - Пропускає наступну команду, якщо даний біт в РРЗП рівний 0.
SBRS[РРЗП],[Номер біта] - Пропускає наступну команду, якщо даний біт в РРЗП рівний 1.
SBIC[РВВ],[Номер біта] - Пропускає наступну команду, якщо даний біт в регістрі вводу-виводу рівний 0.
SBIS[РВВ],[Номер біта] - Пропускає наступну команду, якщо даний біт в регістрі вводу-виводу рівний 1.
Ці команди можна використовувати наприклад, для опитування кнопок, або інших операцій вибору.
От і все на рахунок команд. Насправді, ця таблиця не є повною, тут відображені команди, які є в більшості МК AVR. Але серія Mega має розширений набір команд.
Повертаємося до нашої програми
Отже, щоб оптимізувати нашу програму, ми винесемо часову затримку у підпрограму. Але перед цим я розповім про ще одну річ, яка дозволить зробити нашу програму "більш читабельною".
Справа в тому, що асемблер крім команд, які виконує МК має ще так звані ДИРЕКТИВИ ПРЕПРОЦЕСОРА.
ДИРЕКТИВИ ПРЕПРОЦЕСОРА, це команди, які виконує КОМПІЛЯТОР а не МК. Вони призначені для надання програмі зручної форми, а також для керування розміщенням даних в МК та інших функцій при підготовці прошивки.
Директиви препроцесора починаються з крапки. Зараз нас найбільше будуть цікавити такі директиви:
.def символічне ім'я=РРЗП - присвоює регістру символічне ім'я. Після цього в програмі можна використовувати саме це ім'я а не назву регістра.
.equ Символічне ім'я=Константа - присвоює символічне ім'я константі.
.set Символічне ім'я=Регістр ВВОДУ-ВИВОДУ або вираз - присвоює символічне ім'я Регістрамм вводу-виводу або іншому виразу.
Таким чином, можна назвати регістри і константи згідно функцій, які вони виконують. Тоді програму легше зрозуміти.
УВАГА!!! Ці ДИРЕКТИВИ ПОВИННІ РОЗМІЩУВАТИСЬ ПЕРЕД!!! ВИКОРИСТАННЯМ СИМВОЛІЧНИХ ІМЕН В ПРОГРАМІ
В загальному, прийнято розміщувати .def і .equ на початку програми.
Отже, міняємо наш код ТАКИМ ЧИНОМ.
Як ми бачимо, на початку програми ми присвоюємо імена нашим регістрам і константам, а також регістрам вводу-виводу, що відповідають за включення світлодіодів і зчитування значень натиснутих кнопок.
; Надаємо регістрам імена
.def temp=r16; Регістр для тимчасових даних
.def delcycle0=r17; Змінна внутрішнього циклу
.def delcycle1=r18; Змінна циклу 2 рівня
.def delcycle2=r19; Змінна зовнішнього циклу
.set ledport=PORTA; Порт для виводу на світлодіоди
.set keyport=PINB; Порт для читання стану кнопок
; Надаємо імена константам
.equ delay0=250; Кількість внутрішніх циклів
.equ delay1=250; Кількість циклів 2-го рівня
.equ delay2=16; Кількість зовнішніх циклів
Поняття про Стек
Далі з мітки Reset: починається безпосередньо програма
І починаємо ми її з незрозумілого блока
; Ініціалізація стека
ldi temp,RAMEND; завантажуємо в РРЗП temp адресу кінця оперативної пам'яті МК
out sp,temp; виводимо значення у вказівник стеку
СТЕК, це одна з важливих форм організації пам'яті МК. Принцип дії стеку полягає в тому, що ми можемо поміщати туди значення і зчитувати їх по одному. Безпосередній доступ є до останньої ячейки стеку, тобто останнім записав - першим зчитав. Наочно його можна порівняти з магазином пістолета чи автомата, де патрон вставляється зверху, пропихаючи попередній вглиб магазину, і при пострілі також використовується верхній, тобто останній запханий, після чого його місце займає передостанній і так далі. Тобто записувати туди значення ми можемо по черзі, і зчитувати також, але після зчитування чергового значення воно буде зтерте, на його місце стане попереднє.
Процедура ініціалізації стеку встановлює початок стеку на останню ячейку оперативної пам'яті. SP - це регістр Stack Pointer, тобто вказівник стеку, RAMEND - це константа, визначена в файлі опису МК і містить адресу останньої ячейки ОЗП. Чому саме на кінець ОЗУ встановлюється вказівнек стеку? Справа в тому, що стек наповнюється "задом наперед", це зроблено для того, щоб запобігти перетину стеку і даних в ОЗУ, дані наповнюють озу з початку в кінець, а стек - з кінця до початку. Звичайно, перетин можливий, коли даних багато і стек розрісся, але так його ймовірність все таки нижча.
А для чого нам стек у нашій програмі? А тут - дуже просто - стекова пам'ять використовується для зберігання адреси повернення з підпрограми! Тобто в усіх програмах з використанням підпрограм стек буде використовуватись і потрібно робити ініціалізацію. В принципі просто треба копіювати ці два рядка в кожну свою програму.
Отже, процес використання підпрограми проходить наступним чином:
Коли МК зустрічає при виконанні програми команду RCALL[Мітка], то він записує адресу наступного рядка програми у стек і тоді переходить по мітці. Виконується підпрограма до досягенення команди RET. Тоді зі стека дістається адреса нашого повернення і здійснюється перехід по цій адресі до рядка, який стояв після RCALL. Чому для цього використовуэться саме стек? А тому, що цілком можливий варіант "вкладених" підпрограм, тобто коли з одної підпрограми викликається інша. Тоді в стек запхається ще адреса повернення в першу підпрограму, Після закінчення вкладеної підпрограми ця адреса вийметься зі стеку, і здійсниться перехід в першу підпрограму, при завершенні якої зі стеку дістаємо перше запхане значення - адреса повернення до головної програми - і повертаємось по ній.
Також у стек можна "запихати" і користувацькі дані, але треба бути уважним, щоб "випхати їх" вчасно і недопустити "зрив стеку" - коли невипхане значення сприйметься як адреса повернення і програму переклинить. Такі глюки з непривички досить важко відслідкувати, тому поки що ми не будемо експериментувати зі збереженням своїх даних в стек.
Продовжуємо нашу програму
Наступна дія - це так звана Ініціалізація портів.
Цю операцію ми робили у всіх попередніх програмах.
Ініціалізація портів -це задання режимів їх роботи і початкових значень у керуючих регістрах портів.
Єдиною відмінністю від попередньої програми є те, що ми використовуємо уже назначені символічні імена регістрів, а також, я використав команди SER і CLR замість LDI у місцях, де треба було записати в регістри портів усі одиниці адо усі нулі.
Основний цикл програми тепер короткий
Main:
ldi temp,0b00000001; встановлюємо 1 в першому біті temp
out ledport, temp; виводимо значення в портА, засвічуючи світлодіод
rcall delay05; Виклик підпрограми затримки
clr temp; встановлюємо 0 в temp
out ledport, temp; виводимо значення в портА, гасячи світлодіод
rcall delay05; Виклик підпрограми затримки
rjmp Main; переходимо до мітки Main - тобто зациклюємо програму.
Тут ми засвічуємо світлодіод, викликаємо підпрограму затримки, потім гасимо світлодіод, викликаємо підпрограму затримки і зациклюємо програму
Нижче у нас іде сама підпрограма затримки, аналогічна викладеній у першій частині статті, але з використанням символічних імен регістрів і констант. Закінчується підпрограма, як і треба, командою RET.
Важливо відмітити, що розміщення підпрограми повинне бути таким, щоб уникнути випадкового входу в неї без команди rcall. Тобто не можна, скажімо розмістити підпрограму до rjmp Main Таке розміщення не тільки зробить зайву затримку, бо програма зайде в неї сама, виконуючи свої кроки, але і викличе зрив стеку, бо команда RET.спробує дістати з нього значення адреси повернення, якої там не буде.
Тепер можемо скомпілювати програму і прогнати її у відладчику, змінивши константи затримок на меньші. О-па! Тепер це зручно, на самому початку програми, не шукаючи їх між операндів команд! Також ніхто не забороняє призначити інші регістри для наших символічних імен (тільки варто пам'ятати про різницю між "молодшими" і "старшими" і не вписати "молодший регістр" у операції з константами).
Відео (повний екран, HD):
До речі, можна ще спростити головний цикл програми, адже ми керуємо лишень одним світлодіодом. Тобто можна використати операції з бітами CBI та SBI. Переваги у швидкодії це не дасть, так як команди виконуються за 2 такти, але на вигляд програма спроститься:
Main:
sbi ledport,0; виводимо 1 в нульовий біт портА, засвічуючи світлодіод
rcall delay05; Виклик підпрограми затримки
сbi ledport,0; виводимо 0 в нульовий біт
rcall delay05; Виклик підпрограми затримки
Ось таку програму можна уже залити в МК і протестувати (не забудьте змінити значення констант затримок). Можна поміняти значення delay2 з 16 на 8 - світлодіод буде блимати вдвічі швидше!
Відео:
Отже, наша програма тепер оптимізована по розміру, і більш зрозуміла для читання. В наступній статті я приділю трохи уваги архітектурі, тобто правильній побудові програм для МК. Ми ще ускладнимо нашу програму, а також напишемо ще декілька програм для закріплення матеріалу про просте програмування. Далі я буду знайомити читачів з такими важливими речами, як переривання і таймери, без яких не обходиться ні один серйозний проект на МК.
Немає коментарів:
Дописати коментар