Techno Keyer — Bringing Retro Displays and Morse Code Together
Designing and building Techno Keyer, an automatic Morse-code encoder and decoder

AI-translated from the Chinese original View original

Since getting into amateur radio in 2023, I have tried various parts of the hobby and earned my Class A and Class B operator certificates. CW was naturally on the list. I bought a dual-lever paddle and spent some time practicing sending and copying Morse code. As a programmer with a fondness for automation, I soon wanted to automate the repetitive work of sending it.

A basic transmitter keyer is essentially an automated switch, so the idea sounded manageable. Around the same time, I acquired a BOE vacuum fluorescent display (VFD) module to experiment with. Its distinctive glow, familiar from electronics of the 1980s and 1990s, suggested a small device with a retro-technology feel. I called it Techno Keyer. The name seemed to suit the era, even though I had yet to be born back then :-)

TL;DR

This article covers the design and implementation of Techno Keyer. The firmware and circuit schematics are available on GitHub for anyone interested in the project.

Transmit Mode

The initial goal was straightforward. Read characters from a USB keyboard, encode them as Morse code, and open and close the radio’s keying circuit to send them automatically.

Keyboard Input

Reading a USB keyboard was the first requirement for an automatic keyer, and it became my first obstacle. I initially assumed that a USB-capable MCU would handle it. I built a breadboard prototype around an ESP32-S3 development board and tried the EspUsbHost project from GitHub. An older Logitech membrane keyboard worked as expected.

Reference project EspUsbHost

A spare mechanical keyboard gave a different result. It would fail to initialize properly, so there was no usable key input. Debug output showed the initial connection being detected, followed by failures and repeated restarts. Comparing my results with issues in similar projects pointed to keyboard compatibility in the USB-host setup I was using.

The ESP32-S3 USB-OTG host supports Low Speed and Full Speed devices. I treated the failure as a compatibility issue in my setup and looked for a practical way to handle the keyboards I wanted to use. Espressif USB Host documentation

Following approaches used in other community projects, I tried a CH9350L USB keyboard-and-mouse to serial controller, a chip also used in KVM devices. I bought a development module on Taobao and connected it to the prototype. Keyboard input then worked, although the extra chip raised the cost of the design. Other builders have also used USB-to-PS/2 keyboard connections for similar projects.

CH9350L module

Keying the Radio

Once keyboard input worked, the next step was to make the CW output behave like a conventional key. I wanted broad compatibility and a simple control circuit.

A key is a switch that opens and closes a circuit. On the usual 3.5 mm key connector, the sleeve is ground. With a paddle, the other two contacts represent the dit and dah inputs. With a straight key, the tip provides the keying contact.

Key connector pinout

M0UKD’s simple touch-key circuit uses an NPN transistor as the switching element and was reported to work with several Icom radios. A MOSFET could serve a similar purpose.

Simple transistor keying circuit

A semiconductor switch can respond quickly, but directly connecting the circuits raised a few concerns. Key-input voltages vary between radio designs. The Icom interface I considered used 3.3 V, while other equipment, especially older radios, could expose the circuit to higher voltages. Techno Keyer also has its own USB supply, so connecting the grounds directly could create a ground loop between devices.

I chose an optically isolated solid-state relay as the external switching element. It separates the keyer’s circuit from the radio’s keying circuit. An indicator LED and a buzzer provide visual and audible feedback. The resulting circuit is shown below.

Transmit output circuit

Encoding and Sending

With the hardware approach established, I needed to translate keyboard characters into Morse code. Morse predates ASCII, and its letter and number patterns do not follow a convenient numeric sequence. I used a lookup table to keep the implementation easy to extend.

I represented dits as 0 and dahs as 1. Since a pattern may begin with zero, I stored each pattern in reverse bit order in an eight-bit value, with a leading 1 as an end marker. For example, A is .-, represented as 0b110.

// Morse code table
// 0 -> Dit, 1 -> Dah, Reverse order, Highest bit is 1
const struct morseChar morseTable[] = {
        {'A', 0b110},       // .-
        {'B', 0b10001},     // -...
        {'C', 0b10101},     // -.-.
}

Another approach used in related projects is a binary tree whose branches represent dits and dahs. Traversing the tree locates a character. It offers a compact representation, but I found the table easier to extend for my needs.

A compact tree can be stored as a “magic string”, with each character’s position representing its Morse pattern.

**ETIANMSURWDKGOHVF*L*PJBXCYZQ**54S3***2**+***J16=/***H*7*G*8*90************?_****\"**.****@***'**-********;!*)*****,****:****

Sending a character then becomes a table lookup followed by a series of bit shifts. Each bit determines which signal to send until the end marker is reached.

/**
 * Send one char
 * @param c
 */
void MorseEncoder::sendChar(char c) {
    if (c == ' ') {
        vTaskDelay(7 * _dotTimeMs / portTICK_PERIOD_MS);
    } else {
        uint8_t code = _codec->getCode(c);
        while (code != 1) {
            if (code & 1) {
                _sendSignal(DAH);
            } else {
                _sendSignal(DIT);
            }
            code = code >> 1;
        }
        vTaskDelay(2 * _dotTimeMs / portTICK_PERIOD_MS);
    }
}

/**
 * Send morse signal
 * @param sig
 */
void MorseEncoder::_sendSignal(uint8_t sig) {
    uint16_t sigTimeMs = sig == DIT ?
            _dotTimeMs : _dotTimeMs * 3;

    digitalWrite(CW_PIN, HIGH);
    _buzzer -> on();
    vTaskDelay(sigTimeMs / portTICK_PERIOD_MS);

    digitalWrite(CW_PIN, LOW);
    _buzzer -> off();
    vTaskDelay(_dotTimeMs / portTICK_PERIOD_MS);
}

I assembled a simple breadboard prototype to test the result.

Breadboard prototype

Receive Mode

After implementing the basic transmitter, I decided to add decoding as well.

Audio Processing

Receiving is more involved than switching a keying circuit. The input is analog audio, with interference and background noise from the radio path. Reliable decoding benefits from conditioning the signal to extract the frequency range of interest. A typical processing chain includes noise reduction, filtering, gain, decoding, and debouncing. The radio can handle the first three stages, leaving Techno Keyer to focus on decoding.

Audio signal conditioning

CW audio is commonly set to a tone between 500 and 1000 Hz, which can be isolated with a relatively narrow filter. I used an LM567 tone decoder to detect the selected frequency, setting its operating range through the surrounding circuit. An SPI-controlled digital potentiometer lets the MCU adjust the center frequency from the menu.

Audio decoding circuit

Decoding the Signal

Morse timing is based on the length of a dit. A dah lasts three units. The gap between elements is one unit, the gap between characters is three units, and the gap between words is seven units.

Morse-code timing

Human sending is less precise than computer-generated timing. Even the same operator’s elements vary in length. The decoder therefore needs to distinguish short and long elements despite those variations. I mainly followed K4ICY’s implementation, updating the classification threshold as signals arrive and using the geometric mean of the estimated dit and dah durations as a reference.

Reference project K4ICY CW Decoder

Classifying Morse elements and gaps

Updating the classifications dynamically lets the decoder adapt fairly quickly to different sending speeds. I generated test audio with different simulated operator skill levels. With a clear audio input and reasonably consistent sending, the results were generally good. Machine decoding also handled fast signals at 40 WPM.

Assembly and Bring-up

Controlling the VFD

The main challenge with the VFD was finding documentation. The Taobao seller supplied limited information and rudimentary example code. I eventually found the relevant manual and used its command descriptions to implement the display controls over SPI.

VFD commands

Circuit Design

I combined the hardware into a two-layer PCB. Using a ready-made ESP32 module kept fabrication costs down and saved me from designing an antenna or moving to a four-layer board. The trade-off was limited space. I also placed all surface-mount components on one side to simplify assembly, making the component side fairly dense.

For the rotary encoder, I deliberately chose a right-angle version that could be operated from the edge of the PCB. It avoided an extra support structure or separate board for a conventionally mounted encoder.

PCB layout
PCB components

Enclosure Design

Once the core functions worked, I wanted a proper enclosure. While looking at other VFD projects, I found a triangular-prism clock whose appearance appealed to me.

VFD clock used as a design reference

I designed a similar triangular-prism enclosure around the dimensions of my VFD module. The central support and side covers were CNC-machined from aluminum alloy for a metal finish, while the front and rear panels were 3D-printed.

Exploded assembly view

Clear acrylic would have been my preferred material for the front panel. A dark-tinted translucent sheet could also have given the display a more polished appearance. I had yet to find a suitable supplier for such a small custom part, so I used transparent 3D-printing material for the time being.

I was pleased with the finished device. Twenty-first-century integrated circuits drive a twentieth-century vacuum fluorescent display to send a nineteenth-century communication code. Combining those technologies gave the project the retro character I had hoped for.

Completed Techno Keyer

This is BD4WEI, 73


Last modified on 2024-10-05