How to draw a circle on LCD128x64 and also draw custom chars?

Hello,

I’ve been trying to develop my own code with the LCD128x64. And I’m quite happy that I learned a lot of things and enjoyed working with this LCD.

I’ve done pixel/line draw, send image and default non graphical chars.

I know drawing a box would be relatively easy but drawing a circle is really difficult I didn’t know what is the correct arithmetic to do it.

I want to learn how to draw a circle and custom chars. And also learn other things about the best way to use this LCD for GUI projects.

Here’s my code:

/*
	Project name:		lcd128x64
	Author:				r1s8k
	Date:				22/12/2019, 12:25 AM
	
	Project specs:		This is the source file of the lcd128x64 driver that uses 
						MCP23017 as the lcd driver using SPI interface
	
*/

#include <avr/io.h>
#include <Arduino.h>
#include <SPI.h>
#include <avr/pgmspace.h>
#include "lcd128x64_spi.h"

////////////////////////////////////////////////////////////////////////////////////////////
// global variables
static uint8_t graphics, horizontal,y_pre,p_dr_act,block_pre;
//static uint16_t p_buf;
static uint16_t p_pre;
uint8_t p_hi,p_lo,p_block,p_pos,p_bit;

////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
// control functions

// init
void lcd128x64_init(void){
	pinMode(CS_PIN, OUTPUT);
	pinMode(CLK_PIN, OUTPUT);
	pinMode(MOSI_PIN, OUTPUT);
	digitalWrite(CS_PIN, LOW);							// disable CS_PIN
	SPI.begin();
	_delay_ms(100);										// trying initial delay
	digitalWrite(CS_PIN, HIGH);							// enable CS_PIN
	
	_delay_ms(50);										// datasheet says > 40ms
	lcd128x64_cmd(FUNCTION_SET_BASIC);	//_delay_us(28);// datasheet says > 100us - 72 = 28us
	lcd128x64_cmd(FUNCTION_SET_BASIC);	//_delay_us(28);// important for going to / coming back from graph mode
	lcd128x64_cmd(DISABLE_V_SCROLL);					// if you have extended list to scroll
	lcd128x64_cmd(DISPLAY_CONTROL);		//_delay_us(28);// display = 1, cursor = 0, blink = 0
	lcd128x64_cmd(ENTERY_MODE);							// cursor = right, display shift = 0

	lcd128x64_cmd(DISPLAY_CLEAR);	
	lcd128x64_cmd(RETURN_HOME);
	graphics = 0;
	horizontal = 0;
}
// command tx
void lcd128x64_cmd(uint8_t cmd){
	SPI.transfer(CMD_MSK);
	SPI.transfer(cmd & 0xf0);
	SPI.transfer(cmd << 4);
	if(cmd == 0x01)_delay_us(1600); else _delay_us(72);
}
// data tx
void lcd128x64_data(uint8_t data){
	SPI.transfer(DATA_MSK);
	SPI.transfer(data & 0xf0);
	SPI.transfer(data << 4);_delay_us(72);
}
// move cursor
void lcd128x64_move_cursor(uint8_t row, uint8_t col){
	if		(row == 1)lcd128x64_cmd(0x80 + (col - 1));
	else if	(row == 2)lcd128x64_cmd(0x90 + (col - 1));
	else if	(row == 3)lcd128x64_cmd(0x88 + (col - 1));
	else if	(row == 4)lcd128x64_cmd(0x98 + (col - 1));	
}
// clear lcd
void lcd128x64_clr(void){
    uint8_t y,x;   
    lcd128x64_graphics_set_mode(1);
    for(y=0;y<32;y++){
		for(x=0;x<8;x++){   
			lcd128x64_cmd(0x80+y);lcd128x64_cmd(0x80+x); 
			lcd128x64_data(0x00);lcd128x64_data(0x00);
		}
    }
	for(y=32;y<64;y++){   
		for(x=0;x<8;x++){   
			lcd128x64_cmd(0x80+y-32);lcd128x64_cmd(0x88+x);   
			lcd128x64_data(0x00);lcd128x64_data(0x00);
		}   
	}
}
// graphics mode
void lcd128x64_graphics_set_mode(uint8_t mode){
	lcd128x64_cmd(FUNCTION_SET_EXTENDED);
	if(mode){
		lcd128x64_cmd(GRAPHICS_ON1);
		lcd128x64_cmd(GRAPHICS_ON2);
		graphics = GRAPH;
	}
	else{
		lcd128x64_cmd(GRAPHICS_OFF1);
		lcd128x64_cmd(GRAPHICS_OFF2);
		graphics = TEXT;
	}
}
// write chars
void lcd128x64_write_chars(uint8_t row, uint8_t col, uint8_t *str, uint8_t w){
// row1: 80H~8FH // row2: 90H~9FH // row3: 88H~AFH // row4: 98H~BFH
	if(graphics){lcd128x64_graphics_set_mode(0);}
	
	if		(row == 1)lcd128x64_cmd(0x80 + (col - 1));
	else if	(row == 2)lcd128x64_cmd(0x90 + (col - 1));
	else if	(row == 3)lcd128x64_cmd(0x88 + (col - 1));
	else if	(row == 4)lcd128x64_cmd(0x98 + (col - 1));
	
	uint8_t i;
	for(i=0; i<w; i++){lcd128x64_data(str[i]);}
}

////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
// draw functions

// pixel set
void lcd128x64_pixel_set(uint8_t y_axis, uint8_t x_axis){
	uint16_t p_buf;
	if(!graphics){lcd128x64_graphics_set_mode(1);}

	p_block = x_axis / 16;				// calculate x_axis block
	x_axis ^= 127;
	p_pos = (x_axis % 16);				// calculate the position of pixel in 16-bit	
	p_buf = (1 << p_pos);				// set the bit
	
	if(!p_dr_act){
		p_dr_act = 1;					// set pixel draw activate flag
		y_pre = y_axis;					// copy y_axis current position
		block_pre = p_block;			// store the first block
	}
	
	if(block_pre != p_block){			// if block changed
		p_pre = 0;						// clear global pixel buffer
		block_pre = p_block;			// store new block, 
	}									// to disable triggering this check
	
	if(y_axis == y_pre){
		p_pre |= p_buf;					// put current pixel in global buffer
		p_buf |= p_pre;					// put previous pixels in local buffer	
	}
	else{								// when changing the row, 
		y_pre = y_axis;					// copy new row position
		p_pre = 0;						// clear global buffer
		p_pre = p_buf;					// store the last pixel position
	}

	if(y_axis<32){
		lcd128x64_cmd(0x80+y_axis);
		lcd128x64_cmd(0x80+p_block);
		lcd128x64_data(p_buf>>8);
		lcd128x64_data(p_buf);
	}
	else{
		lcd128x64_cmd((0x80+y_axis)-32);
		lcd128x64_cmd(0x88+p_block);
		lcd128x64_data(p_buf>>8);
		lcd128x64_data(p_buf);
	}	
}
// pixel clear
void lcd128x64_pixel_clear(uint8_t y_axis, uint8_t x_axis){   
	uint16_t p_msk;
	if(!graphics){lcd128x64_graphics_set_mode(1);} 	
	p_block = x_axis / 16;
	x_axis ^= 127;
	p_pos = (x_axis % 16);
    p_msk &= ~(1 << p_pos);

	if(y_axis<32){
		lcd128x64_cmd(0x80+y_axis);
		lcd128x64_cmd(0x80+p_block);
		lcd128x64_data(p_msk>>8);
		lcd128x64_data(p_msk);
	}
	else{
		lcd128x64_cmd((0x80+y_axis)-32);
		lcd128x64_cmd(0x88+p_block);
		lcd128x64_data(p_msk>>8);
		lcd128x64_data(p_msk);
	}		
}
// draw image
void lcd128x64_img(unsigned char *img){
    uint8_t y,x;   
    if(!graphics){lcd128x64_graphics_set_mode(1);}

    for(y=0;y<32;y++){
		for(x=0;x<8;x++){
			lcd128x64_cmd(0x80+y);  
			lcd128x64_cmd(0x80+x);
			lcd128x64_data(pgm_read_byte_near(&img[(y*16)+(x*2)]));  
			lcd128x64_data(pgm_read_byte_near(&img[(y*16)+((x*2)+1)]));
		}
    }
	for(y=32;y<64;y++){   
		for(x=0;x<8;x++){
			lcd128x64_cmd((0x80+y)-32);   
			lcd128x64_cmd(0x88+x);
			lcd128x64_data(pgm_read_byte_near(&img[(y*16)+(x*2)]));  
			lcd128x64_data(pgm_read_byte_near(&img[(y*16)+((x*2)+1)]));  
		}   
	}	
}

// draw line
void lcd128x64_line(uint8_t y0, uint8_t y1, uint8_t x0, uint8_t x1){
	uint8_t ih,iw,j,h,w,div;
	h = y1 - y0;
	w = x1 - x0;
	if(w>h){
		div = w/h;
        for(ih=y0,iw=x0;iw<x1;iw++){
            lcd128x64_pixel_set(ih,iw);
            if((iw%div)==(div-1)){ih++;}
        }		
	}
	else{
		div = h/w;
        for(ih=y0,iw=x0;ih<y1;ih++){
            lcd128x64_pixel_set(ih,iw);
            if((ih%div)==(div-1)){iw++;}
        }			
	}
}

And there is the current function to draw a circle I’m working on:

// draw circle
void lcd128x64_circle(uint8_t y, uint8_t x, uint8_t r){
	uint8_t py,px,d,f;
	if(y>x){
        d = y/x;
        for(py=y-r,px=x,f=0;py<y;py++){
            lcd128x64_pixel_set(py,px);
            //if(((px%d))==(d-1)){py++;}
            if((py%(d%f))==1){px++;f++;}
        }
	}
	else{
        d = x/y;
        for(py=y-r,px=x;px<x+r;px++){
            lcd128x64_pixel_set(py,px);
            if(((px%d))==(d-1)){py++;}
        }
	}
}

Look up Bresenham circle algorithm. There are also implementations in C that you can adapt.

marco_c:
Look up Bresenham circle algorithm.

marco_c:
There are also implementations in C that you can adapt.

http://rosettacode.org/wiki/Bitmap/Midpoint_circle_algorithm#C

Circles can be drawn pixel by pixel as follows:

float variables: x_center, y_center, x_edge, y_edge, radius, angle

the engine consists of a cosine function to calculate x_edge and a sine function to calculate y_edge. i ahve put these instructions in the subroutine draw_the_circle.

Here is the sketch for my 3.5 inch TFT 320x480. I will attempt to modify this for my 128*64 LCD display (a ST_7920, using Olikraus’ library u8g).

// TFT003_one_time_circle
// for TFT #003 3.5 inch ILI 9481

// Photoncatcher
// Jan 19, 2020
// public domain

   #include <SPI.h>                       
   #include <Adafruit_GFX.h>         
   #include <MCUFRIEND_kbv.h>    

   MCUFRIEND_kbv tft;
  
   #define LCD_CS A3 
   #define LCD_CD A2 
   #define LCD_WR A1 
   #define LCD_RD A0 
   #define LCD_RESET A4 

// color definitions
   #define BLACK   0x0000
   #define WHITE   0xFFFF

   float x_center = 240;
   float y_center = 160;
   float x_edge = 0;
   float y_edge = 0;
   float radius = 120;
   float angle_circle = 0; 
   int j = 0;

void setup() {

   uint16_t ID;
   ID = tft.readID ();           //valid for Uno shields  
   tft.reset();
   tft.begin (ID);
   tft.setRotation (3); // landscape upright text
   tft.fillScreen (BLACK);
   tft.drawRect (0,0,480,320,WHITE);


// draw a circle once

   for (j=0; j<360; j++){
     draw_the_circle ();
     }
  
}

void loop() {

}

void draw_the_circle (){

   angle_circle = (j*0.01745331);        // angle expressed in radians - 1 degree = 0,01745331 radians
   x_edge = (x_center + (radius*cos(angle_circle)));
   y_edge = (y_center + (radius*sin(angle_circle))); 
   tft.drawPixel(x_edge,y_edge, WHITE); 
}

@photoncatcher - what if radius is only, say, 50 pixels?

You’d need to calculate a different angular increment.

Better to only plot pixels that lie on the circumference of a given circle, and exploit symmetry to reduce the number of expensive calculations .

(Calling a function that plots a single pixel “draw_the_circle” is misleading at best)

TheMemberFormerlyKnownAsAWOL:
@photoncatcher - what if radius is only, say, 50 pixels?

You'd need to calculate a different angular increment.

Better to only plot pixels that lie on the circumference of a given circle, and exploit symmetry to reduce the number of expensive calculations .

(Calling a function that plots a single pixel "draw_the_circle" is misleading at best)

For line draw function >> if you check my code, for a given line. The line is drawn pixel by pixel that the input go to function parameters then it call a for loop to draw all the pixels. Is this the good way ? If not, could you tell me how to develop more optimized function.

If I don't want to call pixel set function a lot of times, then I guess I have to use more buffer.

Even sending an image requires to write bytes to all locations of the LCD.

Once center coordinates have been defined and a radius the endine does all the work.

I migrated my earlier 320480 TFT pixel-by-pixel circle sketch to my 12864 LCD display. This display has a ST7020 or equivalent controller that works perfectly with Olikraus’ u8g library

of course I changes x_center, y_center to 64,31 and the radius to 30.

(see for manual: userreference · olikraus/u8glib Wiki · GitHub)

here is the sketch - in a hurry; this one keeps drawing circles endlessly

// LCD_128x64_one_time_circle
// by Photoncatcher
// u8g library by Olikraus 
// January 19, 2020
// public domain

   #include "U8glib.h"
   U8GLIB_ST7920_128X64 u8g(13, 11, 12, U8G_PIN_NONE);

   float x_center = 64;
   float y_center = 31;
   float x_edge = 0;
   float y_edge = 0;
   float radius = 30;
   float angle_circle = 0; 
   int j = 0;


void setup(void) {

  // assign default color value
  if ( u8g.getMode() == U8G_MODE_R3G3B2 ) {
    u8g.setColorIndex(255);     // white
  }
  else if ( u8g.getMode() == U8G_MODE_GRAY2BIT ) {
    u8g.setColorIndex(3);         // max intensity
  }
  else if ( u8g.getMode() == U8G_MODE_BW ) {
    u8g.setColorIndex(1);         // pixel on
  }
  else if ( u8g.getMode() == U8G_MODE_HICOLOR ) {
    u8g.setHiColorByRGB(255,255,255);
  }

}


void loop(void) {
  // picture loop
  u8g.firstPage();  
  do {
    draw();
  } while( u8g.nextPage() );
}


void draw(void) {
 
   for (j=0; j<360; j++){
      angle_circle = (j*0.01745331);                                // angle expressed in radians - 1 degree = 0,01745331 radians
      x_edge = (x_center + (radius*cos(angle_circle)));
      y_edge = (y_center + (radius*sin(angle_circle))); 
      u8g.drawPixel(x_edge,y_edge);
     }

}

Imagine using Pythagoras to start drawing a circle about the origin.
x2 + y2 = r2.

So, driving x (simple increments), we solve for y for the first octant of the circle, and we get eight points for the price of one, by simple symmetry.

@photoncatcher - you’re missing the point - a thirty pixel radius circle doesn’t have 360 pixels on its circumference (and a 120 radius circle has a lot more!) you’re needlessly overplotting (or underplotting).

(And, before you come up with a solution to calculate the angle subtended between two pixels and the centre of the circle, remember that this varies around the circle too)

@photoncatcher - you're missing the point - a thirty pixel radius circle doesn't have 360 pixels on its circumference (and a 120 radius circle has a lot more!) you're needlessly overplotting (or underplotting).

One might play with the for-next stepping

photoncatcher:
One might play with the for-next stepping

…and only plot pixels which actually lie on the circumference of the circle - see reply #7

The absolute number of pixels per circle octant as per reply #7 is screen resolution dependent and radius dependent. Can that be done without using too much memory space ? Using a simple the 'drawCircle' instruction from a library, would that not be much more convenient and saving memory as well?

photoncatcher:
Can that be done without using too much memory space ?

Yes of course it can.
It has the bonus of not requiring sin/cos too.

I used this function, but it’s slow and take about 1.7kb of program memory for only 6 lines!

It gave me this output:

    double i, angle, x1, y1;

	for(i = 0; i < 360; i += 0.1){
		angle = i;
		x1 = r * cos(angle * PI / 180);
		y1 = r * sin(angle * PI / 180);
		lcd128x64_pixel_set(x + x1, y + y1);
	}

I’m thinking of a better way and more accurate.

What is this fixation with 360?

I scribbled this code down earlier - it isn’t compiled and it isn’t tested, but should point you in the right direction (the value of “lim”, for instance, may need to be rounded).
Obviously, you need to substitute your own plot function.

void circle (int xc, int yc, int radius)
{
  unsigned long rsq = radius * radius;
  int lim = radius * (sqrt (2.0) / 2);

  // draw the free ones first.
  plot (xc + radius, yc);
  plot (xc - radius, yc);
  plot (xc, yc + radius);
  plot (xc, yc - radius);
  
  for (int x = 1; x < lim; x++) {
    int y = sqrt (rsq - (x*x));
   // I *think* I covered all the permutations here!
    plot (xc + x, yc + y);
    plot (xc + y, yc + x);

    plot (xc - x, yc + y);
    plot (xc - y, yc + x);

    plot (xc + x, yc - y);
    plot (xc + y, yc - x);

    plot (xc - x, yc - y);
    plot (xc - y, yc - x);
  }
}

TheMemberFormerlyKnownAsAWOL:
What is this fixation with 360?

It's just my first try, I copied it from this link.

Turbo_C/DrawCircle

But I think it's for computer programming not microcontrollers.

wolfrose:
But I think it’s for computer programming not microcontrollers.

I wasn’t aware that there was a difference.

TheMemberFormerlyKnownAsAWOL:
I wasn't aware that there was a difference.

Is there a difference in this case?

Inefficient, incorrect code on a computer is still inefficient incorrect code on a microcontroller.