├── uSDX Controller.png
├── go.mod
├── README.md
├── app.go
├── pty
└── pty.go
├── ambEmuLcd
├── usdxLcd.go
├── ambEmuLcd.go
├── vrEmuLcd.h
└── vrEmuLcd.c
├── controls
├── lowLevel.go
├── highLevel.go
└── cat.go
└── gui.go
/uSDX Controller.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adrianboyko/uSDX-Controller-App/HEAD/uSDX Controller.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module uSDX
2 |
3 | go 1.15
4 |
5 | require (
6 | gioui.org v0.0.0-20201103143906-88b3c84ef626
7 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
8 | golang.org/x/sys v0.0.0-20201028094953-708e7fb298ac // indirect
9 | )
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # uSDX-Controller-App
4 |
5 | My uSDX Control system is made up of three parts:
6 | * An app that runs on a Linux/Mac/Windows system (this repository).
7 | * An Arduino-based board that replaces the LCD on the uSDX.
8 | * Some minor, reversible modifications to the uSDX transceiver.
9 |
10 | For information about the board and associated uSDX mods, see:
11 | * [Firmware](https://github.com/adrianboyko/uSDX-Controller-Firmware)
12 | * [Hardware](https://easyeda.com/AdrianBoyko/usdx-controller)
13 |
14 | This project targets WB2CBA's v1.01 and v1.02 uSDX designs. For more information about this design and the uSDX in general, see:
15 | * [WB2CBA's blog post announcing v1.02](https://antrak.org.tr/blog/projeler/usdx-an-arduino-based-sdr-all-mode-hf-transceiver-pcb-iteration-v1-02/)
16 | * [Introduction to uSDX](https://qrper.com/2020/09/an-introduction-to-the-usdx/)
17 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "C"
4 |
5 | import (
6 | "gioui.org/app"
7 | "github.com/tarm/serial"
8 | "log"
9 | "time"
10 | "uSDX/ambEmuLcd"
11 | "uSDX/controls"
12 | )
13 |
14 | const uSdxDev = "/dev/serial/by-id/usb-Arduino_LLC_Arduino_Nano_Every_ACB4982851514746334C2020FF0E2053-if00"
15 |
16 | func main() {
17 |
18 | lcdEvents := make(chan interface{}, 100)
19 |
20 | go gui(loop, lcdEvents)
21 |
22 | uSdxConfig := &serial.Config{Name: uSdxDev, Baud: 500000, ReadTimeout: 50 * time.Millisecond}
23 | uSdxPort, uSdxErr := serial.OpenPort(uSdxConfig)
24 | if uSdxErr != nil {
25 | log.Fatal(uSdxErr)
26 | }
27 | controls.InitLowLevelControls(uSdxPort)
28 | controls.InitHighLevelControls()
29 | controls.ForceRefresh()
30 | go ambEmuLcd.ProcessSerialLcdData(uSdxPort, lcdEvents)
31 |
32 | // TODO: Use ProcessPtyCat where Pty is available (e.g. Linux/Mac), else ProcessSerialCat (e.g. Windows)
33 | go controls.ProcessPtyCat()
34 |
35 | app.Main()
36 |
37 | }
38 |
39 | func loop(w *app.Window, lcdEvents chan interface{}) error {
40 | for {
41 | select {
42 |
43 | case lcdEvt := <-lcdEvents:
44 | w.Invalidate()
45 | switch e := lcdEvt.(type) {
46 | case *ambEmuLcd.Settled:
47 | controls.HandleSettledEvent(e)
48 | case ambEmuLcd.Updated:
49 | // Nothing yet. Might not use.
50 | }
51 |
52 | case e := <-w.Events():
53 | stop, evtErr := handleWindowEvent(e)
54 | if stop {
55 | return evtErr
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pty/pty.go:
--------------------------------------------------------------------------------
1 | // AMB copied this from src/os/signal/internal/pty/pty.go
2 | // TODO: Replace this with https://github.com/creack/pty
3 |
4 | package pty
5 |
6 | /*
7 | #define _XOPEN_SOURCE 600
8 | #include
9 | #include
10 | #include
11 | #include
12 | */
13 | import "C"
14 |
15 | import (
16 | "fmt"
17 | "os"
18 | "syscall"
19 | )
20 |
21 | // Copyright 2017 The Go Authors. All rights reserved.
22 | // Use of this source code is governed by a BSD-style
23 | // license that can be found in the LICENSE file.
24 |
25 | type PtyError struct {
26 | FuncName string
27 | ErrorString string
28 | Errno syscall.Errno
29 | }
30 |
31 | func ptyError(name string, err error) *PtyError {
32 | return &PtyError{name, err.Error(), err.(syscall.Errno)}
33 | }
34 |
35 | func (e *PtyError) Error() string {
36 | return fmt.Sprintf("%s: %s", e.FuncName, e.ErrorString)
37 | }
38 |
39 | func (e *PtyError) Unwrap() error { return e.Errno }
40 |
41 | // Open returns a control pty and the name of the linked process tty.
42 | func Open() (pty *os.File, processTTY string, err error) {
43 | m, err := C.posix_openpt(C.O_RDWR)
44 | if err != nil {
45 | return nil, "", ptyError("posix_openpt", err)
46 | }
47 | if _, err := C.grantpt(m); err != nil {
48 | C.close(m)
49 | return nil, "", ptyError("grantpt", err)
50 | }
51 | if _, err := C.unlockpt(m); err != nil {
52 | C.close(m)
53 | return nil, "", ptyError("unlockpt", err)
54 | }
55 |
56 | // var tio C.struct_termios
57 | // C.tcgetattr(m, &tio)
58 | // tio.c_lflag ^= C.ECHO
59 | // C.tcsetattr(m, C.TCSANOW|C.TCSAFLUSH, &tio)
60 |
61 | processTTY = C.GoString(C.ptsname(m))
62 | return os.NewFile(uintptr(m), "pty"), processTTY, nil
63 | }
64 |
--------------------------------------------------------------------------------
/ambEmuLcd/usdxLcd.go:
--------------------------------------------------------------------------------
1 | package ambEmuLcd
2 |
3 | // #include "vrEmuLcd.h"
4 | import "C"
5 |
6 | const glyphCount = 8
7 | const rowsPerGlyph = 8
8 |
9 | var glyph = [glyphCount][rowsPerGlyph]uint8{
10 | {0b01000, // 1; logo
11 | 0b00100,
12 | 0b01010,
13 | 0b00101,
14 | 0b01010,
15 | 0b00100,
16 | 0b01000,
17 | 0b00000,
18 | },
19 | {0b00000, // 2; s-meter, 0 bars
20 | 0b00000,
21 | 0b00000,
22 | 0b00000,
23 | 0b00000,
24 | 0b00000,
25 | 0b00000,
26 | 0b00000,
27 | },
28 | {0b10000, // 3; s-meter, 1 bars
29 | 0b10000,
30 | 0b10000,
31 | 0b10000,
32 | 0b10000,
33 | 0b10000,
34 | 0b10000,
35 | 0b10000,
36 | },
37 | {0b10000, // 4; s-meter, 2 bars
38 | 0b10000,
39 | 0b10100,
40 | 0b10100,
41 | 0b10100,
42 | 0b10100,
43 | 0b10100,
44 | 0b10100,
45 | },
46 | {0b10000, // 5; s-meter, 3 bars
47 | 0b10000,
48 | 0b10101,
49 | 0b10101,
50 | 0b10101,
51 | 0b10101,
52 | 0b10101,
53 | 0b10101,
54 | },
55 | {0b01100, // 6; vfo-a
56 | 0b10010,
57 | 0b11110,
58 | 0b10010,
59 | 0b10010,
60 | 0b00000,
61 | 0b00000,
62 | 0b00000,
63 | },
64 | {0b11100, // 7; vfo-b
65 | 0b10010,
66 | 0b11100,
67 | 0b10010,
68 | 0b11100,
69 | 0b00000,
70 | 0b00000,
71 | 0b00000,
72 | },
73 | {0b00000, // 8; TBD
74 | 0b00000,
75 | 0b00000,
76 | 0b00000,
77 | 0b00000,
78 | 0b00000,
79 | 0b00000,
80 | 0b00000,
81 | },
82 | }
83 |
84 | func createGlyph(lcd AmbEmuLcd, n int, aGlyph [rowsPerGlyph]uint8) {
85 | C.vrEmuLcdSendCommand(lcd, C.uchar(0x40|(((n+1)&0x7)<<3)))
86 | for i := 0; i != rowsPerGlyph; i++ {
87 | C.vrEmuLcdWriteByte(lcd, C.uchar(aGlyph[i]))
88 | }
89 | }
90 |
91 | func InitUsdxGlyphs(lcd AmbEmuLcd) {
92 | for i := 0; i < glyphCount; i++ {
93 | createGlyph(lcd, i, glyph[i])
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/controls/lowLevel.go:
--------------------------------------------------------------------------------
1 | package controls
2 |
3 | import "github.com/tarm/serial"
4 |
5 | // These MUST be the values defined by the uSDX controller board:
6 | const (
7 | clickLeftButton = byte(1)
8 | clickRightButton = byte(2)
9 | clickEncoderButton = byte(3)
10 | rotateEncoderClockwise = byte(4)
11 | rotateEncoderCounterclockwise = byte(5)
12 | startPushToTalk = byte(6)
13 | endPushToTalk = byte(7)
14 | )
15 |
16 | var controlNames = []string{
17 | "DUMMY",
18 | "Left Button Click",
19 | "Right Button Click",
20 | "Encoder Button Click",
21 | "Encoder Clockwise Turn",
22 | "Encoder Counter Turn",
23 | "Start Push to Talk",
24 | "End Push to Talk",
25 | }
26 |
27 | var port *serial.Port
28 |
29 | func hardwareAction(action byte) {
30 | //fmt.Printf("%s\n", controlNames[action])
31 | _, err := port.Write([]byte{action})
32 | if err != nil {
33 | panic(err)
34 | }
35 | }
36 |
37 | func InitLowLevelControls(p *serial.Port) {
38 | port = p
39 | }
40 |
41 | func StartPushToTalk() { hardwareAction(startPushToTalk) }
42 | func EndPushToTalk() { hardwareAction(endPushToTalk) }
43 | func ClickLeftButton() { hardwareAction(clickLeftButton) }
44 | func ClickRightButton() { hardwareAction(clickRightButton) }
45 | func ClickEncoderButton() { hardwareAction(clickEncoderButton) }
46 | func RotateEncoderClockwise() { hardwareAction(rotateEncoderClockwise) }
47 | func RotateEncoderCounterclockwise() { hardwareAction(rotateEncoderCounterclockwise) }
48 | func RotateEncoder(dir int) {
49 | if dir > 0 {
50 | RotateEncoderClockwise()
51 | } else if dir < 0 {
52 | RotateEncoderCounterclockwise()
53 | } else {
54 | panic("Rotation direction must be nonzero")
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/controls/highLevel.go:
--------------------------------------------------------------------------------
1 | package controls
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "strconv"
7 | "strings"
8 | "time"
9 | "uSDX/ambEmuLcd"
10 | )
11 |
12 | var line1 []byte
13 | var line2 []byte
14 | var cursor image.Point
15 |
16 | var currentFrequencyA int64 = 0 // Must be MMkkkHHH format, with leading zeros.
17 |
18 | var settledEvents chan *ambEmuLcd.Settled = nil
19 |
20 | var uSdxSettingNames []string // The settings on the menu, in order.
21 |
22 | func InitHighLevelControls() {
23 | //uSdxSettingNames = nil
24 | //asyncGatherUsdxSettings()
25 | }
26 |
27 | func indexOfSettingName(name string) (found bool, index int) {
28 | for i, v := range uSdxSettingNames {
29 | if v == name {
30 | return true, i
31 | }
32 | }
33 | return false, 999
34 | }
35 |
36 | func menuLineToTrimmedString(line []byte) string {
37 | sLine1 := strings.TrimLeft(string(line), " 0123456789.")
38 | return strings.TrimSpace(sLine1)
39 | }
40 |
41 | func asyncGatherUsdxSettings() {
42 | go func() {
43 |
44 | time.Sleep(time.Second)
45 | settledEvents = make(chan *ambEmuLcd.Settled, 100)
46 |
47 | lastLine1Seen := ""
48 | ClickLeftButton()
49 | for {
50 | e := <-settledEvents
51 | sLine1 := menuLineToTrimmedString(e.Line1Data)
52 | if sLine1 == lastLine1Seen {
53 | break
54 | }
55 | uSdxSettingNames = append(uSdxSettingNames, sLine1)
56 | RotateEncoderClockwise()
57 | lastLine1Seen = sLine1
58 | }
59 | settledEvents = nil
60 | ClickRightButton()
61 | fmt.Printf("%v\n", uSdxSettingNames)
62 | }()
63 | }
64 |
65 | func HandleSettledEvent(e *ambEmuLcd.Settled) {
66 | line1 = e.Line1Data
67 | line2 = e.Line2Data
68 | cursor = e.CursorPos
69 |
70 | if line2[0] == 6 { // Custom character 6 is used to indicate VFO A
71 | hzStr := string(line2[1:11]) + "0" // uSDX doesn't display "ones" position, so suffix a "0"
72 | hzStr = strings.ReplaceAll(hzStr, ",", "")
73 | hzStr = strings.ReplaceAll(hzStr, " ", "")
74 | currentFrequencyA, _ = strconv.ParseInt(hzStr, 10, 64)
75 | }
76 |
77 | if settledEvents != nil {
78 | settledEvents <- e
79 | }
80 | }
81 |
82 | func asyncSetParameter(nameToSet, val string) {
83 | settledEvents = make(chan *ambEmuLcd.Settled, 100)
84 | go func() {
85 |
86 | // Enter menu and find out where we are in it
87 | ClickLeftButton()
88 | e := <-settledEvents
89 | currentSettingName := menuLineToTrimmedString(e.Line1Data)
90 |
91 | // Calculate how far we need to move, and in wich direction
92 | found1, currentIndex := indexOfSettingName(currentSettingName)
93 | found2, targetIndex := indexOfSettingName(nameToSet)
94 | if !found1 || !found2 {
95 | ClickRightButton()
96 | return
97 | }
98 | diff := targetIndex - currentIndex
99 | dir := 1
100 | if diff < 0 {
101 | dir = -1
102 | diff *= -1
103 | }
104 |
105 | // Move to the target setting
106 | for n := 0; n < diff; n++ {
107 | RotateEncoder(dir)
108 | //<-settledEvents
109 | time.Sleep(50 * time.Millisecond)
110 | }
111 |
112 | // Edit the target setting's value
113 | ClickLeftButton()
114 |
115 | settledEvents = nil
116 | ClickRightButton()
117 | ClickRightButton()
118 | }()
119 | }
120 |
121 | func SkipToFreqDigit(sought int) {
122 | go func() {
123 | delta := sought - cursor.X
124 | if delta < 0 {
125 | delta = 9 + delta // "9" is the number of digits/commas in the frequency display
126 | }
127 | for i := 0; i < delta; i++ {
128 | ClickEncoderButton()
129 | time.Sleep(50 * time.Millisecond)
130 | }
131 | }()
132 | }
133 |
134 | var digitPositionMap = []byte{99, 0, 1, 99, 2, 3, 4, 99, 5, 6}
135 | var mostRecentHzStr = ""
136 |
137 | const uSdrFreqChars = 9 // including leading spaces and commas
138 |
139 | func SetFrequency(hzStr string) {
140 | settledEvents = make(chan *ambEmuLcd.Settled, 100)
141 | go func() {
142 | // Setting frequency is idempotent.
143 | if hzStr == mostRecentHzStr {
144 | return
145 | }
146 | mostRecentHzStr = hzStr
147 |
148 | hzStr = strings.TrimLeft(hzStr, "0")
149 | daHzStr := hzStr[0 : len(hzStr)-1] // deca-hertz string
150 | daHzStr = fmt.Sprintf("%07s", daHzStr)
151 | //fmt.Println(daHzStr)
152 |
153 | startX := cursor.X
154 | for i := startX; i < startX+uSdrFreqChars; i++ { // cycle through the display's frequency characters
155 | iMod := ((i - 1) % uSdrFreqChars) + 1
156 | currChar := line2[iMod]
157 | if currChar != ',' {
158 | var currDigit byte
159 | if currChar == ' ' { // The display shows leading zeros as spaces.
160 | currDigit = 0
161 | } else {
162 | currDigit = currChar - '0'
163 | }
164 | targetDigit := daHzStr[digitPositionMap[iMod]] - '0'
165 | dir := 1
166 | delta := int(targetDigit) - int(currDigit)
167 | if delta < 0 {
168 | dir = -1
169 | delta = -1 * delta
170 | }
171 | //log.Printf("index:%d curr:%d targ:%d delta:%d", iMod, currDigit, targetDigit, delta)
172 | for n := 0; n < delta; n++ {
173 | RotateEncoder(dir)
174 | <-settledEvents
175 | }
176 | }
177 | ClickEncoderButton()
178 | <-settledEvents
179 | }
180 | settledEvents = nil
181 | }()
182 | }
183 |
184 | // ForceRefresh is used at app startup to force the uSDX to "redraw" the main/start "screen".
185 | func ForceRefresh() {
186 | time.Sleep(500 * time.Millisecond)
187 | settledEvents = make(chan *ambEmuLcd.Settled, 100)
188 | go func() {
189 | ClickLeftButton()
190 | <-settledEvents
191 | time.Sleep(500 * time.Millisecond)
192 | ClickRightButton()
193 | <-settledEvents
194 | settledEvents = nil
195 | }()
196 | }
197 |
--------------------------------------------------------------------------------
/controls/cat.go:
--------------------------------------------------------------------------------
1 | package controls
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "github.com/tarm/serial"
7 | "log"
8 | "os"
9 | "strings"
10 | "uSDX/pty"
11 | )
12 |
13 | // Based, in part, on code from https://github.com/threeme3/QCX-SSB.
14 | // threeme3's credits are as follows:
15 | // CAT support inspired by Charlie Morris, ZL2CTM,
16 | // Contribution by Alex, PE1EVX,
17 | // source: http://zl2ctm.blogspot.com/2020/06/digital-modes-transceiver.html?m=1
18 | //
19 | // Emulated radio is documented at:
20 | // https://www.kenwood.com/i/products/info/amateur/ts_480/pdf/ts_480_pc.pdf
21 |
22 | var catFile *os.File = nil
23 | var catSerial *serial.Port = nil
24 |
25 | func ProcessSerialCat(_catSerial *serial.Port) {
26 | catConfig := &serial.Config{Name: "/dev/ttyS21", Baud: 9600}
27 | _catSerial, catErr := serial.OpenPort(catConfig)
28 | if catErr != nil {
29 | log.Fatal(catErr) // Comment out if you're not going to do CAT
30 | }
31 | catSerial = _catSerial
32 |
33 | catReader := bufio.NewReader(catSerial)
34 | for {
35 | data, err := catReader.ReadBytes(';')
36 | if err != nil {
37 | log.Fatal(err)
38 | }
39 | processCatCommand(data)
40 | }
41 | }
42 |
43 | func ProcessPtyCat() {
44 | const catPttyLink = "ttyUSDX1"
45 |
46 | _catFile, catTty, ptyErr := pty.Open()
47 | if ptyErr != nil {
48 | log.Fatal(ptyErr)
49 | }
50 | catFile = _catFile
51 |
52 | homeDir, _ := os.UserHomeDir()
53 | fq := homeDir + "/" + catPttyLink
54 | _ = os.Remove(fq)
55 | linkErr := os.Symlink(catTty, fq)
56 | if linkErr != nil {
57 | log.Fatal(linkErr)
58 | }
59 |
60 | catReader := bufio.NewReader(catFile)
61 | for {
62 | data, err := catReader.ReadBytes(';')
63 | if err != nil {
64 | log.Fatal(err)
65 | }
66 | processCatCommand(data)
67 | }
68 | }
69 |
70 | func processCatCommand(catCmd []byte) {
71 |
72 | log.Printf("CMD %s", string(catCmd))
73 |
74 | noParams := catCmd[2] == ';'
75 |
76 | switch string(catCmd[0:2]) {
77 |
78 | case "AI":
79 | if noParams {
80 | readAutoInfo()
81 | return
82 | } else if catCmd[2] == '0' {
83 | setAutoInfo(0)
84 | return
85 | }
86 |
87 | case "FA":
88 | if noParams {
89 | readFrequencyA()
90 | return
91 | } else if catCmd[13] == ';' {
92 | setFrequencyA(catCmd)
93 | return
94 | }
95 |
96 | case "ID":
97 | if noParams {
98 | readTransceiverId()
99 | return
100 | }
101 |
102 | case "IF":
103 | if noParams {
104 | readTransceiverStatus()
105 | return
106 | }
107 |
108 | case "MD":
109 | if noParams {
110 | readOperatingMode()
111 | return
112 | }
113 |
114 | case "PS":
115 | if noParams {
116 | readPowerOnOffStatus()
117 | return
118 | } else if catCmd[2] == '1' {
119 | setPowerOnOffStatus('1')
120 | return
121 | }
122 |
123 | case "RS":
124 | if noParams {
125 | readTranceiverStatus()
126 | return
127 | }
128 |
129 | case "RX":
130 | if noParams {
131 | setReceiveMode()
132 | return
133 | }
134 |
135 | case "TX":
136 | if noParams {
137 | setTransmitMode(0)
138 | return
139 | } else if strings.ContainsAny(string(catCmd[2]), "012") {
140 | setTransmitMode(catCmd[2])
141 | return
142 | }
143 |
144 | case "VX":
145 | if catCmd[2] != ';' {
146 | setVoxStatus(catCmd[2])
147 | return
148 | }
149 |
150 | }
151 | respond("?;")
152 |
153 | }
154 |
155 | func respond(response string) {
156 | log.Printf("RSP %s", response)
157 | _, err := catFile.Write([]byte(response))
158 | if err != nil {
159 | log.Fatal(err)
160 | }
161 | }
162 |
163 | func readFrequencyA() {
164 | respond(fmt.Sprintf("FA00%09d;", currentFrequencyA))
165 | }
166 |
167 | func setFrequencyA(catCmd []byte) {
168 | SetFrequency(string(catCmd[2:13]))
169 | }
170 |
171 | func add(sb *strings.Builder, addition string) {
172 | sb.WriteString(addition)
173 | }
174 |
175 | func readTransceiverStatus() {
176 | var sb strings.Builder
177 |
178 | add(&sb, "IF")
179 | add(&sb, fmt.Sprintf("00%09d", currentFrequencyA))
180 | add(&sb, " ") // P2 Always 5 spaces for TS-480
181 | add(&sb, "+0000") // P3 RIT/XIT frequency in Hz
182 | add(&sb, "0") // P4 0: RIT OFF, 1: RIT ON
183 | add(&sb, "0") // P5 0: XIT OFF, 1: XIT ON
184 | add(&sb, "0") // P6 Memory channel bank number
185 | add(&sb, "00") // P7 Memory channel number
186 | add(&sb, "0") // P8 0:RX, 1:TX Why does QCX-SSB always have 0?
187 | add(&sb, "2") // P9 Operating Mode. Why does QCX-SSB always have 2?
188 | add(&sb, "0") // P10 "See FR and FT commands"?
189 | add(&sb, "0") // P11 Scan status
190 | add(&sb, "0") // P12 0: Simplex Operation, 1: Split operation
191 | add(&sb, "0") // P13 0: OFF, 1: TONE, 2: CTCSS
192 | add(&sb, "00") // P14 Tone number, refer to the TN anc CN commands
193 | add(&sb, " ") // P15 Always a space for TS-480
194 | add(&sb, ";")
195 |
196 | respond(sb.String())
197 | }
198 |
199 | func readAutoInfo() {
200 | respond("AI0;") // OFF
201 | }
202 |
203 | func setAutoInfo(p1 byte) {
204 | respond("AI0;")
205 | }
206 |
207 | func readOperatingMode() {
208 | respond("MD2;") // TODO: Why does QCX-SSB always return USB?
209 | }
210 |
211 | func setReceiveMode() {
212 | EndPushToTalk()
213 | //respond("RX0;") // REVIEW: Why does a set command have a response?
214 | }
215 |
216 | func setTransmitMode(p1 byte) {
217 | StartPushToTalk()
218 | }
219 |
220 | func readTranceiverStatus() {
221 | respond("RS0;")
222 | }
223 |
224 | func setVoxStatus(p1 byte) {
225 | // TODO: Why was QCX-SSB implementation answering with given byte?
226 | // respond(fmt.Sprintf("VX%c;", p1))
227 | }
228 |
229 | func readTransceiverId() {
230 | respond("ID020;")
231 | }
232 |
233 | func readPowerOnOffStatus() {
234 | respond("PS1;")
235 | }
236 |
237 | func setPowerOnOffStatus(p1 byte) {
238 | // REVIEW: No implementation provided in QCX-SSB
239 | }
240 |
--------------------------------------------------------------------------------
/gui.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "gioui.org/app"
5 | "gioui.org/f32"
6 | "gioui.org/font/gofont"
7 | "gioui.org/io/event"
8 | "gioui.org/io/pointer"
9 | "gioui.org/io/system"
10 | "gioui.org/layout"
11 | "gioui.org/op"
12 | "gioui.org/op/paint"
13 | . "gioui.org/unit"
14 | "gioui.org/widget"
15 | "gioui.org/widget/material"
16 | "image"
17 | "image/color"
18 | "log"
19 | "os"
20 | "uSDX/ambEmuLcd"
21 | "uSDX/controls"
22 | )
23 |
24 | type (
25 | D = layout.Dimensions
26 | C = layout.Context
27 | )
28 |
29 | var (
30 | theme = material.NewTheme(gofont.Collection())
31 | xPixels, yPixels = ambEmuLcd.NumPixels()
32 | rendPixSize = float32(4) // This is the RENDERED displaySize of an LCD pixel
33 | wPixels = float32(xPixels) * rendPixSize
34 | hPixels = float32(yPixels) * rendPixSize
35 | dm = float32(6) // display margin
36 | displaySize = image.Pt(int(wPixels+2*dm), int(hPixels+2*dm)) // Rendered size of display
37 | )
38 |
39 | var (
40 | leftButton = new(widget.Clickable)
41 | ccwButton = new(widget.Clickable)
42 | rightButton = new(widget.Clickable)
43 | cwButton = new(widget.Clickable)
44 | midButton = new(widget.Clickable)
45 | )
46 |
47 | func gui(loop func(*app.Window, chan interface{}) error, lcdEvents chan interface{}) {
48 |
49 | w := app.NewWindow(
50 | app.Title("uSDX Controller"),
51 | app.Size(Px(float32(displaySize.X)), Px(150)),
52 | )
53 |
54 | if err := loop(w, lcdEvents); err != nil {
55 | log.Fatal(err)
56 | }
57 |
58 | os.Exit(0)
59 | }
60 |
61 | var scrollCount uint32 = 0
62 |
63 | func handleWindowEvent(iEvt event.Event) (stop bool, err error) {
64 | var ops op.Ops
65 | stop = false
66 | err = nil
67 |
68 | if iEvt != nil {
69 | switch e := iEvt.(type) {
70 | case pointer.Event:
71 | if e.Type == pointer.Scroll {
72 | scrollCount += 1
73 | if scrollCount%2 == 0 {
74 | if e.Scroll.Y > 0 {
75 | controls.RotateEncoderCounterclockwise()
76 | } else {
77 | controls.RotateEncoderClockwise()
78 | }
79 | }
80 | }
81 | }
82 | }
83 | for leftButton.Clicked() {
84 | controls.ClickLeftButton()
85 | }
86 | for midButton.Clicked() {
87 | controls.ClickEncoderButton()
88 | }
89 | for rightButton.Clicked() {
90 | controls.ClickRightButton()
91 | }
92 | for ccwButton.Clicked() {
93 | controls.RotateEncoderCounterclockwise()
94 | }
95 | for cwButton.Clicked() {
96 | controls.RotateEncoderClockwise()
97 | }
98 |
99 | switch evt := iEvt.(type) {
100 |
101 | case system.DestroyEvent:
102 | stop = true
103 | err = evt.Err
104 |
105 | case system.FrameEvent:
106 | ambEmuLcd.UpdatePixels() // This updates the pixel state in the LCD emulator.
107 | gtx := layout.NewContext(&ops, evt)
108 | flex := layout.Flex{
109 | Axis: layout.Vertical,
110 | Spacing: layout.SpaceBetween,
111 | }
112 | flex.Layout(gtx,
113 | layout.Rigid(func(gtx C) D { return layoutLcdDisplay(gtx) }),
114 | layout.Flexed(0.5, func(gtx C) D { return layoutButtons(gtx) }),
115 | )
116 | evt.Frame(gtx.Ops)
117 |
118 | case pointer.Event:
119 | handlePointerEvent(evt)
120 | }
121 |
122 | return
123 | }
124 |
125 | func guiPtToLcdCharPt(guiPt f32.Point) (onLcd bool, lcdCharPt image.Point) {
126 | lcdPixPt := image.Point{X: int((guiPt.X - dm) / rendPixSize), Y: int(((guiPt.Y - dm) / rendPixSize))}
127 | lcdCharPt = image.Point{X: lcdPixPt.X / 6, Y: int(lcdPixPt.Y / 9)}
128 | if lcdCharPt.X <= 15 && lcdCharPt.Y <= 1 {
129 | onLcd = true
130 | } else {
131 | onLcd = false
132 | }
133 | return
134 | }
135 |
136 | func handlePointerEvent(evt pointer.Event) {
137 | switch evt.Type {
138 | case pointer.Press:
139 | if overLcd, charPt := guiPtToLcdCharPt(evt.Position); overLcd {
140 | if charPt.Y == 1 && charPt.X >= 1 && charPt.X <= 9 {
141 | controls.SkipToFreqDigit(charPt.X)
142 | }
143 | if charPt.Y == 1 && charPt.X >= 11 && charPt.X <= 13 {
144 | controls.ClickRightButton()
145 | }
146 | }
147 | }
148 | }
149 |
150 | func layoutButtons(gtx C) D {
151 | inset := layout.UniformInset(Px(10))
152 | return inset.Layout(gtx, func(gtx C) D {
153 | flex := layout.Flex{
154 | Axis: layout.Horizontal,
155 | Spacing: layout.SpaceBetween,
156 | }
157 | return flex.Layout(gtx,
158 | layout.Rigid(func(gtx C) D { return material.Button(theme, leftButton, " ").Layout(gtx) }),
159 | layout.Rigid(func(gtx C) D { return layoutEncoderButtons(gtx) }),
160 | layout.Rigid(func(gtx C) D { return material.Button(theme, rightButton, " ").Layout(gtx) }),
161 | )
162 | })
163 | }
164 |
165 | func layoutEncoderButtons(gtx C) D {
166 | flex := layout.Flex{Axis: layout.Horizontal}
167 | return flex.Layout(gtx,
168 | layout.Rigid(func(gtx C) D { return material.Button(theme, ccwButton, "<").Layout(gtx) }),
169 | layout.Rigid(func(gtx C) D { return D{Size: image.Pt(5, 0)} }),
170 | layout.Rigid(func(gtx C) D { return material.Button(theme, midButton, " ").Layout(gtx) }),
171 | layout.Rigid(func(gtx C) D { return D{Size: image.Pt(5, 0)} }),
172 | layout.Rigid(func(gtx C) D { return material.Button(theme, cwButton, ">").Layout(gtx) }),
173 | )
174 | }
175 |
176 | func layoutLcdDisplay(gtx C) D {
177 |
178 | paint.ColorOp{Color: color.RGBA{R: 0x1f, G: 0x1f, B: 0xff, A: 0xFF}}.Add(gtx.Ops)
179 | paint.PaintOp{Rect: f32.Rect(0, 0, wPixels+2*dm, hPixels+2*dm)}.Add(gtx.Ops)
180 |
181 | op.Offset(f32.Pt(dm, dm)).Add(gtx.Ops)
182 |
183 | for iX := 0; iX < xPixels; iX++ {
184 | for iY := 0; iY < yPixels; iY++ {
185 | var r, g, b uint8
186 | switch ambEmuLcd.PixelState(iX, iY) {
187 | case -1:
188 | continue
189 | case 0:
190 | r = 0x00
191 | g = 0x00
192 | b = 0xe0
193 | case 1:
194 | r = 0xf0
195 | g = 0xf0
196 | b = 0xff
197 | }
198 | pX0 := float32(iX) * rendPixSize
199 | pY0 := float32(iY) * rendPixSize
200 | pixel := f32.Rect(pX0, pY0, pX0+rendPixSize, pY0+rendPixSize)
201 | paint.ColorOp{Color: color.RGBA{R: r, G: g, B: b, A: 0xFF}}.Add(gtx.Ops)
202 | paint.PaintOp{Rect: pixel}.Add(gtx.Ops)
203 | }
204 | }
205 |
206 | return D{Size: displaySize}
207 | }
208 |
--------------------------------------------------------------------------------
/ambEmuLcd/ambEmuLcd.go:
--------------------------------------------------------------------------------
1 | package ambEmuLcd
2 |
3 | // #include "vrEmuLcd.h"
4 | import "C"
5 |
6 | import (
7 | "fmt"
8 | "github.com/tarm/serial"
9 | "image"
10 | "log"
11 | "unsafe"
12 | )
13 |
14 | type Updated struct{}
15 |
16 | type Settled struct {
17 | Line1Data []byte
18 | Line2Data []byte
19 | CursorPos image.Point
20 | }
21 |
22 | // The microcontroller port pins used for each LCD signal
23 | const RS_BIT = 5
24 | const D7_BIT = 3
25 | const D6_BIT = 2
26 | const D5_BIT = 1
27 | const D4_BIT = 0
28 |
29 | const (
30 | WAITING_FOR_NIBBLE_1 = iota
31 | WAITING_FOR_NIBBLE_2
32 | )
33 |
34 | const (
35 | CMD_REGISTER = 0
36 | DATA_REGISTER = 1
37 | )
38 |
39 | var state = WAITING_FOR_NIBBLE_1
40 |
41 | var rs, d7, d6, d5, d4 byte
42 |
43 | type AmbEmuLcd = *C.VrEmuLcd
44 |
45 | var lcd AmbEmuLcd = C.vrEmuLcdNew(C.int(16), C.int(2), C.EmuLcdRomA02)
46 |
47 | func cBytesToByteSlice(src *C.byte, start int, len int) []byte {
48 | return (*(*[1 << 30]byte)(unsafe.Pointer(src)))[start : start+len : start+len]
49 | }
50 |
51 | func createSettledEvent() *Settled {
52 | ddRam := C.vrEmuLcdGetDisplayRam(lcd)
53 | // Following code assume 2 row, 16 col, unscrolled LCD.
54 | line1 := cBytesToByteSlice(ddRam, 0x00, 16)
55 | line2 := cBytesToByteSlice(ddRam, 0x40, 16)
56 |
57 | cCursor := C.vrEmuLcdGetCursorOffset(lcd)
58 | goCursor := *(*int32)(unsafe.Pointer(&cCursor)) // is defined as int32_t in C code.
59 | var cursorRow, cursorCol int
60 | if 0 <= goCursor && goCursor < 0x40 {
61 | cursorRow = 1
62 | cursorCol = int(goCursor)
63 | } else {
64 | cursorRow = 2
65 | cursorCol = int(goCursor) - 0x40
66 | }
67 | return &Settled{
68 | Line1Data: line1,
69 | Line2Data: line2,
70 | CursorPos: image.Point{X: cursorCol, Y: cursorRow},
71 | }
72 | }
73 |
74 | // NOTE: The following ignores the fact that communication starts in 8bit data mode.
75 | // This works, but only by doing error-recovery from situations that are not actually errors.
76 | // A more sophisticated implementation would take the 8bit startup into account instead of
77 | // assuming only 4bit mode is used.
78 | func interpretByteFromSerial(nibbleByte byte, lcdEvents chan interface{}) {
79 |
80 | nibbleRS := (nibbleByte >> RS_BIT) & 1
81 | nibbleD7 := (nibbleByte >> D7_BIT) & 1
82 | nibbleD6 := (nibbleByte >> D6_BIT) & 1
83 | nibbleD5 := (nibbleByte >> D5_BIT) & 1
84 | nibbleD4 := (nibbleByte >> D4_BIT) & 1
85 |
86 | switch state {
87 |
88 | case WAITING_FOR_NIBBLE_1:
89 |
90 | // Second nibble should reference same register, so save 1st to compare later.
91 | rs = nibbleRS
92 |
93 | // Save the first nibble
94 | d7 = nibbleD7
95 | d6 = nibbleD6
96 | d5 = nibbleD5
97 | d4 = nibbleD4
98 |
99 | state = WAITING_FOR_NIBBLE_2
100 |
101 | case WAITING_FOR_NIBBLE_2:
102 |
103 | d3 := nibbleD7
104 | d2 := nibbleD6
105 | d1 := nibbleD5
106 | d0 := nibbleD4
107 |
108 | fullByte := d7<<7 + d6<<6 + d5<<5 + d4<<4 + d3<<3 + d2<<2 + d1<<1 + d0<<0
109 |
110 | // If RS values differ for two nibbles, it means that we're out of sync.
111 | rsMismatchErr := rs != nibbleRS
112 |
113 | // If RS is zero then we have a command, but 00000000 is not a vaild command.
114 | noSuchCommandErr := rs == 0 && fullByte == 0
115 |
116 | if !rsMismatchErr && !noSuchCommandErr {
117 | // We are probably in sync so proceed normally.
118 | interpretFullByte(fullByte, lcdEvents)
119 | state = WAITING_FOR_NIBBLE_1
120 | } else {
121 | // We are definitely out of sync, so let's try to get back on track
122 | //fmt.Printf("%01b %08b ERROR!\n", rs, fullByte)
123 | rs = nibbleRS
124 | d7 = nibbleD7
125 | d6 = nibbleD6
126 | d5 = nibbleD5
127 | d4 = nibbleD4
128 | state = WAITING_FOR_NIBBLE_2
129 | }
130 | }
131 | }
132 |
133 | func interpretFullByte(b byte, lcdEvents chan interface{}) {
134 | if rs == 0 && b == 255 { // Power UP signal is not a valid LCD command.
135 | // lcdEvents <- PoweredOn{}
136 | } else if rs == 0 && b == 254 { // Power DOWN signal is not a valid LCD command.
137 | // lcdEvents <- PoweredOff{}
138 | // C.vrEmuLcdSendCommand(lcd, 0b00000001) // Clear Display
139 | // C.vrEmuLcdSendCommand(lcd, 0b00001100) // Cursor Off
140 | } else {
141 | sendFullByteToEmulator(b)
142 | lcdEvents <- Updated{}
143 | }
144 | }
145 |
146 | func sendFullByteToEmulator(b byte) {
147 | //fmt.Printf("%01b %08b\n", rs, b)
148 |
149 | switch rs {
150 | case CMD_REGISTER:
151 | C.vrEmuLcdSendCommand(lcd, C.byte(b))
152 | case DATA_REGISTER:
153 | C.vrEmuLcdWriteByte(lcd, C.byte(b))
154 | default:
155 | panic("Bad RS value")
156 | }
157 | //C.vrEmuLcdPrintDisplayRam(lcd)
158 | }
159 |
160 | func NumPixels() (width int, height int) {
161 | var cWidth C.int
162 | var cHeight C.int
163 | C.vrEmuLcdNumPixels(lcd, &cWidth, &cHeight)
164 | return int(cWidth), int(cHeight)
165 | }
166 |
167 | func PixelState(col int, row int) int {
168 | return int(C.vrEmuLcdPixelState(lcd, C.int(col), C.int(row)))
169 | }
170 |
171 | func UpdatePixels() {
172 | C.vrEmuLcdUpdatePixels(lcd)
173 | }
174 |
175 | func PrintPixels() {
176 | pixelsWidth, pixelsHeight := NumPixels()
177 | fmt.Printf("\n")
178 | for row := 0; row < pixelsHeight; row++ {
179 | fmt.Printf(" ")
180 | for col := 0; col < pixelsWidth; col++ {
181 | switch PixelState(col, row) {
182 | case 1:
183 | fmt.Printf("█")
184 | break
185 | case 0:
186 | fmt.Printf(" ")
187 | break
188 | case -1:
189 | fmt.Printf(" ")
190 | break
191 | }
192 | }
193 | fmt.Printf("\n")
194 | }
195 | }
196 |
197 | func ProcessSerialLcdData(uSdx *serial.Port, lcdEvents chan interface{}) {
198 |
199 | buf := make([]byte, 128)
200 | serialIsIdle := false
201 |
202 | InitUsdxGlyphs(lcd)
203 |
204 | for {
205 | bytesRead, err := uSdx.Read(buf)
206 | if err != nil && err.Error() != "EOF" {
207 | log.Fatal(err)
208 | }
209 | if bytesRead == 0 {
210 | if !serialIsIdle {
211 | serialIsIdle = true
212 | state = WAITING_FOR_NIBBLE_1
213 | lcdEvents <- createSettledEvent()
214 | }
215 | } else {
216 | for i := 0; i < bytesRead; i++ {
217 | interpretByteFromSerial(buf[i], lcdEvents)
218 | }
219 | serialIsIdle = false
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/ambEmuLcd/vrEmuLcd.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Troy's HD44780U Lcd Display Emulator
3 | *
4 | * Copyright (c) 2020 Troy Schrapel
5 | *
6 | * This code is licensed under the MIT license
7 | *
8 | * https://github.com/visrealm/VrEmuLcd
9 | *
10 | */
11 |
12 | #ifndef _VR_EMU_LCD_H_
13 | #define _VR_EMU_LCD_H_
14 |
15 | #if _EMSCRIPTEN
16 | #include
17 | #define VR_LCD_EMU_DLLEXPORT EMSCRIPTEN_KEEPALIVE
18 | #elif COMPILING_DLL
19 | #define VR_LCD_EMU_DLLEXPORT __declspec(dllexport)
20 | #else
21 | #define VR_LCD_EMU_DLLEXPORT
22 | #endif
23 |
24 | #undef byte
25 | typedef unsigned char byte;
26 |
27 | #include
28 |
29 | /* PRIVATE DATA STRUCTURE
30 | * ---------------------------------------- */
31 | struct vrEmuLcd_s;
32 | typedef struct vrEmuLcd_s VrEmuLcd;
33 |
34 | /* PUBLIC CONSTANTS
35 | * ---------------------------------------- */
36 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_CLEAR;
37 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_HOME;
38 |
39 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE;
40 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_INCREMENT;
41 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_DECREMENT;
42 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_SHIFT;
43 |
44 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY;
45 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_ON;
46 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_CURSOR;
47 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_CURSOR_BLINK;
48 |
49 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT;
50 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_CURSOR;
51 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_DISPLAY;
52 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_LEFT;
53 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_RIGHT;
54 |
55 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION;
56 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION_LCD_1LINE;
57 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION_LCD_2LINE;
58 |
59 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SET_CGRAM_ADDR;
60 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SET_DRAM_ADDR;
61 |
62 | typedef enum
63 | {
64 | EmuLcdRomA00, // Japanese
65 | EmuLcdRomA02 // European
66 | } vrEmuLcdCharacterRom;
67 |
68 | /* PUBLIC INTERFACE
69 | * ---------------------------------------- */
70 |
71 | /* Function: vrEmuLcdNew
72 | * --------------------
73 | * create a new LCD
74 | *
75 | * cols: number of display columns (8 to 40)
76 | * rows: number of display rows (1, 2 or 4)
77 | * rom: character rom to load
78 | */
79 | VR_LCD_EMU_DLLEXPORT
80 | VrEmuLcd* vrEmuLcdNew(int width, int height, vrEmuLcdCharacterRom rom);
81 |
82 | /* Function: vrEmuLcdDestroy
83 | * --------------------
84 | * destroy an LCD
85 | *
86 | * lcd: lcd object to destroy / clean up
87 | */
88 | VR_LCD_EMU_DLLEXPORT
89 | void vrEmuLcdDestroy(VrEmuLcd* lcd);
90 |
91 | /* Function: vrEmuLcdSendCommand
92 | * --------------------
93 | * send a command to the lcd (RS is low, R/W is low)
94 | *
95 | * byte: the data (DB0 -> DB7) to send
96 | */
97 | VR_LCD_EMU_DLLEXPORT
98 | void vrEmuLcdSendCommand(VrEmuLcd* lcd, byte data);
99 |
100 |
101 | /* Function: vrEmuLcdWriteByte
102 | * --------------------
103 | * write a byte to the lcd (RS is high, R/W is low)
104 | *
105 | * byte: the data (DB0 -> DB7) to send
106 | */
107 | VR_LCD_EMU_DLLEXPORT
108 | void vrEmuLcdWriteByte(VrEmuLcd* lcd, byte data);
109 |
110 | /* Function: vrEmuLcdWriteString
111 | * ----------------------------------------
112 | * write a string to the lcd
113 | * iterates over the characters and sends them individually
114 | *
115 | * str: the string to write.
116 | */
117 | VR_LCD_EMU_DLLEXPORT
118 | void vrEmuLcdWriteString(VrEmuLcd* lcd, const char *str);
119 |
120 |
121 | /* Function: vrEmuLcdGetDataOffset
122 | * ----------------------------------------
123 | * return the character offset in ddram for a given
124 | * row and column on the display.
125 | *
126 | * can be used to set the current cursor address
127 | */
128 | VR_LCD_EMU_DLLEXPORT
129 | int vrEmuLcdGetDataOffset(VrEmuLcd* lcd, int row, int col);
130 |
131 | /* Function: vrEmuLcdReadByte
132 | * --------------------
133 | * read a byte from the lcd (RS is high, R/W is high)
134 | *
135 | * returns: the data (DB0 -> DB7) at the current address
136 | */
137 | VR_LCD_EMU_DLLEXPORT
138 | byte vrEmuLcdReadByte(VrEmuLcd* lcd);
139 |
140 | /* Function: vrEmuLcdReadAddress
141 | * --------------------
142 | * read the current address offset (RS is high, R/W is high)
143 | *
144 | * returns: the current address offset (either CGRAM or DDRAM)
145 | */
146 | VR_LCD_EMU_DLLEXPORT
147 | byte vrEmuLcdReadAddress(VrEmuLcd* lcd);
148 |
149 |
150 | /* Function: vrEmuLcdCharBits
151 | * ----------------------------------------
152 | * return a character's pixel data
153 | *
154 | * pixel data consists of 5 bytes where each is
155 | * a vertical row of bits for the character
156 | *
157 | * c: character index
158 | * 0 - 15 cgram
159 | * 16 - 255 rom
160 | */
161 | VR_LCD_EMU_DLLEXPORT
162 | const byte *vrEmuLcdCharBits(VrEmuLcd* lcd, byte c);
163 |
164 | /* Function: vrEmuLcdUpdatePixels
165 | * ----------------------------------------
166 | * updates the display's pixel data
167 | * changes are only reflected in the pixel data when this function is called
168 | */
169 | VR_LCD_EMU_DLLEXPORT
170 | void vrEmuLcdUpdatePixels(VrEmuLcd* lcd);
171 |
172 | /* Function: vrEmuLcdNumPixels
173 | * ----------------------------------------
174 | * get the size of the entire display in pixels (including unused border pixels)
175 | */
176 | VR_LCD_EMU_DLLEXPORT
177 | void vrEmuLcdNumPixels(VrEmuLcd *lcd, int* width, int* height);
178 |
179 | /* Function: vrEmuLcdNumPixelsX
180 | * ----------------------------------------
181 | * returns: number of horizontal pixels in the display
182 | */
183 | VR_LCD_EMU_DLLEXPORT
184 | int vrEmuLcdNumPixelsX(VrEmuLcd *lcd);
185 |
186 | /* Function: vrEmuLcdNumPixelsY
187 | * ----------------------------------------
188 | * returns: number of vertical pixels in the display
189 | */
190 | VR_LCD_EMU_DLLEXPORT
191 | int vrEmuLcdNumPixelsY(VrEmuLcd *lcd);
192 |
193 | /* Function: char vrEmuLcdPixelState
194 | * ----------------------------------------
195 | * returns: pixel state at the given location
196 | *
197 | * -1 = no pixel (character borders)
198 | * 0 = pixel off
199 | * 1 = pixel on
200 | *
201 | */
202 | VR_LCD_EMU_DLLEXPORT
203 | char vrEmuLcdPixelState(VrEmuLcd *lcd, int x, int y);
204 |
205 | /* Function: vrEmuLcdGetDisplayRam
206 | * ----------------------------------------
207 | * returns: A pointer to the display ram
208 | */
209 | VR_LCD_EMU_DLLEXPORT
210 | byte* const vrEmuLcdGetDisplayRam(VrEmuLcd* lcd);
211 |
212 | /* Function: vrEmuLcdGetCursorOffset
213 | * ----------------------------------------
214 | * returns: offset of cursor into the ddRam, as an integer.
215 | */
216 | VR_LCD_EMU_DLLEXPORT
217 | int32_t vrEmuLcdGetCursorOffset(VrEmuLcd* lcd);
218 |
219 |
220 | /* Function: vrEmuLcdGetDisplayRam
221 | * ----------------------------------------
222 | * A debug utility that prints the display ram to stdout.
223 | */
224 | VR_LCD_EMU_DLLEXPORT
225 | void vrEmuLcdPrintDisplayRam(VrEmuLcd* lcd);
226 |
227 |
228 | #endif // _VR_EMU_LCD_H_
229 |
--------------------------------------------------------------------------------
/ambEmuLcd/vrEmuLcd.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Troy's HD44780U Lcd Display Emulator
3 | *
4 | * Copyright (c) 2020 Troy Schrapel
5 | *
6 | * This code is licensed under the MIT license
7 | *
8 | * https://github.com/visrealm/VrEmuLcd
9 | *
10 | */
11 |
12 | #include "vrEmuLcd.h"
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 |
20 | /* PUBLIC CONSTANTS
21 | * ---------------------------------------- */
22 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_CLEAR = 0b00000001;
23 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_HOME = 0b00000010;
24 |
25 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE = 0b00000100;
26 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_INCREMENT = 0b00000010;
27 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_DECREMENT = 0b00000000;
28 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_ENTRY_MODE_SHIFT = 0b00000001;
29 |
30 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY = 0b00001000;
31 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_ON = 0b00000100;
32 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_CURSOR = 0b00000010;
33 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_DISPLAY_CURSOR_BLINK = 0b00000001;
34 |
35 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT = 0b00010000;
36 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_CURSOR = 0b00000000;
37 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_DISPLAY = 0b00001000;
38 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_LEFT = 0b00000000;
39 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SHIFT_RIGHT = 0b00000100;
40 |
41 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION = 0b00100000;
42 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION_LCD_1LINE = 0b00000000;
43 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_FUNCTION_LCD_2LINE = 0b00001000;
44 |
45 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SET_CGRAM_ADDR = 0b01000000;
46 | VR_LCD_EMU_DLLEXPORT const byte LCD_CMD_SET_DRAM_ADDR = 0b10000000;
47 |
48 | /* PRIVATE CONSTANTS
49 | * ---------------------------------------- */
50 | #define CHAR_WIDTH_PX 5
51 | #define CHAR_HEIGHT_PX 8
52 |
53 | #define DATA_WIDTH_CHARS_1ROW 0x80 // AMB changed to hex
54 | #define DATA_WIDTH_CHARS_2ROW 0x40 // AMB changed to hex
55 | #define DATA_WIDTH_CHARS_4ROW 0x20 // AMB changed to hex
56 | #define DDRAM_SIZE 0x80 // AMB changed to hex
57 | #define DDRAM_VISIBLE_SIZE 0x80 // AMB changed to hex
58 |
59 | #define DISPLAY_MIN_COLS 8
60 | #define DISPLAY_MAX_COLS 40
61 | #define DISPLAY_MIN_ROWS 1
62 | #define DISPLAY_MAX_ROWS 4
63 |
64 | #define CGRAM_STORAGE_CHARS 16
65 | #define ROM_FONT_CHARS (256 - CGRAM_STORAGE_CHARS)
66 |
67 | #define CLOCK_TO_MS (1.0 / (CLOCKS_PER_SEC / 1000.0))
68 |
69 | #define CURSOR_MASK (LCD_CMD_DISPLAY_CURSOR_BLINK | LCD_CMD_DISPLAY_CURSOR)
70 | #define CURSOR_BLINK_PERIOD_MS 350
71 | #define CURSOR_BLINK_CYCLE_MS (2 * CURSOR_BLINK_PERIOD_MS)
72 |
73 |
74 | /* font roms */
75 | static const byte fontA00[ROM_FONT_CHARS][CHAR_WIDTH_PX];
76 | static const byte fontA02[ROM_FONT_CHARS][CHAR_WIDTH_PX];
77 |
78 | #define DEFAULT_CGRAM_BYTE 0xaa
79 |
80 | static int rowOffsets[] = { 0x00, 0x40, 0x14, 0x54 };
81 |
82 |
83 | /* PRIVATE DATA STRUCTURE
84 | * ---------------------------------------- */
85 | struct vrEmuLcd_s
86 | {
87 | // size in characters
88 | int cols;
89 | int rows;
90 |
91 | // current state
92 | byte entryModeFlags;
93 | byte displayFlags;
94 | int scrollOffset;
95 |
96 | // ddRam storage
97 | byte* ddRam;
98 | byte* ddPtr;
99 | int dataWidthCols;
100 |
101 | // cgRam storage
102 | byte cgRam[CGRAM_STORAGE_CHARS][CHAR_HEIGHT_PX];
103 | byte* cgPtr;
104 |
105 | // which character rom?
106 | vrEmuLcdCharacterRom characterRom;
107 |
108 | // display pixels
109 | char* pixels;
110 | int pixelsWidth;
111 | int pixelsHeight;
112 | int numPixels;
113 | };
114 |
115 |
116 | /*
117 | * Function: increment
118 | * --------------------
119 | * increments the ddRam pointer of a VrEmuLcd
120 | *
121 | * automatically skips to the correct line and
122 | * rolls back to the start
123 | */
124 | static void increment(VrEmuLcd* lcd)
125 | {
126 | ++lcd->ddPtr;
127 |
128 | // find pointer offset from start
129 | int offset = lcd->ddPtr - lcd->ddRam;
130 |
131 | // 4 row mode's funky addressing scheme
132 | if (lcd->rows > 2)
133 | {
134 | if (offset == 0x28) lcd->ddPtr = lcd->ddRam + 0x40;
135 | else if (offset == 0x68 || offset >= DDRAM_SIZE) lcd->ddPtr = lcd->ddRam;
136 | }
137 | else if (offset >= DDRAM_VISIBLE_SIZE)
138 | {
139 | lcd->ddPtr = lcd->ddRam;
140 | }
141 | }
142 |
143 | /*
144 | * Function: decrement
145 | * --------------------
146 | * decrements the ddRam pointer of a VrEmuLcd
147 | *
148 | * automatically skips to the correct line and
149 | * rolls back to the end
150 | */
151 | static void decrement(VrEmuLcd* lcd)
152 | {
153 | --lcd->ddPtr;
154 |
155 | // find pointer offset from start
156 | int offset = lcd->ddPtr - lcd->ddRam;
157 |
158 | // 4 row mode's funky addressing scheme
159 | if (lcd->rows > 2)
160 | {
161 | if (offset == -1) lcd->ddPtr = lcd->ddRam + 0x67;
162 | else if (offset == 0x39) lcd->ddPtr = lcd->ddRam + 0x27;
163 | }
164 |
165 | if (offset == -1)
166 | {
167 | lcd->ddPtr += DDRAM_VISIBLE_SIZE;
168 | }
169 | else if (offset >= DDRAM_SIZE)
170 | {
171 | lcd->ddPtr = lcd->ddRam;
172 | }
173 | }
174 |
175 | /*
176 | * Function: doShiftDdram
177 | * --------------------
178 | * shift the cursor or display as required
179 | * by the current entry mode flags
180 | */
181 | static void doShift(VrEmuLcd* lcd)
182 | {
183 | // if we're looking at cgram, shift the cg pointer
184 | if (lcd->cgPtr)
185 | {
186 | if (lcd->entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT)
187 | {
188 | ++lcd->cgPtr;
189 | if (lcd->cgPtr >= (byte*)lcd->cgRam + sizeof(lcd->cgRam))
190 | {
191 | lcd->cgPtr = (byte*)lcd->cgRam;
192 | }
193 | }
194 | else
195 | {
196 | --lcd->cgPtr;
197 | if (lcd->cgPtr < (byte*)lcd->cgRam)
198 | {
199 | lcd->cgPtr = (byte*)lcd->cgRam + sizeof(lcd->cgRam) - 1;
200 | }
201 | }
202 | }
203 | // otherwise, shift the ddram pointer or scroll offset
204 | else
205 | {
206 | if (lcd->entryModeFlags & LCD_CMD_ENTRY_MODE_SHIFT)
207 | {
208 | if (lcd->entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT)
209 | {
210 | ++lcd->scrollOffset;
211 | }
212 | else
213 | {
214 | --lcd->scrollOffset;
215 | }
216 | }
217 |
218 | if (lcd->entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT)
219 | {
220 | increment(lcd);
221 | }
222 | else
223 | {
224 | decrement(lcd);
225 | }
226 | }
227 | }
228 |
229 |
230 | /* Function: vrEmuLcdNew
231 | * --------------------
232 | * create a new LCD
233 | *
234 | * cols: number of display columns (8 to 40)
235 | * rows: number of display rows (1, 2 or 4)
236 | * rom: character rom to load
237 | */
238 | VR_LCD_EMU_DLLEXPORT VrEmuLcd* vrEmuLcdNew(int cols, int rows, vrEmuLcdCharacterRom rom)
239 | {
240 | // validate display size
241 | if (cols < DISPLAY_MIN_COLS) cols = DISPLAY_MIN_COLS;
242 | else if (cols > DISPLAY_MAX_COLS) cols = DISPLAY_MAX_COLS;
243 |
244 | if (rows < DISPLAY_MIN_ROWS) rows = DISPLAY_MIN_ROWS;
245 | else if (rows > DISPLAY_MAX_ROWS) rows = DISPLAY_MAX_ROWS;
246 | if (rows == 3) rows = 2;
247 |
248 | // build lcd data structure
249 | VrEmuLcd* lcd = (VrEmuLcd*)malloc(sizeof(VrEmuLcd));
250 | if (lcd != NULL)
251 | {
252 | lcd->cols = cols;
253 | lcd->rows = rows;
254 | lcd->characterRom = rom;
255 |
256 | lcd->ddRam = (byte*)malloc(DDRAM_SIZE);
257 | lcd->ddPtr = lcd->ddRam;
258 | lcd->entryModeFlags = LCD_CMD_ENTRY_MODE_INCREMENT;
259 | lcd->displayFlags = LCD_CMD_DISPLAY_ON; //0x00; AMB change
260 | lcd->scrollOffset = 0x00;
261 | lcd->cgPtr = NULL;
262 | lcd->pixelsWidth = lcd->cols * (CHAR_WIDTH_PX + 1) - 1;
263 | lcd->pixelsHeight = lcd->rows * (CHAR_HEIGHT_PX + 1) - 1;
264 | lcd->numPixels = lcd->pixelsWidth * lcd->pixelsHeight;
265 | lcd->pixels = (char*)malloc(lcd->numPixels);
266 |
267 | switch (lcd->rows)
268 | {
269 | case 1:
270 | lcd->dataWidthCols = DATA_WIDTH_CHARS_1ROW;
271 | break;
272 |
273 | case 2:
274 | lcd->dataWidthCols = DATA_WIDTH_CHARS_2ROW;
275 | break;
276 |
277 | case 4:
278 | lcd->dataWidthCols = DATA_WIDTH_CHARS_4ROW;
279 | break;
280 | }
281 |
282 | // fill arrays with default data
283 | if (lcd->ddRam != NULL)
284 | {
285 | memset(lcd->ddRam, ' ', DDRAM_SIZE);
286 | }
287 |
288 | memset(lcd->cgRam, DEFAULT_CGRAM_BYTE, sizeof(lcd->cgRam));
289 | memset(lcd->pixels, -1, lcd->numPixels);
290 |
291 | vrEmuLcdUpdatePixels(lcd);
292 | }
293 | return lcd;
294 | }
295 |
296 | /*
297 | * Function: vrEmuLcdDestroy
298 | * --------------------
299 | * destroy an LCD
300 | *
301 | * lcd: lcd object to destroy / clean up
302 | */
303 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdDestroy(VrEmuLcd* lcd)
304 | {
305 | if (lcd)
306 | {
307 | free(lcd->ddRam);
308 | free(lcd->pixels);
309 | memset(lcd, 0, sizeof(VrEmuLcd));
310 | free(lcd);
311 | }
312 | }
313 |
314 | /*
315 | * Function: vrEmuLcdSendCommand
316 | * --------------------
317 | * send a command to the lcd (RS is low)
318 | *
319 | * byte: the data (DB0 -> DB7) to send
320 | */
321 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdSendCommand(VrEmuLcd* lcd, byte command)
322 | {
323 | if (command & LCD_CMD_SET_DRAM_ADDR)
324 | {
325 | // ddram address in remaining 7 bits
326 | int offset = (command & 0x7f);
327 | lcd->ddPtr = lcd->ddRam + offset;
328 | lcd->cgPtr = NULL;
329 | }
330 | else if (command & LCD_CMD_SET_CGRAM_ADDR)
331 | {
332 | // cgram address in remaining 6 bits
333 | lcd->cgPtr = (byte*)lcd->cgRam + (command & 0x3f);
334 | }
335 | else if (command & LCD_CMD_FUNCTION)
336 | {
337 | // ignore
338 | }
339 | else if (command & LCD_CMD_SHIFT)
340 | {
341 | if (command & LCD_CMD_SHIFT_DISPLAY)
342 | {
343 | if (command & LCD_CMD_SHIFT_RIGHT)
344 | {
345 | --lcd->scrollOffset;
346 | }
347 | else
348 | {
349 | ++lcd->scrollOffset;
350 | }
351 | }
352 | else
353 | {
354 | if (command & LCD_CMD_SHIFT_RIGHT)
355 | {
356 | increment(lcd);
357 | }
358 | else
359 | {
360 | decrement(lcd);
361 | }
362 | }
363 | }
364 | else if (command & LCD_CMD_DISPLAY)
365 | {
366 | lcd->displayFlags = command;
367 | }
368 | else if (command & LCD_CMD_ENTRY_MODE)
369 | {
370 | lcd->entryModeFlags = command;
371 | }
372 | else if (command & LCD_CMD_HOME)
373 | {
374 | lcd->ddPtr = lcd->ddRam;
375 | lcd->scrollOffset = 0;
376 | }
377 | else if (command & LCD_CMD_CLEAR)
378 | {
379 | if (lcd->ddRam != NULL)
380 | {
381 | memset(lcd->ddRam, ' ', DDRAM_SIZE);
382 | }
383 | lcd->ddPtr = lcd->ddRam;
384 | lcd->scrollOffset = 0;
385 | }
386 | }
387 |
388 | /*
389 | * Function: vrEmuLcdWriteByte
390 | * --------------------
391 | * write a byte to the lcd (RS is high)
392 | *
393 | * byte: the data (DB0 -> DB7) to send
394 | */
395 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdWriteByte(VrEmuLcd* lcd, byte data)
396 | {
397 | if (lcd->cgPtr)
398 | {
399 | // find row offset
400 | int row = (lcd->cgPtr - (byte*)lcd->cgRam) % CHAR_HEIGHT_PX;
401 |
402 | // find starting byte for the current character
403 | byte* startAddr = lcd->cgPtr - row;
404 |
405 | for (int i = 0; i < CHAR_WIDTH_PX; ++i)
406 | {
407 | byte bit = data & ((0x01 << (CHAR_WIDTH_PX - 1)) >> i);
408 | if (bit)
409 | {
410 | *(startAddr + i) |= (0x80 >> row);
411 | }
412 | else
413 | {
414 | *(startAddr + i) &= ~(0x80 >> row);
415 | }
416 | }
417 | }
418 | else
419 | {
420 | *lcd->ddPtr = data;
421 | }
422 | doShift(lcd);
423 | }
424 |
425 |
426 | /*
427 | * Function: vrEmuLcdReadByte
428 | * --------------------
429 | * read a byte from the lcd (RS is high)
430 | *
431 | * returns: the data (DB0 -> DB7) at the current address
432 | */
433 | VR_LCD_EMU_DLLEXPORT byte vrEmuLcdReadByte(VrEmuLcd* lcd)
434 | {
435 | byte data = 0;
436 |
437 | if (lcd->cgPtr)
438 | {
439 | // find row offset
440 | int row = (lcd->cgPtr - (byte*)lcd->cgRam) % 8;
441 |
442 | // find starting byte for the current character
443 | byte* startAddr = lcd->cgPtr - row;
444 |
445 | for (int i = 0; i < CHAR_WIDTH_PX; ++i)
446 | {
447 | if (*(startAddr + i) & (0x80 >> row))
448 | {
449 | data |= ((0x01 << (CHAR_WIDTH_PX - 1)) >> i);
450 | }
451 | }
452 | }
453 | else
454 | {
455 | data = *lcd->ddPtr;
456 | }
457 |
458 | doShift(lcd);
459 |
460 | return data;
461 | }
462 |
463 |
464 | /* Function: vrEmuLcdReadAddress
465 | * --------------------
466 | * read the current address offset (RS is high, R/W is high)
467 | *
468 | * returns: the current address
469 | */
470 | VR_LCD_EMU_DLLEXPORT byte vrEmuLcdReadAddress(VrEmuLcd* lcd)
471 | {
472 | if (lcd->cgPtr)
473 | {
474 | return (lcd->cgPtr - (byte*)lcd->cgRam) & 0x3f;
475 | }
476 |
477 | return (lcd->ddPtr - lcd->ddRam) & 0x7f;
478 | }
479 |
480 | /*
481 | * Function: vrEmuLcdWriteString
482 | * ----------------------------------------
483 | * write a string to the lcd
484 | * iterates over the characters and sends them individually
485 | *
486 | * str: the string to write.
487 | */
488 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdWriteString(VrEmuLcd* lcd, const char* str)
489 | {
490 | const char* ddPtr = str;
491 | while (*ddPtr != '\0')
492 | {
493 | vrEmuLcdWriteByte(lcd, *ddPtr);
494 | ++ddPtr;
495 | }
496 | }
497 |
498 | /*
499 | * Function: vrEmuLcdCharBits
500 | * ----------------------------------------
501 | * return a character's pixel data
502 | *
503 | * pixel data consists of 5 bytes where each is
504 | * a vertical row of bits for the character
505 | *
506 | * c: character index
507 | * 0 - 15 cgram
508 | * 16 - 255 rom
509 | */
510 | VR_LCD_EMU_DLLEXPORT const byte* vrEmuLcdCharBits(VrEmuLcd* lcd, byte c)
511 | {
512 | if (c < CGRAM_STORAGE_CHARS)
513 | {
514 | return lcd->cgRam[c];
515 | }
516 |
517 | const int characterRomIndex = c - CGRAM_STORAGE_CHARS;
518 |
519 | switch (lcd->characterRom)
520 | {
521 | case EmuLcdRomA00:
522 | return fontA00[characterRomIndex];
523 |
524 | case EmuLcdRomA02:
525 | default:
526 | return fontA02[characterRomIndex];
527 | }
528 | }
529 |
530 | /*
531 | * Function: vrEmuLcdGetDataOffset
532 | * ----------------------------------------
533 | * return the character offset in ddram for a given
534 | * row and column.
535 | *
536 | * can be used to set the current cursor address
537 | */
538 | VR_LCD_EMU_DLLEXPORT int vrEmuLcdGetDataOffset(VrEmuLcd* lcd, int row, int col)
539 | {
540 | // adjust for display scroll offset
541 | if (row >= lcd->rows) row = lcd->rows - 1;
542 |
543 | while (lcd->scrollOffset < 0)
544 | {
545 | lcd->scrollOffset += lcd->dataWidthCols;
546 | }
547 |
548 | int dataCol = (col + lcd->scrollOffset) % lcd->dataWidthCols;
549 | int rowOffset = row * lcd->dataWidthCols;
550 |
551 | if (lcd->rows > 2)
552 | {
553 | rowOffset = rowOffsets[row];
554 | }
555 |
556 | return rowOffset + dataCol;
557 | }
558 |
559 | /*
560 | * Function: vrEmuLcdUpdatePixels
561 | * ----------------------------------------
562 | * updates the display's pixel data
563 | * changes are only reflected in the pixel data when this function is called
564 | */
565 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdUpdatePixels(VrEmuLcd* lcd)
566 | {
567 | // determine cursor blink state
568 | int cursorOn = lcd->displayFlags & CURSOR_MASK;
569 | if (lcd->displayFlags & LCD_CMD_DISPLAY_CURSOR_BLINK)
570 | {
571 | if (((int)(clock() * CLOCK_TO_MS) % CURSOR_BLINK_CYCLE_MS)
572 | < CURSOR_BLINK_PERIOD_MS)
573 | {
574 | cursorOn &= ~LCD_CMD_DISPLAY_CURSOR_BLINK;
575 | }
576 | }
577 |
578 | int displayOn = lcd->displayFlags & LCD_CMD_DISPLAY_ON;
579 |
580 | // /cycle through each row of the display
581 | for (int row = 0; row < lcd->rows; ++row)
582 | {
583 | for (int col = 0; col < lcd->cols; ++col)
584 | {
585 | // find top-left pixel for the current display character position
586 | char* charTopLeft = lcd->pixels + (row * (CHAR_HEIGHT_PX + 1) * lcd->pixelsWidth) + col * (CHAR_WIDTH_PX + 1);
587 |
588 | // find current character in ddram
589 | int offset = vrEmuLcdGetDataOffset(lcd, row, col);
590 | byte* ddPtr = lcd->ddRam + offset;
591 |
592 | // only draw cursor if the data pointer is pointing at this character
593 | int drawCursor = cursorOn && (ddPtr == lcd->ddPtr);
594 |
595 | // get the character data (bits) for the current character
596 | const byte* bits = vrEmuLcdCharBits(lcd, *ddPtr);
597 |
598 | // apply its bits to the pixel data
599 | for (int y = 0; y < CHAR_HEIGHT_PX; ++y)
600 | {
601 | // set pixel pointer
602 | char* pixel = charTopLeft + y * lcd->pixelsWidth;
603 | for (int x = 0; x < CHAR_WIDTH_PX; ++x)
604 | {
605 | // is the display on?
606 | if (!displayOn)
607 | {
608 | *pixel = 0;
609 | continue;
610 | }
611 |
612 | // set the pixel data from the character bits
613 | // Note: The ROM fonts are defined wrong, shifted one pixel too low. Code added to compensate.
614 | int glyphShift = (*ddPtr > 15) ? 1 : 0;
615 | *pixel = (bits[x]<> y)) ? 1 : 0;
616 |
617 | // override with cursor data if appropriate
618 | if (drawCursor)
619 | {
620 | if ((cursorOn & LCD_CMD_DISPLAY_CURSOR_BLINK) ||
621 | ((cursorOn & LCD_CMD_DISPLAY_CURSOR) && y == CHAR_HEIGHT_PX - 1))
622 | {
623 | *pixel = 1;
624 | }
625 | }
626 |
627 | // next pixel
628 | ++pixel;
629 | }
630 | }
631 | }
632 | }
633 | }
634 |
635 | /*
636 | * Function: vrEmuLcdNumPixels
637 | * ----------------------------------------
638 | * get the number of pixels for the entire display
639 | */
640 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdNumPixels(VrEmuLcd* lcd, int* cols, int* rows)
641 | {
642 | if (cols) *cols = vrEmuLcdNumPixelsX(lcd);
643 | if (rows) *rows = vrEmuLcdNumPixelsY(lcd);
644 | }
645 |
646 | /*
647 | * Function: vrEmuLcdNumPixelsX
648 | * ----------------------------------------
649 | * returns: number of horizontal pixels in the display
650 | */
651 | VR_LCD_EMU_DLLEXPORT int vrEmuLcdNumPixelsX(VrEmuLcd* lcd)
652 | {
653 | return lcd->pixelsWidth;
654 | }
655 |
656 | /*
657 | * Function: vrEmuLcdNumPixelsY
658 | * ----------------------------------------
659 | * returns: number of vertical pixels in the display
660 | */
661 | VR_LCD_EMU_DLLEXPORT int vrEmuLcdNumPixelsY(VrEmuLcd* lcd)
662 | {
663 | return lcd->pixelsHeight;
664 | }
665 |
666 | /*
667 | * Function: char vrEmuLcdPixelState
668 | * ----------------------------------------
669 | * returns: pixel state at the given location
670 | *
671 | * -1 = no pixel (character borders) // REVIEW: This is also the out-of-bounds error result?
672 | * 0 = pixel off
673 | * 1 = pixel on
674 | *
675 | */
676 | VR_LCD_EMU_DLLEXPORT char vrEmuLcdPixelState(VrEmuLcd* lcd, int x, int y)
677 | {
678 | int offset = y * lcd->pixelsWidth + x;
679 | if (offset >= 0 && offset < lcd->numPixels)
680 | return lcd->pixels[offset];
681 | return -1;
682 | }
683 |
684 | /* Function: vrEmuLcdGetDisplayRam
685 | * ----------------------------------------
686 | * returns: A pointer to the display ram
687 | */
688 | VR_LCD_EMU_DLLEXPORT void vrEmuLcdPrintDisplayRam(VrEmuLcd* lcd)
689 | {
690 | for (int i = 0; i < DDRAM_SIZE; i++)
691 | {
692 | byte c = lcd->ddRam[i];
693 | if (lcd->ddRam+i == lcd->ddPtr) printf("_");
694 | else if (c < 16) printf(".");
695 | else printf("%c", c);
696 | }
697 | printf("\n");
698 | }
699 |
700 | /* Function: vrEmuLcdGetDisplayRam
701 | * ----------------------------------------
702 | * returns: A pointer to the display ram
703 | */
704 | VR_LCD_EMU_DLLEXPORT byte* const vrEmuLcdGetDisplayRam(VrEmuLcd* lcd)
705 | {
706 | return lcd->ddRam;
707 | }
708 |
709 | /* Function: vrEmuLcdGetCursorOffset
710 | * ----------------------------------------
711 | * returns: offset of cursor into the ddRam, as an integer.
712 | */
713 | VR_LCD_EMU_DLLEXPORT int32_t vrEmuLcdGetCursorOffset(VrEmuLcd* lcd) {
714 | return lcd->ddPtr - lcd->ddRam;
715 | }
716 |
717 | // A00 (Japanese) character set.
718 | // skip first 16 characters reserved for CGRAM
719 | static const byte fontA00[ROM_FONT_CHARS][CHAR_WIDTH_PX] = {
720 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 16 -
721 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 17 -
722 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 18 -
723 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 19 -
724 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 20 -
725 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 21 -
726 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 22 -
727 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 23 -
728 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 24 -
729 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 25 -
730 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 26 -
731 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 27 -
732 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 28 -
733 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 29 -
734 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 30 -
735 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 31 -
736 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 32 -
737 | {0x00, 0x00, 0xf2, 0x00, 0x00}, // 33 - !
738 | {0x00, 0xe0, 0x00, 0xe0, 0x00}, // 34 - "
739 | {0x28, 0xfe, 0x28, 0xfe, 0x28}, // 35 - #
740 | {0x24, 0x54, 0xfe, 0x54, 0x48}, // 36 - $
741 | {0xc4, 0xc8, 0x10, 0x26, 0x46}, // 37 - %
742 | {0x6c, 0x92, 0xaa, 0x44, 0x0a}, // 38 - &
743 | {0x00, 0xa0, 0xc0, 0x00, 0x00}, // 39 - '
744 | {0x00, 0x38, 0x44, 0x82, 0x00}, // 40 - (
745 | {0x00, 0x82, 0x44, 0x38, 0x00}, // 41 - )
746 | {0x28, 0x10, 0x7c, 0x10, 0x28}, // 42 - *
747 | {0x10, 0x10, 0x7c, 0x10, 0x10}, // 43 - +
748 | {0x00, 0x0a, 0x0c, 0x00, 0x00}, // 44 - ,
749 | {0x10, 0x10, 0x10, 0x10, 0x10}, // 45 - -
750 | {0x00, 0x06, 0x06, 0x00, 0x00}, // 46 - .
751 | {0x04, 0x08, 0x10, 0x20, 0x40}, // 47 - /
752 | {0x7c, 0x8a, 0x92, 0xa2, 0x7c}, // 48 - 0
753 | {0x00, 0x42, 0xfe, 0x02, 0x00}, // 49 - 1
754 | {0x42, 0x86, 0x8a, 0x92, 0x62}, // 50 - 2
755 | {0x84, 0x82, 0xa2, 0xd2, 0x8c}, // 51 - 3
756 | {0x18, 0x28, 0x48, 0xfe, 0x08}, // 52 - 4
757 | {0xe4, 0xa2, 0xa2, 0xa2, 0x9c}, // 53 - 5
758 | {0x3c, 0x52, 0x92, 0x92, 0x0c}, // 54 - 6
759 | {0x80, 0x8e, 0x90, 0xa0, 0xc0}, // 55 - 7
760 | {0x6c, 0x92, 0x92, 0x92, 0x6c}, // 56 - 8
761 | {0x60, 0x92, 0x92, 0x94, 0x78}, // 57 - 9
762 | {0x00, 0x6c, 0x6c, 0x00, 0x00}, // 58 - :
763 | {0x00, 0x6a, 0x6c, 0x00, 0x00}, // 59 - ;
764 | {0x10, 0x28, 0x44, 0x82, 0x00}, // 60 - <
765 | {0x28, 0x28, 0x28, 0x28, 0x28}, // 61 - =
766 | {0x00, 0x82, 0x44, 0x28, 0x10}, // 62 - >
767 | {0x40, 0x80, 0x8a, 0x90, 0x60}, // 63 - ?
768 | {0x4c, 0x92, 0x9e, 0x82, 0x7c}, // 64 - @
769 | {0x7e, 0x90, 0x90, 0x90, 0x7e}, // 65 - A
770 | {0xfe, 0x92, 0x92, 0x92, 0x6c}, // 66 - B
771 | {0x7c, 0x82, 0x82, 0x82, 0x44}, // 67 - C
772 | {0xfe, 0x82, 0x82, 0x44, 0x38}, // 68 - D
773 | {0xfe, 0x92, 0x92, 0x92, 0x82}, // 69 - E
774 | {0xfe, 0x90, 0x90, 0x90, 0x80}, // 70 - F
775 | {0x7c, 0x82, 0x92, 0x92, 0x5e}, // 71 - G
776 | {0xfe, 0x10, 0x10, 0x10, 0xfe}, // 72 - H
777 | {0x00, 0x82, 0xfe, 0x82, 0x00}, // 73 - I
778 | {0x04, 0x82, 0x82, 0xfc, 0x00}, // 74 - J
779 | {0xfe, 0x10, 0x28, 0x44, 0x82}, // 75 - K
780 | {0xfe, 0x02, 0x02, 0x02, 0x02}, // 76 - L
781 | {0xfe, 0x40, 0x30, 0x40, 0xfe}, // 77 - M
782 | {0xfe, 0x20, 0x10, 0x08, 0xfe}, // 78 - N
783 | {0x7c, 0x82, 0x82, 0x82, 0x7c}, // 79 - O
784 | {0xfe, 0x90, 0x90, 0x90, 0x60}, // 80 - P
785 | {0x7c, 0x82, 0x8a, 0x84, 0x7a}, // 81 - Q
786 | {0xfe, 0x90, 0x98, 0x94, 0x62}, // 82 - R
787 | {0x62, 0x92, 0x92, 0x92, 0x8c}, // 83 - S
788 | {0x80, 0x80, 0xfe, 0x80, 0x80}, // 84 - T
789 | {0xfc, 0x02, 0x02, 0x02, 0xfc}, // 85 - U
790 | {0xf8, 0x04, 0x02, 0x04, 0xf8}, // 86 - V
791 | {0xfc, 0x02, 0x1c, 0x02, 0xfc}, // 87 - W
792 | {0xc6, 0x28, 0x10, 0x28, 0xc6}, // 88 - X
793 | {0xe0, 0x10, 0x0e, 0x10, 0xe0}, // 89 - Y
794 | {0x86, 0x8a, 0x92, 0xa2, 0xc2}, // 90 - Z
795 | {0x00, 0xfe, 0x82, 0x82, 0x00}, // 91 - [
796 | {0xa8, 0x68, 0x3e, 0x68, 0xa8}, // 92 - fwd slash
797 | {0x00, 0x82, 0x82, 0xfe, 0x00}, // 93 - ]
798 | {0x20, 0x40, 0x80, 0x40, 0x20}, // 94 - ^
799 | {0x02, 0x02, 0x02, 0x02, 0x02}, // 95 - _
800 | {0x00, 0x80, 0x40, 0x20, 0x00}, // 96 - `
801 | {0x04, 0x2a, 0x2a, 0x2a, 0x1e}, // 97 - a
802 | {0xfe, 0x12, 0x22, 0x22, 0x1c}, // 98 - b
803 | {0x1c, 0x22, 0x22, 0x22, 0x04}, // 99 - c
804 | {0x1c, 0x22, 0x22, 0x12, 0xfe}, // 100 - d
805 | {0x1c, 0x2a, 0x2a, 0x2a, 0x18}, // 101 - e
806 | {0x10, 0x7e, 0x90, 0x80, 0x40}, // 102 - f
807 | {0x30, 0x4a, 0x4a, 0x4a, 0x7c}, // 103 - g
808 | {0xfe, 0x10, 0x20, 0x20, 0x1e}, // 104 - h
809 | {0x00, 0x22, 0xbe, 0x02, 0x00}, // 105 - i
810 | {0x04, 0x02, 0x22, 0xbc, 0x00}, // 106 - j
811 | {0xfe, 0x08, 0x14, 0x22, 0x00}, // 107 - k
812 | {0x02, 0x82, 0xfe, 0x02, 0x02}, // 108 - l
813 | {0x3e, 0x20, 0x18, 0x20, 0x1e}, // 109 - m
814 | {0x3e, 0x10, 0x20, 0x20, 0x1e}, // 110 - n
815 | {0x1c, 0x22, 0x22, 0x22, 0x1c}, // 111 - o
816 | {0x3e, 0x28, 0x28, 0x28, 0x10}, // 112 - p
817 | {0x10, 0x28, 0x28, 0x18, 0x3e}, // 113 - q
818 | {0x3e, 0x10, 0x20, 0x20, 0x10}, // 114 - r
819 | {0x12, 0x2a, 0x2a, 0x2a, 0x04}, // 115 - s
820 | {0x20, 0xfc, 0x22, 0x02, 0x04}, // 116 - t
821 | {0x3c, 0x02, 0x02, 0x04, 0x3e}, // 117 - u
822 | {0x38, 0x04, 0x02, 0x04, 0x38}, // 118 - v
823 | {0x3c, 0x02, 0x0c, 0x02, 0x3c}, // 119 - w
824 | {0x22, 0x14, 0x08, 0x14, 0x22}, // 120 - x
825 | {0x30, 0x0a, 0x0a, 0x0a, 0x3c}, // 121 - y
826 | {0x22, 0x26, 0x2a, 0x32, 0x22}, // 122 - z
827 | {0x00, 0x10, 0x6c, 0x82, 0x00}, // 123 - {
828 | {0x00, 0x00, 0xfe, 0x00, 0x00}, // 124 - |
829 | {0x00, 0x82, 0x6c, 0x10, 0x00}, // 125 - }
830 | {0x10, 0x10, 0x54, 0x38, 0x10}, // 126 - ~
831 | {0x10, 0x38, 0x54, 0x10, 0x10}, // 127 -
832 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 128 -
833 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 129 -
834 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 130 -
835 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 131 -
836 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 132 -
837 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 133 -
838 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 134 -
839 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 135 -
840 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 136 -
841 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 137 -
842 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 138 -
843 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 139 -
844 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 140 -
845 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 141 -
846 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 142 -
847 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 143 -
848 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 144 -
849 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 145 -
850 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 146 -
851 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 147 -
852 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 148 -
853 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 149 -
854 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 150 -
855 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 151 -
856 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 152 -
857 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 153 -
858 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 154 -
859 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 155 -
860 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 156 -
861 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 157 -
862 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 158 -
863 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 159 -
864 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 160 -
865 | {0x0e, 0x0a, 0x0e, 0x00, 0x00}, // 161 -
866 | {0x00, 0x00, 0xf0, 0x80, 0x80}, // 162 -
867 | {0x02, 0x02, 0x1e, 0x00, 0x00}, // 163 -
868 | {0x08, 0x04, 0x02, 0x00, 0x00}, // 164 -
869 | {0x00, 0x18, 0x18, 0x00, 0x00}, // 165 -
870 | {0x50, 0x50, 0x52, 0x54, 0x78}, // 166 -
871 | {0x20, 0x22, 0x2c, 0x28, 0x30}, // 167 -
872 | {0x04, 0x08, 0x1e, 0x20, 0x00}, // 168 -
873 | {0x18, 0x12, 0x32, 0x12, 0x1c}, // 169 -
874 | {0x12, 0x12, 0x1e, 0x12, 0x12}, // 170 -
875 | {0x12, 0x14, 0x18, 0x3e, 0x10}, // 171 -
876 | {0x10, 0x3e, 0x10, 0x14, 0x18}, // 172 -
877 | {0x02, 0x12, 0x12, 0x1e, 0x02}, // 173 -
878 | {0x2a, 0x2a, 0x2a, 0x3e, 0x00}, // 174 -
879 | {0x18, 0x00, 0x1a, 0x02, 0x1c}, // 175 -
880 | {0x10, 0x10, 0x10, 0x10, 0x10}, // 176 -
881 | {0x80, 0x82, 0xbc, 0x90, 0xe0}, // 177 -
882 | {0x08, 0x10, 0x3e, 0x40, 0x80}, // 178 -
883 | {0x70, 0x40, 0xc2, 0x44, 0x78}, // 179 -
884 | {0x42, 0x42, 0x7e, 0x42, 0x42}, // 180 -
885 | {0x44, 0x48, 0x50, 0xfe, 0x40}, // 181 -
886 | {0x42, 0xfc, 0x40, 0x42, 0x7c}, // 182 -
887 | {0x50, 0x50, 0xfe, 0x50, 0x50}, // 183 -
888 | {0x10, 0x62, 0x42, 0x44, 0x78}, // 184 -
889 | {0x20, 0xc0, 0x42, 0x7c, 0x40}, // 185 -
890 | {0x42, 0x42, 0x42, 0x42, 0x7e}, // 186 -
891 | {0x40, 0xf2, 0x44, 0xf8, 0x40}, // 187 -
892 | {0x52, 0x52, 0x02, 0x04, 0x38}, // 188 -
893 | {0x42, 0x44, 0x48, 0x54, 0x62}, // 189 -
894 | {0x40, 0xfc, 0x42, 0x52, 0x62}, // 190 -
895 | {0x60, 0x12, 0x02, 0x04, 0x78}, // 191 -
896 | {0x10, 0x62, 0x52, 0x4c, 0x78}, // 192 -
897 | {0x50, 0x52, 0x7c, 0x90, 0x10}, // 193 -
898 | {0x70, 0x00, 0x72, 0x04, 0x78}, // 194 -
899 | {0x20, 0xa2, 0xbc, 0xa0, 0x20}, // 195 -
900 | {0x00, 0xfe, 0x10, 0x08, 0x00}, // 196 -
901 | {0x22, 0x24, 0xf8, 0x20, 0x20}, // 197 -
902 | {0x02, 0x42, 0x42, 0x42, 0x02}, // 198 -
903 | {0x42, 0x54, 0x48, 0x54, 0x60}, // 199 -
904 | {0x44, 0x48, 0xde, 0x68, 0x44}, // 200 -
905 | {0x00, 0x02, 0x04, 0xf8, 0x00}, // 201 -
906 | {0x1e, 0x00, 0x40, 0x20, 0x1e}, // 202 -
907 | {0xfc, 0x22, 0x22, 0x22, 0x22}, // 203 -
908 | {0x40, 0x42, 0x42, 0x44, 0x78}, // 204 -
909 | {0x20, 0x40, 0x20, 0x10, 0x0c}, // 205 -
910 | {0x4c, 0x40, 0xfe, 0x40, 0x4c}, // 206 -
911 | {0x40, 0x48, 0x44, 0x4a, 0x70}, // 207 -
912 | {0x00, 0x54, 0x54, 0x54, 0x02}, // 208 -
913 | {0x1c, 0x24, 0x44, 0x04, 0x0e}, // 209 -
914 | {0x02, 0x14, 0x08, 0x14, 0x60}, // 210 -
915 | {0x50, 0x7c, 0x52, 0x52, 0x52}, // 211 -
916 | {0x20, 0xfe, 0x20, 0x28, 0x30}, // 212 -
917 | {0x02, 0x42, 0x42, 0x7e, 0x02}, // 213 -
918 | {0x52, 0x52, 0x52, 0x52, 0x7e}, // 214 -
919 | {0x20, 0xa0, 0xa2, 0xa4, 0x38}, // 215 -
920 | {0xf0, 0x02, 0x04, 0xf8, 0x00}, // 216 -
921 | {0x3e, 0x00, 0x7e, 0x02, 0x0c}, // 217 -
922 | {0x7e, 0x02, 0x04, 0x08, 0x10}, // 218 -
923 | {0x7e, 0x42, 0x42, 0x42, 0x7e}, // 219 -
924 | {0x70, 0x40, 0x42, 0x44, 0x78}, // 220 -
925 | {0x42, 0x42, 0x02, 0x04, 0x18}, // 221 -
926 | {0x40, 0x20, 0x80, 0x40, 0x00}, // 222 -
927 | {0xe0, 0xa0, 0xe0, 0x00, 0x00}, // 223 -
928 | {0x1c, 0x22, 0x12, 0x0c, 0x32}, // 224 -
929 | {0x04, 0xaa, 0x2a, 0xaa, 0x1e}, // 225 -
930 | {0x1f, 0x2a, 0x2a, 0x2a, 0x14}, // 226 -
931 | {0x14, 0x2a, 0x2a, 0x22, 0x04}, // 227 -
932 | {0x3f, 0x02, 0x02, 0x04, 0x3e}, // 228 -
933 | {0x1c, 0x22, 0x32, 0x2a, 0x24}, // 229 -
934 | {0x0f, 0x12, 0x22, 0x22, 0x1c}, // 230 -
935 | {0x1c, 0x22, 0x22, 0x22, 0x3f}, // 231 -
936 | {0x04, 0x02, 0x3c, 0x20, 0x20}, // 232 -
937 | {0x20, 0x20, 0x00, 0x70, 0x00}, // 233 -
938 | {0x00, 0x00, 0x20, 0xbf, 0x00}, // 234 -
939 | {0x50, 0x20, 0x50, 0x00, 0x00}, // 235 -
940 | {0x18, 0x24, 0x7e, 0x24, 0x08}, // 236 -
941 | {0x28, 0xfe, 0x2a, 0x02, 0x02}, // 237 -
942 | {0x3e, 0x90, 0xa0, 0xa0, 0x1e}, // 238 -
943 | {0x1c, 0xa2, 0x22, 0xa2, 0x1c}, // 239 -
944 | {0x3f, 0x12, 0x22, 0x22, 0x1c}, // 240 -
945 | {0x1c, 0x22, 0x22, 0x12, 0x3f}, // 241 -
946 | {0x3c, 0x52, 0x52, 0x52, 0x3c}, // 242 -
947 | {0x0c, 0x14, 0x08, 0x14, 0x18}, // 243 -
948 | {0x1a, 0x26, 0x20, 0x26, 0x1a}, // 244 -
949 | {0x3c, 0x82, 0x02, 0x84, 0x3e}, // 245 -
950 | {0xc6, 0xaa, 0x92, 0x82, 0x82}, // 246 -
951 | {0x22, 0x3c, 0x20, 0x3e, 0x22}, // 247 -
952 | {0xa2, 0x94, 0x88, 0x94, 0xa2}, // 248 -
953 | {0x3c, 0x02, 0x02, 0x02, 0x3f}, // 249 -
954 | {0x28, 0x28, 0x3e, 0x28, 0x48}, // 250 -
955 | {0x22, 0x3c, 0x28, 0x28, 0x2e}, // 251 -
956 | {0x3e, 0x28, 0x38, 0x28, 0x3e}, // 252 -
957 | {0x08, 0x08, 0x2a, 0x08, 0x08}, // 253 -
958 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 254 -
959 | {0xff, 0xff, 0xff, 0xff, 0xff} // 255 -
960 | };
961 |
962 |
963 | // A02 (European) character set.
964 | // skip first 16 characters reserved for CGRAM
965 | static const byte fontA02[ROM_FONT_CHARS][CHAR_WIDTH_PX] = {
966 | {0x00, 0x7f, 0x3e, 0x1c, 0x08}, // 16 -
967 | {0x08, 0x1c, 0x3e, 0x7f, 0x00}, // 17 -
968 | {0x30, 0x50, 0x00, 0x30, 0x50}, // 18 -
969 | {0x50, 0x60, 0x00, 0x50, 0x60}, // 19 -
970 | {0x11, 0x33, 0x77, 0x33, 0x11}, // 20 -
971 | {0x44, 0x66, 0x77, 0x66, 0x44}, // 21 -
972 | {0x1c, 0x3e, 0x3e, 0x3e, 0x1c}, // 22 -
973 | {0x04, 0x0e, 0x15, 0x04, 0x7c}, // 23 -
974 | {0x10, 0x20, 0x7f, 0x20, 0x10}, // 24 -
975 | {0x04, 0x02, 0x7f, 0x02, 0x04}, // 25 -
976 | {0x08, 0x08, 0x2a, 0x1c, 0x08}, // 26 -
977 | {0x08, 0x1c, 0x2a, 0x08, 0x08}, // 27 -
978 | {0x01, 0x11, 0x29, 0x45, 0x01}, // 28 -
979 | {0x01, 0x45, 0x29, 0x11, 0x01}, // 29 -
980 | {0x02, 0x0e, 0x3e, 0x0e, 0x02}, // 30 -
981 | {0x20, 0x38, 0x3e, 0x38, 0x20}, // 31 -
982 | {0x00, 0x00, 0x00, 0x00, 0x00}, // 32 -
983 | {0x00, 0x00, 0x79, 0x00, 0x00}, // 33 - !
984 | {0x00, 0x70, 0x00, 0x70, 0x00}, // 34 - "
985 | {0x14, 0x7f, 0x14, 0x7f, 0x14}, // 35 - #
986 | {0x12, 0x2a, 0x7f, 0x2a, 0x24}, // 36 - $
987 | {0x62, 0x64, 0x08, 0x13, 0x23}, // 37 - %
988 | {0x36, 0x49, 0x55, 0x22, 0x05}, // 38 - &
989 | {0x00, 0x50, 0x60, 0x00, 0x00}, // 39 - '
990 | {0x00, 0x1c, 0x22, 0x41, 0x00}, // 40 - (
991 | {0x00, 0x41, 0x22, 0x1c, 0x00}, // 41 - )
992 | {0x14, 0x08, 0x3e, 0x08, 0x14}, // 42 - *
993 | {0x08, 0x08, 0x3e, 0x08, 0x08}, // 43 - +
994 | {0x00, 0x05, 0x06, 0x00, 0x00}, // 44 - ,
995 | {0x08, 0x08, 0x08, 0x08, 0x08}, // 45 - -
996 | {0x00, 0x03, 0x03, 0x00, 0x00}, // 46 - .
997 | {0x02, 0x04, 0x08, 0x10, 0x20}, // 47 - /
998 | {0x3e, 0x45, 0x49, 0x51, 0x3e}, // 48 - 0
999 | {0x00, 0x21, 0x7f, 0x01, 0x00}, // 49 - 1
1000 | {0x21, 0x43, 0x45, 0x49, 0x31}, // 50 - 2
1001 | {0x42, 0x41, 0x51, 0x69, 0x46}, // 51 - 3
1002 | {0x0c, 0x14, 0x24, 0x7f, 0x04}, // 52 - 4
1003 | {0x72, 0x51, 0x51, 0x51, 0x4e}, // 53 - 5
1004 | {0x1e, 0x29, 0x49, 0x49, 0x06}, // 54 - 6
1005 | {0x40, 0x47, 0x48, 0x50, 0x60}, // 55 - 7
1006 | {0x36, 0x49, 0x49, 0x49, 0x36}, // 56 - 8
1007 | {0x30, 0x49, 0x49, 0x4a, 0x3c}, // 57 - 9
1008 | {0x00, 0x36, 0x36, 0x00, 0x00}, // 58 - :
1009 | {0x00, 0x35, 0x36, 0x00, 0x00}, // 59 - ;
1010 | {0x08, 0x14, 0x22, 0x41, 0x00}, // 60 - <
1011 | {0x14, 0x14, 0x14, 0x14, 0x14}, // 61 - =
1012 | {0x00, 0x41, 0x22, 0x14, 0x08}, // 62 - >
1013 | {0x20, 0x40, 0x45, 0x48, 0x30}, // 63 - ?
1014 | {0x26, 0x49, 0x4f, 0x41, 0x3e}, // 64 - @
1015 | {0x1f, 0x24, 0x44, 0x24, 0x1f}, // 65 - A
1016 | {0x7f, 0x49, 0x49, 0x49, 0x36}, // 66 - B
1017 | {0x3e, 0x41, 0x41, 0x41, 0x22}, // 67 - C
1018 | {0x7f, 0x41, 0x41, 0x22, 0x1c}, // 68 - D
1019 | {0x7f, 0x49, 0x49, 0x49, 0x41}, // 69 - E
1020 | {0x7f, 0x48, 0x48, 0x48, 0x40}, // 70 - F
1021 | {0x3e, 0x41, 0x49, 0x49, 0x2f}, // 71 - G
1022 | {0x7f, 0x08, 0x08, 0x08, 0x7f}, // 72 - H
1023 | {0x00, 0x41, 0x7f, 0x41, 0x00}, // 73 - I
1024 | {0x02, 0x41, 0x41, 0x7e, 0x00}, // 74 - J
1025 | {0x7f, 0x08, 0x14, 0x22, 0x41}, // 75 - K
1026 | {0x7f, 0x01, 0x01, 0x01, 0x01}, // 76 - L
1027 | {0x7f, 0x20, 0x18, 0x20, 0x7f}, // 77 - M
1028 | {0x7f, 0x10, 0x08, 0x04, 0x7f}, // 78 - N
1029 | {0x3e, 0x41, 0x41, 0x41, 0x3e}, // 79 - O
1030 | {0x7f, 0x48, 0x48, 0x48, 0x30}, // 80 - P
1031 | {0x3e, 0x41, 0x45, 0x42, 0x3d}, // 81 - Q
1032 | {0x7f, 0x48, 0x4c, 0x4a, 0x31}, // 82 - R
1033 | {0x31, 0x49, 0x49, 0x49, 0x46}, // 83 - S
1034 | {0x40, 0x40, 0x7f, 0x40, 0x40}, // 84 - T
1035 | {0x7e, 0x01, 0x01, 0x01, 0x7e}, // 85 - U
1036 | {0x7c, 0x02, 0x01, 0x02, 0x7c}, // 86 - V
1037 | {0x7e, 0x01, 0x0e, 0x01, 0x7e}, // 87 - W
1038 | {0x63, 0x14, 0x08, 0x14, 0x63}, // 88 - X
1039 | {0x70, 0x08, 0x07, 0x08, 0x70}, // 89 - Y
1040 | {0x43, 0x45, 0x49, 0x51, 0x61}, // 90 - Z
1041 | {0x00, 0x7f, 0x41, 0x41, 0x00}, // 91 - [
1042 | {0x20, 0x10, 0x08, 0x04, 0x02}, // 92 - fwd slash
1043 | {0x00, 0x41, 0x41, 0x7f, 0x00}, // 93 - ]
1044 | {0x10, 0x20, 0x40, 0x20, 0x10}, // 94 - ^
1045 | {0x01, 0x01, 0x01, 0x01, 0x01}, // 95 - _
1046 | {0x00, 0x40, 0x20, 0x10, 0x00}, // 96 - `
1047 | {0x02, 0x15, 0x15, 0x15, 0x0f}, // 97 - a
1048 | {0x7f, 0x09, 0x11, 0x11, 0x0e}, // 98 - b
1049 | {0x0e, 0x11, 0x11, 0x11, 0x02}, // 99 - c
1050 | {0x0e, 0x11, 0x11, 0x09, 0x7f}, // 100 - d
1051 | {0x0e, 0x15, 0x15, 0x15, 0x0c}, // 101 - e
1052 | {0x08, 0x3f, 0x48, 0x40, 0x20}, // 102 - f
1053 | {0x18, 0x25, 0x25, 0x25, 0x3e}, // 103 - g
1054 | {0x7f, 0x08, 0x10, 0x10, 0x0f}, // 104 - h
1055 | {0x00, 0x09, 0x5f, 0x01, 0x00}, // 105 - i
1056 | {0x02, 0x01, 0x11, 0x5e, 0x00}, // 106 - j
1057 | {0x7f, 0x04, 0x0a, 0x11, 0x00}, // 107 - k
1058 | {0x01, 0x41, 0x7f, 0x01, 0x01}, // 108 - l
1059 | {0x1f, 0x10, 0x0c, 0x10, 0x0f}, // 109 - m
1060 | {0x1f, 0x08, 0x10, 0x10, 0x0f}, // 110 - n
1061 | {0x0e, 0x11, 0x11, 0x11, 0x0e}, // 111 - o
1062 | {0x1f, 0x14, 0x14, 0x14, 0x08}, // 112 - p
1063 | {0x08, 0x14, 0x14, 0x0c, 0x1f}, // 113 - q
1064 | {0x1f, 0x08, 0x10, 0x10, 0x08}, // 114 - r
1065 | {0x09, 0x15, 0x15, 0x15, 0x02}, // 115 - s
1066 | {0x10, 0x7e, 0x11, 0x01, 0x02}, // 116 - t
1067 | {0x1e, 0x01, 0x01, 0x02, 0x1f}, // 117 - u
1068 | {0x1c, 0x02, 0x01, 0x02, 0x1c}, // 118 - v
1069 | {0x1e, 0x01, 0x06, 0x01, 0x1e}, // 119 - w
1070 | {0x11, 0x0a, 0x04, 0x0a, 0x11}, // 120 - x
1071 | {0x18, 0x05, 0x05, 0x05, 0x1e}, // 121 - y
1072 | {0x11, 0x13, 0x15, 0x19, 0x11}, // 122 - z
1073 | {0x00, 0x08, 0x36, 0x41, 0x00}, // 123 - {
1074 | {0x00, 0x00, 0x7f, 0x00, 0x00}, // 124 - |
1075 | {0x00, 0x41, 0x36, 0x08, 0x00}, // 125 - }
1076 | {0x04, 0x08, 0x08, 0x04, 0x08}, // 126 - ~
1077 | {0x1e, 0x22, 0x42, 0x22, 0x1e}, // 127 -
1078 | {0x7f, 0x49, 0x49, 0x49, 0x66}, // 128 -
1079 | {0x0f, 0x94, 0xe4, 0x84, 0xff}, // 129 -
1080 | {0x77, 0x08, 0x7f, 0x08, 0x77}, // 130 -
1081 | {0x41, 0x41, 0x49, 0x49, 0x36}, // 131 -
1082 | {0x7f, 0x04, 0x08, 0x10, 0x7f}, // 132 -
1083 | {0x3f, 0x84, 0x48, 0x90, 0x3f}, // 133 -
1084 | {0x02, 0x41, 0x7e, 0x40, 0x7f}, // 134 -
1085 | {0x7f, 0x40, 0x40, 0x40, 0x7f}, // 135 -
1086 | {0x71, 0x0a, 0x04, 0x08, 0x70}, // 136 -
1087 | {0x7e, 0x02, 0x02, 0x02, 0x7f}, // 137 -
1088 | {0x70, 0x08, 0x08, 0x08, 0x7f}, // 138 -
1089 | {0x3f, 0x01, 0x3f, 0x01, 0x3f}, // 139 -
1090 | {0x7e, 0x02, 0x7e, 0x02, 0x7f}, // 140 -
1091 | {0x40, 0x7f, 0x09, 0x09, 0x06}, // 141 -
1092 | {0x7f, 0x09, 0x06, 0x00, 0x7f}, // 142 -
1093 | {0x22, 0x49, 0x51, 0x49, 0x3e}, // 143 -
1094 | {0x0e, 0x11, 0x09, 0x06, 0x19}, // 144 -
1095 | {0x03, 0x03, 0x7f, 0x20, 0x18}, // 145 -
1096 | {0x7f, 0x40, 0x40, 0x40, 0x60}, // 146 -
1097 | {0x11, 0x1e, 0x10, 0x1f, 0x11}, // 147 -
1098 | {0x63, 0x55, 0x49, 0x41, 0x41}, // 148 -
1099 | {0x0e, 0x11, 0x11, 0x1e, 0x10}, // 149 -
1100 | {0x06, 0x06, 0xfc, 0xa3, 0x7f}, // 150 -
1101 | {0x08, 0x10, 0x1e, 0x11, 0x20}, // 151 -
1102 | {0x04, 0x3c, 0x7e, 0x3c, 0x04}, // 152 -
1103 | {0x3e, 0x49, 0x49, 0x49, 0x3e}, // 153 -
1104 | {0x1d, 0x23, 0x20, 0x23, 0x1d}, // 154 -
1105 | {0x06, 0x29, 0x51, 0x49, 0x26}, // 155 -
1106 | {0x0c, 0x14, 0x08, 0x14, 0x18}, // 156 -
1107 | {0x1c, 0x3e, 0x1f, 0x3e, 0x1c}, // 157 -
1108 | {0x0a, 0x15, 0x15, 0x11, 0x02}, // 158 -
1109 | {0x3f, 0x40, 0x40, 0x40, 0x3f}, // 159 -
1110 | {0x7f, 0x7f, 0x00, 0x7f, 0x7f}, // 160 -
1111 | {0x00, 0x00, 0x4f, 0x00, 0x00}, // 161 - ¡
1112 | {0x1c, 0x22, 0x7f, 0x22, 0x04}, // 162 - ¢
1113 | {0x09, 0x3e, 0x49, 0x41, 0x02}, // 163 - £
1114 | {0x22, 0x1c, 0x14, 0x1c, 0x22}, // 164 - ¤
1115 | {0x54, 0x34, 0x1f, 0x34, 0x54}, // 165 - ¥
1116 | {0x00, 0x00, 0x77, 0x00, 0x00}, // 166 - ¦
1117 | {0x02, 0x29, 0x55, 0x4a, 0x20}, // 167 - §
1118 | {0x0a, 0x09, 0x3e, 0x48, 0x28}, // 168 - ¨
1119 | {0x7f, 0x41, 0x5d, 0x49, 0x7f}, // 169 - ©
1120 | {0x09, 0x55, 0x55, 0x55, 0x3d}, // 170 - ª
1121 | {0x08, 0x14, 0x2a, 0x14, 0x22}, // 171 - «
1122 | {0x7f, 0x08, 0x3e, 0x41, 0x3e}, // 172 - ¬
1123 | {0x31, 0x4a, 0x4c, 0x48, 0x7f}, // 173 -
1124 | {0x7f, 0x41, 0x53, 0x45, 0x7f}, // 174 - ®
1125 | {0x00, 0x30, 0x50, 0x00, 0x00}, // 175 - ¯
1126 | {0x70, 0x88, 0x88, 0x70, 0x00}, // 176 - °
1127 | {0x11, 0x11, 0x7d, 0x11, 0x11}, // 177 - ±
1128 | {0x48, 0x98, 0xa8, 0x48, 0x00}, // 178 - ²
1129 | {0x88, 0xa8, 0xa8, 0x50, 0x00}, // 179 - ³
1130 | {0xfe, 0xa0, 0xa4, 0x4f, 0x05}, // 180 - ´
1131 | {0x7f, 0x04, 0x04, 0x08, 0x7c}, // 181 - µ
1132 | {0x30, 0x48, 0x48, 0x7f, 0x7f}, // 182 - ¶
1133 | {0x00, 0x0c, 0x0c, 0x00, 0x00}, // 183 - ·
1134 | {0x0e, 0x11, 0x06, 0x11, 0x0e}, // 184 - ¸
1135 | {0x48, 0xf8, 0x08, 0x00, 0x00}, // 185 - ¹
1136 | {0x39, 0x45, 0x45, 0x45, 0x39}, // 186 - º
1137 | {0x22, 0x14, 0x2a, 0x14, 0x08}, // 187 - »
1138 | {0xe8, 0x16, 0x2a, 0x5f, 0x82}, // 188 - ¼
1139 | {0xe8, 0x10, 0x29, 0x53, 0x8d}, // 189 - ½
1140 | {0xa8, 0xf8, 0x06, 0x0a, 0x1f}, // 190 - ¾
1141 | {0x06, 0x09, 0x51, 0x01, 0x02}, // 191 - ¿
1142 | {0x0f, 0x94, 0x64, 0x14, 0x0f}, // 192 - À
1143 | {0x0f, 0x14, 0x64, 0x94, 0x0f}, // 193 - Á
1144 | {0x0f, 0x54, 0x94, 0x54, 0x0f}, // 194 - Â
1145 | {0x4f, 0x94, 0x94, 0x54, 0x8f}, // 195 - Ã
1146 | {0x0f, 0x94, 0x24, 0x94, 0x0f}, // 196 - Ä
1147 | {0x0f, 0x54, 0xa4, 0x54, 0x0f}, // 197 - Å
1148 | {0x1f, 0x24, 0x7f, 0x49, 0x49}, // 198 - Æ
1149 | {0x78, 0x84, 0x85, 0x87, 0x48}, // 199 - Ç
1150 | {0x1f, 0x95, 0x55, 0x15, 0x11}, // 200 - È
1151 | {0x1f, 0x15, 0x55, 0x95, 0x11}, // 201 - É
1152 | {0x1f, 0x55, 0x95, 0x55, 0x11}, // 202 - Ê
1153 | {0x1f, 0x55, 0x15, 0x55, 0x11}, // 203 - Ë
1154 | {0x00, 0x91, 0x5f, 0x11, 0x00}, // 204 - Ì
1155 | {0x00, 0x11, 0x5f, 0x91, 0x00}, // 205 - Í
1156 | {0x00, 0x51, 0x9f, 0x51, 0x00}, // 206 - Î
1157 | {0x00, 0x51, 0x1f, 0x51, 0x00}, // 207 - Ï
1158 | {0x08, 0x7f, 0x49, 0x41, 0x3e}, // 208 - Ð
1159 | {0x5f, 0x88, 0x84, 0x42, 0x9f}, // 209 - Ñ
1160 | {0x1e, 0xa1, 0x61, 0x21, 0x1e}, // 210 - Ò
1161 | {0x1e, 0x21, 0x61, 0xa1, 0x1e}, // 211 - Ó
1162 | {0x0e, 0x51, 0x91, 0x51, 0x0e}, // 212 - Ô
1163 | {0x4e, 0x91, 0x91, 0x51, 0x8e}, // 213 - Õ
1164 | {0x1e, 0xa1, 0x21, 0xa1, 0x1e}, // 214 - Ö
1165 | {0x22, 0x14, 0x08, 0x14, 0x22}, // 215 - ×
1166 | {0x08, 0x55, 0x7f, 0x55, 0x08}, // 216 - Ø
1167 | {0x3e, 0x81, 0x41, 0x01, 0x3e}, // 217 - Ù
1168 | {0x3e, 0x01, 0x41, 0x81, 0x3e}, // 218 - Ú
1169 | {0x1e, 0x41, 0x81, 0x41, 0x1e}, // 219 - Û
1170 | {0x3e, 0x81, 0x01, 0x81, 0x3e}, // 220 - Ü
1171 | {0x20, 0x10, 0x4f, 0x90, 0x20}, // 221 - Ý
1172 | {0x81, 0xff, 0x25, 0x24, 0x18}, // 222 - Þ
1173 | {0x01, 0x3e, 0x49, 0x49, 0x36}, // 223 - ß
1174 | {0x02, 0x95, 0x55, 0x15, 0x0f}, // 224 - à
1175 | {0x02, 0x15, 0x55, 0x95, 0x0f}, // 225 - á
1176 | {0x02, 0x55, 0x95, 0x55, 0x0f}, // 226 - â
1177 | {0x42, 0x95, 0x95, 0x55, 0x8f}, // 227 - ã
1178 | {0x02, 0x55, 0x15, 0x55, 0x0f}, // 228 - ä
1179 | {0x02, 0x55, 0xb5, 0x55, 0x0f}, // 229 - å
1180 | {0x26, 0x29, 0x1e, 0x29, 0x1a}, // 230 - æ
1181 | {0x18, 0x25, 0x27, 0x24, 0x08}, // 231 - ç
1182 | {0x0e, 0x95, 0x55, 0x15, 0x0c}, // 232 - è
1183 | {0x0e, 0x15, 0x55, 0x95, 0x0c}, // 233 - é
1184 | {0x0e, 0x55, 0x95, 0x55, 0x0c}, // 234 - ê
1185 | {0x0e, 0x55, 0x15, 0x55, 0x0c}, // 235 - ë
1186 | {0x00, 0x89, 0x5f, 0x01, 0x00}, // 236 - ì
1187 | {0x00, 0x09, 0x5f, 0x81, 0x00}, // 237 - í
1188 | {0x00, 0x49, 0x9f, 0x41, 0x00}, // 238 - î
1189 | {0x00, 0x49, 0x1f, 0x41, 0x00}, // 239 - ï
1190 | {0x52, 0x25, 0x55, 0x0d, 0x06}, // 240 - ð
1191 | {0x5f, 0x88, 0x90, 0x50, 0x8f}, // 241 - ñ
1192 | {0x0e, 0x91, 0x51, 0x11, 0x0e}, // 242 - ò
1193 | {0x0e, 0x11, 0x51, 0x91, 0x0e}, // 243 - ó
1194 | {0x06, 0x29, 0x49, 0x29, 0x06}, // 244 - ô
1195 | {0x26, 0x49, 0x49, 0x29, 0x46}, // 245 - õ
1196 | {0x0e, 0x51, 0x11, 0x51, 0x0e}, // 246 - ö
1197 | {0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 - ÷
1198 | {0x08, 0x15, 0x3e, 0x54, 0x08}, // 248 - ø
1199 | {0x1e, 0x81, 0x41, 0x02, 0x1f}, // 249 - ù
1200 | {0x1e, 0x01, 0x41, 0x82, 0x1f}, // 250 - ú
1201 | {0x1e, 0x41, 0x81, 0x42, 0x1f}, // 251 - û
1202 | {0x1e, 0x41, 0x01, 0x42, 0x1f}, // 252 - ü
1203 | {0x18, 0x05, 0x45, 0x85, 0x1e}, // 253 - ý
1204 | {0x00, 0x41, 0x7f, 0x15, 0x08}, // 254 - þ
1205 | {0x18, 0x45, 0x05, 0x45, 0x1e}, // 255 - ÿ
1206 | };
1207 |
--------------------------------------------------------------------------------