I have a bunch of various size SSD1306 OLED displays kicking around. I've always used them with downloaded drivers and told myself that "one of these days" I was going to dig through that datasheet and write a SSD1306 driver. Since I'm caught up with work and have some free time, and there is really no driver available yet for the Milk-V Duo, I finally started. And of course it wasn't nearly as difficult as I had thought it would be, though the datasheet isn't the easiest to decipher.

I started out writing code using the WiringX library to do I2C. No go. I think that thing is just not flexible enough to send commands the way the SSD1306 wants them in the init. Maybe I'm doing it wrong, but I couldn't get it to work. I'll probably try again later, but for now I took my old bitbang I2C code and used that. Had some trouble getting the display initialized correctly, but finally got the correct settings in there and got it to light up and display some garbage, and then after learning how the various addressing modes worked and writing some code to do it right, display some pixels and lines. Woohoo!

Then I started on a simple rasterizer - not very difficult, and started on a font for it - got as far as 0-9 and A-Z for now. A couple of the letters are ugly still and need some changes, but mostly they look pretty decent.

Next I started writing a line drawing function by reading the algorithm and converting it to code. Had some trouble getting that working, so I just borrowed some DDA line code from geeksforgeeks.com. Not the fastest line algo, but good enough for now.

After writing a bunch of little line drawing demos I realized that the display was very slow. Good enough for doing text, but any graphical stuff was unacceptably slow. I was setting pixels in a framebuffer and then dumping it to GDDRAM in the display. I tried various things, including writing a function that set pixels directly in GDDRAM. That worked fine, but turned out to be slower than the buffer dump method - too much overhead.

Finally I got around to looking at what turned out to be the real problem. My I2C code was slow. I looked at the SSD1306 spec and its minimum I2C clock cycle time is 2.5uS. I measured the cycle times I was putting out in the logic analyzer - they were around 160 to 170uS! Terribly slow. No wonder the display speed was so pathetic.

But I was using usleep(1) delays in my code. So I replaced all usleeps with NOPs, or multiple NOPs where necessary, checking my results in the logic analyzer until the waveforms looked good. I ended up with I2C clock cycles of 3uS. Close enough. The display is very quick now.

This pic is when my bounceline program wasn't erasing the pixel at the end of one line. All works except it wasn't cleaning up after itself properly.

Here's my bounceline code at present (what's running in the video above). It may have bugs and sloppiness. It changes constantly. There's some commented out stuff that's not being used in this particular program.

ssd1306_128x32.c

#include <stdio.h>
#include <stdlib.h>
//#include <time.h>
#include <math.h>
#include <unistd.h>
#include <wiringx.h>
#include "ssd1306_128x32.h"

int dat = 14;
int clk = 15;
unsigned char i2c_address = 0x3c;
unsigned char framebuff[512];

int main(void){
  int x1,y1,x2,y2,x3,y3,x4,y4;
  int dx1,dy1,dx2,dy2,dx3,dy3,dx4,dy4;

  init();

  x1 = 20;
  y1 = 5;
  x2 = 30;
  y2 = 10;
  x3 = 100;
  y3 = 15;
  x4 = 74;
  y4 = 12;
  dx1 = 1;
  dy1 = 1;
  dx2 = 2;
  dy2 = 1;
  dx3 = -3;
  dy3 = 2;
  dx4 = -2;
  dy4 = -1;

  while(1){
    cls();
    line(x1,y1,x2,y2,1);
    line(x2,y2,x3,y3,1);
    line(x3,y3,x4,y4,1);
    line(x4,y4,x1,y1,1);
    x1 += dx1;
    y1 += dy1;
    x2 += dx2;
    y2 += dy2;
    x3 += dx3;
    y3 += dy3;
    x4 += dx4;
    y4 += dy4;
    if((x1 >= 127 - dx1) || (x1 <= 0 - dx1)){
      dx1 = -dx1;
      x1 += dx1;
    }
    if((y1 >= 31 - dy1) || (y1 <= 0 - dy1)){
      dy1 = -dy1;
      y1 += dy1;
    }
    if((x2 >= 127 - dx2) || (x2 <= 0 - dx2)){
      dx2 = -dx2;
      x2 += dx2;
    }
    if((y2 >= 31 - dy2) || (y2 <= 0 - dy2)){
      dy2 = -dy2;
      y2 += dy2;
    }
   if((x3 >= 127 - dx3) || (x3 <= 0 - dx3)){
      dx3 = -dx3;
      x3 += dx3;
    }
    if((y3 >= 31 - dy3) || (y3 <= 0 - dy3)){
      dy3 = -dy3;
      y3 += dy3;
    }
    if((x4 >= 127 - dx4) || (x4 <= 0 - dx4)){
      dx4 = -dx4;
      x4 += dx4;
    }
    if((y4 >= 31 - dy4) || (y4 <= 0 - dy4)){
      dy4 = -dy4;
      y4 += dy4;
    }
    transbuff();
  }
}

void cls(void){
  for(int x=0;x<512;x++)
    framebuff[x] = 0;
  transbuff();
}

//DDA Line Algorithm - from geeksforgeeks.org
void line(int x1,int y1,int x2,int y2,unsigned char o){
  int dx = x2 - x1;           //#2
  int dy = y2 - y1;

  int steps = abs(dx) > abs(dy) ? abs(dx) : abs(dy);

  float xinc = dx / (float)steps;
  float yinc = dy / (float)steps;

  float x = x1;
  float y = y1;
  for(int i = 0;i <= steps;i++){   //#5
    setpix(round(x),round(y),o);        //#6
    x += xinc;
    y += yinc;
  }
}

//set or clear a pixel in framebuff[]
void setpix(int x,int y,unsigned char o){
  int byte;
  unsigned char bit, mask = 1;
  byte = x + ((y / 8) * 128);
  bit = y - ((y / 8) * 8);
  mask <<= bit;
   if(o)
    framebuff[byte] |= mask;
  else
    framebuff[byte] ^= mask;
}

//write framebuff to GDDRAM
void transbuff(){
  int x;
  i2c_start();
  i2c_write(0x40);
  for(x=0;x<512;x++)
    i2c_write(framebuff[x]);
  i2c_stop();
}

void init(){
  //wiringx init
  if(wiringXSetup("duo", NULL) == -1){
    wiringXGC();
  }

  i2c_address <<= 1;            //shift address over and clear write bit

  //i2c pins init
  pinMode(dat,PINMODE_OUTPUT);
  pinMode(clk,PINMODE_OUTPUT);
  digitalWrite(clk, LOW);       //clk == 0 when set as output
  digitalWrite(dat, LOW);       //dat == 0 when set as output
  pinMode(clk,PINMODE_INPUT);   //clk pin high
  pinMode(dat,PINMODE_INPUT);   //dat pin high

  //OLED init
  i2c_start();

  i2c_write(0x00);     //command stream

  i2c_write(0xae);      //display off

  i2c_write(0xa0);      //set normal display (default)

  i2c_write(0xd5);      //setdisplayclockdiv
  i2c_write(0x80);      //the suggested ratio 0x80

  i2c_write(0xa8);      //set mux ratio
  i2c_write(0x1f);      //height is 32 or 64 (always -1)

  i2c_write(0xd3);      //set display offset
  i2c_write(0x00);      //no offset

  i2c_write(0x40);      //set memory startline

  i2c_write(0x8d);      //charge pump
  i2c_write(0x14);      //enable charge pump

  i2c_write(0x20);      //set memory mode
  i2c_write(0x00);      //page mode - horizontal

  i2c_write(0x21);      //set column address start/end
  i2c_write(0x00);      //128 columns (128 bytes wide)
  i2c_write(0x7f);

  i2c_write(0x22);      //set page address start/end
  i2c_write(0x00);      //4 pages (4 bytes - 32 pixels high)
  i2c_write(0x03);

  i2c_write(0xda);      //hardware pin
  i2c_write(0x02);      //for 32 lines

  i2c_write(0x81);      //contrast control
  i2c_write(0x7f);      //set to

  i2c_write(0xd9);      //set precharge
  i2c_write(0xf1);

  i2c_write(0xdb);      //setvcomdetect
  i2c_write(0x40);

  i2c_write(0xa4);      //entire display on

  i2c_write(0xa6);      //normal display

  i2c_write(0xaf);      //display on

  i2c_write(0x2e);      //stop scroll

  i2c_stop();
}

//*************************
//* bitbang I2C functions *
//*************************
unsigned char i2c_write(unsigned char x){
  unsigned char i;
  asm("nop");
  for(i=0;i<8;i++){               //clock out data byte
    pinMode(dat,PINMODE_OUTPUT);  //set data bit low
    if(x & 0x80)                  //if output bit is high
      pinMode(dat,PINMODE_INPUT); //then set data bit high
    i2c_clock();                  //clock it out
    x <<= 1;                      //shift next bit into position
  }
  //get ack
  pinMode(dat,PINMODE_INPUT);     //set data high
  pinMode(clk,PINMODE_INPUT);     //set clock high
  asm("nop");                     //wait half a clock pulse
  if(digitalRead(dat))            //sample the data bit
    return(1);                    //if high then nack error
  asm("nop");                     //ack good, wait other half of clock pulse
  pinMode(clk,PINMODE_OUTPUT);    //set clock low
  asm("nop");
  pinMode(dat,PINMODE_INPUT);     //set data high
  return(0);
} 

void i2c_start(void){             //send start condition
  pinMode(dat,PINMODE_OUTPUT);    //set data low
  asm("nop");
  pinMode(clk,PINMODE_OUTPUT);    //set clock low
  asm("nop");
  i2c_write(i2c_address);
}

void i2c_stop(void){              //send stop condition
  pinMode(dat,PINMODE_OUTPUT);    //set data low
  asm("nop");
  pinMode(clk,PINMODE_INPUT);     //set clock high
  asm("nop");                     //stop delay
  pinMode(dat,PINMODE_INPUT);     //set data high
  asm("nop");
}

void i2c_clock(void){
  pinMode(clk,PINMODE_INPUT);     //set clock high
  asm("nop");
  asm("nop");
  asm("nop");
  asm("nop");
  pinMode(clk,PINMODE_OUTPUT);    //set clock low
  asm("nop");
}

ssd1306_128x32.h

/*unsigned char font1[] = 
  {
    0xf8,0x14,0x12,0x14,0xf8,0x00,  //A
    0xfe,0x92,0x92,0x92,0x6c,0x00,  //B
    0x7c,0x82,0x82,0x82,0x44,0x00,  //C
    0xfe,0x82,0x82,0x82,0x7c,0x00,  //D
    0xfe,0x92,0x92,0x92,0x82,0x00,  //E
    0xfe,0x12,0x12,0x12,0x02,0x00,  //F
    0x7c,0x82,0x92,0x92,0x74,0x00,  //G
    0xfe,0x10,0x10,0x10,0xfe,0x00,  //H
    0x00,0x82,0xfe,0x82,0x00,0x00,  //I
    0x40,0x80,0x80,0x80,0x7d,0x00,  //J
    0xfe,0x10,0x28,0x44,0x82,0x00,  //K
    0xfe,0x80,0x80,0x80,0x80,0x00,  //L
    0xfe,0x04,0x08,0x04,0xfe,0x00,  //M
    0xfe,0x04,0x18,0x40,0xfe,0x00,  //N
    0x7c,0x82,0x82,0x82,0x7c,0x00,  //O
    0xfe,0x12,0x12,0x12,0x0c,0x00,  //P
    0x7c,0x82,0x82,0xc2,0xfc,0x00,  //Q
    0xfe,0x12,0x12,0x32,0xcc,0x00,  //R
    0x4c,0x92,0x92,0x92,0x64,0x00,  //S
    0x02,0x02,0xfe,0x02,0x02,0x00,  //T
    0x7e,0x80,0x80,0x80,0x7e,0x00,  //U
    0x3e,0x40,0x80,0x40,0x3e,0x00,  //V
    0xfe,0x40,0x20,0x40,0xfe,0x00,  //W
    0x82,0x44,0x38,0x44,0x82,0x00,  //X
    0x02,0x04,0xf8,0x04,0x02,0x00,  //Y
    0xc2,0xa2,0x92,0x8a,0x86,0x00,  //Z
    0x7c,0xc2,0xb2,0x8a,0x7c,0x00,  //0
    0x00,0x84,0xfe,0x80,0x00,0x00,  //1
    0x84,0xc2,0xa2,0x92,0x8c,0x00,  //2
    0x44,0x82,0x92,0x92,0x6c,0x00,  //3
    0x1e,0x10,0x10,0xfe,0x10,0x00,  //4
    0x4e,0x8a,0x8a,0x8a,0x72,0x00,  //5
    0x7c,0x92,0x92,0x92,0x74,0x00,  //6
    0x82,0x42,0x22,0x12,0x0e,0x00,  //7
    0x7c,0x92,0x92,0x92,0x7c,0x00,  //8
    0x4c,0x92,0x92,0x92,0x7c,0x00}; //9
*/
void transbuff(void);
void setpix(int,int,unsigned char);
void line (int,int,int,int,unsigned char);
void cls(void);
void init(void);
unsigned char i2c_write(unsigned char);
void i2c_start(void);
void i2c_stop(void);
void i2c_clock(void);

Next Post Previous Post