Чтение портов avr в си. Регистры и порты микроконтроллера AVR
Любое устройство на микроконтроллере AVR использует порты ввода вывода. Для работы с портами у AVR`ок есть три регистра: PORTx, PINx и DDRx, где x - буква порта, например A, B, C и т.д.
Регистр DDRx - определяет направление выводов микроконтроллера, PINx позволяет читать их состояние, "осязать" внешний мир, а PORTx в зависимости от направления вывода или задает его логический уровень, или подключает подтягивающий резистор.
Выводы микроконтроллера в проекте обычно задают с помощью макроопределений - define`ов. Мы получаем некую "отвязку" от железа и в дальнейшем это позволяет нам переназначать выводы на другие порты. Например, это может выглядеть так.
#define BUT_PIN 3
#define BUT_PORTX PORTB
#
define BUT_DDRX DDRB
#define BUT_PINX PINB
Неудобство такого подхода состоит в том, что для каждого вывода нужно определять три регистра. Бывает, что два (только PORTx и DDRx), но это тоже неудобно, если выводов много. Существует другой подход, позволяющий сократить число макроопределений. Разберемся в чем он заключается.
В микроконтроллер atmega16 регистр PORTB имеет адрес 0x18, а регистры DDRB и PINB - 0x17 и 0x16 соответственно. Тоже самое и с регистрами остальных портов, они тоже расположены друг за другом. Мы можем определить в проекте только один регистр, а к остальным обращаться вычисляя их адрес. За основу можно взять любой из них, главное ничего не напутать. Лучше всего для этих целей использовать макросы. Если отталкиваться от регистров PORTx, то макросы будут выглядеть так.
//это макросы для доступа к регистрам порта
#define PortReg(port) (*(port))
#define DirReg(port) (*((port) - 1))
#define PinReg(port) (*((port) - 2))
Макросы принимают в качестве параметра адрес регистра PORTx. Для взятия адреса регистра используется оператор &. Посмотрим, как можно использовать эти макросы.
... Как видите, определять выводы теперь можно намного короче, однако за удобство приходится платить вставкой макросов. Компилятор преобразует эти макросы в очень компактный код. Точно такой же, как если бы мы обращались к регистрам используя их имена. И дело в том, что с точки зрения ассемблера это так и есть. Если вы посмотрите ассемблерный код этих примеров, то увидите, что обращение к регистрам осуществляется методом прямой адресации с помощью команд IN, OUT. Поэтому я и озаглавил этот раздел "ненастоящая работа с портом через указатели". Указатели вроде как используются, но на самом деле нет.
Иногда приходится прибегать к работе с портом, используя настоящий указатель. Для этого создается переменная указатель, которая инициализируется адресом какого-нибудь регистра порта. Делается это следующим образом. //инициализация //вывод в порт через указатель Также этот указатель можно передавать в функцию. ... //а здесь уже не нужен оператор & Работа с портом через указатель открывает большие возможности. Например, мы можем определить структуру, которая будет хранить все настройки пина микроконтроллера и обращаться к выводу, используя эту структуру. Или можем определить структуру виртуального порта содержащую выводы микроконтроллера из разных физических портов. Вот небольшой пример, как можно использовать указатели при работе с портом. //функция инициализации //конфигурируем вывод на выход //задаем логический уровень //установить на выходе 1 //установить на выходе 0 Пример использования
//инициализируем ее //устанавливаем 1 на выводе OUT1_PIN Еще один пример работы с портом через указатели есть в коде к статье "
Чтобы общаться с внешним миром у микроконтроллера есть порты ввода-вывода, в каждом из которых есть несколько отдельных битов (считай выводов), на которых можно установить ноль (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). Необходимо учитывать что производится в разных корпусах и нумерация выводов может отличатся Управление портами достаточно простое. Используется три восьми битных регистра - 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
можно не использовать, если только для проверки состояния выводов порта. Разберем программу на C
по строкам.
#include В программе вставлен бесконечный цикл while(1),
чтобы микроконтроллер не выполнил ничего лишнего. Все действия с портами в программе выполнены с использованием поразрядных операций в языке C.
Что дало возможность управлять только одним разрядом (одним выводом микроконтроллера) порта B.
На использованном нами выводе микроконтроллера в Arduino UNO
и Arduino Nano v3
подключен светодиод, поэтому в первой программе не придется даже собирать схему, достаточно подключить Arduino
к компьютеру. Программное обеспечение готово, программа написана, микроконтроллер тоже есть в виде 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 Заменяем эту заготовку нашей программой и жмем Происходит компиляция проекта и внизу видим Все прошивка готова, она находится в папке проекта (выбранной при создании проекта). У меня C:\avr\Program1\bin\Release\Program1.hex этот файл и является нашей прошивкой. Начнем прошивать. Запустим программу ArduinoBuilder В окне выбираем файл hex
(находится в папке проекта CodeBlocks >> bin/Release/project1.hex) нашего проекта, выбираем Arduino
и частоту микроконтроллера и жмем кнопку чем программировать (у меня COM9
) обычно это com
порт отличный от 1. После сего проделанного смотрим мигающий диод. На этом задача минимум выполнена. Рассмотрен подборка программного обеспечения, изучены порты ввода/вывода и регистры их управления, написана программа на C
скомпилирована и прошита в микроконтроллер. И все это можно применить для микроконтроллеров AVR
за исключением программы ArduinoBuilder которая в основном создана под Arduino,
но и ее можно заменить при использовании например программатора Чтение/Запись Исходное значение · Бит 7 – Разрешение всех прерываний. Для разрешения прерываний этот бит должен быть установлен в состояние 1. Управление разрешением конкретного прерывания выполняется регистром маски прерывания EIMSK и TIMSK. Если этот бит очищен (=0), то ни одно из прерываний не обрабатывается. Бит аппаратно очищается после возникновения прерывания и устанавливается для последующего разрешения прерывания командой RETI. · Бит 3 – Дополнение до двух флага переполнения. Он поддерживает арифметику дополнения до двух. Микроконтроллер AT90S8535 имеет 4 параллельных порта ввода/вывода A, B,C и D. Чтение/Запись Исходное значение Чтение/Запись Исходное значение Чтение/Запись Исходное значение Порт В является 8-разрядным двунаправленным портом ввода/вывода. Также как и у порта А взаимодействие с портом В осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTB, $18($38), регистр направления данных – DDRB, $17($37) и регистр входных данных – PINB, $16($36). Регистр PINB обеспечивает возможность только чтения. Регистр PINB не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Выводы порта В могут выполнять альтернативные функции, указанные в табл. 2.1. Альтернативная функция T0 – вход тактового сигнала таймера/счетчика 0 T1 – вход тактового сигнала таймера/счетчика 1 AIN0 – положительный вывод компаратора AIN1 – отрицательный вывод компаратора – вход выбора ведомого SPI MOSI – установка ведущий выход/ведомый вход SPI MISO – установка ведущий вход/ведомый выход SPI SCK – тактовый сигнал SPI При использовании выводов для альтернативных функций регистры PORTB, DDRB должны быть установлены соответствующим образом. Чтение/Запись Исходное значение Регистр направления данных порта B –
DDRB
Чтение/Запись Исходное значение Регистр входных данных порта B –
PINB
Чтение/Запись Исходное значение Порт С представляет собой 8-разрядный двунаправленный порт ввода/вывода. Также как у портов А и В взаимодействие с портом С осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTC, $15($35), регистр направления данных – DDRC, $14($34) и регистр входных данных – PINC, $13($33). Регистр PINC обеспечивает только возможность чтения, а регистры PORTC и DDRC – возможность чтения и записи. Регистр PINC не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Чтение/Запись Исходное значение Регистр направления данных порта C –
DDRC
Чтение/Запись Исходное значение Регистр входных данных порта C –
PINC
Чтение/Запись Исходное значение Порт D является 8-разрядным двунаправленным портом ввода/вывода. Также как и у портов А, В и С взаимодействие с портом D осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTD, $12($32), регистр направления данных – DDRD, $11($31) и регистр входных данных – PIND, $10($30). Регистр PIND обеспечивает возможность чтения, а регистры PORTD и DDRD – возможность чтения и записи. Регистр PIND не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Альтернативная функция RxD – вход приемника UART TxD – выход передатчика UART INT0 – вход внешнего прерывания 0 INT1 – вход внешнего прерывания 1 OC1B – вывод сравнения выхода В таймера/счетчика 1 OC1А – вывод сравнения выхода А таймера/счетчика 1 ICP – вход триггера захвата таймера/счетчика 1 OC2 – вывод сравнения выхода таймера/счетчика 2 При использовании выводов для альтернативных функций регистры PORTD, DDRD должны быть установлены соответствующим образом. Чтение/Запись Исходное значение Чтение/Запись Исходное значение Чтение/Запись Исходное значение Так как рассматриваемая работа первая, то для приобретения обучающимися навыков работы с лабораторным комплексом все обучающиеся сначала делают одинаковую работу. Со своих рабочих мест они вводят в ПЭВМ одну и ту же задачу вычитания из числа 5 числа 3, приведённую в п. 1.5.3.1. После компиляции программы она записывается в микроконтроллер рабочего места и демонстрируется её работа преподавателю. С внешним миром микроконтроллер общается через порты ввода вывода. Схема порта ввода вывода указана в даташите: Итак, что же представляет собой один вывод микроконтроллера. Вначале на входе стоит небольшая защита из диодов, она призвана защитить ввод микроконтроллера от превышения напряжения. Если напряжение будет выше питания, то верхний диод откроется и это напряжение будет стравлено на шину питания, где с ним будет уже бороться источник питания и его фильтры. Если на ввод попадет отрицательное (ниже нулевого уровня) напряжение, то оно будет нейтрализовано через нижний диод и погасится на землю. Впрочем, диоды там хилые и защита эта помогает только от микроскопических импульсов от помех
. Если же ты по ошибке вкачаешь в ножку микроконтроллера вольт 6-7 при 5 вольтах питания, то никакой диод его не спасет. Дальше идут ключи управления. Это я их нарисовал рубильниками, на самом деле там стоят полевые транзисторы, но особой сути это не меняет. А рубильники наглядней. Есть в каждом контроллере AVR
(в PIC
есть тоже подобные регистры, только звать их по другому). Например, смотри в даташите на цоколевку микросхемы: Видишь у каждой почти ножки есть обозначение Pxx
. Например, PB4
где буква «B»
означает имя порта, а цифра — номер бита в порту. За порт «B» отвечают три восьмиразрядных регистра PORTB, PINB, DDRB
, а каждый бит в этом регистре отвечает за соответствующую ножку порта. За порт «А
» таким же образом отвечают PORTA, DDRA, PINA
. PINх
При снижении напряжения питания разумеется эти пороги также снижаются, график зависимости порогов переключения от питающего напряжения можно найти в даташите. DDRx
PORTx
Есть еще бит PUD
(PullUp Disable) в регистре SFIOR
он запрещает включение подтяжки сразу для всех портов. По дефолту он равен 0. Честно говоря, я даже не знаю нафиг он нужен — ни разу не доводилось его применять и даже не представляю себе ситуацию когда бы мне надо было запретить использование подтяжки сразу для всех портов. Ну да ладно, инженерам Atmel
видней, просто знай что такой бит есть. Мало ли, вдруг будешь чужую прошивку ковырять и увидишь что у тебя подтяжка не работает, а вроде как должна. Тогда слазаешь и проверишь этот бит, вдруг автор прошивки заранее где то его сбросил. Общая картина работы порта показана на рисунке: Теперь кратко о режимах: Также почти каждая ножка имеет дополнительные функции
. На распиновке они подписаны в скобках. Это могут быть выводы приемопередатчиков, разные последовательные интерфейсы, аналоговые входы, выходы ШИМ генераторов. Да чего там только нет. По умолчанию все эти функции отключены
, а вывод управляется исключительно парой DDR
и PORT
, но если включить какую-либо дополнительную функцию, то тут уже управление может полностью или частично перейти под контроль периферийного устройства и тогда хоть запишись в DDR/PORT
— ничего не изменится. До тех пор пока не выключишь периферию занимающую эти выводы. Совет:
Как запомнить режимы, чтобы не лазать каждый раз в справочник:
Итак:
Есть еще один способ, мнемонический:
Все просто! :) Для детей в картинках и комиксах:)
В режиме PullUp
эту планку мы пружиной подтянули кверху. Слабые помехи не смогут больше ее дрыгать как угодно. С другой стороны шине она может помешать, но не факт что заблокирует ее работу. От шины зависит и ее силы. А еще мы можем отслеживать тупую внешнюю силу, вроде кнопки, которая может взять и придавить ее к земле. Тогда мы узнаем что кнопка нажата. В режиме OUT
у нас планка прибита гвоздями к земле или прижата домкратом к питанию. Внешняя сила может ее пересилить только сломав домкрат или сломается сама. Тупая внешняя сила просто разрушает наш домкрат или вырывает гвозди из пола с мясом. В любом случае — девайс в помойку. При программировании микроконтроллеров постоянно приходится работать с битами. Устанавливать их, сбрасывать, проверять их наличие в том или ином регистре. В AVR ассемблере для этих целей существует целый ряд команд. Во-первых, это группа команд операций с битами – они предназначены для установки или сброса битов в различных регистрах микроконтроллера, а во-вторых, группа команд передачи управления – они предназначены для организации ветвлений программ. В языке Си естественно нет подобных команд, поэтому у начинающих программистов часто возникает вопрос, а как в Си работать с битами. Эту тему мы сейчас и будем разбирать. << - сдвиг влево Сдвигает число на n разрядов влево. Старшие n разрядов при этом исчезают, а младшие n разрядов заполняются нулями. Tmp = tmp << 3; Выражения, в которых над переменной производится какая-либо операция, а потом результат операции присваивается этой же переменной, можно записывать короче, используя составные операторы. Tmp = 7; //0b00000111
Операция сдвига влево на n разрядов эквивалентна умножению переменной на 2 n . Сдвигает число на n разрядов вправо. Младшие n разрядов при этом теряются. Заполнение старших n разрядов зависит от типа переменной и ее значения. Старшие n разрядов заполняются нулями в двух случаях – если переменная беззнакового типа или если переменная знаковая и ее текущее значение положительное. Когда переменная знаковая и ее значение отрицательное – старшие разряды заполняются единицами. Пример для беззнаковой переменной unsigned char
tmp = 255; //0b11111111
Tmp >>= 3; //сокращенный вариант записи
Пример для переменной знакового типа int
tmp = 3400; //0b0000110101001000
Tmp = -1200; //0b1111101101010000
Операция сдвига вправо на n разрядов эквивалентна делению на 2 n . При этом есть некоторые нюансы. Если потерянные младшие разряды содержали единицы, то результат подобного “деления” получается грубоватым. Например
9/4 = 2,5 а 9>>2 (1001>>2) равно 2 Во втором случае ошибка больше, потому что оба младших разряда единицы. В третьем случае ошибки нет, потому что потерянные разряды нулевые. Поразрядно инвертирует число. Разряды, в которых были нули – заполняются единицами. Разряды, в которых были единицы – заполняются нулями. Оператор поразрядной инверсии являтся унарным оператором, то есть используется с одним операндом. unsigned char
tmp = 94; //0b01011110
Tmp = ~tmp; Оператор | осуществляет операцию логического ИЛИ между соответствующими битами двух операндов. Результатом операции логического ИЛИ между двумя битами будет 0 только в случае, если оба бита равны 0. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности. Оператор | обычно используют для установки заданных битов переменной в единицу.
Tmp = 155 155 0b100110
11 Использовать десятичные числа для установки битов довольно неудобно. Гораздо удобнее это делать с помощью операции сдвига влево <<. Читаем справа налево – сдвинуть единицу на четыре разряда влево, выполнить операцию ИЛИ между полученным числом и значением переменной tmp, результат присвоить переменной tmp. Tmp = tmp | (1<<7)|(1<<5)|(1<<0); С помощью составного оператора присваивания |= можно сделать запись компактней. Tmp |= (1<<4); Оператор & осуществляет операцию логического И между соответствующими битами двух операндов. Результатом операции логического И между двумя битами будет 1 только в том случае, если оба бита равны 1. Во всех других случаях результат будет 0. Это проиллюстрировано в таблице истинности. Оператор & обычно применяют, чтобы обнулить один или несколько битов.
Tmp = 155; 155 0b10011
011 Видите, третий бит стал равен 0, а остальные биты не изменились. Обнулять биты, используя десятичные цифры, неудобно. Но можно облегчить себе жизнь, воспользовавшись операторами << и ~ Tmp = 155; 1<<3 0b00001
000 Читаем справа налево – сдвинуть единицу на три разряда влево, выполнить инверсию полученного числа, выполнить операцию & между значением переменной 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){ Оператор ^ осуществляет операцию логического исключающего ИЛИ между соответствующими битами двух операндов. Результатом операции логического исключающего ИЛИ будет 0 в случае равенства битов. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности. Оператор ^ применяется не так часто как остальные битовые операторы, но и для него находится работенка. Например, с помощью него можно инвертировать один или несколько битов переменной
. 155 0b10011
011 Четвертый бит изменил свое значение на противоположное, а остальные биты остались без изменений. Tmp = tmp ^ 8; // опять инвертируем четвертый бит переменой tmp
147 0b10010
011 Видите, четвертый бит снова изменил свое значение на противоположное. Так записывать выражение намного удобнее Tmp = tmp ^ (1<<3); // инвертируем третий бит переменой tmp
А так и удобно и компактно Tmp ^= (1<<4); //инвертируем четверый бит
Можно инвертировать несколько битов одновременно Tmp ^= ((1<<4)|(1<<2)|(1<<1)); //инвертируем 4,2 и 1 биты
У поразрядного исключающего ИЛИ есть еще одно интересное свойство
. Его можно использовать, для того чтобы поменять значения двух переменных местами. Обычно для этого требуется третья переменная. Но используя оператор ^ переставить значения можно так: var1 ^= var 2; Чистая магия, хотя, честно говоря, я ни разу не пользовался таким приемом. Теперь мы знаем, как устанавливать, обнулять и инвертировать биты, знаем, как проверять установлен ли бит или нет. Рассмотренные выше выражения довольно громоздки, но с помощью директивы препроцессора #define
, им можно придать более приятный вид. Директива #define
используется для присваивания символических имен константам и для макроопределений. Использование символических имен делают программу более модифицируемой и переносимой. Например, вы используете в тексте программы константу, и вдруг вам понадобилось изменить ее значение. Если она встречается всего в трех местах, то исправить ее можно и в ручную, а что делать, если она встречается в пятидесяти строчках? Мало того, что исправление займет много времени, так еще и ошибиться в этом случае проще простого. Здесь то, как раз и выручает директива #define
. В начале программы задается символическое имя константы, которое используется по ходу программы. Если нам нужно изменить это значение, это делается всего лишь в одном месте. А перед компиляцией препроцессор сам подставит во все выражения вместо имени константы ее значение. Программирование микроконтроллера неразрывно связано с его аппаратной частью и чаще всего с внешней обвязкой. Взять хотя бы кнопки - опрашивая их в своей программе, мы обращаемся к реальным выводам микроконтроллера. А если нам вдруг понадобилось использовать программу опроса кнопок в другой схеме, где кнопки подключены к другим выводам? Придется исправлять программу. Опять таки, задав с помощью директивы #define
символическое имя для соответствующих выводов, модифицировать программу будет проще простого #include
"iom8535.h" //порт, к которому подключены кнопки
//выводы, к которым подключены кнопки
int
main() При задании символического имени можно использовать и выражения #define
MASK_BUTTONS ((1< пример использования:
Используя #define
не жалейте скобок чтобы четко задать последовательность вычисления выражений! Некоторые выражения можно замаскировать под «функции». #define
ADC_OFF() ADCSRA = 0 пример использования: Можно использовать многострочные определения, используя в конце каждой строки символ \ #define
INIT_Timer() TIMSK = (1< пример использования: Ну и самое мощное применение директивы #define
– это задание макроопределений (или просто макросов). Вот как с помощью #define
можно задать макросы для рассмотренных ранее операций с битами #define
SetBit(reg, bit) reg |= (1< пример использования: … Перед компиляцией препроцессор заменит эти строчки объявленными ранее выражениями, подставив в них соответствующие аргументы. Макросы очень мощное средство, но использовать их нужно осторожно. Вот самые распространенные грабли, о которых написано во всех учебниках по программированию. Определим макрос, вычисляющий квадрат числа: #define
SQUARE(x) x*x выражение А что будет если в качестве аргумента макроопределения использовать выражение my_var+1 tmp = SQUARE(my_var +1); Препроцессор заменит эту строчку на tmp = my_var + 1 * my_var +1; а это вовсе не тот результат, который мы ожидаем. Чтобы избежать таких ошибок не скупитесь на скобки при объявлении макросов!
Если объявить макрос так #define
SQUARE(x) ((x)*(x)) выражение записываем их в папку проекта, а в начале файла main.c прописываем #include
"bits_macros.h"
//определили вывод мк
#define BUT_PIN 3
#define BUT_PORT PORTB
...
// конфигурируем вывод как вход
DirReg(&BUT_PORT) &= ~(1<
PortReg(&BUT_PORT) |= (1<
//проверяем нажата ли кнопка
if(! (PinReg(&BUT_PORT)&(1<
}
Такой подход можно использовать не со всеми микроконтроллерами AVR, потому что в некоторых моделях регистры порта располагаются не по соседним адресам. Как, например, регистры порта F в микроконтроллере ATmega128.
Настоящая работа с портом через указатель
//объявляем указатель на регистр
//обязательно должно присутствовать volatile
volatile uint8_t *portReg;
//передаем адрес регистра PORTB
portReg = &PORTB;
//перед указателем ставиться оператор *
(*portReg) = 0xff;
void OutPort(volatile uint8_t *pReg, uint8_t data)
{
*pReg = data;
}
//записываем в PORTB число 0xff
OutPort(&PORTB, 0xff);
//так как мы передаем переменную с адресом порта
OutPort(portReg, 0xff);
Все это так, но есть ложка дегтя. Работа с регистрами порта через указатель "тяжеловесна" с точки зрения размера кода и его быстродействия.
Чтобы в этом убедиться, достаточно взглянуть на получаемый ассемблерный код. Если эти два фактора не критичны, то такой подход можно использовать, если нет, то придется работать по старинке.
Также п
ри работе с портом через указатели, даже операция установки/сброса разряда будет неатомарна.
Атомарность операций в этом случае нужно обеспечивать самостоятельно.
//струтура для хранения настроек вывода - номера и порта
typedef struct outputs{
uint8_t pin;
volatile uint8_t *portReg;
}outputs_t;
void OUT_Init(outputs_t *out, uint8_t pin, volatile uint8_t *port, uint8_t level)
{
//сохраняем настройки в структуру
out->pin = pin;
out->portReg = port;
(*(port-1)) |= (1<
if (level) {
(*port) |= (1<
else{
(*port) &= ~(1<
}
void OUT_Set(outputs_t *out)
{
(*(out->portReg)) |= (1 << out->pin);
}
void OUT_Clear(outputs_t *out)
{
(*(out->portReg)) &= ~(1 << out->pin);
}
//определили вывод мк
#define OUT1_PIN 4
#define OUT1_PORT PORTB
...
//объявляем переменную для хранения
//настроек пина
outputs_t out1;
OUT_Init(&out1, OUT1_PIN, OUT1_PORT, 0);
OUT_Set(&out1);2.1. Порты и выводы.
2.2. Регистры управления портами.
2.3. Программа
2.4. Проект на C и компиляция
2.5. Прошиваем микроконтроллер
Бит
· Бит 6 – Бит сохранения копии. Команды копирования бита BLD и BST используют этот бит как источник и приемник при операциях с битами. Командой BST бит регистра общего назначения копируется в бит T, командой BLD бит Т копируется в бит регистра общего назначения.
· Бит 5 – Флаг полупереноса. Он указывает на перенос между тетрадами при выполнении ряда арифметических операций.
· Бит 4 – Бит знака. Бит S имеет значение результата операции исключающее ИЛИ (N(+)V) над флагами отрицательного значения (N) и дополнения до двух флага переполнения (V).
· Бит 2 – Флаг отрицательного значения. Этот флаг указывает на отрицательный результат ряда арифметических и логических операций.
· Бит 1 – Флаг нулевого значения. Этот флаг указывает на нулевой результат ряда арифметических и логических операций.
· Бит 0 – Флаг переноса. Этот флаг указывает на перенос при арифметических и логических операциях.
Порт А является 8-разрядным двунаправленным портом. Взаимодействие с портом А осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTA, $1B($3B), регистр направления данных – DDRA, $1A($3A), регистр входных данных – PINA, $19($39). Регистр PINA обеспечивает только возможность чтения, а регистры PORTA и DDRA – возможность чтения и записи. Регистр PINA не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Порт А служит также для ввода аналоговых сигналов A/D.Регистр данных порта А –
PORTA
Бит
Регистр направления данных порта А –
DDRA
Бит
Регистр входных данных порта А –
PINA
Бит
Таблица 2.1. Альтернативные функции выводов порта В
Вывод порта
Регистр данных порта
B
–
PORTB
Бит
Бит
Бит
У порта С только два вывода могут выполнять альтернативные функции: выводы PC6 и PC7 выполняют функции TOSC1 и TOSC2 таймера/счетчика 2.Регистр данных порта
C
–
PORTC
Бит
Бит
Бит
Выводы порта D могут выполнять альтернативные функции, указанные в табл. 2.2.Таблица 2.2. Альтернативные функции выводов порта D
Вывод порта
Регистр данных порта
D
–
PORTD
Бит
Регистр направления данных порта
D
–
DDRD
Бит
Регистр входных данных порта
D
–
PIND
Бит
После такого знакомства с комплексом обучающийся приступает к выполнению индивидуального задания. При наличии времени преподаватель может усложнить индивидуальное задание.
Каждый рубильник подчинен логическому условию которое я подписал на рисунке. Когда условие выполняется — ключ замыкается. PIN, PORT, DDR
это регистры конфигурации порта.
Это регистр чтения. Из него можно только читать. В регистре PINx
содержится информация о реальном текущем логическом уровне
на выводах порта. Вне зависимости от настроек порта. Так что если хотим узнать что у нас на входе — читаем соответствующий бит регистра PINx
Причем существует две границы: граница гарантированного нуля и граница гарантированной единицы — пороги за которыми мы можем однозначно четко определить текущий логический уровень. Для пятивольтового питания это 1.4 и 1.8 вольт соответственно. То есть при снижении напряжения от максимума до минимума бит в регистре PIN переключится с 1 на 0 только при снижении напруги ниже 1.4 вольт, а вот когда напруга нарастает от минимума до максимума переключение бита с 0 на 1 будет только по достижении напряжения в 1.8 вольта. То есть возникает гистерезис переключения с 0 на 1, что исключает хаотичные переключения под действием помех и наводок, а также исключает ошибочное считывание логического уровня между порогами переключения.
Это регистр направления порта. Порт в конкретный момент времени может быть либо входом либо выходом (но для состояния битов PIN
это значения не имеет. Читать из PIN реальное значение можно всегда).
Режим управления состоянием вывода. Когда мы настраиваем вывод на вход, то от PORT
зависит тип входа (Hi-Z или PullUp, об этом чуть ниже).
Когда ножка настроена на выход
, то значение соответствующего бита в регистре PORTx определяет состояние вывода. Если PORTxy=1
то на выводе лог1, если PORTxy=0
то на выводе лог0.
Когда ножка настроена на вход
, то если PORTxy=0
, то вывод в режиме Hi-Z
. Если PORTxy=1
то вывод в режиме PullUp
с подтяжкой резистором в 100к до питания.
Ну тут, думаю, все понятно — если нам надо выдать в порт 1 мы включаем порт на выход (DDRxy=1
) и записываем в PORTxy
единицу — при этом замыкается верхний ключ и на выводе появляется напряжение близкое к питанию. А если надо ноль, то в PORTxy
записываем 0 и открывается уже нижний вентиль, что дает на выводе около нуля вольт.
Этот режим включен по умолчанию. Все вентили разомкнуты, а сопротивление порта очень велико
. В принципе, по сравнению с другими режимами, можно его считать бесконечностью. То есть электрически вывод как бы вообще никуда не подключен и ни на что не влияет. Но! При этом он постоянно считывает свое состояние в регистр PIN
и мы всегда можем узнать что у нас на входе — единица или ноль. Этот режим хорош для прослушивания какой либо шины данных, т.к. он не оказывает на шину никакого влияния. А что будет если вход висит в воздухе? А в этом случае напряжение будет на нем скакать в зависимости от внешних наводок, электромагнитных помех и вообще от фазы луны и погоды на Марсе (идеальный способ нарубить случайных чисел!). Очень часто на порту в этом случае нестабильный синус 50Гц — наводка от сети 220В, а в регистре PIN будет меняться 0 и 1 с частотой около 50Гц
При DDRxy=0
и PORTxy=1
замыкается ключ подтяжки и к линии подключается резистор в 100кОм, что моментально приводит неподключенную никуда линию в состояние лог1. Цель подтяжки очевидна — недопустить хаотичного изменения состояния на входе под действием наводок. Но если на входе появится логический ноль (замыкание линии на землю кнопкой или другим микроконтроллером/микросхемой), то слабый 100кОмный резистор не сможет удерживать напряжение на линии на уровне лог1 и на входе будет нуль.
Например, приемник USART
. Стоит только выставить бит разрешения приема RXEN
как вывод RxD
, как бы он ни был настроен до этого, переходит в режим входа.
С целью снижения энергопотребления и повышения надежности рекомендуется все неиспользованные пины включить в режим PullUp
тогда их не будет дергать туда сюда помехой, а если на порт свалится грубая сила (например, монтажник отвертку уронит и коротнет на землю) то линия не выгорит.
Чем зазубривать или писать напоминалки, лучше понять логику разработчиков, проектировавших эти настройки, и тогда все запомнится само.
1 похожа на стрелку. Стрелка выходящая из МК — выход. Значит DDR=1 это выход! 0 похож на гнездо, дырку — вход! Резистор подтяжки дает в висящем порту единичку, значит PORT в режиме Pullup должен быть в единичке!
Для большей ясности с режимами приведу образный пример:
В Си существуют 6 операторов для манипулирования битами. Их можно применять к любым целочисленным знаковым или беззнаковым типам переменных.
>> - сдвиг вправо
~ - поразрядная инверсия
| - поразрядное ИЛИ
& - поразрядное И
^ - поразрядное исключающее ИЛИ_______________ сдвиг влево << _______________
unsigned char
tmp = 3; //0b00000011
tmp = tmp << 1;
//теперь в переменной tmp число 6 или 0b00000110
//теперь в переменной tmp число 48 или 0b00110000
tmp <<= 2; //сокращенный вариант записи
//теперь в переменной tmp число 28 или 0b00011100
_______________ сдвиг вправо >> _______________
tmp = tmp >> 1;
//теперь в переменной tmp число 127 или 0b01111111
//теперь в переменной tmp число 15 или 0b00001111
tmp >>= 2;
//теперь в переменной число 850 или 0b0000001101010010
tmp >>= 2;
//теперь в tmp число -300 или 0b1111111011010100
//видите - два старших разряда заполнились единицами
11/4 = 2,75 а 11>>2 (1011>>2) равно 2
28/4 = 7 а 28>>2 (11100>>2) равно 7_______________поразрядная инверсия ~ _______________
tmp = ~tmp;
//теперь в переменной tmp число 161 или 0b10100001
//теперь в tmp снова число 94 или 0b01011110
_______________ поразрядное ИЛИ | ______________
tmp = tmp | 4; //устанавливаем в единицу второй бит переменной tmp
4
0b000001
00
159 0b100111
11
tmp = tmp | (1<<4); //устанавливаем в единицу четвертый бит переменной tmp
Установить несколько битов в единицу можно так
//устанавливаем в единицу седьмой, пятый и нулевой биты переменной tmp
tmp |= (1<<7)|(1<<5)|(1<<0);_______________ побитовое И & _______________
tmp = tmp & 247; //обнуляем третий бит переменной tmp
&
247 0b11110
111
147 0b10010
011
tmp = tmp & (~(1<<3)); //обнуляем третий бит
~(1<<3) 0b11110
111
tmp & (~(1<<3)) 0b10011
011 & 0b11110
111
результат 0b10010
011
Обнулить несколько битов можно так
Используя составной оператор присваивания &= ,можно записать выражение более компактно
Как проверить установлен ли бит в переменной?
Нужно обнулить все биты, кроме проверочного, а потом сравнить полученное значение с нулем
// блок будет выполняться, только если установлен
}
// блок будет выполняться, только если не установлен
// второй бит переменной tmp
}_______________побитовое исключающее ИЛИ ^ _______________
tmp = 155;
tmp = tmp ^ 8; // инвертируем четвертый бит переменой tmp
^
8 0b00001
000
147 0b10010
011
^
8 0b0000
1
000
155 0b10011
011
tmp = var1;
var1 = var2;
var2 = tmp;
var 2 ^= var 1;
var 1 ^= var 2;________________Директива #define__________________
Пример:
#define
PORT_BUTTON PORTA
#define
PIN_BUTTON PINA
#define
DDRX_BUTTON DDRA
#define
DOWN 3
#define
CANCEL 4
#define
UP 5
#define
ENTER 6
{
//конфигурируем порт на вход,
//и включаем подтягивающие резисторы
DDRX_BUTTON = 0;
PORT_BUTTON = 0xff;
tmp = PORTB & MASK_BUTTONS;
ADC_OFF();
OCR0 = 0x7d
INIT_Timer();
…
SetBit(PORTB, 0); //установить нулевой бит порта B
InvBit(tmp,6); //инвертировать шестой бит переменной tmp
if
(BitIsClear(PIND, 0)) { //если очищен нулевой бит в регистре PIND
….. //выполнить блок
}
tmp = SQUARE(my_var);
даст корректный результат.
tmp = SQUARE(my_var +1);
даст корректный результат, потому что препроцессор заменит эту строчку на
tmp = ((my_var + 1) * (my_var +1));