Я делаю некоторую работу с Raspberry PI GPIO. До сих пор я писал код, как вы делаете это на C, используя функции для группировки частей кода.
Моя работа доведена до такой степени, что я рад, что все работает, но сейчас все становится грязным, и поэтому я хотел бы перейти к объектно-ориентированному подходу.
Вот проблема, с которой я сталкиваюсь.
На данный момент у меня есть класс, который представляет мое «устройство». (Аппаратное обеспечение, которое я собрал и которое подключено к порту GPIO.) Аппаратное обеспечение имеет 2 отдельных раздела. Один раздел является разделом «вход», а другой раздел «выход».
Чтобы помочь вам лучше понять это, раздел «вход» представляет собой АЦП. (Аналого-цифровой преобразователь.) Это устройство, которое преобразует аналоговый сигнал в 10-битное двоичное представление. (В случае, если вы не были знакомы с электроникой.)
Секция «выход» — это просто транзистор, который включает и выключает набор светодиодов.
Я хотел иметь класс, представляющий плату АЦП, и класс, представляющий плату светодиодов, поскольку они представляют собой два концептуально разных устройства, поскольку они никак не «связаны».
Это вызывает проблему, так как выводы GPIO должны быть установлены на определенные значения, прежде чем будут установлены их режимы. Под значениями я подразумеваю «ВЫСОКИЙ» или «НИЗКИЙ», 1 или 0. Под режимами я подразумеваю «ВХОД» или «ВЫХОД». Это звучит странно, но в основном АЦП будет десинхронизирован, если линии управления не будут установлены на правильные значения LOGIC HIGH и LOGIC LOW до его включения. (Это очень странное устройство, оно не включается до тех пор, пока ему не сообщается, даже если оно подключено к источнику питания. (VCC или VDD 5.0V) Одна из линий управления отправляет сигнал на включение устройства.
Для выполнения вышесказанного учтите, что выводы GPIO изначально находятся в режиме INPUT. Чтобы «заставить АЦП работать должным образом», мы сначала устанавливаем значения данных (ВЫСОКИЙ / НИЗКИЙ), которые должны присутствовать на выводах ДО того, как они будут переведены в режим ВЫХОДА. Таким образом, когда режим изменяется с INPUT на OUTPUT, присутствуют данные с правильными значениями, и мы не расстроим АЦП.
Моя первоначальная идея заключалась в том, чтобы иметь конструктор для АЦП, который сначала устанавливает значения для выходных данных, а затем изменяет требуемые выводы из режима INPUT в режим OUTPUT. Но это заставляет нас строить класс платы АЦП перед классом платы светодиодов.
Это может быть исправлено обоими конструкторами, выполняющими один и тот же код для установки режимов вывода, но это кажется плохой идеей, потому что мы вызываем 2 бита кода дважды — не очень элегантное решение.
Другое решение состоит в том, чтобы иметь класс GPIOPort, который объединяет устройства ввода и вывода, но это тоже не очень элегантно, и было бы трудно изменить, если бы мы когда-либо добавили вторую, идентичную светодиодную плату. (Например.)
Я думаю, что мне нужен еще один класс, который представляет сам GPIOPort. (Думаю, это своего рода абстрактная идея?) Тогда я думаю, что я хочу, чтобы «класс в классе» представлял плату АЦП, и «класс в классе» также представлял плату СИД. Я не могу вспомнить, как называется эта техника, но обычно «внешний класс» похож на оболочку, с указателем на объект типа, который является «внутренним классом», а также на метод create и метод destroy. Внешний класс делает что-то вроде pointer = new type;
в методе создания и delete pointer
в методе уничтожения. Это позволяет при необходимости вызывать конструктор, а при необходимости вызывать деструктор класса.
Дело в том, что конструктор класса GPIOPort обрабатывает порядок, в котором создаются эти объекты, что скрывает все от main (). В основном, программист просто делает что-то вроде GPIOPort myGPIOPort;
, и это обрабатывает все, что вам нужно, поэтому вам не нужно включать 20 строк кода в main () для настройки данных для выходных выводов, что является единственным другим решением. (Который я не упомянул выше.)
Итак, мой первый вопрос: как называется эта техника? Я думал, что он называется классом-оболочкой, но я понимаю, что класс-оболочка предназначен для использования фундаментальных типов, таких как double
а также int
как объекты. (И добавление таких методов, как clear()
или же reset()
или что-то в этом роде.) Это то, чем я на самом деле хочу заниматься, или есть лучший метод? (Я предполагаю, что все сводится к тому, «как я могу решить мою проблему».)
Мой второй вопрос заключается в том, что, насколько я помню, я должен сделать некоторые из методов (деструктор?) Виртуальными методами, но я не могу вспомнить почему. (Или возможно я не делаю, и я просто смущен.)
Мой третий вопрос: есть ли примеры этого, которые я могу использовать, чтобы помочь себе понять это, или, альтернативно, куда я могу пойти, чтобы улучшить свое понимание. (Ресурсы.)
Спасибо, очевидно, это довольно длинный вопрос. Я пытался включить как можно больше информации, чтобы помочь объяснить ситуацию. Если вы хотите разъяснений, то я постараюсь улучшить то, что я сказал.
Данные должны быть отправлены на выводы GPIO, прежде чем их режим будет изменен с входа на выход.
Контакты GPIO выглядят как все нули, так как на контактах есть понижающие резисторы, и они все еще устанавливаются как входы. Данные, которые были отправлены, не отображаются до тех пор, пока не будет изменен их режим.
Затем контакты устанавливаются в режим вывода. (Или некоторые из них в любом случае.) Теперь отправленные данные появляются на контактах.
Если контакты отправляются в режим вывода до отправки данных, мы не можем предотвратить включение АЦП, поскольку вывод данных, который управляет включением АЦП, может быть установлен на ВЫСОКИЙ. Он может быть установлен на НИЗКИЙ, но невозможно сказать, его состояние не определено, пока мы не сообщим GPIO, какие значения мы хотим, прежде чем устанавливать режим для вывода. К счастью, мы можем гарантировать, что все контакты будут в режиме ввода.
Некоторые определения:
Я настоятельно рекомендую не использовать синглтон. Однажды вы можете подключить второе устройство к другим контактам GPIO, и у вас возникнут проблемы.
Если вы создаете отдельные классы для LEDBoard и ADCBoard, вы должны спросить:
«Что мне нужно для создания LEDBoard / ADCBoard?» Ну … Вам нужно устройство!
Так что мой дизайн будет следующим:
struct DeviceDescriptor
{
int portNumber;
// add additional variables to identify the device
}
class Device
{
Device(DeviceDescriptor descriptor)
{
//Insert your initialization...
//You can maintain a static vector of already opened Devices to throw an
//error if an allready opened device is reopened
}
~Device()
{
//Deinit device
}
// A device should not be copyable
Device(const& Device) = delete;
//TODO do the same for copy assignment
//TODO implement move ctr and move assignment operator
//TODO add needed mamber variables
}
class LEDBoard
{
LEDBoard(std::shared_ptr<Device> device) : m_device(device)
{
//Do init stuff
}
//Your functions
private:
std::shared_ptr<Device> m_device;
}
//ADCBoard is analog to LEDBoard
Вы можете использовать классы так:
int main(void)
{
auto device = std::make_shared<Device>(DeviceDescriptor());
LEDBoard led1(device);
ADCBoard adc1(device);
//Your stuff...
}
Выгоды:
Редактировать: Я широко редактирую этот пост, потому что считаю, что в настоящее время это не лучшее решение. Это редактирование действительно похоже на ответ @MarkusMayer, но я пришел к нему совершенно иначе (я думаю), так что, возможно, оно поможет вам.
Во-первых, позвольте определить вывод GPIO, который может быть любым, чем вы хотите (класс будет хорош, тогда вы можете сделать pin.setOutput()
или же pin.set()
и т.д. Я позволю вам определить это так, как вы хотите, давайте просто предположим, что у нас есть GPIOPin
учебный класс.
Во-первых, я определяю абстрактную доску как набор выводов, которые выглядят для меня совершенно корректно:
template <int N>
class Board {
protected:
Board (std::array <GPIOPin, N> const& pins) : _pins(pins) { }
std::array <GPIOPin, N> _pins ;
};
Затем я определяю интерфейс для ADC
а также LEDs
которые также являются абстрактными:
class ADC {
public:
ADC () { }
float read () { }
} ;
class LEDs {
public:
LEDs () { }
void set (int) { }
} ;
Теперь я могу создать то, что представляет реальную доску с ADC
а также LED
:
class MyBoard : public Board <5> { // Let's assume it's connect to 5 bits
public:
MyBoard (std::array <GPIOPin, N> const& pins) : Board<5>(pins) {
// Here you can initialize what you want
}
} ;
Затем вы создаете свой собственный ADC
а также LED
:
class AD7813 : public ADC {
Board <5> _board ;
public:
AD7813 (Board <5> *board) : ADC(), _board(board) { }
} ;
// Same for the LED
Наконец, вы можете просто использовать его следующим образом:
Board <5> *board = new MyBoard(/* The corresponding GPIO pins. */) ;
ADC *adc = new AD7813(board) ;
LEDs *led = new MyLEDs(board) ;
Я не определил деструктор для MyBoard
или же Board
но конечно можно. Вы также можете использовать shared_ptr
как @MarkusMayer.
Конец редактирования.
Я думаю, что есть разные подходы к этой проблеме, я представлю здесь, что бы я сделал. Часто сложно использовать OO-дизайн во встроенной системе, во-первых, вы должны иметь синглтон-класс почти везде, потому что у вас только один ADC
(вы не можете создать несколько ADC), поэтому ваш класс ADC (и класс LEDBoard) должен выглядеть следующим образом:
class ADC {
public:
static ADC *getInstance () {
if (_instance == nullptr) {
_instance = new ADC () ;
}
return _instance ;
}
private:
ADC () ;
};
Чтобы ответить на вашу проблему, я бы создал базовый класс, который будет выполнять вашу инициализацию и сделает это только один раз (используйте статический член, чтобы узнать, инициализированы ли порты).
class GPIOs {
protected:
GPIOs () {
if (!GPIOs::_init) {
/* Do what you want. */
GPIOs::_init = true ;
}
}
private:
static bool _init ;
} ;
bool GPIOs::_init = false ;
Тогда ваш ADC
а также LEDBoard
класс наследуется от GPIOs
:
class ADC : public GPIOs {
public:
ADC *getInstance () { /* ... */ }
private:
ADC () : GPIOs () { } // Call constructor
} ;
Затем в своем коде вы просто делаете:
ADC *adc = ADC::getInstance () ;
Вы также можете использовать синглтон для GPIOs
класс, но так как это абстрактный класс, чем используется только ADC
а также LEDBoard
которые уже синглтон, это не самое полезное.
Я уверен, что есть много других способов справиться с вашей проблемой, но главная идея, которую я хотел показать, это использование init
метод / класс, который можно вызывать несколько раз, не выполняя многократную инициализацию из-за _init
логическое значение.
Я нарисую вам идею. Я не знаю каких-либо шаблонов дизайна для этого, но ниже может соответствовать вашим потребностям.
Во-первых, я согласен с вашей идеей использования GPIOPort для управления всем портом, но я хочу представить более модульный подход, чем «класс в классе». Вместо того, чтобы настраивать порты в конструкторе устройств, я предлагаю создать объект, описывающий устройство, и позволить GPIOPort настроить устройства на основе этих дескрипторов.
Моя идея заключается в том, чтобы инкапсулировать доступ к GPIO через класс GPIOPort. Но оставьте необработанный вывод открытым для работы с пользователем кода. Это можно сочетать и с другими классами, но они должны быть с помощью GPIOPort
в этом дизайне, а не наоборот.
Один из многих (иногда противоречивых) советов в ООП заключается в том, что вы должны подтиповать класс только в том случае, если его поведение изменилось. Если вы можете выразить разницу между двумя классами, просто изменив атрибуты, они принадлежат к одному и тому же классу. Я не уверен, так ли это здесь, в зависимости от того, сколько вам нужно сделать, чтобы инициализировать устройство.
using ports = uint64_t; // some suitable unsigned bit-maskable type.
// Used to control the IO.
struct DeviceDescriptor {
ports in_mask, // Which pins does this device use for input
out_mask, // Which pins does this device use for output
init, // Initial state of the pins.
shutdown; // State to send when device should power down.
};
class GPIOPort {
static const ports ALL_PORTS = ~static_cast<ports>(0);
std::vector<Device> devices;
public:
// Initialize the devices.
GPIOPort( std::vector<Device> & devices ) : devices(devices) {
ports used_ports = 0, init = 0;
for ( auto & device : devices ) {
init |= device.init;
// Assert no overlapping ports
ports partition = device.init | device.in_mask | device.out_mask | device.shutdown;
if ( used_ports & partition){
// Signal overlapping ports.
} else {
used_bits |= current;
}
}
set_bits(init, ALL_PORTS); // Actually sets the output.
}
// Read the input of device number 'dev'
ports read_state( int dev, ports mask = ALL_PORTS ) {
return read_bits( devices.at(dev).input_mask & mask );
}
// etc...
~GPIOPort() {
ports shutdown;
for ( auto & device : devices ) {
shutdown |= device.shutdown;
}
set_bits(shutdown, ALL_PORTS);
}
};