Проектирование систем на базе микроконтроллера AVR
БЕЛОРУССКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
МЕХАНИКО-МАТЕМАТИЧЕСКИЙ ФАКУЛЬТЕТ
Курсовая работа
на тему:
Проектирование систем на базе микроконтроллера AVR
Выполнил: студент
3 курса ММФ
Кенть Павел Геннадьевич
Руководитель:
Бухтояров Сергей Евгеньевич
Минск, 2012
Оглавление
Введение 3
Структура микроконтроллера 4
Простейшая программа. 7
Прерывания 8
Подключение дисплея (asm) 12
Программа для работы с дисплеем через COM-порт. 15
Литература 18
Введение
В современной электронике
микропроцессором называют специальную
микросхему, которая предназначена для
выполнения некоего набора сложных функций
по управлению тем либо иным электронным
устройством.
Микропроцессор – это сердце любого компьютера.
Но не только. Те же
технологии, которые применяются в компьютерах,
с успехом применяются
и в более простых электронных устройствах.
Теперь же интеграция пошла до фантастических пределов. Одна микросхема содержит не только сам процессор, но и сопутствующие ему элементы. Такая микросхема называется микроконтроллером.
Микроконтроллер (англ. Micro Controller Unit, MCU) — микросхема, предназначенная для управления электронными устройствами. Типичный микроконтроллер сочетает на одном кристалле функции процессора и периферийных устройств, содержит ОЗУ или ПЗУ. По сути, это однокристальный компьютер, способный выполнять простые задачи.
Для изучения мною были выбрана серия AVR т.к. это очень простые и удобные в использовании микроконтроллеры. Чтобы задействовать какое-нибудь периферийное устройства, понадобятся всего пара строчек кода, что позволяет разработчику сосредоточить свое внимание над самой задачей. Также, одно из преимуществ данных микроконтроллеров (а может быть и главное) является большое количество сред для разработки/отладки приложении, сформировавшиеся сообщества, огромное количество библиотек/проектов/примеров.
В своей работе я приведу примеры программ, написанных на двух языках программирования: Assembler и C. Все программы на ассемблере будут написаны в среде AVRStudio, а программа на C будет написана в среде mikroC PRO for AVR. Для проверки правильности работы программ будет использоваться программа моделирования Proteus.
Структура микроконтроллера
Перед тем, как начать писать программы, нужно понять, из чего же состоит микроконтроллер. На рисунке 1.1 представлена структурная схема типичного современного микроконтроллера.
Из рисунка видно, что
микроконтроллер может
дополнительных узлов, так как большое
число периферийных схем
уже имеется непосредственно на кристалле
микроконтроллера. Это
позволяет уменьшить, размеры конструкции
и снизить потребление
энергии от источника питания.
Давайте рассмотрим типичные схемы, присутствующие в микроконтроллерах.
1. Центральное процессорное устройство (ЦПУ) — сердце микроконтроллера. Оно принимает из памяти программ коды команд, декодирует их и выполняет. ЦПУ состоит из регистров, арифметико-логического устройства (АЛУ) и цепей управления.
2. Память программ. Здесь хранятся коды команд, последовательность которых формирует программу для микроконтроллера.
3. Оперативная память данных. Здесь хранятся переменные программ. У большинства микроконтроллеров здесь расположен также стек.
4. Тактовый генератор. Этот генератор определяет скорость работы микроконтроллера.
5. Цепь сброса. Эта цепь служит для правильного запуска микроконтроллера.
6. Последовательный порт — очень полезный элемент микроконтроллера. Он позволяет обмениваться данными с внешними устройствами при малом количестве проводов.
7. Цифровые линии ввода/вывода. По сравнению с последовательным портом, с помощью этих линий возможно управлять одновременно несколькими линиями (или проверять несколько линий).
8. Таймер. Используется для отсчета временных интервалов.
9. Сторожевой таймер. Это специальный таймер, предназначенный для предотвращения сбоев программы. Он работает следующим образом: после запуска он начинает отсчет заданного временного интервала. Если программа не перезапустит его до истечения этого интервала времени, сторожевой таймер перезапустит микроконтроллер. Таким образом, программа должна давать сторожевому таймеру сигнал — все в порядке. Если она этого не сделала, значит, по какой-либо причине произошел сбой.[2]
Семейство микроконтроллеров AVR довольно обширно. Оно делится на три группы: Classic, Tiny и Mega. Вообще говоря, для решения задачи может быть использован практически любой микроконтроллер этого семейства. Система команд для всех микроконтроллеров одинаковая, отличие составляет лишь усложнение или упрощение внутренней архитектуры и увеличение или уменьшение аппаратных ресурсов. Мной был выбран представитель группы Mega8-битный микроконтроллер ATMega8, так как он занимает среднее положение между сложностью внутренней архитектуры и наличием аппаратных ресурсов.
Основные параметры ATMega8:
Flash (Кбайт) |
EEPROM (байт) |
SRAM (байт) |
Линий ввода-вывода |
Fmax (МГц) |
Vcc (В) |
8 |
512 |
1024 |
23 |
16 |
2.7-5.5 |
Flash: объем внутренней памяти микроконтроллера, используемой в качестве памяти программ (память программ – пространство, которое содержит машинный код выполняемый микроконтроллером и полученный в результате трансляции специальной программной среды).
EEPROM: объем внутренней энергонезависимой памяти данных (память данных – пространство используемое микроконтроллером для временного хранения полученных в результате работы вычислений и прочих данных которые будут использованы в дальнейшем).
SRAM: объем внутренней энергозависимой памяти данных.
Линии ввода-вывода: фактически количество ножек микроконтроллера, с помощью которых он взаимодействует с внешним миром.
Fmax – максимальное значение рабочей тактовой частоты микроконтроллера, чем она выше, тем соответственно он работает быстрее.
Vcc – питание необходимое для работы микроконтроллера, чем меньше значение питания тем меньше энергопотребление, тем эффективнее использование микроконтроллера.
Микроконтроллеры серии AVR имеют так называемую Гарвардскую архитектуру, основной особенностью которой является наличие раздельной шины данных и шины команд.[3]
Простейшая программа.
Рассмотрим очень простой пример: написать программу, которая при первом нажатии на кнопку зажигает светодиод, а при повторном нажатии тушит его.
Удобнее всего светодиод
подключить к одному из портов, а
кнопку — к другому. В этом случае
управляющая программа должна будет
настроить порт,
к которому подключен светодиод, на вывод,
а порт, к которому подключена
кнопка, на ввод. Других специальных требований
к микроконтроллеру не требуется.
.include "m8def.inc" ;Подключение микроконтроллера ATMega8
.list
.cseg ; Выбор сегмента программного кода
.org 0 ; Устанеовка текущего адреса на ноль
.def temp = r16 ; Переименуем регистры для упрощения дальнейшего использования
.def rab = r17
ldi rab, 0
;-------------------------- Инициализация портов Вода-Вывода
out DDRD, temp ; Записываем ноль в DDRD (порт PD на ввод)
ldi temp, 0xFF ; Записываем число FF в регистр temp
out DDRB, temp ; Записываем это число в DDRB (порт PB на вывод)
out PORTD, temp ; Записываем то же число в PORTB (потушить светодиод)
out PORTB, temp ; Записываем его же в PORTD (включаем внутр. резисторы)
;-----------------------------
wait_off: in temp, PIND
sbrs temp, 0
rjmp wait_off
wait_on: in temp, PIND
sbrc temp, 0
rjmp wait_on
sbrc rab, 3
rjmp vikl
vkl: ldi rab,0b01010101
out PORTB, rab
ldi rab, 0xFF
rjmp wait_off
vikl: ldi temp, 0xFF
out PORTB, temp
ldi rab, 0
rjmp wait_off
Принцип работы очень простой. В начале программы настраиваем порты ввода-вывода и переходим к основному циклу. Далее проверяем значение на PD0 (ножка 2) для определения, нажата ли кнопка. Значение rab показывает, что нужно делать со светодиодом (зажигать или тушить), и соответственно, подавать нужные значения на порт В. Метки wait_off и wait_on нужны для того, чтобы при удерживании кнопки в нажатом состоянии светодиод не моргал.
Прерывания
Не будет преувеличением
утверждать, что основное занятие
почти любого
современного вычислительного устройства,
от простенького контроллера
стиральной машины до "навороченного"
ноутбука — ожидание и обработка
неких событий. Это могут быть сигналы
от других устройств, посылаемые
в автоматическом режиме, а также действия
пользователя. Как можно обнаружить и
обработать такие события?
Наиболее универсальный
способ, который применяется в
системах всех
уровней, — организация основной программы
в виде бесконечного цикла.
Внутри этого цикла тем или иным способом
отслеживается возникновение
неких событий — такой цикл, например,
представляет собой почти любая
Windows-программа (как и Windows в целом). В основе
такого способа лежит
один и тот же принцип: при наступлении
события устанавливается определенное
значение некоей переменной, часто именуемой
флагом; в простейшем
случае это может быть один бит в специально
отведенном регистре или в
ячейке памяти (или даже непосредственно
состояние вывода МК). Основная
программа в цикле проверяет значение
всех необходимых флагов, и при изменении
какого-то из них переходит к соответствующей
процедуре обработки события.
Внешних прерываний у большинства
МК AVR два или три. Естественно, они могут
возникать независимо друг от друга. Внешнее
прерывание — событие, которое наступает
при наличии сигнала на одном из входов,
специально пред-
назначенных для этого (например, INTO и
INT1).
Выделяют три вида событий,
вызывающих такое прерывание, и их
можно
различать, программно: это может быть
низкий уровень напряжения, а также
положительный или отрицательный фронт
на соответствующем выводе. Любопытно,
что прерывания по всем этим событиям
выполняются, даже если
соответствующий вывод порта сконфигурирован
на выход (это, в частности,
позволяет вызывать такие прерывания
программно).
Кратко рассмотрим особенности
этих режимов. Прерывание по низкому
уровню (режим установлен по умолчанию,
для его инициализации достаточно разрешить
соответствующее прерывание) возникает всякий раз, когда
на
соответствующем входе присутствует низкий
уровень. "Всякий раз" — это
значит, что действительно всякий, т. е.
если отрицательный импульс длится
какое-то время, то прерывание, закончившись
(т. е. когда выполнится соответствующий
обработчик), повторится снова и снова,
почти не давая основ-
ной программе работать. Поэтому обычная
схема использования этого режима — сразу
же по возникновении в начале обработчика
его запретить (процедура обработки при
этом, раз уж началась, выполнится до конца)
и разрешить
опять только тогда, когда внешнее воздействие
должно уже закончиться (на-
пример, если это нажатие кнопки, то его
следует опять разрешить по таймеру
через одну-две секунды). Выход из обработки
прерывания выполняется с помощью команды reti.
Ниже представлена программа, которая при нажатии одной из кнопок либо включает светодиод с семисегментным индикатором, либо выключает их. Обработка производиться с помощью прерываний.
Программа работает аналогично предыдущей, отличие лишь в том, что основной часть программы – бесконечный пустой цикл, а остальное – обработка прерываний. Также следует добавить, что за разрешение или запрет прерываний int0, int1 отвечают два старших бита регистра GICR.[5],[1]
include "m8def.inc"
.list
.def temp = r16
.def rab = r17; Определение главного рабочего регистра
.cseg ; Выбор сегмента программного кода
.org 0 ; Устанеовка текущего адреса на ноль
;далее идет таблица прерываний, в которой мы указываем, что делать при выполнении того или ;иного прерывания. Подробнее с таблицей прерываний можно ознакомиться в datasheet ;микроконтроллера
rjmp reset ; Переход на начало программы
rjmp prer ;прерывание int0
rjmp prer ;прерывание int1
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reti
reset:
ldi temp,low(RAMEND)
out SPL,temp
ldi temp, high(RAMEND)
out SPH,temp
;-------------------------- Инициализация портов ВВ
ldi temp,0
ldi rab, 0
out DDRD, temp ; Записываем ноль в DDRD (порт PD на ввод)
ldi temp, 0xFF ; Записываем число $FF в регистр temp
out DDRB, temp ; Записываем это число в DDRB (порт PB на вывод)
out PORTD, temp ; Записываем то же число в PORTB (потушить светодиод)
out PORTB, temp ; Записываем его же в PORTD (включаем внутр. резисторы)
;----------------------------- Основной цикл--------------------------
ldi temp,0b11000000
out GICR,temp ;Разрешаем прерывания int0,int1
sei
stop:
rjmp stop
prer: clr temp
out GICR,temp ;Запрещаем прерывания int0,int1
sbrc rab, 3
rjmp vikl
vkl: ldi rab,0
out PORTB, rab
ldi rab, 0xFF
ldi temp,0b11000000
out GICR,temp ;Разрешаем прерывания int0,int1
reti
vikl: ldi temp, 0b11010101
out PORTB, temp
ldi rab, 0
ldi temp,0b11000000
out GICR,temp ;Разрешаем прерывания int0,int1
reti
Подключение дисплея (asm)
Поставим перед собой задачу написания программы на ассемблере, для работы с дисплеем. А именно программа будет выводить на дисплей некоторый текст. Для написания программы, добавим библиотеку hd44780.asm, текст которой представлен ниже. В ней происходит инициализация дисплея, выбор порта для подключения, настройки курсора и описание функций.
#define HD44780_CLEAR 0x01
#define HD44780_HOME 0x02
#define HD44780_ENTRY_MODE 0x04
#define HD44780_EM_SHIFT_CURSOR 0
#define HD44780_EM_SHIFT_DISPLAY 1
#define HD44780_EM_DECREMENT 0
#define HD44780_EM_INCREMENT 2
#define HD44780_DISPLAY_ONOFF 0x08
#define HD44780_DISPLAY_OFF 0
#define HD44780_DISPLAY_ON 4
#define HD44780_CURSOR_OFF 0
#define HD44780_CURSOR_ON 2
#define HD44780_CURSOR_NOBLINK 0
#define HD44780_CURSOR_BLINK 1
#define HD44780_DISPLAY_CURSOR_SHIFT
#define HD44780_SHIFT_CURSOR 0
#define HD44780_SHIFT_DISPLAY 8
#define HD44780_SHIFT_LEFT 0
#define HD44780_SHIFT_RIGHT 4
#define HD44780_FUNCTION_SET 0x20
#define HD44780_FONT5x7 0
#define HD44780_FONT5x10 4
#define HD44780_ONE_LINE 0
#define HD44780_TWO_LINE 8
#define HD44780_4_BIT 0
#define HD44780_8_BIT 16
#define HD44780_CGRAM_SET 0x40
#define HD44780_DDRAM_SET 0x80
.equ LCD_PORT = PORTC
.equ LCD_DDR = DDRC
.equ LCD_PIN = PINC
.equ LCD_D4 = 0
.equ LCD_D5 = 1
.equ LCD_D6 = 2
.equ LCD_D7 = 3
.equ LCD_RS = 4
.equ LCD_EN = 5
;-----------------------------
LCD_WriteNibble:
sbi LCD_PORT, LCD_EN
sbrs r16, 0
cbi LCD_PORT, LCD_D4
sbrc r16, 0
sbi LCD_PORT, LCD_D4
sbrs r16, 1
cbi LCD_PORT, LCD_D5
sbrc r16, 1
sbi LCD_PORT, LCD_D5
sbrs r16, 2
cbi LCD_PORT, LCD_D6
sbrc r16, 2
sbi LCD_PORT, LCD_D6
sbrs r16, 3
cbi LCD_PORT, LCD_D7
sbrc r16, 3
sbi LCD_PORT, LCD_D7
cbi LCD_PORT, LCD_EN
ret
;-----------------------------
LCD_WriteData:
sbi LCD_PORT, LCD_RS
push r16
swap r16
rcall LCD_WriteNibble
pop r16
rcall LCD_WriteNibble
clr XH
ldi XL,250
rcall Wait4xCycles
ret
;-----------------------------
LCD_SetAddressDD:
ori r16, HD44780_DDRAM_SET
rcall LCD_WriteCommand
ret
;-----------------------------
LCD_Init:
sbi LCD_DDR, LCD_D4
sbi LCD_DDR, LCD_D5
sbi LCD_DDR, LCD_D6
sbi LCD_DDR, LCD_D7
sbi LCD_DDR, LCD_RS
sbi LCD_DDR, LCD_EN
cbi LCD_PORT, LCD_RS
cbi LCD_PORT, LCD_EN
ldi r16, 100
rcall WaitMiliseconds
ldi r17, 3
InitLoop:
ldi r16, 0x03
rcall LCD_WriteNibble
ldi r16, 5
rcall WaitMiliseconds
dec r17
brne InitLoop
ldi r16, 0x02
rcall LCD_WriteNibble
ldi r16, 1
rcall WaitMiliseconds
ldi r16, HD44780_FUNCTION_SET | HD44780_FONT5x7 | HD44780_TWO_LINE | HD44780_4_BIT
rcall LCD_WriteCommand
ldi r16, HD44780_DISPLAY_ONOFF | HD44780_DISPLAY_OFF
rcall LCD_WriteCommand
ldi r16, HD44780_CLEAR
rcall LCD_WriteCommand
ldi r16, HD44780_ENTRY_MODE |HD44780_EM_SHIFT_CURSOR | HD44780_EM_INCREMENT
rcall LCD_WriteCommand
ldi r16, HD44780_DISPLAY_ONOFF | HD44780_DISPLAY_ON | HD44780_CURSOR_ON | HD44780_CURSOR_BLINK
rcall LCD_WriteCommand
ret
;-----------------------------
#ifndef F_CPU
#error "F_CPU must be defined!"
#endif
#if F_CPU < 4000000
#warning "F_CPU too low, possible wrong delay"
#endif
#define CYCLES_PER_US (F_CPU/1000000)
#define C4PUS (CYCLES_PER_US/4)
#define DVUS(x) (C4PUS*x)
;-----------------------------
; Input : XH:XL - number of CPU cycles to wait (divided by four)
;-----------------------------
Wait4xCycles:
sbiw XH:XL, 1
brne Wait4xCycles
ret
;-----------------------------
; Input : r16 - number of miliseconds to wait
;---------- WaitMiliseconds:
push r16
WaitMsLoop:
ldi XH,HIGH(DVUS(500))
ldi XL,LOW(DVUS(500))
rcall Wait4xCycles
ldi XH,HIGH(DVUS(500))
ldi XL,LOW(DVUS(500))
rcall Wait4xCycles
dec r16
brne WaitMsLoop
pop r16
ret
;-----------------------------
; End of file
;-----------------------------
;**** Subroutine Register Variables
.def drem8u =r15 ;remainder
.def dres8u =r16 ;result
.def dd8u =r16 ;dividend
.def dv8u =r17 ;divisor
.def dcnt8u =r18 ;loop counter
;***** Code
div8u:
sub drem8u,drem8u ; clear remainder and carry
ldi dcnt8u,9 ; init loop counter
d8u_1:
rol dd8u ; shift left dividend
dec dcnt8u ; decrement counter
brne d8u_2 ; if done
ret ; return
d8u_2:
rol drem8u ; shift dividend into remainder
sub drem8u,dv8u ; remainder = remainder - divisor
brcc d8u_3 ; if result negative
add drem8u,dv8u ; restore remainder
clc ; clear carry to be shifted into result
rjmp d8u_1 ; else
d8u_3: sec ; set carry to be shifted into result
rjmp d8u_1
Подключив библиотеку hd44780.asm программа вывода текста становиться очень простой. Работа программы хорошо описана в комментариях.
.include "m8def.inc";Используем Мегу16
;=============================
#define F_CPU (8000000)
;Здесь идёт определение тактовой частоты. Нужно для
;библиотеки задержек,
;Частота в Герцах.
;=============================
ldi r16,low(RAMend)
out SPL,r16
ldi r16,high(RAMend)
out SPH,r16
;Инициализация стека. Обязательно!!!
;-----------------------------
rcall lcd_init;Инициализация дисплея.
;-----------------------------
ldi r16, 'K'
rcall LCD_WriteData
ldi r16, 'e'
rcall LCD_WriteData
ldi r16, 'n'
rcall LCD_WriteData
ldi r16, 't'
rcall LCD_WriteData
ldi r16,0x40
rcall LCD_SetAddressDD
; Переходим ко второй строке, поэтому запись будем начинать с 40-го адреса
ldi r16, 'P' ;Выводим на дсплей следующую строку
rcall LCD_WriteData
ldi r16, 'a'
rcall LCD_WriteData
ldi r16, 'v'
rcall LCD_WriteData
ldi r16, 'e'
rcall LCD_WriteData
ldi r16, 'l'
rcall LCD_WriteData
;-----------------------------
main:
;В главном цикле мы ничего не делаем.
rjmp main
;-----------------------------
.include "hd44780.asm"; Подключение библиотеки для работы с дисплеем.
Программа для работы с дисплеем через COM-порт.
Как упоминалось выше, микроконтроллер может работать и с компьютером через последовательный порт UART (universal asynchronous receiver-transmitter, “универсальный асинхронный приемопередатчик”).
Поставим перед собой задачу: микроконтроллер принимает через COM-порт некоторую строку, вводимую пользователем на компьютере, а затем выводит ее на дисплей.
Рассмотрим программу для микроконтроллера, написанную в среде mikroC PRO for AVR на языке программирования С. В начале программы происходит инициализация дисплея – указывается, каким образом микроконтроллер будет соединен с дисплеем. Очень важно установить частоту приема-передачи данных одинаковой с частотой COM-порта. В нашей программе сделаем ее равной 9600. Программа записывает принятые данные, до прихода «!!!» – что будет являться признаком конца строки.
Программа на VisualBasic отправляет строку, введенную пользователем через UART, при этом дописывая в конце «!!!». Также программа подсказывает, что максимальное число символов, которые может корректно вывести дисплей, равняется 16.[5]
Прграмма на С |
Программа на VisualBasic |
sbit LCD_RS at PORTB2_bit; sbit LCD_EN at PORTB3_bit; sbit LCD_D7 at PORTB4_bit; sbit LCD_D6 at PORTB5_bit; sbit LCD_D5 at PORTB6_bit; sbit LCD_D4 at PORTB7_bit;
sbit LCD_RS_Direction at DDB2_bit; sbit LCD_EN_Direction at DDB3_bit; sbit LCD_D7_Direction at DDB4_bit; sbit LCD_D6_Direction at DDB5_bit; sbit LCD_D5_Direction at DDB6_bit; sbit LCD_D4_Direction at DDB7_bit; int i,pos; char text[20]; void main() { LCD_Init(); LCD_Cmd(_LCD_CURSOR_OFF); UART1_Init(9600); while (1) { if (UART1_Data_Ready() == 1) { UART1_Read_Text(text, "!!!", 255); Lcd_Cmd(_LCD_CLEAR); LCD_Out(1,1,text); text[0]='\0'; } }} |
Public Class Form1
Private Sub ComboBox1_
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click SerialPort1.PortName = "COM2" SerialPort1.Open() Label4.ForeColor = Color.Green Label4.Text = "Connected" End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click SerialPort1.Write(TextBox1. SerialPort1.Write("!!!") End Sub
Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
If (Len(TextBox1.Text) > 16) Then Label2.ForeColor = Color.Red Else Label2.ForeColor = Color.Black End If Label2.Text = Len(TextBox1.Text) End Sub
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click SerialPort1.Close() Label4.ForeColor = Color.Red Label4.Text = "Disconnected" End Sub
End Class |
С помощью программы VSPE создадим виртуальную пару COM-портов (COM1 и COM2). К порту COM1 подключим микроконтроллер, а к COM2 будет подключаться пользователь.
Теперь осталось создать проект в Proteus, не забыв установить частоту 9600.
Проверим правильность работы всех программ:
Литература
- Белов А. В. «Самоучитель разработчика устройств на микроконтроллерах AVR» – Наука и Техника, Санкт-Петербург, 2008.
- Голубцов М. С. «Микроконтроллеры AVR: от простого к сложному» – СОЛОН-Пресс, Москва, 2003.
- Евстисфеев А. В. «Микроконтроллеры AVR семейства Mega» – издательский дом «Додэка – XXI», Москва, 2007.
- Кравченко А. В. «10 практических устройств на AVR-микроконтроллерах» – издательский дом «Додэка – XXI», 2008.
- Ревич Ю. «Практическое программирование микроконтроллеров Atmel AVR на языке ассемблера» - БВХ-Петербург, 2008.