Пособие по AVR. Ввод-вывод: основы.

Пособие по AVR. Ввод-вывод: основы.

Предлагаю Вашему вниманию перевод следующей главы пособия по разработке на микроконтроллерах AVR. В этой главе рассмотренаработа с портами ввода/вывода с использованием светодиодов и кнопок.

Ввод-вывод: аппаратное обеспечение

Для первых экспериментов нам нужно подключить всего лишь пару кнопок и светодиодов к портам ввода/вывода (далее «IO-портам». — прим. переводчика) микроконтроллера AVR. Между выводами PB0-PB5 и Vcc через резисторы по 1 кОм подключаются 6 светодиодов. Как подключать резистор — перед светодиодом или после него — на практике значения не имеет, главное, чтобы он был подключен. Дополнительную информацию о светодиодах можно прочитать в этой статье (на немецком языке. — прим. переводчика).

Ввод-вывод: Подключение светодиода

Стандартная схема подключения светодиода

То, что светодиоды подключены к тем же выводам микроконтроллера, что и ISP-программатор, обычно никак не влияет. Если все же вопреки ожиданиям возникают какие-то проблемы, то попробуйте увеличить сопротивление R1.

К выводам PD0-PD3 подключаются 4 кнопки каждый с подтягивающим резистором на 10 кОм:

Ввод-вывод: Подключение кнопки

Стандартная схема подключения кнопки через подтягивающий резистор

Системы счисления

Прежде, чем мы перейдем к практике, пару слов о различных системах счисления.

Двоичные числа кодируются в ассемблере в формате 0b00111010, шестнадцатеричные — 0x7F. Переводить числа из одной системы в другую можно, например, с помощью калькулятора Windows. Приведем несколько примеров:

Десятичное Шестнадцатеричное Двоичное
0 0x00 0b00000000
1 0x01 0b00000001
2 0x02 0b00000010
3 0x03 0b00000011
4 0x04 0b00000100
5 0x05 0b00000101
6 0x06 0b00000110
7 0x07 0b00000111
8 0x08 0b00001000
9 0x09 0b00001001
10 0x0A 0b00001010
11 0x0B 0b00001011
12 0x0C 0b00001100
13 0x0D 0b00001101
14 0x0E 0b00001110
15 0x0F 0b00001111
100 0x64 0b01100100
255 0xFF 0b11111111

«0b» и «0x» никакой вычислительной нагрузки не несут, а показывают лишь, что речь идет соответственно о двоичных и шестнадцатеричных числах.

Важным является то, что как шестнадцатеричные, так и двоичные и десятичные числа отличаются лишь написанием и представляют одно и то же — числа. Какая форма записи больше подходит, зависит от конкретного случая.

Еще один важный пункт: компьютеры, как и микроконтроллеры, начинают считать с нуля, т.е. если у нас есть 8 объектов (например, 8 бит), то первый объект будет иметь нулевой порядковый номер, а порядковым номером восьмого объекта будет 7.

Вывод

Ассемблерный исходный код

Наша первая программа, которую мы запустим на микроконтроллере, выглядит следующим образом:

.include "m8def.inc"         ; Файл с определением переменных для микроконтроллера

         ldi r16, 0xFF       ; загрузка в регистр r16 константы 0xFF
         out DDRB, r16       ; содержимое регистра r16 записываем в IO-регистр DDRB

         ldi r16, 0b11111100 ; загружаем число 0b11111100 в регистр r16
         out PORTB, r16      ; выводим содержимое r16 IO-регистр PORTB

ende:    rjmp ende           ; переход к метке "ende" -> бесконечный цикл

Компиляция программы

Наш исходный код нужно сохранить с расширением «.asm», например, «leds.asm». Но напрямую загрузить этот файл в микроконтроллер у нас не получится: нужно сначала «скормить» его ассемблеру. В wavrasm нужно для этого открыть новое окошко, скопировать туда исходный код, сохранить и кликнуть на «assemble». При этом важно, чтобы файл «m8def.inc» (поставляется с ассемблером Atmel) находился в той же папке, что и исходный код. Ассемблер интерпретирует удобочитаемый ассемблерный код в бинарный код, понятный микроконтроллеру, и сохраняет его в т.н. hex-файл. Именно этот полученный файл можно загрузить непосредственно в микроконтроллер посредством соответствующих вспомогательных инструментов.

После компиляции должен появиться файл с именем «leds.hex» или «leds.rom», который можно загрузить во flash-память микроконтроллера с помощью yaap, PonyProg или AVRISP. Если все сделано верно, то первые два светодиода должны гореть.

Объяснение работы программы

В первой строке подключается файл m8def.inc, который определяет сопоставляет адреса различных регистров конкретного процессора с их символьными именами. Без этого файла ассемблер не узнает, что понимать под «PORTB», «DDRD» и т.д. Для каждого AVR микроконтроллера есть свой такой подключаемый файл, т.к. хоть обозначения регистров на всех контроллерах более-менее одинаковы, но сами регистры по-разному подключаются к выводам чипа; да и не все контроллеры имеют все типы функциональных регистров. Так, например, подключаемый файл для микроконтроллера ATmega8 называется m8def.inc. Обычно название файла так или иначе содержит обозначение чипа, здесь, например, в сокращенной форме. Название файла, соответствующего, Вашему микроконтроллеру можно посмотреть, например, в папке с установленными AVR Tools (например, C:\Programme\Atmel\AVR Tools\AvrAssembler\Appnotes\).

Ниже приведены некоторые типы микроконтроллеров AVR и соответствующие им подключаемые файлы:

 AT90s2313:  2313def.inc
 ATmega8:    m8def.inc
 ATmega16:   m16def.inc
 ATmega32:   m32def.inc
 ATTiny12:   tn12def.inc
 ATTiny2313: tn2313def.inc

Убедиться в правильности выбора подключаемого файла можно, если открыть include-файл в текстовом редакторе. Atmel помещает название процессора обычно в начало файла:

;***************************************************************************
;* A P P L I C A T I O N   N O T E   F O R   T H E   A V R   F A M I L Y
;* 
;* Number               :AVR000
;* File Name            :"2313def.inc"
;* Title                :Register/Bit Definitions for the AT90S2313
;* Date                 :99.01.28
;* Version              :1.30
;* Support E-Mail       :avr@atmel.com
;* Target MCU           :AT90S2313
...

Вернемся к нашей программе.

Во второй строке командой ldi r16, 0xFF значение 0xFF (соответствует 0b11111111) записывается в регистр r16 (подробнее в заметке об адресации данных). Микроконтроллеры AVR имеют 32 рабочих регистра, r0-r31, которые используются как промежуточное хранилище между регистрами ввода-вывода (например, DDRB, PORTB, UDR и т.д.) и оперативной памятью. Стоит отметить, что первые 16 регистров (r0-r15) могут использоваться не всеми ассемблерными командами. Регистр можно представить как ячейку памяти прямо в микроконтроллере. Конечно же, в микроконтроллере кроме регистров есть множество других ячеек памяти, но они используются исключительно для хранения данных. Однако для работы с этими данными их нужно сначала загрузить в один из регистров. Изменять и обрабатывать данные можно только в регистрах микроконтроллера. Образно говоря, регистр — это рабочий верстак, в то же время остальная память — это просто хранилище (этакий склад). Чтобы приступить к работе, нужно сначала взять материал со склада и перенести его на верстак. Команда ldi загружает определенное постоянное значение (константу) напрямую в регистр. В этом случае загружаемое значение берется не со «склада», а задается напрямую, программист его знает заранее. Названия ассеблерных команд — это не просто случайный набор букв, а обычно сокращение какого-нибудь действия или последовательности действий. Например, ldi расшифровывается как load immediate, т.е. «загрузить непосредственно». «Непосредственно» здесь указывает на то, что загружаемое значение передается прямо в команду напрямую.

Заметки после точки с запятой в коде — это всего лишь комментарии, которые не обрабатываются ассемблером.

Третья команда, out, выдает значение регистра r16 (0xFF) на регистр направления порта B. Регистр направления определяет, какие контакты (пины) порта используются как вход, а какие как выход. Если бит установлен в ноль, то соответствующий контакт будет работать на ввод данных, если же бит установлен в единицу, то соответствующий контакт будет работать на вывод данных. В нашем конкретном примере все 6 контактов порта В установлены как выходы. В регистры направления нельзя записывать напрямую, поэтому нужно использовать промежуточные регистры r16-r31.

Следующая команда, ldi r16, 0b11111100, загружает в регистр r16 значение 0b11111100, которе затем следующей командой out PORTB, r16 записывается в регистр ввода/вывода PORTB, т.е. это значение выдается на тот порт, к которому подключены светодиоды. Единица регистре PORTB выдает на соответствующий контакт порта напряжение 5В, ноль дает нулевое напряжение (массу).

Наконец, rjmp ende переводит программу на метку «ende:«, т.е. на то же самое место. За счет этого получается бесконечный цикл. Обычно метки пишут в самом начале строки, во втором (условном) столбце пишутся команды, а в третьем — комментарии. Метка — это просто какое-то символьное обозначение, на которое можно ссылаться в коде программы. Она указывает на адрес команды, которая стоит непосредственно сразу за меткой. Во время ассемблирования в понятный микроконтроллеру машинный код метки заменяются конкретными адресами памяти.

В командах копирования и загрузки (ldi, in, out и т.д.) второй операнд всегда копируется в первый:

         ldi r17, 15     ; в регистр r17 записывается константа 15
         mov r16, r17    ; в регистр r16 записывается содержимое r17
         out PORTB, r16  ; в IO- регистр "PORTB" записывается содержимое r16
         in r16, PIND    ; в регистр 16 записывается содержимое IO-регистра "PIND"

За дополнительной информацией по ассемблерным командам AVR можно обратиться к ассемблерной справке AVR-Studio или к документации Atmel (pdf, нужна читалка). Обратите внимание, что некоторые микроконтроллеры могут поддерживать не все команды.

Итак, теперь оба светодиода должны гореть, т.к. пины порта B PB0 и PB1 установлены в ноль, а другой выход светодиода подключен к Vcc (5В). Другие четыре светодиода не горят, т.к. соответствующие пины порта B установлены в единицу и на них подано напряжение в 5В.

Несмотря на то, что обнулены последние два бита, горят первые два светодиода. Это связано с тем, что биты перечисляются справа на лево. Справа стоит младший бит (LSB, least significant bit), который можно обозначить как нулевой бит. Крайний слева бит является старшим битом (MSB, most significant bit), в нашем случае седьмой бит. Префикс «0x» не относится к числу, а лишь указывает ассемблеру на то, что это двоичное число.

bits.gif

В этом случае LSB — это PB0, а MSB — это PB7. Но, например, в микроконтроллере AT90S4433 вывода PB7 нет вообще, старший вывод там PB5. Это связано с тем, что на корпусе этого микроконтроллера недостаточно выводов для полноценного порта B, поэтому старшие биты существуют только внутри микроконтроллера (т.е. могут быть использованы программно, но не аппаратно. — прим. переводчика).

Ввод

В программе ниже будем использовать порт B как выход, а порт D как вход:

.include "m8def.inc"

         ldi r16, 0xFF
         out DDRB, r16     ; Все контакты порта B устанавливаем на выход. Для
                           ; этого записываем 0xFF в регистр направления DDRB
         ldi r16, 0x00
         out DDRD, r16     ; Все контакты порта D устанавливаем на вход. Для
                           ; этого записываем 0x00 в регистр направления DDRD
loop:
         in r16, PIND      ; Логические значения на порте D считываем в r16
         out PORTB, r16    ; Содержимое r16 выдаем на порт B
         rjmp loop         ; Переходим на метку "loop:" -> Бесконечный цикл

Если порт D настроен на вход, то данные с него могут быть считаны с помощью регистр ввода/вывода PIND. Для этого используется команда in, которая копирует данные IO-регистра (в данном случае PIND) в один из рабочих регистров (например, r16). После этого с помощьб команды out данные из регистра r16 выводятся в порт B. Такой «крюк» через рабочий регистр необходим, т.к. копирование из одного IO-регистра в другой напрямую невозможно.

Команда rjmp loop отвечает за то, что команды in r16, PIND и out PORTB, r16 постоянно повторяются в бесконечном цикле. Поэтому каждая нажатая кнопка будет «зажигать» соответствующий светодиод.

Обратите внимание: хоть здесь отдельно и не указано, но каждый выход каждого порта можно отдельно настроить на вход или выход. В приведенном примере для простоты на вход или на выход устанавливается весь порт.

Возможные задержки по времени

Осторожно! В определенных ситуациях выводы могут быть неправильно прочитаны.

Особенно при опросе матричных клавиатур возможен эффект, что кнопки не реагируют. Обычно это заметно вот в таких случаях:

        ldi r16,0x0F      
        out DDRD,r16    ; верхний полубайт - вход, нижний - выход
        ldi r16,0xFE     
        out PORTD,r16   ; PD0 устанавливаем в 0, PD4..7 - pull up
        in  r17,PIND    ; Чтение пинов здесь может выдать неверные данные!

В чем же проблема? AVR — это RISC-микроконтроллер, выполняющий большинство команд за один такт. В то же время опрос внешних сигналов может приводить к задержке до полутора тактов. Подробную информацию можно найти в разделе «I/O Ports — Reading the Pin Value» даташита микроконтроллера.

Как решить эту проблему? Если вход одного порта непосредственно зависит от выхода другого порта, нужно предусмотреть минимум один такт между записью и чтением, в общем случае хватит и одной команды nop. nop означает no operation, или «нет операции», и делает именно это: ничего! Единственное предназначение этой команды — занять процессор ничем в течение одного такта, т.е. чтобы процессор что-то делал, но ничего не менял ни в каких регистрах.

        ldi r16,0x0F
        out DDRD,r16    ; верхний полубайт - вход, нижний - выход
        ldi r16,0xFE
        out PORTD,r16   ; PD0 устанавливаем в 0, PD4..7 - pull up
        NOP             ; Задержка для правильной синхронизации сигналов.
        in  r17,PIND    ; Теперь читаются правильные данные.

Подтягивающее сопротивление

При обсуждении подключения периферии к порту было предложено подключить кнопку с дополнительным сопротивлением на Vcc. Это сопротивление называется pullup-сопротивление или подтягивающее сопротивление.

Ввод-вывод: подключение кнопки через подтягивающий резистор

Схема подключения кнопки через подтягивающий резистор

При разомкнутой кнопке задачей подтягивающего резистора является «дотягивание» напряжения на ножке порта до уровня Vcc. Отсюда и название этого сопротивления. Без этого сопротивления ножка порта остается висеть в воздухе (при разомкнутой кнопке), т.е. не соединена ни с GND, ни с Vcc. Этого состояния следует избегать, т.к. всякие наводки могут привести к неправильному состоянию конкретного входа. Подтягивающий резистор обеспечивает, таким образом, логическую единицу при разомкнутой кнопке. Стоит ее замкнуть, как уровень упадет на 0. Через подтягивающий резистор в этом случае потечет ток. Поэтому следует выбирать высокоомные резисторы (например, 10кОм), чтобы ток между Vcc и GND был пренебрежительно малым.

Вместо внешнего резистора можно также автивировать встроенный pullup-резистор микроконтроллера. Тогда подключение кнопки сводится к простейшему случаю: кнопка будет подключена непосредственно между ножкой порта и GND.

Ввод-вывод: подключение кнопки без подтягивающего резистора

Подключение кнопки с использованием внутреннего подтягивающего сопротивления

Но это так, только если соответствующая ножка микроконтроллера настроена на вход. Подтягивающее сопротивление имеет смысл только для входа. В случае, когда ножка настроена на выход, микроконтроллер следит за тем, чтобы на этой ножке поддерживалось напряжение, соответствующее логическому уровню соответствующего бита. Подтягивающее сопротивление в этом случае попросту мешало бы, т.к. оно пытается «притянуть» напряжение на ножке к уровню Vcc, в то время как ноль на выходе привел бы к тому, что контроллер попытался бы установить на ножке напряжение на GND.

Подтягивающее сопротивление на входе управляется с помощью регистра PORT. Таким образом, регистр PORT имеет две функции. Если порт настроен как выход, то этот регистр задает логический уровень соответствующих бит. Если же порт настроен как вход, то PORT указывает на включенные/выключенные подтягивающие сопротивления. Логическая единица активирует соответствующее подтягивающее сопротивление.

DDRx PORTx Состояние IO-пина
0 0 Вход без pull-Up (изначально)
0 1 Вход с pull-Up
1 0 push-pull-выход по логическому LOW
1 1 push-pull-выход по логическому HIGH
.include "m8def.inc"

         ldi r16, 0xFF
         out DDRB, r16     ; Все ножки порта B устанавливаем как выходы. Для 
                           ; этого записываем в DDRB значение 0xFF
         ldi r16, 0x00
         out DDRD, r16     ; Все ножки порта D устанавливаем на вход. Для
                           ; этого записываем 0x00 в DDRD

         ldi r16, 0xFF     ; Активируем внутренние pullup-сопротивления на всех
         out PORTD, r16    ; ножках порта D. Т.к. порт D настроен на вход, то 
                           ; достаточно записать в PORTD значение 0xFF.
loop:
         in r16, PIND      ; Читаем значение порта D в r16.
         out PORTB, r16    ; Содержимое r16 записываем в порт B.
         rjmp loop         ; Переходим к метке "loop:" -> бесконечный цикл.

При активном внутреннем подтягивающем сопротивлении отпадает необходимость в дополнительных сопротивлениях, и схема подключения сокращается до единственной кнопки, подключенной между ножкой порта и GND.

Доступ к отдельным битам

Не всегда целесообразно считывать или записывать весь регистр. Порой удобнее пользоваться командами чтения/записи отдельных битов:

  • Команда sbic («skip if bit cleared») пропускает следующую команду, если соответствующий бит равен нулю.
  • sbis («skip if bit set») проделывает то же самое, но при установленном в единицу бите.
  • С помощью cbi («clear bit») обнуляется указанный бит.
  • А sbi («set bit») устанавливает указанный бит в единицу.

Внимание! Эти команды применины только для регистров ввода/вывода.

Большим плюсом команд cbi и sbi является то, что они действительно манипулируют лишь одним конкретным битом. Особенно полезно это бывает, когда на одном порте «висят» несколько светодиодов, которые отображают абсолютно разные состояния. Если нужно включить/выключить конкретный светодиод, достаточно указать номер бита. Если же использовать команду out, то значения обновляются во всех битах порта, что нужно обязательно учитывать (дабы не сбросить или не установить нежелательные значения).

Ниже приведу наглядный пример:

 
.include "m8def.inc"

         ldi r16, 0xFF
         out DDRB, r16       ; Port B ist Ausgang

         ldi r16, 0x00
         out DDRD, r16       ; Port D ist Eingang

         ldi r16, 0xFF
         out PORTB, r16      ; PORTB auf 0xFF setzen -> alle LEDs aus

loop:    sbic PIND, 0        ; "skip if bit cleared", nächsten Befehl überspringen,
                             ; wenn Bit 0 im IO-Register PIND =0 (Taste 0 gedrückt)
         rjmp loop           ; Sprung zu "loop:" -> Endlosschleife

         cbi PORTB, 3        ; Bit 3 im IO-Register PORTB auf 0 setzen -> 4. LED an

ende:    rjmp ende           ; Endlosschleife

Этот код ждет (в цикле «loop:«…»rjmp loop«), когда нулевой бит порта PIND обнулится, т.е. пока не будет нажата первая кнопка. Как только кнопка будет нажата, команда sbic заставить «перепрыгнуть» через rjmp loop, т.е. мы покинем цикл и пойдем дальше. В самом конце программа, можно сказать, застывает засчет бесконечного цикла. Если бы не было цикла, код начал бы выполняться с начала.

Обощение по регистрам портов

Каждому аппаратному порту соответствует по три регистра:

  • Регистр направления DDRx. Применяется для задания направления каждого бита порта. Единица настраивает соответствующую ножку на выход, а ноль – на вход.
  • Регистр чтения PINx. Используется для чтения текущего состояния на входе порта. Конечно же, порт должен быть настроен на вход.
  • Регистр вывода PORTx. В зависимости от значения регистра DDRx выполняет две функции.
    • Если порт (или конкретный выход порта) настроен на выход, то значения битов из регистра PORTx будет установлено на выходе соответствующих ножек порта.
    • Если порт (или конкретная ножка порта) настроен на вход, то регистр PORTx подключает (логическая 1) или отключает (логический 0) подтягивающее сопротивление на соответствующей ножке порта.