Ликбез по периферии контроллеров AVRАртемий «Di Halt» Исламов (di_halt@mail.ru) Судя по числу писем, тема контроллеров актуальна и интересна – каждый второй спрашивал у меня, как реализовать на AVR какую-либо функцию. Поэтому я решил не распылять усилия, а выложить подробный мануал по использованию того барахла, что разработчики из Atmel насовали в свой микроконтроллер. В качестве примера опишу все на контроллере ATMega8. Почему не ATMega8535? А, для разнообразия. Чтобы, не замыкаясь на одном контроллере, наглядно показать, насколько они похожи. Великие и ужасные FUSE BitsВ прошлых статьях я советовал тебе не лезть к этим битам. И на то были свои основания. Неправильно выставив биты, ты можешь наглухо заблокировать контроллер для перепрошивки (или, вообще, какого-либо использования). Но без знания этой особенности контроллера далеко не уедешь. Так что, распишу все по порядку. У разных версий контроллеров число фьюзов разное, какие-то могут отсутствовать, но основные есть всегда. Вот по ним и пройдемся. Конфигурационные биты находятся в особой области памяти и могут быть изменены только с помощью программатора при записи контроллера. Итак, главное! В Atmel AVR принята нотация – сброшенный в ноль fuse bit считается активным. С одной стороны, это нелогично и криво, с другой – их контроллер, что хотят, то и делают. Поэтому просто запомни правило. Однако есть такой популярный программатор – Pony Prog. Они там решили, что всех умнее и сделали все наоборот: в Pony Prog уже установленный бит считается активным. Возникает жуткая путаница! Поэтому надо быть внимательным, как никогда (иначе последствия могут быть печальными). По умолчанию все контроллеры AVR сконфигурированы так, чтобы работать от внутреннего источника тактов. За это отвечают биты CKSEL. CKSEL3…0 = 0000 Внешний источник сигнала – на вход XTAL1 подаются прямоугольные импульсы. Такое делают в синхронных системах, когда несколько контроллеров работают от одного генератора. Чтобы запустить контроллер от внутреннего источника тактов, необходимо выставить CKSEL следующим образом: CKSEL3…0 = 0001 - 1MHz Иногда нужен внешний тактовый генератор, например, чтобы его подстраивать без вмешательства в прошивку. Для этого можно подключить RC цепочку, как показано на схеме и подсчитать частоту по формуле f = 1/3RC, где f будет частотой в герцах, а R и С, соответственно, сопротивлением резистора и емкостью конденсатора, в Омах и Фарадах. CKSEL3…0 = 0101 – для частот ниже 0.9 MHz Проблема внутреннего генератора и внешних RC-цепочек обычно в нестабильности частоты, и если сделать на ней часы, то они будут врать – не сильно, но будут. Поэтому бывает полезным запустить контроллер на кварце. Кроме того, только на кварце можно выдать максимум частоты, а значит, и производительности проца. CKSEL3…0 = 1001 – низкочастотный «часовой» кварц. На несколько десятков килогерц. Для обычных кварцев ситуация несколько отличается. Тут максимальная частота кварца зависит также и от бита CKOPT. Когда CKOPT = 1(по дефолту), то: CKSEL3…0 = 1010 или 1011 - от 0,4 до 0.9 MHz А если CKOPT равен 0? В этом случае, при тех же значения CКSEL, имеет смысл поставить кварц от 1 до 16MHz. Разумеется, кварц на 16MHz можно поставить только на Мегу без индекса «L». Стоит упомянуть бит SCKDIV8, которого нет в Atmega8, но который часто встречается в других контроллерах AVR. Это – делитель частоты. Когда он установлен (т.е. в нуле), то частота, выставленная в битах CКSEL0…3, делится на 8 – на чем в свое время застрял dlinyj, долго пытаясь понять, почему у него западло не работает. Прелесть в том, что этот делитель можно отключить программно, записав в регистр CLKPR нужный коэффициент деления, например, 1. Бит RSTDISBL способен превратить линию Reset в одну из ножек порта, что порой очень нужно, когда на какой-нибудь крошечной Tiny не хватает ножек на все задачи. Но надо помнить, что, если отрубить Reset, то автоматически отваливается возможность прошивать контроллер по пяти проводкам. И для перешивки потребуется высоковольтный параллельный программатор, который стоит несколько тысяч. На коленке сделать его проблематично, хотя и возможно. Я выложу на наш диск простенький HV-программатор для восьминогих контроллеров Tiny11-Tiny15; надо будет – спаяешь. Второй подлый бит – это SPIEN. Если его поставить в 1, то возможность прошивать «по-простому» тоже мгновенно отваливается. Опять потребуется параллельный программатор! WDTON отвечает за Собачий таймер, он же – Watch Dog. Этот таймер перезагружает процессор, если его периодически не сбрасывать (профилактика зависаний). Поставишь WDTON в 0, и собаку вообще нельзя будет выключить. Остальные биты особо тебе пока не нужны. Можешь их не трогать или почитать datasheet на предмет того, что они значат. Порты – это простоПочти все выводы микроконтроллера являются выводами портов. Собственно, назначение контроллера: дрыгать этими портами по своему усмотрению, управляя всяким барахлом, что на них заботливо повесил разработчик девайса. Режим работы вывода порта определяется регистрами DDRx, PORTх и PINx. Каждый бит регистра DDR указывает, в каком направлении работает соответствующая ножка порта: если в разряде «0», то эта ножка работает на вход, а если «1» – на выход. Выставляется обычно все один раз – в момент инициализации порта в начале программы. В зависимости от регистра DDR, регистр PORT может либо определять тип входа, либо, если нога настроена в DDR на выход, выдавать в ножку «1» или «0». DDR = 0, PORT = 0 – вывод порта работает в режиме Hi-Z, о котором я уже писал в статье «Электронные исходники». Напомню, вывод прикидывается обрывом и никак не влияет на подключенную к нему линию. Однако он может чувствовать логический уровень на шине. Зачем? Например, тебе надо засниферить сигнал в проводе витой пары. Напрямую сделать это нельзя – наличие какого-либо активного девайса на линии вызовет невозможность передачи данных. Когда же ты вешаешь туда ногу порта в режиме Hi-Z, то – никакой разницы, данные по-прежнему текут рекой, и ты их можешь спокойно считывать. DDR = 0 PORT = 1 – режим с подтягивающими резисторами (pullup). К выводу изнутри, через резистор, подается питание. В результате – на выводе слабая логическая единица. «Слабая» в том смысле, что ток через внутренний резистор течет крохотный, а значит, его можно легко придавить к нулевому уровню. Используется в тех случаях, когда надо получить сигнал от пассивного устройства, например, от кнопки. Вывод устанавливается в режим pullup и через кнопку замыкается на землю. Когда кнопка не нажата, подтяжка выставляет там единичку (это можно наблюдать в регистре PIN). А вот когда кнопку нажимают, то нога контроллера жестко замыкается на нулевой уровень, на землю, и в регистре PIN будет ноль. DDR = 1 PORT = 0 – на выходе будет нулевой уровень (земля). Тут все просто. Можно задавить, например, вывод с типом pullup у другого контроллера, четко дав понять ему, что мы на эту линию выставляем ноль. Или дать питание светодиоду. Для этого светодиод, через резистор, плюсом подключают к плюсу питания, а минусом – на вывод контроллера. Когда выставим DDR = 1 и PORT = 0, светодиод зажжется. DDR = 1 PORT = 1 – ситуация прямо обратная: на выводе будет напряжение питания (или близкое к нему). Пригодится, чтобы явно указать, что мы хотим на этой линии видеть единицу и ничто иное. Им тоже можно зажечь светодиод, только подключить его надо будет плюсом к ноге, а минусом к земле. В последних двух режимах надо помнить, что если таким способом попытаться дать питание какой-либо мощной нагрузке (скажем, лампочке), то велик риск спалить порт контроллера к чертовой матери. А если одну и ту же линию связи два контроллера потянут в разные стороны – один жестко вверх, другой жестко вниз – то почти наверняка один из них сгорит. Максимальный ток, который может потянуть на себя ножка порта – не более 30 миллиампер. Время пошло!Как засечь время, чтобы ровно через секунду, после того, как знакомый прошел под датчиком, активировать магнит, который скинет на него ведро воды? Конечно, нужно использовать таймер. В контроллерах AVR таймеров существует целая пачка. Помимо отсчета времени таймер может служить счетчиком, подсчитывая перепады напряжения на ножке T0 или T1. Или даже генерировать ШИМ-сигнал. Проходя через конденсаторный фильтр, тот преобразуется в постоянное напряжение, зависящее от формы ШИМ-сигнала. Таймер представляет из себя встроенный в контроллер девайс, который, будучи однажды запущенным, начинает отсчет. А когда досчитает до максимума, то, во-первых, выставит битовый флажок в специальном регистре, а во-вторых, может перехватить управление на себя, вызвав прерывание. Управляется таймер внешними регистрами. Их я сейчас и распишу, на примере восьмиразрядного таймера номер ноль, контроллера Atmega8. Регистр TCCR0 (Timer/Counter Control Register) задает режим работы таймера. В самом простом случае (Atmega 8, Timer0) активными будут три последних разряда CS02, CS01, CS00. Режим работы они задают следующим образом:
Если принять за данное, что длительность одного такта на 8MHz = 1,25*10^-7 секунды, то длительность одного такта в этом режиме будет порядка 1,2*10^-4 секунды, а полный цикл пересчета от загрузки нуля до переполнения при числе 255 и прерывания – 0,032 секунды. Мало? А ты помножь это дело счетчиком на регистрах. Timer_over: Когда таймер переполнится, сюда «прибежит» наша программа и установит флаг Т. Это флаг общего пользования – он ни на что не влияет, поэтому его можно юзать для своих целей. Пораскинем мозгами. Нам надо 1 секунду и у нас частота 8МГц. Длительность одного тика с максимальным делителем – 1.2*10^-4 секунды. Чтобы получить 0.01 секунду, надо сделать 83 тика. Поэтому в регистр TCNT0 надо загрузить число 255-83 = 172. Тогда до переполнения будет ровно 83 тика. Получился код: Second: DEC R17 ; Уменьшаем счетчик интервалов на 1 Со счетчиками работать одно удовольствие, но восьмиразрядный слабоват для больших задержек. Поэтому тут лучше заюзать Timer 1. Он шестнадцатиразрядный, а значит, не потребуется дополнительный пересчет. Хватит и одного таймера. Запомни это!Иногда нужно сохранить данные так, чтобы они восстановились после перезагрузки контроллера. В этом тебе поможет EEPROM (почти все контроллеры серии AVR имеют на борту некоторое количество этой памяти). Физически и логически эта память находится в отдельном адресном пространстве, а чтение из EEPROM и запись туда осуществляется через специальные порты. Чтобы записать что-либо в EEPROM, нужно в регистры адреса EEARH и EEARL (EEPROM Adress Register) положить адрес ячейки, в которую мы хотим записать байт. Затем нужно дождаться готовности памяти к записи – EEPROM довольно медленная штука. О готовности к записи нам доложит флаг EEWE (EEPROM Write Enable). Когда он будет равен 0, – память готова к следующей записи. Сам байт, который нужно записать, помещается в регистр EEDR (EEPROM Data Register). После чего взводится предохранительный бит EEMWE (EEPROM Master Write Enable). Теперь, в течение четырех тактов, нужно установить бит EEWE, и байт будет записан. Если не успеешь выставить бит EEWE, то предохранительный бит EEMWE сбросится и его придется выставлять снова. Это сделано для защиты от случайной записи в EEPROM-память. Чтение происходит примерно аналогичным образом. Вначале ждем готовности памяти, потом заносим в регистры нужный адрес, а затем выставляем бит чтения EERE (EEPROM Read Enable) и следующей командой забираем из регистра данных EEDR наше число, сохраняя его в любом регистре общего назначения. Для наглядности я тебе набросал две процедуры – на чтение и на запись. Чтобы записать байт, нужно в регистры R16 и R17 занести младший и старший байт адреса нужной ячейки, а в регистр R21 – байт, который мы хотим записать. И вызвать процедуру записи. Аналогично с чтением: в регистра R16 и R17 – адрес, а в регистре R21 должно быть считанное значение. … LDI R16,0 ; загружаем адрес нулевой ячейки EEWrite: CLI ; затем запрещаем прерывания SBI EECR,EEMWE ; взводим предохранитель EERead: Память EEPROM маленькая (какие-то считанные байты), а иногда нужно сохранить кучу данных, например, послание инопланетянам или таблицу синусов, чтобы не тратить время на ее расчет. Да мало ли что нужно заранее сныкать в памяти! Поэтому данные можно забивать в память программ, в те самые килобайты флеша, что имеет контроллер на борту. Записать-то запишем, а как достать? Сначала туда надо что-либо положить – для этого добавляй в конце программы метку, например, «data» и после нее, используя оператор «.db», вписывай свои данные. Оператор DB означает, что мы на каждую константу используем по байту. Есть еще операторы «.dw», задающие двухбайтные константы. data: .db 12,34,45,23 Теперь метка data указывает на адрес первого байта массива. Остальные байты находятся смещением (просто добавляй к адресу единичку). Кстати, есть одна тонкость: адрес изначально указан в двухбайтных словах. Из-за этого он меньше в два раза, что надо учитывать. Для загрузки данных из памяти программ используется команда LPM Rn,Z Она заносит в регистр Rn число из ячейки, на которую указывает регистровая пара Z. Напомню, что Z – это два регистра: R30 (ZL) и R31 (ZH). В R30 заносится младший байт адреса, а в R31 – старший. В коде выглядит это так: LDI ZL,low(data*2) ; заносим младший байт адреса, в регистровую пару Z LPM R16, Z ; в регистре R16 после этой команды будет число 12, Пошли всех подальшеОдной из самых любимых моих штуковин, запрятанных в дебрях контроллера, является UART. Он же – последовательный асинхронный порт, работающий по протоколу RS232 – тому же самому, что и известный тебе COM-порт. Чтобы послать байт, понадобится отправить командой OUT в регистр UDR (UART Data Register) число, а чтобы принять байт, соответственно, нужно оттуда число командой IN считать. Но предварительно передатчик надо сконфигурировать, так как UART умеет работать почти на всех мыслимых и немыслимых вариациях протокола RS232. Конфигурируется UART посредством трех контрольных регистров – UCSRA, UCSRB, UCSRC и регистровой пары UBRRH:UBRRL, в которую записывается делитель скорости передачи. В UCSRA нас, в первую очередь, интересуют биты RXC и TXC – флаги, сигнализирующие завершение приема и передачи. Отслеживая их, можно прикидывать, что делать дальше – забирать свежеполученный байт или готовить новую передачу. Бит UDRE (UDR Empty) сигнализирует о том, что прошлый байт передан, и в UDR можно загружать следующий байт. От ТXC его отличает то, что он выставляется при передачи каждого байта, а TXC только при окончании передачи вообще. Регистр UCSRВ интересен своими битами RXCIE, TXCIE и UDRIE, разрешающими прерывания по окончании приема, передачи и опустошении буфера приема. Это позволяет не тупо ждать завершения, а перевесить весь геморрой с приемо-передачей на обработчики прерываний. Также тут находятся биты RXEN и TXEN, которые разрешают прием и передачу (соответственно). Регистр UCSRC содержит информацию о формате передачи, количество стоп битов, сколько бит в посылке, какой контроль четности, синхронный или асинхронный режим передачи. Причем, тут есть один косяк, из-за которого я долго тупил в свое время – бит селектора URSEL. Дело в том, что по неизвестной причине создатели решили сэкономить байт адреса и разместили регистры UCSRC и UBRRH в одной адресном пространстве. Как же определить, куда записать? А по старшему биту! Поясню на примере. Если мы записываем число, у которого седьмой бит равен 1, то оно попадет в UCSRC, а если 0, то UBRRH. Смотри код: LDI R16,0x10000010b ; старший бит равен 1 Вот такая вот загогулина! А во всем виноват переключающий бит, так как, с точки зрения компилятора, метки UBRRH и UCSRC ведут в одно место. Если заглянуть в m8def.inc, то увидим там следующие строки: .equ UBRRH= 0x20 Ну что, теория кончилась, сейчас выдам тебе голую практику. А конкретно – инициализирующую процедуру, настраивающую UART на работу со стандартным виндовым терминалом. Восемь бит, один старт/стоп! uart_init: LDI R16, low(bauddivider) Теперь, чтобы послать число во внешний мир, нужно всего лишь дождаться готовности и забросить его в регистр UDR. Отправим наше послание человечеству через UART. У кого как, а у меня послание обычно начинается с буквы «F» :). OUT UDR, R16 ; засылаем нашу букву в порт и ловим ее на другом RETIВот так, камрад. Для начала хватит. Своего первого робота или другую полезную безделушку на этом сделать можно запросто. Если интересует больше, то вперед, в инет. На сайте http://easyelectronics.ru есть неплохой, на мой взгляд, учебный курс по AVR, где на примере постройки робота последовательно и подробно изучается использование контроллера ATMEGA8. |