Управление портами через регистры atmega. Программирование микроконтроллеров AVR на C. Регистр данных порта А – PORTA

Бит

Чтение/Запись

Исходное значение

· Бит 7 – Разрешение всех прерываний. Для разрешения прерываний этот бит должен быть установлен в состояние 1. Управление разрешением конкретного прерывания выполняется регистром маски прерывания EIMSK и TIMSK. Если этот бит очищен (=0), то ни одно из прерываний не обрабатывается. Бит аппаратно очищается после возникновения прерывания и устанавливается для последующего разрешения прерывания командой RETI.
· Бит 6 – Бит сохранения копии. Команды копирования бита BLD и BST используют этот бит как источник и приемник при операциях с битами. Командой BST бит регистра общего назначения копируется в бит T, командой BLD бит Т копируется в бит регистра общего назначения.
· Бит 5 – Флаг полупереноса. Он указывает на перенос между тетрадами при выполнении ряда арифметических операций.
· Бит 4 – Бит знака. Бит S имеет значение результата операции исключающее ИЛИ (N(+)V) над флагами отрицательного значения (N) и дополнения до двух флага переполнения (V).

· Бит 3 – Дополнение до двух флага переполнения. Он поддерживает арифметику дополнения до двух.
· Бит 2 – Флаг отрицательного значения. Этот флаг указывает на отрицательный результат ряда арифметических и логических операций.
· Бит 1 – Флаг нулевого значения. Этот флаг указывает на нулевой результат ряда арифметических и логических операций.
· Бит 0 – Флаг переноса. Этот флаг указывает на перенос при арифметических и логических операциях.

Микроконтроллер AT90S8535 имеет 4 параллельных порта ввода/вывода A, B,C и D.
Порт А является 8-разрядным двунаправленным портом. Взаимодействие с портом А осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTA, $1B($3B), регистр направления данных – DDRA, $1A($3A), регистр входных данных – PINA, $19($39). Регистр PINA обеспечивает только возможность чтения, а регистры PORTA и DDRA – возможность чтения и записи. Регистр PINA не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Порт А служит также для ввода аналоговых сигналов A/D.

Регистр данных порта А – PORTA

Бит

Чтение/Запись

Исходное значение

Регистр направления данных порта А – DDRA

Бит

Чтение/Запись

Исходное значение

Регистр входных данных порта А – PINA

Бит

Чтение/Запись

Исходное значение

Порт В является 8-разрядным двунаправленным портом ввода/вывода. Также как и у порта А взаимодействие с портом В осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTB, $18($38), регистр направления данных – DDRB, $17($37) и регистр входных данных – PINB, $16($36). Регистр PINB обеспечивает возможность только чтения. Регистр PINB не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Выводы порта В могут выполнять альтернативные функции, указанные в табл. 2.1.

Таблица 2.1. Альтернативные функции выводов порта В

Вывод порта

Альтернативная функция

T0 – вход тактового сигнала таймера/счетчика 0

T1 – вход тактового сигнала таймера/счетчика 1

AIN0 – положительный вывод компаратора

AIN1 – отрицательный вывод компаратора

– вход выбора ведомого SPI

MOSI – установка ведущий выход/ведомый вход SPI

MISO – установка ведущий вход/ведомый выход SPI

SCK – тактовый сигнал SPI

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

Регистр данных порта B PORTB

Бит

Чтение/Запись

Исходное значение

Регистр направления данных порта B – DDRB

Бит

Чтение/Запись

Исходное значение

Регистр входных данных порта B – PINB

Бит

Чтение/Запись

Исходное значение

Порт С представляет собой 8-разрядный двунаправленный порт ввода/вывода. Также как у портов А и В взаимодействие с портом С осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTC, $15($35), регистр направления данных – DDRC, $14($34) и регистр входных данных – PINC, $13($33). Регистр PINC обеспечивает только возможность чтения, а регистры PORTC и DDRC – возможность чтения и записи. Регистр PINC не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта.
У порта С только два вывода могут выполнять альтернативные функции: выводы PC6 и PC7 выполняют функции TOSC1 и TOSC2 таймера/счетчика 2.

Регистр данных порта C PORTC

Бит

Чтение/Запись

Исходное значение

Регистр направления данных порта C – DDRC

Бит

Чтение/Запись

Исходное значение

Регистр входных данных порта C – PINC

Бит

Чтение/Запись

Исходное значение

Порт D является 8-разрядным двунаправленным портом ввода/вывода. Также как и у портов А, В и С взаимодействие с портом D осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTD, $12($32), регистр направления данных – DDRD, $11($31) и регистр входных данных – PIND, $10($30). Регистр PIND обеспечивает возможность чтения, а регистры PORTD и DDRD – возможность чтения и записи. Регистр PIND не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта.
Выводы порта D могут выполнять альтернативные функции, указанные в табл. 2.2.

Таблица 2.2. Альтернативные функции выводов порта D

Вывод порта

Альтернативная функция

RxD – вход приемника UART

TxD – выход передатчика UART

INT0 – вход внешнего прерывания 0

INT1 – вход внешнего прерывания 1

OC1B – вывод сравнения выхода В таймера/счетчика 1

OC1А – вывод сравнения выхода А таймера/счетчика 1

ICP – вход триггера захвата таймера/счетчика 1

OC2 – вывод сравнения выхода таймера/счетчика 2

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

Регистр данных порта D PORTD

Бит

Чтение/Запись

Исходное значение

Регистр направления данных порта D DDRD

Бит

Чтение/Запись

Исходное значение

Регистр входных данных порта D PIND

Бит

Чтение/Запись

Исходное значение

Так как рассматриваемая работа первая, то для приобретения обучающимися навыков работы с лабораторным комплексом все обучающиеся сначала делают одинаковую работу. Со своих рабочих мест они вводят в ПЭВМ одну и ту же задачу вычитания из числа 5 числа 3, приведённую в п. 1.5.3.1. После компиляции программы она записывается в микроконтроллер рабочего места и демонстрируется её работа преподавателю.
После такого знакомства с комплексом обучающийся приступает к выполнению индивидуального задания. При наличии времени преподаватель может усложнить индивидуальное задание.

Побитовые операции основаны на логических операциях, которые мы уже рассмотрели ранее. Они играют ключевую роль при программировании микроконтроллеров AVR и других типов. Практически ни одна программа не обходится без применения побитовых операций. До этого мы намеренно избегали их, чтобы облегчить процесс изучения программирования МК.

Во всех предыдущих статьях мы программировали только порты ввода-вывода а и не задействовали дополнительные встроенные узлы, например, такие как таймеры, аналогово-цифровые преобразователи, прерывания и другие внутренние устройства без которых МК теряет всю свою мощь.

Прежде, чем перейти к освоению встроенных устройств МК, необходимо научится управлять или проверять отдельные биты регистров МК AVR. Ранее же мы выполняли проверку или устанавливали разряды сразу всего регистра. Давайте разберемся, в чем состоит отличие, а затем продолжим далее.

Побитовые операции

Чаще всего при программировании микроконтроллеров AVR мы пользовались , поскольку она имеет большую наглядность по сравнению с и хорошо понятна для начинающих программистов МК. Например, нам нужно установить только 3-й бит порта D. Для этого, как мы уже знаем, можно воспользуемся следующим двоичным кодом:

PORTD = 0b00001000;

Однако этой командой мы устанавливаем 3-й разряд в единицу, а все остальные (0, 1, 2, 4, 5, 6 и 7-й) мы сбрасываем в ноль. А теперь давайте представим ситуацию, что 6-й и 7-й разряды задействованы как входы АЦП и в это время на соответствующие выводы МК поступает сигнал от какого-либо устройства, а мы, применяемой выше командой, обнуляем эти сигналы. В результате чего микроконтроллер их не видит и считает, что сигналы не приходили. Поэтому вместо такой команды нам следует применить другую, которая бы установила только 3-й бит в единицу, при этом не влияя на остальные биты. Для это обычно применяется следующая побитовая операция:

PORTD |= (1<<3);

Синтаксис ее мы подробно разберем далее. А сейчас еще один пример. Допустим нам нужно проверить состояние 3-го разряда регистра PIND, тем самым проверяя состояние кнопки. Если данный разряд сброшен в ноль, то мы знаем, что кнопка нажата и далее выполняется код команды, который соответствует состоянию нажатой кнопки. Ранее мы бы воспользовались следующей записью:

if (PIND == 0b00000000)

{ какой-либо код}

Однако с помощью нее мы проверяем не отдельный, – 3-й, а сразу все биты регистра PIND. Поэтому даже если кнопка нажат и нужный разряд сброшен, но в это время на какой-либо другой вывод порта D поступит сигнал, то соответствующий быт установится в единицу, и условие в круглых скобках будет ложным. В результате код, находящийся в фигурных скобках, не будет выполняться даже при нажатой кнопке. Поэтому для проверки состояния отдельного 3-го бита регистра PIND следует применять побитовую операцию:

if (~PIND & (1<<3))

{ какой-либо код}

Для работы с отдельными битами микроконтроллера в арсенале языка программирования C имеются , с помощью которых можно изменять или проверять состояние одного или нескольких отдельных бит сразу.

Установка отдельного бита

Для установки отдельного бита, например порта D, применяется побитовая операция ИЛИ. Именно ее мы применяли в начале статьи.

PORTD = 0b00011100; // начальное значение

PORTD = PORTD | (1<<0); применяем побитовую ИЛИ

PORTD |= (1<<0); // сокращенная форма записи

PORTD == 0b00011101; // результат

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

Для примера установим еще 6-й разряд порта D.

PORTD = 0b00011100; // начальное состояние порта

PORTD |= (1<<6); //

PORTD == 0b01011100; // результат

Чтобы записать единицу сразу в несколько отдельных бит, например нулевой, шестой и седьмой порта B применяется следующая запись.

PORTB = 0b00011100; // начальное значение

PORTB |= (1<<0) | (1<<6) | (1<<7); //

PORTB == 0b1011101; // результат

Сброс (обнуление) отдельных битов

Для сброса отдельного бита применяются сразу три ранее рассмотренные команды: .

Давайте сбросим 3-й разряд регистра PORTC и оставим без изменений остальные.

PORTC = 0b00011100;

PORTC &= ~(1<<3);

PORTC == 0b00010100;

Выполним подобные действия для 2-го и 4-го разрядов:

PORTC = 0b00111110;

PORTC &= ~((1<<2) | (1<<4));

PORTC == 0b00101010;

Переключение бита

Кроме установки и сброса также применяется полезная команда, которая переключает отдельный бит на противоположное состояние: единицу в ноль и наоборот. Данная логическая операция находит широкое применение при построении различных световых эффектов, например, таких как новогодняя гирлянда. Рассмотрим на примере PORTA

PORTA = 0b00011111;

PORTA ^= (1<<2);

PORTA == 0b00011011;

Изменим состояние нулевого, второго и шестого битов:

PORTA = 0b00011111;

PORTA ^= (1<<0) | (1<<2) | (1<<6);

PORTA == 0b01011010;

Проверка состояния отдельного бита. Напомню, что проверка (в отличии от записи) порта ввода-вывода осуществляется с помощью чтения данных из регистра PIN.

Наиболее часто проверка выполняется одним из двух операторов цикла: if и while. С этими операторами мы уже знакомы ранее.

Проверка разряда на наличие логического нуля (сброса) с if

if (0==(PIND & (1<<3)))

Если третий разряд порта D сброшен, то выполняется Код1. В противном случае, выполняется Код2.

Аналогичные действия выполняются при и такой форме записи:

if (~PIND & (1<<3))

Проверка разряда на наличие логической единицы (установки) с if

if (0 != (PIND & (1<<3)))

if (PIND & (1<<3))

Приведенные выше два цикла работаю аналогично, но могут, благодаря гибкости языка программирования C, иметь разную форму записи. Операция!= обозначает не равно. Если третий разряд порта ввода-вывода PD установлен (единица), то выполняется Код1, если нет ‑ Код2.

Ожидание сброса бита с while

while (PIND & (1<<5))

Код1 будет выполняться пока 5-й разряд регистра PIND установлен. При сбросе его начнет выполняться Код2.

Ожидание установки бита с while

Здесь синтаксис языка С позволяет записать код двумя наиболее распространёнными способами. На практике применяются оба типа записи.

Долгое время мы оставляли без внимания микроконтроллеры AVR , и вот пришла пора исправить это недоразумение! Как и для других контроллеров, будем постепенно рассматривать различную периферию AVR’ок, сначала теорию, всякие регистры, ну и под конец небольшие примерчики.

В качестве IDE я использую AVR Studio 5 , шестую версию AVR Studio даже не пробовал, не так часто последнее время мне попадаются задачи для AVR) А вообще неплохо иметь еще и установленную AVR Studio 4, потому что порой случается так, что запрограммировать контроллер из AVR Studio 5 не представляется возможным. Вот совсем недавно я хотел прошить ATMega2560 при помощи программатора STK500 и это оказалось неосуществимо через 5 студию) Хорошо осталась со старых времен AVR Studio 4, и проблема решилась в течение пары минут.

Что тут еще можно сказать?.. Да в принципе, это все, можно переходить к делу;)

Начинать, естественно, будем с GPIO – портов ввода-вывода , потому как без них никуда) И прежде чем описывать регистры, которые управляют работой портов, отмечу несколько «электрических» моментов.

На входе каждой ножки микроконтроллера заботливые разработчики поставили пару диодов, которые должны спасти микроконтроллер в случае превышения допустимого напряжения. НО! На деле все не столь радужно, и если подать на вход микроконтроллера, например, 7.5 Вольт, то контроллеру уже никто и ничто не поможет, проверено на собственном опыте. Поэтому все эксперименты нужно проводить аккуратно)

Теперь к регистрам. Вся работа с портами ввода-вывода в AVR’ках сосредоточена в трех регистрах – DDRx, PORTx, PINx. Символ «x» заменяется соответствующим названием порта (A,B…). То есть, если мы хотим работать с портом A микроконтроллера, то нам нужны регистры DDRA, PORTA, PINA. Если мы хотим работать с пятым пином порта А (РА5), то нас интересует пятый бит упомянутых выше регистров. Как видите, все довольно просто) Осталось лишь разобраться, что же и куда надо записывать, за что отвечает каждый из этих регистров. Итак, начали…

Регистр DDRx.

DDRx отвечает за направление работы соответствующих выводов микроконтроллера. Каждый пин может быть либо входом, либо выходом, третьего не дано. Для того, чтобы настроить вывод на работу в режиме входа в регистр DDR для этого порта нужно записать 0, для выхода – 1. Пусть нам нужно настроить РА6 как вход, а РА3 как выход. Что делаем? Верно, третий бит регистра DDRA выставляем в 1, а в 6 бит все того же регистра DDRA записываем 0. Готово!

Регистр PINx.

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

Регистр PORTx.

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

PORTx = 1 – при такой конфигурации мы получаем вход с подтяжкой вверх (PullUp)

PORTX = 0 – высокоимпедансный вход (Hi-Z) – это значит, что сопротивление порта настолько велико, что его можно считать бесконечным)

Итак, продолжаем с регистром PORTx. Если вывод работает в качестве выхода, а в регистре PORTx единица – то на выводе будет высокий уровень сигнала, аналогично, PORTx = 0 – низкий уровень.

Давайте небольшой пример для иллюстрации 😉

Настроим вывод РС4 на работу в режиме входа с подтяжкой вверх. Для этого в четвертый бит регистра DDRC запишем 0 (режим входа), а в регистре PORTC четвертый бит надо выставить в 1 (подтяжка вверх). Вот и все.

В принципе это все, что касается теории, углубляться дальше не будем. Осталось соблюсти традиции и поиграть с диодиком. Пусть диод подключен к выводу PВ5. Заставим его помигать! И прежде всего создадим проект. Я, как уже говорил, использую AVR Studio 5, а в качестве контроллера выберу мой любимый ATMega88)

// Подключаем нужный файл #include /*******************************************************************/ // Простенькая функция для формирования задержки void delay(unsigned int time ) { unsigned int i = 0 ; for (i = 0 ; i < time ; i++ ) ; } /*******************************************************************/ // Инициализация нашего вывода, он работает на выход void initAll() { DDRB = 0b00100000 ; } /*******************************************************************/ // Собственно тело функции main() int main(void ) { initAll() ; while (1 ) { // Зажигаем диод PORTB = 0b00100000 ; // Отдыхаем delay(50000 ) ; // Гасим диод PORTB = 0b00000000 ; delay(50000 ) ; } } /*******************************************************************/

Вот таким получилось наше первое общение с микроконтроллерами AVR . В скором времени рассмотрим по возможности всю их периферию, а потом можно замутить что-нибудь поинтересней 😉

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

В других курсах там, чуть ли не с первых строк, делают что то существенное — диодиком мигают и говорят, что это наш Hello World. А тут? Гыде???

Да-да-да, я тебя понимаю. Более того, наверняка ты уже сбегал к конкурентам и помигал у них диодиком;)))) Ничего, простительно.

Я просто не хотел на этом же мигании дидодиков и остановиться, а для прогресса нужно четкое понимание основ и принципов — мощная теоретическая база. Но вот пришла очередь практики.

О портах было рассказано, шаблон программы у вас уже есть, так что сразу и начнем.

Инструментарий
Работа с портами, обычно, подразумевает работу с битами. Это поставить бит, сбросить бит, инвертировать бит. Да, конечно, в ассемблере есть удобные команды

cbi/sbi, но работают они исключительно в малом адресном диапазоне (от 0 до 1F, поэтому давайте сначала напишем универсальные макросы, чтобы в будущем применять их и не парить мозг насчет адресного пространства.

Макросы будут зваться:

  • SETB byte,bit,temp
  • CLRB byte,bit,temp
  • INVB byte,bit,temp,temp2

Причем при работе с битами младших РВВ (0-1F адрес) то значение параметра TEMP можно и не указывать — он все равно подставляться не будет. За исключением команд инверсии — там промежуточные регистры полюбому нужны будут.

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

  • SETBM byte,bit
  • CLRBM byte,bit
  • INVBM byte,bit

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

;= Start macro.inc ======================================== ;SET BIT with stack .MACRO SETBM .if @0 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

Со временем, когда пишешь на ассемблере, таких вот макросов становится очень и очень много. Они выносятся в отдельный файл и просто подключаются к любому твоему проекту, а написание кода становится легким и приятным.

Но вернемся к коду,
Мигнем уж светодиодиком то, наконец?

Да не вопрос. На демоплате уже смонтированы светодиоды, почему бы их не заюзать? Они висят на выводах порта PD4,PD5, PD7. Надо только одеть джамперы.

; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16 ; DDRD.5 = 1 SETB DDRD,7,R16 ; DDRD.7 = 1 ; End Internal Hardware Init ===================================

Осталось зажечь наши диоды. Зажигаются они записью битов в регистр PORT. Это уже делаем в главной секции программы.

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 SETB PORTD,7,R16 ; Зажгли LED3 JMP Main ; End Main =====================================================

Компилиуем, можешь в трассировщике прогнать, сразу увидишь как меняются биты. Прошиваем… и после нажатия на RESET и выгрузки bootloader (если конечно у тебя ) увидишь такую картину:


И для версии II


Во! Тока это же скучно. Давай ка ими помигаем.

Заменим всего лишь наши макрокоманды.

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 JMP Main ; End Main =====================================================

Зажгли, прошили…

А вот фиг — горят оба, но один чуть-чуть тусклей. На самом деле он мерцает, но очень очень быстро. Если ткнуть осциллографом в вывод PD7, то будет видно, что уровень меняется там с бешеной частотой:


Что делать? Очевидно замедлить. Как? Самый простой способ, который практикуют в подавляющем большинстве обучалок и быстрых стартов — тупой задержкой. Т.е. получают код вида:

; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main ; End Main ===================================================== ; Procedure ==================================================== .equ LowByte = 255 .equ MedByte = 255 .equ HighByte = 255 Delay: LDI R16,LowByte ; Грузим три байта LDI R17,MedByte ; Нашей выдержки LDI R18,HighByte loop: SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC Loop ; Если нет переноса - переход RET ; End Procedure ================================================

Прошили, запустили… О да, теперь мигание будет заметно.

При параметрах 255.255.255 длительность выдержки на 8Мгц будет около 2.1 секунды. Можно увеличить разрядность задержки еще на несколько байт. Тогда можно хоть час зарядить.

Но этот метод ущербен, сейчас покажу почему.

Давай добавим кнопку. LED3 пусть мигает в инверсии. А мы сделаем так, что когда кнопка нажата у нас горит LED1, а когда отпущена горит LED2.

В качестве кнопки возьмем тактовую A, подключим ее к порту PD6.


Для версии II аналогично. Только кнопку возьмем с группы кнопок и соединим вывод PD6 контроллера с штырем COL1 на кнопочной панели.

Обрати только внимание на джамперы, что стоят на перемычках кнопочного поля. Синие такие. А также на один неприметный черный джамперок, на который указывает правая стрелка. Он соединяет крайне левую колонку кнопок с землей. Набрасывается на пины GND и ROW1. На плате там все подписано.

Проверка кнопки делается командой SBIC, но вначале ее надо инициализировать. Сделать DDR=0, PORT=1 — вход с подтяжкой.

Добавляем в секцию инициализации (Internal Hardware Init) эти строчки:

; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

Ну чо, работает. Кнопочка жмется — диодики меняются. Третий же бодро подмигивает. Но есть западло:

Торомозит программка то! Я кнопочку нажал, а картинка не сменилась, нужно подождать, подержать… Почему? А это из-за нашего быдлокодинга.

Помнишь я тебе говорил, что тупые задержки, в которых МК ничего не делает это адское зло? Вот! Теперь ты в этом убедился сам. Чтож, со злом надо бороться. Как? Ну эт я тоже уже говорил — делать непрерывный цикл с флажками. Хотя бы внести в нашу задержку полезную работу.

Шарманка
Сейчас я тебе покажу как можно сделать цифровую шарманку. Помнишь как она устроена?

Там барабан с торчащими гвоздями и пружинки на разные тона. Гвозди вращаются, дергают пружинки — они звякают. Получается расколбасный музон. А что если нашу шарманку развернуть в ленту. Не правда ли гвозди похожи на единички? ;))))

Лентой будет счетчик, считающий от нуля, скажем, до FF.FF.FF.FF и потом опять до нуля или еще какой величины, сколько надо столько и сделаем. А наши подпрограммы будут играть роль пружинок, цепляясь в нужных местах — сравнивая свою константу с текущим значением счетчика.

Совпало? Делаем «ДРЫНЬ!»

Осталось только прописать на временном цикле нашей шарманки где и что должно запускаться. И тут есть одна очень удобная особенность — для построения циклических последовательностей нам достаточно отлавливать один разряд.

Скажем, считает шарманка от 0 до 1000, а нам надо 10 раз мигнуть диодом. Не обязательно втыкать 10 обработчиков с разными значениями. Достаточно одного, но чтобы он ловил значение **10. Все, остальное нам не важно. И сработает он на 0010, 0110, 0210, 0310, 0410, 0510, 0610, 0710, 0810, 0910. Более частые интервалы также делятся как нам надо, достаточно влезть в другой разряд. Тут надо только не забыть отрезать нафиг старшие разряды, чтобы не мешались.

Приступим. Вначале создадим наш счетчик в сегменте данных:

LDS R16,CCNT LDS R17,CCNT+1 LDS R18,CCNT+2 LDS R19,CCNT+3

Все, теперь в R16 самый младший байт нашего счетчика, а в R19 самый старший.

Регистры можно предварительно затолкать в стек, но я дам тебе лучше другой совет — когда пишешь программу, продумывай алгоритм так, чтобы использовать регистры как сплошной TEMP данные которого актуальны только здесь и сейчас. А что будет с ними в следующей процедуре уже не важно — все что нужно должно будет сохранено в оперативке.

По можно сделать так:

LDI R20,1 ; Нам нужна единичка CLR R15 ; А еще нолик. ADD R16,R20 ; Прибавляем 1 если в регистре 255, то будет С ADC R17,R15 ; Прибавляем 0+С ADC R18,R15 ; Прибавляем 0+С ADC R19,R15 ; Прибавляем 0+С

Пришлось потратить еще два регистра на хранение констант нашего сложения. Все от того, что AVR не умеет складывать регистры с непосредственным числом. Зато умеет вычитать.

Я уже показывал, что R-(-1)=R+1, а ведь никто не запрещает нам этот же прием устроить и тут — сделать сложение через вычитание.

1 2 3 4 SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

Даст нам инкремент четырехбайтного числа R19:R18:R17:R16

А теперь я вам покажу немного целочисленной магии
Почему это будет работать? Смотри сам:

SUBI R16,(-1) это, по факту R16 — 255 и почти при всех раскладах она нам даст нам заём из следующего разряда — С. А в регистре останется то число на которое больше.

Т.е. смотри как работает эта математика, вспомним про число в доп кодах. Покажу на четырехрязрядном десятичном примере. У Нас есть ВСЕГО ЧЕТЫРЕ РАЗРЯДА, ни больше ни меньше. Отрицательное число это 0-1 так? Окей.

1 С 0000-1=9999+C

Т.е. мы как бы взяли как бы из пятиразрядной 1 С 0000 отняли 1, но разрядов то у нас всего четыре! Получили доп код 9999 и флаг заема С (сигнализирующий о том, что был заем)

Т.е. в нашей целочисленной математике 9999=-1:) Проверить легко -1+1 = 0 Верно?

9999+1 = 1 С 0000 Верно! :))) А 1 старшего разряда банально не влезла в разрядность и ушла в флаг переноса C, сигнализирующего еще и о переполнении.

Оки, а теперь возьмем и сделаем R-(-1). Пусть R=4

1 С 0004-9999 = 0005+С

Вот так вот взяли и сложили через вычитание. Просто магия, да? ;)

Прикол ассемблера в том, что это всего лишь команды, а не доктрина и не правило. И команды которые подразумевают знаковые вычисления можно использовать где угодно, лишь бы они давали нужный нам результат!

Вот и тут — наш счетчик он же беззнаковый, но мы используем особенности знакового исчисления потому что нам так удобней.

Флаг С не выскочит лишь когда у нас дотикает до 255 (9999 в десятичном примере), тогда будет 255-255 = 0 и вскочит лишь Z, но нам он не нужен.

STS CCNT,R16 STS CCNT+1,R17 STS CCNT+2,R18 STS CCNT+3,R19

Код Инкремента четырехбайтной константы в памяти можно свернуть в макрос, чтобы не загромождать код

; Main ========================================================= Main: SETB PORTD,4 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 Next: INCM CCNT JMP Main

Запусти режим отладки и поставь точку останова (F9) на метку Main и загони курсор на первый брейкпоинт.

0хB6(CCNT) 0х9F(CCNT+1) 0х04(CCNT+2) 0x00(CCNT+3)

Осталось теперь только сравнить число с этим слепком.

Как сравнивать? Да довольно просто. Все зависит от того что мы хотим получить. Если ОДНО событие за ВЕСЬ период глобального счетчика нашей шарманки, то тупо, побайтно. В этом случае у тебя диодик моргнет через секунду после старта, а дальше ты будешь ждать пол часа до переполнения всего четырехбайтного счетчика.

Чтобы он моргал каждую секунду тебе надо при сравнении замаскировать старшие биты глобального счетчика, словно у него разрядность не 32 бита, а меньше (и переполняется он чаще).

Младшие байты сравниваем как есть, а самый старший только до его максимального разряда, остальные надо отрезать.

Т.е. самый старший разряд для этого случая это CCNT+2=0х04 если в двоичном представлении то 0х04 = 00000 100 так вот, счетчик у нас четырех разрядный, значит событие с маской

00 04 9F B6 (00000000 00000 100 10011111 10110110)

до переполнения возникнет дофига число раз. Видишь я нули жирным шрифтом выделил. Старший самый у мы вообще сравнивать не будем, а вот пред старший надо через AND по маске 00000111 продавить, чтобы отсечь старшие биты.

Их еще надо заполнить до переполнения и обнуления счетчика. Но если мы их замаскируем то их дальнейшая судьба нас не волнует. Пусть хоть до второго пришествия тикает — нам плевать.

LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 ; Накладываем маску CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: Next: INCM CCNT ; Проворачиваем шарманку JMP Main

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

Вот только мигает заметно медленней чем мы хотели. Не 1 секунда, а 8. Ну а что ты хотел — добавив процедуру сравнения мы удлиннили цикл еще на несколько команд. И теперь он выполняется не 25 тактов, а 36. Пересчитывай все циферки заново:)))))

Но это еще не самый цимес! Прикол в том, что у тебя часть кода выполняется, а часть нет — команды сравнения и перехода. Поэтому точно высчитать задержку по тактам это проще сразу удавиться — надо по каждой итерации высчитать когда и сколько у тебя будет переходов, сколько они тактов займут…

А если код будет еще больше, то ваще труба и погрешность накапливается с каждой итерацией!

Зато, если добавить код обработки кнопок:

; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: NOP INCM CCNT JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

То увидим, что от тормозов и следов не осталось. Кнопки моментально реагируют на нажатия, а диодик мигает сам по себе. Многозадачность! :)

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

Например, периодически сканировать клавиатуру, скажем, каждые 2048 оборотов главного цикла. Сам прикинь какое число надо нагрузить на сравнение и какую маску наложить:)

Можешь скачать и поглядеть , протрассировать его, чтобы увидеть как там все вертится.

А для точных вычислений времени существуют таймеры. Но о них разговор отдельный.

  • Tutorial

Работа портов ввода/вывода

Изучив данный материал, в котором все очень детально и подробно описано с большим количеством примеров, вы сможете легко овладеть и программировать порты ввода/вывода микроконтроллеров AVR.

Пример будем рассматривать на микроконтроллере ATMega8 .

Программу писать будем в Atmel Studio 6.0 .

Эмулировать схему будем в Proteus 7 Professional .

С внешним миром микроконтроллер общается через порты ввода вывода. Схема порта ввода вывода указана в даташите:

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

Каждый порт микроконтроллера AVR (обычно имеют имена A, B и иногда C или даже D) имеет 8 разрядов, каждый из которых привязан к определенной ножке корпуса. Каждый порт имеет три специальных регистра DDRx , PORTx и PINx (где x соответствует букве порта A, B, C или D). Назначение регистров:

DDRx – Настройка разрядов порта x на вход или выход.

PORTx – Управление состоянием выходов порта x (если соответствующий разряд настроен как выход), или подключением внутреннего pull-up резистора (если соответствующий разряд настроен как вход).

PINx –Чтение логических уровней разрядов порта x.

PINхn – это регистр чтения. Из него можно только читать. В регистре PINxn содержится информация о реальном текущем логическом уровне на выводах порта. Вне зависимости от настроек порта. Так что если хотим узнать что у нас на входе - читаем соответствующий бит регистра PINxn . Причем существует две границы: граница гарантированного нуля и граница гарантированной единицы - пороги за которыми мы можем однозначно четко определить текущий логический уровень. Для пятивольтового питания это 1.4 и 1.8 вольт соответственно. То есть при снижении напряжения от максимума до минимума бит в регистре PINx переключится с 1 на 0 только при снижении напряжение ниже 1.4 вольт, а вот когда напряжение нарастает от минимума до максимума переключение бита с 0 на 1 будет только по достижении напряжения в 1.8 вольта. То есть возникает гистерезис переключения с 0 на 1, что исключает хаотичные переключения под действием помех и наводок, а также исключает ошибочное считывание логического уровня между порогами переключения.

При снижении напряжения питания разумеется эти пороги также снижаются.

DDRxn – это регистр направления порта. Порт в конкретный момент времени может быть либо входом либо выходом (но для состояния битов PINxn это значения не имеет. Читать из PINxn реальное значение можно всегда).

DDRxy = 0 – вывод работает как ВХОД.

DDRxy = 1 – вывод работает на ВЫХОД.

PORTxn – режим управления состоянием вывода. Когда мы настраиваем вывод на вход, то от PORTх зависит тип входа (Hi-Z или PullUp, об этом чуть ниже).

Когда ножка настроена на выход, то значение соответствующего бита в регистре PORTx определяет состояние вывода. Если PORTxn =1 то на выводе лог.1, если PORTxn =0 то на выводе лог.0.

Когда ножка настроена на вход, то если PORTxn =0, то вывод в режиме Hi-Z. Если PORTxn =1 то вывод в режиме PullUpс подтяжкой резистором в 100к до питания.

Таблица. Конфигурация выводов портов.

DDRxn PORTxn I/O Comment
0 0 I (Input) Вход Высокоимпендансный вход. (Не рекомендую использовать, так как могут наводится наводки от питания)
0 1 I (Input) Вход Подтянуто внутренне сопротивление.
1 0 O (Output) Выход На выходе низкий уровень.
1 1 O (Output) Выход На выходе высокий уровень.

Общая картина работы порта показана на рисунках:

Рис. DDRxn =0 PORTxn =0 – Режим: HI-Z – высоко импендансный вход.

Рис. DDRxn =0 PORTxn =1 – Режим: PullUp – вход с подтяжкой до лог.1.

Рис. DDRxn =1 PORTxn =0 – Режим: Выход – на выходе лог.0. (почти GND)

Рис. DDRxn =1 PORTxn =1 – Режим: Выход – на выходе лог.1. (почти VCC)

Вход Hi-Z - режим высокоимпендансного входа.
Этот режим включен по умолчанию. Все ключи разомкнуты, а сопротивление порта очень велико. В принципе, по сравнению с другими режимами, можно его считать бесконечностью. То есть электрически вывод как бы вообще никуда не подключен и ни на что не влияет. Но! При этом он постоянно считывает свое состояние в регистр PINn и мы всегда можем узнать что у нас на входе - единица или ноль. Этот режим хорош для прослушивания какой либо шины данных, т.к. он не оказывает на шину никакого влияния. А что будет если вход висит в воздухе? А в этом случае напряжение будет на нем скакать в зависимости от внешних наводок, электромагнитных помех и вообще от фазы луны и погоды на Марсе (идеальный способ нарубить случайных чисел!). Очень часто на порту в этом случае нестабильный синус 50Гц - наводка от сети 220В, а в регистре PINn будет меняться 0 и 1 с частотой около 50Гц

Вход PullUp - вход с подтяжкой.
При DDRxn =0 и PORTxn =1 замыкается ключ подтяжки и к линии подключается резистор в 100кОм, что моментально приводит не подключенную никуда линию в состояние лог.1. Цель подтяжки очевидна - не допустить хаотичного изменения состояния на входе под действием наводок. Но если на входе появится логический ноль (замыкание линии на землю кнопкой или другим микроконтроллером/микросхемой), то слабый 100кОмный резистор не сможет удерживать напряжение на линии на уровне лог.1 и на входе будет лог.0.

Режим выхода.
Тут, думаю, все понятно - если нам надо выдать в порт лог.1, мы включаем порт на выход (DDRxn =1) и выдаем лог.1 (PORTxn =1) - при этом замыкается верхний ключ и на выводе появляется напряжение, близкое к питанию. А если надо лог.0, то включаем порт на выход (DDRxn =1) и выдаем лог.0 (PORTxn =1) - при этом открывается уже нижний вентиль, что дает на выводе около нуля вольт.