Периодически вспоминается разговор с одним из выпускников. Мы обсуждали кем он хочет стать и какую ему выбрать профессию. В результате он сказал «хочу программировать микроконтроллеры на асме», ответа на вопрос почему не начать с Си, не последовало :) В общем всем студентам-любителям асма посвящается…

Давным давно, еще когда только начал изучать 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байт. Делают одно и тоже.

Собственно все рассказано довольно таки утрированно, конечно есть много особенностей, много тонкостей, просто относитесь к статье в прочем как и к ассемблеру спокойно. Выводы вы должны сделать самостоятельно. В качестве своих доводов напишу:
Если он вам помогает разобраться в тонкостях микроконтроллера, или без этого не обойтись, или просто нравится писать на асме — пишите.
Но если вы думаете:
-что на нем нельзя говнокодить, то ошибаетесь, можно.
-что написав программу на асме вы тут же станете профессионалом, то это не так.
-что ваши программы будут мега быстрыми, это тоже не так.
-что начинать нужно именно с асма — это ваш выбор, главное чтобы вы не бросили еще не начав, таких примеров масса.
-что на это уйдут годы — все зависит только от вас, нет ничего невозможного.

В сухом остатке — выбор инструмента скорее зависит от исходных данных, необходимых условий, привычек, удобства. Что то я сегодня разошелся :) Если вы до сюда честно дочитали и разобрались, обязательно скушайте печеньку с чаем. Надеюсь кому нибудь пригодится :)

6 комментариев: C vs ASM

  • Печеньку съел :mrgreen:

  • Если вы хотите научиться программировать микроконтроллеры, всегда начинайте с языка C. Я «играться» начинал с AVR8 именно на С, потом перешел на STM8 и STM32 (т.к. их цена значительно ниже а возможностей больше) и переход этот был очень быстрым и легким. Сейчас во всю использую FreeRTOS. И если появиться более «рентабельный микроконтроллер», то переход на него я думаю тоже будет простым.

  • Стоит учесть, что компилятор Си кроме самих инструкций помещает в прошивку еще и библиотеки, отдельные функции которых могут не использоваться. От этого и лишние 300 байт.
    Так что на практике код на Си зачастую занимает почти что равноценный с ассемблером объём. Там всё по-честному.
    А оптимизации Си под AVR может позавидовать даже всю жизнь программировавший на ассемблере. Особенно подобным насладится можно в GCC компиляторах.

  • а я с асма для AVR как раз начинал. и юарт делал, и часы, и мигания разные…
    потом перешёл на си.

  • Думаю, здесь нет универсального рецепта, все сугубо индивидуально.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Свежие записи
Последние комментарии
  • Загрузка...
Счетчик
Яндекс.Метрика