1

I have been developing control software in C++. My hardware consists of a microcontroller with an integrated a/d converter and an external on board a/d converter. Both of these a/d converters have different features but there are some commonalities (both of them are capable to offer value of given analog input).

// analog inputs
enum class Input
{
  Channel_00,
  Channel_01,
  Channel_02,
  Channel_04,
  Channel_05,
  Channel_06,
  Channel_07
};

// driver of the internal a/d converter
class AdcInt
{
  public:
    float getValue(Input input);
};

// driver of the external a/d converter
class AdcExt
{
  public:
    float getValue(Input input);
};

Based on the drivers for the a/d converters I am going to start building the application layer. One of the building stones of my design is the AnalogInput class. This class is intended to exploit the drivers and offers some additional services e.g. conversion the raw value into the physical units.

class AnalogInput
{
  public:
    float getConvertedValue();
}

I have been thinking about how to ensure that the AnalogInput class can operate with both the driver objects (AdcInt, AdcExt) which is necessary because the analog inputs can be connected to any of the a/d converters and I need to work with all the analog inputs in uniform manner.

  • First idea

My first idea how to do that is based on common interface for all the a/d converter drivers let's say AdcDriver and defining the virtual getValue method as a part of this interface. The AnalogInput class would then receive pointer to the AdcDriver interface in its constructor. The drawback of this idea is that the getValue method is virtual which is unsuitable for usage in the interrupt service routine (which is my requirement).

  • Possible solution

Due to the drawback of my first idea I have started to look for another approach how to create common interface AdcDriver without virtual method. I have found the so called curiously recurring template pattern (CRTP).

template<class T>
class AdcDriver 
{
  public:
    float getValue(Input input)
    {
      return static_cast<T*>(this)->getValue(input);
    }
}

class AdcInt : public AdcDriver<AdcInt>
{
  public:
    float getValue(Input input)
    {
      // ... implementation specific for AdcInt
    }
}

class AdcExt : public AdcDriver<AdcExt>
{
  public:
    float getValue(Input input)
    {
      // ... implementation specific for AdcExt
    }
}

template<class T>
class AnalogInput
{
  public:
    AnalogInput(T& _driver, Input _input_id) : driver(_driver)
    {
      input_id = _input_id;
    }

    float getConvertedValue()
    {
      return convert(driver.getValue());
    }

  private:
    T& driver;
    Input input_id;

    float convert(float); 
}

int main(int argc, char** argv) {

  AdcInt internal_adc;
  AdcExt external_adc;

  AnalogInput<AdcInt> analog_input_01(internal_adc, Input::Channel_01);
  AnalogInput<AdcExt> analog_input_02(external_adc, Input::Channel_02);

  analog_input_01.getConvertedValue();
  analog_input_02.getConvertedValue();

  return 0;
}

Do you think that the second approach which I have described above is appropriate solution of my problem? If you don't think so can you recommend me better idea?

6
  • 1
    1) Which interrupt are you handling when calling that getValue() method? 2) Why do you think calling a virtual method is not allowed in an interrupt? It is just an indirect function call. – Bart van Ingen Schenau Feb 26 at 12:27
  • 1) I am going to call the getValue method from the "end of conversion" interrupt. 2) Based on various materials I have got an impression that it is not a "good practice" although in my eyes it is only dereferencing a function pointer. – Steve Feb 26 at 14:14
  • It's not clear (to me) whether you want to pick between the two A/D converters at run-time (e.g., when the program starts running) or at compile time. CRTP is, of course, a compile-time solution. – davidbak Feb 26 at 14:39
  • 1
    Your comment (2nd above) about "not a good practice" leads me to say the following: The only problem with a virtual call is that it takes longer - usually an extra memory cycle, but maybe a bit more if your CPU's addressing modes are particularly poor. Anyway, it is slower than an ordinary (non-virtual) call, and obviously also slower than no call at all (it can't usually be inlined). So speed is of the essence in an interrupt routine, thus the recommendation. You might try measuring the speed with and without to see how important it is to you. (Or simply inspect the generated code.) – davidbak Feb 26 at 15:04
  • Actually, CRTP can be used as follows: You create (in code) your AdcDriver class twice - once for each A/D device. Then at run time you instantiate the correct one only and hook it up to your interrupt vector. So I take back my remark about CRTP above. – davidbak Feb 26 at 15:54
6

You don't need AdcDriver at all. It does nothing. AdcInt and AdcExt both expose the same interface.

If you want to have an object that accepts AdxInt or AdcExt based on runtime information, you will need virtual somewhere. If not, you can use a simpler template.

class AdcInt
{
  public:
    float getValue(Input input)
    {
      // ... implementation specific for AdcInt
    }
}

class AdcExt
{
  public:
    float getValue(Input input)
    {
      // ... implementation specific for AdcExt
    }
}

template<class T>
class AnalogInput
{
  public:
    AnalogInput(T& _driver, Input _input_id) : driver(_driver), input_id(_input_id)
    {
    }

    float getConvertedValue()
    {
      return convert(driver.getValue());
    }

  private:
    T& driver;
    Input input_id;

    float convert(float); 
}

int main(int argc, char** argv) {

  AdcInt internal_adc;
  AdcExt external_adc;

  AnalogInput<AdcInt> analog_input_01(internal_adc, Input::Channel_01);
  AnalogInput<AdcExt> analog_input_02(external_adc, Input::Channel_02);

  analog_input_01.getConvertedValue();
  analog_input_02.getConvertedValue();

  return 0;
}

This is the same kind of idea as the iterator / algorithm interface in the standard library. There is no common ancestor of std::vector<int>::iterator and std::deque<int>::iterator, but they can be used by the same algorithms, because they present the same behaviour.

1

As you intend to use the getValue() method from the "end of conversion" interrupt, I am going to propose a completely different architecture.

Conceptually, the interrupt handler for the "end of conversion" interrupt of the internal ADC is part of the AdcInt driver and the corresponding interrupt handler for the external ADC is part of the AdcExt driver. So, the interrupt handlers should use those drivers without any layer in-between.

Then the driver can call a callback function or call a member of AnalogInput to inform them that a new value is available.

This would make the classes look something like

class AnalogInput
{
public:
  float getConvertedValue() const;
  void setRawValue(float aValue);
private:
  float raw_value;
};

class AdcInt
{
public:
  void registerCallback(Input input_id, AnalogInput* callback);
  void handle_interrupt()
  {
    // determine current input
    // read value from ADC
    
    callbacks[current_input]->setRawValue(value);
  }
};

class AdcExt
{
public:
  void registerCallback(Input input_id, AnalogInput* callback);
  void handle_interrupt()
  {
    // determine current input
    // read value from ADC
    
    callbacks[current_input]->setRawValue(value);
  }
};
0

I am not sure, but I think here you could use a pimpl pattern?

1
  • But what is at the other end of the pimpl-pointer? If it is not polymorphic itself (i.e., based on virtual methods) how is this to work? And if it is polymorphic then it is more expensive than not using it (extra indirection). (Most often, when it is used, the other end of the pimpl-pointer is not polymorphic ...) – davidbak Feb 28 at 18:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.