From 03ad81d7afaea119df607b6e65f19bd5362fab39 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Thu, 15 Apr 2021 13:07:56 +0200 Subject: Get FELDHELL to work --- sw/eval-clock-cw-tx/README.md | 9 +- sw/eval-clock-cw-tx/gui.py | 222 +++++++++++++++++++++ sw/eval-clock-cw-tx/src/feldhell_font.rs | 277 +++++++++++++++++++++++++++ sw/eval-clock-cw-tx/src/main.rs | 82 +++++--- sw/eval-clock-cw-tx/src/state.rs | 25 ++- sw/eval-clock-cw-tx/src/ui.rs | 5 +- sw/eval-clock-cw-tx/src/usb.rs | 318 ++++++++++++++++++++++++++++--- sw/picardy/src/main.rs | 1 + 8 files changed, 882 insertions(+), 57 deletions(-) create mode 100755 sw/eval-clock-cw-tx/gui.py create mode 100644 sw/eval-clock-cw-tx/src/feldhell_font.rs (limited to 'sw') diff --git a/sw/eval-clock-cw-tx/README.md b/sw/eval-clock-cw-tx/README.md index de12a37..5a22503 100644 --- a/sw/eval-clock-cw-tx/README.md +++ b/sw/eval-clock-cw-tx/README.md @@ -1,11 +1,14 @@ CW TX firmware for eval-clock ============================= -Mostly the same as ../picardy except where different +Sends CW and FELDHELL https://www.qsl.net/zl1bpu/HELL/Feld.htm -pios are different: both Si5351 and display are on same i2c +Taken from ../picardy and then adapted. Includes two FELDHELL fonts from fldigi, therefore the project is also +GPL-licensed, and my own code is MIT licensed. -PB3 is PPTOUTn +pios are different than on picardy: both Si5351 and display are on same i2c + +PB3 is PTTOUTn PB15 is CWOUTn diff --git a/sw/eval-clock-cw-tx/gui.py b/sw/eval-clock-cw-tx/gui.py new file mode 100755 index 0000000..4efde84 --- /dev/null +++ b/sw/eval-clock-cw-tx/gui.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# +# The MIT License (MIT) +# +# Copyright (c) 2021 Matthias P. Braendli +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import socket +import serial +from tkinter import * +from tkinter import ttk +from functools import partial +import sys + +NUM_MACROS=5 + +DEFAULT_MACROS=["CQ WCA de HB9HI HB9HI HB9HI pse K", "REF WCA HB-xxxxx", "12345667890123455678901234567890", "", ""] + +class GUI(Frame): + def __init__(self, root): + super().__init__(root) + self.root = root + self.create_widgets() + self.last_tx_freq = None + self.last_serial_message = bytes() + try: + self.serial = serial.Serial("/dev/ttyACM0", 115200, timeout=0) + print(f"Opened serial port {self.serial.name}") + except Exception as e: + self.serial = None + self.set_status(f"Serial port failed {e}") + + self.root.after(1000, self.fetch_gqrx_freq) + self.root.after(1000, self.read_serial) + + def read_serial(self): + if self.serial is not None: + try: + dat = self.serial.read(1) + if dat: + self.last_serial_message += dat + if self.last_serial_message.endswith(b"\n"): + print(f"< {self.last_serial_message.decode().strip()}") + self.set_status(f"Received: {self.last_serial_message.decode().strip()}") + self.last_serial_message = bytes() + except serial.SerialException as e: + self.set_status(f"Serial read failed: {e:!r}") + except Exception as e: + self.set_status(f"Serial read failed: {e:!r}") + self.root.after(10, self.read_serial) + else: + self.root.after(1000, self.read_serial) + + def fetch_gqrx_freq(self): + gqrx_sock = socket.socket() + gqrx_sock.settimeout(100) + try: + gqrx_sock.connect(("127.0.0.1", 7356)) + gqrx_sock.send(b"f\n") + buf = gqrx_sock.recv(1024) + + freq = int(buf.decode()) + if self.enable_freq_sync.get(): + self.gqrx_freq_var.set(freq) + + tx_freq = freq + self.gqrx_offset_var.get() + + if self.last_tx_freq != tx_freq: + self.last_tx_freq = tx_freq + self.send_serial('f{}\n'.format(tx_freq).encode("ascii")) + + self.root.after(100, self.fetch_gqrx_freq) + except Exception as e: + self.set_status(f"GQRX socket error {e}") + self.root.after(1000, self.fetch_gqrx_freq) + + gqrx_sock.close() + + def terminate(self): + print("Terminate: send RX") + self.send_serial(b'rx\n') + + def load_macro(self, i): + self.tx_message.set(self.macro_widgets[i]["var"].get()) + + def send_serial(self, message): + if self.serial is not None: + self.serial.write(message) + + def set_font_narrow(self): + print("Set font narrow") + self.send_serial(b'fontn\n') + + def set_font_wide(self): + print("Set font wide") + self.send_serial(b'fontw\n') + + def send_tx_message(self): + msg = self.tx_message.get() + print(f"Send '{msg}'") + self.send_serial('m{}\n'.format(msg).encode("ascii")) + + def toggle_tx_enabled(self): + transmit = self.tx_enabled_var.get() + if transmit: + print("Enable TX") + self.send_serial(b'tx\n') + else: + print("Disable TX") + self.send_serial(b'rx\n') + + def create_widgets(self): + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + self.top_frame = Frame(self.root, borderwidth=5, relief="ridge", width=800, height=600) + self.top_frame.grid(column=0, row=0, sticky=(N, S, E, W)) + self.top_frame.columnconfigure(0, weight=1) + self.top_frame.columnconfigure(1, weight=1) + self.top_frame.rowconfigure(0, weight=0) + self.top_frame.rowconfigure(1, weight=1) + + self.macros_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") + self.macros_frame.grid(column=0, row=0, sticky=(N, S, E, W)) + + self.macro_widgets = [] + for i in range(NUM_MACROS): + tv = StringVar() + tv.set(DEFAULT_MACROS[i]) + e = Entry(self.macros_frame, textvariable=tv, width=80) + e.grid(column=0, row=i) + b = Button(self.macros_frame, text=f"Load {i}", command=partial(self.load_macro, i)) + b.grid(column=1, row=i) + + self.macro_widgets.append({'entry': e, 'button': b, 'var': tv}) + + + self.control_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") + self.control_frame.grid(column=1, row=0, sticky=(N, S, E, W)) + + self.font_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") + self.font_frame.grid(column=0, row=0) + self.font_label = Label(self.font_frame, text="Font selection") + self.font_label.grid(column=0, row=0) + self.button_font_narrow = Button(self.font_frame, text="Narrow", command=self.set_font_narrow) + self.button_font_narrow.grid(column=0, row=1) + self.button_font_wide = Button(self.font_frame, text="Wide", command=self.set_font_wide) + self.button_font_wide.grid(column=1, row=1) + + self.gqrx_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") + self.gqrx_frame.grid(column=0, row=1, sticky=(N, S, E, W)) + self.enable_freq_sync = IntVar() + self.gqrx_checkbutton = Checkbutton(self.gqrx_frame, text="Sync", variable=self.enable_freq_sync) + self.gqrx_checkbutton.grid(column=0, row=0, columnspan=1) + + self.gqrx_freq_label = Label(self.gqrx_frame, text="Frequency") + self.gqrx_freq_label.grid(column=0, row=1) + self.gqrx_freq_var = IntVar() + self.gqrx_freq = Label(self.gqrx_frame, width=10, textvariable=self.gqrx_freq_var) + self.gqrx_freq.grid(column=1, row=1, columnspan=1, sticky=(N, S, E, W)) + + self.gqrx_offset_label = Label(self.gqrx_frame, text="Offset") + self.gqrx_offset_label.grid(column=0, row=2) + self.gqrx_offset_var = IntVar() + self.gqrx_offset_var.set(800) + self.gqrx_offset = Entry(self.gqrx_frame, width=10, textvariable=self.gqrx_offset_var) + self.gqrx_offset.grid(column=1, row=2, columnspan=1, sticky=(N, S, E, W)) + + + self.tx_frame = Frame(self.control_frame, borderwidth=1, relief="ridge") + self.tx_frame.grid(column=0, row=2, sticky=(N, S, E, W)) + self.tx_enabled_var = IntVar() + self.tx_checkbutton = Checkbutton(self.tx_frame, text="Transmit", variable=self.tx_enabled_var, command=self.toggle_tx_enabled) + self.tx_checkbutton.grid(column=0, row=0, columnspan=2) + + self.transmit_frame = Frame(self.top_frame, borderwidth=1, relief="ridge") + self.transmit_frame.grid(column=0, row=1, columnspan=2) + self.transmit_frame.columnconfigure(0, weight=1) + self.transmit_frame.columnconfigure(1, weight=1) + + self.tx_message = StringVar() + self.tx_message_entry = Entry(self.transmit_frame, width=90, textvariable=self.tx_message) + self.tx_message_entry.grid(column=0, row=0, sticky=(N, S, E, W)) + self.tx_message_entry.columnconfigure(0, weight=1) + self.tx_message_entry.columnconfigure(1, weight=1) + self.tx_message_button = Button(self.transmit_frame, text="Send", command=self.send_tx_message) + self.tx_message_button.grid(column=0, row=1, sticky=(N, S, E, W)) + + self.status_var = StringVar() + self.status_label = Label(self.top_frame, bd=1, relief=SUNKEN, anchor=W, textvariable=self.status_var) + self.status_label.grid(column=0, row=2, columnspan=2, sticky=(N, S, E, W)) + self.set_status("") + + def set_status(self, message): + self.status_var.set(f"Status: {message}") + + +root = Tk() + +app = GUI(root) + +try: + root.mainloop() +finally: + app.terminate() + diff --git a/sw/eval-clock-cw-tx/src/feldhell_font.rs b/sw/eval-clock-cw-tx/src/feldhell_font.rs new file mode 100644 index 0000000..e4f65d2 --- /dev/null +++ b/sw/eval-clock-cw-tx/src/feldhell_font.rs @@ -0,0 +1,277 @@ +// ---------------------------------------------------------------------------- +// Copyright (C) 2014 +// David Freese, W1HKJ +// +// This file is part of fldigi +// +// fldigi is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// (at your option) any later version. +// +// fldigi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ---------------------------------------------------------------------------- + +/* Format of the arrays: + * Each line is a character, first line is space (0x20), last one is the tilda 0x7E. + * 14 lines, 8 columns: Each u8 is one line + * + * FELDHELL letters are scanned bottom-to-top, left-to-right */ + +// feld-fat font +static FONT_FELDHELL_FAT : [ [u8; 14]; 95] = [ + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0 ], + [ 0x00, 0x00, 0xD8, 0xD8, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0xFE, 0x6C, 0x6C, 0xFE, 0xFE, 0x6C, 0x6C, 0x00, 0x00 ], + [ 0x00, 0x10, 0x7E, 0xFE, 0xD0, 0xD0, 0xFC, 0x7E, 0x16, 0x16, 0xFE, 0xFC, 0x10, 0x00 ], + [ 0x00, 0xE6, 0xE6, 0xE6, 0x06, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xCE, 0xCE, 0xCE, 0x00 ], + [ 0x00, 0x18, 0x3E, 0x66, 0x60, 0x30, 0x7B, 0x7F, 0xCE, 0xC6, 0xC7, 0xFF, 0x7C, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x18, 0x38, 0x70, 0xE0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0x70, 0x38, 0x18, 0x00 ], + [ 0x00, 0xC0, 0xE0, 0x70, 0x38, 0x18, 0x18, 0x18, 0x18, 0x38, 0x70, 0xE0, 0xC0, 0x00 ], + [ 0x00, 0x00, 0x10, 0x10, 0x10, 0xFE, 0xFE, 0x38, 0x38, 0x6C, 0xC6, 0x82, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0x60, 0x60, 0xE0, 0xC0, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xE0, 0xE0, 0x00, 0x00 ], + [ 0x00, 0x0C, 0x0C, 0x1C, 0x18, 0x38, 0x30, 0x70, 0x60, 0xE0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xCE, 0xCE, 0xD6, 0xD6, 0xE6, 0xE6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x30, 0x70, 0xF0, 0xF0, 0x30, 0x30, 0x30, 0x30, 0xFC, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0x06, 0x3E, 0x7C, 0xC0, 0xC0, 0xFE, 0xFE, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0x06, 0x1C, 0x1C, 0x06, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x3C, 0x7C, 0xEC, 0xCC, 0xCC, 0xFE, 0xFE, 0x0C, 0x0C, 0x0C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFE, 0xFE, 0xC0, 0xC0, 0xFC, 0xFE, 0x06, 0x06, 0xFE, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFC, 0xC0, 0xC0, 0xFC, 0xFE, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFE, 0xFE, 0x0C, 0x0C, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0xC6, 0x7C, 0x7C, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0xC6, 0xFE, 0x7E, 0x06, 0x06, 0x7E, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x60, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x20, 0x20, 0xE0, 0xC0, 0x00 ], + [ 0x00, 0x00, 0x0C, 0x1C, 0x38, 0x70, 0xE0, 0xE0, 0x70, 0x38, 0x1C, 0x0C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x1C, 0x38, 0x70, 0xE0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x78, 0xFC, 0xCC, 0x0C, 0x38, 0x70, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00 ], + [ 0x00, 0x7C, 0xFE, 0xC6, 0xC6, 0xDE, 0xDE, 0xDC, 0xC0, 0xC0, 0x7C, 0x3C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFC, 0xFE, 0xC6, 0xC6, 0xFC, 0xFC, 0xC6, 0xC6, 0xFE, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0x3C, 0x7E, 0xE6, 0xC0, 0xC0, 0xC0, 0xC0, 0xE6, 0x7E, 0x3C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xF8, 0xFC, 0xCE, 0xC6, 0xC6, 0xC6, 0xC6, 0xCE, 0xFC, 0xF8, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFE, 0xFE, 0xC0, 0xC0, 0xF8, 0xF8, 0xC0, 0xC0, 0xFE, 0xFE, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFE, 0xFE, 0xC0, 0xC0, 0xF8, 0xF8, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x3E, 0x7E, 0xE0, 0xC0, 0xCE, 0xCE, 0xC6, 0xE6, 0x7E, 0x3C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0xF0, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0xF0, 0xF0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x0E, 0x0E, 0x06, 0x06, 0x06, 0x06, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCE, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0xFE, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7C, 0x38, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFC, 0xFE, 0xC6, 0xC6, 0xFE, 0xFC, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0xDE, 0xDE, 0xFE, 0x7C, 0x0C, 0x0C ], + [ 0x00, 0x00, 0xFC, 0xFE, 0xC6, 0xC6, 0xFE, 0xFC, 0xCC, 0xCE, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0x7E, 0xFE, 0xC0, 0xC0, 0xFC, 0x7E, 0x06, 0x06, 0xFE, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFC, 0xFC, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x6C, 0x38, 0x38, 0x10, 0x10, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0xFE, 0x6C, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC6, 0xC6, 0xEE, 0x7C, 0x38, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xFC, 0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00 ], + [ 0x00, 0x00, 0xFE, 0xFE, 0x0E, 0x0C, 0x18, 0x30, 0x60, 0xE0, 0xFE, 0xFE, 0x00, 0x00 ], + [ 0x00, 0x00, 0xF0, 0xF0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xF0, 0xF0, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xE0, 0x60, 0x70, 0x30, 0x38, 0x18, 0x1C, 0x0C, 0x0C, 0x00 ], + [ 0x00, 0x00, 0xF0, 0xF0, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xF0, 0xF0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x20, 0x20, 0x70, 0x70, 0xF8, 0xD8, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7C, 0x7E, 0x06, 0x7E, 0xFE, 0xC6, 0xFE, 0x7E, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xDC, 0xFE, 0xE6, 0xC6, 0xC6, 0xC6, 0xFE, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7C, 0xFC, 0xC0, 0xC0, 0xC0, 0xC0, 0xFC, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x06, 0x06, 0x76, 0xFE, 0xCE, 0xC6, 0xC6, 0xC6, 0xFE, 0x7E, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0xFE, 0xFE, 0xC0, 0xFC, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x3C, 0x7C, 0x60, 0x60, 0xF8, 0xF8, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7E, 0xFE, 0xC6, 0xC6, 0xC6, 0xFE, 0x7E, 0x06, 0x7E, 0x7C ], + [ 0x00, 0x00, 0xC0, 0xC0, 0xDC, 0xFE, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x60, 0x60, 0x00, 0xE0, 0xE0, 0x60, 0x60, 0x60, 0x60, 0xF0, 0xF0, 0x00, 0x00 ], + [ 0x18, 0x18, 0x00, 0x38, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0xD8, 0xF8, 0x70, 0x00 ], + [ 0x00, 0x00, 0xC0, 0xCC, 0xDC, 0xF8, 0xF0, 0xF0, 0xF8, 0xDC, 0xCC, 0xCC, 0x00, 0x00 ], + [ 0x00, 0x00, 0xE0, 0xE0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0xF0, 0xF0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFE, 0xFE, 0xD6, 0xD6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xDC, 0xFE, 0xEE, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7C, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0x7C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFE, 0xC6, 0xC6, 0xE6, 0xFE, 0xDC, 0xC0, 0xC0, 0xC0 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7E, 0xFE, 0xC6, 0xC6, 0xCE, 0xFE, 0x76, 0x06, 0x06, 0x06 ], + [ 0x00, 0x00, 0x00, 0x00, 0xDC, 0xFE, 0xE6, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x7E, 0xFE, 0xC0, 0xFC, 0x7E, 0x06, 0xFE, 0xFC, 0x00, 0x00 ], + [ 0x00, 0x00, 0x60, 0x60, 0xF0, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x78, 0x38, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xCE, 0xFE, 0x76, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xEE, 0x7C, 0x38, 0x10, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0xFE, 0x6C, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xC6, 0xEE, 0x7C, 0x38, 0x38, 0x7C, 0xEE, 0xC6, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xEE, 0x7C, 0x38, 0x30, 0x70, 0xE0, 0xC0 ], + [ 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFE, 0x0C, 0x18, 0x30, 0x60, 0xFE, 0xFE, 0x00, 0x00 ], + [ 0x00, 0x30, 0x70, 0x60, 0x60, 0x60, 0xE0, 0xE0, 0x60, 0x60, 0x70, 0x70, 0x30, 0x00 ], + [ 0x00, 0xC0, 0xC0, 0xE0, 0x60, 0x70, 0x30, 0x38, 0x18, 0x1C, 0x0C, 0x0C, 0x00, 0x00 ], + [ 0x00, 0xC0, 0xE0, 0x60, 0x60, 0x60, 0x70, 0x70, 0x60, 0x60, 0x60, 0xE0, 0xC0, 0x00 ], + [ 0x00, 0x00, 0x20, 0x70, 0xF8, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + ]; + +static FONT_FELDHELL : [ [u8; 14]; 95] = [ + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0xC0, 0xC0, 0x00 ], + [ 0x00, 0xA0, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x50, 0x50, 0xF8, 0xF8, 0x50, 0x50, 0xF8, 0xF8, 0x50, 0x50, 0x00, 0x00, 0x00 ], + [ 0x00, 0x20, 0x70, 0xF8, 0xA8, 0xA0, 0xF0, 0x78, 0x28, 0xA8, 0xF8, 0x70, 0x20, 0x00 ], + [ 0x00, 0xC8, 0xC8, 0xC8, 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x98, 0x98, 0x98, 0x00 ], + [ 0x00, 0x40, 0xE0, 0xA0, 0xA0, 0x40, 0x40, 0xA0, 0xA0, 0x90, 0x90, 0xF8, 0x78, 0x00 ], + [ 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x20, 0x60, 0xC0, 0xC0, 0x80, 0x80, 0xC0, 0xC0, 0x60, 0x20, 0x00, 0x00 ], + [ 0x00, 0x00, 0x80, 0xC0, 0x60, 0x60, 0x20, 0x20, 0x60, 0x60, 0xC0, 0x80, 0x00, 0x00 ], + [ 0x00, 0x00, 0x88, 0x88, 0x50, 0x70, 0xF8, 0xF8, 0x70, 0x50, 0x88, 0x88, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x20, 0x20, 0x20, 0xF8, 0xF8, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0x20, 0x20, 0xE0, 0xC0 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x08, 0x08, 0x18, 0x10, 0x30, 0x20, 0x60, 0x40, 0xC0, 0x80, 0x80, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x98, 0x98, 0xA8, 0xA8, 0xC8, 0xC8, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x20, 0x60, 0xE0, 0xA0, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0x60, 0xF0, 0x90, 0x10, 0x30, 0x20, 0x40, 0xC0, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x08, 0x08, 0x38, 0x38, 0x08, 0x08, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x80, 0x90, 0x90, 0x90, 0x90, 0x90, 0xF8, 0xF8, 0x10, 0x10, 0x10, 0x00, 0x00 ], + [ 0x00, 0x78, 0x78, 0x40, 0x40, 0x70, 0x78, 0x08, 0x08, 0x18, 0xF0, 0xE0, 0x00, 0x00 ], + [ 0x00, 0x20, 0x60, 0xC0, 0x80, 0x80, 0xB0, 0xF8, 0xC8, 0x88, 0x88, 0xF8, 0x70, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x18, 0x10, 0x30, 0x20, 0x60, 0x40, 0xC0, 0x80, 0x80, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x88, 0xD8, 0x70, 0x70, 0xD8, 0x88, 0x88, 0xF8, 0x70, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x88, 0xF8, 0x78, 0x08, 0x08, 0x18, 0x70, 0x60, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0xC0, 0xC0, 0xC0, 0x00, 0x00 ], + [ 0x00, 0x00, 0xC0, 0x00, 0x60, 0x60, 0x60, 0x00, 0x00, 0x60, 0x60, 0x20, 0x20, 0xE0 ], + [ 0x00, 0x00, 0x08, 0x18, 0x30, 0x60, 0xC0, 0xC0, 0x60, 0x30, 0x18, 0x08, 0x00, 0x00 ], + [ 0x00, 0x00, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00, 0xF8, 0xF8, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x00, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x08, 0x18, 0x30, 0x20, 0x20, 0x00, 0x00, 0x20, 0x20, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x88, 0xB8, 0xB8, 0xB0, 0x80, 0xC0, 0x78, 0x38, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0xD8, 0x88, 0x88, 0xF8, 0xF8, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x70, 0x70, 0x48, 0x48, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0xC8, 0x80, 0x80, 0x80, 0x80, 0xC8, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x80, 0x80, 0xE0, 0xE0, 0x80, 0x80, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x80, 0x80, 0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00 ], + [ 0x00, 0x78, 0xF8, 0xC0, 0x80, 0x98, 0x98, 0x88, 0xC8, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x88, 0x88, 0xF8, 0xF8, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0xE0, 0xE0, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x88, 0x88, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x98, 0xB0, 0xE0, 0xE0, 0xB0, 0x98, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0x82, 0xC6, 0xEE, 0xBA, 0x92, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0xC8, 0xC8, 0xE8, 0xA8, 0xB8, 0x98, 0x98, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x78, 0x70, 0x40, 0x40, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0xD8, 0x88, 0x88, 0xA8, 0xA8, 0xB8, 0xF0, 0x70, 0x18, 0x08, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x88, 0x88, 0xF8, 0xF0, 0x90, 0x98, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x78, 0xF8, 0x80, 0x80, 0xF0, 0x78, 0x08, 0x08, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x98, 0x90, 0xB0, 0xA0, 0xE0, 0xC0, 0xC0, 0x80, 0x00, 0x00, 0x00 ], + [ 0x00, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0xBA, 0xEE, 0x44, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0xD8, 0x70, 0x20, 0x20, 0x70, 0xD8, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0xD8, 0x50, 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x18, 0x10, 0x30, 0x60, 0x48, 0xC8, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x80, 0x80, 0xC0, 0x40, 0x60, 0x20, 0x30, 0x10, 0x18, 0x08, 0x08, 0x00, 0x00 ], + [ 0x00, 0xE0, 0xE0, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x20, 0x20, 0x70, 0x50, 0xD8, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + [ 0x00, 0x20, 0x70, 0xD8, 0x88, 0x88, 0xF8, 0xF8, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x78, 0x78, 0x48, 0x48, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0xC8, 0x80, 0x80, 0x80, 0x80, 0xC8, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x80, 0x80, 0xF8, 0xF8, 0x80, 0x80, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x80, 0x80, 0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00 ], + [ 0x00, 0x78, 0xF8, 0xC0, 0x80, 0x98, 0x98, 0x88, 0xC8, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x88, 0x88, 0xF8, 0xF8, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0xE0, 0xE0, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x88, 0x88, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x98, 0x98, 0xB0, 0xE0, 0xE0, 0xB0, 0x98, 0x98, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0x82, 0xC6, 0xEE, 0xBA, 0x92, 0x82, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0xC8, 0xC8, 0xE8, 0xA8, 0xB8, 0x98, 0x98, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x48, 0x48, 0x78, 0x70, 0x40, 0x40, 0xE0, 0xE0, 0x00, 0x00, 0x00 ], + [ 0x00, 0x70, 0xF8, 0xD8, 0x88, 0x88, 0xA8, 0xA8, 0xB8, 0xF0, 0x70, 0x18, 0x08, 0x00 ], + [ 0x00, 0xF0, 0xF8, 0x88, 0x88, 0xF8, 0xF0, 0x90, 0x98, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x78, 0xF8, 0x80, 0x80, 0xF0, 0x78, 0x08, 0x08, 0xF8, 0xF0, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0xF8, 0x70, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0x98, 0x90, 0xB0, 0xA0, 0xE0, 0xC0, 0xC0, 0x80, 0x00, 0x00, 0x00 ], + [ 0x00, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0x92, 0xBA, 0xEE, 0x44, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0xD8, 0x70, 0x20, 0x20, 0x70, 0xD8, 0x88, 0x88, 0x00, 0x00, 0x00 ], + [ 0x00, 0x88, 0x88, 0xD8, 0x50, 0x70, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00 ], + [ 0x00, 0xF8, 0xF8, 0x18, 0x10, 0x30, 0x60, 0x48, 0xC8, 0xF8, 0xF8, 0x00, 0x00, 0x00 ], + [ 0x00, 0x20, 0x60, 0x40, 0x40, 0x40, 0xC0, 0xC0, 0x40, 0x40, 0x40, 0x60, 0x20, 0x00 ], + [ 0x00, 0x80, 0x80, 0xC0, 0x40, 0x60, 0x20, 0x30, 0x10, 0x18, 0x08, 0x08, 0x00, 0x00 ], + [ 0x00, 0x80, 0xC0, 0x40, 0x40, 0x40, 0x60, 0x60, 0x40, 0x40, 0x40, 0xC0, 0x80, 0x00 ], + [ 0x00, 0x20, 0x70, 0xD8, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ], + ]; + +fn get_letter(letter : u8, fat : bool) -> Option<&'static [u8; 14]> { + if letter >= b' ' && letter <= 0x7e { + let offset = letter - b' '; + + Some(if fat { + &FONT_FELDHELL_FAT[offset as usize] + } + else { + &FONT_FELDHELL[offset as usize] + }) + } + else { + None + } +} + +/* Returns the columns for the given letter. + * Bit 14 of each column says if the column needs to be transmitted or not. + * The rows are in bits 13 down to 0. + */ +pub fn get_letter_by_columns(letter : u8, fat : bool) -> Option<[u16; 8]> { + if let Some(lines) = get_letter(letter, fat) { + let mut pattern = [0u16; 8]; + + // Count number of effective columns + let effective_columns = if letter == b' ' { + 5 + } + else { + 9 - lines.iter() + .fold(0x00, |acc, x| acc | x ) + .trailing_zeros() + } as usize; + + assert!(effective_columns <= 8); + + // lines is 14x u8, each u8 is a line + // pattern is 8x u16, each u16 is a column, where bits 13..0 correspond to the 14 columns, + // and bit 14 is always 1 to mark the end + for j in 0..effective_columns { + for i in 0..14 { + if (lines[13-i] >> (7-j)) & 0x1 != 0 { + pattern[j] |= 1 << i; + } + } + pattern[j] |= 1 << 14; + } + + Some(pattern) + } + else { + None + } +} diff --git a/sw/eval-clock-cw-tx/src/main.rs b/sw/eval-clock-cw-tx/src/main.rs index a548f81..0d70630 100644 --- a/sw/eval-clock-cw-tx/src/main.rs +++ b/sw/eval-clock-cw-tx/src/main.rs @@ -47,10 +47,10 @@ use hd44780_driver::{Cursor, CursorBlink, Display, DisplayMode, HD44780}; pub mod ui; pub mod usb; +pub mod feldhell_font; pub mod cw; pub mod state; pub mod si_clock; -pub mod log10f; use state::*; @@ -59,6 +59,7 @@ const TICKS_PER_SECOND : u32 = 100; struct SharedWithISR { state : State, last_sequence_state_change : u32, + feldhell_ptt : bool, cw_ptt_timestamp : u32, cw_key_out_n : gpio::gpioa::PA15>, ui : ui::UI, @@ -78,12 +79,6 @@ fn _ticks_now() -> u32 { cortex_m::interrupt::free(|_cs| unsafe { *TICK_COUNTER.as_ptr() }) } -fn get_state_copy() -> State { - cortex_m::interrupt::free(|_cs| unsafe { - (*SHARED.as_ptr()).state.clone() - }) -} - #[cortex_m_rt::entry] fn main() -> ! { let cp = cortex_m::Peripherals::take().unwrap(); @@ -109,9 +104,14 @@ fn main() -> ! { let mut gpiob = dp.GPIOB.split(&mut rcc.apb2); let mut gpioc = dp.GPIOC.split(&mut rcc.apb2); + let mut timer4 = Timer::tim4(dp.TIM4, &clocks, &mut rcc.apb1) + .start_count_down(usb::TIMER_FREQ_HZ.hz()); + timer4.listen(Event::Update); + let usb_dm = gpioa.pa11; let usb_dp = gpioa.pa12.into_floating_input(&mut gpioa.crh); - let usb = usb::USB::new(dp.USB, usb_dm, usb_dp); + let mut usb = usb::USBData::new(timer4, dp.USB, usb_dm, usb_dp); + usb::enable_interrupts(); // Buttons as analog inputs (multi-level) let pb0 = gpiob.pb0.into_floating_input(&mut gpiob.crl); // BTN1 Button B, has external pullup @@ -184,13 +184,12 @@ fn main() -> ! { lcd.write_str(" 30m CW TX 2021 ", &mut delay).unwrap(); delay.delay_ms(1_500u16); - usb.enable_interrupts(); - let mut siclock = { let shared = unsafe { &mut *SHARED.as_mut_ptr() }; *shared = SharedWithISR { state : State::new(), last_sequence_state_change : 0, + feldhell_ptt : false, cw_ptt_timestamp : 0, cw_key_out_n, ui, @@ -202,7 +201,7 @@ fn main() -> ! { si_clock::SiClock::new(i2c_busmanager.acquire_i2c(), 0, shared.state.vfo_display()) }; - ui::update_disp(&mut lcd, &get_state_copy(), &mut delay); + ui::update_disp(&mut lcd, unsafe { &(*SHARED.as_ptr()).state }, &mut delay); let mut last_encoder_count = qei.count(); @@ -221,13 +220,31 @@ fn main() -> ! { unsafe { pac::NVIC::unmask(pac::Interrupt::TIM2); } let mut last_disp_update_counter = 1; + let mut previous_usb_freq = usb.frequency; let mut previous_vfo = 0; let mut previous_state = SequenceState::Rx; loop { let mut update_disp_required = false; - let state = get_state_copy(); + usb.handle(); + + if previous_usb_freq != usb.frequency { + previous_usb_freq = usb.frequency; + + cortex_m::interrupt::free(|_cs| { + let shared = unsafe { &mut *SHARED.as_mut_ptr() }; + shared.state.set_vfo(usb.frequency); + }); + + update_disp_required = true; + } + + let state = cortex_m::interrupt::free(|_cs| unsafe { + let shared = SHARED.as_mut_ptr(); + (*shared).feldhell_ptt = usb.is_transmit(); + (*shared).state.clone() + }); let encoder_count : u16 = qei.count(); if encoder_count != last_encoder_count { @@ -267,6 +284,7 @@ fn main() -> ! { } } +#[allow(non_snake_case)] #[interrupt] fn TIM2() { let timer = unsafe { &mut *CLOCK_TIMER.as_mut_ptr() }; @@ -286,7 +304,7 @@ fn TIM2() { let cw_paddle_ring_low = shared.cw_paddle_ring.is_low().unwrap(); let cw_ptt_delay : u32 = TICKS_PER_SECOND * 800 / 1000; - let cw_ptt = match shared.state.mode { + let ptt = match shared.state.mode { Mode::CW(_) => { if cw_paddle_tip_low || cw_paddle_ring_low { shared.cw_ptt_timestamp = *ticks; @@ -295,49 +313,58 @@ fn TIM2() { else { shared.cw_ptt_timestamp + cw_ptt_delay > *ticks } + }, + Mode::FeldHell => { + shared.feldhell_ptt } }; let cw_beep = match shared.state.mode { Mode::CW(CWMode::StraightKey) => cw_paddle_tip_low, Mode::CW(CWMode::Iambic) => shared.cw_keyer.tick(*ticks, cw_paddle_tip_low, cw_paddle_ring_low), + Mode::FeldHell => false, // Done in usb.c }; let next_state = match shared.state.sequence_state { SequenceState::Rx => { shared.ptt_out.set_low().unwrap(); shared.led.set_high().unwrap(); - if cw_ptt { - SequenceState::SwitchingCW + if ptt { + if shared.state.mode == Mode::FeldHell { + SequenceState::Switching(SequenceMode::FeldHell) + } + else { + SequenceState::Switching(SequenceMode::CW) + } } else { SequenceState::Rx } }, - SequenceState::SwitchingCW => { + SequenceState::Switching(m) => { shared.ptt_out.set_high().unwrap(); shared.led.set_low().unwrap(); - if cw_ptt { - SequenceState::TxCW + if ptt { + SequenceState::Tx(m) } else { SequenceState::Rx } }, - SequenceState::TxCW => { + SequenceState::Tx(m) => { shared.ptt_out.set_high().unwrap(); shared.led.set_low().unwrap(); - if cw_ptt { - SequenceState::TxCW + if ptt { + SequenceState::Tx(m) } else { - SequenceState::SwitchingCW + SequenceState::Switching(m) } }, }; match shared.state.sequence_state { - SequenceState::TxCW => { + SequenceState::Tx(SequenceMode::CW) => { if cw_beep { shared.cw_pwm.on(); shared.cw_key_out_n.set_low().unwrap(); @@ -347,6 +374,15 @@ fn TIM2() { shared.cw_key_out_n.set_high().unwrap(); } }, + SequenceState::Tx(SequenceMode::FeldHell) => { + // cw_key_out_n is handled by TIM4 ISR in usb.rs + if cw_beep { + shared.cw_pwm.on(); + } + else { + shared.cw_pwm.off(); + } + }, _ => { shared.cw_pwm.off(); shared.cw_key_out_n.set_high().unwrap(); diff --git a/sw/eval-clock-cw-tx/src/state.rs b/sw/eval-clock-cw-tx/src/state.rs index a5891c5..cd89ede 100644 --- a/sw/eval-clock-cw-tx/src/state.rs +++ b/sw/eval-clock-cw-tx/src/state.rs @@ -29,13 +29,20 @@ pub enum CWMode { #[derive(PartialEq, Eq, Clone, Copy)] pub enum Mode { CW(CWMode), + FeldHell, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum SequenceMode { + CW, + FeldHell, } #[derive(Clone, PartialEq, Eq)] pub enum SequenceState { Rx, - SwitchingCW, - TxCW, + Switching(SequenceMode), + Tx(SequenceMode), } #[derive(Clone)] @@ -55,7 +62,8 @@ impl State { pub fn new() -> Self { State { ui_sel : UISelection::VFO, - mode : Mode::CW(CWMode::StraightKey), + //mode : Mode::CW(CWMode::StraightKey), + mode : Mode::FeldHell, vfo_sel : VFOSelection::A, vfo_a : INITIAL_VFO, vfo_b : INITIAL_VFO, @@ -66,6 +74,13 @@ impl State { } } + pub fn set_vfo(&mut self, freq : u32) { + match self.vfo_sel { + VFOSelection::A => self.vfo_a = freq, + VFOSelection::B => self.vfo_b = freq, + } + } + pub fn vfo_display(&self) -> u32 { match self.vfo_sel { VFOSelection::A => self.vfo_a, @@ -90,4 +105,8 @@ impl State { TuneSpeed::Fast => 1000, } } + + pub fn allow_feldhell_keying(&self) -> bool { + self.sequence_state == SequenceState::Tx(SequenceMode::FeldHell) + } } diff --git a/sw/eval-clock-cw-tx/src/ui.rs b/sw/eval-clock-cw-tx/src/ui.rs index 3cc8c90..38355e5 100644 --- a/sw/eval-clock-cw-tx/src/ui.rs +++ b/sw/eval-clock-cw-tx/src/ui.rs @@ -170,7 +170,8 @@ impl UI { if button_updates.c { let (new_ui_sel, new_filter_shift) = match (state.ui_sel, state.mode) { (UISelection::Mode, Mode::CW(CWMode::StraightKey)) => (UISelection::Mode, Mode::CW(CWMode::Iambic)), - (UISelection::Mode, Mode::CW(CWMode::Iambic)) => (UISelection::Mode, Mode::CW(CWMode::StraightKey)), + (UISelection::Mode, Mode::CW(CWMode::Iambic)) => (UISelection::Mode, Mode::FeldHell), + (UISelection::Mode, Mode::FeldHell) => (UISelection::Mode, Mode::CW(CWMode::StraightKey)), (_, f) => (UISelection::Mode, f), }; @@ -203,7 +204,6 @@ impl UI { result } - // Returns true if bfo must be reprogrammed pub fn update_encoder(&mut self, state: &mut State, delta : i32) { match state.ui_sel { UISelection::VFO => { @@ -259,6 +259,7 @@ pub fn update_disp(lcd: &mut HD44780, state: let mode = match state.mode { Mode::CW(CWMode::StraightKey) => "CWs", Mode::CW(CWMode::Iambic) => "CWp", + Mode::FeldHell => "HEL", }; let speed = match state.tune_speed { diff --git a/sw/eval-clock-cw-tx/src/usb.rs b/sw/eval-clock-cw-tx/src/usb.rs index e803c91..467c148 100644 --- a/sw/eval-clock-cw-tx/src/usb.rs +++ b/sw/eval-clock-cw-tx/src/usb.rs @@ -1,23 +1,96 @@ +use core::mem::MaybeUninit; use core::fmt::Write; -use stm32f1xx_hal::gpio; -use stm32f1xx_hal::pac; -use stm32f1xx_hal::pac::{interrupt, Interrupt}; +use stm32f1xx_hal::{ + pac, + pac::{interrupt, Interrupt}, + gpio, + timer::CountDownTimer, +}; use stm32f1xx_hal::usb::{Peripheral, UsbBus, UsbBusType}; use usb_device::{bus::UsbBusAllocator, prelude::*}; use usbd_serial::{SerialPort, USB_CLASS_CDC}; +use arrayvec::{ArrayVec, ArrayString}; + +/* Transmit 17.5 columns per second + * With 14 lines per column, this gives 245 lines per second + */ +pub const TIMER_FREQ_HZ : u32 = 245; + +const FELDHELL_PATTERN_LEN : usize = 16; +const MESSAGE_LEN : usize = 64; + +static mut FELDHELL_TIMER: MaybeUninit> = MaybeUninit::uninit(); + +static mut SHARED_USB: MaybeUninit = MaybeUninit::uninit(); static mut USB_BUS: Option> = None; static mut USB_SERIAL: Option> = None; static mut USB_DEVICE: Option> = None; -pub struct USB { + +/* Commands + * ======== + * Commands end with \n + * + * Switch to transmit: + * tx + * + * Switch to receive: + * rx + * + * Set frequency: + * f10121000 + * + * Append to transmit message + * mThe message to transmit + * + * Clear transmit message + * clear + * + * Set FELDHELL wide font + * fontw + * + * Set FELDHELL narrow font + * fontn +*/ + + +#[derive(Clone, Copy)] +enum Font { + Narrow, + Wide, } -impl USB { +// Shared between application and interrupt context +struct USBSharedData { + incoming_buf: ArrayVec::<[u8; MESSAGE_LEN]>, + message : ArrayString::<[u8; MESSAGE_LEN]>, + feldhell_pattern: ArrayVec::<[u16; FELDHELL_PATTERN_LEN]>, // FELDHELL columns to transmit +} + +pub fn enable_interrupts() { + unsafe { + pac::NVIC::unmask(Interrupt::USB_HP_CAN_TX); + pac::NVIC::unmask(Interrupt::USB_LP_CAN_RX0); + pac::NVIC::unmask(Interrupt::TIM4); + } +} + +pub struct USBData { + font : Font, + pub frequency : u32, + pub transmit : bool, + + current_str : ArrayVec::<[u8; 96]>, + send_ix : usize, +} + +impl USBData { pub fn new( - usb: stm32f1xx_hal::pac::USB, + timer4: CountDownTimer, + usb: pac::USB, pin_dm: gpio::gpioa::PA11>, - pin_dp: gpio::gpioa::PA12>) -> Self { + pin_dp: gpio::gpioa::PA12>) -> USBData { let peripheral = Peripheral { usb, @@ -25,32 +98,161 @@ impl USB { pin_dp, }; - unsafe { - let bus = UsbBus::new(peripheral); - - USB_BUS = Some(bus); + { + let timer = unsafe { &mut *FELDHELL_TIMER.as_mut_ptr() }; + *timer = timer4; + } + unsafe { + USB_BUS = Some(UsbBus::new(peripheral)); USB_SERIAL = Some(SerialPort::new(USB_BUS.as_ref().unwrap())); - - let usb_dev = UsbDeviceBuilder::new(USB_BUS.as_ref().unwrap(), UsbVidPid(0x1d50, 0x5120)) // Openmoko Neo1973 serial + let usb_device = UsbDeviceBuilder::new(USB_BUS.as_ref().unwrap(), UsbVidPid(0x1d50, 0x5120)) // Openmoko Neo1973 serial .manufacturer("HB9EGM") .product("Beep Machine") .serial_number("1") .device_class(USB_CLASS_CDC) .build(); + USB_DEVICE = Some(usb_device); - USB_DEVICE = Some(usb_dev); + let shared_usb = &mut *SHARED_USB.as_mut_ptr(); + *shared_usb = USBSharedData { + incoming_buf : ArrayVec::new(), + message : ArrayString::new(), + feldhell_pattern : ArrayVec::new(), + } } - USB{} + USBData { + font : Font::Narrow, + frequency : 10121000, + transmit : false, + current_str : ArrayVec::new(), + send_ix : 0, + } } - pub fn enable_interrupts(&self) { - unsafe { - pac::NVIC::unmask(Interrupt::USB_HP_CAN_TX); - pac::NVIC::unmask(Interrupt::USB_LP_CAN_RX0); + pub fn handle(&mut self) { + let in_message = cortex_m::interrupt::free(|_cs| { + let shared_usb = unsafe { &mut *SHARED_USB.as_mut_ptr() }; + + if shared_usb.message.len() > 0 { + let mut outgoing = ArrayString::<[u8; MESSAGE_LEN]>::new(); + + if let Err(_) = write!(outgoing, "{}", &shared_usb.message) { + outgoing.clear(); + write!(outgoing, "EXT FAIL\n").ok(); + } + let serial = unsafe { USB_SERIAL.as_mut().unwrap() }; + serial.write(outgoing.as_bytes()).ok(); + + let in_message = shared_usb.message.clone(); + shared_usb.message.clear(); + Some(in_message) + } + else { + None + } + }); + + let mut outgoing = ArrayVec::<[u8; MESSAGE_LEN]>::new(); + if let Some(m) = in_message { + if m.as_str() == "tx\n" { + outgoing.try_extend_from_slice(b"TX ok\n").ok(); + self.transmit = true; + } + else if m.as_str() == "rx\n" { + outgoing.try_extend_from_slice(b"RX ok\n").ok(); + self.transmit = false; + } + else if m.chars().nth(0).unwrap() == 'm' { + self.current_str.clear(); + for c in m.bytes().skip(1) { + self.current_str.push(c); + } + outgoing.try_extend_from_slice(b"m ok\n").ok(); + } + else if m.as_str() == "clear\n" { + self.current_str.clear(); + outgoing.try_extend_from_slice(b"clear ok\n").ok(); + } + else if m.as_str() == "fontw\n" { + self.font = Font::Wide; + outgoing.try_extend_from_slice(b"fontw ok\n").ok(); + } + else if m.as_str() == "fontn\n" { + self.font = Font::Narrow; + outgoing.try_extend_from_slice(b"fontn ok\n").ok(); + } + else if m.chars().nth(0).unwrap() == 'f' { + let mut as_str = ArrayString::<[u8; MESSAGE_LEN]>::new(); + + for c in m.chars().skip(1) { + as_str.push(c); + } + + match u32::from_str_radix(as_str.as_str().trim(), 10) { + Ok(val) => { + self.frequency = val; + outgoing.try_extend_from_slice(b"f ok\n").ok(); + } + _ => { + outgoing.try_extend_from_slice(b"F").ok(); + outgoing.try_extend_from_slice(as_str.as_bytes()).ok(); + outgoing.try_extend_from_slice(b" err\n").ok(); + } + } + } + else { + outgoing.try_extend_from_slice(b"no\n").ok(); + } + } + + let feldhell_pattern_length = cortex_m::interrupt::free(|_cs| { + let serial = unsafe { USB_SERIAL.as_mut().unwrap() }; + serial.write(&outgoing).ok(); + + let shared_usb = unsafe { &mut *SHARED_USB.as_mut_ptr() }; + shared_usb.feldhell_pattern.len() + }); + + if self.current_str.len() > 0 && feldhell_pattern_length < FELDHELL_PATTERN_LEN-8 { + match self.current_str.get(self.send_ix) { + Some(letter) => { + let fat = match self.font { + Font::Wide => true, + Font::Narrow => false, + }; + + match crate::feldhell_font::get_letter_by_columns(*letter, fat) { + Some(columns) => { + cortex_m::interrupt::free(|_cs| { + let shared_usb = unsafe { &mut *SHARED_USB.as_mut_ptr() }; + shared_usb.feldhell_pattern.try_extend_from_slice(&columns).ok(); + }); + + } + None => {} + } + } + None => {} + } + + self.send_ix += 1; + if self.send_ix >= self.current_str.len() { + cortex_m::interrupt::free(|_cs| { + let serial = unsafe { USB_SERIAL.as_mut().unwrap() }; + let mut outgoing = ArrayString::<[u8; 16]>::new(); + write!(&mut outgoing, "FH sent {}\n", self.send_ix).ok(); + serial.write(outgoing.as_bytes()).ok(); + }); + + self.send_ix = 0; + self.current_str.clear(); + } } } + + pub fn is_transmit(&self) -> bool { self.transmit } } #[allow(non_snake_case)] @@ -73,18 +275,82 @@ fn usb_interrupt() { return; } - let mut buf = [0u8; 8]; - let mut string = arrayvec::ArrayString::<[_; 16]>::new(); + let mut buf = [0u8; 64]; + + let shared_usb = unsafe { &mut *SHARED_USB.as_mut_ptr() }; match serial.read(&mut buf) { Ok(count) if count > 0 => { - // Echo back in upper case - for c in buf[0..count].iter_mut() { - write!(string, "{:02x}", *c).ok(); + if count > 64 { + panic!("illegal count"); + } + shared_usb.incoming_buf.try_extend_from_slice(&buf[0..count]).ok(); + + let mut split_at = None; + for (i, c) in shared_usb.incoming_buf.iter().enumerate() { + if *c == b'\n' { + split_at = Some(i); + break; + } } - serial.write(string.as_bytes()).ok(); - string.clear(); + + match split_at { + Some(i) if i+1 == shared_usb.incoming_buf.len() => { + write!(&mut shared_usb.message, "{}", core::str::from_utf8(&shared_usb.incoming_buf).unwrap()).ok(); + shared_usb.incoming_buf.clear(); + } + Some(i) => { + let (front, back) = shared_usb.incoming_buf.split_at(i+1); + + write!(&mut shared_usb.message, "{}", core::str::from_utf8(front).unwrap()).ok(); + + let mut keep = ArrayVec::<[u8; MESSAGE_LEN]>::new(); + keep.try_extend_from_slice(back).ok(); + + shared_usb.incoming_buf.clear(); + shared_usb.incoming_buf.try_extend_from_slice(&keep).ok(); + } + _ => { } + }; + } _ => {} } } + +#[allow(non_snake_case)] +#[interrupt] +fn TIM4() { + let timer = unsafe { &mut *FELDHELL_TIMER.as_mut_ptr() }; + timer.clear_update_interrupt_flag(); + + let shared_usb = unsafe { &mut *SHARED_USB.as_mut_ptr() }; + while shared_usb.feldhell_pattern.len() > 0 && shared_usb.feldhell_pattern.as_slice()[0] == 0 { + shared_usb.feldhell_pattern.pop_at(0); + } + + if shared_usb.feldhell_pattern.len() > 0 { + let remove_first_element = { + let pat = shared_usb.feldhell_pattern.as_mut_slice().first_mut().unwrap(); + + let shared = unsafe { &mut *crate::SHARED.as_mut_ptr() }; + + use embedded_hal::digital::v2::OutputPin; + + if shared.state.allow_feldhell_keying() && (*pat & 0b1) == 0b1 { + shared.cw_key_out_n.set_low().unwrap(); + } + else { + shared.cw_key_out_n.set_high().unwrap(); + } + + *pat >>= 1; + assert_ne!(*pat, 0); + *pat == 0b1 + }; + + if remove_first_element { + shared_usb.feldhell_pattern.pop_at(0); + } + } +} diff --git a/sw/picardy/src/main.rs b/sw/picardy/src/main.rs index efdc59d..3a2e500 100644 --- a/sw/picardy/src/main.rs +++ b/sw/picardy/src/main.rs @@ -97,6 +97,7 @@ fn main() -> ! { let mut rcc = dp.RCC.constrain(); let mut afio = dp.AFIO.constrain(&mut rcc.apb2); let clocks = rcc.cfgr + // TODO .use_hse(8.mhz()) .adcclk(2.mhz()) .freeze(&mut flash.acr); let mut delay = Delay::new(cp.SYST, clocks); -- cgit v1.2.3