Передача порта 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. Для взятия адреса регистра используется оператор &. Посмотрим, как можно использовать эти макросы.


//определили вывод мк
#define BUT_PIN 3
#define BUT_PORT PORTB
.
// конфигурируем вывод как вход
DirReg(&BUT_PORT) &= ~(1<//включаем подтягивающий резистор
PortReg(&BUT_PORT) |= (1<
.
//проверяем нажата ли кнопка
if(! (PinReg(&BUT_PORT)&(1 <.
>

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

Компилятор преобразует эти макросы в очень компактный код. Точно такой же, как если бы мы обращались к регистрам используя их имена. И дело в том, что с точки зрения ассемблера это так и есть. Если вы посмотрите ассемблерный код этих примеров, то увидите, что обращение к регистрам осуществляется методом прямой адресации с помощью команд IN, OUT. Поэтому я и озаглавил этот раздел «ненастоящая работа с портом через указатели». Указатели вроде как используются, но на самом деле нет.
Такой подход можно использовать не со всеми микроконтроллерами 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 <>
>

//установить на выходе 1
void OUT_Set(outputs_t *out)
(*(out->portReg)) |= (1 pin);
>

//установить на выходе 0
void OUT_Clear(outputs_t *out)
(*(out->portReg)) &= ~(1 pin);
>

//определили вывод мк
#define OUT1_PIN 4
#define OUT1_PORT PORTB
.
//объявляем переменную для хранения
//настроек пина
outputs_t out1;

//инициализируем ее
OUT_Init(&out1, OUT1_PIN, OUT1_PORT, 0);

//устанавливаем 1 на выводе OUT1_PIN
OUT_Set(&out1);

Еще один пример работы с портом через указатели есть в коде к статье » сенсорная кнопка на микроконтроллере «.

Передача порта АВР как параметра в функцию

uchet-jkh.ru

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

Для работы с портами на микроконтроллерах AVR используется регистр DDRx, который позволяет установить режим ввода-вывода для каждого пина порта. При использовании функций, передача порта в качестве параметра позволяет выполнять однотипные операции с разными портами, делая код более универсальным и масштабируемым.

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

void setPortDirection(volatile uint8_t *port)

В данном примере функция setPortDirection принимает указатель на регистр порта в качестве параметра и устанавливает все пины порта в режим вывода, установив значение регистра в 0xFF. Обратите внимание на использование ключевого слова volatile перед типом данных регистра, что гарантирует, что значение регистра будет обновлено даже при оптимизации компилятором.

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

Как использовать порт avr в качестве параметра

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

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

Вот пример простой функции, которая принимает порт AVR в качестве параметра:

void readPort(volatile uint8_t* port)

uint8_t value = *port; // читаем значение порта

// делаем что-то с полученным значением

>

Здесь мы определили функцию `readPort`, которая принимает указатель на тип данных `volatile uint8_t`. Тип данных `volatile uint8_t` означает, что это указатель на 8-битное значение, которое может изменяться внешними факторами и должно быть обрабатываемым с высокой степенью точности.

Внутри функции мы можем использовать указатель `port` для доступа к значению порта. Например, мы можем прочитать текущее значение порта, используя операцию разыменования указателя `*port`. Затем мы можем выполнить какие-либо операции с полученным значением.

Вот пример использования функции `readPort`:

int main()

// определение порта

volatile uint8_t* myPort = &PORTB;

// вызов функции с передачей порта в качестве аргумента

readPort(myPort);

return 0;

>

В этом примере мы определили переменную `myPort`, которая является указателем на `PORTB` – один из портов микроконтроллера AVR. Затем мы передаем эту переменную в функцию `readPort`. Внутри функции `readPort` мы можем работать с этим портом, прочитав значение и выполнив какие-либо операции с ним.

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

Шаг 1: Понимание функций и параметров

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

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

Например, представим функцию, которая складывает два числа:

function addNumbers(a, b)

return a + b;

>

В данном примере функция addNumbers имеет два параметра a и b. Когда функция вызывается, значения, которые передаются в качестве аргументов, будут использоваться вместо соответствующих параметров. Например, при вызове функции addNumbers(2, 3) результатом будет 5, так как значения 2 и 3 передаются в параметры a и b соответственно.

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

Шаг 2: Изучение порта avr

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

Основные регистры порта avr:

  • DDR – регистр направления
  • PORT – регистр значений выводов
  • PIN – регистр чтения значений выводов

Регистр направления (DDR) позволяет настроить каждый вывод порта на вход или на выход.

Регистр значений выводов (PORT) определяет логическое значение на каждом выводе порта. Если вывод настроен на выход, то изменение значения регистра PORT приведет к установке или сбросу соответствующего вывода.

Регистр чтения значений выводов (PIN) позволяет считать состояние выводов порта. Если вывод настроен на вход, то с помощью этого регистра можно получить текущее состояние вводимого сигнала.

Для управления выводами используются две наиболее важные функции:

  • pinMode – функция для настройки направления вывода (вход или выход)
  • digitalWrite – функция для установки или сброса логического значения вывода

Теперь, когда вы понимаете основы работы с портом avr, вы готовы перейти к объявлению и передаче порта avr в качестве параметра в функцию.

Шаг 3: Определение функции с параметром порта AVR

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

Для определения функции с параметром порта AVR, вы должны указать тип данных параметра в заголовке функции. В данном случае тип данных будет указан как unsigned char, поскольку порт AVR может принимать значения от 0 до 255.

Вот пример определения функции с параметром порта AVR:

void blink_led(unsigned char port)

// Ваш код для мигания светодиодом или управления портом AVR

>

В этом примере функция blink_led принимает параметр port типа unsigned char. Внутри функции вы можете использовать переданный порт для мигания светодиодом или для управления другими компонентами, подключенными к порту AVR.

Шаг 4: Пример использования функции с портом avr

Теперь, когда у нас есть функция, которая принимает порт avr в качестве параметра, давайте рассмотрим пример его использования. Предположим, у нас есть плата Arduino, и мы хотим установить определенное значение на порт C.

Вначале мы объявляем переменную типа volatile uint8_t, которая будет содержать значение порта C. Затем мы вызываем функцию setPortValue(), передавая ей адрес переменной порта C в качестве параметра:

#include

// Функция установки значения порта

void setPortValue(volatile uint8_t *port, uint8_t value)

*port = value;

>

int main()

// Объявляем переменную порта C

volatile uint8_t *portC = &PORTC;

// Устанавливаем значение порта C

setPortValue(portC, 0xFF);

// .

// Остальной код программы

// .

return 0;

>

В этом примере мы объявляем переменную portC как указатель на volatile uint8_t. Затем мы передаем этот указатель в функцию setPortValue(), чтобы установить значение порта C равным 0xFF.

Обратите внимание, что мы используем оператор разыменования * для изменения значения, на которое указывает указатель port. Это позволяет нам установить значение порта C через указатель, переданный функции.

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

Оформление кода. Функции

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

Для этого существует много способов оформления кода. И один из них — функции. Функции в языке C/C++ используются для того, чтобы объединить какой-то участок кода, который особенно может в любой момент повторяться, или который представляет собой какую-то логически-законченную процедуру в отдельный фрагмент и может быть в любой момент вызван из какого-то другого участка кода.

Как пишутся функции — мы в принципе знаем с самого первого занятия, но до сих пор мы использовали только одну функцию. Это была функция main(), которая является точкой входа в нашу программу. Всё выполнение кода начинается именно с этой функции. А в дальнейшем мы будем использовать очень много различных функций. Поэтому, если кто-то что-то по функциям не поймёт, то в процессе наших дальнейших занятий он обязательно возместит данный пробел за счёт огромной практики использования функций.

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

Сегодня мы попытаемся написать такую функцию, которая благодаря входному аргументу, будет отображать на индикаторе определённую цифру.

Сегодня я уже не буду рассказывать, как создавать проект, я думаю, это уже всем надоело. Запустим уже созданный заранее из кода прошлого занятия и целиком настроенный проект и начнём писать нашу функцию. Напишем мы её до фунции main(), так как интерпретатор команд «смотрит» код сверху вниз и наша функция будет поэтому уже ему «знакома» в коде функции main(), и будет доступна для вызова в коде (или как говорят «видна»). Можно также писать функции и после того места, откуда они вызываются, но тогда нужно будет уже писать прототип функции. С прототипами функций мы познакомимся в более поздних занятиях. А сегодня напишем функцию сразу после объявления глобальных переменных. Глобальные переменные «видны» во всех функциях файла. Вот наша функция — пока без кода, с пустым телом. Дадим ей имя segchar. Имя функции должно говорить тому, кто будет читать код о том, чем данная функция будет заниматься. В нашем случае функция будет с помощью сегментов (seg) определённые символы (char). Вот поэтому и такое название

void segchar ( unsigned char seg )

У данной функции будет только один входной аргумент — это целочисленное короткое значение с именем seg. Возвращать функция ничего не будет, о чём свидетельствует значение void перед её именем, то есть мы будем «верить» функции, что она справится с выводом символа на экран, и проверять это не будем.

Также чтобы функция «понимала», какой именно зажигать символ, нам желательно изучить ещё один оператор — это оператор switch, который в зависимости от значения аргумента выполняет определённый участок кода. Это непростой оператор. Для этих целей конечно подойдёт и оператор if, но когда вариантов слишком много (цифр ведь целых 10), то удобнее всё-таки применить оператор ветвления switch. Вот что это за операор

image00

Я думаю, несмотря на сложность данного оператора, мы с ним непременно разберёмся в процессе многочисленного его использования. Мастерство приходит с практикой.

Давайте применим данный оператор в нашей функции

void segchar ( unsigned char seg )

switch ( seg )

case 1: PORTD = 0b11111001; break ;

case 2: PORTD = 0b10100100; break ;

case 3: PORTD = 0b10110000; break ;

case 4: PORTD = 0b10011001; break ;

case 5: PORTD = 0b10010010; break ;

case 6: PORTD = 0b10000010; break ;

case 7: PORTD = 0b11111000; break ;

case 8: PORTD = 0b10000000; break ;

case 9: PORTD = 0b10010000; break ;

case 0: PORTD = 0b11000000; break ;

Давайте немного разберёмся. Здесь у нас в зависимости от значения переменной seg мы будем попадать в определённый участок кода, и будем его выполнять, пока не встретится оператор break. Как только он попадётся, то мы из тела оператора switch выйдем, ну и так как за закрывающей тело фигурной скобкой следует следующая скобка, то мы вернёмся из функции в то место, откуда мы данную функцию вызвали. Ну конечно попадать, возвращаться, выходить будем не мы, это так образно говорится. Заниматься этим будет АЛУ. Мы просто представляем себя в роли АЛУ.

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

Перейдём в функцию main() и удалим оттуда ненужные нам комментарии

PORTB = 0b00000001; // 1 2 3 4 5 6 7 8

Перед тем, как вызвать нашу функцию в бесконечном цикле, мы организуем цикл, который будет перебирать подряд цифры от 0 до 9. Код скопируем из закомментированного участка бесконечного цикла, переместив его до обработчика кнопки, а комментарий потом уберем

segchar ( i );

_delay_ms (500);

if (!( PINB &0b00000001))

Код кнопки пока не трогаем, кнопку эту мы сегодня ещё вспомним.

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

Но это ещё не всё. у нас без дела простаивает кнопка. Давайте и ей дадим какую-нибудь работу. Здесь уже одной переменной для кнопки не обойдёмся, так как нам нужен будет флаг состояния кнопки. Добавим переменную

unsigned butcount =0, butstate =0;

Также нам нужен будет ещё один цикл, но не бесконечный, как я его назвал неправильно в видеоверсии, а условный, то есть тот, который выполняется, пока выполняется условие в скобках. Данный цикл мы вставим в тело цикла for. Условием будет определённое значения флага состояния кнопки. А в тело данного цикла мы вставим весь код отслеживания состояния кнопки, который мы написали на позапрошлом занятии, когда занимались с кнопкой. Также поубираем из данного кода управление состоянием порта D, а вместо этого, если кнопка будет нажата, то мы будем обнулять счётчик. Также в обоих случаях, то есть и в случае нажатой кнопке, и в случае отжатой, мы будем устанавливать статус кнопки в единицу, чтобы выйти из нашего цикла. А после того как мы выйдем из данного цикла, мы будем вызывать функцию отображения нашей цифры, затем вызывать задержку, а затем сбрасывать статус кнопки в ноль, чтобы в следующем цикле оператора for заново попадать в цикл с обработчиком кнопки. Вот такой вот у нас теперь получится код в бесконечном цикле

while ( butstate ==0)

Передача порта avr как параметра в функцию

AVR-port-sticker

Меня часто спрашивают, как управлять ножками GPIO микроконтроллера AVR, и как читать их состояние. Несмотря на то, что это довольно просто, описано не только в даташите, но и во многих статьях, например [1], поток вопросов не уменьшается.

Все порты AVR (под AVR обычно подразумеваются микроконтроллеры популярных серий megaAVR и tinyAVR компании Atmel, например ATmega32A, примененный в макетной плате AVR-USB-MEGA16) обладают функционалом чтение-модификация-запись (read-modify-write) при работе с выводами микроконтроллера как обычными портами ввода вывода (general purpose I/O ports, GPIO). При этом можно поменять направление (задать что это — вход или выход) для каждой отдельной ножки GPIO (вывода порта AVR). Каждая ножка также имеет симметричный выходной буфер (два CMOS-ключа, один на +, другой на -), который может выдавать выходной ток от плюса питания (VCC, обычно +5V, drive source), или от земли (GND, минус источника питания, sink source). Мощность выходного драйвера более чем достаточна для прямого управления светодиодом (LED). Также для всех выводов портов, настроенных как вход, можно селективно подключить внутренний верхний нагрузочный резистор (pull-up), встроенный прямо в кристалл микроконтроллера. Все выводы имеют защищающие от перенапряжения диоды, подключенные к VCC и GND.

AVR-input-simply-sch

Упрощенная схема порта AVR, настроенного как вход (состояние по умолчанию, DDRxn == 0).

AVR-output-simply-sch

Упрощенная схема порта AVR, настроенного как выход (DDRxn == 1).

Примечания к рисункам:

Pxn — имя ножки порта микроконтроллера, где x буква порта (A, B, C или D), n номер разряда порта (7 .. 0).
Cpin — паразитная емкость порта.
VCC — напряжение питания.
Rpu — отключаемый нагрузочный верхний резистор (pull-up).
PORTxn — бит n регистра PORTx.
PINxn — бит n регистра PINx.
DDRxn — бит n регистра DDRx.

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

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

Регистр DDRx выбирает направление работы каждой отдельной ножки порта. Если в разряд регистра DDRx записана лог. 1, то соответствующая ножка будет сконфигурирована как выход. Ноль означает, что порт сконфигурирован как вход (состояние по умолчанию, которое устанавливается после сброса или включения питания). Если в разряд DDRx записан 0, и в соответствующий разряд PORTx записана 1, то порт не только сконфигурирован как вход, но к нему также еще и подключен внутренний верхний нагрузочный резистор (input pull-up). Если в разряд DDRx записан 0, и в соответствующий разряд PORTx также записан 0, то порт сконфигурирован как вход с высоким входным сопротивлением, что соответствует отключенному выходному состоянию (третье состояние), при этом можно для искусственного создания логических уровней подключать внешние нагрузочные резисторы (pull-up верхний на VCC или pull-down нижний на GND).

Если в разряд PORTx записана лог. 1, и в соответствующий разряд DDRx записана лог. 1, то порт сконфигурирован как выход, и на выходе будет лог. 1. Если в разряд PORTx записана лог. 0, и в соответствующий разряд DDRx записана лог. 1, то порт сконфигурирован как выход, и на выходе будет лог. 0. Т. е. биты PORTx управляют состоянием выходного порта, при условии что в соответствующий порту разряд DDRx записана лог. 1.

AVR-General-Digital-IO

Логическая схема организации порта ввода/вывода (GPIO) микроконтроллера ATmega32A.

[Предварительная настройка проекта для доступа к GPIO]

Теперь перейдем к языку C — как можно управлять ножками микроконтроллера AVR? Когда Вы используете WinAVR GCC (или соответствующий GCC-тулчейн от Atmel, который устанавливается вместе с AVR Studio или Atmel Studio), то нужно правильно настроить проект, чтобы нормально распознавались имена регистров AVR (имена DDRA, DDRB, PORTA, PORTB, PINA, PINB и другие), и преобразовывались в нужные адреса. Для этого имеется специальный файл заголовка io.h, который находится в папках инсталляции WinAVR (к примеру, это может быть папка c: \ WinAVR-20100110 \ avr \ include \ avr ) или соответствующего тулчейна Atmel (для Atmel Studio это может быть папка c: \ Program Files \ Atmel \ Atmel Studio 6.0 \ extensions \ Atmel \ AVRGCC \ 3.3.2.31 \ AVRToolchain \ avr \ include \ avr ). Для того, чтобы подключить заголовок io.h, нужно в код модуля (например файл main.c) добавить строку с директивой #include, и в настройках проекта правильно указать тип микроконтроллера. Вот как указывается директива #include:

#include < avr/io.h > 

Имя (модель) процессора AVR может быть указано либо прямо в файле Makefile определением переменной DEVICE, или просто в настройках проекта (AVR Studio или Atmel Studio). Вот пример куска Makefile, где задан тип микроконтроллера ATmega32:

F_CPU = 12000000 DEVICE = atmega32 BOOTLOADER_ADDRESS = 0x7000 ..

Вот так настраивается тип микроконтроллера в свойствах проекта AVR Studio 4.19 (меню Project -> Configuration Options):

AVR-Studio419-select-device

Вот так настраивается тип микроконтроллера в свойствах проекта Atmel Studio 6.0 (меню Project -> Properties):

Atmel-Studio60-select-device

После того, как подключен файл io.h и задан тип микроконтроллера для проекта, можно в коде программы на языке C использовать имена регистров AVR. Через имена регистров осуществляется доступ к портам GPIO микроконтроллера.

[Как работать с портами AVR как с выходами]

DDRD = 0xFF; //настройка всех выводов порта D как выходов PORTD = 0x0F; //вывод в разряды порта D значений 00001111 

Если у нас есть 8-битная переменная i, то мы можем присвоить её значение регистру PORTx, и тем самым установить ножки микроконтроллера в состояние, соответствующее значению переменной i:

1 2 3
uint8_t i=0x54; 
PORTD = i;

Здесь показана работа с портом D, но Вы точно так же можете работать и с портами A, B, C, если будете использовать соответствующие имена регистров (DDRA, PORTA, DDRB, PORTB и т. п.).

[Как работать с портами AVR как со входами]

Вот как можно прочитать логические уровни из порта D в переменную i:

DDRD = 0; //настройка всех выводов порта D как входов i = PIND; //прочитать все 8 ножек порта D и сохранить значение в i 

[Как работать с отдельными ножками порта AVR как с выходами]

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

Некоторые из выводов 8-разрядного порта могут быть сконфигурированы и работать как выходы, причем остальные выводы порта могут работать как входы. Т. е. разные разряды одного порта могут выполнять разные функции в зависимости от потребностей пользователя.

Например, нам нужно, чтобы у порта D разряды 0, 2, 4, 6 (PD0, PD2, PD4, PD6) работали как входы (input), и разряды 1, 3, 5, 7 (PD1, PD3, PD5, PD7) работали как выходы (output). Тогда мы можем использовать код наподобие следующего:

1 2 3 4 5 6
DDRD=0; // Сброс всех битов в 0 (00000000). DDRD |= (1  1)|(1  3)|(1  5)|(1  7); // Использование операции сдвига // битов // чтобы установить биты 1, 3, 5, 7 // в лог. 1. Остальные разряды // останутся в состоянии лог. 0 

Теперь мы можем вывести любое значение в разряды ножек порта (выходы) 1, 3, 5 и 7. Вот как можно установить эти разряды в лог. 1:

PORTD |= (1  1)|(1  3)|(1  5)|(1  7);

А так можно сбросить эти ножки в лог. 0:

PORTD &= ~((1  1)|(1  3)|(1  5)|(1  7));

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

1 2 3 4
PORTD |= (1  PD5)|(1  PD7); // установка битов 5 и 7 PORTD &= ~((1  PD5)|(1  PD7)); // сброс битов 5 и 7 PORTD |= (1  PD1); // установка бита 1 PORTD &= ~ (1  PD1); // сброс бита 1 

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

AVR-LED-connect-example

1 2 3 4 5 6
#define LED PD2 #define LED_ON() PORTD |= (1 #define LED_OFF() PORTD &= ~(1 
LED_ON(); //Зажечь светодиод LED_OFF(); //Погасить светодиод

Здесь макросы LED_ON и LED_OFF заданы для удобного управление одним разрядом порта D. Они позволяют включать и выключать светодиод, подключенный к ножке порта PD2, и делают программу наглядной и простой.

Для составления масок есть также удобные макросы наподобие _BV(n).

[Чтение отдельных разрядов порта AVR]

Чтение отдельных разрядов порта (ножек микроконтроллера) AVR также осуществляется очень просто. К примеру, настройте отдельные ножки (разряды 1 и 3) порта D как входы, и прочитайте их состояние в переменную:

DDRD &=~ ((1  1)|(1  3)); // Очистка битов 1 и 3 регистра направления порта D. i = PIND; // Прочитать состояние всех 8 ножек порта D. 

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

1 2 3 4 5 6 7 8 9 10
if ((1  PD3) & i) < //Здесь можно выполнить некоторые действия, если PD3==1 .. > else < //Здесь можно выполнить некоторые действия, если PD3==0 .. >

Точно так же можно узнать состояние ножки PD1, если наложить на i маску (1<

1 2 3 4 5 6 7 8 9 10
if ((1  PD1) & i) < //Здесь можно выполнить некоторые действия, если PD1==1 .. > else < //Здесь можно выполнить некоторые действия, если PD1==0 .. >

Второй способ — сдвинуть i вправо нужное число раз (для нашего примера 1 или 3 раза), и затем проверить значение младшего разряда i.

1 2 3 4 5 6 7 8 9 10 11
i >> =3; if (1 & i) < //Здесь можно выполнить некоторые действия, если PD3==1 .. > else < //Здесь можно выполнить некоторые действия, если PD3==0 .. >

Есть также удобные библиотечные макросы наподобие bit_is_set() или bit_is_clear(), имеющихся в файле sfr_defs.h, которые упрощают задачу проверки отдельных бит.

[Бит PUD]

Имеется также специальная возможность отключения всех внутренних нагрузочных резисторов AVR на всех портах сразу, если установить бит PUD (аббревиатура расшифровывается Pull-Up Disable) в регистре SFIOR (Special Function I/O Register). По умолчанию (после сброса или включения питания) этот бит сброшен, и не оказывает влияние на настройку нагрузочных резисторов.

AVR-SFIOR

Регистр SFIOR и размещение в нем бита PUD.

Обычно бит PUD не используют (он остается сброшенным) и настраивают подключением pull-up резисторов только через регистры DDRx. В таблице показано, как влияет на настройку порта бит PUD и биты PORTx, DDRx.

DDxn PORTxn PUD
(SFIOR)
I/O pull-up Пояснение
0 0 X Вход Нет Третье состояние (отключено, Hi-Z).
0 1 0 Вход Да Через порт Pxn будет течь ток через нагрузочный резистор, если на землю подключена внешняя цепь (нагрузка).
0 1 1 Вход Нет Третье состояние (отключено, Hi-Z).
1 0 X Выход Нет Выход, замкнутый на землю (открыт нижний ключ буфера, верхний ключ закрыт).
1 1 X Выход Нет Выход, замкнутый на плюс питания VCC (открыт верхний ключ буфера, нижний ключ закрыт).

[Практический пример работы с портами GPIO макетной платы AVR-USB-MEGA16]

Микроконтроллер может управлять электрическими сигналами (зажигать светодиоды, пищать динамиком, включать реле), получать сигналы из внешнего мира (например, нажатия кнопок, сигналы с датчиков). Для этого как раз и используются порты GPIO. В этом разделе приведен простейший для понимания пример таких подключений. К макетной плате AVR-USB-MEGA16 подключена кнопка к порту PB3, которую будет читать программа. Светодиод, подключенный к PB0, установлен на макетной плате.

avr-usb-mega16-GPIO-using-example

Полный пример кода main.c, который иллюстрирует работу с портами микроконтроллера ATmega32A макетной платы AVR-USB-MEGA16:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
#include "avr\io.h" #include "avr\iom8.h" 
int main(void) < DDRB &= ~_BV(3); //Сброс разряда 0 регистра DDRB в лог. 0 (PB3 // настроен как вход). Этот порт используется // для чтения состояния кнопки. Кнопка подключена // к ножке порта PB3 и к земле. PORTB |= _BV(3); //Разрешить нагрузочный резистор на PB3. DDRB |= _BV(0); //Установка разряда 0 регистра DDRB в лог. 1 // (PB0 настроен как выход). PORTB &= ~_BV(0); //Погасить светодиод, подключенный к PB0. while(1) < if (bit_is_clear(PINB, 3)) //Кнопка нажата? < //Да, кнопка нажата! PORTB |= _BV(0); //Зажечь светодиод. loop_until_bit_is_set(PINB, 3); //Светодиод будет гореть, // пока кнопка нажата. PORTB &= ~_BV(0); //Погасить светодиод. > > >

[Как управлять портами микроконтроллера по USB]

Мы уже разобрались, как программа микроконтроллера сама может управлять выводами портов — устанавливать в лог. 0 и лог. 1 (что можно использовать для управления лампочками, реле и другими периферийными устройствами), читать их состояние (можно использовать для принятия нажатия от кнопок, снятия информации с датчиков и различать другие события окружающего мира). Но как заставить компьютер передать команду через USB, чтобы микроконтроллер поменял состояние своей ножки/порта, или чтобы можно было считать в компьютер данные с GPIO, аналого-цифрового преобразователя, SPI или любого другого периферийного устройства микроконтроллера?

Ответ очевиден — нужно просто научиться наладить обмен данными между микроконтроллером и компьютером через USB. Если такой обмен будет налажен, то дальше уже дело техники — микроконтроллер сможет интерпретировать переданный ему байт как команду выполнить определенное действие (например, зажечь или погасить светодиод). И наоборот, микроконтроллер сможет передавать через USB какие-то данные, которые программа компьютера будет принимать и распознавать определенным образом.

Итак, вся загвоздка в том, как наладить обмен между микроконтроллером и компьютером через USB. Вопрос выбора практического решения этой задачи может оказаться весьма непрост — из-за того, что на рынке имеется очень много разных микроконтроллеров, и имеется очень много вариантов программных библиотек для обмена данными между устройствами USB. Но если уже имеются какие-то предпочтения, или выбрана модель микроконтроллера или макетная плата, то выбрать уже проще. Здесь я кратко остановлюсь на возможных решениях для обмена данными по USB для макетных плат AVR-USB-MEGA16 и AVR-USB162. Далее для простоты программу для микроконтроллера буду называть firmware (эта программа будет работать как устройство USB HID или USB CDC), а программу на компьютере, которая обменивается данными с устройством USB, буду называть ПО хоста.

AVR-USB-MEGA16, работающая в качестве устройства USB

На этой макетной плате установлен микроконтроллер ATmega32A. Он не имеет на кристалле специально выделенного аппаратного контроллера USB. Поэтому протокол USB обрабатывается программно. В этом случае firmware микроконтроллера строится на основе популярной, многократно испытанной на деле библиотеке V-USB.

Написать firmware устройства USB HID для микроконтроллера на основе V-USB довольно просто, так имеется много хорошо документированных примеров таких устройств с открытым исходным кодом (как в составе самой библиотеки, так и в Интернете). Для ПО хоста V-USB предлагает примеры, написанные с помощью кроссплатформенной (Windows, Mac, Lunux) библиотеки LibUSB.

Для AVR-USB-MEGA16 есть также два готовых решения — [3] и [4], специально предназначенные для доступа (через USB из ПО хоста) не только к GPIO микроконтроллера, но и к его регистрам на чтение и запись. Благодаря этому можно получить полное управления над всеми портами микроконтроллера и его периферийными устройствами — можно управлять ножками и читать их значение, можно читать данные с аналого-цифрового преобразователя, можно пользоваться интерфейсом SPI, UART и проч.

[3] представляет из себя устройство класса USB HID, для которого не нужен драйвер. Однако из-за того, что для ПО хоста используется LibUSB, необходимо на компьютере установить так называемый драйвер фильтра — программную прослойку между LibUSB и периферийным устройством USB.

[4] — это устройство класса USB CDC (виртуальный COM-порт). ПО хоста, работающее с виртуальным COM-портом, можно написать довольно просто, так как есть много примеров программ и библиотек, которые передают и принимают данные через COM-порт. [4] организовано так, что содержит текстовый интерфейс команд, с помощью которых Вы обычной консоли (putty, TerraTerm, HyperTerminal и т. п.) можете управлять ножками микроконтроллера, читать их значение, можете получить доступ ко всем регистрам AVR. [5] содержит готовый пример (исходный код и скомпилированные бинарники, а также драйвер) firmware USB CDC, портированного на макетную плату AVR-USB-MEGA16.

Наладив обмен данными с AVR-USB-MEGA16 с помощью [3, 4, 5], Вы легко сможете управлять портами микроконтроллера через USB.

AVR-USB162, работающая в качестве устройства USB

Макетная плата AVR-USB162 (или её малогабаритный вариант AVR-USB162MU) выполнена на микроконтроллере AT90USB162. Этот микроконтроллер имеет в своем составе для работы с USB специальный аппаратный интерфейс, и для firmware используются уже другие библиотеки. Самые распространенные — LUFA [6] и библиотека Atmel для устройств AVR USB Series2 [7]. С помощью этих библиотек Вы легко сами можете создать собственное устройство USB HID или USB CDC.

Для ПО хоста USB HID можно использовать не только LibUSB, но и многие другие популярные библиотеки [8]. Наладив обмен данными с AVR-USB162 с помощью [7, 8], Вы легко сможете управлять портами микроконтроллера через USB.

[Ссылки]

Добавить комментарий

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