1

I have a situation where I have a class on a microcontroller which deals with pulse width modulation. Extremely simplified example:

class MotorDriver
{
    int pin_;
public:
    MotorDriver(int pin);
    void init();
    void start();
    void stop();
    void changeDutyCycle(int dc);
};

It has functions to initialize, start, stop and change pwm. If I connect 4 motors to the microcontroller, I will create 4 instances of that class and put them in an array, then call functions like

motors[0].changeDutyCycle(50);
motors[1].changeDutyCycle(40);
....

Problem arises because there is no generic way to configure timers on said microcontroller. For example, one motor will have to use Timer3 while another motor will have to use Timer4. Different timers have different bit sizes, registers, channels, pins, ... I want to be able to write custom functions for each timer, but still be able to put all objects into same array and call functions on them, ie

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);
}

I know I can achieve what I want with virtual functions. This function is short and will be called often, so price of virtual functions is high. Is there a way to avoid them in this case or different design pattern? The goal was to have reusable code which is easy to use on the outside. Having everything that I need in an array makes many things much easier.

Edit: I know about this post Avoiding virtual functions but answer that relates to what I need states:

If you're in a situation where every cycle per call counts, that is you're doing very little work in the function call and you're calling it from your inner loop in a performance critical application you probably need a different approach altogether.

Community
  • 1
  • 1
Terraviper-5
  • 61
  • 2
  • 7
  • I suggest using MotorDriver id when you create a new motor. ID is a private variable, we use ID to access a timer array. This means each timer maps into 1 ID. – Ngo Thanh Nhan Jan 05 '17 at 02:43
  • 1
    Probably, the timers are not that different at all, and you are exaggerating the need to treat them differently? An alternative way to write drivers when you have x amount of hardware peripherals with exactly the same register layout, is something like [this](http://stackoverflow.com/questions/29034417/c-preprocessor-generate-macros-by-concatenation-and-stringification/29035658#29035658) where you just pass a pointer to the register base offset around. The driver code for the different timers will then be 100% identical apart from that. – Lundin Jan 05 '17 at 09:19

2 Answers2

-1

The differences between timers are usually pretty minor, especially when it comes to configuring the actual output width - the initialization may be different, but there you can have virtual functions. Just store the reference to underlying TIM registers and the channel index in your class and that seems all you have to do. If you use things like "complementary" channels, then you can store them as negative indexes.

Check this code - it's for a very similar purpose (driving stepper motors) on STM32F4, but should give you an idea.

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;
}

Don't worry too much about the performance - in reality microcontrollers are extremely fast and just using proper architecture (like RTOS or event driven) will cause them to be bored for 80-90% of the time!

If you implement the simple code and it will in fact cause your application to be too slow, then - assuming that you cannot improve the algorithm or overall architecture - just precompute most of the values from start() in your constructor and maybe drop error checking (or move it somewhere else, out from the loop).

Or just use virtual functions, the impact of the indirect call is usually negligible.

Freddie Chopin
  • 8,440
  • 2
  • 28
  • 58
  • "in reality microcontrollers are extremely fast" That's quite an over-simplification. Even in the year 2017, some people are still using ancient stuff like PIC, 8051 or AVR. Terribly slow things. – Lundin Jan 05 '17 at 09:11
  • @Lundin It all comes down to the architecture of the software. You cannot expect things like FFT of compression to run fast on such chips, but the firmware can still be responsive enough. The hardware that was used on moon landing was running at 2MHz and they did not crash (; All currently available MCUs - even the "ancient stuff" is ~10x faster. – Freddie Chopin Jan 05 '17 at 09:20
  • I am using channels of timers in pairs because I also need inverse output of PWM for locked antiphase. For initialization I would have to pass bus, prescaler, timer, pins and gpio of pins, but the biggest problem is channel initialization, as each channel has its own function: TIM_OC1Init, TIM_OC2Init, TIM_OC3Init, TIM_OC4Init, and each motor is using two, one class needs first pair, second class second pair, third need first pair of next timer, ... I know Timers 1 and 8 have built in inverse channels but I need more of them so I needed a way to utilize all timers with more than one channel. – Terraviper-5 Jan 05 '17 at 14:01
  • But you do have a point! I could use virtual functions only for initialization and save timer and its CCR register for changing pulse as private variables. – Terraviper-5 Jan 05 '17 at 14:06
  • @Terraviper-5 The initialization of channels is actually almost identical (as you see from the code above). The problem is in the crapware from ST which is written in an absolute clueless way, so here you have 4 almost identical functions TIM_OCxInit(), doing exactly the same but on different registers... If you are concerned about performance, the first step would be to dump that pointless "library" and write your own before even considering replacement of virtual functions. – Freddie Chopin Jan 05 '17 at 15:28
  • "The differences between timers are usually pretty minor" - without a specific hardware, this is very wrong. There are a lot of MCUs which have a bunch of different timers with different programming model. And speed depneds on the application. A 400MHz Cortex-M7 can very well be at the edge of performance such that the OOP overhead will push it over the edge. In hard real-time a single clock cycle can make the difference... – too honest for this site Jan 09 '17 at 13:21
  • @Olaf - too bad I did not know about the 400MHz chip being on the edge when I wrote the whole multithreaded firmware in C++11 for a 24MHz STM32F100... I would then refuse to believe the RTOS metrics which keep telling me that the firmware is using just 15% of available power. What a lie that was! – Freddie Chopin Jan 09 '17 at 15:33
  • @FreddieChopin: You should try more advanced projects. Just because **your** project is not does not imply there are no projects which do. And the 400Mhz CM7 was just an extreme example. I do have customers which have to worry about that. Not all projects use leading edge hardware and some are actually maintained over decades. Oh and "15% of availablke power" is a nonsense metrics. You should know the difference betwee "average" and "peak". A system can have 1% utilisation, but if there are multiple interrupts pending, it can very well violate real-time constraints. – too honest for this site Jan 09 '17 at 15:37
  • But aas you are the measure of all code, you certainly know this well and use the DWT cycle-counters to verify your interrupt handlers are within limits. – too honest for this site Jan 09 '17 at 15:40
  • @Olaf - sure, 99% of projects out there have hard real-time requirements measured in the order of single nanoseconds. Right. If you don't want to use C++ because of the _ENORMOUS_ overhead, then fine - don't use it. But please stop convincing us. We think the overhead is actually negligible (especially compared to benefits) if you know what you are doing. This bold assumption is based on simple facts which most C-only experts chose to ignore, so there's no way we can agree. – Freddie Chopin Jan 09 '17 at 17:55
-1

You could use single timer interrupt to all of then you wouldn't be limited to number of timers. Instead of changing in duty cycle setting of timer you would just change variable that would say that each X ticks toggle/set/reset the pin corresponding to this motor. And in timer routine you would just create simple for loop with iterations equal to number of motors connected and check for each i.e. modulo operation if now is the time to change pin. Software PWM using timer interrupt is good option in this scenario.

koper89
  • 645
  • 3
  • 9
  • It is a very bad idea. You definitively introduce jitter and might have a problem with performance and reliability, too. Not to forget about interrupt latency for other interupts and the software-PWM itself. If there are hardware timers, use them. – too honest for this site Jan 09 '17 at 13:24