Как избежать виртуальных функций в этом случае?
У меня есть ситуация, когда у меня есть класс на микроконтроллере, который имеет дело с широтно-импульсной модуляцией. Чрезвычайно упрощенный пример:
class MotorDriver
{
int pin_;
public:
MotorDriver(int pin);
void init();
void start();
void stop();
void changeDutyCycle(int dc);
};
Он имеет функции для инициализации, запуска, остановки и изменения ШИМ. Если я подключу 4 двигателя к микроконтроллеру, я создам 4 экземпляра этого класса и поместу их в массив, а затем вызову такие функции, как
motors[0].changeDutyCycle(50);
motors[1].changeDutyCycle(40);
....
Проблема возникает из-за отсутствия общего способа настройки таймеров на указанном микроконтроллере. Например, один двигатель должен будет использовать Timer3, в то время как другой двигатель должен будет использовать Timer4. Разные таймеры имеют разные размеры битов, регистры, каналы, выводы... Я хочу иметь возможность писать пользовательские функции для каждого таймера, но все же иметь возможность помещать все объекты в один массив и вызывать на них функции, т.е.
class MotorDriver
{
void changeDutyCycle(int dc) = 0;
};
class MotorDriver1 : public MotorDriver
{
void changeDutyCycle(int dc)
{
TIM3->CCR2 = dc;
}
};
class MotorDriver2 : public MotorDriver
{
void changeDutyCycle(int dc)
{
TIM4->CCR1 = dc;
}
};
MotorDriver1 md1();
MotorDriver2 md2();
MotorDriver* mds[] = { &md1, &md2 };
int main()
{
mds[0]->changeDutyCycle(10);
mds[1]->changeDutyCycle(20);
}
Я знаю, что могу достичь того, чего хочу, с помощью виртуальных функций. Эта функция короткая и будет вызываться часто, поэтому цена виртуальных функций высока. Есть ли способ избежать их в этом случае или другой шаблон дизайна? Цель состояла в том, чтобы иметь повторно используемый код, который легко использовать снаружи. Наличие в массиве всего, что мне нужно, значительно облегчает многие вещи.
Изменить: я знаю об этом посте Избегать виртуальных функций, но ответ, который относится к тому, что мне нужно, говорится:
Если вы находитесь в ситуации, когда каждый цикл на один вызов имеет значение, то есть вы выполняете очень мало работы в вызове функции и вызываете его из своего внутреннего цикла в приложении, критичном к производительности, вам, вероятно, потребуется совсем другой подход.
2 ответа
Вы можете использовать одно таймерное прерывание для всех, тогда вы не будете ограничены количеством таймеров. Вместо того, чтобы изменять настройку таймера рабочего цикла, вы просто изменили бы переменную, которая бы гласила, что каждый тик Х переключает / устанавливает / сбрасывает вывод, соответствующий этому двигателю. А в процедуре таймера вы просто создаете простой цикл for с итерациями, равными количеству подключенных двигателей, и проверяете для каждой операции, то есть по модулю, наступило ли время для смены контакта. Программный ШИМ, использующий прерывание по таймеру, является хорошим вариантом в этом сценарии.
Различия между таймерами обычно довольно незначительны, особенно когда речь идет о настройке фактической ширины вывода - инициализация может быть разной, но там вы можете иметь виртуальные функции. Просто сохраните ссылку на базовые регистры TIM и индекс канала в вашем классе, и это все, что вам нужно сделать. Если вы используете такие вещи, как "дополнительные" каналы, вы можете хранить их как отрицательные индексы.
Проверьте этот код - он предназначен для очень похожих целей (привод шаговых двигателей) на STM32F4, но он должен дать вам представление.
namespace
{
/// array with all CCR registers
const decltype(&TIM_TypeDef::CCR1) ccrs[]
{
&TIM_TypeDef::CCR1,
&TIM_TypeDef::CCR2,
&TIM_TypeDef::CCR3,
&TIM_TypeDef::CCR4
};
constexpr bool isAdvancedControlTimer(const TIM_TypeDef& tim)
{
return &tim == TIM1 || &tim == TIM8;
}
} // namespace
TIM_TypeDef& HardwareTimer::getTim() const
{
// "timBase_" is "uintptr_t timBase_;"
// initialized with TIM1_BASE, TIM2_BASE, ...
return *reinterpret_cast<TIM_TypeDef*>(timBase_);
}
int HardwareTimer::start(const int8_t channel, const uint16_t compare) const
{
if (channel == 0)
return EINVAL;
const auto complementaryChannel = channel < 0;
const auto channelShift = (complementaryChannel == true ? -channel : channel) - 1;
if (channelShift >= 4)
return EINVAL;
auto& tim = getTim();
const auto advancedControlTimer = isAdvancedControlTimer(tim);
if (complementaryChannel == true && advancedControlTimer == false)
return EINVAL;
tim.*ccrs[channelShift] = compare;
if (advancedControlTimer == true)
tim.BDTR |= TIM_BDTR_MOE;
tim.CR1 |= TIM_CR1_CEN | TIM_CR1_URS;
return 0;
}
Не стоит сильно беспокоиться о производительности - на самом деле микроконтроллеры работают очень быстро, и простое использование надлежащей архитектуры (например, RTOS или событийно-ориентированного управления) приведет к тому, что им будет скучно в течение 80-90% времени!
Если вы реализуете простой код, и это на самом деле приведет к тому, что ваше приложение будет работать слишком медленно, тогда - при условии, что вы не сможете улучшить алгоритм или общую архитектуру - просто предварительно вычислите большинство значений из start()
в вашем конструкторе и, возможно, отбросьте проверку ошибок (или переместите ее куда-нибудь еще, из цикла).
Или просто используйте виртуальные функции, влияние косвенного вызова обычно незначительно.