自从 2023 年开始玩起了业余无线电 (Amature Radio) 以后,Ham 圈各种好玩的都尝试碰了下,也陆续拿到了 A/B 类操作证。最为经典的等幅电报 (CW) 自然也没有落下,也尝试自己买了双桨电键联系了一段时间摩尔斯码拍发与听抄。不过身为爱偷懒的程序员,拍电报这种机械式的重复操作,自然会想着搞个自动化的方案来解决。
毕竟仅仅一个电报发报装置,理论上只是一个自动化的开关,想象中实现并不复杂。正好手头拿到了一块京东方的 VFD (真空荧光显示) 模块来玩玩,于是就有了一个想法,希望配合上这样曾经流行于上世纪 80~90 年代,独具特色的荧光显示屏,做一个带有复古科技味道的小玩具。想了下名字,就叫做 Techno Keyer 吧,特别能体现出那个年代的感觉,虽然我本人在那个年代还没有出生 :-) 。
TL;DR
本文内容将重点聚焦于 Techno Keyer 的设计实现过程分享。Techno Keyer 项目的固件实现,原理图设计均已开源发布至 Github,供有兴趣的朋友参考。
发报模式
最初的想法是先实现一个简易的发报功能,通过识别 USB 键盘输入,将输入了字符转码为摩尔斯码,控制电键回路的通断,实现自动发报能力。
键盘输入
Techno Keyer 作为一款全自动电键,自动发报部分最基础的部分,自然是要首先识别 USB 键盘输入的内容,结果就踩到了第一个坑。起初我以为不就是简单的号识别嘛,选个带 USB 功能的 MCU 来不就行了,于是就拿了 ESP32-S3 开发板,在面包板上搭建起原型电路来试试。从 Github 上找来了 EspUsbHost 这个项目来测试下效果,一开始插上了手头一块逻辑早年的薄膜键盘,发现按键识别功能完全正常。
就当我信心满满,准备进入下一阶段工作时,不出意外的话就出意外了。我又拿来了一块手头多余的机械键盘试了下,发现键盘本身都不能被正常启动,更别说是输入信号的检测。先 Debug 看了下设备加载时候的检测输出,发现接入启动检测正常,但后续的信号就识别不到了,自动进入无限重启的过程。在找其他类似项目的 Issue,以及自己对比后,确认是由于 ESP32-S3 自身的频率最高只能到 240MHz,通过 GPIO 的方式,无法达到 USB 2.0 Full Speed 的 480 Mbit/s 信号要求。而现在很多机械键盘的设计为了追求低响应延迟的设计,普遍采用了 USB 2.0 全速的标准,并且也不提供对于低速协议的兼容能力。
为了解决对高速 USB 键盘的兼容问题,参考社区其他项目对类似问题的解决方案,我找来了一颗 KVM 设备上用来实现键鼠远程通信功能的 CH9350L 串口控制芯片。淘宝上先搞来一个开发模块,简单接入电路验证下,键盘输入识别的问题,算是被解决了。只不过相对来说,引入这颗芯片后,整体方案的成本被拉高了不少。其实也有其他大佬在自己项目上,通过将 USB 键盘转换为更为古早的 PS2 接口接入,实现类似的效果。
电键控制
能正确识别输入信号后,接下来便需要将电报 CW 的输出控制信号模拟成传统电键行为的方式,发送给电台。考虑到最广泛的兼容性,降低控制电路的复杂度。
电键 (key) 按照其英文字面含义就带有开关的意思,其实现原理其实也就是最简单的开关控制电路通断的行为。电键 3.5mm 接口的最外圈一般为接地,对于自动键而言,内部 2 个触点位置分别用来表示「短」与「长」两种信号;而对于传统直键来说,内圈只有最头部的触点能够起到信号接通的作用。

至于需要怎样实现向电台发送信号,参考 MOUKD 大佬给出的最简易的方案,直接用 NPN 三极管作为开关来直接控制,同时也给出了实测在 ICOM 的一些机器上完全可以正常工作。进一步,用 MOS 管控制也能达到类似的效果。

不过采用这类方式的实现,理论上能够获得很快的控制响应速度,但会存在几个问题。首先,不同厂商设计的电台,对于电键的信号检测接口电压并没有标准的规范,比如 ICOM 提供的是 3.3V,但如果是其他厂商的电台,甚至是一些上古时期的其他设备,不排除会遇到一些相对高的电压,贸然接入电路,会带来引入高压的风险。再者,由于我们的 Techno Keyer 电键采用独立的 USB 电源供电,大概率不会和电台使用相同的电源,直接连通接地线路,会在设备之间带来存在地线回路的可能。
考虑到以上的一系列问题,我决定选择通过在隔离电路中非常常见的光耦继电器,作为电键信号传递的外部开关控制电路,将内部电路与外部完全隔离开来,保证隔离性与使用的稳定性。配合用于指示信号的 LED 以及蜂鸣器,最终设计为下图所示的电路。

转码发报
对于发报场景的硬件控制实现有初步眉目后,紧接着就是来考虑实现将输入的键盘按键编码,转为摩尔斯码发送出去。由于 Morse Code 的编码诞生远早于 ASCII 码,且其数字/字母编码规则并没有连续性,考虑到功能扩展性,实现上我才用了查表的方式来构造映射关系。
可以将莫尔斯码中的 Dit 与 Dah 分别用 0 和 1 表示,由于字符编码可以用 0 开头,因而采用二进制倒序排列的方式,将字符编码表示为一个 8 位二进制数存储在表中。并且由于不同编码的长度不同,在最高位补上 1 作为结束标志,以便后续的解码。例如,字符 A 的 Morse Code 为 .-
,对应二进制表示为 0b110
。
// Morse code table
// 0 -> Dit, 1 -> Dah, Reverse order, Highest bit is 1
const struct morseChar morseTable[] = {
{'A', 0b110}, // .-
{'B', 0b10001}, // -...
{'C', 0b10101}, // -.-.
}
其实在诸多相关的项目中,还有一种策略是将 Dit 与 Dah 的组合构造为一棵二叉树,通过树遍历检索的位置来获取对应的字符编码,这样最大的优势是可以减少查表存储的复杂度,但会给扩展带来比较大的麻烦,所以我这里就不考虑采用了。
|
比如可以通过以下这样一个 Magic String 来存储表示常见的 Morse Code 编码表,通过对应字符在树结构中的位置来表示其编码。 |
**ETIANMSURWDKGOHVF*L*PJBXCYZQ**54S3***2**+***J16=/***H*7*G*8*90************?_****\"**.****@***'**-********;!*)*****,****:****
在确定编码规则后,对应发送转码逻辑的实现就相对明确了。直接按照字母查表获得对应编码的二进制,通过移位运算的方式,逐位检测并发送对应的 Morse Code 信号。
/**
* 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);
}
然后可以在面包板上,组装一个简单的原型电路测试下实际的效果。

接收模式
在完成了基础的发报功能之后,本着来都来了,再往前走一步的心态,顺带着考虑把解码能力也给加上。
音频处理
相对于发报模式下简单的开关控制逻辑,音频输入信号自身的模拟信号特性,加之无线电传输过程带来的信号干扰以及背噪,会给解码带来诸多的干扰因素。为了提高解码的准确性,通常需要对输入的信号进行预处理,以提取我们关注的频率范围。对于 CW 解码场景,通常建议对取得的音频信号,按顺序进行降噪、滤波、增益、解码、消抖一系列处理来获得预期的结果。对于音频信号降噪、滤波、增益,可以直接在电台内部完成,我们的 Techno Keyer 需要重点关注解码部分。

CW 音频信号,通常被设置为 500~1000Hz 之间的低频正弦音调,能通过一个频率范围相对窄的滤波器提取出来。不过由于这个信号是一个模拟音频信号,Techno Keyer 的设计上选用了在经典音频解调设备上运用广泛的 LM567 来实现特定频率音调信号的检测,通过合理布置外围电路,将关注的音频频率范围锁定在常用 CW 频率范围上。同时,为了进一步提供对中心频率调节的能力,还配合引入了一个数字电位器,通过单片机的 SPI 信号控制调节,实现由菜单动态控制监听解码时所关注的信号频率。

信号解码
摩尔斯码的信号主要分为长短两种,按照标准规范约定,以短信号长度为一个时间单位,长信号的长度应该是三个单位。信号与信号之间需要间隔一个时间单位,字符与字符之间需要间隔三个时间单位,而单词与单词之间建议间隔七个时间单位。

在实际的手工发报操作过程中,人毕竟不是电脑,时间控制上并不会特别精准,不同时间点发送的相同信号也会存在一定的误差。对于解码过程而言,比较重要的工作就是来识别信号的长短差异。在这里,我主要参考 K4ICY 大佬的实现方案,根据持续不断输入的信号,动态更新长短信号的分类规则。计算出长短信号时间长度的几何平均值,作为分类的阈值参考。

通过对照输入的长短信号动态分类,能够相对快速的自适应不同拍发速率的莫尔斯码输入。为了测试效果,我尝试用生成工具,生成不同拍发熟练度的莫尔斯码音频内容,测试解码的效果。测试下来,在音频信号输入清晰的情况下,对于正常熟练度的拍发,解码效果普遍是比较理想的。甚至由于是机器解码,能够应对 40WPM 这样的高速拍发信号。
组装调试
VFD 控制
Techno Keyer 的显示部分采用 VFD 实现,对于 VFD 控制的主要的困难来源于缺少参考资料,淘宝商家提供的资料相对有限,控制程序代码的实现也相对简陋。最后还是自己想办法找到对应的说明手册,对照手册上的指令与其他控制说明参考,通过自己封装 SPI 协议通信来实现对 VFD 显示屏幕的控制。

电路设计
把所有的硬件部分整合起来,设计成一块两层 PCB 作为核心控制电路。为了控制制造成本,我直接选择现成的 ESP32 模块,省去自己打四层版,以及设计天线的麻烦。不过带来的挑战便是,板子空间比较紧凑,且为了方便加工,元器件采用了单面贴片的设计,正面的元器件布置密度就闲的相对高了。
旋转编码器的选择上,我刻意找了一个转置 90° 安装的版本,方便安装在 PCB 的侧面操作。避免传统平面安装需要额外设计支撑结构或者独立 PCB 的问题,降低整体复杂度。


外观设计
核心功能实现完成后,就可以来考虑下把这个小装置做成一个像样点的小装置了。找了下其他 VFD 项目的设计,看到一款三棱柱造型的时钟特别有科技感。

于是我也想参考设计复刻下类似的效果,也将整个外壳对照着 VFD 模块的尺寸,设计成一个三棱柱形式。中间的支撑和侧盖采用铝合金材质 CNC 加工,尽可能还原金属质感的设计,带来一种现代科技感。前后盖采用 3D 打印的方式来完成。

其实对于透明的前盖部分,效果最佳的方案应该是采用透明亚克力材质,甚至还可以选择带有一定灰黑色减光效果的半透明亚克力来体现出高级感。只可惜这么小尺寸的定制化加工,暂时还没有找到相对靠谱的地方来加工,所以先用透明 3D 打印材料来替代了。
最终完成的效果,还是比较满意的。用 21 世纪的集成电路,驱动 20 世纪的真空荧光显示器,发出 19 世纪的通信编码,实现了复古与现代的融合,带来一种人类文明科技进步历史下的别样味道。

这里是 BD4WEI, 73
最后修改于 2024-10-05