Coffee machine display emulation with Arduino - Timing issue

Hello,
I'm trying to make replacement display for my coffee machine using Arduino. Original display was simple 59x12 pixels LED matrix controlled by 9 74HC595 shift registers. LED matrix went defective and I'm trying to make some emulator that can use OLED display for it.
Protocol is pretty simple, there are 3 pins, CLOCK, DATA, LOAD, LAST_COLUMN. Display shows one row at the time after LOAD pin is pulled to HIGH.
First 12 cycles of clock dictates the row to be shown, second 58 cycles dictates the data on that row, and last pixel on the row is lit up when LAST_COLUMN is high (that pin is not necessary since it's on very rarely). Clock is about 80 kHz, with pauses of 600uS between column data burst. (So, 70 clock pulses, 600uS pause). Since data transfer to OLED via I2C is a lot slower than 600uS, I'm dropping some frames because I can live with that. I hacked up some first version of the code, (I'm planning to send 8xRow to display once I got everything working up, and some other optimizations), which works for few seconds and then it stops. The problem is the slowness of the pin pooling which ultimately leads to de-synchronization. Do you have some idea what can be sped up here, or should I use another micro (prototyping on UNO, will put the micro in machine once everything is done). Perhaps, I should turn off the interrupts, since timer interrupt will happen once in a while. I'm reading the pin state from PORB register instead of in interrupt since for that frequency, I think the interrupt will behave more erratically.

Time between two clock pulses is 6uS, so code in state2() function should take less time than that (I don't have any profiling tools to see the actual speed, perhaps I should count machine instructions and check the manual for avr to see cycle count for each of them in that routine).
I've inlined all functions in order to not introduce the function calling overhead (put the parameters on stack, long jump, function exec, popping return address, putting result on stack, etc.)

// PIN0 - Clock
// PIN1 - Data
// PIN3 - Load
// PIN4 - COL59
#define CLK 0b00000100
#define DATA 0b00001000
#define LOAD 0b00010000
#define COL59 0b00001000
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0,U8X8_PIN_NONE);
bool oldclk = false;
uint8_t state = 0;
uint8_t clkNum = 0;
uint8_t rowNum = 0;

char disp[96]; //display memory
void setup() {
  for (int i = 0; i < 96; i++) {
    disp[i] = 0x00;
  }
  pinMode(4, INPUT);
  pinMode(2, INPUT);
  pinMode(3, INPUT);
  u8g2.begin();
  for (;;) {
    switch (state) {
      case 0:
        state0();
        break;
      case 1:
        state1();
        break;
      case 2:
        state2();
        break;
      case 3:
        state3();
        break;
      case 4:
        state4();
        break;
      case 5:
        state5();
        break;
    }
  }

}

// new display frame
inline void state0() {
  while (true)
    if (PIND & LOAD) {
      state = 1;
      clkNum = 0;
      oldclk = 0;
      break;
    }
}

// first row
inline void state1() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum++;
      if (PIND & DATA) {
        if (clkNum == 12) {
          state = 2;
          rowNum = 12;
          clkNum = 0;
          break;
        } else {
          state = 0;
          break;
        }
      }
    }
    oldclk = clk;
  }
}
// read pixels in row
inline void state2() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      char pixel = (PIND & DATA) >> 3;
      uint8_t byteIdx = (12 - rowNum) << 3 + clkNum >> 3;
      uint8_t bitIdx = 7 - clkNum % 8;
      if (pixel) {
        disp[byteIdx] |= 1 << bitIdx;
      } else {
        disp[byteIdx] &= ~(1 << bitIdx);
      }
      clkNum++;
    }
    if (PIND & LOAD) {
      clkNum = 0;
      if (rowNum == 1) {
        state = 4;
        break;
      } else {
        state = 3;
        break;
      }
    }
    oldclk = clk;
  }
}

// other rows
inline void state3() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum++;
      if ((PIND & DATA)) {
        state = 5;
        rowNum = clkNum;
        clkNum = 0;
        break;
      }

    }
    oldclk = clk;
  }
}

// enough data for display
inline void state4() {
  u8g2.clearBuffer();
  u8g2.drawBitmap(20, 10, 8, 12, disp);
  u8g2.sendBuffer();
  state = 0;
  clkNum = 0;
  rowNum = 0;
}

// pause until end of row number payload
inline void state5() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum ++;
      if (rowNum + clkNum == 12) {
        state = 2;
        clkNum = 0;
        break;
      }
    }
    oldclk = clk;
  }
}
void loop() {

}

Here's how one column of data looks like

Current state:

Kind regards

Is there a reason you are not using hardware SPI?

Sure, can try to wire it up as SPI (LOAD pin on CS, SCK - Clock, Data - MOSI).
I can do the processing of the result on that 600uS pause. I should read about SPI on Arduino and I'll try that.
The problem I could see with this solution is that when acting as a SPI slave, Arduino will trigger the interrupt when the SPI buffer is full which is only one byte. That could be mitigated by appending that byte to some buffer inside ISR, but I don't have byte-dividable payload in the first place (70 clock impulses, needs 2 more for the last ISR event)

Ah, yes. That might make using hardware SPI difficult.

Ok, I think this is what I might try.

Use an external interrupt pin for the clock signal. Maybe use another external interrupt pin for the load signal.

Keep all code that reads the input data in the interrupt routine(s). Nothing else except updating the data buffer. Certainly not updating the OLED. Do that only in the main code.

This way, you have 2 processes. One in the interrupt routine(s), which has high priority. The second process in loop() which has lower priority. This lower priority code updates the OLED.

Try removing the " >> 3". It might save a cycle or two. Not very orthodox, but it works.

I noticed you have edited your original code. That is confusing. Publish any new versions in a new post. Please.

Thanks, I will try slightly modified approach.
There will be two interrupts, one for clock on rising edge, and one for LOAD on rising edge.
There is a buffer of nine bytes where whole column should be packed.
LOAD ISR will be slightly heavier since it should check whether the stored buffer makes sense (i.e. row number) and memcpy rest of the data to the display buffer.
Once all 12 rows are stored, I'll detach interrupts and print the screen, after that I will enable them. I can afford to loose few display refreshes since whole screen is refreshed about 60 times per second because of persistence of vision (OLED controller is offloading that job for me), but real data change (eg. animations) are 2 times per second. With this method I plan to achieve about 5 real refreshes per second which will be sufficient. I'm trying that approach tomorrow and I'll post the results and eventually ask for help :).

void counterInterrupt(){
  currentBuffer[count >> 3] |= ((PIND & DATA) >> 3) << bitCount;
  bitCount--;
  bitCount &= 0b00000111;
  count++;

}

This would actually save some cpu time in comparison to the previous ISR.

void counterInterrupt(){
  currentBuffer[count >> 3] |= ((PIND & DATA) << 4) >> bitCount;
  count++;
  bitCount = count & 0b00000111;
}

EDIT: Typo

Code with ISR had some drawbacks, it's sadly not fast enough so I needed more optimization in the tight-loop pin polling approach. This is more robust version of the code, next step is display scaling and memory optimization. I'll post new findings and eventual questions. If anybody needs it, this is for Jura J7 machine.

// PIN0 - Clock
// PIN1 - Data
// PIN3 - Load
// PIN4 - COL59
#define CLK 0b00000100
#define DATA 0b10000000
#define LOAD 0b00010000
#define COL59 0b00001000
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0,U8X8_PIN_NONE);
bool oldclk = false;
uint8_t state = 0;
uint8_t clkNum = 0;
uint8_t byteIndex = 0;
char disp[96]; //display memory

void setup() {
  for (int i = 0; i < 96; i++) {
    disp[i] = 0x00;
  }
  pinMode(4, INPUT);
  pinMode(2, INPUT);
  pinMode(3, INPUT);
  u8g2.begin();
  // disable timer0, don't need it, ISR for millis() steals some time
  _SFR_BYTE(TIMSK0) &= ~_BV(TOIE0);
  for (;;) {
    switch (state) {
      case 0:
        state0();
        break;
      case 1:
        state1();
        break;
      case 2:
        state2();
        break;
      case 3:
        state3();
        break;
      case 4:
        state4();
        break;
      case 5:
        state5();
        break;
    }
  }

}


// new display frame
inline void state0() {
  while (true)
    if (PIND & LOAD) {
      state = 1;
      clkNum = 0;
      oldclk = 0;
      break;
    }
}

// first row
inline void state1() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum++;
      if (PIND & DATA) {
        if (clkNum == 12) {
          state = 2;
          clkNum = 0;
          byteIndex = 0;
          break;
        } else {
          state = 0;
          break;
        }
      }
    }
    oldclk = clk;
  }
}
// read pixels in row
inline void state2() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      // DATA wire moved to D7 in order to skip bit-shifting (saved 500 nS)
      disp[byteIndex + clkNum >> 3] |= ((PIND & DATA)) >> (clkNum & 0b00000111);
      clkNum++;
    }
    if (PIND & LOAD) {
      clkNum = 0;
      if (byteIndex == 11) {
        state = 4;
        break;
      } else {
        state = 3;
        break;
      }
    }
    oldclk = clk;
  }
}

// other rows
inline void state3() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum++;
      if ((PIND & DATA)) {
        state = 5;
        byteIndex = 12 - clkNum;
        break;
      }

    }
    oldclk = clk;
  }
}

// enough data for display
inline void state4() {
  u8g2.clearBuffer();
  u8g2.drawBitmap(20, 10, 8, 12, disp);
  u8g2.sendBuffer();
  for (uint8_t i = 0; i < 96; i++){
    disp[i] = 0;
  }
  state = 0;
  clkNum = 0;
}

// pause until end of row number payload
inline void state5() {
  while (true) {
    uint8_t clk = PIND & CLK;
    if (clk && !oldclk) {
      clkNum ++;
      if (clkNum == 12) {
        state = 2;
        clkNum = 0;
        break;
      }
    }
    oldclk = clk;
  }
}
void loop() {

}

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