Программирование микроконтроллеров AVR на C. Изучение системы команд микроконтроллера AVR Чтение портов avr в си

Программы

Программирование микроконтроллеров AVR на C. Изучение системы команд микроконтроллера AVR Чтение портов avr в си

В этом статье мы напишем первую программу и научимся программировать порты ввода-вывода микроконтроллера.

Наша первая программа будет управлять, по началу, одним из выводов микроконтроллера. Для того чтобы удостоверится в том, что программа работает, к управляемому выводу через токоограничивающий резистор мы подключим светодиод, анод которого соединен с выводом МК, а катод с минусом (общим проводом).

По умолчанию, на всех выводах незапрограммированного микроконтроллера напряжение близкое к нулю, поэтому светодиод не будет светиться. Наша задача состоит в том, чтобы написать программу, с помощью которой на выводе МК появится напряжение +5 В. Это напряжения (точнее буде ток) засветит светодиод.

Порты ввода-вывода микроконтроллера



Микроконтроллер ATmega8 имеет 28 выводов, каждый из них выполняет определенные функции. Светодиод можно подключить к большинству выводов, однако, не ко всем, ведь минимум пара выводов занята под питание. Чтобы четко знать назначение каждого вывода МК воспользуется даташитом. В даташите находим распиновку (обозначение) всех выводов.

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

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

Плюс («+») источника питания подсоединяется в 7-й ножке, обозначенной VCC. Минус («-») – к 8-й или 22-й ножке, которые имеют обозначение GND (GND – сокращенно от ground – «земля»).

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

Микроконтроллер ATmega8 имеет три порта ввода-вывода: B, C и D. Порты могут быть полными и неполными. Полный порт состоит из восьми бит и соответственно имеет столько же одноименных выводов. У неполного порта меньше 8 бит, поэтому число выводов такого порта также менее восьми.

У данного МК порты B и D полные. А порт C неполный и имеет семь бит. Еще раз обращаю внимание, что нумерация битов начинается с нуля, например PB0, PB1, PB2…

Не удивляйтесь, что у ATmega8 отсутствует порт A. Другие МК, имеющие большее число выводов, могут содержать как порт A, так и порт E. У микроконтроллеров с меньшим числом выводов может быть только один порт и тот неполный.

Знакомство с Atmel Studio 7



Теперь перейдем к написанию кода программы. Наша первая программа будет устанавливать + 5 В на нулевом бите порта C PC0, т.е. на 23-м выводе микроконтроллера.

Запускаем Atmel Studio.

Сначала необходимо создать проект. Для этого в открывшемся окне кликаем по вкладке New Project
.

Также проект можно создать, кликнув по вкладке File
. В выпавшем меню следует выбрать New
и далее Project
. Или нажать комбинацию клавиш Ctrl+Shift+N
.

В появившемся окне выбираем язык программирования C/C++ и кликаем по вкладке.

Далее нам нужно задать имя и место на диске для нашего проекта. Назовём наш первый проект именем 1 в строке для ввода Name
. Изменить место расположения файла можно кликнув по кнопке Browse напротив строки Location
. Если оставить галочку возле Create directory for solution
, то в выбранном месте автоматически создастся папка с именем проекта. В данной папке помимо проекта будут созданы и другие вспомогательные файлы, поэтому я рекомендую не убирать галочку.

После того, как имя проекта и его место выбраны, нажимаем кнопку OK
. Снова появляется окно. В нем нам нужно выбрать тип микроконтроллера. В нашем случае – это ATmega8
. Кликаем по вкладке Device Family
. В выпавшем меню выбираем серию микроконтроллеров ATmega
.

С помощью прокрутки находим и выделяем микроконтроллер ATmega8
и наживаем кнопку OK
.

В открывшемся окне мы видим, что Atmel Studio нам автоматически сформировала заготовку или же шаблон программы.

Рассмотрим все по порядку.

Atmel Studio 7 | Первая программа


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

Цвет комментариев и других элементов программы можно изменять в настройках Atmel Studio.

* 1.c

* Created: 07.08.2017 16:57:59

Комментарии бываю однострочные и многострочные. В данном шаблоне программы применяются многострочные комментарии. Они начинаются косой линией со звездочкой, а заканчиваются звездочкой с косой линией.

/*
— начало комментария

*/
— конец комментария

Весь текс, который помещен между /*
и */
полностью пропускается компилятором.

Однострочный комментарий обозначается двумя косыми линиями и действует в пределах одной строки. Текст перед двумя косыми распознается компилятором как код программы, а после – как комментарий.

// Здесь пишется однострочный комментарий

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

Директива препроцессора


Следующим элементом программы является строка

#include

Эта строка указывает компилятору, что к данному файлу нужно подключить другой файл с именем io.h
, который находится в папке avr. В подключаемом файле находится информация о базовых настройках микроконтроллера.

По сути, можно было бы и не подключать файл io.h
, а набрать его содержимое вручную, однако это очень неудобно.

Знак #
означает, что данная команда – это директива препроцессора. Дело в том, что прежде чем скомпилировать файл компилятору необходимо выполнит предварительную его обработку. Поэтому сначала выполняется некая подготовка файла к компиляции путем добавления некоторых инструкций, прописанных в файле io.h

io
– название файла, которое происходит от сокращения input/output – ввод/вывод.

H
– расширение файла, название его происходит от слова header – заголовок.

Теперь все вместе. io.h
– это заголовочный файл, в котором записана информация о настройках ввода-вывода микроконтроллера.

Главная функция main


Ниже нам встречается следующая строка:

int
main
(void
)

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

В синтаксисе языка Си идентификатором функции служат круглые скобки

Внутри скобок помещено слово void (void). Оно обозначает пустота. Это указывает на то, что функция main ничего не принимает, т. е. не принимает никаких аргументов. По мере написания более сложных программ, мы детальнее остановимся на этом моменте.

int

– это целочисленный тип данных. В данном случае функция работает с целыми числами: как положительными, так и отрицательными. Существуют и другие типы данных, например с плавающей запятой, символьные и др. Более подробно мы будем рассматривать типы данных по мере необходимости или в отдельной статье. Однако для функции main рекомендуется всегда использовать тип данных int, поскольку конструкция int
main
(void
)
определена стандартом языка Си и распознается любым компилятором.

Область действия функции определяется фигурными скобками

.
→ тело функции

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

В общем случае любая функция имеет следующую конструкцию:

тип
данных
имя функции
(агрумент
)

тело функции (код)

Функция while



Внутри функции main
находится функция while
:

while
(1)

While
переводится с английского «пока». Это говорит о том, что программа, которая находится в теле данной функции, будет выполняться до тех пор, пока условие истинно. Единица в круглых скобках указывает, что условие истинно, поэтому код программы, написанный в данной функции, будет повторяться бесконечное число раз, т.е. программа будет зациклена. Для чего это делается? Дело в том, что микроконтроллер должен непрерывно выполнять записанную программу. Поэтому программа не может просто взять и оборваться. Микроконтроллер всегда опрашивает порты ввода-вывода либо выдает в них сигналы, даже находясь в ждущем режиме.

Теперь, когда мы рассмотрели основные элементы конструкции программы, давайте посмотрим целиком на базовый шаблон. Без комментариев он имеет следующий вид:

#include

int
main
(void
)

while
(1)

Программирование портов ввода-вывода микроконтроллера ATmega8


Сейчас мы уже можем дополнить программу нужным нам кодом. Первым делом необходимо настроить нулевой бит порта C PC0 на выход.

Мы уже знаем, что МК может, как принимать, так и выдавать сигнал, т.е. выводы (порты) его могут работать как входы
и как выходы
. Поэтому предварительно нужно настроить вывод МК на соответствующий режим. Для этого в микроконтроллере есть специальный регистр, который называется DDR – d
irect d
ata r
egister

– регистр направления данных.

У каждого порта есть свой такой регистр. Например, регистр порта C называется DDRC

, порта B – DDRB

, порта D – DDRD

.

Чтобы настроить вывод порта на вход

в регистр DDR необходимо записать ноль

, а на выход

единицу

.

Команда настройки нулевого бита порта C выглядит следующим образом

DDRC = 0b0000001;

Данной командой в регистр DDRC записывается двоичное число равное десятичному 1. Префикс 0b
идентифицирует данное число, как двоичное.

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

Также можно записать в регистр шестнадцатеричное число:

DDRC = 0x1;

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

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

DDRB = 0b10001001;

Регистр микроконтроллера PORT


После того, как мы настроили нулевой бит порта C PC0 на выход, нужно еще выполнить настройку, чтобы на данном выводе появилось напряжение +5 В. Для этого необходимо установить нулевой бит в регистре PORT

. Если бит установлен в единицу

, то на выводе будет +5 В

(точнее говоря величина напряжения питания микроконтроллера, которая может находится в пределах 4,5…5,5 В для микроконтроллера ATmega8). Если бит установлен в ноль

, — то на выводе будет напряжение, величина которого близка к нулю

.

Каждый порт имеет свой регистр: порт A – PORTA

, порт B – PORTB

, порт C – PORTC

.

И так, чтобы получить на выводе PC0 напряжение +5 В, необходимо записать такую команду:

PORT = 0b0000001;

Обратите внимание на то, что каждая команда заканчивается точкой с запятой.

Таким образом, чтобы засветить светодиод, нам необходимы всего лишь две команды:

DDRC = 0b0000001;

PORTС = 0b0000001;

Первой командой мы определяем вывод PC0 как вход, а второй устанавливаем на нем напряжение +5 В.

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

#include

int
main
(void
)

DDRC = 0b0000001;

While
(1)

PORTC = 0b0000001;

Здесь необходимо заметить следующее: команда DDRC = 0b0000001;
выполнится всего один раз, а команда PORTC = 0b0000001;
будет выполняться все время в цикле, поскольку она находится в теле функции while (1)
. Но даже если мы вынесем команду за пределы функции и поместим ее после DDRC = 0b0000001;
, светодиод и в этом случае будет светиться все время. Однако, разместив команду PORTC = 0b0000001
; в теле while (1)
, мы делаем явный акцент на том, что светодиод должен светится все время.

Компиляция файла



Теперь, когда код полностью готов, его нужно скомпилировать. Для этого необходимо нажать клавишу F7
или кликнуть по кнопке Build
и в выпавшем меню выбрать Build Solution
.

Если ошибок в коде нет, то файл скомпилируется, а в нижней части экрана появится запись о том, что проект скомпилирована успешно: Build succeeded
.

Таким образом программируются порты ввода-вывода микроконтроллера практически любого типа.
Следующий наш шаг – это . Также можно проверить корректность работы кода с помощью программы-симулятора микроконтроллеров –

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

В программе вставлен бесконечный цикл 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:avrProgram1binReleaseProgram1.hex этот файл и является нашей прошивкой.

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

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

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

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

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

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

_______________ сдвиг влево

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

unsigned char

tmp = 3; //0b00000011

tmp = tmp //теперь в переменной tmp число 6 или 0b00000110

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

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

Tmp = 7; //0b00000111

tmp //сокращенный вариант записи
//теперь в переменной 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//устанавливаем в единицу четвертый бит переменной tmp

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

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

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

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

Tmp |= (1 tmp |= (1

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

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

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

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

155 0b10011
011
&
247 0b11110
111

147 0b10010
011

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

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

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

1~(1tmp & (~(1результат 0b10010
011

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

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

tmp = tmp & (~((1//обнуляем третий, пятый и шестой биты

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

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

if

((tmp & (1 // блок будет выполняться, только если установлен

}

if

((tmp & (1// блок будет выполняться, только если не установлен
// второй бит переменной 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/ инвертируем третий бит переменой tmp

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

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

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

Tmp ^= ((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”

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

Итак, что же представляет собой один вывод микроконтроллера. Вначале на входе стоит небольшая защита из диодов, она призвана защитить ввод микроконтроллера от превышения напряжения. Если напряжение будет выше питания, то верхний диод откроется и это напряжение будет стравлено на шину питания, где с ним будет уже бороться источник питания и его фильтры. Если на ввод попадет отрицательное (ниже нулевого уровня) напряжение, то оно будет нейтрализовано через нижний диод и погасится на землю. Впрочем, диоды там хилые и защита эта помогает только от микроскопических импульсов от помех
. Если же ты по ошибке вкачаешь в ножку микроконтроллера вольт 6-7 при 5 вольтах питания, то никакой диод его не спасет.

Дальше идут ключи управления. Это я их нарисовал рубильниками, на самом деле там стоят полевые транзисторы, но особой сути это не меняет. А рубильники наглядней.
Каждый рубильник подчинен логическому условию которое я подписал на рисунке. Когда условие выполняется — ключ замыкается. PIN, PORT, DDR
это регистры конфигурации порта.

Есть в каждом контроллере AVR
PIC
есть тоже подобные регистры, только звать их по другому).

Например, смотри в даташите на цоколевку микросхемы:

Видишь у каждой почти ножки есть обозначение Pxx
. Например, PB4
где буква «B»
означает имя порта, а цифра — номер бита в порту. За порт «B» отвечают три восьмиразрядных регистра PORTB, PINB, DDRB
, а каждый бит в этом регистре отвечает за соответствующую ножку порта. За порт «А
» таким же образом отвечают PORTA, DDRA, PINA
.

PINх

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

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

DDRx

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

  • DDRxy=0 — вывод работает как ВХОД.
  • DDRxy=1 вывод работает на ВЫХОД.

PORTx

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

Есть еще бит PUD
(PullUp Disable) в регистре SFIOR
он запрещает включение подтяжки сразу для всех портов. По дефолту он равен 0. Честно говоря, я даже не знаю нафиг он нужен — ни разу не доводилось его применять и даже не представляю себе ситуацию когда бы мне надо было запретить использование подтяжки сразу для всех портов. Ну да ладно, инженерам Atmel
видней, просто знай что такой бит есть. Мало ли, вдруг будешь чужую прошивку ковырять и увидишь что у тебя подтяжка не работает, а вроде как должна. Тогда слазаешь и проверишь этот бит, вдруг автор прошивки заранее где то его сбросил.

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

Теперь кратко о режимах:

  • Режим выхода

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

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

Совет:

С целью снижения энергопотребления и повышения надежности рекомендуется все неиспользованные пины включить в режим PullUp
тогда их не будет дергать туда сюда помехой, а если на порт свалится грубая сила (например, монтажник отвертку уронит и коротнет на землю) то линия не выгорит.

Как запомнить режимы, чтобы не лазать каждый раз в справочник:

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

Итак:

  • Самый безопасный для МК и схемы, ни на что не влияющий режим это Hi-Z.
  • Очевидно что этот режим и должен быть по дефолту.
  • Значения большинства портов I/O при включении питания/сбросе = 0х00, PORT и DDR не исключение.
  • Соответственно когда DDR=0 и PORT=0 это High-Z — самый безопасный режим, оптимальный при старте.
  • Hi-Z это вход, значит при DDR=0 нога настроена на вход. Запомнили.
  • Однако, если DDR=0 — вход, то что будет если PORT переключить в 1?
  • Очевидно, что будет другой режим входа. Какой? Pullup, другого не дано! Логично? Логично. Запомнили.
  • Раз дефолтный режим был входом и одновременно в регистрах нуль, то для того, чтобы настроить вывод на выход
    надо в DDR записать 1.
  • Ну, а состояние выхода уже соответствует регистру PORT — высокий это 1, низкий это 0.
  • Читаем же из регистра PIN.

Есть еще один способ, мнемонический:

1 похожа на стрелку. Стрелка выходящая из МК — выход. Значит DDR=1 это выход! 0 похож на гнездо, дырку — вход! Резистор подтяжки дает в висящем порту единичку, значит PORT в режиме Pullup должен быть в единичке!

Все просто! 🙂

Для детей в картинках и комиксах:)

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

В режиме PullUp
эту планку мы пружиной подтянули кверху. Слабые помехи не смогут больше ее дрыгать как угодно. С другой стороны шине она может помешать, но не факт что заблокирует ее работу. От шины зависит и ее силы. А еще мы можем отслеживать тупую внешнюю силу, вроде кнопки, которая может взять и придавить ее к земле. Тогда мы узнаем что кнопка нажата.

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

Управление портами в AVR GCC. Регистры DDRx и PORTx.
Представление чисел. Побитные операции.
Функция задержки. Безусловный переход в программе.

Порты микроконтроллера – это устройства ввода/вывода, позволяющие микроконтроллеру передавать или принимать данные. Стандартный порт микроконтроллера AVR имеет восемь разрядов данных, которые могут передаваться или приниматься параллельно. Каждому разряду (или биту) соответствует вывод (ножка) микроконтроллера. Ножки микроконтроллера также называют пинами. Для обозначения портов используются латинские буквы А, В, С и т.д. Количество портов ввода/вывода варьируется в зависимости от модели микроконтроллера.

Любой порт микроконтроллера можно сконфигурировать как вход или как выход. Для того чтобы это сделать, следует записать в соответствующий порту регистр DDRx
необходимое значение. Кроме того, как вход или выход можно сконфигурировать отдельно любой вывод (пин) порта. В любом случае, хотите вы сконфигурировать весь порт или отдельный вывод, вам необходимо будет работать с ригистрами DDRx.

DDRx – регистр направления передачи данных. Этот регистр определяет, является тот или иной вывод порта входом или выходом. Если некоторый разряд регистра DDRx содержит логическую единицу, то соответствующий вывод порта сконфигурирован как выход, в противном случае – как вход. Буква x в данном случае должна обозначать имя порта, с которым вы работаете. Таким образом, для порта A это будет регистр DDRA, для порта B – регистр DDRB и т. д.

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

Для всего порта сразу.

DDRD = 0xff;

Все выводы порта D будут сконфигурированы как выходы.

0xff
– шестнадцатиричное представление числа ff, где 0x является префиксом, используемым для записи шестнадцатиричных чисел. В десятичном представлении это будет число 255, а в двоичном виде оно будет выглядеть как 11111111. То есть во всех битах регистра DDRD будут записаны логические единицы.

В AVR GCC
для представления двоичных чисел используется префикс 0b. Таким образом, число 11111111 должно представляться в программе как 0b11111111. Мы можем записать предыдущую команду в более читабельном виде.

DDRD = 0b11111111;

Хотя такая запись и выглядит более наглядной, при конфигурировании портов принято использовать шестнадцатиричное представление чисел.

Для того чтобы сконфигурировать все выводы порта D как входы, следует записать во все биты регистра DDRD логические нули.

DDRD = 0x00;

В регистр DDRD можно записать и другие числа. Например:

DDRD = 0xb3;

0xb3
– шестнадцатиричное представление числа 179. В двоичном виде оно будет выглядеть как 10110011. То есть часть выводов порта D будет сконфигурирована как выходы, а часть – как входы.


PD0 – 1 (выход)
PD1 – 1 (выход)
PD2 – 0 (вход)
PD3 – 0 (вход)
PD4 – 1 (выход)
PD5 – 1 (выход)
PD6 – 0 (вход)
PD7 – 1 (выход)

Чтобы сконфигурировать отдельно вывод PD2 как вход, нам необходимо в соответствующий бит регистра DDRD записать 0. Для этого применяют следующую конструкцию.

DDRD &= ~(1
В данном случае результат сдвига единицы на две позиции влево инвертируется с помощью операции побитного инвертирования, обозначаемой значком “~
“.

При инверсии мы получаем вместо нулей единички, а вместо единичек – нули. Эта логическая операция иначе называется операцией НЕ
(английское название NOT).

Таким образом, при побитном инвертировании 00000100 мы получаем 11111011. (Подробнее о работе с числами в микроконтроллере см. во врезке ниже.)


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

При логическом умножении 0*0=0, 0*1=0, 1*1=1
. Операцию логического умножения иначе называют операцией И
(английское название AND).

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

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

Если вывод сконфигурирован как выход, то единичка в соответствующем бите регистра PORTx формирует на выводе сигнал высокого уровня, а ноль – сигнал низкого уровня.

Если же вывод сконфигурирован как вход, то единичка в соответствующем бите регистра PORTx подключает к выводу внутренний подтягивающий pull-up резистор, который обеспечивает высокий уровень на входе при отсутствии внешнего сигнала.

Установить “1” на всех выводах порта D можно следующим образом.

PORTD = 0xff;

А установить “0” на всех выводах порта D можно так.

PORTD = 0x00;

К каждому биту регистров PORTx можно обращаться и по отдельности так же, как в случае с регистрами DDRx.

Например, команда

PORTD |= 1
установит “1” (сигнал высокого уровня) на выводе PD3.

PORTD &= ~(1
установит “0” (сигнал низкого уровня) на выводе PD4.

В AVR GCC
сдвиг можно осуществлять и с помощью функции _BV()
, которая выполняет поразрядный сдвиг и вставляет результат в компилируемый код.

В случае использования функции _BV() две предыдущие команды будут выглядеть следующим образом.

PORTD |= _BV(PD3);
// установить “1” на линии 3 порта D

PORTD &= ~_BV(PD4);
// установить “0” на линии 4 порта D

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

/*************************************************************************
ПРИМЕР ВКЛЮЧЕНИЯ СВЕТОДИОДА СИГНАЛОМ ВЫСОКОГО УРОВНЯ
Пример подключения на рисунке 1
**************************************************************************/

#include
$WinAVR = ($_GET[“avr”]); if($WinAVR) include($WinAVR);?>
int
main(void
) { // начало основной программы

DDRD = 0xff;
PORTD |= _BV(PD1); // установить “1” (высокий уровень) на выводе PD1

}

Теперь попробуем мигнуть светодиодом, подключенным так, как это изображено на левом рисунке. Для этого используем функцию задержки _delay_ms().

Функция _delay_ms() формирует задержку в зависимости от передаваемого ей аргумента, выраженного в миллисекундах (в одной секунде 1000 миллисекунд). Максимальная задержка может достигать 262.14 миллисекунд. Если пользователь передаст функции значение более 262.14, то произойдет автоматическое уменьшение разрешения до 1/10 миллисекунды, что обеспечивает задержки до 6.5535 секунд. (О формировании более длительных задержек можно прочитать в статье .)

Функция _delay_ms() содержится в файле delay.h, поэтому нам будет необходимо подключить этот файл к программе. Кроме того, для нормальной работы этой функции необходимо указать частоту, на которой работает микроконтроллер, в герцах.

/*************************************
ПРИМЕР МИГАНИЯ СВЕТОДИОДОМ
Пример подключения на рисунке 1
**************************************/

#define F_CPU 1000000UL

#include

#include int
main(void
) { // начало основной программы

DDRD = 0xff; // все выводы порта D сконфигурировать как выходы

PORTD |= _BV(PD1);
_delay_ms(500); // ждем 0.5 сек.

PORTD |= _BV(PD1); // установить “1” (высокий уровень) на выводе PD1,
//зажечь светодиод

_delay_ms(500); // ждем 0.5 сек.

PORTD &= ~_BV(PD1); // установить “0” (низкий уровень) на выводе PD1,
//погасить светодиод

} // закрывающая скобка основной программы


Серия миганий светодиодом будет очень короткой. Для того чтобы сделать мигание непрерывным, можно организовать бесконечный цикл с помощью оператора безусловного перехода “goto”. Оператор goto осуществляет переход к месту программы, обозначенному меткой. Имя метки не должно содержать пробелов. После имени метки ставится знак двоеточия. Между именем метки и двоеточием не должно быть пробелов.


/*******************************************************
ПРИМЕР БЕСКОНЕЧНОГО МИГАНИЯ СВЕТОДИОДОМ
Пример подключения на рисунке 1
********************************************************/

#define F_CPU 1000000UL
// указываем частоту в герцах

#include

#include int
main(void
) { // начало основной программы

DDRD = 0xff; // все выводы порта D сконфигурировать как выходы

start: // метка для команды goto start

PORTD |= _BV(PD1); // установить “1” (высокий уровень) на выводе PD1,
//зажечь светодиод

_delay_ms(250); // ждем 0.25 сек.

PORTD &= ~_BV(PD1); // установить “0” (низкий уровень) на выводе PD1,
//погасить светодиод

_delay_ms(250); // ждем 0.25 сек.

goto
start; // перейти к метке start

} // закрывающая скобка основной программы

Оцените статью
Добавить комментарий

двадцать − 20 =