Порты микроконтроллера AVR. Программирование микроконтроллеров AVR на C Операции с битами регистра pinb в avr

Побитовые операции основаны на логических операциях, которые мы уже рассмотрели ранее. Они играют ключевую роль при программировании микроконтроллеров 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

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

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


У этого МК 3 порта. B C и D. К примеру порту B соответствуют выводы: PB0 PB1 ... PB7. Здесь у каждого порта кроме C (у него 7) по 8 выводов. (может быть меньше, к тому же у некоторых специальное предназначение, но об том потом). На то что в скобках пока что не обращаем внимания.

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

Для работы с портами есть 3 регистра PINx PORTx и DDRx.

где x буква обозначающая порт. DDRA для порта А

В регистре PINx хранится состояние порта, он считывает на какой ножке логическая 1 (+ 5 В) на какой лог 0, и сохраняет эти значения. Этот регистр доступен только для чтения.

В регистре DDRx мы определяем работает ли вывод как вход или как выход. Если выставим 1 в какой либо бит, то соответствующая ножка порта будет работать как выход. Если 0, то как вход.

В регистре PORTx мы определяем режим работы.

Высокоимпедансный вход - Это режим по умолчанию, МК в этом случае лучше всего защищён, вывод соединён с большим сопротивлением внутри микроконтроллера.

Вход c pull up резистором - вывод МК соединён через резистор 30 К с питанием VCC

# define F_CPU 8000000UL // Выбираем частоту МК #include // Для работы с портами #include // для задержки int main(void) { // 2 и 3 выводы порта B как выход DDRB = 0b00001100; // двоичный код DDRB = 0x0C; // 16 - чный код DDRB = 12; // 10 - чный код DDRB |= (1 << 2) | (1 << 3); // побитовое или // Это всё означает одно и то же // на 2 и 3 выводы подаем логическую 1 (+ 5В) PORTB |= (1 << 2) | (1 << 3); while(1) { // мигаем 2 и 3 выводом PORTB = PORTB ^ 0b00001100; // инвертирующее или PORTB ^= 0b00001100; // тоже самое короче _delay_ms(200); } }

Результат

// работа с кнопкой DDRB = 0x00; // вход PORTB |= (1 << 2) | (1 << 3); // резистор до VCC // на выводах 2 и 3 Порта B + 5В while(1) { // кнопки нужно соединить 1 выводом к ножке МК // 2 выводом к земле if (PINB == 0b00000110) // кнопки не нажаты? if (PINB == 0b00000100) // нажата кнопка 2 вывода? if (PINB & 0b00000100) // побитовое и, НЕ нажата 3 кнопка? // инвертируем регистр PINB если было 0b00000110 стало 0b11111001 if (!PINB & 0b00000100) // нажата 3 кнопка? if (!PINB & (1 << 3)) // то же самое } Про бегущий огонь
// Еще 1 полезная вещь называется битовый сдвиг // записали 1 в 1 бит PORTB = 0b00000001; PORTB = PORTB << 1; // PORTB == 0b00000010; // сдвинули 1 влево на 1 шаг можно на 2, 3 и т.д PORTB = PORTB >> 1; // PORTB == 0b00000001; // сдвинули 1 вправо на 1 шаг // но после 0b10000000 если единичка сдвинется влево будет 0x00 // нужно самому вручную выставлять заново
Теперь про выводы МК на примере 32 АтМеги (т.к у неё много ножек и тот же DIP корпус)

VCC - питание (зависит от МК обычно от 2.7 В до 5.5 В)

GND - земля

RESET - По умолчанию тут 1, если заземлить на 2 мкс МК перезагрузиться.

XTAL1 - вход внешнего генератора тактовой частоты.

XTAL2 - его выход. (между этими ножками ставиться кварц с конденсаторами, ему не важно какой стороной его ставить)

AVCC - сюда подается напряжение для работы АЦП

AREF - эталон напряжения для АЦП (с каким значением сравнивать АЦП)

ADC(0...7) - Ножки на которые подается напряжение которое нужно преобразовать с помощью АЦП, 8 каналов,

(SS, MOSI, MISO, SCK) - используются для передачи данных по SPI, через них также программируется МК.

(T1, T0) - сюда подключается внешний источник тактов и таймеры T1 и T0 подсчитывают внешние такты.

(AIN0, AIN1) - соответсвенно положительный и отрицательный выводы аналогового компаратора.

INT2 - вывод для проверки прерываний, к примеру если настроить и подать 1 на этот вывод в МК произойдет прерывание.

OC0 - Вывод ШИМ

XCK - используется при передаче данных с USART

Порт C

TOSC2, TOSC1 - если настроен сюда подключается тактовый генератор, от которого работает МК
TDI, TDO, TMS, TCK - используется для отладки JTAG
SDA - вывод для работы последовательного интерфейса, ДАТА
SCL - вывод для работы последовательного интерфейса, подав сюда импульс передаём 1 бит информации по выводу SDA.

При программировании микроконтроллеров постоянно приходится работать с битами. Устанавливать их, сбрасывать, проверять их наличие в том или ином регистре. В AVR ассемблере для этих целей существует целый ряд команд. Во-первых, это группа команд операций с битами – они предназначены для установки или сброса битов в различных регистрах микроконтроллера, а во-вторых, группа команд передачи управления – они предназначены для организации ветвлений программ. В языке Си естественно нет подобных команд, поэтому у начинающих программистов часто возникает вопрос, а как в Си работать с битами. Эту тему мы сейчас и будем разбирать.

В Си существуют 6 операторов для манипулирования битами. Их можно применять к любым целочисленным знаковым или беззнаковым типам переменных.

<< - сдвиг влево
>> - сдвиг вправо
~ - поразрядная инверсия
| - поразрядное ИЛИ
& - поразрядное И
^ - поразрядное исключающее ИЛИ

_______________ сдвиг влево << _______________

Сдвигает число на n разрядов влево. Старшие n разрядов при этом исчезают, а младшие n разрядов заполняются нулями.


unsigned char tmp = 3; //0b00000011
tmp = tmp << 1;
//теперь в переменной tmp число 6 или 0b00000110

Tmp = tmp << 3;
//теперь в переменной tmp число 48 или 0b00110000

Выражения, в которых над переменной производится какая-либо операция, а потом результат операции присваивается этой же переменной, можно записывать короче, используя составные операторы.

Tmp = 7; //0b00000111
tmp <<= 2; //сокращенный вариант записи
//теперь в переменной tmp число 28 или 0b00011100

Операция сдвига влево на n разрядов эквивалентна умножению переменной на 2 n .

_______________ сдвиг вправо >> _______________

Сдвигает число на n разрядов вправо. Младшие n разрядов при этом теряются. Заполнение старших n разрядов зависит от типа переменной и ее значения. Старшие n разрядов заполняются нулями в двух случаях – если переменная беззнакового типа или если переменная знаковая и ее текущее значение положительное. Когда переменная знаковая и ее значение отрицательное – старшие разряды заполняются единицами.

Пример для беззнаковой переменной

unsigned char tmp = 255; //0b11111111
tmp = tmp >> 1;
//теперь в переменной tmp число 127 или 0b01111111

Tmp >>= 3; //сокращенный вариант записи
//теперь в переменной tmp число 15 или 0b00001111

Пример для переменной знакового типа

int tmp = 3400; //0b0000110101001000
tmp >>= 2;
//теперь в переменной число 850 или 0b0000001101010010

Tmp = -1200; //0b1111101101010000
tmp >>= 2;
//теперь в tmp число -300 или 0b1111111011010100
//видите - два старших разряда заполнились единицами

Операция сдвига вправо на n разрядов эквивалентна делению на 2 n . При этом есть некоторые нюансы. Если потерянные младшие разряды содержали единицы, то результат подобного “деления” получается грубоватым.

Например 9/4 = 2,5 а 9>>2 (1001>>2) равно 2
11/4 = 2,75 а 11>>2 (1011>>2) равно 2
28/4 = 7 а 28>>2 (11100>>2) равно 7

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

_______________поразрядная инверсия ~ _______________

Поразрядно инвертирует число. Разряды, в которых были нули – заполняются единицами. Разряды, в которых были единицы – заполняются нулями. Оператор поразрядной инверсии являтся унарным оператором, то есть используется с одним операндом.

unsigned char tmp = 94; //0b01011110
tmp = ~tmp;
//теперь в переменной tmp число 161 или 0b10100001

Tmp = ~tmp;
//теперь в tmp снова число 94 или 0b01011110

_______________ поразрядное ИЛИ | ______________

Оператор | осуществляет операцию логического ИЛИ между соответствующими битами двух операндов. Результатом операции логического ИЛИ между двумя битами будет 0 только в случае, если оба бита равны 0. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности.

Оператор | обычно используют для установки заданных битов переменной в единицу.

Tmp = 155
tmp = tmp | 4; //устанавливаем в единицу второй бит переменной tmp

155 0b100110 11
4 0b000001 00
159 0b100111 11

Использовать десятичные числа для установки битов довольно неудобно. Гораздо удобнее это делать с помощью операции сдвига влево <<.


tmp = tmp | (1<<4); //устанавливаем в единицу четвертый бит переменной tmp

Читаем справа налево – сдвинуть единицу на четыре разряда влево, выполнить операцию ИЛИ между полученным числом и значением переменной tmp, результат присвоить переменной tmp.


Установить несколько битов в единицу можно так

Tmp = tmp | (1<<7)|(1<<5)|(1<<0);
//устанавливаем в единицу седьмой, пятый и нулевой биты переменной tmp

С помощью составного оператора присваивания |= можно сделать запись компактней.

Tmp |= (1<<4);
tmp |= (1<<7)|(1<<5)|(1<<0);

_______________ побитовое И & _______________

Оператор & осуществляет операцию логического И между соответствующими битами двух операндов. Результатом операции логического И между двумя битами будет 1 только в том случае, если оба бита равны 1. Во всех других случаях результат будет 0. Это проиллюстрировано в таблице истинности.

Оператор & обычно применяют, чтобы обнулить один или несколько битов.

Tmp = 155;
tmp = tmp & 247; //обнуляем третий бит переменной tmp

155 0b10011 011
&
247 0b11110 111
147 0b10010 011

Видите, третий бит стал равен 0, а остальные биты не изменились.

Обнулять биты, используя десятичные цифры, неудобно. Но можно облегчить себе жизнь, воспользовавшись операторами << и ~

Tmp = 155;
tmp = tmp & (~(1<<3)); //обнуляем третий бит

1<<3 0b00001 000
~(1<<3) 0b11110 111
tmp & (~(1<<3)) 0b10011 011 & 0b11110 111
результат 0b10010 011

Читаем справа налево – сдвинуть единицу на три разряда влево, выполнить инверсию полученного числа, выполнить операцию & между значением переменной tmp и проинвертированным числом, результат присвоить переменной tmp.


Обнулить несколько битов можно так

tmp = tmp & (~((1<<3)|(1<<5)|(1<<6))); //обнуляем третий, пятый и шестой биты



Используя составной оператор присваивания &= ,можно записать выражение более компактно

Tmp &= (~((1<<3)|(1<<5)|(1<<6)));

Как проверить установлен ли бит в переменной?
Нужно обнулить все биты, кроме проверочного, а потом сравнить полученное значение с нулем

if ((tmp & (1<<2)) != 0){
// блок будет выполняться, только если установлен

}

if ((tmp & (1<<2)) == 0){
// блок будет выполняться, только если не установлен
// второй бит переменной tmp

}

_______________побитовое исключающее ИЛИ ^ _______________


Оператор ^ осуществляет операцию логического исключающего ИЛИ между соответствующими битами двух операндов. Результатом операции логического исключающего ИЛИ будет 0 в случае равенства битов. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности.

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


tmp = 155;
tmp = tmp ^ 8; // инвертируем четвертый бит переменой tmp

155 0b10011 011
^
8 0b00001 000
147 0b10010 011

Четвертый бит изменил свое значение на противоположное, а остальные биты остались без изменений.

Tmp = tmp ^ 8; // опять инвертируем четвертый бит переменой tmp

147 0b10010 011
^
8 0b0000 1 000
155 0b10011 011

Видите, четвертый бит снова изменил свое значение на противоположное.

Так записывать выражение намного удобнее

Tmp = tmp ^ (1<<3); // инвертируем третий бит переменой tmp

А так и удобно и компактно

Tmp ^= (1<<4); //инвертируем четверый бит

Можно инвертировать несколько битов одновременно

Tmp ^= ((1<<4)|(1<<2)|(1<<1)); //инвертируем 4,2 и 1 биты

У поразрядного исключающего ИЛИ есть еще одно интересное свойство . Его можно использовать, для того чтобы поменять значения двух переменных местами. Обычно для этого требуется третья переменная.


tmp = var1;
var1 = var2;
var2 = tmp;

Но используя оператор ^ переставить значения можно так:

var1 ^= var 2;
var 2 ^= var 1;
var 1 ^= var 2;

Чистая магия, хотя, честно говоря, я ни разу не пользовался таким приемом.

________________Директива #define__________________


Теперь мы знаем, как устанавливать, обнулять и инвертировать биты, знаем, как проверять установлен ли бит или нет. Рассмотренные выше выражения довольно громоздки, но с помощью директивы препроцессора #define , им можно придать более приятный вид.

Директива #define используется для присваивания символических имен константам и для макроопределений. Использование символических имен делают программу более модифицируемой и переносимой.

Например, вы используете в тексте программы константу, и вдруг вам понадобилось изменить ее значение. Если она встречается всего в трех местах, то исправить ее можно и в ручную, а что делать, если она встречается в пятидесяти строчках? Мало того, что исправление займет много времени, так еще и ошибиться в этом случае проще простого. Здесь то, как раз и выручает директива #define . В начале программы задается символическое имя константы, которое используется по ходу программы. Если нам нужно изменить это значение, это делается всего лишь в одном месте. А перед компиляцией препроцессор сам подставит во все выражения вместо имени константы ее значение.

Программирование микроконтроллера неразрывно связано с его аппаратной частью и чаще всего с внешней обвязкой. Взять хотя бы кнопки - опрашивая их в своей программе, мы обращаемся к реальным выводам микроконтроллера. А если нам вдруг понадобилось использовать программу опроса кнопок в другой схеме, где кнопки подключены к другим выводам? Придется исправлять программу. Опять таки, задав с помощью директивы #define символическое имя для соответствующих выводов, модифицировать программу будет проще простого


Пример:

#include "iom8535.h"

//порт, к которому подключены кнопки
#define PORT_BUTTON PORTA
#define PIN_BUTTON PINA
#define DDRX_BUTTON DDRA

//выводы, к которым подключены кнопки
#define DOWN 3
#define CANCEL 4
#define UP 5
#define ENTER 6

int main()
{
//конфигурируем порт на вход,
//и включаем подтягивающие резисторы

DDRX_BUTTON = 0;
PORT_BUTTON = 0xff;

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

#define MASK_BUTTONS ((1<

пример использования:
tmp = PORTB & MASK_BUTTONS;

Используя #define не жалейте скобок чтобы четко задать последовательность вычисления выражений!

Некоторые выражения можно замаскировать под «функции».

#define ADC_OFF() ADCSRA = 0

пример использования:
ADC_OFF();

Можно использовать многострочные определения, используя в конце каждой строки символ \

#define INIT_Timer() TIMSK = (1< TCCR0 = (1< TCNT0 = 0;\
OCR0 = 0x7d

пример использования:
INIT_Timer();

Ну и самое мощное применение директивы #define – это задание макроопределений (или просто макросов). Вот как с помощью #define можно задать макросы для рассмотренных ранее операций с битами

#define SetBit(reg, bit) reg |= (1<#define ClearBit(reg, bit) reg &= (~(1<#define InvBit(reg, bit) reg ^= (1<#define BitIsSet(reg, bit) ((reg & (1<#define BitIsClear(reg, bit) ((reg & (1<

пример использования:

SetBit(PORTB, 0); //установить нулевой бит порта B
InvBit(tmp,6); //инвертировать шестой бит переменной tmp


if (BitIsClear(PIND, 0)) { //если очищен нулевой бит в регистре PIND
….. //выполнить блок
}

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

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

Определим макрос, вычисляющий квадрат числа:

#define SQUARE(x) x*x

выражение
tmp = SQUARE(my_var);
даст корректный результат.

А что будет если в качестве аргумента макроопределения использовать выражение my_var+1

tmp = SQUARE(my_var +1);

Препроцессор заменит эту строчку на

tmp = my_var + 1 * my_var +1;

а это вовсе не тот результат, который мы ожидаем.

Чтобы избежать таких ошибок не скупитесь на скобки при объявлении макросов!

Если объявить макрос так

#define SQUARE(x) ((x)*(x))

выражение
tmp = SQUARE(my_var +1);
даст корректный результат, потому что препроцессор заменит эту строчку на
tmp = ((my_var + 1) * (my_var +1));

записываем их в папку проекта, а в начале файла main.c прописываем #include "bits_macros.h"

2.1. Порты и выводы.

Чтобы общаться с внешним миром у микроконтроллера есть порты ввода-вывода, в каждом из которых есть несколько отдельных битов (считай выводов), на которых можно установить ноль (0) или единицу (1).

У таких портов 3 это порты B,C и D. На каждом порту по 8 битов (за исключением порта C он 7 - разрядный ) которыми можно (нужно) управлять. Но управлять с некоторыми ограничениями.

Ограничения:

    D0 и D1 используются для прошивки микроконтроллерах на плате Arduino через USB;

    C6 – используется для перезагрузки (reset) ;

    B6 и B7 - на этих выводах микроконтроллера подключается внешний кварцевый резонатор .

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

    порт B – B0, B1, B2, B3, B4, B5 (соответственно выводы микроконтроллера с 1 4 по 1 9 );

    порт C – С0, С1, С2, С3, С4, С5 (выводы - с 23 по 28);

    порт D – D2, D3, D4, D5, D6, D7 (выводы - 4, 5, 6, 11, 12, 13).

Необходимо учитывать что производится в разных корпусах и нумерация выводов может отличатся

2.2. Регистры управления портами.

Управление портами достаточно простое. Используется три восьми битных регистра -

DDRx, PORTx и PINx, где x имя порта (в нашем случае B,C и D ).

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

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

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

Настройка и работа портов сводится к трем операциям в зависимости от настройки входа или выхода.

Ввод:

    В регистре DDRx на нужный разряд устанавливаем 0 обозначая его как ввод;

    При необходимости на указанном разряде устанавливаем 1 для подключения подтягивающего резистора (резистор подтягивает указанный вывод к 1), подтягивающий резистор включают для уменьшения внешних помех и его можно не подключать;

    Считываем из регистра PINx с того-же разряда состояние 0 или 1.

Вывод:

    В регистре DDRx на нужный разряд устанавливаем 1 обозначая его как вывод;

    В регистр PORTx на этот разряд устанавливаем его состояние 0 или 1;

Пример : На выводе 5 порта B установить 1 ( вывод 17 микроконтроллера переключить на логическую 1 )

регистр DDRB

Установили разряд DDRB 5 в 1 настроив вывод как вывод

регистр PORT B

PORT B 7

PORT B 6

PORT B 5

PORT B 4

PORT B 3

PORT B 2

PORT B 1

PORT B 0

Установили разряд PORT B 5 переключив вывод микроконтроллера в 1. Дальнейшее переключение этого вывода производится без изменения регистра DDRx если не понадобится переключить разряд порта на ввод.

Регистр PIN B можно не использовать, если только для проверки состояния выводов порта.

2.3. Программа

Разберем программу на C по строкам.

#include // Подключение библиотеки // ввода/вывода AVR #include // Подключение библиотеки создания задержек #define V_V 5 // Указываем макроопределение регистра 5 порта B int main() { DDRB |= 1 << V_V; // Устанавливаем 5 бит регистра DDRB в // (назначаем как вывод) while(1) { // Безконечный цикл основной программы PORTB |= 1 << V_V; // Устанавливаем вывод микроконтроллера в 1 _delay_ms(100); // Ждем 100 мс PORTB &= ~(1 << V_V); // Устанавливаем вывод микроконтроллера в 0 _delay_ms(100); // Ждем 100 мс } return 0; }

В программе вставлен бесконечный цикл while(1), чтобы микроконтроллер не выполнил ничего лишнего. Все действия с портами в программе выполнены с использованием поразрядных операций в языке C. Что дало возможность управлять только одним разрядом (одним выводом микроконтроллера) порта B.

На использованном нами выводе микроконтроллера в Arduino UNO и Arduino Nano v3 подключен светодиод, поэтому в первой программе не придется даже собирать схему, достаточно подключить Arduino к компьютеру.

2.4. Проект на C и компиляция

Программное обеспечение готово, программа написана, микроконтроллер тоже есть в виде Arduino. Начнем.

Запускаем CodeBlocks, в меню File >> New >> Project начинаем создавать проект.

Выбираем AVR Project и Go.

В поле Project title указываем название проекта, ниже в Folder to create project in указываем путь к папке куда создаем проект и жмем Next.

В следующем окне оставляем галку только Create “Release” configuration и опять Next.


Выбираем наш микроконтроллер (у меня ) устанавливаем частоту (для Arduino Nano v3 - 16МГц ) и оставляем создание только hex файла и Finish.

И наконец в созданном проекте находим файл main.c и открываем его. Внутри видим:

/* */ #include int main(void) { // Insert code while(1); return 0; }

Заменяем эту заготовку нашей программой и жмем

Происходит компиляция проекта и внизу видим

2.5. Прошиваем микроконтроллер

Все прошивка готова, она находится в папке проекта (выбранной при создании проекта). У меня C:\avr\Program1\bin\Release\Program1.hex этот файл и является нашей прошивкой.

Начнем прошивать. Запустим программу ArduinoBuilder

В окне выбираем файл hex (находится в папке проекта CodeBlocks >> bin/Release/project1.hex) нашего проекта, выбираем Arduino и частоту микроконтроллера и жмем кнопку чем программировать (у меня COM9 ) обычно это com порт отличный от 1. После сего проделанного смотрим мигающий диод.

На этом задача минимум выполнена. Рассмотрен подборка программного обеспечения, изучены порты ввода/вывода и регистры их управления, написана программа на C скомпилирована и прошита в микроконтроллер. И все это можно применить для микроконтроллеров AVR за исключением программы ArduinoBuilder которая в основном создана под Arduino, но и ее можно заменить при использовании например программатора

Порты ввода и вывода микроконтроллера AVR, необходимы для обмена данными с различными подключенными к нему устройствами, например, реле, световыми и звуковыми индикаторами, датчиками и т.п. С помощью AVR портов, осуществляется не только обмен данными, но и синхронизация схемы в целом. Количество AVR портов зависит от модели МК. В среднем имеется (1-7) портов. Обычно, порты AVR восьмиразрядные, если разрядность не ограничена количеством выводов на корпусе МК.


На, практике в принципиальных схемах порты AVR обозначаются латинскими символами, например, PORT A, PORT B - PORTG. Каждый вывод – обладает своим порядковым номером, причем, нумерация начинается с цифры 0. Если МК использует 8 разрядов, то нумерация выглядит так – PB0… PB7.


Выводы портов способны выполнять также и альтернативные задачи. Если, допустим, сигнал модуля USART совпадает с выводом BP5, то выводы BP4 и BP3, начинают работать в режимах (SCK, MISO и MOSI) и не могут быть задействованы как элементы порта ввода/вывода. Если модуль отсоединить, то эти выводы продолжают работать как элементы порта.

Управлять любым портом МК X можно тремя регистрами: DDRx; PORTx; PINx.

Например, порт PB4 где буква «B» - имя порта, а цифра - номер бита. За порт «B» в конкретном примере отвечают три восьмиразрядных регистра PORT B, PIN B, DDR B, а каждый бит в этом регистре отвечает за свою ножку порта. Т.е за порт «А» аналогичным образом отвечают PORTA, DDRA, PINA.

Регистр DDR x стандартный 8 битный порт , осуществляющий передачу данных каждой линии порта X. 0 – вход, 1 – выход. Каждый из восьми бит, отвечает только за свою линию порта Px (0-7). Т.к выводы нумеруются с (0) – первый бит отвечает за BP0, второй соответственно за BP1 и т.п. Если вам необходимо, чтобы конкретный вывод начал работать на вход – значения регистра устанавливаем равным 0, на выход = 1. При включении, параметры всех выводов, сбросятся в ноль.

PINх регистр чтения . Из него возможно произвести операцию чтение. В этом регистре PINx имеется информация о текущем логическом уровне на выводах, причем вне зависимости от настроек порта. Так что если возникает необходимость узнать, что у нас имеется на входе - читаем нужный бит регистра PINx.

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

При снижении напряжения эти пороги также становятся ниже, график зависимости переключения от питающего напряжения можно найти в даташите на каждый микроконтроллер.

PORTx это регистр управления состоянием вывода. Если мы производим настройку вывода на вход, то от регистра PORT зависит тип входа (Hi-Z или PullUp). Если ножка настроена на выход, то значение бита в регистре PORTx зависит от состояния вывода. Например, PORTxy=1 то на выводе логическая "1", а при PORTxy=0 на нем логический ноль. Если ножка настроена на вход PORTxy=0, то вывод в режиме Hi-Z. Если PORTxy=1 то вывод в режиме PullUp с настройкой сопротивлением в 100к до питания.

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

Теперь о режимах работы портов:

Режим выхода если нам требуется выдать в порт логическую единицу мы включаем порт на выход (DDRxy=1) и записываем в PORTxy "1" - при этом осуществляется замыкание верхнего ключа и на выводе устанавливается напряжение близкое к уровню питанию. А если надо логический "0", то в PORTxy записываем ноль и открывается уже нижний ключ, на выводе устанавливается напряжение близкое к нулю вольт.
Вход Hi-Z - режим высокоимпендансного входа (включен по умолчанию). Все ключи разомкнуты, а сопротивление порта очень большое. Этот режим хорошо подходит для прослушивания какой либо шины данных, т.к. он не оказывает на нее абсолютно никакого влияния.
Вход PullUp - При DDRxy=0 и PORTxy=1 замыкается вентиль подтяжки и к линии подсоединяется сопротивление номиналом 100кОм, что моментально приводит неподключенную никуда в состояние логической "1". Основная задача режима - недопустить хаотичного переключения состояния на входе под действием помех. Но если на входе установится логический ноль (замыкание линии на корпус кнопкой или другим образом), то слабый 100 кОмный резистор не способен удержать напряжение на линии на уровне логической "1" и на входе установится ноль.

Также почти каждая ножка типового МК обладает дополнительными функциями. На распиновке в даташите они обычно подписаны в скобках. Это могут быть выводы разных последовательные интерфейсов, приемопередатчиков, выходы ШИМ генераторов, аналоговые входы. По умолчанию дополнительные функции всегда отключены, а вывод управляется только парой DDR и PORT, но если включить дополнительную функцию, то управление может перейти под контроль какого-либо периферийного устройства. Например, приемник USART. Как только выставляем бит разрешения приема RXEN, так RxD, сразу переходит в режим входа.

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

Типовая схема порта выхода МК AVR



VDD – источник питания; V1 источник импульсного или постоянного сигнала; M1, M2 и M3 полевые транзисторы; R1- подтягивающий» внутренний резистор (20..50 кОм и 50..150 кОм); CL емкость нагрузки или линии к нагрузке может изменяться от десятка пФ и до уровня «пока не сгорит»)

Статическая характеристика выхода или нагрузочная - зависимость напряжения на выходе от тока нагрузки.

Например у ATmega 328p максимальное значение тока через один выход для уровней «лог. 1 или 0» не более 40 мА, а максимальный уровень суммарного тока через все выходы, должен быть не выше 200 мА. Сопротивление канала (Rdrain) RD= 45 Ом. Смотри даташит в .

Статическая характеристика порта AVR «вход -выход», представляет из себя зависимость выходного напряжения от входного. Рассмотрим схему тестирования КМОП выхода порта по входному сигналу.


Передаточная характеристика КМОП выхода для этой схемы будет следующая:


Как видно из графиков, КПОМ схема отлично работает в роли переключательного элемента. «Сквозной» ток не превышает 5 мА. Кстати, схема эта типовой логический инвертор.

Динамическая характеристика это реакция выхода схемы на входной сигнал во временной области, т.е. зависимость выходного сигнала от входного на шкале времени.


V1 - источник сигнала с параметрами меандр, частота 1 МГц, уровни от 0 и до +5 В, длительность фронтов 5нс, выходное сопротивление Rout= 1Ом; CL емкость нагрузки (0 и 1000пФ).

Причем в лияние емкости нагрузки заметно, в соответствии с рисунком ниже, где представлены динамические характеристики КМОП-выхода порта AVR при емкости нагрузки CL=0 и CL= 1nF.


Основой этой схемы является микроконтроллер AVR ATmega32. ЖК дисплей с разрешением 128 х 64 точек. Схема осциллографа на микроконтроллере предельно проста. Но есть один существенный минус - это достаточно низкая частота измеряемого сигнала, всего лишь 5 кГц.