News:

PROTON pic BASIC Compilers for PIC, PIC24, dsPIC33

Main Menu

Positron8 - Graphic LCD 128x64 Analogue Meter Simulation

Started by top204, Apr 21, 2024, 03:18 PM

Previous topic - Next topic

top204

Many years ago, when colour graphic LCDs were not readily available, around approx 2009, I created a program that simulated an analogue meter on the graphic LCDs that were readily available. i.e. A Samsung monochrome KS0108 128x64 type.

For some reason I never released the code, and now there is talk on the forum about converting code for an analogue meter simulation on a colour graphic LCD and it jogged my memory. So I, finally, got to release the source for it and adjusted it to use procedures, just in case it is useful to some users.

The code does not use complex trigonometry or floating point and is how code should be written for a small microcontroller, and not just copied and pasted from a C++ program that was originally written for a more powerful microprocessor machine. i.e. A Personal Computer. This makes it smaller and faster to operate. I'm sure the principle behind the code could be converted and enhanced to colour graphic LCDs using some of the excellent graphic LCD libraries that have been created by users on the forum, because it uses simple operations on the display. It even uses one of the early graphic LCD libraries created by TimB for multi-sized fonts, so that shows the code's age. i.e. PPrint. That takes use back a while doesn't it Tim?

A demo code listing is shown below that simulates an analogue meter movement on a Samsung KS0108 graphic LCD, and the meter's needle moves in relation to the reading from the ADC:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Demonstrate an analogue meter movement simulation on a Samsung KS0108 128x64 graphic LCD.
'
' Written by Les Johnson for the Positron8 BASIC compiler.
'
    Device = 18F25K20                                   ' Tell the compiler what device to compile for
    Declare Xtal = 16                                   ' Tell the compiler what frequency the device is 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

$define Meter_cTextPosX 52                              ' The X position (in pixels) of the meter's face text
$define Meter_cTextPosY 32                              ' The Y position (in pixels) of the meter's face text
$define Meter_cText "Meter"                             ' The text to place within the Meter's face

    Include "GLCD_AnalogueMeter.inc"                    ' Load the analogue meter code into the program
'
' Create gobal variables for the demo here
'
    Dim bMeterValue As Byte                             ' Holds the ADC value to transfer to the meter

'----------------------------------------------------------------------------------------------
' Main demo program starts here
' Read the ADC and show its value on the analogue meter image
'
Main:
    Setup()                                             ' Setup the program and any peripherals
    Do                                                  ' Create a loop
        bMeterValue = ADC_Read8(1)                      ' Read the ADC
        Meter_Move(bMeterValue)                         ' Move the meter appropriately
        Print At 56, 0, "ADC Value = ", Dec bMeterValue, "      " ' Display the ADC value on the bottom of the LCD
        DelayMS 100                                     ' A small delay
    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
    wANSEL = 2                                          ' Make pin AN1 an analogue input
    ADC_Init8()                                         ' Initialise the ADC for 8-bit operation
    Meter_Draw()                                        ' Draw the meter on the LCD
    Print Font Small_7                                  ' Choose the meter font. Always choose a font before using the MoveMeter procedure
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

'----------------------------------------------------------------------------------------------
' Load the demo's font into the program
'
    Include "Small_Font_7.inc"

The analogue meter's source code is listed below, and is what is contained in the attached file: "GLCD_AnalogueMeter.inc":

$ifndef _GLCDMETER_
$define _GLCDMETER_
'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Implement an analogue meter movement simulation on a Samsung KS0108 128x64 graphic LCD
' Uses proportional user defined fonts with Tim Box's Pprint routines
'
' Written by Les Johnson for the Positron8 BASIC compiler.
'
    Declare GLCD_External_Print = Pprint                                ' Point to Tim's Pprint routine for fonts

$ifndef Meter_cTextPosX                                                  ' Has the Meter_cTextPosX $define been used in the main program?
    $define Meter_cTextPosX 50                                           ' No. So set the default X position (in pixels) of the meter's face text
$endif
$ifndef Meter_cTextPosY                                                  ' Has the Meter_cTextPosY $define been used in the main program?
    $define Meter_cTextPosY 32                                           ' No. So set the he default Y position (in pixels) of the meter's face text
$endif
$ifndef Meter_cText                                                      ' Has the Meter_cText $define been used in the main program?
    $define Meter_cText "METER"                                          ' No. So set the he default text to place within the Meter's face
$endif
'
' Create global constants and variable here
'
    Symbol Meter_cDialOffset = 6                                        ' Alter this value to move the meter dial up or down
    Symbol Meter_cDialHeight = 4
    Symbol Meter_cTickOffset = Meter_cDialHeight - 1

'----------------------------------------------------------------------
    GoTo _Meter_Main_                                                   ' Jump over the font data
'----------------------------------------------------------------------
' Load the proportional font into the program
' This is used for the meter dial numbers

    Include "SmallFont.inc"

'----------------------------------------------------------------------------------------------
' Draw the analogue meter
' Input     : None
' Output    : None
' Notes     : None
'
Proc Meter_Draw()
Global Dim Meter_bNeedleLength[127] As Byte Heap Shared                ' Holds the values of the needle's length
    Dim bTickIndex   As Byte
    Dim bTickPos     As Byte
    Dim bDialNumbers As Byte
    Symbol cDialTop = 61

    Meter_DialDraw(cDialTop - Meter_cDialOffset)                        ' Draw the top dial
'
' Draw the ticks on the dial and the dial values
'
    Print Font Small5                                                   ' Choose a font
    bDialNumbers = 1
    For bTickIndex = 0 To 127 Step 16                                   ' Alter the step value for the amount of ticks
        bTickPos = Meter_bNeedleLength[bTickIndex]
        If bTickIndex > 0 Then
            If bTickIndex < 63 Then
                Line 1, bTickIndex, (bTickPos + 1), (bTickIndex + Meter_cTickOffset), (bTickPos + Meter_cDialHeight)
                Print At (bTickPos - 6), (bTickIndex - 2), Dec bDialNumbers

            ElseIf bTickIndex > 64 Then
                Line 1, bTickIndex, (bTickPos + 1), (bTickIndex - Meter_cTickOffset), (bTickPos + Meter_cDialHeight)
                Print At (bTickPos - 6), (bTickIndex + 2), Dec bDialNumbers

            Else
                Line 1, bTickIndex, (bTickPos + 1), bTickIndex, (bTickPos + Meter_cDialHeight)
                Print At (bTickPos - 6), bTickIndex, Dec bDialNumbers
            EndIf
            Inc bDialNumbers
        EndIf
    Next
    Meter_DialDraw(cDialTop - Meter_cDialHeight - Meter_cDialOffset)    ' Draw the lower dial line
    Circle 1, 63, 46 + Meter_cDialOffset, 2                             ' Draw the needle pivot
'
' Draw a box around the meter
'
    Line 1, 61, (46 + Meter_cDialOffset), 0, (46 + Meter_cDialOffset)
    LineTo 1, 0, 0
    LineTo 1, 127, 0
    LineTo 1, 127, (46 + Meter_cDialOffset)
    LineTo 1, 65, (46 + Meter_cDialOffset)
EndProc

'----------------------------------------------------------------------------------------------
' Draw the dial of the meter
' Input     : pRadius holds the radius of the dial
' Output    : None
' Notes     : None
'
Proc Meter_DialDraw(pRadius As Byte)
Global Dim Meter_bNeedleLength[127] As Byte Heap Shared                ' Holds the values of the needle's length
    Dim bDialX      As Byte Access
    Dim bDialY      As Byte Access
    Dim wDialRad    As Word
    Dim bSideToSide As Byte Access
    Dim bDownUp     As Byte Access

    Symbol cDialX_Limit = 64
    Symbol cDialY_Limit = 127

    bDialX = 0
    bDialY = pRadius + 64
    wDialRad = 3 - (bDialY * 2)
    While bDialX <= bDialY
        bDownUp = cDialY_Limit - bDialY
        '
        ' Calculate the right half of the dial curve
        '
        bSideToSide = cDialX_Limit + bDialX
        Plot bDownUp, bSideToSide
        Meter_bNeedleLength[bSideToSide] = bDownUp
        If bSideToSide >= 127 Then Break
        '
        ' Calculate the left half of the dial curve
        '
        bSideToSide = cDialX_Limit - bDialX
        Plot bDownUp, bSideToSide
        Meter_bNeedleLength[bSideToSide] = bDownUp
        If bSideToSide = 0 Then Break

        If wDialRad.15 = 1 Then
            wDialRad = wDialRad + 6
            wDialRad = wDialRad + (bDialX * 4)
        Else
            wDialRad = wDialRad + 10
            wDialRad = wDialRad + ((bDialX - bDialY) * 4)
            Dec bDialY
        EndIf
        Inc bDialX
    Wend
    Meter_bNeedleLength[0] = bDownUp
EndProc

'----------------------------------------------------------------------------------------------
' Draw and move the meter's needle
' Input     : pValue holds the needle's movement (0 to 255)
' Output    : None
' Notes     : Adds a delay between movements for a more realism
'             The delay is caused by the Print command within the loop
'             If this is removed, add a few milliseconds delay
'
Proc Meter_Move(pValue As Byte)
Global Dim Meter_bNeedleLength[127] As Byte Heap Shared                     ' Holds the values of the needle's length
Static Dim bNeedlePos    As Byte = 0
Static Dim bPrevValue    As Byte = 0
Static Dim bPrevLineLen  As Byte = 0
Static Dim bPrevLineXpos As Byte = 0

    bNeedlePos = pValue / 2
    If bNeedlePos = 0 Then bNeedlePos = 1
    If bNeedlePos >= 127 Then bNeedlePos = 126
    If bNeedlePos = bPrevValue Then ExitProc                                ' Don't move the meter if it hasn't changed
    Repeat
        Print At Meter_cTextPosY, Meter_cTextPosX, Meter_cText              ' Draw the meter text
        Line 0, 62, (44 + Meter_cDialOffset), bPrevLineXpos, bPrevLineLen
        LineTo 0, 64, (44 + Meter_cDialOffset)                              ' Erase the previous line
        Line 1, 62, (44 + Meter_cDialOffset), bPrevValue, (Meter_bNeedleLength[bPrevValue] + 2)
        LineTo 1, 64, (44 + Meter_cDialOffset)                              ' Draw a new Line
        bPrevLineLen = Meter_bNeedleLength[bPrevValue] + 2                  ' Keep a record of the needle's length
        bPrevLineXpos = bPrevValue                                          ' Keep a record of the needle's X position
        If bNeedlePos >= bPrevValue Then                                    ' Is the needle pointer greater than it was?
            Inc bPrevValue                                                  ' Yes. So increment to catch up
        Else                                                                ' Otherwise...
            Dec bPrevValue                                                  ' Decrement to catch up
        EndIf
    Until bNeedlePos = bPrevValue                                           ' Until the needle is at the correct position
EndProc

'----------------------------------------------------------------------------------------------
_Meter_Main_:
$endif

Below is a screenshot from the proteus simulator, running the demo program, and the demo and meter source code is attached to this thread. If you run the program in the proteus simulator, please be aware that the KS0108 model in proteus is flawed, and has been for many years now, in that it will not operate fast enough when the microcontroller is also operating at higher speeds, and does not display correctly. To get the KS0108 model to operate correctly, the microcontroller has to be set to run at around 8MHz to 10MHz, and the KS0108 simulation model has to be set for 2MHz instead of the default 300KHz. This has been the case for many, many years now and I thought it would have been corrected by now, but it has not:

Screenshot_Analogue_Meter.jpg

SeanG_65

OOPS, forgot the proteus file Les   :o

I think I can use this as a starting point.

The reason I brought this up is that if one were desinging a piece of test equipment, it might be nice to switch between analogue and digital displays, and I prefer analogue to digital anyday.

I agree though, the C++ code in the other article is VERY bloated.

top204

I didn't add the proteus project because the KS0108 LCD model in it has problems with higher speed devices, and I did not want the compiler to get the blame for it if it was also in the zip file.

Adding colour or a higher resolution LCD to the meter code would be very straightforward because the curve Y position of the dial is held in the Needle Length array when it is created, so the needle knows how long it should be for a Line function, so it stops underneath the dial. It's as simple as that. :-) Even for a 256x192 colour LCD, the needle length array would require only 256 bytes of RAM.

It could even read a data table holding the image of a small dial in 8-bit RGB format, and the needle's length manually added to a flash memory needle length table for its X movement size releationship, for a photo realistic graphic LCD meter without any floating point or triginometry etc, making it still fast to operate.

Then use one of the ScaleX procedures to match the value to used as the input to the meter, to the value required for the X positioning of the needle to match the values on its dial. The ScaleX procedures are in the "Scale.inc" library.