AVR. Работа с портами через указатели

17/08/2013 - 17:09 Павел Бобков

Ненастоящая работа с портом через указатель

   Любое устройство на микроконтроллере 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<<BUT_PIN);
//включаем подтягивающий резистор
PortReg(&BUT_PORT) |= (1<<BUT_PIN);

...
//проверяем нажата ли кнопка
if(! (PinReg(&BUT_PORT)&(1<<BUT_PIN)) ){
   ...
}


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

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

   //задаем логический уровень
   if (level) {
      (*port) |= (1<<pin);
   }
   else{
      (*port) &= ~(1<<pin);
   }
}

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

//установить на выходе 0
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);

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


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

Комментарии   

# Peter 18.08.2013 10:17
Видимо, и это кому-нибудь пригодится. Как косвенная адресация бит портов. Только тут даже при установке одного бита видится полновесные операции Чтение-Модифика ция-Запись со всеми вытекающими из-под них последствиями. Надо бы указать, что товарищи, об атомарности заботьтесь сами.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 18.08.2013 11:04
Добавил.
Ответить | Ответить с цитатой | Цитировать
# Владимир 18.08.2013 21:10
Давно об этом задумывался, да все никак не мог выкроить время почитать. А тут все лаконично и самодостаточно.
Уже даже есть идеи как использовать это в своих библиотеках.
Спасибо, Павел!
Ответить | Ответить с цитатой | Цитировать
# Pashgan 18.08.2013 21:14
Пожалуйста.
Ответить | Ответить с цитатой | Цитировать
# neiver 19.08.2013 14:09
Цитата:
Мы можем определить в проекте только один регистр, а к остальным обращаться вычисляя их адрес
.
К сожалению это не всегда так. Например порты E и F в AtMega128.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 19.08.2013 14:45
Там только регистры порта F разнесены.
Я это добавлю.
Ответить | Ответить с цитатой | Цитировать
# Stas 13.09.2013 19:09
Где бы почитать, желательно с примерами, вот про это " -> " ( out->pin = pin; ) ??? Единственное, что нарыл, что это обращение к члену структуры ...
Ответить | Ответить с цитатой | Цитировать
# Pashgan 13.09.2013 19:31
В любом учебнике по Си это написано. Например здесь - "Программирован ие на Си" Стефан Кочан. Это обращение к члену структуры с использованием указателя. Можно писать так (*out).pin, а можно так out->pin. Второй вариант удобнее.
Ответить | Ответить с цитатой | Цитировать
# Stas 14.09.2013 18:05
Поддерживает ли CodeVisionAVR вышеупомянутую конструкцию ??? (*out).pin, out->pin ???
Ответить | Ответить с цитатой | Цитировать
# Алексашка305 15.10.2015 09:00
В примере использования:
....
//инициализируем ее
OUT_Init(&out1, &OUT1_PIN, &OUT1_PORT, 0);
....
Ответить | Ответить с цитатой | Цитировать

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

Защитный код
Обновить