基于Arduino的开源CW摩尔斯电码训练器制作与实战 📅 发布时间:2026/9/2 16:58:55 👁 浏览次数: 简介CW Morse Code Trainer是一款面向业余无线电爱好者及莫尔斯电码初学者的开源训练软件它将任意文本实时转为莫尔斯电码声音也可自动生成随机符号序列供盲听解码练习并支持发送速度与法恩斯沃思压缩调节能够帮助用户循序渐进地提升听抄能力为备考业余无线电执照提供实用训练手段。资源包体积仅72KB共包含3个文件可直接在Windows下运行的exe主程序、用C语言编写的完整源代码以及简要说明txt用户既可以开箱即用也可通过源码学习核心算法在Linux等平台自行编译扩展。作为一个开源项目其代码完全开放开发者能够根据自己的需求修改或增强功能。目前已有269人学习下载对于希望低成本掌握莫尔斯电码的入门者和无线电爱好者来说这是一份轻量而实用的工具包。 如果你玩业余无线电八成绕不开 CWContinuous Wave等幅报这道门槛。CW 用莫尔斯电码Morse Code以通断射频信号的方式传递信息是短波通信里抗干扰能力最强、最省功率的模式之一。很多新手栽在听抄速度上——耳机里一串嘀嘀嗒嗒过去根本反应不过来是哪个字母。这时候一个能按你自己的节奏出题、反复播放、还能立刻对答案的 CW Morse Code Trainer 就特别管用。我今天要聊的就是一个开源版本的 CW 训练器。它的硬件电路和固件代码完全开源你可以自己用 Arduino 搭一个也可以修改代码扩展成 ESP32 或 STM32 版本。无论你是刚接触莫尔斯电码的零基础新人还是准备考业余无线电操作证的准 HAM这个项目都能在练听抄的路上帮你省下不少力气。1. 先定位CW Morse Code Trainer 到底在练什么1.1 摩尔斯电码学习的三个核心问题摩尔斯电码听起来神秘本质就是一套时间编码短音点、长音划、音内间隔、字符间隔、单词间隔靠这几样东西的不同组合表达 26 个字母、10 个数字和常用标点。新手面对的困难基本就三个。第一个是反应速度。单个字母比如 K 是划点划你听到时能反应过来但连成串就懵了因为大脑还在处理上一个字符下一个已经播完。第二个是速度感。很多人一开始就练 5 WPM听得很轻松可一提到 15 WPM 就彻底乱套因为耳朵没有建立音素到字母的条件反射。第三个是练习方式。随机播报、抄写、对答案这个循环看着简单但用录音文件练效率很低——因为你没法动态调节速度也没法把总是听错的字符单独揪出来反复练。CW Morse Code Trainer 这类开源工具解决的就是这三点它把出题、播放、反馈、重练做成了一个闭环你能随时调整速度甚至指定只练某几个字符。练习效率比单纯听录音高得多。1.2 训练器需要满足的核心功能在动手做之前先想清楚一个训练器必须具备哪些功能否则做着做着就会失去方向。按我自己的使用经验最低限度需要四件事。可调速度至少支持 5 到 30 WPM 连续调节最好用旋钮训练时手不用离开桌子去按按键。随机出题从字母、数字、符号池里随机抽取避免同一组字符反复出现导致的背答案。即时反馈播完一个字符或一组字符后能显示或播报正确答案错了还能重播一次。多样模式最基础的是单字符听抄进阶一点要有呼号训练、常用 Q 简语训练、短报文听抄。这四项是底线。至于音量、音调、自动解码这些属于锦上添花可以放到第二个版本再做。我见过很多新手一上来就想把训练器做成全功能一体机结果卡在功能设计上迟迟无法收尾。开源项目的正确做法是先跑通最小闭环再逐步加功能。2. 硬件选型三条开源路线为什么我推荐 Arduino2.1 三条路线的对比开源这两个字在 CW 训练器上有一个好处你不需要从零开始设计电路和写代码GitHub 上能找到大量现成方案。但不同的方案硬件门槛差得很远。我梳理下来主要有三条路线。路线代表方案优点缺点纯软件Python 脚本、Web 页面零硬件成本改代码方便缺少物理按键手感不贴近真实报务场景单片机Arduino、ESP32、STM32体积小、可便携、能接蜂鸣器和耳机需要做简单接线新手要过一道硬件门槛成品改造老式练习器加解码模块外观复古、手感好开源资料少改造难度高如果你想老老实实练听抄纯软件方案完全够用甚至有网页版训练器可以直接在浏览器里跑。但如果你的目标是以后上电台实操我建议至少做一版单片机硬件训练器——因为真实 CW 通联时你听到的是从收音机喇叭里出来的声音和电脑扬声器播放的音色完全不同提前用蜂鸣器或小喇叭适应训练会让过渡更平滑。2.2 为什么我选了 Arduino而且推荐你也这么干我的理由是Arduino 生态学习资料最密集遇到问题搜索引擎一抓一大把对刚接触硬件的朋友非常友好。具体到本项目Arduino 自带的tone()函数可以直接在指定引脚输出方波信号驱动蜂鸣器不需要额外写 PWM 驱动代码几行就能搞定音频输出。另一个关键原因是成本。一颗 Arduino Nano 兼容板只要二三十块加上无源蜂鸣器、10k 电位器、1602 LCD 显示屏总物料成本可以控制在 60 元以内。相比 ESP32 开发板要额外处理 WiFi 配置、网页服务器这些无关功能Arduino 的简单粗暴正好符合训练器的定位它是一个工具不是一个玩具。如果你手上已经有 STM32 板子当然也可以做但代码里需要自己实现定时器产生音频信号排查问题时也更依赖示波器。对于 90% 的初学者Arduino 是投入产出比最高的选择。我后面所有代码和接线都以 Arduino UNO/Nano 为例。3. 核心逻辑拆解摩尔斯码表、发声时序与速度计算3.1 摩尔斯码表与编码结构一个 CW 训练器最核心的数据结构就是字符到莫尔斯码的映射。在代码里我建议用一个简单的查表结构来存而不是硬编码一串 if-else。以 Arduino 为例可以这样组织struct MorseEntry { char ch; // 字符例如 A const char* code; // 莫尔斯码例如 .- }; MorseEntry morseTable[] { {A, .-}, {B, -...}, {C, -.-.}, {D, -..}, {E, .}, {F, ..-.}, {G, --.}, {H, ....}, {I, ..}, {J, .---}, {K, -.-}, {L, .-..}, {M, --}, {N, -.}, {O, ---}, {P, .--.}, {Q, --.-}, {R, .-.}, {S, ...}, {T, -}, {U, ..-}, {V, ...-}, {W, .--}, {X, -..-}, {Y, -.--}, {Z, --..}, {0, -----}, {1, .----}, {2, ..---}, {3, ...--}, {4, ....-}, {5, .....}, {6, -....}, {7, --...}, {8, ---..}, {9, ----.}, // 常用标点按需添加 };有人会问为什么不用二维数组或者直接用字符串因为struct结构更好扩展。你后面想加字母 Q 简语或者常用缩略语只需要在这个表里继续追加。播放时遍历这个表找到对应字符把code字符串里的.和-逐个解释成短音和长音就行。需要注意一点这个表是广播级的莫尔斯码和某些老教材里手键拍发习惯略有差异比如某些连发习惯但对于训练听抄来说完全够用。如果你之后要自己加中文电报码那是另一套体系需要单独建表不要混在一起。3.2 发声时序WPM 到底怎么算出来的这一节是整个训练器最容易出错的地方也是为什么我的训练器听起来比别人快/慢的根源。CW 世界里速度通常用 WPMWords Per Minute每分钟单词数表示。但单词不是按真实英文单词长度算的而是用标准校准词 PARIS 来定义。PARIS 由 5 个字母组成可以被拆成 50 个点单位。一个点的时长为1200 / WPM毫秒。为什么是 1200因为 PARIS 这个词恰好等于 50 个点单位一分钟 60000 毫秒除以 50就是 1200。这个推导很关键点dot时长 1200 / WPM毫秒划dash时长 3 × 点时长字符内点划间隔 1 × 点时长字符间隔 3 × 点时长单词间隔 7 × 点时长举个例子我常用 20 WPM 练习那么一个点就是 1200 / 20 60 毫秒一个划就是 180 毫秒。如果训练器里的速度参数是 20 WPM但播出来的码听起来比标准快很多多半是代码里把字符间隔算错了把 3 倍点时长写成了 1 倍。还有一个经常被提到但容易忽略的概念叫 Farnsworth 间隔也就是提高字符内速度、拉长字符间间隔的训练方法。它的思路是让每个字符以较快的速度播放但字符之间留出更长的思考时间这样可以避免大脑对慢速字符形成依赖。开源训练器一般会在高级设置里提供两个参数一个是字符速度Character Speed一个是有效速度Effective Speed。如果你一开始跟不上整体速度可以试试把有效速度调到 8 WPM字符速度保持 20 WPM耳朵会舒服很多。3.3 播放状态的实现不能用 delay 硬等很多新手写播放逻辑时喜欢用delay()跑起来也确实能响但马上会遇到问题在蜂鸣器播放一个字符的几分钟里你无法响应按键、无法更新屏幕、无法调节速度。整个程序卡死在延时里。正确的做法是用非阻塞播放也就是状态机思路。把播放过程拆成多个状态读下一个字符、取码、播点、停点间隔、播划、停划间隔、字符结束、单词结束。每次loop()只判断当前时间是否到了该切换状态的时刻然后播放对应的音。这样 CPU 一直有空闲按键扫描和屏幕刷新都能正常执行。用 Arduino 写非阻塞播放核心是millis()时间戳管理。我习惯把当前播放到哪一步存成一个状态变量每一步记录开始时间然后循环里不断比较millis() - lastTick是否超过当前状态的时长。这种写法一开始有点绕但写顺了之后扩展按任意键停止播放这类功能就非常简单。4. 实操记录从零搭一个能出声音的 CW 训练器4.1 硬件清单与接线我实际做这一版时用的零件清单如下都是一些很常见的物料某宝随便凑齐。元器件型号/规格数量用途说明主控板Arduino UNO 或 Nano1控制逻辑与音频输出蜂鸣器无源蜂鸣器5V1发出莫尔斯音电位器10k 单圈1旋钮调节速度LCD 显示屏1602 I2C 转接板1显示当前模式和速度按键轻触开关2模式切换/重播面包板及跳线-若干快速搭电路接线很简单。无源蜂鸣器的正极接 Arduino 的 D8 引脚负极接 GND电位器三个脚分别接 5V、A0、GNDLCD 的 I2C 接口接 SDAA4和 SCLA5两个按键一端接 D2/D3另一端接 GND并在代码里启用内部上拉。这里有一个很容易踩的坑蜂鸣器必须买无源的不能买有源的。有源蜂鸣器上电就会响只能用开或关两种状态控制无法产生不同频率的莫尔斯音调接上去只会听到一个噗声根本练不了听抄。4.2 固件上传与参数调整代码部分我用的是 Arduino IDE 2.x直接在库管理器里装好LiquidCrystal I2C库然后烧录以下核心框架即可。这里给出最关键的速度调节和播放逻辑。const int buzzerPin 8; const int potPin A0; static int state 0; static unsigned long lastTick 0; static int dotDuration 0; static const char* currentCode ; static int codeIndex 0; int readSpeedWpm() { int val analogRead(potPin); // 0-1023 return map(val, 0, 1023, 5, 30); // 映射到 5-30 WPM } void startPlayChar(char ch) { currentCode morseToCode(ch); // 查表 codeIndex 0; state 1; lastTick millis(); } void updatePlayback() { unsigned long now millis(); int dot 1200 / readSpeedWpm(); // 实时读取旋钮速度 switch (state) { case 1: // 开始播放 point 或 dash if (currentCode[codeIndex] .) { tone(buzzerPin, 700); // 700Hz 点音 lastTick now; state 2; } else if (currentCode[codeIndex] -) { tone(buzzerPin, 700); lastTick now; state 3; } break; case 2: // 点结束等待间隔 if (now - lastTick dot) { noTone(buzzerPin); lastTick now; state 4; } break; case 3: // 划结束等待间隔 if (now - lastTick dot * 3) { noTone(buzzerPin); lastTick now; state 4; } break; case 4: // 字符内间隔播放下一个码 if (now - lastTick dot) { codeIndex; if (currentCode[codeIndex] \0) { state 5; // 字符结束 } else { state 1; } } break; case 5: // 字符间间隔 if (now - lastTick dot * 3) { // 播放下一个随机字符 startPlayChar(nextRandomChar()); } break; } }这段代码我做了两件比较重要的设计第一readSpeedWpm()每次都在播放循环里调用所以你在播放过程中转电位器下一秒速度就会变化不用重新启动。第二用状态机而不是delay()这样在播放长报文时屏幕依然能刷新显示当前模式按键也能随时重播。实际烧录的时候记得先在 Arduino IDE 工具菜单里选对开发板型号和串口端口。Nano 的话还要确认处理器型号是 ATmega328P老版本还是 ATmega328POld Bootloader选错了会出现上传成功但板子没反应的情况这是新手最容易卡住的问题。4.3 训练模式与使用感受我设计了三档模式用按键切换单字符随机听抄、五字符一组听抄、常见 Q 简语听抄。第一周用 15 WPM单字符随机模式每天练 20 分钟感觉进步比之前用录音文件快很多因为每次播完都会显示正确答案脑子里音-形对应的回路被不断强化。等到字母基本不出错后再切到五字符一组模式模拟真实呼号的节奏。LCD 屏幕我固定显示两行第一行显示当前模式名称和速度第二行显示刚播报过的字符答案。这样耳机听一遍、眼睛扫一遍形成双重记忆。如果你不想盯着屏幕练可以把屏幕信息改成成绩统计模式记录十轮听抄的正确率训练会更有目标感。5. 常见问题与排查技巧实录5.1 声音异常蜂鸣器不响、响不停、音色不对最大概率的问题是有源蜂鸣器当作无源用。判断方法非常简单给蜂鸣器两个引脚直接接 5V 和 GND如果接上就长响那肯定是有源蜂鸣器赶紧换。第二类是音色刺耳或者太尖锐。tone()函数默认输出的是方波方波本身含大量谐波听起来比真实电台的 CW 音硬很多。我测试后觉得 650 到 750Hz 听起来最舒服太高会扎耳朵。如果还想更圆润可以在蜂鸣器引脚对地之间串一个 100Ω 电阻和一个 0.1uF 电容组成一个简单的 RC 低通滤波把方波的高频分量滤掉一部分。还有一种是声音爆音或者咔哒声。这通常是因为tone()和noTone()切换太频繁扬声器振膜还没稳定下来就又收到下一个指令。解决办法是在每次切换状态时加一个 2 到 5 毫秒的缓冲或者用tone(buzzerPin, 0)代替noTone()让输出平滑归零。5.2 速度感不对感觉越快越乱如果你觉得 20 WPM 的码听起来比网上标准训练音频快很多先用万用表或者示波器测一下引脚输出确认点时长到底是 60ms 还是更短。常见错误就是代码里把字符间隔 3 × dot写成了字符间隔 dot这样字符会挤成一团实际感知速度接近翻倍。另一个容易搞混的是有效速度和字符速度。有些开源固件界面上写 WPM实际上只控制字符内部速度字符之间的间隔固定不变。训练时会发现无论怎么调快慢码的密度总是不对。我建议只要涉及速度参数的修改都在代码注释里写清楚到底指的是哪个速度否则一个晚上调试全耗在这上面。5.3 LCD 不显示、按键失灵LCD 不显示八成是 I2C 地址不对。我用的转接板地址是 0x27但有些模块是 0x3F。Arduino 里可以先跑一个 I2C 扫描程序看看实际地址是多少再把代码里的LiquidCrystal_I2C lcd(0x27, 16, 2);改掉。按键失灵基本都是引脚定义冲突。Arduino UNO 的 D2、D3 本身就带内部上拉不需要外接上拉电阻如果外接了一个 10k 的上拉到 5V反而可能把引脚电平状态搞乱。接法直接按键一端接 D2另一端接 GND然后在代码里pinMode(2, INPUT_PULLUP);读取时检测LOW即为按下。6. 开源项目的进阶玩法与后续扩展6.1 加一个自动解码模块训练器练到后期你会发现听抄和发报是两回事。如果你希望训练器能检测你自己的拍发是否正确可以加一个解码模块。思路是用 Goertzel 算法提取特定频率的音频信号识别出点、划和间隔再反查摩尔斯码表。这个功能在 Arduino 上性能偏紧张但放到 ESP32 上就很轻松。GitHub 上有个开源项目叫 Arduino CW Decoder可以直接参考它的 Goertzel 实现移植过来后你的训练器就从一个播放器变成了陪练教练。6.2 改成 ESP32 实现 Web 控制如果随身带旋钮和屏幕不方便可以换 ESP32 做主控用 WebSocket 在浏览器里控制训练器的速度和模式。这个改造并不复杂ESP32 开一个 AP 热点手机连上来打开网页就能操作蜂鸣器仍然挂在 GPIO 引脚上。这样你在地铁上都能用手机控制一个口袋里的小盒子练听抄。我实测 ESP32 的ledc写 PWM 驱动蜂鸣器足够流畅没有杂音。6.3 对接真实电台的拍发练习最有价值的扩展是把训练器输出接到真实电台的耳机输出口用解码模块检测自己手键拍发的正确性。这里要注意电气隔离建议用一个音频变压器或者光耦把训练器的地线和电台地线隔开避免地环路引入交流声。电台上手键拍发一次训练器屏幕上直接显示你发的字符对不对这个反馈回路对纠正手型非常有效。我做这个项目的总体感受是CW 训练器本身不算复杂但它把训练内容、速度控制、反馈机制三件事做得很扎实对技术学习的意义完全不亚于它作为教具的意义。整个过程从代码到硬件都是开源透明的任何一步出了问题都能打开源码一行行排查这本身就是很好的电子与编程实践。如果你也想练好 CW我建议先照这个方案搭一台基础版用两周时间把听抄速度稳定到 20 WPM再决定要不要往解码和 Web 方向扩展。动手永远比犹豫重要。本文还有配套的精品资源点击获取