display.display() call locking up with generic SSD1306 OLED

andnate

Member
I'm using a generic 12864 OLED with the Adafruit SSD1306 library, periodically updating it using elapsedMillis while a higher priority function is running in a timed interrupt (Though I previously had everything running based on elapsedMillis timers and had the same issue). I have an IO pin set to go high when display.display() is called and low when it finishes and can see that initially after bootup it works fine, and display.display() takes 24ms to complete. However eventually the display locks up and stops updating, and I can see that display.display() takes much, much longer, calling immediatley after finishing since it takes longer than the elapsedMillis interval to run.
The display has integrated 4.7k pullup resistors, I added additional pullups which seemed to help the issue but didn't solve it. I can't reliably cause the issue to occur, either.
What could be causing this?
 
Hi, sorry I took so long, here's some code:

C-like:
#include <FreqMeasureMulti.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define W 6               // Single character width
#define H 8               // Single character height
#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels
#define OLED_RESET    -1  // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C //< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define SCROLL_WIDTH 23

// Pins
#define OUT1_PIN 6
#define OUT2_PIN 5
#define OUT3_PIN 4
#define OUT4_PIN 3
#define IN1_INPUT_PIN 22
#define IN2_INPUT_PIN 24
#define DEBUG_PIN 7
#define NUM_PWM_PINS 13 // Pins 0-12 are PWM, usable in setFrequency function

enum Input_Mode {
  ANALOG,
  IN1,
  IN2
};

// Global variables
bool pinState[NUM_PWM_PINS];
bool speedMode[NUM_PWM_PINS]; // 0 for slow, 1 for fast
bool zeros[NUM_PWM_PINS];     // For state checking
float frequencyRead = 0;
float frequencyToSet = 0;
float Analog_HzPerCount;  // Will be overwritten during setup
FreqMeasureMulti freq1;
IntervalTimer setFrequencyTimer;
Filt::FiltVal<float> analogFiltVolts(0, ANALOG_MIN_THRESH, ANALOG_MAX_THRESH, ANALOG_DVALPERSEC); // Initial, Lower, Upper, DeltaPerSec

void setup() {
  Serial.begin(115200);
  // Display configuration
  dispConfig();
 
  int cnt = 1000;     // Will wait for Serial to connect, but timeout if taking too long
  while (!Serial && cnt--) {
    delay(1);
  }

  SDInit_Config();          // Read in SD configuration
 
  dispSetup();        // Display more useful information
 
  if(inputMode == IN1){       // IN1 input mode
    freq1.begin(IN1_INPUT_PIN);
  }
  else if(inputMode == IN2){  // IN2 clock input mode
    freq1.begin(IN2_INPUT_PIN);
  }
  pinMode(OUT4_PIN, OUTPUT);
  pinMode(OUT3_PIN, OUTPUT);
  pinMode(OUT2_PIN, OUTPUT);
  pinMode(OUT1_PIN, OUTPUT);

  setFrequencyTimer.begin(setPinFrequencies, FREQ_SET_INTERVAL);
  setFrequencyTimer.priority(0);
}

void loop() {
  if (((inputMode == IN1) || (inputMode == IN2)) && freq1.available()) { // If in frequency mode and a reading is ready
    freqModeRead();
  }
  else if(inputMode == ANALOG){                  // Analog mode
    analogModeRead();
  }
  noInterrupts();
  frequencyToSet = frequencyRead; // Save a safe copy of frequencyRead for the interrupt to use
  interrupts();
 
  dispUpdate(frequencyRead);
}

void setPinFrequencies(){
  setFrequency(OUT1_PIN, frequencyToSet*OUT1_Scale);
  setFrequency(OUT2_PIN, frequencyToSet*OUT2_Scale);
  setFrequency(OUT3_PIN, frequencyToSet*OUT3_Scale);
  setFrequency(OUT4_PIN, frequencyToSet*OUT4_Scale);
}
// Read frequency data
void freqModeRead(){
  static double sum=0;
  static int count=0;
  // average several reading together, copied from example
  sum = sum + freq1.read();
  count = count + 1;
  if (count > 1) {                    // Average 2 samples
    frequencyRead = freq1.countToFrequency(sum / count);
    sum = 0;
    count = 0;
    Serial.print(F("Freq = "));
    Serial.println(frequencyRead);
  }
}

// Read analog to get frequency data
void analogModeRead(){
  if(sinceAnalogSample >= ANALOG_SAMPLE_INTERVAL){
    sinceAnalogSample = 0;
    float analogVal = analogRead(IN2_INPUT_PIN);
    analogFiltVolts.Update(analogVal/COUNTS_PER_VOLT);
    if(analogFiltVolts.IsSaturated() == '+'){       // Top of scale
      frequencyRead = ANALOG_MAXFREQ;
    }
    else if((analogFiltVolts.IsSaturated() == '-') || ((analogFiltVolts.Out - ANALOG_MIN_THRESH)*Analog_HzPerVolt) < MIN_FREQ){  // Bottom of scale or too slow to output
      frequencyRead = 0;
    }
    else{                                 // Normal range
      frequencyRead = (analogFiltVolts.Out - ANALOG_MIN_THRESH)*Analog_HzPerVolt;
    }
  }
}
// Set frequency of pin using analogWriteFrequency or call to setFrequencySlow, with hysteresis between them
void setFrequency(int pin, float frequencyToSet){
  unsigned long currentMillis = millis();   // capture the latest value of millis() for using in slow mode
  if(frequencyToSet > (FASTSLOW_THRESH + FASTSLOW_HYST)){                                                // Fast mode
    analogWriteFrequency(pin, frequencyToSet); // set frequency
    analogWrite(pin, 128);                  // reset duty cycle to 50%
    speedMode[pin] = 1;                     // Remember we're in fast mode
  } else if((frequencyToSet < FASTSLOW_THRESH - FASTSLOW_HYST) && (frequencyToSet > MIN_FREQ)){        // Slow mode but not stopped
    setFrequencySlow(pin, frequencyToSet, currentMillis);
    speedMode[pin] = 0;                     // Remember we're in slow mode
  } else if(speedMode[pin] == 1){                                             // Inbetween, fast still
    pinState[pin] = 1;                      // Start high coming out of fast mode
    analogWriteFrequency(pin, frequencyToSet); // set frequency
    analogWrite(pin, 128);                  // reset duty cycle to 50%
  }
  else if((speedMode[pin] == 0) && (frequencyToSet > MIN_FREQ)){              // Inbetween, slow still but not stopped
    setFrequencySlow(pin, frequencyToSet, currentMillis);
  }
  else if(frequencyToSet <= MIN_FREQ){                                        // 0 speed, stop output
    digitalWrite(pin, LOW);
  }
}

// Bit-bang outputs at f<18 Hz, slower than analogWriteFrequency can do
void setFrequencySlow(int pin, float frequencyToSet, unsigned long currentMillis){
  //[Bit-bang outputs at slow frequency]
}

// Output static info on display
void dispSetup(){
    //[Display some basic info]
  displayPrintConstants();
  display.display();
}

// Initial display configuration
void dispConfig(){
  Wire.setSDA(18); // Teensy 4.1
  Wire.setSCL(19); // Teensy 4.1
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever //TODO: Remove infinite loops, allow critical functions to work without display
  }

  // Dim a bit
  display.ssd1306_command(SSD1306_SETCONTRAST);                   // 0x81
  display.ssd1306_command(0x01);
 
  // Clear the buffer
  display.clearDisplay();
  display.setTextSize(0);      // Normal 1:1 pixel scale
  //display.setFont(&FreeMono9pt7b);
  //display.setTextColor(SSD1306_WHITE); // Draw white text with "transparent" background - text will glob over itself if updated
  display.setTextColor(SSD1306_WHITE,SSD1306_BLACK); // Draw white text with black backgroud - changes a character
  display.cp437(true);         // Use real 256 char 'Code Page 437' font
  display.setTextWrap(false);
  display.display();
}

// Update useful info on display
void dispUpdate(float freqToUpdate){
  if(sinceDisplayUpdate > DISP_UPDATE_INTERVAL){
    sinceDisplayUpdate = 0;
    //[Update a bunch of info using display.print()]
    if(sinceDisplayConstUpdate > DISP_CONST_UPDATE_INTERVAL){
      sinceDisplayConstUpdate = 0;
      displayPrintConstants();
    }
    // DEBUG: Monitor how long display.display() takes
    digitalWrite(DEBUG_PIN, HIGH);
    display.display();
    digitalWrite(DEBUG_PIN, LOW);
  }
}

// Initialize SD card and read config
void SDInit_Config(){
    //[Read in configuration from SD card]
}

// Re-update the scale and unchanging text on the display incase it gets corrupted over time, does not call display.display()
void displayPrintConstants(){
    //[Update a bunch of info on the display, don't call display.display()]
}

The code is measuring a frequency from one of two different input pins (or an analog voltage with a scale factor) and outputting scaled versions of the input frequency on 4 output channels. The "locking up" behavior I was seeing seems to happen either relatively soon after bootup or not at all. I can pretty consistently cause it to happen if I start in analog input mode with 0V on the input and then ramp up the voltage, but I haven't figured out a way to replicate it in frequency input mode.
I'm using one of those generic 12864 OLEDs that have built in pullup resistors with no additional pullups installed.
 
The analog mode is too complex for me to understand or debug visually. If your sketch works with inputMode IN1 or IN2, but not ANALOG, I would start by disabling the IntervalTimer so that frequency updates cannot occur during the display update.
 
The analog mode is too complex for me to understand or debug visually. If your sketch works with inputMode IN1 or IN2, but not ANALOG, I would start by disabling the IntervalTimer so that frequency updates cannot occur during the display update.
The same issue occurs in all 3 input modes, but I can only reliably reproduce it in ANALOG mode.
In a previous version I had it set up so that frequency updates occurred based on the same elapsedMillis system as everything else and still had the display issues, so I switched to using the IntervalTimer so that frequency updates wouldn't be blocked by the display updating.
 
I'm wondering if anything in my code could be messing with the I2C bus since that's the only thing I can think of that would mess up the display. It's very unpredictable though, the steps to reproduce that I had found previously don't seem to be working anymore. I can wiggle the jumper wires handling the I2C bus and it doesn't cause it, and tried adding external pullups with no luck fixing it.
 
That's what let
I'm wondering if anything in my code could be messing with the I2C bus since that's the only thing I can think of that would mess up the display. It's very unpredictable though, the steps to reproduce that I had found previously don't seem to be working anymore. I can wiggle the jumper wires handling the I2C bus and it doesn't cause it, and tried adding external pullups with no luck fixing it.
That's what led me to question the IntervalTimer. Have you tested whether the same design works okay, but with writes to the serial monitor rather than the OLED display? If it works okay with serial monitor, that would point to the issue being related to OLED display via I2C, but if it also hangs up when using serial monitor, that would point toward some other aspect of the design.
 
The serial monitor prints come much slower due to the main loop being held up by display.display(), but they still do come whereas the display just doesn't respond at all anymore.
 
I2C uses interrupts, so I still suspect an issue related to the IntervalTimer. That function is quite long to execute at interrupt level.
 
I2C uses interrupts, so I still suspect an issue related to the IntervalTimer.
In previous versions of the code I wasn't using IntervalTimer and was still having the issue. Is it documented what priority the I2C interrupt timer is? I could try setting the timer interrupt priority below it.
 
Shoot I totally just realized I misremembered the history of this project. I switched to using IntervalTimer because at slow output frequencies I'm bit-banging the outputs (the setFrequencySlow function), and to maintain good resolution I need to call that function very often. I had an issue where display.display() was taking long enough that setFrequencySlow couldn't run often enough so I put it in an interrupt that had high priority.
So, I guess the real thing I need to fix is how to make display.display() not lock up the program for so long.
 
Well I went back and looked at my notes better, and I the post above is wrong-ish. I did switch to the interval timer because at slow speeds I needed to call setFrequency often, but it only became a problem when the display locked up. I went back to the version of code when I first implemented the display and found that it was still locking up, and even stripped it down to the point that the only thing I'm doing after setup is incrementing a counter on the display and still had issues with it locking up. I took a scope to the I2C pins and saw that on the last I2C message before it locks up the SDA line gets stuck pulled low instead of being released back high. Does this point towards faulty hardware? I'm using those cheap SSD1306 0.96" OLED displays off Amazon and have used multiple Teensys, though I think all the displays are from the same order/vendor so they could be a bad batch or something. But I think they've been used for other projects here with no issues.
I2C bus when it locks up
In normal operation that last blip on the SDA line (orange) would rise to VCC with SCL (blue), but it ends up pulled low. The pulses on SDA also look awfully short. The display seems to have integrated 4.7k pullups so right now I don't have any external ones added, but I have tried adding external ones and it didn't fix the issue so that's not the only issue.
 
Back
Top