Шаг 2.5. Указательный

Едем дальше: помимо того, что код должен быть понятен, так программист должен ещё предусмотреть, что милый заказчик вдруг - рааааз! - и изменит начальные условия. Вот, например, есть у нас программка, показывающая световую дорожку. А тут приходит Он и - хоп! - «Хочу», - говорит: «чтобы кнопка была на 5 выводе, да и вообще давайте-ка меняйте местами её порт и порт лампочек ваших, хочется мне так!»
И ведь не поспоришь…

С конкретным выводом мы вроде уже разобрались - среда MikroE позволяет обращаться к конкретному биту порта (о том, что делать в классических языках, мы поговорим потом).

Теперь разберемся с портами, а точнее - как же на них ссылаться: сперва рассмотрим, опять же, возможности MikroE.

В MikroE есть ключевое слово at, которое позволяет создать "ссылку" на переменную - так называемый «алиас» - прозвище, синоним (он же лысый, он же горбатый :). Так, например, написав

unsigned char ddrLED_Road at DDRB;

Мы сможем в дальнейшем везде использовать ddrLED_Road вместо "DDRB"! При этом обратите внимание, что ключевое слово at используется только с глобальными переменными. Впрочем, в больших программах практикуется глобальное объявление констант и переменных, связанных с местоположением какой-либо периферии (выводов кнопок, датчиков, дисплеев и т.д.).

Посмотрим, как это будет выглядеть на Си:

typedef unsigned char byte;
//определяем ссылки на регистры ввода-вывода
byte ddrLED_Road at DDRB;
byte portLED_Road at PORTB;
byte ddrButton at DDRD;
byte portButton at PORTD;
byte pinButton at PIND;

//"Бегающая" световая дорожка
void main()
{
  //переменные и константы
  const byte ddrLED_RoadInit = 0xFF;                                            //настройка порта со светодиодами - на выход и
  const byte portLED_RoadInit = 0xFC;                                           //два светодиода включены (на ноль)
  const byte ddrButtonInit = 0;                                                 //настройка порта с кнопкой - кнопка на вход с подтяжкой
  const byte portButtonInit = 0x1;
  const byte ButtonOutputNumber = 0;                                            //номер вывода для кнопки
  const byte DelayTimeMs = 100;                                                 //время задержки дорожки в одном состоянии в мс

  //инициализация портов
  ddrLED_Road = ddrLED_RoadInit;
  portLED_Road = portLED_RoadInit;
  ddrButton = ddrButtonInit;
  portButton = portButtonInit;

  while(1)
  {
    if(pinButton.ButtonOutputNumber == 0)                                       //если кнопка нажата
    {
      portLED_Road = (portLED_Road >> 7) | (portLED_Road << 1);                 //циклический сдвиг на один бит влево
      Delay_ms(DelayTimeMs);                                                    //задержка
    }
  }
}

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

...
byte ddrButton at DDRD;
byte portButton at PORTD;
byte pinButton at PIND;
byte ddrSensor at DDRD;
byte portSensor at PORTD;
byte pinSensor at PIND;

void main()
{
  ... 
  const byte ddrButtonInit = 0;                                                 //настройка порта с кнопкой - кнопка на вход с подтяжкой
  const byte portButtonInit = 0x1;
  const byte ddrSensorInit = 0x02;                                                 //настройка порта с датчиком -вывод 1 на выход и к единице, вывод 2 на вход с подтяжкой
  const byte portSensorInit = 0x06;	
  ...
  ddrButton = ddrButtonInit;
  portButton = portButtonInit;
  ddrSensor = ddrButtonInit;
  portSensor = portButtonInit;
  ...

И что же мы получим? Да то, что при инициализации датчика "перетрётся" инициализация кнопки! Отсюда делаем вывод - инициализировать устройства также нужно конкретно побитово. И тут может быть два варианта решения: мы используем возможность MikroE обращаться к конкретным битам и и вместо "больших" констант для инициализации инициализируем конкретные биты. При этом, если под одно устройство задействовано несколько ножек, нужно будет инициализировать каждую из них отдельно. Вот так это будет выглядеть:

...
  const byte ButtonPinNumber = 0;                                               //номер вывода для кнопки
  const byte SensorOutPinNumber = 1;                                            //номера выводов (выход и вход) для датчика
  const byte SensorInPinNumber = 2;
...
  ddrButton.ButtonPinNumber = 0;
  portButton.ButtonPinNumber = 1;
  ddrSensor.SensorOutPinNumber = 1;
  portSensor.SensorOutPinNumber = 1;
  ddrSensor.SensorInPinNumber = 0;
  portSensor.SensorInPinNumber = 1;

Что ж, метод имеет право на жизнь. Но можно сделать "по-взрослому" и использовать номера выводов и маски:

...
  const byte ButtonPinMask = 0x01;                                              //кнопка на выводе 0
  const byte SensorOutMask = 0x02;                                              //выход для датчика на выводе 1
  const byte SensorInMask = 0x04;                                               //вход для датчика на выводе 2
...
  ddrButton &= ~ButtonPinMask;
  portButton |= ButtonPinMask;
  ddrSensor = (ddrSensor | SensorOutMask) & (~SensorInMask);
  portSensor |= SensorOutMask | SensorInMask;  

Примерно так и инициализируют порты в проектах на "чистом" Си. Рассмотрим теперь, как будет выглядеть наш проект с использованием масок:

typedef unsigned char byte;
//определяем ссылки на регистры ввода-вывода
byte ddrLED_Road at DDRB;
byte portLED_Road at PORTB;
byte ddrButton at DDRD;
byte portButton at PORTD;
byte pinButton at PIND;

//"Бегающая" световая дорожка
void main()
{
  //переменные и константы
  const byte ddrLED_RoadInit = 0xFF;                                            //настройка порта со светодиодами - на выход и
  const byte portLED_RoadInit = 0xFC;                                           //два светодиода включены (на ноль)
  const byte ButtonPinNumber = 0;                                               //номер вывода для кнопки
  const byte ButtonPinMask = 0x01;                                              //кнопка на выводе 0
  const byte DelayTimeMs = 100;                                                 //время задержки дорожки в одном состоянии в мс

  //инициализируем все порты ввода-вывода на вход без подтяжки
  DDRA = 0;
  PORTA = 0;
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  
  //инициализация занятых портов
  ddrLED_Road = ddrLED_RoadInit;
  portLED_Road = portLED_RoadInit;
  ddrButton &= ~ButtonPinMask;
  portButton |= ButtonPinMask;

  while(1)
  {
    if(pinButton.ButtonPinNumber == 0)                                          //если кнопка нажата
    {
      portLED_Road = (portLED_Road >> 7) | (portLED_Road << 1);                 //циклический сдвиг на один бит влево
      Delay_ms(DelayTimeMs);                                                    //задержка
    }
  }
}

Как видим, вначале мы ещё инициализируем все-все порты ввода-вывода на вход без подтяжки - именно в таком состоянии отъедается минимальное количество энергии (когда мы делаем какие-либо устройства "на батарейках", нам очень важно сократить потребление).
Также мы почти не изменили работу с портом с дорожкой из светодиодов - потому что занят весь порт.

Вот программа на Паскале:

program Road;
{ "Бегающая" световая дорожка }
const
  ddrLED_RoadInit = 0xFF;                                                       //настройка порта со светодиодами - на выход и
  portLED_RoadInit = 0xFC;                                                      //два светодиода включены (на ноль)
  ButtonPinNumber = 0;                                                          //номер вывода для кнопки
  ButtonPinMask = 0x01;                                                         //кнопка на выводе 0
  DelayTimeMs = 100;                                                            //время задержки дорожки в одном состоянии в мс

var
  //определяем ссылки на регистры ввода-вывода
  ddrLED_Road: byte at DDRB;
  portLED_Road: byte at PORTB;
  ddrButton: byte at DDRD;
  portButton: byte at PORTD;
  pinButton: byte at PIND;
begin
   //инициализируем все порты ввода-вывода на вход без подтяжки
   DDRA := 0;
   PORTA := 0;
   DDRB := 0;
   PORTB := 0;
   DDRD := 0;
   PORTD := 0;
   //инициализация занятых портов
   ddrLED_Road := ddrLED_RoadInit;
   portLED_Road := portLED_RoadInit;
   ddrButton := ddrButton and (not ButtonPinMask);
   portButton := portButton or ButtonPinMask;
   
   while(True) do
   begin
    if(pinButton.ButtonPinNumber = 0) then                                      //если кнопка нажата
    begin
      portLED_Road := (portLED_Road shr 7) or (portLED_Road shl 1);             //циклический сдвиг на один бит влево
      Delay_ms(DelayTimeMs);                                                    //задержка
    end;
   end;
end.

А теперь пару слов о том, как делают "отвязку" от конкретных портов ввода-вывода взрослые ˆˆ программисты в средах типа IAR Embedded Workbench.

В Си наиболее часто выводы микросхемы задаются с помощью директивы препроцессора define:

#define ddrLED_Road DDRB
#define portLED_Road PORTB
#define ddrButton DDRD
#define portButton PORTD
#define pinButton PIND

При этом нам достаточно переписать только те места, где мы использовали ссылки с кодовым словом at. Также заметьте, что при использовании define мы не пишем в конце ; - потому, что при компиляции препроцессор заменит первое "слово" после define оставшимся концом строки.

В Паскале же директива define отвечает за видимость константы, то есть здесь таким образом задать назначение выводов микроконтроллера не получится(

Следующий вариант - использование указателей.

Немножечко о памяти: представьте себе книгу – большую такую, толстую, можно с картинками) И есть в ней разделы, главы… Так вот, названия этих самых глав и прочего можно сравнить с названиями наших переменных, констант. Те же массивы – это так большие разделы, разбитые на элементики – подглавки, подраздельчики, у которых, кстати, как раз нет собственного названия, но есть номера: 1, 2, 3, …

И при этой всей красоте в книге есть просто нумерация страниц – этакие адреса, по которым и размещаются конкретные разделы и главы.

Так, тогда вспомним ещё о ссылках в книжках – «ооо, об этом Вы можете узнать на странице …» - примерно так и работают наши указатели. Мы можем сослаться и просто на страницу – адрес, и на конкретную главу или раздел (переменную, массив).

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

А вот и примеры, заодно и синтаксис посмотреть:

На Си:

int i;
int* pointer;	//наш указатель на тип int
i = 10;	
pointer = (int*)&i;	//положили в pointer адрес нашей переменной
*pointer = *pointer + 1;	//а вот и разыменование – теперь в i хранится не 10, а 11

А вот пример с безтиповым указателем:

int i;
void* pointer;	//указатель на «что-то»
i = 10;
pointer = (int*)&i;
(int*)pointer = (int*)pointer + 1;

На Паскале, соответственно:

var
	i : integer;
	pointerInt : ˆinteger;
begin
	i := 10;
	pointerInt := @i;
	pointerIntˆ := pointerIntˆ + 1;
end.

Таким образом, в i будет храниться уже 11.

С безтиповыми указателями именно среда MikroPascal, увы, работать как-то не хочет(

ОЧЕНЬ, ОЧЕНЬ ВАЖНОЕ ЗАМЕЧАНИЕ! При запихивании порта в указатель необходимо при объявлении этого самого указателя написать перед ним ключевое слово volatile. Этот квалификатор сообщает компилятору, что переменная может изменяться вне зависимости от самой программы - то есть, например, будет у нас указатель на PINx, а к выводу порта X будет присоединена кнопка - тогда значение по адресу в указатели будет меняться "само по себе", вне зависимости от кода в программе.

Дело в том, что при собирании программы компилятор может оптимизировать код - например, если у нас в цикле увеличивается на единицу какая-то переменная, а потом значение этой переменной нигде не используется, то компилятор просто возьмёт и "выкинет" строку с увеличением переменной на единицу. То же самое может произойти и с разыменовыванием нашего указателя - по коду вроде переменная не изменяется, значит, можно что-нибудь убрать или сократить! Оптимизация, кстати, настраивается во вкладке "Output settings" окна "Settings" (Tools->Settings).

Таким образом, используя ключевое слово volatile, мы сообщаем компилятору, что переменная может измениться в любой момент, поэтому он не может оптимизировать её.

Если использовать указатели, то получим следующие варианты наших программ:

typedef unsigned char byte;
//"Бегающая" световая дорожка
void main()
{
  //указатели на порты ввода-вывода
  volatile byte* ddrLED_Road = &DDRB;
  volatile byte* portLED_Road = &PORTB;
  volatile byte* ddrButton = &DDRD;
  volatile byte* portButton = &PORTD;
  volatile byte* pinButton = &PIND;
  //переменные и константы
  const byte ddrLED_RoadInit = 0xFF;                                            //настройка порта со светодиодами - на выход и
  const byte portLED_RoadInit = 0xFC;                                           //два светодиода включены (на ноль)
  const byte ButtonPinNumber = 0;                                               //номер вывода для кнопки
  const byte ButtonPinMask = 0x01;                                              //кнопка на выводе 0
  const byte DelayTimeMs = 100;                                                 //время задержки дорожки в одном состоянии в мс

  //инициализируем все порты ввода-вывода на вход без подтяжки
  DDRA = 0;
  PORTA = 0;
  DDRB = 0;
  PORTB = 0;
  DDRD = 0;
  PORTD = 0;
  
  //инициализация занятых портов
  *ddrLED_Road = ddrLED_RoadInit;
  *portLED_Road = portLED_RoadInit;
  *ddrButton &= ~ButtonPinMask;
  *portButton |= ButtonPinMask;

  while(1)
  {
    if((*pinButton).ButtonPinNumber == 0)                                       //если кнопка нажата
    {
      *portLED_Road = ((*portLED_Road) >> 7) | ((*portLED_Road) << 1);          //циклический сдвиг на один бит влево
      Delay_ms(DelayTimeMs);                                                    //задержка
    }
  }
}

И на Паскале:

program Road;
{ "Бегающая" световая дорожка }
const
  ddrLED_RoadInit = 0xFF;                                                       //настройка порта со светодиодами - на выход и
  portLED_RoadInit = 0xFC;                                                      //два светодиода включены (на ноль)
  ButtonPinNumber = 0;                                                          //номер вывода для кнопки
  ButtonPinMask = 0x01;                                                         //кнопка на выводе 0
  DelayTimeMs = 100;                                                            //время задержки дорожки в одном состоянии в мс

var
  //определяем ссылки на регистры ввода-вывода
  ddrLED_Road: ˆbyte; volatile;
  portLED_Road: ˆbyte; volatile;
  ddrButton: ˆbyte; volatile;
  portButton: ˆbyte; volatile;
  pinButton: ˆbyte; volatile;
begin
   //инициализируем все порты ввода-вывода на вход без подтяжки
   DDRA := 0;
   PORTA := 0;
   DDRB := 0;
   PORTB := 0;
   DDRD := 0;
   PORTD := 0;
   //инициализируем указатели
   ddrLED_Road := @DDRB;
   portLED_Road := @PORTB;
   ddrButton := @DDRD;
   portButton := @PORTB;
   pinButton := @PINB;
   //инициализация занятых портов
   ddrLED_Roadˆ := ddrLED_RoadInit;
   portLED_Roadˆ := portLED_RoadInit;
   ddrButtonˆ := ddrButtonˆ and (not ButtonPinMask);
   portButtonˆ := portButtonˆ or ButtonPinMask;
   
   while(True) do
   begin
    if(pinButtonˆ.ButtonPinNumber = 0) then                                     //если кнопка нажата
    begin
      portLED_Roadˆ := ((portLED_Roadˆ) shr 7) or ((portLED_Roadˆ) shl 1);      //циклический сдвиг на один бит влево
      Delay_ms(DelayTimeMs);                                                    //задержка
    end;
   end;
end.

У нас есть различие в использовании обращения к конкретному биту вместе с разыменовыванием указателя: в MikroC компилятор требует скобочки: "(*pinButton).ButtonPinNumber", а в MikroPascal наоборот, при установке скобочек компилятор ругается, и требует, чтобы было вот так: "pinButtonˆ.ButtonPinNumber".

Скажем сразу - работа с регистрами порта через указатель очень "тяжела" как с точки зрения быстродействия, так и с точки зрения размера кода; так что предпочтительней всё же использовать define-ы.

Автор - Moriam
Обсудить на форуме