After creating the EM34 valve emulator using a CG9A01 circular TFT, it was obvious what would be a good project for it, and that is an Analogue Meter, because the round display of the CG9A01 would mimic the older analogue meters very nicely.
Below is an image of the CG9A01 circular TFT I used for the meter emulator. It uses an SPI interface, that can operate extremely fast, and is very simple to control.
In some ways, the analogue meter emulator's code is similar, in principle, to the EM34 valve emulater, in that it requires a line mechanism to draw a line of a certain length, using an angle as its parameter, but not as many lines as the EM34 emulator, so the code listing is a bit less complex. The Positron8 compiler code listing can be viewed below:
'
' /\\\\\\\\\
' /\\\///////\\\
' \/\\\ \/\\\ /\\\ /\\\
' \/\\\\\\\\\\\/ /\\\\\ /\\\\\\\\\\ /\\\\\\\\ /\\\\\\\\\\\ /\\\\\\\\\\\ /\\\\\\\\\
' \/\\\//////\\\ /\\\///\\\ \/\\\////// /\\\/////\\\ \////\\\//// \////\\\//// \////////\\\
' \/\\\ \//\\\ /\\\ \//\\\ \/\\\\\\\\\\ /\\\\\\\\\\\ \/\\\ \/\\\ /\\\\\\\\\\
' \/\\\ \//\\\ \//\\\ /\\\ \////////\\\ \//\\/////// \/\\\ /\\ \/\\\ /\\ /\\\/////\\\
' \/\\\ \//\\\ \///\\\\\/ /\\\\\\\\\\ \//\\\\\\\\\\ \//\\\\\ \//\\\\\ \//\\\\\\\\/\\
' \/// \/// \///// \////////// \////////// \///// \///// \////////\//
' Let's find out together what makes a PIC Tick!
'
' An analogue meter movement emulator, using a circular CG9A01 Graphic LCD on a PIC18FxxK40 device.
' A 10-bit ADC input is used to move the meter's needle on the LCD.
'
' Written by Les Johnson for the Positron8 BASIC compiler.
' https://sites.google.com/view/rosetta-tech/home
'
Device = 18F26K40 ' Tell the compiler what device to compile for
Declare Xtal = 64 ' Tell the compiler what frequency the device is operating at (in MHz)
Declare Float_Display_Type = Fast ' Tell the compiler to use the faster Floating Point to ASCII library sub
Declare Auto_Heap_Strings = On ' Make Strings Heap types, so they get placed after standard variables
Declare Auto_Variable_Bank_Cross = On ' Make sure all multi-byte variables remain within the same RAM bank
'
' Setup USART1 for debugging
'
Declare Hserial1_Baud = 9600
Declare HRSOut1_Pin = PORTC.6
'
' Define the pins used for the CG9A01 LCD display
'
$define CG9A01_CLK_Pin PORTC.3 ' Connects to the CG9A01 CLK pin (named SCL on some displays)
$define CG9A01_CLK_PPS_Pin Pin_C3 ' Define the CLK Pin to use for PPS
$define CG9A01_DTA_Pin PORTC.5 ' Connects to the CG9A01 DTA pin (named SDI on some displays)
$define CG9A01_DTA_PPS_Pin Pin_C5 ' Define the DTA Pin to use for PPS
$define CG9A01_CS_Pin PORTC.2 ' Connects to the CG9A01 CS pin
$define CG9A01_DC_Pin PORTC.1 ' Connects to the CG9A01 DC pin
$define CG9A01_RST_Pin PORTC.0 ' Connects to the CG9A01 Reset pin
$define CG9A01_BLK_Pin PORTB.0 ' Connects to the CG9A01 BLK (Backlight) pin
'
' Setup the ADC input pin and SFR
'
$define ADC_Pin PORTA.0 ' The pin to use for the ADC input
$define ADC_SFR ANSELA.0 ' The ADC analogue SFR and bit, for the Port and Pin used
$define ADC_Pin_Analogue() ADC_SFR = 1 ' A meta-macro to set the ADC channel pin as analogue
$define ADC_Pin_Digital() ADC_SFR = 0 ' A meta-macro to set the ADC channel pin as digital
$define GLCD_Backlight_On() PinHigh CG9A01_BLK_Pin ' A meta-macro to illuminate the LCD's backlight
$define GLCD_Backlight_Off() PinLow CG9A01_BLK_Pin ' A meta-macro to extinguish the LCD's backlight
Include "Tahoma_12.inc" ' Load a font table into the program
Include "TahomaBold_14.inc" ' Load a font table into the program
Include "CG9A01.inc" ' Load the CG9A01 LCD library into the program
Include "HSPI_K40.inc" ' Load the SPI peripheral library into the program
'
' Convert an RGB888 constant value into an RGB565 constant value
'
$define RGB565(r,g,b) $eval ((((r) >> 3) & 0x1F) << 11) | ((((g) >> 2) & 0x3F) << 5) | (((b) >> 3) & 0x1F)
$define clBackGnd RGB565(200, 190, 190) ' The meter's background colour
'
' Create global constants here
'
$define cCENTER_X $eval (cCG9A_Width / 2) ' The center X position of display
$define cCENTER_Y $eval (cCG9A_Height / 2) ' The center Y position of the display
$define cPI 3.1415926 ' The value for PI
$define cPI_Div180 0.0174532 ' Used to convert degrees to radians (PI / 180)
$define cNeedle_MinPos 230 ' The maximum position of the meter's dial
$define cNeedle_MaxPos 270 ' The maximum position of the meter's dial
$define cNeedlePos 220 ' The default position of the needle
$define cNeedleRadius 120 ' The radius of the needle
$define cNeedleSpeed 10 ' The speed that the needle returns to the left
$define cPivotX cCENTER_X ' The needle's pivot X position
$define cPivotY $eval (cCENTER_Y + 50) ' The needle's pivot Y position
$define cPivotRadius 8 ' The needle's pivot radius
'
' Create global variables here
'
Dim wADC_Value As SWord ' Holds the ADC reading
'--------------------------------------------------------------------------------------------
' The main program starts here
' Move the meter based upon the reading of the 10-bit ADC
'
Main:
Setup() ' Setup the program and any peripherals
Do ' Create a loop
wADC_Value = ADC_Read10(0) ' Take a reading from the ADC
Move_Meter(wADC_Value) ' Move the meter's needle based upon it
DelayMS 100 ' Wait before moving the needle again
Loop ' Do it forever
'--------------------------------------------------------------------------------------------
' Move the meter's needle
' Input : pValue holds the scaled 10-bit ADC reading (0 to 1023)
' Output : None
' Notes : If the needle does not need to move, it is not re-drawn
'
Proc Move_Meter(pValue As SWord)
Static Dim wNeedleValue As Word = cNeedle_MinPos ' Start with a known needle angle
Static Dim wPrevValue As Word = 0 ' Holds the previous needle's value
Dim wAngle As Word ' Holds the needle's angle based upon the scaling of pValue
wAngle = Scale16S(pValue, 0, 1023, cNeedle_MinPos, cNeedle_MaxPos) ' Scale the 10-bit analogue input to the needle position
'
' If the required value is greater than current needle position, move quickly
'
If wAngle > wNeedleValue Then
wNeedleValue = wAngle
ElseIf wAngle < wNeedleValue Then ' If the needle value is lower, move more slowly to simulate damping
wNeedleValue = wNeedleValue - cNeedleSpeed ' Decrease the value gradually
If wNeedleValue < wAngle Then ' \
wNeedleValue = wAngle ' | Make sure the needle does not overshoot
EndIf ' /
EndIf
If wNeedleValue <> wPrevValue Then ' Has the value changed for the needle's movement?
Draw_Needle(wNeedleValue) ' Yes. So update the needle position
GLCD_FillCircle(cPivotX, cPivotY, cPivotRadius, clBlack) ' Re-Draw the needle's pivot
EndIf
wPrevValue = wNeedleValue ' Keep a copy of the needle's value
EndProc
'--------------------------------------------------------------------------------------------
' Draw the moving needle, without the tip arrow
' Input : pValue holds the position the needle will move to
' Output : None
' Notes : Draws a double thickness line for the needle to make it more visible
'
Proc Draw_NeedleOnly(pValue As Word)
Symbol cCENTER_Y_50 = (cCENTER_Y + 50)
Global Dim fNeedlePos As Float Access Shared ' Holds the needle's position
Global Dim fCosValue As Float Access Shared
Global Dim fSinValue As Float Access Shared
Static Dim wNdl1_PrevX As Word = cCENTER_X
Static Dim wNdl1_PrevY As Word = cCENTER_Y_50
Dim wNdl1X As Word
Dim wNdl1Y As Word
fNeedlePos = (pValue * cPI_Div180) * 1.8 ' \ Convert degrees to radians
fNeedlePos = fNeedlePos - cPI ' /
fCosValue = Cos(fNeedlePos) ' \
fSinValue = Sin(fNeedlePos) ' |
wNdl1X = cPivotX + (cNeedleRadius * fCosValue) ' | Calculate the position of the needle's tip
wNdl1Y = cPivotY + (cNeedleRadius * fSinValue) ' /
GLCD_Line(cPivotX, cPivotY, wNdl1_PrevX, wNdl1_PrevY, clBackGnd) ' \ Erase the old needle
GLCD_Line(cPivotX, cPivotY, wNdl1_PrevX + 1, wNdl1_PrevY, clBackGnd) ' /
'
' Place texts on the meter's dial
'
GLCD_PenColour(clBlack) ' Choose the texts ink colour
GLCD_SetFont(TahomaBold_14) ' Choose the font to use for the "Meter" text
GLCD_PrintAtCentre(cPivotY - 40, "Meter") ' Draw text above the pivot
GLCD_SetFont(Tahoma12) ' Choose the font to use for the texts
Print At 72, 25, "Min", ' \ Draw the "Min" and "Max" texts on the dial
At 72, 182, "Max" ' /
GLCD_Line(cPivotX, cPivotY, wNdl1X, wNdl1Y, clBlack) ' \ Draw the needle
GLCD_Line(cPivotX, cPivotY, wNdl1X + 1, wNdl1Y, clBlack) ' /
wNdl1_PrevX = wNdl1X ' \
wNdl1_PrevY = wNdl1Y ' / Store the X and Y positions of the needle
EndProc
'--------------------------------------------------------------------------------------------
' Draw the moving needle, with its tip arrow
' Input : pValue holds the position the needle will move to
' Output : None
' Notes : None
'
Proc Draw_Needle(pValue As Word)
Symbol cCENTER_Y_50 = (cCENTER_Y + 50)
Symbol cCENTER_Y_30 = (cCENTER_Y + 30)
Symbol cRadius_Min_15 = (cNeedleRadius - 15)
Symbol cRadius_Min_90 = (cNeedleRadius - 90)
Global Dim fNeedlePos As Float Access Shared ' Holds the needle's position
Global Dim fCosValue As Float Access Shared
Global Dim fSinValue As Float Access Shared
Static Dim wNdl1_PrevX As Word = cCENTER_X
Static Dim wNdl1_PrevY As Word = cCENTER_Y_50
Static Dim wNdl_PrevLTipX As Word = cCENTER_X
Static Dim wNdl_PrevLTipY As Word = cCENTER_Y_50
Static Dim wNdl_PrevRTipX As Word = cCENTER_X
Static Dim wNdl_PrevRTipY As Word = cCENTER_Y_50
Dim wNdl1X As Word
Dim wNdl1Y As Word
Dim wNdl_LTipX As Word
Dim wNdl_LTipY As Word
Dim wNdl_RTipX As Word
Dim wNdl_RTipY As Word
fNeedlePos = (pValue * cPI_Div180) * 1.8 ' \ Convert degrees to radians
fNeedlePos = fNeedlePos - cPI ' /
fCosValue = Cos(fNeedlePos) ' \
fSinValue = Sin(fNeedlePos) ' |
wNdl1X = cPivotX + (cNeedleRadius * fCosValue) ' | Calculate the Needle position
wNdl1Y = cPivotY + (cNeedleRadius * fSinValue) ' /
fCosValue = Cos(fNeedlePos - 0.05) ' \
fSinValue = Sin(fNeedlePos - 0.05) ' |
wNdl_LTipX = cPivotX + (cRadius_Min_15 * fCosValue) ' | Calculate the Needle tip triangle left
wNdl_LTipY = cPivotY + (cRadius_Min_15 * fSinValue) ' /
fCosValue = Cos(fNeedlePos + 0.05) ' \
fSinValue = Sin(fNeedlePos + 0.05) ' |
wNdl_RTipX = cPivotX + (cRadius_Min_15 * fCosValue) ' | Calculate the Needle tip triangle right
wNdl_RTipY = cPivotY + (cRadius_Min_15 * fSinValue) ' /
GLCD_Line(cPivotX, cPivotY, wNdl1_PrevX, wNdl1_PrevY, clBackGnd) ' Erase the old needle
'
' Erase the tip triangle
'
GLCD_FillTriangle(wNdl1_PrevX, wNdl1_PrevY, wNdl_PrevLTipX, wNdl_PrevLTipY, wNdl_PrevRTipX, wNdl_PrevRTipY, clBackGnd)
'
' Place texts on the meter's dial
'
GLCD_PenColour(clBlack) ' Choose the texts ink colour
GLCD_SetFont(TahomaBold_14) ' Choose the font to use for the "Meter" text
GLCD_PrintAtCentre(cPivotY - 40, "Meter") ' Draw text above the pivot
GLCD_SetFont(Tahoma12) ' Choose the font to use for the texts
Print At 72, 25, "Min", ' \ Draw the "Min" and "Max" texts on the dial
At 72, 182, "Max" ' /
GLCD_Line(cPivotX, cPivotY, wNdl1X, wNdl1Y, clBlack) ' Draw the needle
GLCD_FillTriangle(wNdl1X, wNdl1Y, wNdl_LTipX, wNdl_LTipY, wNdl_RTipX, wNdl_RTipY, clBlack)' Draw the tip triangle
wNdl1_PrevX = wNdl1X ' \
wNdl1_PrevY = wNdl1Y ' |
wNdl_PrevLTipX = wNdl_LTipX ' | Store the previous needle position
wNdl_PrevLTipY = wNdl_LTipY ' |
wNdl_PrevRTipX = wNdl_RTipX ' |
wNdl_PrevRTipY = wNdl_RTipY ' /
EndProc
'--------------------------------------------------------------------------------------------
' Draw the meter's dial
' Input : None
' Output : None
' Notes : None
'
Proc Draw_Dial()
Symbol cBlipPos = (cNeedleRadius + 15)
Global Dim fNeedlePos As Float Access Shared ' Holds the needle's position
Global Dim fCosValue As Float Access Shared
Global Dim fSinValue As Float Access Shared
Dim wArc_X As Word
Dim wArc_Y As Word
Dim bRadian As Byte
GLCD_Cls(clBackGnd) ' \
GLCD_Circle(cCENTER_X, cCENTER_Y, 118, clGray2) ' |
GLCD_Circle(cCENTER_X, cCENTER_Y, 117, clBlack) ' | Create the dial's edges
GLCD_Circle(cCENTER_X, cCENTER_Y, 116, clBlack) ' |
GLCD_Circle(cCENTER_X, cCENTER_Y, 115, clGray2) ' /
For bRadian = 30 To 44 Step 5 ' \
fNeedlePos = (bRadian * cPI_Div180) * 1.8 ' |
fNeedlePos = fNeedlePos - cPI ' |
fCosValue = Cos(fNeedlePos) ' | Calculate the positions for the dial's beginning green blips
fSinValue = Sin(fNeedlePos) ' |
wArc_X = (cPivotX + (cBlipPos * fCosValue)) ' |
wArc_Y = (cPivotY + (cBlipPos * fSinValue)) ' /
GLCD_FillCircle(wArc_X, wArc_Y, 4, clGreen) ' Draw a green blip of the dial
Next
For bRadian = 45 To 59 Step 5 ' \
fNeedlePos = (bRadian * cPI_Div180) * 1.8 ' |
fNeedlePos = fNeedlePos - cPI ' |
fCosValue = Cos(fNeedlePos) ' | Calculate the positions for the dial's beginning black blips
fSinValue = Sin(fNeedlePos) ' |
wArc_X = (cPivotX + (cBlipPos * fCosValue)) ' |
wArc_Y = (cPivotY + (cBlipPos * fSinValue)) ' /
GLCD_FillCircle(wArc_X, wArc_Y, 4, clBlack) ' Draw a black blip of the dial
Next
For bRadian = 60 To 74 Step 5 ' \
fNeedlePos = (bRadian * cPI_Div180) * 1.8 ' |
fNeedlePos = fNeedlePos - cPI ' |
fCosValue = Cos(fNeedlePos) ' | Calculate the positions for the dial's ending red blips
fSinValue = Sin(fNeedlePos) ' |
wArc_X = (cPivotX + (cBlipPos * fCosValue)) ' |
wArc_Y = (cPivotY + (cBlipPos * fSinValue)) ' /
GLCD_FillCircle(wArc_X, wArc_Y, 4, clBrightRed) ' Draw a red blip of the dial
Next
GLCD_SetFont(Tahoma12) ' Choose the font to use for the texts
Print At 72, 25, "Min",
At 72, 182, "Max"
EndProc
'-------------------------------------------------------------------------------------------------------------
' Scale one 16-bit signed integer value range to another 16-bit signed integer value range
' Input : pValueIn holds the value to scale
' : pInMin is the lower bound of the value's input range
' : pInMax is the upper bound of the value's input range
' : pOutMin is the lower bound of the value's target range
' : pOutMax is the upper bound of the value's target range
' Output : Returns the scaled result
' Notes : None
'
Proc Scale16S(pValueIn As SDword, pInMin As SWord, pInMax As SWord, pOutMin As SWord, pOutMax As SWord), SWord
Result = (((pValueIn - pInMin) * (pOutMax - pOutMin)) / (pInMax - pInMin)) + pOutMin
EndProc
'--------------------------------------------------------------------------------------------
' Setup the program and any peripherals
' Input : None
' Output : None
' Notes : None
'
Proc Setup()
'
' Setup the SPI1 peripheral (if used)
'
$ifndef _cSOFTWARE_SPI_ ' Is the CG9A01 library using bit-bashed SPI routines?
PPS_Unlock() ' No. So Unlock the PPS for the SPI peripheral
Set_PPSForSCK1(CG9A01_CLK_PPS_Pin) ' Setup the SPI1 peripheral's PPS, CLK for the LCD's SCK pin
Set_PPSForSDO1(CG9A01_DTA_PPS_Pin) ' Setup the SPI1 peripheral's PPS, SDO pin for the LCD's DTA pin
Set_PPSForSDI1(Pin_C4) ' Setup the SPI1 peripheral's PPS, SDI pin
HSPI1_Init_Mode0_MaxMHz() ' Setup the SPI1 peripheral to operate in Mode 0 at its fastest speed
$endif
GLCD_Init() ' Initialise the CG9A01 LCD
'
' Extinguish the LCD's backlight. So that it does not show the initial dial being drawn
'
GLCD_Backlight_Off()
ADC_Setup() ' Initialise the ADC
GLCD_Rotate(2) ' Rotate the display
GLCD_Cls(clBackGnd) ' Clear the LCD with the background colour
GLCD_SetFont(TahomaBold_14) ' Choose the default font to use for the texts
GLCD_PaperColour(clBackGnd) ' Choose the text's background colour
GLCD_PenColour(clBlack) ' Choose the text's ink colour
Draw_Dial() ' Draw the meter's dial
Draw_Needle(cNeedlePos) ' Draw the meter's needle
GLCD_FillCircle(cPivotX, cPivotY, cPivotRadius, clBlack) ' Draw the needle's pivot
GLCD_Backlight_On() ' Illuminate the LCD's backlight
EndProc
'------------------------------------------------------------------------------------------------------------
' Setup the ADC peripheral on a PIC18FxxK40 device
' Input : None
' Output : None
' Notes : Set for 10-bit operation
'
Proc ADC_Setup()
ADLTHL = %00000000
ADLTHH = %00000000
ADUTHL = %00000000
ADUTHH = %00000000
ADSTPTL = %00000000
ADSTPTH = %00000000
ADRPT = %00000000
ADPCH = %00000000
ADCAP = %00000000
ADCON0 = %10000100 ' Right justify for 10-bit operation. ADCS is ADCLK
ADCON1 = %00000000
ADCON2 = %00000000 ' Basic_mode
ADCON3 = %00000000
ADSTAT = %00000000
ADREF = %00000000 ' -Vref is VSS. +Vref is VDD
ADACT = %00000000
ADCLK = %00001111 ' fOSC/32: (FOSC / (2 * (n + 1)))
ADACQ = %00000000
EndProc
'------------------------------------------------------------------------------------------------------------
' Read the ADC on a PIC18F26K40 device
' Input : pChan holds the ADC channel to read
' Output : Returns the 10-bit ADC value
' Notes : Requires the ADFM bit set for right justified 10-bit operation
'
Proc ADC_Read10(pChan As WREG), Word
ADPCH = pChan ' Load the channel into the relevant SFR
ADCON0bits_ADON = 1 ' Enable the ADC
DelayUS 100 ' A delay before sampling
ADCON0bits_GO_DONE = 1 ' \
Repeat : Until ADCON0bits_GO_DONE = 0 ' / Start a sample and wait for it to finish
Result.Byte1 = ADRESH ' \
Result.Byte0 = ADRESL ' / Return the sample value
EndProc
'------------------------------------------------------------------------------------------------------------
' Setup the config fuses to use the internal oscillator at 64MHz on PIC18FxxK40 devices
' With pins RA6 and RA7 as general purpose I/O
'
Config_Start
RSTOSC = HFINTOSC_64MHZ ' HFINTOSC with HFFRQ = 64MHz and CDIV = 1:1
FEXTOSC = Off ' External Oscillator not enabled
WDTE = Off ' Watchdog Timer disabled
CLKOUTEN = Off ' CLKOUT function is disabled
CSWEN = On ' Writing to NOSC and NDIV is allowed
FCMEN = Off ' Fail-Safe Clock Monitor disabled
MCLRE = EXTMCLR ' MCLR pin is MCLR
PWRTE = On ' Power up timer enabled
LPBOREN = Off ' ULPBOR disabled
BOREN = Off ' Brown-out disabled
BORV = VBOR_245 ' Brown-out Reset Voltage (VBOR) set to 2.45V
ZCD = Off ' ZCD disabled
PPS1WAY = Off ' PPSLOCK bit can be set and cleared repeatedly
STVREN = Off ' Stack full/underflow will not cause reset
Debug = Off ' Background debugger disabled
XINST = Off ' Extended instruction set disabled
SCANE = Off ' Scanner module is Not available for use. SCANMD bit is ignored
LVP = Off ' Low Voltage programming disabled
WDTCPS = WDTCPS_2 ' Watchdog Divider Ratio 1:128 (4 milliseconds)
WDTCWS = WDTCWS_7 ' Watchdog Window always open (100%)
WDTCCS = LFINTOSC ' WDT reference clock is the 31.2kHz
WRT0 = Off ' Block 0 (000800-001FFF) not write-protected
WRT1 = Off ' Block 1 (002000-003FFF) not write-protected
WRTC = Off ' Configuration registers (300000-30000B) not write-protected
WRTB = Off ' Boot Block (000000-0007FF) not write-protected
WRTD = Off ' Data EEPROM not write-protected
Cp = Off ' User NVM code protection disabled
CPD = Off ' Data NVM code protection disabled
EBTR0 = Off ' Block 0 not protected from table reads executed in other blocks
EBTR1 = Off ' Block 1 not protected from table reads executed in other blocks
EBTRB = Off ' Boot Block not protected from table reads in other blocks
Config_End
A circuit diagram for the connection of the CG9A01 TFT display to a PIC18F26K40 device is shown below. The Positron8 code sets the PIC18F26K40 microcontroller to use its internal oscillator, running at 64 MHz, so no crystal or resonator is required. The ADC input (AN0) can be supplied with a voltage from 0 to 3.3 volts, and this will be displayed as the meter's needle moving on the CG9A01 TFT display.
The code also has a mechanism to imitate the lag on an analogue meter's needle when voltage is removed, in that it moves to its resting place more slowly:
A photo of the Analogue Meter running on a breadboard shield, connected to an EasyDriver Amicus8 board with a PIC18F26K40 placed in its socket, is shown below, and the source code for the Analogue Meter emulator written in Positron8 can be downloaded from here: Analogue_Meter_with_CG9A01.zip. The zip file also contains the CG9A01 TFT library source code, and library source code to operate the on-board SPI peripheral for many of the newer 18FxxKxx devices.