История вопроса
Имея опыт разработки программного обеспечения по Windows на С++, давно хотел использовать для встраиваемых систем достаточно мощный инструмент объектно-ориентированного языка программирования С++ – виртуальные методы (функции) и абстрактные классы. Естественно использование виртуальных методов и абстрактных классов должно быть логичным и рациональным (не для галочки). И вот такой случай подвернулся, потребовалось разработать устройство, к которому подключается шесть датчиков типа DS18B20, имеющих интерфейс 1Wire. Интерфейс 1Wire позволяет подключить все шесть датчиков на одну магистраль, однако из-за особенностей применения, такое решение было не рациональным и автор принял решение использовать топологию «точка-точка» т.е. независимое подключение каждого датчика. При такой постановке задачи использование виртуальных методов и абстрактных классов, позволило создать простую в использовании, и между тем гибкую, библиотеку для работы с 1Wire устройствами.
Немного теории
Для начала вспомним немного теории о классах, наследовании полиморфизме и других элементах объектно-ориентированного С++.
Класс представляет собой совокупность данных (переменных) и функций. Функции в классах называются методами. По сути метод — это та же функция, но работающая лишь с данными, хранящимися в классе, и данным, передаваемыми методу в качестве аргументов.
Наверняка у читателей возникает вопрос: «Зачем нужны классы, если можно определить переменные в программном модуле, определить в нем функции, при необходимости защиты данных, объявить их с квалификатором static, ограничив тем самым доступ к ним. И наоборот при необходимости доступа к данным другими программными модулями объявить данные с квалификатором extern ? ».
Классы имеют очень важные свойства, позволившие упростить разработку сложных программ, – наследование и полиморфизм. Вообще у классов множество специфических свойств, но в рамках данной статьи они не используются и поэтому не описываются, при необходимости их можно изучить в соответствующей литературе.
Наследование – это свойство классов, позволяющее на основе одного класса (называемого базовым или родительским) создать другой класс (называемый производным или наследником).
Наследование позволяет усложнять классы, например, мы имеем класс стандартного калькулятора, какой имеется в обычном Windows. Используя этот класс как базовый, мы создаем производный класс инженерного калькулятора. От базового класса у нас сохраняются методы ввода чисел, сложения, вычитания деления, отображения результата и пр. К базовому классу мы добавляем методы вычисления тригонометрических функций, логарифмов и др. Таким образом, используя наследование, нам не нужно переписывать весь класс калькулятора, мы просто добавляем методы дополнительных функций и при необходимости дополнительные данные. Кроме того, мы можем не иметь исходных кодов класса калькулятора, он может распространяться разработчиком в виде скомпилированного объектного файла и наследование в данном случае является единственным способом расширить функции калькулятора.
Полиморфизм – это свойство классов позволяющее переопределять методы класса при наследовании. Применительно к описанному выше калькулятору нам потребуется переопределить метод отображения диалогового окна калькулятора т.к. потребуется отображение дополнительных кнопок.
Если читатель совсем не знаком с классами, способами их определения, с конструкторами класса, областью видимости данных класса, то для большего понимания материалов статьи ему рекомендуется обратиться к соответствующей литературе.
Итак, мы вспомнили, что такое наследование и полиморфизм и можем перейти рассмотрению виртуальных методов.
Предположим, что у нас имеется некий базовый класс высокого уровня, например класс кнопки диалогового окна. В этом базовом классе реализуются функции (методы) обычной стандартной кнопки диалогового окна.
При создании диалогового окна мы захотели кроме стандартных кнопок сделать пару нестандартных, одну треугольную, другую круглую. Поскольку кнопки отличаются лишь формой, логично эти кнопки сделать наследниками базового класса стандартной кнопки, изменив метод «рисования», который будет формировать на диалоговом окне не прямоугольную, а круглую или треугольную кнопку.
На первый взгляд задача решается просто – переопределяем метод отображения кнопки, используя полиморфизм. Но такое решение хоть и возможно, но не совсем удачно.
Представим упрощенно метод рисования диалогового окна, он последовательно вызывает методы рисования элементов этого окна: кнопок, чек-боксов, полос прокрутки и пр. Если переопределенный метод отображения кнопки не виртуальный, то при добавлении новых типов кнопок (круглых и треугольных) нам потребуется изменять метод рисования диалогового окна т.к. он должен вызывать метод отображения производного класса. А вот если метод отображения кнопки будет виртуальным, то мы можем вызывать метод отображения не производного, а базового класса, при этом будет вызываться метод отображения кнопки производного класса.
Для большей наглядности далее приведен поясняющий код.
Вариант без использования виртуальных методов
// Базовый класс стандартной кнопки
class CStandartButton
{
public:
// метод отображения кнопки
void show();
…
};
// Первый производный класс круглой кнопки
class CType1Button : public CStandartButton
{
public:
// переопределяем метод отображения
void show();
…
};
// Второй производный класс треугольной кнопки
class CType2Button : public CStandartButton
{
public:
// переопределяем метод отображения
void show();
…
};
// Класс диалогового окна
class CDialog
{
protected:
// Стандартная кнопка
CStandartButton *button1;
// Круглая кнопка
CType1Button * button2;
// Треугольная кнопка
CType2Button * button3;
public:
// метод отображения
void show()
{
…
// Отображение стандартной кнопки
button1-> show();
// Отображение круглой кнопки
button2-> show();
// Отображение треугольной кнопки
button3-> show();
};
};
Вариант c использованием виртуальных методов
…
// Базовый класс стандартной кнопки
class CStandartButton
{
public:
// метод отображения кнопки
virtual void show();
…
};
// Первый производный класс круглой кнопки
class CType1Button : public CStandartButton
{
public:
// переопределяем метод отображения
virtual void show();
…
};
// Второй производный класс треугольной кнопки
class CType2Button : public CStandartButton
{
public:
// переопределяем метод отображения
virtual void show();
…
};
// Класс диалогового окна
class CDialog
{
protected:
// Массив указателей на любые производные от стандартной кнопки
CStandartButton *button[3];
public:
// метод отображения
void show()
{
…
// Отображение стандартной кнопки
button[0]-> show();
// Отображение круглой кнопки
button[1]-> show();
// Отображение треугольной кнопки
button[2]-> show();
};
// Конструктор класса
CDialog()
{
button[0] = new CStandartButton;
button[1] = new CType1Button;
button[2] = new CType2Button;
};
};
В приведенном примере преимущество использования виртуальных методов не столь очевидно т.к. он достаточно простой. Приемущество становится более очевидным если метод отображения (перерисовки) кнопки используется многократно в различных местах класса диалогового окна или присвоение указателям объектов кнопок button[] выполняется в функции main, а сами кнопки (используемые классы кнопок) изменяются в процессе выполнения программ, при этом в случае использования виртуальных методов метод отображения окна остается неизменным. Кроме того мы можем сделать метод отображения диалогового окна ещё более абстрактным, если используем указатели не на кнопки на некий элемент диалогового окна в принципе, производным от которого является кнопка.
Кроме виртуальных методов в С++ имеются так называемые чисто виртуальные методы, которые объявляются в классе, но не определяются в нем.
Объявляются эти методы следующим образом
virtual void show() = 0;
Такая запись «говорит» компилятору, что метод будет определен в производных классах и он не ищет определения этого метода в базовом классе.
Классы в которых определены чисто виртуальные методы являются абстрактными. В отличие от обычных классов эти классы не могут образовывать объекты, а могут лишь быть базовыми и порождать производные классы. Причины, по которым абстрактный класс не может образовывать объекты достаточно очевидны, поскольку метод не определен компилятор просто не знает, что ему делать при вызове неопределенного виртуального метода.
Классы, производные от абстрактного базового класса, не имеющие определения чисто виртуальных методов базового класса также являются абстрактными, абстрактность класса наследуется до тех пор не будут определены все чисто виртуальные методы.
Абстрактные классы представляют своего рода оболочку, набор методов обработки данных высокого уровня абстракции. Абстрактный класс может не знать как приходят и уходят данные, он просто их преобразует по требуемому алгоритму, а реализацию приема и передачи информации берут на себя производные от абстрактного классы. Но не следует путать абстрактные классы с шаблонными классами или контейнерами, которые прежде всего оперируют с обобщенными типами данных, и в отличие от абстрактных классов имеют четко определенные методы.
С первого взгляда виртуальные методы и абстрактные классы непонятны и требуется некоторый навык для того, чтобы понять их преимущества.
Автор надеется, что приведенный пример библиотеки для устройств с интерфейсом 1Wire, позволит читателям поближе познакомиться с этим мощным инструментом С++.
Описание библиотеки
Библиотека состоит из 3-х классов.
Самым базовым классом является абстрактный класс COneWireMaster, который описан в файлах OneWireMaster.cpp и OneWireMaster.h.
Класс COneWireMaster реализует основные функции передачи и приема данных по 1Wire шине и содержит следующие методы:
Закрытые методы (protected)
uint8_t ResetLine() – сброс и обнаружение устройства на линии, метод возвращает результат обнаружения устройства: RES_REZ_OK – устройство найдено; RES_REZ_NO_FIND – устройство не найдено; RES_REZ_ERROR – ошибка обмена, вызванная помехами или замыканием линии;
uint8_t ReadByte(uint8_t *data) – чтение байта данных, результат передается в переменную data, при успешном чтении байта данных метод возвращает RES_REZ_OK, иначе RES_REZ_NO_FIND или RES_REZ_ERROR;
uint8_t WriteByte(uint8_t data) – запись байта данных, передаваемого в метод аргументом data, при успешной передаче байта данных метод возвращает RES_REZ_OK, иначе RES_REZ_NO_FIND или RES_REZ_ERROR;
uint8_t _crc_ibutton_update(uint8_t crc, uint8_t data) – стандартная функция расчета контрольной суммы, заимствованная из GNU компилятора WinAVR;
Открытые методы (public)
uint8_t ReadId(uint8_t *id) – чтение идентификатора устройства, результат передается в массив id, при успешном чтении данных метод возвращает RES_REZ_OK, иначе RES_REZ_NO_FIND или RES_REZ_ERROR.
Класс использует две чисто виртуальные функции:
SetLine (uint8_t bit) – установка на линии 0 или 1;
uint8_t GetLine(void) – опрос состояния линии 0 или 1.
Кроме того, в данном программном модуле (и в последующих) использованы функции формирования задержек _delay_us() и _delay_ms(), строго говоря их тоже можно было бы сделать виртуальными, но автор посчитал, что это будет нецелесообразным т.к. функция задержки как правило является встроенной в компилятор, а если нет, то создается универсальная библиотека, кочующая от проекта к проекту. Используемая автором библиотека формирования задержек прилагается.
От класса COneWireMaster наследуется класс CDS18x20, который описан в файлах DS18x20.cpp и DS18x20.h.
Класс CDS18x20 реализует чтение данных из датчика температуры.
Поскольку из всех функций микросхемы DS18x20 автору потребовалось лишь одна – чтение температуры, класс содержит всего один открыты метод:
uint8_t ReadTemp(int *data) – запуск преобразования и чтение температуры, результат чтения температуры сохраняется в переменную data в формате с фиксированной точкой, цена младшего разряда 0,01 градус Цельсия. При успешном чтении данных метод возвращает RES_REZ_OK, иначе RES_REZ_NO_FIND или RES_REZ_ERROR.
От класса CDS18x20 наследуется семейство классов CTermSens1…CTermSens6 (далее CTermSensx), которые описаны в файлах TermSensor.cpp, TermSensor.h и TermSensorHardware.h.
Классы CTermSensx реализуют виртуальные функции SetLine (uint8_t bit) и uint8_t GetLine(void), а также в своих конструкторах выполняют инициализацию портов ввода/вывода.
Данный пример создавался для микроконтроллеров серии STM32F100, при этом управление линией 1Wire обеспечивается с использованием дополнительного внешнего транзистора, который замыкает линию на землю, схема управления линией 1Wie приведена ниже
При использовании другого типа контроллера или другой схемы управления линией, необходимо переписать функции SetLine (uint8_t bit) и uint8_t GetLine(void) в классах CTermSensx .
Пример использования библиотеки приведен ниже
#include «delay.h»
#include «TermSensor.h»
int main(void)
{
// переменная для хранения результата чтения температуры
int Sensor[6];
_delay_init();
CDS18x20 *TermSens[6];
TermSens[0] = new CTermSens1;
TermSens[1] = new CTermSens2;
TermSens[2] = new CTermSens3;
TermSens[3] = new CTermSens4;
TermSens[4] = new CTermSens5;
TermSens[5] = new CTermSens6;
while(1)
{
_delay_ms(100);
for(int i = 0; i < 6; i++)
{
if(TermSens[i]->ReadTemp(&Sensor[i]) != RES_REZ_OK) Sensor[i] = 0xffffffff;
_delay_ms(100);
}
}
}
Из примера видно, что не смотря на кажущуюся сложность виртуальных методов и абстрактных классов, использование библиотеки достаточно простое.
Вместо заключения
Библиотеку не обязательно использовать в случае когда требуется подключение нескольких 1Wire устройств, она вполне успешно может использоваться и когда подключается всего одно устройство, при этом используя базовый класс COneWireMaster можно создать производные классы поддерживающие различные типы 1Wire устройств, например такие как память DS1996, часы DS1994 и др.
Кроме того, библиотека демонстрирует возможность многоуровнего построения сложных библиотек. Таким образом может быть реализована файловая система, где на одном уровне реализуются функции работы с портами контроллера или в случае использования аппаратного интерфейса (например SPI) функции управления регистрами, на втором уровне описываются функции обмена данными с конкретной микросхемой, на третьем описывается непосредственно файловая система, при этом базовым классом будет класс файловой системы. При подключении нескольких микросхем, при таком подходе, достаточно просто создается несколько файловых хранилищ, аналогов дисков на компьютере.
Некоторые электроники считают, что использование С++ вообще и классов в частности увеличивает исходный код и делает программу более «медленной». Это мнение не совсем верное, дело в том что различные «навороты» С++ представляют сложность исключительно для компилятора т.е. увеличивается не время исполнения программы, а время ее компиляции. С++ это как и Си язык с помощью которого мы объясняем микроконтроллеру чего мы от него хотим, при этом переводчиком выступает компилятор и в некоторых случаях «навороты» С++ позволяют создать более компактный и понятный код, который при этом будет быстро исполняться контроллером, для чего естественно необходимо понимать особенности языка.
Язык это инструмент общения и он сам по себе не хороший и не плохой, не медленный и не быстрый, таковым он становится в конкретном применении.
Можно долго рассказывать теорию и приводить много примеров исходного кода, но чтобы понять нужно брать и делать. Беритесь смелее, просто повторяйте, дорабатывайте, дополняйте и у вас все получится. Фраза «Лучше день потерять — потом за час долететь» в перспективе оправдывает себя. Удачи!
Прикрепленные файлы:
- 1Wire.rar (5 Кб)