My switch case doesn't seem to work correctly

This is more of a C++ question than an Arduino one. Quick disclaimer - I’m very new to coding so please bear with me. If it helps, I’m using an Arduino Uno with all the latest software.

About the project - I am trying to code a function generator (two stage attack/decay envelope generator to be specific) that is triggered by a piezo. Four different potentiometers control the timing and shape of the envelope created. Right now the envelope ramps from 0 to 1 (attack phase) back to 0 again (decay phase), but eventually this value will be mapped to output from a 12 bit DAC (ramping from 0 to 4095 to 0). When a trigger is sensed from the piezo, a timestamp is used with the millis() function that tracks how long an envelope has been active in order to scale the rise and fall accordingly.

That’s not my main issue though. It seems that the first half of the function works until the decay phase, where the arduino is not shifting to look at the other knobs. During the attack portion, the serial prints of the two respective potentiometer values make it to the serial monitor, but the decay knobs do not. It looks like I’ve coded the two stages identically, with only the potentiometer references being different. Have I coded the switch case incorrectly? Is it something else within the main loop that I’m not understanding?

If it helps, I’ve attached an image of what my serial monitor looks like during a trigger. Everything in the attack portion works just how I’d like it to, but it seems to get stuck at the middle (highest point) of the envelope. Retriggering the piezo just resets the envelope and scales an attack portion again.

tl;dr it seems that only the first two cases of the switch code I’ve written are working as expected, and the final case does not get read (if it did, wouldn’t the serial print commands show up in the serial monitor?)

Here’s the code if you’d like to see what I’m talking about.

#include <MapFloat.h>
#include <Wire.h>

int atkpot = A0;              //controls length of attack
int decpot = A1;              //controls length of decay
int atkSlopePot = A4;         //controls the attack curve
int decSlopePot = A3;         //controls the decay curve
int piezo = A2;               //sensor to detect triggers

float atkSlopeMod;
float decSlopeMod;
float envAmp;
float scaledAmp;

float atkTime;
float decTime;

unsigned long envLifetime;
unsigned long trigtime;

bool env_on;      //envelope specific boolean variables
bool attackState;
bool decayState;

int piezoVal;
int env_phase;      //controls the switch case in some functions.

void setup() {
  pinMode(atkpot, INPUT);
  pinMode(decpot, INPUT);
  pinMode(atkSlopePot, INPUT);
  pinMode(decSlopePot, INPUT);
  pinMode(piezo, INPUT);
  Serial.begin(9600);
}

void loop() {

  unsigned long currentTime = millis();       //running timer of milliseconds since program initiated

  envLifetime = (currentTime - trigtime);     //number of milliseconds since the trigger was sensed

  check_piezo();                              //call the function to check for triggers

  lookat_knobs();                             //call the function to check for attack or decay time

  envelope_scaling();                         //call the function to calculate the attack or decay slope

  Serial.print("Piezo: ");                    //print all the parameters
  Serial.print(piezoVal);
  Serial.print(" ");

  Serial.print("Env. On : ");
  Serial.print(env_on);
  Serial.print(" ");

  Serial.print("Phase: ");
  Serial.print(env_phase);
  Serial.print(" ");

  Serial.print("Attack: ");
  Serial.print(attackState);
  Serial.print(" ");

  Serial.print("Decay: ");
  Serial.print(decayState);
  Serial.print(" ");

  Serial.print("Lifetime: ");
  Serial.print(envLifetime);
  Serial.print(" ");

  Serial.print("Amplitude ");
  Serial.print(envAmp);
  Serial.print(" ");

  Serial.print("Scaled ");
  Serial.print(scaledAmp);
  Serial.println(" ");

}

void check_piezo() {
  int piezoVal = analogRead(piezo);   //any reading above 25 from the sensor triggers an envelope
  if (piezoVal < 25) piezoVal = 0;
  if (piezoVal >= 25) {
    env_on = true;        //and bools are set accordingly
    attackState = true;
    decayState = false;
    env_phase = 1;
    envAmp = 0.0;       //envelope starts at 0 amplitude
    envLifetime = 0;        //envelope timer resets
    trigtime = millis();      //time stamp is made when the trigger was hit
  }
}

void envelope_scaling() {
  switch (env_phase) {
    case 0:               //idle phase
      break;

    case 1:              //attack phase
      float atkSlopeKnob = analogRead(atkSlopePot);
      if (atkSlopeKnob < 512) atkSlopeMod = mapFloat(atkSlopeKnob, 512.0, 0.0, 1, 15);
      if (atkSlopeKnob >= 512) atkSlopeMod = mapFloat(atkSlopeKnob, 1024.0, 512.0, 0.02, 1);

      envAmp = ((float)envLifetime / atkTime);    
      if (envAmp > 0.99) envAmp = 1;

      scaledAmp = pow(envAmp, atkSlopeMod);
      if (scaledAmp > 0.99) scaledAmp = 1;

      if (envLifetime >= atkTime) {
        attackState = false;
        decayState = true;
        env_phase = 2;
      }

      Serial.print("Mod: ");
      Serial.print(atkSlopeMod);
      Serial.print(" ");

      break;

    case 2:              //decay phase
      float decSlopeKnob = analogRead(decSlopePot);
      if (decSlopeKnob < 512) decSlopeMod = mapFloat(decSlopeKnob, 512.0, 0.0, 1, 15);
      if (decSlopeKnob >= 512) decSlopeMod = mapFloat(decSlopeKnob, 1024.0, 512.0, 0.02, 1);

      envAmp = (((float)envLifetime - atkTime) / decTime);

      scaledAmp = pow(envAmp, decSlopeMod);
      if (scaledAmp < 0.001) scaledAmp = 0.0;

      Serial.print("Mod: ");
      Serial.print(decSlopeMod);
      Serial.print(" ");

      break;
  }
}

void lookat_knobs() {
  switch (env_phase) {
    case 0:               //idle phase
      break;

    case 1:               //attack phase
      float atkMod = map(analogRead(atkpot), 1024, 0, 0, 100);
      if (atkMod < 50) atkTime = map(atkMod, 0, 50, 0, 1000);
      else if (atkMod > 49) atkTime = map(atkMod, 50, 100, 1000, 10000);

      Serial.print("T_atk: ");
      Serial.print(atkTime);
      Serial.print(" ");

      break;

    case 2:              //decay phase
      float decMod = map(analogRead(decpot), 1024, 0, 0, 100);
      if (decMod < 50) decTime = map(decMod, 0, 50, 1, 1000);
      if (decMod > 49) decTime = map(decMod, 50, 100, 1000, 10000);

      Serial.print("T_dec: ");
      Serial.print(decTime);
      Serial.print(" ");

      break;
  }
}

      float atkSlopeKnob = analogRead(atkSlopePot);

The analogRead() function does not return a float.

      float atkMod = map(analogRead(atkpot), 1024, 0, 0, 100);

The map() function does not return a float.

tl;dr it seems that only the first two cases of the switch code I've written are working as expected, and the final case does not get read (if it did, wouldn't the serial print commands show up in the serial monitor?)

Cases are not read. The switch statement executes some code based on the value matching a case statement. Some proof that env_phase ever gets to 2 is missing from your code/post.

Thanks for the help! It's greatly appreciated! Just a few more questions because I'm such a noob.

  • If the analogRead and map functions don't return floats, why/how are the functions still calculating properly?

  • Would the proof of env_phase need to go in the void loop section? What would that look like? I have the if statement in case one of the envelope_scaling function that changes env_phase to 2, but is there something else that needs to be added? Does this if statement need to be put somewhere else?

Thanks again!

When you assign an integer value (8, 16 or 32 bit) to a variable of type float (or double), it is converted to a float.

For example the code:

float fnumber = 2049;

will result in the variable fnumber containing 2049.0. You can then do any floating point math you'd like on the contents of fnumber.

  • Would the proof of env_phase need to go in the void loop section?

Yes.

What would that look like?

Serial.print("env_phase = ");
Serial.println(env_phase);

If you see env_phase = 2 being printed, but:

switch(env_phase)
{
   case 2:
      Serial.println("In case 2...");
      break;
}

does not result in In case 2… being printed, then I’ll accept your claim that there is a problem with the switch statement.

If you never see env_phase = 2, even when you think it should be printed, then the problem is not with the switch statement.

First, C/C++ syntax does not allow the declaration of a variable in a case construct. The reason deals with some very weird design failures of C which had to be retained in C++.

   case 1:              //attack phase
      float atkSlopeKnob = analogRead(atkSlopePot);
      ...

should not even compile. However

   case 1:              //attack phase
    {
      float atkSlopeKnob = analogRead(atkSlopePot);
      ...
    }
    break;

will compile correctly. Next, it has been pointed out that analogRead does not return a float. This is true, but there is an implicit conversion in C/C++, for example, if I write

float f;
f = 32;
Serial.print(f, 3);

it will, print out 32.000. This is because the language specifies that certain conversions, which we often call "widening conversions", are always valid. Narrowing conversions require an explicit cast, so the compiler knows that you mean it.

byte i;
int j;

...
i = j; // compiler should complain about narrowing assignment
i = (byte)j;  // no complaint, you told the compiler "trust me"

j = i; // valid, because j covers -32768 to 32767, and i will always be in range

float f;
f = i;  // valid; the result will be in the range 0.000 to 255.000
f = j;  // valid; the result will be in the range -32768.000 to 32767.000
i = f; // not valid, because f might be outside the range 0.000 to 255.000,
        // and besides, what if it were 37.25?  You would lose data
i = (byte)f;  // valid, you have told the compiler you accept responsibility for bad data

so the analogRead is converted to a floating point number in the range 0.000 to 1023.000.

The correct code, however, is

static const uint16_t maxvolts = 5000;  // max voltage in millivolts

or you might write

#if defined(...) || defined(...) // ...etc
static const uint16_t maxvolts = 3300;
#else
static const uint16_t maxvolts = 5000;
#endif

where the defined(...)is one of the symbols for a 3.3V processor. If you search far enough in the 6-foot-long command line, you will find things like -DADAFRUIT_FEATHER or something similar; this does a

#define ADAFRUIT_FEATHER

before the compilation begins.

uint16_t adc = analogRead(...);
uint16_t mV = map(adc, 0, 1023, 0, maxvolts);
float V = (float)mV / 1000.00;

I always put explicit (float) casts on integer values to make my intentions clear.

After following the advice of Flounder, I curly bracketed each case statement and voila, the missing serial prints are showing up on the serial monitor! Thanks so much for the help!

I'm still working through some of the variable conversion/casting and math to calculate amplitude correctly, but my biggest question mark has been solved!