Positron8 - Graphic LCD 128x64 Analogue Meter Needle Simulation Experiment

Started by top204, Apr 24, 2024, 07:31 PM

Previous topic - Next topic

top204

With all the talk recently on the forum of analogue meter simulations on graphics LCDs, I did some more experimenting and came up with a set of procedures that move a line on a graphic LCD to simulate the needle moving on an analogue meter. The procedures are easy to understand and change for a different graphic LCD type (colour or monochrome), because I have created procedures to draw a line and draw a circle (filled or not filled). So it is down to you to experiment with the code. It differs significantly from the previous analogue meter simulator I uploaded, in that it is a lot more flexible with the needle drawing and moving mechanism.

The program only has the needle moving on the display, and further experiments will need to be made to draw a flexible scale for the meter, but that can be accomplished ad-hoc by a user, and the scale can be drawn or a bit-image can be used for it. That is the easy part if the meter is used for a dedicated function such as volts or current etc... The difficult part was creating and moving the needle to simulate an analogue meter. :-)

The experimental code listing is below, and it operates on a Samsung KS0108 monochrome graphic LCD with a resolution of 128x64 pixels:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Experimental analogue needle movement simulator.
'
' Note. The program is set to run slow (8MHz) so it will simulate in the proteus simulator.
' Faster running code does not simulate correctly in proteus because of an anomaly in its KS0108 LCD model.
' When running on a real device, the code will work nicely when operating up to 64MHz (or over).
'
' Written for the Positron8 compiler by Les Johnson.
'
    Device = 18F25K20                                       ' Tell the compiler what device to compile for
    Declare Xtal = 8                                        ' Tell the compiler what frequency the device will be operating at (in MHz)
'
' Configure the Samsung KS0108 Graphic LCD
'
    Declare LCD_DTPort = PORTB                              ' Connects to the LCD's data lines (PORTB.0 to PORTB.7)
    Declare LCD_CS1Pin = PORTC.0                            ' Connects to the LCD's CS1 line
    Declare LCD_CS2Pin = PORTC.1                            ' Connects to the LCD's CS2 line
    Declare LCD_RSPin = PORTA.3                             ' Connects to the LCD's RS line
    Declare LCD_RWPin = PORTA.5                             ' Connects to the LCD's RW line
    Declare LCD_ENPin = PORTA.2                             ' Connects to the LCD's EN line
    Declare LCD_Type = Samsung                              ' LCD type (Samsung KS0108)
    Declare Internal_Font = On                              ' Use an internal (flash memory) font

    Include "Font.inc"                                      ' Load the graphic LCD's font table into the program
'
' Create some constants for the graphic LCD's resolution and the needle's position and size
'
$define GLCD_Xres 128                                       ' The X resolution of the LCD (in pixels)
$define GLCD_Yres 64                                        ' The Y resolution of the LCD (in pixels)
$define GLCD_Xmax $eval (GLCD_Xres - 1)                     ' The maximum X resolution of the LCD (in pixels)
$define GLCD_Ymax $eval (GLCD_Yres - 1)                     ' The maximum Y resolution of the LCD (in pixels)

$define cNeedle_Start_XPos $eval (GLCD_Xres / 2)            ' The X position of the needle's start
$define cNeedle_Start_YPos 5                                ' The Y position of the needle's start (bottom of screen)
$define cNeedle_Length 50                                   ' The length of the needle (in pixels)

$define cNeedle_Start_Degree 120                            ' The starting degree position of the needle
$define cNeedle_End_Degree 240                              ' The ending degree position of the needle

$define cMeter_Text "Positron"                              ' The text to display on the meter's front panel
'
' Create global variables used by the procedures here
'
    Dim GLCD_tColour As Bit                                 ' Holds the colour of a pixel (0 or 1, because it is a monochrome LCD)
'
' Create some global variables for the demo here
'
    Dim bADC_Value As Byte                                  ' Holds the ADC value to transfer to the meter

'----------------------------------------------------------------------------
' The main program starts here
' Draw an analogue meter's needle on the graphic LCD
' When the ADC valtage changes, the needle moves in relationship to it
'
Main:
    Setup()                                                 ' Setup the program and any peripherals

    Do                                                      ' Create a loop
        bADC_Value = ADC_Read8(1)                           ' Read the ADC
        Needle_Move(bADC_Value)                             ' Move the needle based upon the ADC value of 0 to 255
    Loop                                                    ' Do it forever

'----------------------------------------------------------------------------------------------
' Setup the program and any peripherals
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    Dim wANSEL As ANSEL.Word                                ' Combine ANSEL and ANSELH into a 16-bit SFR

    DelayMS 200                                             ' Wait for things to stabilise
    Cls                                                     ' Clear the Graphic LCD's display
    GLCD_Circle(cNeedle_Start_XPos, 60, 3, 1, 1)            ' Draw the needle's pivot on the LCD
    wANSEL = 2                                              ' Make pin AN1 an analogue input
    ADC_Init8()                                             ' Initialise the ADC for 8-bit operation
EndProc

'----------------------------------------------------------------------------------------------
' Draw and move a line on the LCD to simulate an analogue meter's needle
' Input     : pValue holds the value show on the meter (0 to 255)
' Output    : None
' Notes     : None
'
Proc Needle_Move(pValue As Byte)
    Dim dTemp       As SDword                               ' A temporary signed Dword for calculations
    Dim wValue      As dTemp.SWord0 = pValue                ' Make a 16-bit signed version of the pValue parameter
    Dim wNdlDegree  As dTemp.Word0                          ' The degrees required for the needle's line
Static Dim wPrevDeg As Word = 0                             ' The previous degrees value of the needle's line
'
' Scale the degrees of the needle line from cNeedle_End_Degree degrees to cNeedle_Start_Degree degrees
' From a value in of 0 to 255
' Equivalent to "wScale(pValue, 0, 255, 240, 120)"
'
    dTemp = (wValue * -cNeedle_Start_Degree) / 255          ' Scale the starting degree position of the needle
    wNdlDegree = dTemp + cNeedle_End_Degree                 ' Scale the ending degree position of the needle

    If wNdlDegree = wPrevDeg Then ExitProc                  ' Don't move the needle if it hasn't changed position
    If wNdlDegree <> wPrevDeg Then                          ' Has the needle moved position since the last time?
        NeedleDraw(cNeedle_Start_XPos, cNeedle_Start_YPos, 4, cNeedle_Length, wPrevDeg, 0)  ' Yes. So erase the needle's line
        Print At 20, 7, cMeter_Text                         ' Display some text on the meter
        wPrevDeg = wNdlDegree                               ' Update the previous position value
    EndIf
    NeedleDraw(cNeedle_Start_XPos, cNeedle_Start_YPos, 4, cNeedle_Length, wNdlDegree, 1)    ' Draw the needle's line
EndProc

'----------------------------------------------------------------------------------------------
' Draw a line at a given angle (in degrees 0-360)
' Input     : pSYpos = Y position of the line's start
'             pSXpos = X position of the line's start
'             pInRadius = Inner radius of the needle's line
'             pOutRadius = Outer radius of the needle's line
'             pDegree = Angle of the line (0 to 360)
'             pColour = Whether the line is drawn or erased
' Output    : None
' Notes     : None
'
Proc NeedleDraw(pSXpos As GLCD_wXpos, pSYpos As GLCD_wYpos, pInRadius As Word, pOutRadius As Word, pDegree As Word, pColour As GLCD_tColour)
    Dim fSinCalc As Float
    Dim fCosCalc As Float
    Dim fTemp    As fCosCalc
    Dim wEXpos   As Word
    Dim wEYpos   As Word

    fTemp = (pDegree * 3.14) / 180.0
    fSinCalc = Sin(fTemp)                                   ' Get the Sin of the degrees
    fCosCalc = Cos(fTemp)                                   ' Get the CoSin of the degrees

    wEXpos = pSXpos + (pOutRadius * fSinCalc)               ' \
    wEYpos = pSYpos - (pOutRadius * fCosCalc)               ' / Calculate the outer radius of the line
    pSXpos = pSXpos + (pInRadius * fSinCalc)                ' \
    pSYpos = pSYpos - (pInRadius * fCosCalc)                ' / Calculate the inner radius of the line
    GLCD_Line(pSXpos, (GLCD_Yres - pSYpos), wEXpos, (GLCD_Yres - wEYpos), pColour)  ' Draw the line
EndProc

'-------------------------------------------------------------------------------------------------
' Draw a Circle on the display
' Input     : pXpos holds the starting X position
'           : pYpos holds the starting Y position
'           : pRadius holds the radius of the circle
'           : pColour holds the colour of the circle (1 is set, 0 is clear)
'           : pFill is 1 if the circle is to be filled
' Output    : None
' Notes     : None
'
Proc GLCD_Circle(pXpos As Word, pYpos As Word, pRadius As Word, pColour As GLCD_tColour, pFill As Bit)
    Dim wDD As Word
    Dim wXX As Word
    Dim wYY As Word
    Dim wTR As Word
    Dim wXpos_S As Word

    Dim wQuadA As Word
    Dim wQuadB As Word
    Dim wQuadC As Word
    Dim wQuadD As Word
    Dim wWidth As wQuadA

    wXpos_S = pXpos                                         ' Transfer pXpos into its working variable
    If pRadius = 0 Then ExitProc                            ' Trap a radius of 0

    wDD = pYpos - wXpos_S
    wXX = 0
    wYY = pRadius
    wTR = 3 - (2 * pRadius)
    While wXX <= wYY
        wQuadA = wXpos_S + wXX
        wQuadB = pYpos + wYY

        wQuadC = wXpos_S - wXX
        wQuadD = pYpos - wYY
        If pFill = 1 Then
            wWidth = wQuadA - wQuadC
            GLCD_HLine(wQuadC, wQuadD, wWidth, pColour)      ' Draw a line for the top half of the circle
            GLCD_HLine(wQuadC, wQuadB, wWidth, pColour)      ' Draw a line for the bottom half of the circle
        Else
            GoSub PlotIt
        EndIf
        wQuadA = pYpos + wYY
        wQuadA = wQuadA - wDD

        wQuadB = pYpos + wXX
        wQuadC = pYpos - wYY

        wQuadC = wQuadC - wDD
        wQuadD = pYpos - wXX
        If pFill = 1 Then
            wWidth = wQuadA - wQuadC
            GLCD_HLine(wQuadC, wQuadD, wWidth, pColour)      ' Draw a line for the top half of the circle
            GLCD_HLine(wQuadC, wQuadB, wWidth, pColour)      ' Draw a line for the bottom half of the circle
        Else
            GoSub PlotIt
        EndIf
        If wTR.SWord < 0 Then
            wTR = wTR + 6
            wTR = wTR + (wXX * 4)
        Else
            wTR = wTR + 10
            wTR = wTR + ((wXX - wYY) * 4)
            Dec wYY
        EndIf
        Inc wXX
    Wend
    ExitProc
PlotIt:
    GLCD_Plot(wQuadA, wQuadB, pColour)                      ' Draw a pixel in the bottom right quadrant of the circle
    GLCD_Plot(wQuadA, wQuadD, pColour)                      ' Draw a pixel in the top right quadrant of the circle
    GLCD_Plot(wQuadC, wQuadB, pColour)                      ' Draw a pixel in the bottom left quadrant of the circle
    GLCD_Plot(wQuadC, wQuadD, pColour)                      ' Draw a pixel in the top left quadrant of the circle
EndProc

'---------------------------------------------------------------------------------------------
' Draw a line to the display
' Input     : pStartXpos holds the start X position of the line
'           : pStartYpos holds the start Y position of the line
'           : pEndXpos holds the end X position of the line
'           : pEndYpos holds the end Y position of the line
'           : pColour holds the colour of the line (0 is clear, 1 is black)
' Output    : None
' Notes     : Not a very flexible mechanism, but a fast one
'
Proc GLCD_Line(pStartXpos As Word, pStartYpos As Word, pEndXpos As Word, pEndYpos As Word, pColour As GLCD_tColour)
    Dim wXpos  As Word
    Dim wYpos  As Word
    Dim wErr   As SWord
    Dim bYstep As SByte
    Dim tSteep As Bit = 0

    If pEndXpos.SWord < 0 Then pEndXpos = 0
    If pEndXpos > GLCD_Xmax Then pEndXpos = GLCD_Xmax
    If pEndYpos.SWord < 0 Then pEndYpos = 0
    If pEndYpos > GLCD_Ymax Then pEndYpos = GLCD_Ymax

    If Abs(pEndYpos - pStartYpos) > Abs(pEndXpos - pStartXpos) Then
        tSteep = 1
    EndIf

    If tSteep = 1 Then
        Swap(pStartXpos, pStartYpos)
        Swap(pEndXpos, pEndYpos)
    EndIf

    If pStartXpos > pEndXpos Then
        Swap(pStartXpos, pEndXpos)
        Swap(pStartYpos, pEndYpos)
    EndIf

    wXpos = pEndXpos - pStartXpos
    wYpos = Abs(pEndYpos - pStartYpos)

    wErr = wXpos / 2
    If pStartYpos < pEndYpos Then
        bYstep = 1
    Else
        bYstep = -1
    EndIf

    Repeat
        If tSteep = 1 Then
            GLCD_Plot(pStartYpos, pStartXpos, pColour)
        Else
            GLCD_Plot(pStartXpos, pStartYpos, pColour)
        EndIf
        wErr = wErr - wYpos
        If wErr < 0 Then
            pStartYpos = pStartYpos + bYstep
            wErr = wErr + wXpos
        EndIf
        Inc pStartXpos
    Until pStartXpos > pEndXpos
EndProc

'-------------------------------------------------------------------------------------------------
' Draw or Erase a horizontal line from pStartXpos, pStartYpos to pEndXpos, pEndYpos
' Input     : pStartXpos = Starting X position of the Line
'           : pStartYpos = Starting Y position of the Line
'           : pEndXpos = Ending X position of the Line
' Output    : None
' Notes     : None
'
Proc GLCD_HLine(pStartXpos As Word, pStartYpos As Word, pEndXpos As Word, pColour As GLCD_tColour)
    pEndXpos = pEndXpos + pStartXpos

    If pEndXpos.SWord < 0 Then pEndXpos = 0
    If pEndXpos > GLCD_Xmax Then pEndXpos = GLCD_Xmax
    Repeat
        GLCD_Plot(pStartXpos, pStartYpos, pColour)
        Inc pStartXpos
    Until pStartXpos >= pEndXpos
EndProc

'----------------------------------------------------------------------------------------------
' Set or Clear a pixel on the graphic LCD
' Input     : pXpos holds the X position of the pixel
'           : pYpos holds the Y position of the pixel
'           : pColour holds the colour of the pixel (1 is set, 0 is clear)
' Output    : None
' Notes     : Uses the compilers built in Plot/UnPlot commands because the LCD is the KS0108 type
'
Proc GLCD_Plot(pXpos As GLCD_wXpos, pYpos As GLCD_wYpos, pColour As GLCD_tColour)
Global Dim GLCD_wXpos As Word Shared
Global Dim GLCD_wYpos As Word Shared
    If pColour = 0 Then
        UnPlot pYpos, pXpos
    Else
        Plot pYpos, pXpos
    EndIf
EndProc

'----------------------------------------------------------------------------------------------
' Initialise the ADC for 8-bit operation
' Input     : None
' Output    : None
' Notes     : Setups are for a PIC18F25K20 device
'
Proc ADC_Init8()
    ADCON1 = %00000000
    ADCON2 = %00000111                                  ' Left justification for an 8-bit ADC reading. ADC clock is Frc
    ADCON0 = %00000001                                  ' Enable the ADC
EndProc

'----------------------------------------------------------------------------------------------
' Read the ADC for an 8-bit result
' Input     : pChan holds the channel to read
' Output    : Returns the 8-bit ADC reading
' Notes     : Setups are for a PIC18F25K20 device
'
Proc ADC_Read8(pChan As Byte), Byte
    ADCON0bits_ADON = 1                                 ' Turn on the ADC module (just in case)
    pChan = pChan << 2                                  ' Move the channel value into its correct position for ADCON0
    ADCON0 = ADCON0 & %11000011                         ' Clear the channel bits of ADCON0 (bits 2 to 5)
    ADCON0 = ADCON0 | pChan                             ' Or in the channel value
    DelayUS 20                                          ' Delay to allow the ADC's capacitor to be maintained
    ADCON0bits_GO_DONE = 1                              ' Start the conversion
    Repeat: Until ADCON0bits_GO_DONE = 0                ' Wait for the conversion to finish
    Result = ADRESH                                     ' Return the ADC 8-bit result
EndProc

Below is a screenshot of the program running in the proteus simulator, and the source code and the proteus project are attached below as well.
Moving the variable resistor in the simulation will cause the ADC to change in value and this will be represented by the needle moving:

Needle_Screenshot.jpg


RGV250

Hi Sean,
That looks really impressive but the small display is a lot of money. There is a 7" one for 1/3 the price, hopefully the code will work and I can migrate it to Positron.

Bob

trastikata

Quote from: SeanG_65 on Aug 19, 2024, 12:58 AMHow about THIS little gem?

https://www.youtube.com/watch?v=ugS3rKBZVVc

The hardest part is designing and drawing the files for the display, it requires artistic skills and some experience with artistic software.   


top204

It looks like that display is a form of very small "Nextion", where it has on-board storage and a microcontroller and a serial interface. That is why it is a rather silly high price for such a small item.

It could be done with a standard microcontroller operating at a higher speed, with a flash chip (or a micro SD card), and a collection of images in modified .BMP format, or better still, not using a FAT system and the images just loaded into the flash memory's pages, for a lot of extra speed because the FAT mechanisms are actually rather slow and cumbersome.

When I was first experimenting with colour graphic LCDs in around 2012, I wrote a program that read BMP files from the SD card in sequence using a fast FAT16 read library I created, and displayed each image on the LCD, and it made a crude movie player. It worked well, because I had the ILI9320 display in parallel mode. I still remember what AVI file I extracted from as well. It was a 1940s Donald Duck cartoon. I did not get around to adding the audio, and was distracted with work, so I never finished it. The SD card had hundreds of small BMP files on it, each with a sequential file name, so a simple count within the program would read the appropriate file for display.

The many BMP files were sequentially extracted from an AVI file and were each formatted in RGB565 mode and with a smaller X and Y resolution, and the header data removed because the format was known, by a program I wrote in Delphi, so the LCD interface program did not have to waste time with the conversion for every RGB pixel, or fit a full 320x240 display.

So the same could be done for a meter, with a set of modified BMP files with a smaller RGB format on flash, and an image read and displayed for a particular meter value.

Now to me, that is creative programming! Not just using something that does everything for you in the first place, and if required in a few years has been discontinued. :-)

A better method is something that trastikata did with his TFT library, where the meter background is an image, and a method similar to my first post on here is used, to calculate and move the meter's needle using trigonometry. Now that is creative and knowledgeable programming! :-)

SeanG_65

WAY too many clever people on here ;-)

I like logging in just to see some of the hugely innovative ways that people solve problems. I find a great many "Now THAT IS smart" moments on here.