Hardware and Software serial

I would like to have an overloaded functions in a class that allows me to use either a Hardware or Software UART.

How do I get the software serial stream to work? Hardware stream, m_serialPort, works as expected.

Contained in the .cpp file

void TC_SWI::begin(HardwareSerial* serial, uint32_t baud, uint8_t rxPin, uint8_t txPin)
{
  m_serialPort = serial;

  // Setup Serial
  #if defined(Board_ESP32_S3)
    serial->begin(baud, SERIAL_8N1, rxPin, txPin);
    serial->setRxFIFOFull(1);                       // Must be set after begin()
  #elif defined(Board_STM32)
    serial->begin(baud);
  #endif
}

void TC_SWI::begin(uint16_t SW_RX_pin, uint16_t SW_TX_pin, uint32_t baud)
{
  #if Board_STM32
  
    SoftwareSerial SWSerial(SW_RX_pin, SW_TX_pin);
    SWSerial.begin(115200);
    m_serialPort = SWSerial;
      
  #endif
}

Contained in the .h file

  private:
    // Variables
    Stream* m_serialPort;

Example of how the stream is used

      int bytesThisLoop = min(m_serialPort->availableForWrite(), bytesRemaining);

it's probably a better idea to let the user of your class configure the stream and give you a reference to that stream for your class

that would let the user of your class use standard and known process for defining the pins, the baud rate, possibly use something else than 8N1 etc and will be more portable likely

1 Like

Ideally I would prefer to try keep the initialisation of the port inside the class

what is a TC_SWI ?
does it make sense for the TC_SWI to own the Serial interface ?

Serial exists outside your class whereas the SoftwareSerial instance can only exist (if you want to pass it as a pointer to the constructor) if you create it outside your class.

you could have two constructors, one for HardwareSerial and one for SoftwareSerial and the class' instance variable to store that would be a Stream pointer

It is a our class for controlling TMC stepper drivers so the interface is only used from within the class and should never be used from outside.

We have everything in the class working for HW serial and I am trying to implement the SW serial component because one of our historical boards does not have a spare serial port available

I know there is already a commonly used library TMC Lib but it was deemed not the right choice prior to me inheriting the project.

Still not a compelling or cogent reason to attempt initializing hardware and software Serial inside the class. It's much cleaner to let the user code choose the interface and initialize it. Then pass a Stream reference into the class keeping it generic and agnostic to the physical interface.

ABSTRACTION is a key characteristic of Object Oriented Programming.

I agree with @gfvalvo . It's not a good design.

When you pass Serial, it's been instantiated for you already - right?
why don't you do the same for Software Serial?

Points taken about it possibly not being the best way.

If I was wanting to understand how I would implement the overloaded function from within the class, is anyone able to help me understand what I am doing wrong and why the SW serial version does not work?

it's not an overload if you use a Stream pointer to reference the port

you would do something like this, when creating the Serial handle outside the class. (remember a Stream does not have a begin function for example and you don't want to call begin in the constructor)

#include <SoftwareSerial.h>

class SerialHandler : public Print {
  public:
    SerialHandler(SoftwareSerial &ss) : serialInstance(&ss) {}
    SerialHandler(HardwareSerial &hs) : serialInstance(&hs) {}
    virtual size_t write(uint8_t data) {return serialInstance->write(data);}
    using Print::write; // Use other write functions from Print class
    int available() {return serialInstance->available();}
    int read() {return serialInstance->read();}
    int peek() {return serialInstance->peek();}
    void flush() {serialInstance->flush();}

  private:
    Stream *serialInstance; // Pointer to a Stream object (either SoftwareSerial or HardwareSerial)
};

SoftwareSerial softSerial(10, 11);
SerialHandler softHandler(softSerial);
SerialHandler hardHandler(Serial);

void setup() {
  softSerial.begin(9600);
  Serial.begin(115200);
}

void loop() {}

Why inherit from Print and not Stream?

OP wanted two constructors (with an opportunity to add specific code related to the type of Stream that was passed if needed), that was the idea.

but that's not how I would do it :slight_smile:

This can't work because SWSerial is a local variable that goes out of scope at the end of the begin function. (Assuming you meant m_serialPort = &SWSerial.) You'll just end up with a dangling pointer, and your code will probably crash horribly.


While I do agree that it's best to have the user provide the serial port object, there are ways to still be able to call begin on it, even though that function is not part of the Stream interface.

For example, if you have a reasonably recent standard library:

https://godbolt.org/z/nK8Px1xhY

#include <variant>

class MyWrapper {
  public:
    MyWrapper(HardwareSerial &s) : stream(&s) {}
    MyWrapper(SoftwareSerial &s) : stream(&s) {}

    void write() { std::visit([](auto *s) { return s->write(); }, stream); }
    void begin(unsigned long long baud) { std::visit([&](auto *s) { s->begin(baud); }, stream); }

  private:
    std::variant<HardwareSerial *, SoftwareSerial *> stream;
};
int main() {
    HardwareSerial hws;
    SoftwareSerial sws{2, 3};

    MyWrapper w1{hws};
    MyWrapper w2{sws};

    w1.begin(12345);
    w1.write(...);
    w2.begin(67890);
    w2.write(...);
}

Or a more low-level solution without using std::variant:

https://godbolt.org/z/Ybod8sjfx

class MyWrapper {
  public:
    MyWrapper(HardwareSerial &s)
      : stream(&s), begin_func(make_begin_func(&s)) {}
    MyWrapper(SoftwareSerial &s)
      : stream(&s), begin_func(make_begin_func(&s)) {}

    void write() { stream->write(); }
    void begin(unsigned long long baud) { begin_func(stream, baud); }

  private:
    Stream *stream;
    using begin_func_t = void(Stream *, unsigned long long baud);
    begin_func_t *begin_func;
    // This function returns a pointer to a function that takes a
    // pointer to base (Stream) and performs an unchecked downcast
    // to the given type S. It then invokes S's begin method with
    // the given baud rate.
    // Only do this kind of unchecked casting if well encapsulated,
    // so no mismatch between the stream pointer and the begin_func
    // function can arise.
    template <class S>
    static begin_func_t *make_begin_func(S *) {
        return +[](Stream *s, unsigned long long baud) {
            return static_cast<S *>(s)->begin(baud); // Note: unsafe downcast
        };
    }
};
1 Like

that's cool. C++17 needed, right ?

The first one requires C++17 (for std::variant), but the second one works with C++11 as well (and can even be made to work in C++98 with some minor tweaks: GCC 4.6.4 - https://godbolt.org/z/KhWf9sz7G).

cool - I need to find time to explore this https://godbolt.org stuff - seems you can do lots of fun stuff with it

Thanks for the help. Other than copying this code I do not understand exactly what is happening so will do more research to understand it better.

Any chance you have an example on how you recommend doing it by passing a stream?

take my example from post 9 and keep only one constructor like this

SerialHandler(Stream &ss) : serialInstance(&ss) {}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.