compiler weirdness with switch statement in derived class override...

Status
Not open for further replies.

chlore

Member
I am attempting to extend the ST7735_t3 class to include some customized (albeit high SPI transaction cost) print functions. Specifically I am adding the ability to outline or fill in the background of each printed character based on a enum in the derived class. The issue is I have a switch statement that will only ever execute code from the one of the first few case statements (when it does indeed match)... all other lower statements are skipped (in spite of the case matching the switch variable). On using if statements directly below the switch statement the code executes: validating the input of the switch statement. Is this something that I need to look out for? Are the lower cases of switch statements getting optimized out? (of course I could have fat fingered something but I refactored bunch of different ways and produced a standalone .ino and .h to reproduce the issue, still got the same result... which is why I am here) I have a workaround with the if's, but I really am curious as to what is going on here. I have included code for an .ino and .h that demonstrates the issue. This was compiled with all the different optimizations attempting to figure this out (it is a bit wordy since I started explicitly casting things during debugging).

Arduino 1.8.15
Teensy 4.0 connected to a st7735
Teensy Loader 1.55
only external library is the ST7735_t3.h


tft_xtenderTest.ino
Code:
//tft_xtenderTest.ino

#include "tftXtndr.h"

#define TFT_MISO  12
#define TFT_MOSI  11  //a12
#define TFT_SCK   13  //a13
#define TFT_DC   9 
#define TFT_CS   10  
#define TFT_RST  8

static tftXtndr tft = tftXtndr(TFT_CS, TFT_DC, TFT_RST);

void setup(void) {
  
  Serial.begin(115200);; 
  delay(2000);  // give serial monitor time to come up
  
  // Use this initializer if you're using a 1.8" TFT 128x160 displays
  tft.initR(INITR_BLACKTAB);
  tft.initR(INITR_BLACKTAB);
  tft.initR(INITR_BLACKTAB);
  // yes... 3 times... only way to get st7735 to (consistently) initialize 
  // (one time results in intermittent (1 out of 4 times) white screen of death)

  tft.fillScreen(ST7735_BLACK);
   
  tft.SetCustBackgroundColor(ST7735_BLACK);
  tft.myOutColor = ST77XX_BLUE;
  tft.myBgType = tftXtndr::myBackGroundTypes::outline;  
  tft.println("This should print with a BLUE outline .");
  Serial.println();

  tft.SetCustBackgroundColor(ST7735_MAGENTA);
  tft.myBgType = tftXtndr::myBackGroundTypes::solidBg;
  tft.println("solid: This should print with a MAGENTA solid background.");  
  Serial.println();

  //tft.SetCustBackgroundColor(ST7735_GREEN);
  tft.myOutColor = ST7735_GREEN;
  tft.myBgType = tftXtndr::myBackGroundTypes::outline;
  tft.println("outline: This should print with a GREEN outline background.");  
  Serial.println();

  tft.SetCustBackgroundColor(ST77XX_BLUE);
  tft.myBgType = tftXtndr::myBackGroundTypes::nada;   // just for fun
  tft.println("solid: This should print with a BLUE solid background.");  
  Serial.println();

 tft.myOutColor =  ST77XX_ORANGE;
  tft.myBgType = tftXtndr::myBackGroundTypes::outline;
  tft.println("outline: This should print with a ORANGE outline .");  
  Serial.println();

  tft.SetCustBackgroundColor(ST77XX_ORANGE);
  
  //tft.myOutColor =  ST77XX_ORANGE;
  tft.myBgType = tftXtndr::myBackGroundTypes::solidBg;
  tft.setTextColor(ST77XX_RED);
  tft.println("outline: This should print with a ORANGE background .");  
  Serial.println();
  
}

void loop() {
  // put your main code here, to run repeatedly:
}

tftXtndr.h

Code:
// tftXtndr.h
#include "ST7735_t3.h"
class tftXtndr :
    public ST7735_t3
{
  
public:
  enum myBackGroundTypes : uint8_t
  {
    none = 0,
    outline = 1,
    solidBg = 2,
    nada = 3,
  };
  
  // controller variable used to specify background type.
  myBackGroundTypes myBgType = myBackGroundTypes::solidBg;

  tftXtndr(uint8_t cs, uint8_t rs, uint8_t sid, uint8_t sclk, uint8_t rst) :ST7735_t3(cs, rs, sid, sclk, rst)
  {
    this->myTxtColor = ST77XX_WHITE;
    this->myOutColor = ST77XX_BLACK;
    this->myBakGndColor = ST77XX_BLACK;
    this->myBgType = myBackGroundTypes::none;
  }

  tftXtndr(uint8_t CS, uint8_t RS, uint8_t RST = -1) :ST7735_t3(CS, RS, RST)
  {
    this->myTxtColor = ST77XX_WHITE;
    this->myOutColor = ST77XX_BLACK;
    this->myBakGndColor = ST77XX_BLACK;
    this->myBgType = myBackGroundTypes::none;
  }

  
  //overriding the virtual write function of base class
  size_t write(const uint8_t* buffer, size_t size);


  //overriding the other virtual write function of base class 
  // not needed for this example.
  //size_t write(uint8_t);

  void SetCustBackgroundColor(uint16_t bakGrndColor); 
    
  uint16_t myTxtColor;  // buffer for text color
  uint16_t myOutColor;    // buffer for custom outline color
  uint16_t myBakGndColor;  // buffer for background color
};

void tftXtndr::SetCustBackgroundColor(uint16_t bakGrndColor)
{
  this->textbgcolor = bakGrndColor;
  this->myBakGndColor = bakGrndColor;
}


size_t tftXtndr::write(const uint8_t* buffer, size_t size)
{

  Serial.print(this->myBgType);
  Serial.print(F(","));

  // FOR SOME REASON THE SWITCH IS WILL NOT EXECUTE THE "solidBg" or "nada" cases
  switch ((uint8_t)(tftXtndr::myBackGroundTypes)this->myBgType)
  {
  case (uint8_t)tftXtndr::myBackGroundTypes::outline:
    Serial.print(F("Switch outline: "));
    int16_t x = this->cursor_x;
    int16_t y = this->cursor_y;

    this->setTextColor(this->myOutColor);
    if (x > 0) { this->cursor_x--; }
    if (y > 0) { this->cursor_y--; }
    ST7735_t3::write(buffer, size);

    if (x < (this->_screenHeight - 1)) { this->cursor_x = (x + 1); }
    if (y < (this->_screenHeight - 1)) { this->cursor_y = (y + 1); }
    ST7735_t3::write(buffer, size);

    this->cursor_x = x;
    this->cursor_y = y;
    this->setTextColor(this->myTxtColor); 
    break;
  case(uint8_t)tftXtndr::myBackGroundTypes::solidBg:
    Serial.print(F(" This never executes..."));
    this->textbgcolor = this->myBakGndColor;
    break;
  case (uint8_t)tftXtndr::myBackGroundTypes::nada:
    Serial.print(F("nadda This never executes..."));
    this->textbgcolor = this->myBakGndColor;
    break;
  default:
    Serial.print(F("default"));
    break;
  }

  if (this->myBgType == (uint8_t)tftXtndr::myBackGroundTypes::solidBg)
  {
    Serial.print(F(" IFFFF solidBg"));
    this->textbgcolor = this->myBakGndColor;
  }
  else if (this->myBgType == (uint8_t)tftXtndr::myBackGroundTypes::outline)
  {
    Serial.print(F(" IF outline: "));

    int16_t x = this->cursor_x;
    int16_t y = this->cursor_y;

    this->setTextColor(this->myOutColor);
    if (x > 0) { this->cursor_x--; }
    if (y > 0) { this->cursor_y--; }
    ST7735_t3::write(buffer, size);

    if (x < (this->_screenHeight - 1)) { this->cursor_x = (x + 1); }
    if (y < (this->_screenHeight - 1)) { this->cursor_y = (y + 1); }
    ST7735_t3::write(buffer, size);

    this->cursor_x = x;
    this->cursor_y = y;
    this->setTextColor(this->myTxtColor);
  }

  //this->fillRect(this->cursor_x, this->cursor_y, (6 * size), 8, ST77XX_BLUE);

  return ST7735_t3::write(buffer, size);  
}

Output of code: you should see matching
"2,This never executes..: IFFFF solidBg" statments like the
"1,Switch outline: If outline"
Code:
1,Switch outline:  IF outline: 1,Switch outline:  IF outline: 
2, IFFFF solidBg2, IFFFF solidBg
1,Switch outline:  IF outline: 1,Switch outline:  IF outline: 
3,3,
1,Switch outline:  IF outline: 1,Switch outline:  IF outline: 
2, IFFFF solidBg2, IFFFF solidBg
 
Perhaps test with simpler expressions in your switch and case statements, e.g. does the logic work correctly with a uint_8 variable for the switch, and constants 1,2,3 for the cases?
 
Weirdness continues, but progress... and answer.

Thank you for the input. Taking your advice I simplified down to using just uint8_t types for the switch and the weird behavior persisted... so I further simplified down to just putting Serial.print statements in the switch and it worked! Now it gets weird: Upon adding my outline code back into the switch, it fails to execute anything further down in the switch below the outline code! So after a bunch of whittling down and refactor I found the answer... I was declaring variables inside of the switch statement without managing their scope. This did not generate any compiler errors in my case, but does in fact trigger a jump in code out of the switch (even if it never hits that case)... so is this a bug, feature, or inexperience... (I'm leaning toward the latter...)

Explained somewhat here: https://stackoverflow.com/questions/92396/why-cant-variables-be-declared-in-a-switch-statement

So the moral is: if I'm going to declare variables inside a switch statement enclose them in curly braces. Hindsight: looking back at the compiler output it did complain of cross-initialization and a jump... but let me compile anyway.

-Chlore
 
Sorry, I have not fully read through the code or the like, but I get the impression, like your switch statement, that maybe your cases have multiple ones that could be true and if so that it always takes the first one?
If so probably by design...

That is if you have something like:

Code:
int x = 3;
switch (x) {

case: 1 ... 5: Serial.print("a"); break;
case: 6 ... 10: Serial.print("b"); break;
case 3: Serial.print("I found the 3"); break;
}
The 3 case will never execute as the first case of range 1-5 is true and it will always there...

Think of this as a set of if() else if else if ..
 
While I have in fact fallen victim to out of order switch issues... this time it really was the initialization of a variable in the switch statement that caused the failure.
Throw the following into setup() of a blink.ino or something to reproduce:

Code:
for (int i = 0; i < 5; i++)
{
	switch (i) {
		case 0: 
			Serial.print("All good here!");
			break;
		case 1: 
			{
				int y = 6;
				Serial.print(i);
				Serial.println(". Also, all good here! (The curly braces have saved us.)");
			}
			break;
		case 2:
			int y = 7;  // <- this is the problem. it generates compiler warnings, and triggers switch failure after this case.
			Serial.println(". Houston we have a problem! (but still executes.) Nothing below will ever be hit.");
			break;
		case 3:
			Serial.print(i);
			Serial.println(". major malfunction... switch breaks out in above case. This case and nothing below executes.");
			break;
		case 4:
			Serial.print(i);
			Serial.println(". also dead code never to execute...");
			break;
		default:
			Serial.print(i);
			Serial.println(". default... nope not this either.");
			break;		
	}
}

The non-braced initialization of the variable inside the switch does something weird (read: I don't fully understand) causing a jump out of the switch for all cases below it.
-Chlore
 
The non-braced initialization of the variable inside the switch does something weird (read: I don't fully understand) causing a jump out of the switch for all cases below it.

I noticed that in your code, and it had me scratching my head because some compilers will flag that as an error, as opposed to a warning. I wondered if that limitation existed in C, but not in C++. In any case, yes, I agree that's the problem, and you have the fix.

Just as an aside, I was reading recently about ProtoThreads, which somehow make clever use of switch statements to provide their pseudo-thread-like behavior. There is a warning in the ProtoThread documentation that if you use them, you can't use switch statements elsewhere in your code, and now I think this is why. See this discussion by the author of ProtoThreads as to how they work.

http://dunkels.com/adam/pt/expansion.html
 
So... it's a feature! :)
That is a significant restriction for protothreads. I didn't read the whole specification. Does the switch statement restriction also apply to scoped classes and libraries as well?
-Chlore
 
Does the switch statement restriction also apply to scoped classes and libraries as well?

Do you mean if used in conjunction with ProtoThreads? I don't know. Protothreads are based on "local continuations". Their implementation depends on the platform and compiler, and the restrictions that apply vary between platforms. They are implemented via macros, which somehow combine to create switch statements where the "functions" are actually cases of the switch statement. There are restrictions on the use of local variables, which are now understandable. I only experimented with them a little bit, trying to understand how they work and when they might be used as an alternative to stack-based multi-threading/tasking.
 
Status
Not open for further replies.
Back
Top