'*****************************************************************************
'*  TM1622_SPI_Soft.bas
'*  TM1622 10-digit, 16-segment LCD library for PIC18 Oshonsoft BASIC v6.10
'*  By COS, dogflu66@yahoo.es
'*  With AI assistant: ChatGPT.
'*  v2026/07/17
'*  --------------------------------------------------------------------------
'*  Driver for 10-character alphanumeric LCD modules based on the TM1622
'*  (HT1622-compatible serial protocol), typically sold as DM8BA10 modules.
'*	Test: 1, 2, 4, 8, 16 y 64 Mhz
'*  IMPORTANT
'*  ---------
'*  This first version has not yet been tested on the physical module.
'*  TM1622 communication is implemented by software using CS, WR and DATA.
'*  It is not standard hardware SPI:
'*
'*    - Mode code, commands and addresses are sent MSB first.
'*    - Display data is stored in 4-bit addresses and sent LSB first.
'*    - Data is latched by the TM1622 on the rising edge of WR.
'*
'*  The ten digit-address constants are grouped in TM1622_GetDigitAddr().
'*  If the module uses a different glass-to-RAM connection, only that function
'*  and/or TM1622_GetEncode() should need modification.
'*
'* 	Because the TM1622 does not use a fully standard SPI interface, the MSSP module
'*	is not used.
'*	The Write header is 9 bits.
'* 	Commands are 12 bits.
'* 	Individual writes are 13 bits.
'* 	Data in each nibble is transmitted LSB-first.
'*
'*  Features
'*  --------
'*  - 10 alphanumeric digits, 16 segments per digit.
'*  - Local 10-word display buffer.
'*  - Character, string and raw 16-bit pattern output.
'*  - Decimal-point interpretation controlled by TM1622_DPMode(True/False).
'*  - Circular right-to-left scrolling.
'*  - Display ON/OFF and system oscillator control.
'*  - Complete RAM, digit-order and segment-identification tests.
'*  - Optional backlight output pin.
'*
'*  Main functions
'*  --------------
'*  TM1622_Init()
'*  TM1622_Clear_RAM()
'*  TM1622_Clear()
'*  TM1622_Update()
'*  TM1622_Display_On()
'*  TM1622_Display_Off()
'*
'*  TM1622_Cursor(pos)
'*  TM1622_CursorHome()
'*  TM1622_PutC(char)
'*  TM1622_Print(string)
'*	TM1622_PadLeft(char, ByRef string)
'*  TM1622_WriteDigit(pos, pattern)
'*  TM1622_SetSegment(pos, segment, state)
'*
'*  TM1622_DPMode(dp)
'*  TM1622_PutDP()
'*  TM1622_SetDP(pos, state)
'*  TM1622_ClearDP()
'*
'*  TM1622_ScrollReset()
'*  TM1622_ScrollLeft(string)
'*
'*  TM1622_Test_All()
'*
'*  Typical module connections
'*  --------------------------
'*    VDD   -> +5 V
'*    GND   -> GND
'*    CS    -> MCU digital output
'*    WR    -> MCU digital output
'*    DATA  -> MCU digital output
'*    LED+  -> +3.3/+5 V through the module/current-limiting resistor,
'*             or to the optional backlight control output.
'*
'*****************************************************************************

'-----------------------------------------------------------------------------
' User configuration
'-----------------------------------------------------------------------------
Symbol TM1622_CS = 	 	  LATC.0
Symbol TM1622_WR = 	 	  LATC.1
Symbol TM1622_DATA = 	  LATC.2
'Symbol TM1622_LED =  	  LATE.2

Symbol TM1622_CS_TRIS =   TRISC.0
Symbol TM1622_WR_TRIS =   TRISC.1
Symbol TM1622_DATA_TRIS = TRISC.2
'Symbol TM1622_LED_TRIS =  TRISE.2

Const TM1622_Digits = 	 10
Const TM1622_RAM_ADDRS = 64

' Software timing. Increase to 2 or 3 if required with a very fast MCU.
Const TM1622_Delay_us = 1

' Commands, without the initial 3-bit COMMAND mode code (100).
Const TM1622_CMD_SYS_DIS =  0x00
Const TM1622_CMD_SYS_EN =   0x01
Const TM1622_CMD_LCD_OFF =  0x02
Const TM1622_CMD_LCD_ON =   0x03
Const TM1622_CMD_RC_32K =   0x18
Const TM1622_CMD_TONE_OFF = 0x08
Const TM1622_CMD_IRQ_DIS =  0x80
Const TM1622_CMD_NORMAL =   0xE3

' Local framebuffer: one 16-bit segment mask per digit.
Dim TM1622_RAM(10) As Word

' Independent decimal-point state, one bit per visible digit.
Dim TM1622_DP_RAM As Word

' Scrolling state.
Dim TM1622_ScrollIndex As Byte
Dim TM1622_ScrollCount As Byte

'-----------------------------------------------------------------------------
' Low-level serial functions
'-----------------------------------------------------------------------------

' Send bits MSB first.
Function TM1622_Send_MSB(value As Word, bits As Byte) As Byte
    Dim index As Byte
    Dim mask As Word

    mask = 1
    mask = mask << (bits - 1)

    CFor (index = 0; index < bits; ++index)
        TM1622_WR = 0

        If value And mask Then
            TM1622_DATA = 1
        Else
            TM1622_DATA = 0
        Endif

        WaitUs TM1622_Delay_us
        TM1622_WR = 1
        WaitUs TM1622_Delay_us

        mask = mask >> 1
    CNext
End Function

' Send bits LSB first. TM1622 display RAM data uses this bit order.
Function TM1622_Send_LSB(value As Word, bits As Byte) As Byte
    Dim index As Byte

    CFor (index = 0; index < bits; ++index)
        TM1622_WR = 0

        If value.0 = 1 Then
            TM1622_DATA = 1
        Else
            TM1622_DATA = 0
        Endif

        WaitUs TM1622_Delay_us
        TM1622_WR = 1
        WaitUs TM1622_Delay_us

        value = value >> 1
    CNext
End Function

' Send one 8-bit TM1622 command.
' Frame: 100 + C7..C0 + X
Function TM1622_Cmd(cmd As Byte) As Byte
    TM1622_CS = 0
    WaitUs TM1622_Delay_us

    TM1622_Send_MSB(0x04, 3)       ' COMMAND mode = 100
    TM1622_Send_MSB(cmd, 8)
    TM1622_Send_MSB(0, 1)          ' X = don't care, send 0

    WaitUs TM1622_Delay_us
    TM1622_CS = 1
End Function

' Write one 4-bit TM1622 RAM address.
' Frame: 101 + A5..A0 + D0..D3
Function TM1622_WriteNibble(addr As Byte, dt As Byte) As Byte
    If addr >= TM1622_RAM_ADDRS Then Exit

    TM1622_CS = 0
    WaitUs TM1622_Delay_us

    TM1622_Send_MSB(0x05, 3)       ' WRITE mode = 101
    TM1622_Send_MSB(addr, 6)
    TM1622_Send_LSB(dt And 0x0F, 4)

    WaitUs TM1622_Delay_us
    TM1622_CS = 1
End Function

' Write one complete 16-segment digit as four consecutive nibbles.
Function TM1622_WriteWord(addr As Byte, value As Word) As Byte
    Dim nibble As Byte

    If addr > 60 Then Exit

    TM1622_CS = 0
    WaitUs TM1622_Delay_us

    TM1622_Send_MSB(0x05, 3)       ' WRITE mode = 101
    TM1622_Send_MSB(addr, 6)

    ' Four sequential RAM nibbles. The first nibble is bits 15..12,
    ' while each nibble itself is transmitted D0 first.
    nibble = value.HB >> 4
    TM1622_Send_LSB(nibble, 4)

    nibble = value.HB And 0x0F
    TM1622_Send_LSB(nibble, 4)

    nibble = value.LB >> 4
    TM1622_Send_LSB(nibble, 4)

    nibble = value.LB And 0x0F
    TM1622_Send_LSB(nibble, 4)

    WaitUs TM1622_Delay_us
    TM1622_CS = 1
End Function

''-----------------------------------------------------------------------------
'' Module-specific digit address map
''-----------------------------------------------------------------------------
'' Address order verified on the tested 10-character TM1622 module.
''
'' The corrected left-to-right map is:
''
''   Visible digit :  0    1    2    3    4    5    6    7    8    9
''   RAM address   : 24   20   1C   18   14   10   0C   08   04   00
''
Function TM1622_GetDigitAddr(pos As Byte) As Byte
    Select Case pos
        Case 0
            ReturnValue 0x24
        Case 1
            ReturnValue 0x20
        Case 2
            ReturnValue 0x1C
        Case 3
            ReturnValue 0x18
        Case 4
            ReturnValue 0x14
        Case 5
            ReturnValue 0x10
        Case 6
            ReturnValue 0x0C
        Case 7
            ReturnValue 0x08
        Case 8
            ReturnValue 0x04
        Case 9
            ReturnValue 0x00
        Case Else
            ReturnValue 0x24
    EndSelect
End Function

' Transfer all nine decimal points. Each RAM nibble contains three points in
' bits 1..3; bit 0 is left cleared because it is not part of the DP map.
Function TM1622_UpdateDP() As Byte
    Dim value As Byte

    value = (TM1622_DP_RAM.LB And 0x07) << 1
    TM1622_WriteNibble(41, value)

    value = (TM1622_DP_RAM.LB >> 3) And 0x07
    value = value << 1
    TM1622_WriteNibble(43, value)

    value = TM1622_DP_RAM >> 6
    value = value And 0x07
    value = value << 1
    TM1622_WriteNibble(45, value)
End Function

Function TM1622_SetDP(pos As Byte, state As Bit) As Byte
    Dim mask As Word

    If pos >= 9 Then Exit

    mask = 1
    mask = mask << pos

    If state = 1 Then
        TM1622_DP_RAM = TM1622_DP_RAM Or mask
    Else
        TM1622_DP_RAM = TM1622_DP_RAM And Not mask
    Endif

    TM1622_UpdateDP()
End Function

'Function TM1622_ClearDP() As Byte
    'TM1622_DP_RAM = 0
    'TM1622_UpdateDP()
'End Function

' Transfer the local framebuffer to the display.
Function TM1622_Update() As Byte
    Dim pos As Byte
    Dim addr As Byte

    CFor (pos = 0; pos < TM1622_Digits; ++pos)
        addr = TM1622_GetDigitAddr(pos)
        TM1622_WriteWord(addr, TM1622_RAM(pos))
    CNext

    TM1622_UpdateDP()
    TM1622_DP_RAM = 0'TM1622_ClearDP()
End Function

Function TM1622_Clear_RAM() As Byte
    Dim pos As Byte

    CFor (pos = 0; pos < TM1622_Digits; ++pos)
        TM1622_RAM(pos) = 0
    CNext
End Function

Function TM1622_Clear() As Byte
    TM1622_Clear_RAM()
    TM1622_DP_RAM = 0
    TM1622_Update()
    TM1622_CursorHome()
End Function

' Turn an individual segment bit on or off.
' segment = 0..15. Use TM1622_Test_Segments() to identify physical segments.
Function TM1622_SetSegment(pos As Byte, segment As Byte, state As Bit) As Byte
    Dim mask As Word

    If pos >= TM1622_Digits Then Exit
    If segment > 15 Then Exit

    mask = 1
    mask = mask << segment

    If state = 1 Then
        TM1622_RAM(pos) = TM1622_RAM(pos) Or mask
    Else
        TM1622_RAM(pos) = TM1622_RAM(pos) And Not mask
    Endif

    TM1622_WriteWord(TM1622_GetDigitAddr(pos), TM1622_RAM(pos))
End Function

'-----------------------------------------------------------------------------
' Display and backlight control
'-----------------------------------------------------------------------------
Function TM1622_Display_On() As Byte
    TM1622_Cmd(TM1622_CMD_SYS_EN)
    TM1622_Cmd(TM1622_CMD_LCD_ON)
End Function

Function TM1622_Display_Off() As Byte
    TM1622_Cmd(TM1622_CMD_LCD_OFF)
End Function

Function TM1622_System_Off() As Byte
    TM1622_Cmd(TM1622_CMD_SYS_DIS)
End Function

'-----------------------------------------------------------------------------
' Cursor and text output
'-----------------------------------------------------------------------------
Function TM1622_DPMode(dp As Bit) As Bit
    ReturnValue dp
End Function

Function TM1622_Cursor(pos As Byte) As Byte
    If pos >= TM1622_Digits Then pos = 0
    ReturnValue pos
End Function

Function TM1622_CursorHome() As Byte
    TM1622_Cursor = 0
End Function

Function TM1622_PutC(char As Byte) As Byte
    Dim pos As Byte
    Dim mask As Word

    If TM1622_Cursor >= TM1622_Digits Then TM1622_Cursor = 0

    ' TM1622_DPMode(True): "." activates the independent decimal point after
    ' the previously written character and does not consume a digit position.
    ' TM1622_DPMode(False): "." is written as the D2 segment and consumes one
    ' complete digit position.
    If char = "." And TM1622_DPMode = True Then
        TM1622_PutDP()
        Exit
    Endif

    mask = TM1622_GetEncode(char)
    pos = TM1622_Cursor
    TM1622_RAM(pos) = mask
    TM1622_WriteWord(TM1622_GetDigitAddr(pos), mask)

    ++TM1622_Cursor
End Function

' Activate the independent decimal point after the previously written digit.
' The decimal point does not consume a character position.
Function TM1622_PutDP() As Byte
    Dim pos As Byte

    If TM1622_Cursor = 0 Then Exit

    pos = TM1622_Cursor - 1
    If pos < 9 Then TM1622_SetDP(pos, 1)
End Function

' Print interprets "." as the independent decimal point of the preceding
' character. If the string begins with ".", it is printed as a dot character.
Function TM1622_Print(str[40] As String) As Byte
    Dim index As Byte

    index = 0

    ' PrintC applies TM1622_DPMode(True/False) to every character, including ".".
    While str(index) > 0
        TM1622_PutC(str(index))
        ++index
    Wend
End Function

' Fill a string with a character specified from the left until
' the numBer of digits on the display is reached.
Function TM1622_PadLeft(char As Byte, ByRef str As String) As Byte
	Dim i As Byte
	Dim Lenstr As Byte
	i = 0
	Lenstr = 0
	' Calculate the length of the input string by counting the "."
	' Or not, as indicated by TM1622_DPMode.
	While Pointer(str + i) > 0
		If TM1622_DPMode = 1 Then
			If Pointer(str + i) <> "." Then ++Lenstr
		Else
			++Lenstr
		Endif
		++i
	Wend
	'Calculate the numBer of characters that need To be added.
	i = TM1622_Digits - Lenstr
	' Add the chosen character the number of times i.
	While i > 0
		str = Chr(char) + str
		--i
	Wend
End Function

'-----------------------------------------------------------------------------
' Circular right-to-left scrolling
'-----------------------------------------------------------------------------
Function TM1622_ScrollReset() As Byte
    TM1622_ScrollIndex = 0
    TM1622_ScrollCount = 0
End Function

Function TM1622_ScrollLeft(str[40] As String) As Byte
    Dim raw_len As Byte
    Dim text_len As Byte
    Dim raw_index As Byte
    Dim logical_index As Byte
    Dim target_index As Byte
    Dim visible As Byte
    Dim first_digit As Byte
    Dim char_index As Byte
    Dim digit_index As Byte
    Dim index As Byte
    Dim mask As Word
    Dim found As Bit

    raw_len = Len(str)
    text_len = 0

    ' In DP mode, decimal points modify the preceding character and do not
    ' consume display positions. Outside DP mode, they are normal characters.
    CFor (raw_index = 0; raw_index < raw_len; ++raw_index)
        If TM1622_DPMode = True Then
            If str(raw_index) <> "." Then ++text_len
        Else
            ++text_len
        Endif
    CNext

    If text_len = 0 Then
        TM1622_Clear()
        Exit
    Endif

    If TM1622_ScrollCount < TM1622_Digits Then
        ++TM1622_ScrollCount
    Endif

    visible = TM1622_ScrollCount
    TM1622_Clear_RAM()
    TM1622_DP_RAM = 0
    first_digit = TM1622_Digits - visible

    char_index = TM1622_ScrollIndex
    index = visible - 1

    While index > 0
        If char_index = 0 Then
            char_index = text_len - 1
        Else
            --char_index
        Endif
        --index
    Wend

    CFor (digit_index = 0; digit_index < visible; ++digit_index)
        target_index = char_index
        raw_index = 0
        logical_index = 0
        found = False
        mask = 0

        While raw_index < raw_len And found = False
            If TM1622_DPMode = True And str(raw_index) = "." Then
                ++raw_index
            Else
                If logical_index = target_index Then
                    mask = TM1622_GetEncode(str(raw_index))

                    If TM1622_DPMode = True Then
                        If raw_index + 1 < raw_len Then
                            If str(raw_index + 1) = "." Then
                                If first_digit + digit_index < 9 Then
                                    TM1622_DP_RAM = TM1622_DP_RAM Or (1 << (first_digit + digit_index))
                                Endif
                            Endif
                        Endif
                    Endif

                    found = True
                Else
                    ++logical_index
                    ++raw_index
                Endif
            Endif
        Wend

        If found = False Then mask = 0
        TM1622_RAM(first_digit + digit_index) = mask

        ++char_index
        If char_index >= text_len Then char_index = 0
    CNext

    TM1622_Update()

    ++TM1622_ScrollIndex
    If TM1622_ScrollIndex >= text_len Then TM1622_ScrollIndex = 0
End Function

'-----------------------------------------------------------------------------
' Initialization
'-----------------------------------------------------------------------------
Function TM1622_Init() As Byte
    TM1622_CS_TRIS = 0
    TM1622_WR_TRIS = 0
    TM1622_DATA_TRIS = 0
    'TM1622_LED_TRIS = 0

    TM1622_CS = 1
    TM1622_WR = 1
    TM1622_DATA = 0
    'TM1622_LED = 0

    TM1622_Cursor(0)
    TM1622_DPMode(1)
    TM1622_DP_RAM = 0
    TM1622_ScrollReset()

    WaitMs 10

    TM1622_Cmd(TM1622_CMD_SYS_DIS)
    TM1622_Cmd(TM1622_CMD_RC_32K)
    TM1622_Cmd(TM1622_CMD_NORMAL)
    TM1622_Cmd(TM1622_CMD_TONE_OFF)
    TM1622_Cmd(TM1622_CMD_IRQ_DIS)
    TM1622_Cmd(TM1622_CMD_SYS_EN)
    TM1622_Cmd(TM1622_CMD_LCD_ON)

    TM1622_Clear()
End Function

'-----------------------------------------------------------------------------
' Diagnostic functions
'-----------------------------------------------------------------------------

' Turn on all 256 TM1622 RAM bits.
Function TM1622_Test_All() As Byte
    Dim addr As Byte

    CFor (addr = 0; addr < TM1622_RAM_ADDRS; ++addr)
        TM1622_WriteNibble(addr, 0x0F)
    CNext
End Function

' Clear all 256 TM1622 RAM bits directly.
Function TM1622_Clear_All_RAM() As Byte
    Dim addr As Byte

    CFor (addr = 0; addr < TM1622_RAM_ADDRS; ++addr)
        TM1622_WriteNibble(addr, 0x00)
    CNext

    TM1622_Clear_RAM()
    TM1622_DP_RAM = 0
End Function

'-----------------------------------------------------------------------------
' 16-segment font for the tested LCD glass.
'
' Physical bit order, from bit 15 to bit 0:
'
'   Bit:     15  14  13  12  11  10   9   8   7   6   5   4   3   2   1   0
'   Segment: G1   F   H  A1  D1   E   M   N   K   B   J  A2  D2   C   L  G2
'
' Segment masks:
'   G1=0x8000  F =0x4000  H =0x2000  A1=0x1000
'   D1=0x0800  E =0x0400  M =0x0200  N =0x0100
'   K =0x0080  B =0x0040  J =0x0020  A2=0x0010
'   D2=0x0008  C =0x0004  L =0x0002  G2=0x0001
'-----------------------------------------------------------------------------
Function TM1622_GetEncode(value As Byte) As Word
    Symbol mask = TM1622_GetEncode

    Select Case value
        Case " "
            mask = 0x0000

        Case "0"
            mask = 0x5C5C
        Case "1"
            mask = 0x00C4
        Case "2"
            mask = 0x9C59
        Case "3"
            mask = 0x985D
        Case "4"
            mask = 0xC045
        Case "5"
            mask = 0xD81D
        Case "6"
            mask = 0xDC1D
        Case "7"
            mask = 0x1054
        Case "8"
            mask = 0xDC5D
        Case "9"
            mask = 0xD85D

        Case "A", "a"
            mask = 0xD455
        Case "B", "b"
            mask = 0x1A7D
        Case "C", "c"
            mask = 0x5C18
        Case "D", "d"
            mask = 0x1A7C
        Case "E", "e"
            mask = 0xDC19
        Case "F", "f"
            mask = 0xD411
        Case "G", "g"
            mask = 0x5C1D
        Case "H", "h"
            mask = 0xC445
        Case "I", "i"
            mask = 0x1A38
        Case "J", "j"
            mask = 0x0C4C
        Case "K", "k"
            mask = 0xC482
        Case "L", "l"
            mask = 0x4C08
        Case "M", "m"
            mask = 0x64C4
        Case "N", "n"
            mask = 0x6446
        Case "O", "o"
            mask = 0x5C5C
        Case "P", "p"
            mask = 0xD451
        Case "Q", "q"
            mask = 0x5C5E
        Case "R", "r"
            mask = 0xD453
        Case "S", "s"
            mask = 0xD81D
        Case "T", "t"
            mask = 0x1230
        Case "U", "u"
            mask = 0x4C4C
        Case "V", "v"
            mask = 0x4580
        Case "W", "w"
            mask = 0x4546
        Case "X", "x"
            mask = 0x2182
        Case "Y", "y"
            mask = 0xCA41
        Case "Z", "z"
            mask = 0x1998
         Case "Ñ", "ñ"
            mask = 0x6416

        Case "!"
            mask = 0x0224
        Case 34
            mask = 0x4040
        Case "#"
            mask = 0xC621
        Case "$"
            mask = 0xDA3D
        Case "%"
            mask = 0xD3AD
        Case "&"
            mask = 0xBC2A
        Case "'"
            mask = 0x0020
        Case "("
            mask = 0x2100
        Case ")"
            mask = 0x0082
        Case "*"
            mask = 0xA3A3
        Case "+"
            mask = 0x8221
        Case ","
            mask = 0x0100
        Case "-"
            mask = 0x8001
        Case "."
            mask = 0x0008
        Case "/"
            mask = 0x0180
        Case "\"
            mask = 0x2002
        Case ":"
            mask = 0x0220
        Case ";"
            mask = 0x0300
        Case "<"
            mask = 0x2101
        Case "="
            mask = 0x8809
        Case ">"
            mask = 0x8082
        Case "?"
            mask = 0x9091
        Case "_"
			mask = 0x0808
        Case "["
            mask = 0x0238
        Case "]"
            mask = 0x1A20
        Case "º"
            mask = 0xD020
        Case "{"
            mask = 0x8238
        Case "}"
            mask = 0x1A21
        Case Else
            mask = 0x0000
    EndSelect

    ReturnValue mask
End Function
