Шаг 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
Обсудить на форуме