Периодически вспоминается разговор с одним из выпускников. Мы обсуждали кем он хочет стать и какую ему выбрать профессию. В результате он сказал «хочу программировать микроконтроллеры на асме», ответа на вопрос почему не начать с Си, не последовало 🙂 В общем всем студентам-любителям асма посвящается…
Давным давно, еще когда только начал изучать AVR, мне захотелось посмотреть что такое ассемблер для AVR. С помощью неких мануалов мне удалось написать программу мигалки, но без особого вникания в суть процесса, тогда мне показалось слишком сложно. По воле случая понадобилось кое что поправить в асмовском исходнике одного товарища. В итоге получилось быстрее переписать полностью исходник на Си, но связано это было лишь с нехваткой времени, зато отношение к асму полностью изменилось.
Сейчас уже с улыбкой смотрю как некоторые люди говорят об этом, как чем то сверхъестественном. Для простоты понимания, в качестве задачи — отправить байт по юарту.
С чего начнем? Наверно с того как бы выглядела такая же программа на си:
#include <mega8.h> #include <stdio.h> #include <delay.h> void main(void) { // USART initialization // Communication Parameters: 8 Data, 1 Stop, No Parity // USART Receiver: Off // USART Transmitter: On // USART Mode: Asynchronous // USART Baud Rate: 9600 UCSRA=0x00; UCSRB=0x08; UCSRC=0x86; UBRRH=0x00; UBRRL=0x33; while (1) { putchar('a'); delay_ms(1000); } } |
Проект создан при помощи визарда. Скорость 9600, настроен только передатчик, посылаем символ ‘a’ раз в секунду библиотечной функцией putchar. Программа пишется за 30 секунд 🙂
Теперь перейдем к нашей цели. Последуем той же логике, что и выше: настройка передатчика, на скорость 9600, отправка байта. Начнем с начала:
#inlude <mega8.h> |
Задумывались ли вы когда нибудь что внутри этого файла? Многие скорее всего даже не догадываются. Так вот микроконтроллер не знает что такое PORTD и ему абсолютно на это наплевать, ровно также он не знает что такое UDR, DDRD и т.п. Изначально названия регистров это просто выдуманные общепринятые дефайны, вы можете совершенно спокойно написать вместо PORTD — PORTDDDDD, или еще как угодно, важно чтобы тем сам не нарушалась логика работы.
Весь смысл в том, что все регистры имеют свой адрес. Поэтому когда мы пишем PORTD=0xFF, то на самом деле мы записываем число 0xFF в адрес 0x12. Так вот все эти адреса и прописаны в файле mega8.h в ассемблере есть аналог такого файла называется он m8Adef.inc, и его содержимое идентично. Поэтому если мы хотим обращаться к регистрам по именам, то мы должны включить данный файл в проект.
.include "m8Adef.inc" |
Как узнать каким образом подключать файл к проекту? Да посмотреть 2-3 исходника из гугла, не обязательно читать тонны книг и мануалов. Как узнать название заголовочного файла именно для вашего микроконтроллера? В директории установленной авр студии/атмел студии есть папка include, там все имена заголовочных файлов. Думаю не нужно быть телепатом, чтобы догадаться что m8def для меги8, m8Adef для меги8а, m16def для меги16 и т.п.
Две следующие библиотеки пропускаю, наверняка есть их аналоги для асма, но сейчас они нам не интересны.
Точка входа в программу, да да все программы должны откуда то начинаться, в си это функция main(), для асма это .ORG.
Далее .CSEG — все что ниже помещается во флеш, не для кого не секрет, что мы прошивку грузим во флеш. Как все это узнал? Посмотрел готовые прошивки. Загуглил. 1 раз чтобы запомнить на всю оставшуюся жизнь.
Дальше идут настройки самого юарта, кодоген для 8МГц выдал это:
UCSRA=0x00; UCSRB=0x08; UCSRC=0x86; UBRRH=0x00; UBRRL=0x33; |
Как же быть с асмом? Идем в даташит и в разделе UART выдираем код инициализации:
USART_Init: ; Set baud rate out UBRRH, r17 out UBRRL, r16 ; Enable Receiver and Transmitter ldi r16, (1<<RXEN)|(1<<TXEN) out UCSRB,r16 ; Set frame format: 8data, 2stop bit ldi r16, (1<<URSEL)|(1<<USBS)|(3<<UCSZ0) out UCSRC,r16 ret |
Воу, воу, мы же еще не знаем синтаксис. Там тоже все просто, все в том же даташите есть таблица Instruction Set Summary, где расписаны все команды, которые можно использовать для этого камня. Там даже есть описание того, что делает каждая команда. Вероятно лень читать — попробуем немного разъяснить принципы:
USART_Init: ... ret |
аналог этого в сишке это произвольная функция
void USART_Init() { ... } |
Смысл абсолютно тот же
Как бы мы вызвали функцию в сишечке?
USART_Init(); |
Как мы вызовем ее в асме?
rcall USART_Init |
Смотрим дальше out это вывод числа в регистр
out UBRRH, r17 |
Ты же помнишь анон что регистр UBRRH относится к настройке юарта?
Аналог в си это присвоение значения через некую промежуточную переменную
UBRRH = temp; |
Только здесь r17 это регистр общего назначения, коих у нас аж 32. Непонятно? Представьте себе что r0-r31 это тупо переменные типа char. Кроме того, работа с некоторыми командами имеет свои особенности, например нельзя в порт писать число напрямую т.е команда
out PORTD, 0x01 |
не прокатит, нужно делать так
ldi r16,0x01 ;Загрузить число в регистр общего назначения out PORTD, r16 ;из регистра отрыгнуть это в порт |
Это тоже одна из немногих истин которые нужно где то услышать, прочитать, увидеть и запомнить один раз на всю жизнь.
Таки смысл out UBRRH, r17 становится понятен, положить в r17 некое число, которое далее из r17 отправляем в UBRRH. Собственно и смысл остального должен быть понятен:
USART_Init: ;подпрограмма инициализации юарта ; Set baud rate out UBRRH, r17 ;настройка бодрейта out UBRRL, r16 ; Enable Receiver and Transmitter ldi r16, (1<<RXEN)|(1<<TXEN) ;настройка приемника и передатчика out UCSRB,r16 ; Set frame format: 8data, 2stop bit ldi r16, (1<<URSEL)|(1<<USBS)|(3<<UCSZ0) ;настройка стопбитов и количества битов данных out UCSRC,r16 ret |
Естественно вы должны понимать, что каждый раз перед использованием r16 и r17, в них уже должно лежать некое число, которое вы посчитали по формуле из даташита, для своей частоты и скорости юарта. Для ленивых можно сделать проще, взять эти числа из генератора CAVR, это ведь не меняет смысл, итого строчки
UCSRA=0x00; UCSRB=0x08; UCSRC=0x86; UBRRH=0x00; UBRRL=0x33; |
Превратятся в
clr r16 ;очистить r16 т.е. = 0 out UCSRA,r16 ;UCSRA = 0 ldi r16,0x08 ;загрузить 0x08 в r16 out UCSRB,r16 ;UCSRB = 0x08 ldi r16,0x86 out UCSRC,r16 clr r16 out UBRRH,r16 ldi r16,0x33 out UBRRL,r16 |
Так ли уж существенна разница? Да есть предварительный загруз числа в некий промежуточный регистр, но так ли это критично с точки зрения юзера?
Далее перейдем к нашей функции putchar(), увы открыть либу кодвижна не возможно стандартными средствами, поэтому можно только догадываться что скрывается в этой функции, поэтому идем в наш любимый даташит и берем кусок кода оттуда аналогичный putchar. Опять оформлено в виде функции, все четко.
USART_Transmit: ; Wait for empty transmit buffer sbis UCSRA,UDRE rjmp USART_Transmit ; Put data (r16) into buffer, sends the data out UDR,r16 ret |
Новая команда sbis — Skip if Bit in I/O Register is Set — сравнение, если равно единице, то пропустить следующую строчку. Аналог в си if(bit != 1){} В нашем случае пока UDRE не равен 1, выполняется строчка rjmp, т.е программа прыгнет к метке USART_Transmit, таким образом выполняется проверка пуст ли буфер и до тех пор пока он не освободится мы так и будем крутиться в цикле проверки. Аналог этому в си:
while ( !( UCSRA & (1<<UDRE)) ) |
И если буфер пустой т.е. UDRE == 1, то можно смело отправлять информацию, т.е. присваивать значение регистру UDR, допустим символ ‘a’
USART_Transmit: sbis UCSRA,UDRE rjmp USART_Transmit ldi r16,'a' out UDR,r16 |
Осталось реализовать только задержку, тут немного позаковыристей, при тактовой 8МГц, один такт у нас выполняется 1/8000 000=0,000000125c, нам надо сделать задержку в 1с, т.е. как раз выполнить 8000 000 тактов 🙂
пары регистров r27:r26, r29:r28, r31:r30 образуют регистры X,Y,Z к которым можно обращаться как к 16битным. Будем их юзать загрузим в них число побольше и вычитаем из него единицу, пока оно не станет равным нулю.
d01ms: ldi YL,low(4000) ;1 такт ldi YH,high(4000) ;2 такт d01_1: sbiw YL,1 ;3 такт ;5 такт ;7 такт вычитаем единицу brne d01_1 ;4 такт ;6 такт ;8 такт ... пока регистр Y не станет равным 0 ret |
Таким образом задержка будет выполняться: ((1 такт на вычитание + 1 такт на проверку равенства нулю)*4000раз вычитание*0.000000125секунд)+ (1 и 2 такт на первоначальную загрузку) = 0.001с
Останется вызвать эту подпрограмму еще 500 раз 🙂
d01ms: ;подпрограмма 1мс ldi YL,low(500) ;загружаем частями ldi YH,high(500) d01_1: sbiw YL,1 ;вычитаем из 4000 единицу brne d01_1 ;пока результат не станет равным нулю ret d1000ms: ;вызываем подпрограмму 1мс еще 500раз ldi XL,low(500) ldi XH,high(500) d1000: rcall d01ms sbiw XL,1 brne d1000 ret |
Осталось только все это оформить финальным кодом.
.include "m8Adef.inc" .CSEG .ORG 0 ;инициализация юарта clr r16 out UCSRA,r16 ldi r16,0x08 out UCSRB,r16 ldi r16,0x86 out UCSRC,r16 clr r16 out UBRRH,r16 ldi r16,0x33 out UBRRL,r16 ;функция передачи байта USART_Transmit: sbis UCSRA,UDRE rjmp USART_Transmit ldi r16,'a' out UDR,r16 ;задержка в 1000мс rcall d1000ms ;бесконечный цикл, прыгаем к USART_Transmit rjmp USART_Transmit ;функции задержки d01ms: ;инициализация ldi YL,low(4000) ldi YH,high(4000) d01_1: ;вычитание и проверка равенству 0 sbiw YL,1 brne d01_1 ret d1000ms: ldi XL,low(500) ;инициализация ldi XH,high(500) d1000: ;вычитание и проверка равенству 0 rcall d01ms sbiw XL,1 brne d1000 ret |
Вы можете скомпилировать сишную прошивку в CAVR, в папке проекта появится асмовский файл в котором будет весьма похожий исходник. Полученный хекс CAVR — 529байт, атмел студии — 190байт. Делают одно и тоже.
Собственно все рассказано довольно таки утрированно, конечно есть много особенностей, много тонкостей, просто относитесь к статье в прочем как и к ассемблеру спокойно. Выводы вы должны сделать самостоятельно. В качестве своих доводов напишу:
Если он вам помогает разобраться в тонкостях микроконтроллера, или без этого не обойтись, или просто нравится писать на асме — пишите.
Но если вы думаете:
-что на нем нельзя говнокодить, то ошибаетесь, можно.
-что написав программу на асме вы тут же станете профессионалом, то это не так.
-что ваши программы будут мега быстрыми, это тоже не так.
-что начинать нужно именно с асма — это ваш выбор, главное чтобы вы не бросили еще не начав, таких примеров масса.
-что на это уйдут годы — все зависит только от вас, нет ничего невозможного.
В сухом остатке — выбор инструмента скорее зависит от исходных данных, необходимых условий, привычек, удобства. Что то я сегодня разошелся 🙂 Если вы до сюда честно дочитали и разобрались, обязательно скушайте печеньку с чаем. Надеюсь кому нибудь пригодится 🙂
Печеньку съел
Если вы хотите научиться программировать микроконтроллеры, всегда начинайте с языка C. Я «играться» начинал с AVR8 именно на С, потом перешел на STM8 и STM32 (т.к. их цена значительно ниже а возможностей больше) и переход этот был очень быстрым и легким. Сейчас во всю использую FreeRTOS. И если появиться более «рентабельный микроконтроллер», то переход на него я думаю тоже будет простым.
Стоит учесть, что компилятор Си кроме самих инструкций помещает в прошивку еще и библиотеки, отдельные функции которых могут не использоваться. От этого и лишние 300 байт.
Так что на практике код на Си зачастую занимает почти что равноценный с ассемблером объём. Там всё по-честному.
А оптимизации Си под AVR может позавидовать даже всю жизнь программировавший на ассемблере. Особенно подобным насладится можно в GCC компиляторах.
а я с асма для AVR как раз начинал. и юарт делал, и часы, и мигания разные…
потом перешёл на си.
Думаю, здесь нет универсального рецепта, все сугубо индивидуально.
Например в AT90S1200 и иных МК без ОЗУ без асма не обойтись