├── 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 | --------------------------------------------------------------------------------