Programming (Arduino/Teensy)

I had a quick look and no appropriate thread came up, but I figured it might be useful to have a thread for feedback/help/support for Arduino-based code to go along with some of the other coding threads (C, DSP, etc…). There’s obviously the actual Arduino forum, but I find that music/controller/niche stuff tends to get lost in the churn over there, so it might be good to have a feedback/discussion thread on here for the platform/language.

So I’ve got a DIY controller that I’ve been using a load recently that’s based on a Teency-LC and using an external ADC (ADS1115) for higher resolution output. It works really well for what I want (until I upgrade the physical fader itself, which is a separate discussion).

BUT

I have noticed some funny business. Sometimes the Teensy just drops out and I stop getting MIDI. I have to physically unplug/replug the Teensy to get it all working again. It kind of seems like it’s gotten worse, so I want to see if there’s anything in my code (pasted below) that could be blowing up (for example I have no delay in my main loop).

Can an Arduino/Teensy guru have a quick look over the code and see if there’s anything that could cause the Teensy to drop out and/or if there’s something else I should be looking out for? (like checking to see if there is something physically wrong with the Teensy or ADC)

//////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////
 /*
 *  USB-MIDI high resolution (14-bit) DJ-style crossfader.
 *  by Rodrigo Constanzo, http://www.rodrigoconstanzo.com // rodrigo.constanzo@gmail.com
 *  
 *  coded for
 *     TT Electronics PS45G-C1LBR10KN fader: https://www.digikey.com/product-detail/en/tt-electronics-bi/PS45G-C1LBR10KN/987-1402-ND/2620671
 *     TeensyLC: https://www.pjrc.com/teensy/teensyLC.html
 *     Adafruit ADS1115: https://www.adafruit.com/product/1085
 *  
 *  EXPLANATION
 *  -----------
 *  The code takes analog readings from the ADS1115 external ADC and scales, smooths, and
 *  constrains the output before sending it as high resolution MIDI CCs (using two CCs to
 *  MSB and LSB). The code includes a calibration routine that takes the minimum and maximum
 *  readings of the fader used and stores them in the internal EEPROM. To use the calibration
 *  routine, you must send Program Change message 13 followed by 69, both on channel 11. The
 *  initial state of the device can be reset by sending Program Change message 10 followed by 
 *  110.
 *
 */
//////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

// required libraries
#include <Wire.h>
#include <Adafruit_ADS1015.h>
#include <ResponsiveAnalogRead.h>
#include <EEPROM.h>


// declared variables

// define MIDI channel
const int channel = 11;

// define CC for the first CC (MSB)
const int cc_msb = 1;

// define CC for the second CC (LSB)
// **this should be 32 higher than the MSB if you want it to work properly in apps that accept 14-bit MIDI**
const int cc_lsb = 33;

// internal LED for calibration/reset status
const int led = 13;

// declare the available sensor range
// using a TeensyLC and ADS1115 gives you 80% of 4.096v (3.3v is 80%) so that makes 26400 80% of 32768
const int sensor_range = 26400;

// set output range (14-bit = 16383)
const int output_range = 16383;

// ints for filtering repeats
int current_value = 0;
int previous_value = 0;

// flag to check if in calibration mode
int calibrate_flag = 0;

// flag to check if in reset mode
int reset_flag = 0;

// store whether device has ever been calibrated in order to allow default settings
int has_been_calibrated = EEPROM.read(10);

// by default leave 5% slop at the top end and 2.5% slop at the bottom
int calibrate_min = 0 + (sensor_range / 40);
int calibrate_max = sensor_range - (sensor_range / 20);

// instantiate adafruit library
Adafruit_ADS1115 ads;

// variable to store sensor reading
int16_t adc0;

// initialize sensor reading
ResponsiveAnalogRead analog(0, true);


/////////////////////////// SETUP ///////////////////////////

void setup(void) 
{

  // Serial.begin(9600);

  // look for program change messages (for calibration routine)
  usbMIDI.setHandleProgramChange(OnProgramChange);

  // if the device has been calibrated, use the stored settings instead of default settings
  if (has_been_calibrated == 1) {
    calibrate_min = constrain((BitShiftCombine(EEPROM.read(0), EEPROM.read(1))), 0, sensor_range);
    calibrate_max = constrain((BitShiftCombine(EEPROM.read(2), EEPROM.read(3))), 0, sensor_range);
  }

  /*
  Serial.print("The initial calibrate_min value is");
  Serial.print(" ");
  Serial.println(calibrate_min);

  Serial.print("The initial calibrate_max value is");
  Serial.print(" ");
  Serial.println(calibrate_max);
  */

  // initialize ADS1115 library
  ads.begin();

  // set ADS1115 gain to 1x, to get the maximum possible range with the 3.3v Teensy
  ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV

  // set smoothing resolution
  analog.setAnalogResolution(sensor_range);

  // reset sketch to initial state
  // EEPROM.write(10,255);

}


/////////////////////////// LOOP ///////////////////////////

void loop(void) {

  // read analog pin from ADS1115
  adc0 = ads.readADC_SingleEnded(0);
  analog.update(adc0);

  // scale the available reading range to the available output range (typically 14-bit MIDI)
  current_value = map(analog.getValue(), calibrate_min, calibrate_max, 0, output_range); 

  // constrain value after scaling to avoid exceeding the available range
  current_value = (constrain(current_value, 0, output_range));

  // filter repeats
  if (current_value != previous_value) {
      if (current_value >> 7 != previous_value >> 7) {
        usbMIDI.sendControlChange(cc_msb, current_value >> 7, channel);
      }
      usbMIDI.sendControlChange(cc_lsb, current_value & 127, channel); 
      previous_value = current_value;
  }

  // update MIDI
  usbMIDI.read();
  
}


/////////////////////////// FUNCTIONS ///////////////////////////

// create a single 16-bit int from two 8-bit ints
int BitShiftCombine(unsigned char x_high, unsigned char x_low) {
  int combined; 
  combined = x_high;              //send x_high to rightmost 8 bits
  combined = combined<<8;         //shift x_high over to leftmost 8 bits
  combined |= x_low;                 //logical OR keeps x_high intact in combined and fills in                                                             //rightmost 8 bits
  return combined;
}

// calibration and reset routines to run when receiving program change messages on channel 11
void OnProgramChange(byte channel, byte program) {
  // offset midi program channel message since they count from 1 instead of 0
  program++;
  if (channel == 11) {
    if (program == 13) {
      calibrate_flag = 1;
      } else if (calibrate_flag == 1) {
         if (program == 69) {

          // enable LED notification of status
          digitalWrite(led, HIGH);
          // Serial.println("calibrating!");
          
          while (calibrate_flag == 1) {
            
            // actual calibration function 
            calibrateSensor();

            // terminate while loop
            calibrate_flag = 0;      
          }

          // turn off notification LED
          digitalWrite(led, LOW);

          // add a little buffer at each extreme to ensure the values can achieve the full range
          calibrate_min += 5;
          calibrate_max -= 20;
            
          // clamp and slightly limit reading values and write new min/max values to EEPROM
          EEPROM.write(0, (constrain(calibrate_min, 0, sensor_range / 5)) >> 8);
          EEPROM.write(1, (constrain(calibrate_min, 0, sensor_range / 5)) & 255);
          EEPROM.write(2, (constrain(calibrate_max, sensor_range - sensor_range / 5, sensor_range) - 10) >> 8);
          EEPROM.write(3, (constrain(calibrate_max, sensor_range - sensor_range / 5, sensor_range) - 10) & 255);
          
          // write the fact that device has been calibrated to EEPROM
          EEPROM.write(10, 1);

          /*
          Serial.println("done calibrating!");
          Serial.println(calibrate_min);
          Serial.println(calibrate_max);
          */

         } else {
          calibrate_flag = 0;
         }
    }
  }
    // reset initial state
    if (channel == 11) {
      if (program == 10) {
        reset_flag = 1;
        } else if (reset_flag == 1) {
          if (program == 110) {

          digitalWrite(led, HIGH);
          delay(100);
         
          // write the fact that device has been reset to EEPROM
          EEPROM.write(10, 255);

          // reset minimum and maximum to initial values
          calibrate_min = 0 + (sensor_range / 40);
          calibrate_max = sensor_range - (sensor_range / 20);

          // Serial.println("values reset!");
          
          // turn off notification LED
          digitalWrite(led, LOW);

          } else {
            reset_flag = 0;
          }
    }
  }
}

void calibrateSensor() {
  // declare variables to use in calibration routine
  bool calib_bool = true;
  bool schmitt = false;
  int schmitt_count = 0;

  // set minimum and maximum to absurd values
  calibrate_min = 1000000;
  calibrate_max = -100;
  
  while(calib_bool) {
    adc0 = ads.readADC_SingleEnded(0);
    analog.update(adc0);
    int reading = analog.getValue();

    // update the minimum and maximum values
    if (reading < calibrate_min) {
      calibrate_min = reading;
    }
    if(reading > calibrate_max) {
      calibrate_max = reading;
    }

    // use a schmitt trigger to count how many times the full range of the fader has been moved
    if (reading > sensor_range - (sensor_range / 4) && schmitt == false) {
      schmitt = true;
    }
    if (reading < sensor_range / 4 && schmitt == true) {
      schmitt = false;
      schmitt_count++;
    }

    // stop calibration routine after 4 passes have been made
    if(schmitt_count >= 4) {
      calib_bool = false;
    }
  }
}
1 Like

A few questions…

  • is this connected to your computer - if so what OS?
  • if so, are you running serialosc?
  • is the teensy programmed as MIDI only or as Serial+MIDI?
  • when MIDI drops out is the teensy midi device still listed as a midi device your system?

I wonder about the EEPROM.write - does the documentation mention anything about this resetting anything on the teensy?

1 Like
  • It’s plugged into my desktop (2012 mac mini) running OSX 10.14.5 via a USB3 hub
  • serialosc is installed/running, but no monome equipment plugged in
  • I think I set it up as Serial+MIDI so I could do testing but I’m not 100% sure. If I open up the Arduino app and check for the Port I see this:

I didn’t think to check actually… I’ll leave Audio MIDI Setup open and check it next time it drops out.

I’m also using it in Max, if that matters and I’m using a custom name for it as USB MIDI as detailed on this page (search for “name.c” on the page).

As far as I know, the EEPROM.write shouldn’t have any impact on anything beyond reading/writing whatever you want to it. It was a while back that I wrote this code but I remember looking it up that the EEPROM was good for thousands of writes. Unless there’s bug in the code, it only gets written when the calibrateSensor() function is called.

I can also try reading/writing from different EEPROM memory locations.

In the last few months of using it, it’s acted funny maybe three times, either not showing up at all, or me having to unplug/replug to get it to work again, but today it seemed to fail every 10-15min or so. At one point it even failed as I opened another app (which I found suspicious…).

Which app did you open? (wondering if that app does some setup that kicks the Teensy off for some reason)

The serialosc question - I asked this because sometimes when I have serialosc running I will need to manually reboot a Teensy to get it to program - what’s happening here is that serialosc is holding on to the serial port. Alternately I need to kill serialosc in order to get the Serial Monitor to show me debug.

I use launchctl unload /Library/LaunchAgents/org.monome.serialosc.plist to deactivate serialosc.

You can then reactivate it with launchctl load /Library/LaunchAgents/org.monome.serialosc.plist

I don’t think this should make a difference with MIDI, but it might be worth trying to program the teensy as MIDI only to see if the problem persists. Or also deactivate serialosc and test.

I don’t think the custom name should matter (I do this all the time now) However, if you’ve used the same custom name for the Teensy with multiple, different Teensy’s then I would suggest deleting all of those objects from AudioMIDI Setup. It should re-instantiate itself the next time you plug it it.

Edit - also if this seems Teensy specific, asking on the PJRC forum might be better than the Arduino forum.

I should I have mentioned it, but it was Fusion360, so nothing to do with MIDI/serial at all. It seemed like a “memory” freeze, which is what got me curious about what was going on.

I left the Teensy plugged in overnight and coming to it now, it’s not working in Max. Rather, it’s not sending messages but interestingly, it still shows up as connected to the system (both in Audio MIDI Setup, and if I double-click any of the MIDI objects in Max, I see the Teensy as an available port/device).

For the name, I only have this device with that unique name, and did the clearing/deteling it from AudioMIDI Setup step before.

I’ll try doing the MIDI only mode to see if that helps.

Oh, just to clarify. The fact that I don’t have a delay(1) in the main loop isn’t a problem is it? Like I wouldn’t be flooding the MIDI bus or anything like that? (is that a thing?)

1 Like

I don’t think that’s a thing, but I’m not really sure. I don’t have a delay in my any of the midi processing code I use - but those have mostly been copied from other examples.

It does not look like there’s Sysex being used and that’s one area where it dumps a large amount of data at a time.

Another thing to test might be running the code on a Teensy 3.2 instead of the LC (?)

1 Like

K, that’s good to know. I was initially worried about that, as I used to have all manner of problems way back in the day using Arduino serial->Max where I would even get grey sceens of death (GSOD!) from turning on the serial object in Max with the Arduino running.

Fingers crossed and setting it to MIDI only mode has done the trick. I’ve not done a huge amount of playing with it today, but it’s not dropped out at all.

I’ve got an expression pedal on the way which I plan on modding soon, and the Teensy I have laying around for it is a 3.2 I believe, so I’ll be able to test the same code on that to see if something else funny is going on.

1 Like

Also nice to have a powered usb hub :wink:

Maybe the cpu spike to launch the solid works sent a tiny bit less juice to the peripheral.

Yeah I’m running into a powered hub.

Also did loads of playing yesterday and no dropouts so it’s possible it was a serialosc-related issue.

1 Like

Debugging can be tricky when you can’t have it connected to the computer for trace output.

What I’ve done in these situations is use the LED , either just to show a ‘heartbeat’ ( to know what’s happening in render loop) , or use simple blink patterns to indicate events or such like.

Not ideal, but usually means I can find out more about what’s happening - useful once you’ve stared at the code for too long and need so inspiration!
Besides we all like blinky lights - no?

Edit: I’m usually using something like a nano, so use it’s onboard led, but you can use an external one

2 Likes

Ok, I spoke too soon. I came to it today and it was borked.

So would you just chuck a blinking LED in the main loop and if it’s no longer running something has gone fucked?

I don’t have (or know how to setup) interrupts in my main code though, so not sure how to put in an LED that wouldn’t just be flashing permanently.

Also, what would be the next level of troubleshooting after that? As in, if the LED “dies” at some point, that would point to something in the code being fucked? And if the LED doesn’t die… then something in the computer?

1 Like

I’d try moving some of those global variables to static variables inside functions if possible. For example current_value and previous_value are only used in the loop function so you can move them into that function and declare them static.

I’d get rid of the global channel const. It is confusing since it is also used as an argument name to functions.

I’d be a bit concerned about the calibrateSensor function getting stuck in the while(calib_bool) loop. Turn the led on just before that while statement and turn it off at the very end of that function. I’d bet it gets stuck on at some stage. It is generally a bad idea to rely on external sensor data to break out of an infinite loop.

This bit of code is silly…

while (calibrate_flag == 1) {
// actual calibration function
calibrateSensor();
// terminate while loop
calibrate_flag = 0;
} … change the while to an if

1 Like

certainly that’s where Id start, as its an easy test…

something like

void setup() {
... your code... 
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
...  your code...



  static long prevT = 0;
  static const long blinkT=5000;

  unsigned long currT = millis();
  digitalWrite(LED_BUILTIN, (currT-prevT)>=blinkT ? HIGH : LOW );
  if(currT-prevT>=(blinkT+100)) prevT=currT; 
}

this example will just shortly blink (100ms) every 5 seconds, but only if your code is still executing the main the loop

1 Like

Thanks!

I’ll have a go at implementing these ideas and see how it goes.

Hi,
I’m very new to this but I’m trying my luck with a teensy 3.6.
What I’m trying to do is just to have the teensy register when i send it an i2c message on pin18 and 19.
But it seems like it either isn’t reacting or maybe not even receiving anything.
I’m trying to send it messages from my teletype, with a powered busboard, so that should take care of the pullup resistors, right?
Here you can see my simple code, which compiles fine, and runs and blinks the LED.

#include <i2c_t3.h>

int howManyReceived;

void setup() {
pinMode(LED_BUILTIN,OUTPUT); // LED
//init variable
howManyReceived = 0;

// Setup for Slave mode, address 0x20, pins 18/19, external pullups, 400kHz
Wire.begin(I2C_SLAVE, 0x20, I2C_PINS_18_19, I2C_PULLUP_EXT, 400000);
Wire.onReceive(receiveEvent);

Serial.begin(115200);


}

void loop() {
// blinking LED to make sure it's alive
digitalWrite(LED_BUILTIN,HIGH);
delay(10);
digitalWrite(LED_BUILTIN,LOW);
delay(400);
//print how many things happened
Serial.println(howManyReceived);
}

void receiveEvent() {
// increment howManyReceived
howManyReceived = howManyReceived + 1;

}

The main idea is just to send it a i2c message on the whitewhale adress, and then print the number of messages i’ve sent in the serial monitor.
I’m sending it the white whale position message.

WW.POS 1

Is there something i’m simplifying in my code, or shouldn’t this theoretically work?
I connect SCL to pin 19 and SDA to pin 20, and GND to GND on the teensy.
Any help, pointing in the direction of a resource that can teach me is greatly appreciated.

Thanks
Anders

There’s a bit of extra stuff in the i2c_t3 basic examples. Like the databuf and memset etc (although I can’t tell you exactly what those do).

I’d start over with some of that extra stuff added. Or just use this example with the WW address and see what you get.

FWIW - You could also set the LED to blink when you receiveEvent

// -------------------------------------------------------------------------------------------
// Basic Slave
// -------------------------------------------------------------------------------------------
//
// This creates a simple I2C Slave device which will print whatever text string is sent to it.
// It will retain the text string in memory and will send it back to a Master device if 
// requested.  It is intended to pair with a Master device running the basic_master sketch.
//
// This example code is in the public domain.
//
// -------------------------------------------------------------------------------------------

#include <i2c_t3.h>

// Function prototypes
void receiveEvent(size_t count);
void requestEvent(void);

// Memory
#define MEM_LEN 256
char databuf[MEM_LEN];
volatile uint8_t received;

//
// Setup
//
void setup()
{
    pinMode(LED_BUILTIN,OUTPUT); // LED

    // Setup for Slave mode, address 0x66, pins 18/19, external pullups, 400kHz
    Wire.begin(I2C_SLAVE, 0x66, I2C_PINS_18_19, I2C_PULLUP_EXT, 400000);

    // Data init
    received = 0;
    memset(databuf, 0, sizeof(databuf));

    // register events
    Wire.onReceive(receiveEvent);
    Wire.onRequest(requestEvent);

    Serial.begin(115200);
}

void loop()
{
    // print received data - this is done in main loop to keep time spent in I2C ISR to minimum
    if(received)
    {
        digitalWrite(LED_BUILTIN,HIGH);
        Serial.printf("Slave received: '%s'\n", databuf);
        received = 0;
        digitalWrite(LED_BUILTIN,LOW);
    }
}

//
// handle Rx Event (incoming I2C data)
//
void receiveEvent(size_t count)
{
    Wire.read(databuf, count);  // copy Rx data to databuf
    received = count;           // set received flag to count, this triggers print in main loop
}

//
// handle Tx Event (outgoing I2C data)
//
void requestEvent(void)
{
    Wire.write(databuf, MEM_LEN); // fill Tx buffer (send full mem)
}

There are a few i2c_t3 examples in Arduino at File->Examples->i2c_t3

In AndersSkibsted code the recieveEvents function might not be called because it has the wrong prototype. The Wire.onRecieve takes a function with no return (ie. void) that takes a single int parameter. see https://www.arduino.cc/en/Reference/WireOnReceive I would have though the compiler would give an error.

You might receive data in one big chunk or a byte at a time…or even no data at all. That is why the receiveEvent has a count argument. You should handle the possibilities that count could be 0,1 or any positive number.

The databuf is a area of memory set aside for pulling data from the i2c. The memset sets the databuf to contain only 0’s. The size of the data buffer is 256 bytes. You should make sure your code cannot write more than that into the buffer.

The example above has a few problems. In receiveEvent, it should call “received += count” instead of “received = count” since the receiveEvent could be called more that once before the loop() gets a chance to execute. Also it should check that count is less that MEM_LEN to make sure it doesn’t write off the end of the buffer.

1 Like

Thanks for the explanations on those items. That’s very helpful.

I’m still a bit of a noob with this and was just going from the example scripts provided with the i2c_t3 library.

It worked. Thank you @widdly and @okyeron for your replies.
I don’t know excactly what did it, but using the basic_slave_range worked, as I could then receive on multiple adresses, and I realized i was using the wrong adresses for WW op.
Now I just have to get to know the protocol and how I read and print and manipulate the data.
It seems like i always get the same content in databuf no matter what teletype i2c command i send.
The adress prints fine, but i keep getting the same number 536812592 printed in my serial monitor.
The adress comes up right, but the “data” is the same no matter what. Any advice how to familiarize myself with Wire.read and functions connected to it?
I’ll definitely have a look through the examples.

#include <i2c_t3.h>

// Function prototypes
void receiveEvent(size_t len);
void requestEvent(void);

// Memory
#define MEM_LEN 256
uint8_t databuf[MEM_LEN];
volatile uint8_t target;
volatile uint8_t received;


void setup()
{
pinMode(LED_BUILTIN,OUTPUT); // LED

// Setup for Slave mode, addresses 0x08 to 0x77, pins 18/19, external pullups, 400kHz
Wire.begin(I2C_SLAVE, 0x01, 0xFF, I2C_PINS_18_19, I2C_PULLUP_EXT, 400000);

// Data init
received = 0;
target = 0;
memset(databuf, 0, sizeof(databuf));

// register events
Wire.onReceive(receiveEvent);

Serial.begin(115200);
}

void loop()
{
// print received data
if(received)
{
    digitalWrite(LED_BUILTIN,HIGH);
    Serial.printf("Slave 0x%02X received: '%d'\n", target, databuf);
    received = 0;
    delay(200);
    digitalWrite(LED_BUILTIN,LOW);
}
}

//
// handle Rx Event (incoming I2C data)
//
void receiveEvent(size_t count)
{
if(count<MEM_LEN) {
target = Wire.getRxAddr();  // getRxAddr() is used to obtain Slave address
Wire.read(databuf, count);  // copy Rx data to databuf
received += count;           // set received flag to count, this triggers print in main loop
}
}

I think you are printing the memory address of the buffer. Change the databuf in your printf to databuf[0] to print the first byte.