I2C-LCD display causes incorrect results on interrupt driven inputs

My code is designed to take input from a flow sensor. I am going to show liters per minute and pulse count, that latter is essentially how much water flowed.

Using a compiler directive, I can decide whether to compile for an I2C-LCD display or output to the computer terminal. The display uses just the I2C wires; the brightness of the display is set with a fixed resistor.

My ISR sets a flag and I determine what to do in the main loop, most of the time, increment a counter and reset the flag

To test the code I inject a 400Hz square wave into pin 2, When I compile for the computer terminal output, the results are within the tolerance of the Arduino Uno, the function generator and the oscilloscope, 400Hz +/- 0.5hz.

When I compile for the I2C-LCD display, the results are way off, 380Hz +/- 0.5Hz.

You will see that I use millis() to determine the time of the pulse cycle. Because the time spent in the ISR plus some overhead is definitely well under 1mSec, I must conclude that the I2C-LCD library is throwing my measurement off 5%, even though I only call the display routines in the same routine I use for output to the terminal.

But how is it doing that? I suspect it is using interrupts.

OSD

-----------------------------code
FrequencyOfPulses003.ino (4.4 KB)

Your sketch:

#define VER 003
#define enableDisplay true
#define sensorConstant 6.6 
const byte pinToMonitor = 2;
boolean LeadingEdgeOccured = false;
unsigned long endCycle;
unsigned long beginCycle = 0;
unsigned long beginAverageCycle;
float frequency;
float AverageFrequency;
unsigned long count = 0;
unsigned long countStart;
float LitersPerMinute;

unsigned long displayPeriodically = millis();

/*
 * Measures Frequency and counts pulses on pin to monitor
 * 
 * The manufacturer provides the formula F=6.6Q(l/min)
 *  and stated that Q is 60 min
 *  I suspect that lpm = F/6.6
 *  If max lpm is 60 lpm, then max F = 200hz, which would be pretty fast
 *  ver 003
 *    removed spin character
*/




#if enableDisplay
// Display
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);

/*
byte s0[8] = {B00000,B00100,B00100,B00100,B00100,B00100,B00000,B00000};  // |
byte s1[8] = {B00000,B00001,B00010,B00100,B01000,B10000,B00000,B00000};  // /
byte s2[8] = {B00000,B00000,B00000,B11111,B00000,B00000,B00000,B00000};  // -
byte s3[8] = {B00000,B10000,B01000,B00100,B00010,B00001,B00000,B00000};  // \\ (backslash)
byte spinPtr = 0;
unsigned long spinTimeStart = millis();
#define spinTimeBetween 250
//-----------------------------------------------------------------------------------spin
void spin(){
  if (millis()-spinTimeStart < spinTimeBetween) return; 
  spinTimeStart = millis();
  lcd.setCursor(15,0);
  lcd.write(byte(spinPtr++%4));
}
//-----------------------------------------------------------------------------------setupDisplay
*/
void setupDisplay(){
  lcd.begin();
  lcd.noBacklight();
  lcd.clear();
/*  
  lcd.createChar(0,s0);
  lcd.createChar(1,s1);
  lcd.createChar(2,s2);
  lcd.createChar(3,s3);
*/
  lcd.setCursor(0,0);
  lcd.print("Flow Sensor ");
  //         0123456789ABCDEF
  lcd.print(VER);

  lcd.backlight();
}
#endif
//-----------------------------------------------------------------------------------ISR acknowledgeLeadingEdge
void acknowledgeLeadingEdge() {// want this to be quick to minimize error in timing
  LeadingEdgeOccured = true;
}
//-----------------------------------------------------------------------------------displayFrequencyAndCount
void displayFrequencyAndCount(){
  char outline[33],outline2[17] ;
  char Hz[5],HzA[5],lpm[5];
  dtostrf(frequency,4,0,Hz);
  dtostrf(AverageFrequency,4,0,HzA);
  dtostrf(LitersPerMinute,4,1,lpm);
  sprintf(outline,"%sHz %sHz   ",Hz,HzA);
  sprintf(outline2,"%sl/m %9lu",lpm,count);
  //         0123456789ABCDEF
  //         ffffHz ffffHz
  //         ff.fl/m  
#if enableDisplay
  lcd.setCursor(0,0);
  lcd.print(outline);
  lcd.setCursor(0,1);
  lcd.print(outline2);
/* 
  lcd.print("   Hz       LPM ");
  lcd.print("           count");
  lcd.setCursor(0,0);lcd.print(frequency,0);
  lcd.setCursor(6,0);lcd.print(LitersPerMinute,1);
  lcd.setCursor(0,1);lcd.print(count);
*/ 
#else
    Serial.print("\r");
    Serial.print(outline);Serial.print(outline2);
#endif  
}
//-----------------------------------------------------------------------------------setup
void setup() {
  Serial.begin(115200);
  Serial.print(F("\r\nFrequency Measurement Version "));Serial.println(VER);
  pinMode(pinToMonitor, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinToMonitor), acknowledgeLeadingEdge, RISING);
#if enableDisplay
  setupDisplay();
#endif
}
//-----------------------------------------------------------------------------------loop
void loop () {
  if (LeadingEdgeOccured){
    LeadingEdgeOccured = false;
    count++;

    endCycle = millis();
    if (endCycle-displayPeriodically > 1000) { // only print to console every second
      if (beginCycle == 0 ){
        beginCycle = millis();
        beginAverageCycle = beginCycle;
        count = 0;
        countStart = 0;
      }
      else {
          displayPeriodically = millis();
          frequency = float(count-countStart) * 1000.0/float(endCycle-beginCycle);
          AverageFrequency =     float(count) * 1000.0/float(endCycle-beginAverageCycle);
          LitersPerMinute = frequency / sensorConstant;
          displayFrequencyAndCount();
          if (endCycle == beginCycle or endCycle == beginAverageCycle) while(1);
          countStart=count;
  
          beginCycle = endCycle;
      }
    }
  }
    
#if enableDisplay
//  spin();
#endif

}

The Wire library uses interrupts and uses time. Even if you capture all the interrupts, the time by the display library in the loop() might cause a miss of reading the flag.

You can start by adding a counter in a interrupt, instead of just setting a flag.
The Arduino Uno has a 8-bit microcontroller, if you use a 8-bit value to count (use byte), then the code is easier.

The variable that is used both in a interrupt and the loop() should be volatile.

I noticed this:

if (endCycle == beginCycle or endCycle == beginAverageCycle) while(1);

No buzzer, no flashing lights, no message to Serial Monitor, no message on display ?
You could set a variable to a error and print that once in a while. I prefer to let the sketch run at all times.

@Koepel Thanks for catching that line. It was for debug, The display would have shown INF for values on the display then entered an infinite loop. That came out in the complete rewrite.

Now what I do is:

o ISR increments a counter (yes, important to be volatile!)
o set the count to 0
o wait for count to be non-zero
o note time start
o again set the count to zero
o wait the sample period
o capture the count as it will change over the next 47mSecs
o calculate the frequency
o calculate the liters per minute
o update the averaging variables.
o display the output.

What is amazing is that the time in the loop not waiting for the sample period goes from 1mSec for output to the terminal to 47mSec for the 32 characters sent to the display. This is not a particularly efficient library.

Since I could measure the not-in-sample-time time I could compensate the average frequency and total pulse count.

The results are satisfying, though not perfect.

The next steps are:
o finish the Processin.org code half written to capture the output to a file.
o build the sensor test setup. (bucket to bucket: plumbing, electrical, etc.)
o test the sensor.

Any ideas why these four lines of code take 46mSec? @Koepel

  lcd.setCursor(0,0);
  lcd.print(outline);
  lcd.setCursor(0,1);
  lcd.print(outline2);

OSD
FrequencyOfPulses005.ino (4.6 KB)

in the LiquidCrystal_I2C each LCD command and each single character adds several delayMicroseconds. Furthermore I2C is not very fast. All together writing to an I2C LCD might take a noticeable time.

you can do following
step 1: you can try my Noiasca LCD Library. The default LCD class for the PCF8574 should print faster than your current library.
step 2: if the speed gain is still not sufficient, you can try the lcd_PCF8574_fast.h variant. It has a different way how a stream of characters is sent to the LCD display. That class will use a buffer and send several characters with one I2C message to the LCD which will significantly increase the transmission time. Especially if you combine your output with sprintf and print a whole line of 16 characters. The fast class will need an additional buffer library.

16 characters printed with the

  • old LiquidCrystal_I2C will take around 23ms
  • lcd_PCF8574.h will take around 11ms
  • lcd_PCF8574_fast.h will take around 7 ms

If speed real matters consider to use parallel interface (down to 1,7ms in 4bit mode) or SPI (2ms)

Thanks, @noiasca! I didn't know that the I2C was so slow. My previous experiences dealt with applications that were only sensitive to whole seconds or more.

I appreciate your input and hope that others will also find what we post here useful.

OSD

@OldSurferDude
If you plan to use Noiasca LCD library, make sure to carefully read the license agreement for the library to make sure it is compatible with your project and where you may want to take your project.
The current s/w license for version 2.x of the library gives me great concern.
It has recently changed from LGPL 2.1 on version 1.x of the library to a proprietary license on version 2.x of the library.
So while the 2.x library code license currently allows direct use for certain use cases (primarily non commercial), it is not really open source.
The license does not even permit making any changes to the code if the library code is ever shared or distributed to others. i.e. you have to ask permission and get the "creator" to accept any changes/modifications to the library code if you publish library code with your project.
This would be problematic if you include the library code in your project source and use a source control site like github as you couldn't ever make any changes to the library code unless they were approved by the creator of the library.

This is what gives me the most concern:

The creator keeps full control over his/her work.

That means that the terms could theoretically change at any point in time, Including for non commercial projects unless you get a specific license agreement from the creator/author.

IMO, this type of license goes completely against the spirit of open source and, me personally, I would look for less restrictive alternatives.

For example, if you want a faster library for hd44780 LCDs using a PCF8574 backpack, that is readily available in the IDE library manager, you could use the hd44780 library with the hd44780_I2Cexp i/o class.
It transfers instructions to the hd44780 LCD 3X faster than LIquidCrystal_I2C on the same h/w using the standard LiquidCrystal and LCD 1.0 APIs.
Even faster if you bump the i2c clock rate up to 400kHZ vs the default 100kHZ.

The hd44780 library is licensed GPL v3.0 which is fully open source.
While GPL v3 is more restrictive than LGPL 2.1 and GPL 2,
GPL v3 still gives you the freedom to make and publish any changes to the code you like.


The speed of the i2c bus is just one component in how fast the LCD display can be updated.
There is also how the library uses the i2c bus in conjunction with the PCF8574 when controlling the LCD. And then there is how the library handles the hd44780 instruction timing.
How those things are handled can make a significant difference.
For example the same LCD using the same i2c backpack using the same i2c bus clock rate can have as much as a 4x or more difference in the amount of time to transfer a byte to the LCD.

A big obstacle in trying to improve the speed is actually the Arduino core library.
The way the Print class API was designed, it passes individual characters to the i/o library. If more than one character were handed down then the i/o library could further optimize and reduce the i2c bus overhead significantly as it could bundle more than a single byte transfer to the LCD per i2c message.
An i/o library can sometimes work around this to provide a performance benefit by buffering characters handed to it from the Print class but it often comes at the expense of breaking compatibility with existing APIs and can create some other types of issues related to buffering.

--- bill

Yes, the problem is the slow speed of I2C and the inefficiencies of most of the drivers. I have recently published a stand alone driver that absolutely optimizes the I2C bus bandwidth and is under the MIT license. Look here GitHub - KStandiford/Fast-LCD-I2C: A fast driver for LCD displays on the I2C bus for Pi Pico and Arduino. You might also try setting whe wire speed to 400000.

If you like my driver or it’s documentation, give me a star!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.