Passing Teensy 3.0 hardware serial as an object?

Status
Not open for further replies.
I'm making a library to interface the Teensy with a Spektrum Satellite R/C receiver which sends out serial data instead of standard PPM servo pulses. What I'm trying to do is initialize the receiver object by passing the constructor a hardware serial port reference.
Code:
SatelliteRX(HardwareSerial3& _serial);

SatelliteRX::SatelliteRX(HardwareSerial3& _serial) : _serialPort(_serial)
{
	// Do nothing
}
But so far I'm stuck as to how I can tell it which serial port to use without multiple variations of the code (one for each HardwareSerial). I've seen that for the Arduino Mega, all the hardware serial ports (Serial1, Serial2, and Serial3) are still just HardwareSerial objects. So is there a way to make the Teensy's HardwareSerial, HardwareSerial2, and HardwareSerial3 act in that manner? So that I can pass it any hardware serial port and it will use that port?
 
Perhaps this may be of some help. The three HardwareSerialX classes are all subclasses of HardwareSerial, which itself is a subclass of Stream.

It seems to me that if your constructor accepted a reference to a HardwareSerial or possibly a Stream rather than a particular choice from the three, then the code invoking the constructor could provide a reference to whatever serial-ish object is required at the time.

Perhaps I'm not understanding your question?
 
I have tried that actually, swapping out HardwareSerial3& for HardwareSerial&. But if I then pass it Serial3 it doesn't work. Though it's a helpful suggestion, I'm afraid it doesn't solve my problem. Though you do seem to understand it right.
 
Looking deeper into the code in HardwareSerial.h shows my error (for which I apologize). The member functions for HardwareSerial are defined *in its header file*. My suggestion to use HardwareSerial as a base class is incorrect. This gets into C++ that I forgot in the several years since last working in it regularly.

IFF the SatelliteRX object could use only the member functions defined by Stream, then letting the constructor use Stream& would work.

I learned this by looking at hardware/teensy/cores/teensy3/HardwareSerial.h and (finally) noticing that the HardwareSerial "base class" fully defines the behavior of the object as ONLY Serial1. (Lines 88-109 in the teensyduino 1.15 code that I am using). Furthermore, I see in Stream.h that the Stream class is intended to be the working base class. (Evidence: It's pure virtual and objects of type Stream cannot be instantiated directly.)

Based on your work so far, do you think your class can depend only on Stream member functions? If so, I am relatively confident that the constructor should use references to Stream, and it should work (at least syntactically :) ).
 
It could work in theory I think... I'd rather use the HardwareSerial classes as they let me begin the serial port as well, but I suppose I could tolerate making the user do that instead. However I have no idea how I'd go about getting it to use Stream on a given serial port.
 
I have tried that actually, swapping out HardwareSerial3& for HardwareSerial&. But if I then pass it Serial3 it doesn't work.
It looks like some stuff that arguably should be virtual isn't (for whatever reason). Making everything in HardwareSerial virtual should work (ask Paul to fix that).

Your only other option would be to use templates.

Looking deeper into the code in HardwareSerial.h shows my error (for which I apologize). The member functions for HardwareSerial are defined *in its header file*.
It doesn't matter where it's defined. What matters is that it's not virtual.

It could work in theory I think... I'd rather use the HardwareSerial classes as they let me begin the serial port as well, but I suppose I could tolerate making the user do that instead. However I have no idea how I'd go about getting it to use Stream on a given serial port.
It will just work. Nothing you need to do. (As mentioned, just pass Stream& in the constructor.)
 
Another C++ ism...

Good point, constructing and initializing the serial port object would need to apply to the particular port being opened (somewhat reasonable, it's a common idiom in O-O design to perform multiple steps to initialize an object. That's one of several services that a Factory pattern implementation helps with, for example). Once the serial port object has been instantiated and initialized, it is ready to be given to the SatelliteRX constructor.

Define the SatelliteRX class to use Stream& in its constructor and the data member; have the initialization code build a Serial1, Serial2 or Serial3 as needed, then begin() it as needed, and finally invoke the SatelliteRX constructor referring to the instantiated serial object. It's very likely that you'll want to capture the initialization logic in a factory function. It's totally reasonable to implement it as a static method in the SatelliteRX class. That way, the static method has lots of extra influence over the shape of the created SatelliteRX instance object.

...It's *way* too long since I've done any real C++... I've run a test compile to verify the basic idea, but truth to be told, I don't have the time to verify that it's fully functional.
 
Passing a Stream& in the constructor or a begin() function is the most common way. You can also pass a HardwareSerial& if you really want to call begin() within your library.
 
C++ can be subtle... I re-examined the HardwareSerial class definition (quoted here for convenience):

Code:
class HardwareSerial : public Stream
{
public:
	void begin(uint32_t baud)       { serial_begin(BAUD2DIV(baud)); }
	void end(void)                  { serial_end(); }
	virtual int available(void)     { return serial_available(); }
	virtual int peek(void)          { return serial_peek(); }
	virtual int read(void)          { return serial_getchar(); }
	virtual void flush(void)        { serial_flush(); }
	void clear(void)                { serial_clear(); }
	virtual size_t write(uint8_t c) { serial_putchar(c); return 1; }
	size_t write(unsigned long n)   { return write((uint8_t)n); }
	size_t write(long n)            { return write((uint8_t)n); }
	size_t write(unsigned int n)    { return write((uint8_t)n); }
	size_t write(int n)             { return write((uint8_t)n); }
	virtual size_t write(const uint8_t *buffer, size_t size)
					{ serial_write(buffer, size); return size; }
        size_t write(const char *str)	{ size_t len = strlen(str);
					  serial_write((const uint8_t *)str, len);
					  return len; }
};
extern HardwareSerial Serial1;

The begin(), clear() and end() functions are not virtual, so they are syntactically hardwired to Serial1. However, single-byte write() and buffered write() are exemplary of Paul's intent, being virtual. The only functions that are not virtual are begin(), end() and clear(); the rest of the nonvirtual functions invoke virtual functions.

So I'm guessing that BeattieBoy assumed that the whole object is polymorphic, which it is not. Once initialized though, I believe the basic operation of the port is polymorphic. Though I have not tested the hypothesis, I believe that Paul is correct that once begin() is completed on an object, the basic operations of the object are polymorphic.

Standard disclaimers apply: This is based on reading, not testing, and it's 10+ years since I've done any real practice with C++.
 
So to somewhat put this to rest, making the begin function of each HardwareSerial class did the trick! However I'd much rather have a solution that doesn't involve cracking open and editing deeply buried Arduino libraries. So if Paul will change this to be standard in future, then we're done and solved my problem. Otherwise, I'd really like to find a way of doing this that is a little simpler for the average person.

Still, this has all been a huge help!
 
Can you be a bit more specific about what I should consider changing in future versions of Teensyduino?

Making more functions virtual adds RAM usage and call overhead, so there needs to be a good reason to do so. I'm usually more easily convinced by seeing code from actual projects than purely hypothetical analysis.
 
What I did (and what fixed my problem) was making begin() and end() in HardwareSerial.h virtual. As an example, I put my edited version below. I left HardwarSerial (aka Serial1) as it was, to show the changes I made in HardwareSerial2, where I changed begin() and end() to be virtual (though if it were to be an official change it'd probably be worth doing clear() as well, I just don't specifically need it so I didn't bother).
Code:
class HardwareSerial : public Stream
{
public:
	void begin(uint32_t baud)       { serial_begin(BAUD2DIV(baud)); }
	void end(void)                  { serial_end(); }
	virtual int available(void)     { return serial_available(); }
	virtual int peek(void)          { return serial_peek(); }
	virtual int read(void)          { return serial_getchar(); }
	virtual void flush(void)        { serial_flush(); }
	void clear(void)                { serial_clear(); }
	virtual size_t write(uint8_t c) { serial_putchar(c); return 1; }
	size_t write(unsigned long n)   { return write((uint8_t)n); }
	size_t write(long n)            { return write((uint8_t)n); }
	size_t write(unsigned int n)    { return write((uint8_t)n); }
	size_t write(int n)             { return write((uint8_t)n); }
	using Print::write;
};
extern HardwareSerial Serial1;

class HardwareSerial2 : public HardwareSerial
{
public:
	virtual void begin(uint32_t baud)       { serial2_begin(BAUD2DIV(baud)); }
	virtual void end(void)                  { serial2_end(); }
	virtual int available(void)     { return serial2_available(); }
	virtual int peek(void)          { return serial2_peek(); }
	virtual int read(void)          { return serial2_getchar(); }
	virtual void flush(void)        { serial2_flush(); }
	void clear(void)                { serial2_clear(); }
	virtual size_t write(uint8_t c) { serial2_putchar(c); return 1; }
	size_t write(unsigned long n)   { return write((uint8_t)n); }
	size_t write(long n)            { return write((uint8_t)n); }
	size_t write(unsigned int n)    { return write((uint8_t)n); }
	size_t write(int n)             { return write((uint8_t)n); }
	using Print::write;
};
extern HardwareSerial2 Serial2;
Making the functions virtual in all of the HardwareSerial classes solved my problem of not being able to pass HardwareSerial3 to a HardwareSerial& reference (as I would be able to for, say, an Arduino Mega). Making the functions virtual seems to make the Teensy's hardware serial ports useable in a similar way to a standard Arduino, where Serial, Serial1, Serial2, and Serial3 are all just plain HardwareSerial objects.
 
What I did (and what fixed my problem) was making begin() and end() in HardwareSerial.h virtual. As an example, I put my edited version below. I left HardwarSerial (aka Serial1) as it was, to show the changes I made in HardwareSerial2, where I changed begin() and end() to be virtual (though if it were to be an official change it'd probably be worth doing clear() as well, I just don't specifically need it so I didn't bother).
Given the use of begin, end, and presumably clear, I can't imagine they would have any impact in terms of performance, since they are not the primary functions (and the primary function, write is already virtualized).
 
Zombie issue?

Hi,

So, not sure if this is resolved - I'm having a similar issue with an ESP8266 library from Itead (https://github.com/itead/ITEADLIB_Arduino_WeeESP8266).

Trying to put the device on Serial1.

Teensy locks up on the constructor. The constructor takes the Serial1 object, tries to begin it and clear the buffer.

If I mangle the constructor to remove the begin (ie put Serial1.begin(9600); in my main file), and modify the clear to simply perform Serial1.clear() then everything seems to work & I can send/receive etc just fine. Issue is - I'm very new to arduino / teensy / C++ etc, so it took me almost a day to figure this out.

I looked at the teensy hardware related files (latest version of teensyduino) and see that all the methods are now virtualised (I take it this was the issue before), but this doesn't seem to have fixed the problem???

Code snippets as follows:

in my main file:

#include "ESP8266.h"
ESP8266 wifi(Serial1);​

in ESP8266.h:
class ESP8266 {
public:

/**
* Constuctor.
*
* @param uart - an reference of HardwareSerial object with baud 9600.
*/
ESP8266(HardwareSerial &uart); /* baud rate is 9600 */

...

Private:
HardwareSerial *m_puart;

in ESP8266.cpp
ESP8266::ESP8266(HardwareSerial &uart): m_puart(&uart)
{
m_puart->begin(9600);
rx_empty();
}


void ESP8266::rx_empty(void)
{
while(m_puart->available() > 0) {
m_puart->read();
}
}​

Any suggestions as to cause and how to fix "properly"?

Many thanks!!
 
I ran into the exact issue you described.
If I call the begin in the constructor, the program hangs, but not if I move it out.
I have not tried this on arduino, but I take it that it will work in arduino and the problem is in teensy specific.

I am using Serial3, and even if I comment out the implementation code like this, it still hangs.
Code:
	virtual void begin(uint32_t baud) { /*serial3_begin(BAUD2DIV3(baud)); */}

I just moved it to another function for now.
 
I ran into the exact issue you described.
If I call the begin in the constructor, the program hangs, but not if I move it out.

Please post a complete sample program that demonstrates the problem (eg, the "forum rule").

If a reproducible test program is posted (soon), I'll try to get this fixed and into 1.21-beta7. Hopefully that can allow a little time for testing and we can have this fully solved by the 1.21 release.
 
Hi Paul,

Amazed that you're so responsive, thanks!!!

The library in question is at https://github.com/itead/ITEADLIB_Arduino_WeeESP8266

The offending code is examples/TCPserver

Don't even need an ESP8266 at the other end, as it's the constructor which wedges...

EDIT: for total clarity, all I did was change the constructor at the top of the TCPserver example sketch from ESP8266 wifi(Serial); to ESP8266 wifi(Serial1); When that didn't work I then edited the library to change the m_puart->begin(9600) to Serial1.begin() in the constructor; and changed rx_empty(void) to simply Serial1.clear(). That worked, but obviously isn't a 'fix'.

Is that enough to go on for now?
 
Last edited:
It is just as described.
I created a simplified code here
Test.h
Code:
#include <Arduino.h>

class Test
{
  private:
  HardwareSerial& myserial; 
  public:
  Test(HardwareSerial& serial);
  void init();
};

Test::Test(HardwareSerial& serial) : myserial(serial) {
  myserial.begin(115200); //comment out this line to test init
}

void Test::init(){
  myserial.begin(115200);
}

Test.ino
Code:
#include "Test.h"

Test mytest(Serial3);

void setup()
{
//   mytest.init();  //uncomment this line to test init
   Serial.println("I'm here!"); 
   delay(1000);
   Serial.println("I'm here!"); 
   delay(1000);
   Serial.println("I'm here!"); 
   delay(1000);
   Serial.println("I'm here!"); 
   delay(1000);
   Serial.println("I'm here!"); 
   delay(1000);
}

void loop()
{
}
 
Is there a dependency where the usb serial Serial must be initialized first before the other Serial1 to Serial3?
 
I'm working with this now. I've managed to reproduce the error.

I believe what's happening here is called the static initialization order fiasco. The constructor for "Test" is running before the constructor for Serial3.

Serial3 doesn't actually do anything in its constructor, so if you call Serial3.begin() before Serial3's constructor runs, it should work. But you can't call Serial3.begin() because it's a virtual function, where the call depends on a value in RAM in the virtual table. The virtual table gets initialized as part of the Serial3 constructor process.

So, in general, you shouldn't call functions in other objects from ANY C++ constructor that's a static instance, because your static constructor might run before that object's constructor. For Serial3, it would work if we weren't using virtual functions. But the virtual functions are needed for pointers and references to the different serial objects to work correctly.

I'm afraid I just don't see any solution with the C++ language the way it is.
 
I tested the code on an arduino mega, and it works just fine. (I just needed to add Serial.begin(9600); to setup())

So perhaps this limitation is introduced by how serial is implemented in Teensy rather than the language itself.
 
Status
Not open for further replies.
Back
Top