LCDscope
A PIC18 text LCD oscilloscope
How to build a simple and low-cost oscilloscope with a text LCD?
Overview
LCDscope uses custom characters on a text LCD to build a tiny graphic screen for a mini oscilloscope. Works with both 4x20 and 2x16 text LCDs.
Circuit Schematic
- LCD1 is a 4x20 text LCD (also works with 2x16).
- Input signal is 0-5V.
How it works
LCDscope uses a miniGLCD pseudo-graphic library with functions: miniGLCD_fill, miniGLCD_setPixel, miniGLCD_line, miniGLCD_getPixel. Uses Bresenham's line drawing algorithm.
ISR handles Timer0 for ADC sampling with trigger detection. PORTB buttons for frequency/range/hold control.
Pin assignment:
- RA0: analog input
- RB0-RB4: buttons
- RD2-RD7: LCD 4-bit mode
C Source Code
LCDscope.c
/*
*******************************************************************************
* LCDscope : a PIC18 oscilloscope on a 20x4 text LCD
* also works with a standard 16x4 text LCD
*******************************************************************************
*
* This program shows how to use custom chars on a text LCD to build a tiny
* graphic screen. To illustrate this, here is a quick & dirty mini oscilloscope.
*
* source code for mikro C compiler V8.2
* feel free to use this code at your own risks
*
* target : PIC18 with 10 Mhz crystal, HS PLL
*
* PIC PIN Assignment :
*
* RA0 : analog input, 0-5V
*
* control buttons on PORTB (internal pull-ups, switchs to GND) :
* RB0/RB1 : change horizontal frequency
* RB2/RB3 : change input range
* RB4 : hold/release screen
*
* 4-bit LCD on PORTD :
* RD2 : RS
* RD3 : E
* RD4 : D4
* RD5 : D5
* RD6 : D6
* RD7 : D7
* Note : R/W pin of LCD must be tied to ground.
*
*******************************************************************************
*/
/*********************
* CONSTANTS
*********************/
// if you have a 16x2 standard LCD, untag this line :
//#define LCD16x2
// otherwise, you are supposed to have the nice 20x4 LCD
#ifndef LCD16x2
#define LCD20x4
#endif
#define C_X 4 // number of columns in pseudo-graphic screen
#define C_Y 2 // number of rows in pseudo-graphic screen
#define miniGLCD_x (5*C_X) // number of X pixels in pseudo-graphic screen
#define miniGLCD_y (8*C_Y) // number of Y pixels in pseudo-graphic screen
#define LEVEL 16 // trigger sensitivity (number of ADC points)
// colors of pseudo-graphic screen
#define miniGLCD_COLOR_WHITE 0
#define miniGLCD_COLOR_BLACK 1
#define miniGLCD_COLOR_REVERSE 2
// pseudo-graphic function prototypes
void miniGLCD_fill(unsigned char c) ;
void miniGLCD_setPixel(char x, char y, unsigned char mode) ;
void miniGLCD_line(char x0, char y0, char x1, char y1, unsigned char pcolor) ;
unsigned char miniGLCD_getPixel(char x, char y) ;
/***************
* RAM VARIABLES
***************/
// horizontal frequency structure
struct TIMEBASE
{
unsigned char t0con ;
unsigned char period[8] ;
unsigned char unit ;
} timeBase[] =
{
{ 0b10000100, "1.04857", ' ' },
{ 0b10000011, "524.288", 'm' },
{ 0b10000010, "262.144", 'm' },
{ 0b10000001, "131.072", 'm' },
{ 0b10000000, "65.536 ", 'm' },
{ 0b11000111, "32.768 ", 'm' },
{ 0b11000110, "16.384 ", 'm' },
{ 0b11000101, "8.192 ", 'm' },
{ 0b11000100, "4.096 ", 'm' },
{ 0b11000011, "2.048 ", 'm' },
{ 0b11000010, "1.024 ", 'm' },
{ 0b11000001, "512 ", '\xe4' },
{ 0b11000000, "256 ", '\xe4' },
{ 0b11001000, "128 ", '\xe4' }
} ;
unsigned char tbase = 0 ;
// vertical input range structure
struct INPUT
{
unsigned char div ;
unsigned char *ampl ;
} input[] =
{
{ 4, "2.500" },
{ 2, "1.250" },
{ 1, "0.625" },
} ;
unsigned char ipt = 0 ;
unsigned char vdiv ;
#ifdef LCD20x4
// scrolling message
unsigned char msg[][8] =
{
" ", " ", " ", " ",
" LCD ", " SCOPE ", " By ", "BrunoG ",
"* ", " ", " ",
" see ", " more ", "details", " and ",
"dwnload", "mikroC ", "source ", "code on",
" ", "www. ", "micro- ", "example",
"s.com ", "* ", ""
} ;
unsigned char firstMsg = 0 ;
#endif
unsigned char miniGLCD_screen[C_X * C_Y * 8] ; // pseudo-screen bitmap
unsigned char samples[miniGLCD_x] ; // sample table
unsigned char sIdx = 0 ; // sample index
unsigned char trigger = 0 ; // trigger status
unsigned char trigValue = 0 ; // trigger value
unsigned char hold = 0 ; // hold screen flag
unsigned int t0ctr ; // timer 0 overflow counter
/****************************
* INTERRUPT ROUTINE
****************************/
void interrupt()
{
if(INTCON.TMR0IF)
{
if(sIdx < miniGLCD_x) // sampling in progress?
{
if(trigger == 2) // sampling triggered?
{
samples[sIdx++] = miniGLCD_y - (ADRESH >> vdiv) ;
}
else if(trigger == 1) // maximum detected
{
if((trigValue > LEVEL) && (ADRESH < trigValue - LEVEL))
{
trigger = 2 ;
}
else
{
if(ADRESH > trigValue)
{
trigValue = ADRESH ;
}
}
}
else // looking for maximum
{
if((trigValue < 255 - LEVEL) && (ADRESH > trigValue + LEVEL))
{
trigger = 1 ;
trigValue = 0 ;
}
else
{
if(ADRESH < trigValue)
{
trigValue = ADRESH ;
}
}
}
ADCON0.GO = 1 ; // start ADC
}
t0ctr++ ;
INTCON.TMR0IF = 0 ;
}
}
/*******************************
* UPDATE SETTINGS & DRAW SCREEN
*******************************/
void mkScreen()
{
T0CON = timeBase[tbase].t0con ;
vdiv = input[ipt].div ;
ADCON1 = 0b00001110 ;
ADCON0 = 0b11000001 ;
#ifdef LCD20x4
LCD_out(1, 14, timeBase[tbase].period) ;
LCD_chr(2, 15, timeBase[tbase].unit) ;
LCD_out(3, 16, input[ipt].ampl) ;
LCD_out(1, 9, hold ? "Hold" : "\xff\xff\xff\xff") ;
#else
LCD_out(1, 12, timeBase[tbase].period) ;
LCD_chr(2, 12, timeBase[tbase].unit) ;
LCD_out(1, 1, input[ipt].ampl) ;
LCD_out(1, 6, hold ? "H" : "\xff") ;
#endif
}
/************************
* DEBOUNCE PORTB KEYS
************************/
void debounce()
{
mkScreen() ;
Delay_ms(20) ;
while(PORTB != 0xff) ;
Delay_ms(20) ;
}
/***********************
* PSEUDO GRAPHIC FUNCTIONS
***********************/
void miniGLCD_fill(unsigned char c)
{
memset(miniGLCD_screen, c, sizeof(miniGLCD_screen)) ;
}
void miniGLCD_setPixel(char x, char y, unsigned char mode)
{
unsigned char *ptr ;
unsigned char mask ;
if(x < 0) return ;
if(y < 0) return ;
if(x > miniGLCD_x) return ;
if(y > miniGLCD_y) return ;
ptr = miniGLCD_screen + (((y * (C_X * 8)) + x) / 8) ;
mask = 1 << (x & 7) ;
switch(mode)
{
case miniGLCD_COLOR_BLACK:
*ptr &= ~mask ;
break ;
case miniGLCD_COLOR_WHITE:
*ptr |= mask ;
break ;
default:
*ptr ^= mask ;
break ;
}
}
unsigned char miniGLCD_getPixel(char x, char y)
{
unsigned char *ptr ;
unsigned char mask ;
if(x < 0) return(0) ;
if(y < 0) return(0) ;
if(x > miniGLCD_x) return(0) ;
if(y > miniGLCD_y) return(0) ;
ptr = miniGLCD_screen + (((y * (C_X * 8)) + x) / 8) ;
mask = 1 << (x & 7) ;
return(*ptr & mask) ;
}
/*
* miniGLCD_line : Bresenham's line drawing algorithm
*/
void miniGLCD_line(char x0, char y0, char x1, char y1, unsigned char pcolor)
{
int dy ;
int dx ;
int stepx, stepy ;
dy = y1 - y0 ;
dx = x1 - x0 ;
if(dy < 0) { dy = -dy ; stepy = -1 ; }
else { stepy = 1 ; }
if(dx < 0) { dx = -dx ; stepx = -1 ; }
else { stepx = 1 ; }
dy <<= 1 ;
dx <<= 1 ;
miniGLCD_setPixel(x0, y0, pcolor) ;
if(dx > dy)
{
int fraction = dy - (dx >> 1) ;
while(x0 != x1)
{
if(fraction >= 0)
{ y0 += stepy ; fraction -= dx ; }
x0 += stepx ;
fraction += dy ;
miniGLCD_setPixel(x0, y0, pcolor) ;
}
}
else
{
int fraction = dx - (dy >> 1) ;
while(y0 != y1)
{
if(fraction >= 0)
{ x0 += stepx ; fraction -= dy ; }
y0 += stepy ;
fraction += dx ;
miniGLCD_setPixel(x0, y0, pcolor) ;
}
}
}
/*
* program custom character n at line pos_row column pos_char
*/
void CustomChar(unsigned char mode, unsigned char n, char pos_row, char pos_char)
{
unsigned char i, j ;
LCD_Cmd(64 + n * 8) ;
for(i = 0 ; i < 8 ; i++)
{
unsigned char bm = 0 ;
for(j = 0 ; j < 5 ; j++)
{
bm <<= 1 ;
bm |= miniGLCD_getPixel(pos_char * 5 + j, pos_row * 8 + i) ? 1 : 0 ;
}
LCD_Chr_Cp(bm) ;
}
LCD_Cmd(LCD_RETURN_HOME) ;
#ifdef LCD20x4
if(mode) LCD_Chr(pos_row + 2, pos_char + 9, n) ;
#else
if(mode) LCD_Chr(pos_row + 1, pos_char + 7, n) ;
#endif
}
/******************
* MAIN LOOP
******************/
void main()
{
unsigned char i, j ;
unsigned int wait ;
TRISA = 0xff ; // PORTA as inputs
TRISB = 0xff ; // PORTB as inputs
INTCON2.NOT_RBPU = 0 ; // enable PORTB pull-ups
TRISD = 0 ; // PORTD is output (LCD)
INTCON.TMR0IF = 0 ;
INTCON.TMR0IE = 1 ;
INTCON.GIE = 1 ;
LCD_Init(&LATD) ;
LCD_Cmd(Lcd_CLEAR) ;
LCD_Cmd(Lcd_CURSOR_OFF) ;
#ifdef LCD20x4
LCD_out(1, 8, "\xff\xff\xff\xff\xff\xff") ;
LCD_out(2, 8, "\xff \xff s/Div") ;
LCD_out(3, 8, "\xff \xff") ;
LCD_out(4, 8, "\xff\xff\xff\xff\xff\xff V/Div") ;
#else
LCD_out(1, 1, " \xff \xff") ;
LCD_out(2, 1, "V/Div\xff \xff s/Div") ;
#endif
// send custom chars
CustomChar(1, 0, 0, 0) ;
CustomChar(1, 1, 0, 1) ;
CustomChar(1, 2, 0, 2) ;
CustomChar(1, 3, 0, 3) ;
CustomChar(1, 4, 1, 0) ;
CustomChar(1, 5, 1, 1) ;
CustomChar(1, 6, 1, 2) ;
CustomChar(1, 7, 1, 3) ;
mkScreen() ;
for(;;)
{
if((hold == 0) && (sIdx == miniGLCD_x))
{
miniGLCD_fill(0) ;
// draw wave
for(i = 0 ; i < miniGLCD_x - 1 ; i++)
{
j = i + 1 ;
miniGLCD_line(i, samples[i], j, samples[j], miniGLCD_COLOR_WHITE) ;
}
// program custom chars
CustomChar(0, 0, 0, 0) ;
CustomChar(0, 1, 0, 1) ;
CustomChar(0, 2, 0, 2) ;
CustomChar(0, 3, 0, 3) ;
CustomChar(0, 4, 1, 0) ;
CustomChar(0, 5, 1, 1) ;
CustomChar(0, 6, 1, 2) ;
CustomChar(0, 7, 1, 3) ;
trigValue = 255 ;
trigger = 0 ;
sIdx = 0 ;
}
// change horizontal frequency
if(PORTB.F0 == 0)
{
tbase++ ;
if(tbase == sizeof(timeBase) / sizeof(struct TIMEBASE))
{ tbase = 0 ; }
hold = 0 ;
debounce() ;
}
else if(PORTB.F1 == 0)
{
if(tbase == 0)
{ tbase = sizeof(timeBase) / sizeof(struct TIMEBASE) - 1 ; }
else
{ tbase-- ; }
hold = 0 ;
debounce() ;
}
// change vertical range
else if(PORTB.F2 == 0)
{
ipt++ ;
if(ipt == sizeof(input) / sizeof(struct INPUT))
{ ipt = 0 ; }
hold = 0 ;
debounce() ;
}
else if(PORTB.F3 == 0)
{
if(ipt == 0)
{ ipt = sizeof(input) / sizeof(struct INPUT) - 1 ; }
else
{ ipt-- ; }
hold = 0 ;
debounce() ;
}
// hold/release screen
else if(PORTB.F4 == 0)
{
hold ^= 1 ;
debounce() ;
}
#ifdef LCD20x4
// scrolling message
if(wait)
{
if(t0ctr > (1u << (tbase + 5)))
{
firstMsg++ ;
if(msg[firstMsg][0] == 0)
{ firstMsg = 0 ; }
t0ctr = 0 ;
wait = 0 ;
}
}
else if(t0ctr > (1u << (tbase + 1)))
{
j = firstMsg ;
for(i = 1 ; i <= 4 ; i++)
{
if((i == 4) && (msg[j + 1][0] == '*'))
{ wait++ ; }
if(msg[j][0] == '*')
{ LCD_out(i, 1, " ") ; }
else
{ LCD_out(i, 1, msg[j]) ; }
j++ ;
if(msg[j][0] == 0)
{ j = 0 ; }
}
firstMsg++ ;
if(msg[firstMsg][0] == 0)
{ firstMsg = 0 ; }
t0ctr = 0 ;
}
#endif
}
}
Download project
LCDscope project files
Download LCDscope-project.zip for mikroC PRO for PIC18F4620:
LCDscope-project.zip (ZIP, 34 KB)
Includes:
- mikroC PRO project files for PIC18F4620
- Source code (~680 lines)
- HEX files