Question

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?

Was it helpful?

Solution

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.

OTHER TIPS

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

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

Licensed under: CC-BY-SA with attribution
scroll top