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<<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);


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

У вас недостаточно прав для комментирования.