Мигаем светодиодами на языке программирования Assembler используя микроконтроллер Atmega8.

В данном уроке Мы с Вами начнем изучать Assembler и первое, что мы сделаем это поморгаем светодиодом. Изучать Assembler мы будем пошагово на примерах. Но для начала рассмотрим структуру памяти микроконтроллера Atmega8. Память микроконтроллера Atmega8 разбита на три отдельные части:

  1. Регистры общего назначения (РОН).
  2. Регистры ввода-вывода (РВВ).
  3. ОЗУ микроконтроллера.

При этом РОН и РВВ  имеют собственное адресное пространство, который могут использовать только команды определенного типа. Все у нас 32 регистра общего назначения с символьными именами R0…R31.  Данные регистры подключены непосредственно к арифметико-логическому устройству (АЛУ) и служат для обработки данных и вычислительных процессов. 
Доступ ко всем аппаратным ресурсам микроконтроллера производится через РВВ.  Наш микроконтроллер имеет 64 РВВ, у которых свой диапазон адресов. Символьные имена регистров и месторасположение в памяти РВВ зависит от конкретной модели микроконтроллера.
Дальше у нас идет ОЗУ, основным предназначением которого является хранение переменных нашей программы. 
После подачи напряжения питания, все РОН микроконтроллера обнуляются, регистры РВВ переходят в свои значения по умолчанию, значение ячеек  ОЗУ не как не сбрасываются — другими словами там храниться мусор (случайный набор 1 и 0).

Теперь мы можем начать писать нашу первую программу:

.include «m8def.inc»

.equ Delay = 50 ; установка константы времени задержки

.CSEG ; начало сегмента кода
.ORG 0x0000 ; начальное значение для адресации

; — инициализация стека —
LDI R16, Low(RAMEND) ; младший байт конечного адреса ОЗУ в R16
OUT SPL, R16 ; установка младшего байта указателя стека
LDI R16, High(RAMEND) ; старший байт конечного адреса ОЗУ в R16
OUT SPH, R16 ; установка старшего байта указателя стека

; — устанавливаем пины PD0 и PD1 порта PORTD (PD) на вывод —
ldi R16, 0b00000011 ; поместим в регистр R16 число 3 (0x3)
out DDRD, R16 ; загрузим значение из регистра R16 в порт DDRD

; — основной цикл программы —
Start:
sbi PORTD, PORTD0 ; подача на пин PD0 высокого уровня
cbi PORTD, PORTD1 ; подача на пин PD1 низкого уровня
rcall Wait ; вызываем подпрограмму задержки по времени
sbi PORTD, PORTD1 ; подача на пин PD1 высокого уровня
cbi PORTD, PORTD0
rcall Wait
rjmp Start ; возврат к метке Start, повторяем все в цикле

; — подпрограмма задержки по времени —
Wait:
   ldi r17, Delay ; загрузка константы для задержки в регистр R17
WLoop0:
   ldi  r18, 50 ; загружаем число 50 (0x32) в регистр R18
WLoop1:
   ldi r19, 0xC8 ; загружаем число 200 (0xC8) в регистр R19
WLoop2:
   dec R19 ; уменьшаем значение в регистре R19 на 1
   brne WLoop2 ; возврат к WLoop2 если значение в R19 не равно 0

   dec  r18 ; уменьшаем значение в регистре R18 на 1
   brne WLoop1 ; возврат к WLoop1 если значение в R18 не равно 0

   dec r17 ; уменьшаем значение в регистре R17 на 1
   brne WLoop0 ; возврат к WLoop0 если значение в R17 не равно 0
ret ; возврат из подпрограммы Wait

Рассмотрим начало нашей программы:
Директива  .CSEG  определяет начало программного сегмента. То есть,  все, что размещено ниже этой директивы относится к программному коду.
Для определения сегмента данных (RAM или памяти EEPROM) используются директивы  .DSEG и  .ESEG. Таким образом выполняется распределение памяти по сегментам.
Директивы  .ORG указывает компилятору начальный адрес «0x0000» сегмента, по умолчанию адрес программного кода всегда 0x0000.
Инициализация стека :  — инициализация области памяти, которая используется микропроцессором для хранения и последующего считывания адресов возврата из подпрограмм, а также для других программных нужд.
RAMEND — предопределенная константа в файле m8def.inc которая содержит адрес последней ячейки SRAM.
SP — указатель стека, который используется для адресации вершины стека.

Рассмотрим наши команды:
ldi  — загрузка константы в регистр (1 такт)
out — запись значения в порт ( 1 такт)
sbi — устанавливает бит в порту ( 2 такта)
cbi — сбрасывает бит в порту ( 2 такта)
dec — уменьшает значение регистра на 1
brne — перейти если не равно (1/2 такта)
Функция задержки у нас по сути с частотой 1МГц (так как у меня кварц 4МГц) уменьшает значения чисел внесенных в наши РОН. Таким образом мы можем высчитать задержку:   у нас первый цикл  Wloop2 состоит из двух команд которые занимают по 1 такту и 2 такта переход, и того у нас выходит 199*2 +3=401 тактов, следующий цикл у нас выполняется 50 раз, но в нем находиться первый цикл, и того у нас выходит 401*49+49+3= 19701 тактов. И последний цикл выполняется заданное количество нами, в нашем случаи 50 раз. И того у нас выходит 20102*49+49+3 = 985050 тиков. Учитывая нашу частоту, у нас выходит почти 1 секунда задержки.