使用 Arduino 发送摩尔斯电码






4.53/5 (8投票s)
一个小项目,
引言
开始学习 Arduino 编程可能看起来是一项艰巨的任务。除了学习 C 语言编写 Arduino 代码的细节之外,您通常还需要使用面包板组装电子元件。这对于想要迈出 Arduino 世界第一步的经验丰富的程序员来说可能会让人不知所措。
幸运的是,Arduino 板(以及 Arduino 克隆板)都有一个内置 LED,可以让我们通过摩尔斯电码闪烁消息。使用内置 LED,我们可以在不使用任何额外硬件的情况下启动并运行一个简单的 Arduino 项目。您甚至可以使用 Tinkercad 的 Arduino 模拟器来完成这个项目,完全不需要任何硬件!
我首先要说明一点:本文中的代码可能不是最高效的。它的目的是让新手用户易于理解和逐步学习。在阅读过程中,您很可能会想到更好或更有效的方法来完成相同的任务。这很棒!我强烈鼓励您编写自己版本的项目,其中描述了其他实现方式。
您需要什么
要完成此项目,您需要一个 Arduino、Arduino Nano 或 Arduino 克隆板。要对 Arduino 进行编程,您需要下载并安装免费的 Arduino IDE。
或者,您可以注册一个 Tinkercad 帐户,并使用其内置的 Arduino 模拟器。
背景
Arduino 应用程序可以使用 C 或 C++ 编写。我们将使用纯 C,因为这是您在实际应用中遇到的大多数 Arduino 项目所使用的。
如果您不熟悉 C 语言,请不要担心。我们将把代码分成小块,您可以逐行输入。有些部分您可能会想要复制粘贴。虽然通过手动输入程序的主循环来确保您理解正在发生的事情是有价值的,但复制粘贴字符映射和常量定义是可以的。
项目
这个项目的想法很简单:我们想利用 Arduino 的内置 LED,通过摩尔斯电码闪烁出您选择的消息。我们不会花费大量时间去解释摩尔斯电码究竟是什么,但借用维基百科的一句话:
引用摩尔斯电码是一种通过一系列发音、灯光或咔嗒声来传输文本信息的方法,熟练的听者或观察者无需特殊设备即可直接理解。
在这种情况下,我们将打开和关闭 LED 来传输字母。如果您有兴趣,可以阅读完整的维基百科文章,了解摩尔斯电码及其迷人的历史。
使用摩尔斯电码,字母是通过一系列的点和划来编码的。点代表一个短信号,划代表一个长信号。点和划没有固定的长度。相反,它们是相对于一个时间单位定义的。规则如下:
- 一个点的长度等于一个时间单位
- 一个划的长度等于三个时间单位
- 字母各个部分之间的间隔是一个时间单位
- 字母之间的间隔是三个时间单位
- 单词之间的间隔是七个时间单位
例如,假设我们将 250 毫秒作为我们的时间单位。在这种情况下,每个点将持续 250 毫秒,每个划将持续 750 毫秒。字母的每个部分之间间隔 250 毫秒,字母之间间隔 750 毫秒,单词之间间隔 1150 毫秒。当我们使用灯光或 LED 发送摩尔斯电码时,当发送点或划时,灯会亮起;当发送间隔时,灯会熄灭。
为了弄清楚如何发送消息,让我们看看国际摩尔斯电码字母表
(感谢维基百科提供此公共领域字母表图片)
正如我们所见,每个字母或数字都由其独特的点和划序列表示。
这听起来足够简单了!让我们来看看编写使之工作的代码。
代码
如果您使用的是物理 Arduino 硬件,请打开 Arduino IDE,然后将您的 Arduino 插入计算机的 USB 端口。您可能需要单击“工具”菜单并向下滚动到“端口”以正确设置您的 Arduino 端口。
如果您使用的是 Tinkercad,请加载 Tinkercad 仪表板。在屏幕左侧的菜单中,选择“电路”,然后单击“创建新电路”按钮。当电路工作区出现时,在零件列表中找到“Arduino Uno R3”并将其拖到工作区。接下来,单击屏幕右上角的“代码”按钮。代码屏幕将以块模式打开,这不是我们想要的。单击显示“块”的下拉菜单,并将其更改为“代码”。这将显示一个代码编辑器,允许我们输入代码。
我们现在准备开始编码了!如果您的编辑器中有任何可见的代码,请将其删除。我们将从头开始。
首先,我们将定义需要支持的字符集:
const char* characters = "abcdefghijklmnopqrstuvwxyz0123456789";
接下来,我们将添加一组映射,以便一旦找到所需字符的位置,就可以查找其摩尔斯电码符号。
const char* mappings[] = { ".-\0", //a "-...\0", //b "-.-.\0", //c "-..\0", //d ".\0", //e "..-.\0", //f "--.\0", //g "....\0", //h "..\0", //i ".---\0", //j "-.-\0", //k ".-..\0", //l "--\0", //m "-.\0", //n "---\0", //o ".--.\0", //p "--.-\0", //q ".-.\0", //r "...\0", //s "-\0", //t "..-\0", //u "...-\0", //v ".--\0", //w "-..-\0", //x "-.--\0", //y "--..\0", //z "-----\0", //0 ".----\0", //1 "..---\0", //2 "...--\0", //3 "....-\0", //4 ".....\0", //5 "-....\0", //6 "--...\0", //7 "---..\0", //8 "----.\0", //9 };
乍一看,您可能会不喜欢这段代码:查找我们想要的摩尔斯电码符号的字符索引将需要对消息中的每个字符进行 O(n)(线性时间)查找。这效率不高!我们真正想要的是哈希表,或者 C++ 标准库中的 std::map。
然而,Arduino 开发环境不提供 C++ 标准库,编写自己的哈希表实现会使这个初学者级别的教程过于复杂。事实证明,在这个简单的项目中,低效的线性时间查找并不重要。但请记住,当您为 Arduino 或其他可能由电池供电的微控制器编写应用程序时,您需要仔细考虑应用程序的算法和数据结构,以确保您使用的 CPU 周期不会超过实际需要。
另外请注意,我们正在仔细地对所有字符串进行空终止。这很重要,因为我们将使用 `strlen` 来确定它们的长度,而对未进行空终止的字符串使用 `strlen` 会导致未定义行为。在我们的应用程序中,这很可能会导致 Arduino 锁定并需要重新启动。
接下来,让我们决定基准时间单位是什么。这很重要,因为点、划和间隔的长度都将取决于我们的选择。如果我们选择一个过小的时间长度,点和划会闪烁得太快,难以看到。如果我们选择一个过长的时间长度,我们可能在闪烁 LED 显示消息之前就老死了。
由于所有 Arduino 引脚和延迟命令都接受毫秒为单位的时间,因此如果我们将摩尔斯电码时间单位也定义为毫秒,会更方便。所以让我们从将一个时间单位定义为 250 毫秒开始。这似乎是合理的。它足够长,点可以清晰可见,但又足够短,以至于划不会显得花费太长时间来显示。在您的代码编辑器中,添加以下行:
const int TIME_UNIT = 250;
这给了我们一个时间单位,我们所有的其他单位都将以此为基础。接下来,根据我们上面输入的摩尔斯电码规则,添加以下代码:
const int DOT = TIME_UNIT; const int DASH = 3 * TIME_UNIT; const int SYMBOL_SPACE = TIME_UNIT; const int LETTER_SPACE = 3 * TIME_UNIT - SYMBOL_SPACE; const int WORD_SPACE = 7 * TIME_UNIT - LETTER_SPACE;
请注意,当我们定义字母间隔时,我们减去了符号间隔。当我们定义单词间隔时,我们减去了字母间隔。我们这样做是因为每次显示一个符号(点或划)时,我们也会添加一个间隔——也就是说,在显示的每个符号之后,我们会让 LED 熄灭一个时间单位。
所以,每个符号中已经包含了一个时间单位的延迟。为了确保字母之间的间隔是正确的,当我们定义 `LETTER_SPACE` 时,我们减去了 `SYMBOL_SPACE` 使用的时间。字母间隔和单词间隔之间的关系也适用同样的推理。
接下来,让我们定义我们要转换成摩尔斯电码的消息:
const char* message = "codeproject\0";
定义部分就是这些了!现在我们准备编写使我们的摩尔斯电码闪烁器工作的代码了。首先,我们将添加一个 `setup` 函数,它将 13 号引脚设置为输出模式。13 号引脚控制 Arduino 的内置 LED。
void setup() { pinMode(13, OUTPUT); }
如果您熟悉 C 语言,您可能会想知道 `OUTPUT` 这个常量是从哪里来的。如果我们没有在任何地方定义它,我们怎么能使用它呢?答案是 Arduino 开发环境会自动导入许多有用的函数和常量。`setup` 函数将在您的程序首次启动时自动调用一次。
接下来,我们将添加实际闪烁 LED 的代码。
void loop() { int size = strlen(message); //loop through the message for (int i = 0; i < size; i++) { const char* ch = strchr(characters, tolower(message[i])); if (ch != NULL) { int index = (int)(ch - characters); const char* morseSymbols = mappings[index]; int count = strlen(morseSymbols); for (int i = 0; i < count; i++) { digitalWrite(13, HIGH); int symbolTime; char symbol = morseSymbols[i]; if (symbol == '.') symbolTime = DOT; else symbolTime = DASH; delay(symbolTime); digitalWrite(13, LOW); delay(SYMBOL_SPACE); } delay(LETTER_SPACE); } } delay(WORD_SPACE); }
在 Arduino 程序中,`loop` 函数会不断被调用。在我们的程序中,每次运行此函数将导致我们的消息显示一次。
让我们逐步分析程序,看看发生了什么。
我们首先检查消息的长度,然后开始一个循环,一次处理一个字符。
然后我们调用 `strchr` 来在支持的字符列表中查找当前字符。如果当前字符是我们不支持的,`strchr` 将返回 NULL,我们将跳到下一个字符。请注意,我们对正在查找的每个字符都调用 `tolower`。`tolower` 是一个 C 函数,顾名思义,它将大写字符转换为小写字符。这将允许我们处理包含大写和小写字符的消息字符串。将所有字符转换为小写是可以的,因为摩尔斯电码不区分字母大小写。
如果 `strchr` 找到了字符,我们将进入 `if (ch != NULL)` 语句包含的代码块。我们首先要做的就是计算 `index`,它查找当前字符在 `characters` 数组中的索引。因为 `mappings` 中的摩尔斯电码字母与 `characters` 中的 ASCII 字母顺序相同,所以我们将能够使用 `index` 来查找当前正在处理的字母的摩尔斯电码符号。
如果 `index` 的计算方式看起来有些奇怪或令人困惑,请不要担心。我们只是做了一些指针算术。我通常*非常*讨厌那些说“它有效,相信我!”的教程。但在这种情况下,我希望避免将摩尔斯电码教程变成指针和指针算术教程!我只会请您相信我一件事:这里没有什么魔法。即使您现在对指针没有很好的理解,您也可以学会它们。然后,您就能回顾您写的这段代码并更深入地理解它。
在计算完 index 后,我们调用 `strlen`,它告诉我们当前摩尔斯电码字母的长度。我们需要知道这一点,以便逐个字符地循环它,并用点和划来闪烁 LED。
接下来,我们创建一个名为 `morseSymbols` 的 `char` 指针。它只是指向 `mappings` 数组中当前字母的摩尔斯电码符号。我们并不严格*需要*这个指针。但在接下来的几行中,拥有这个指针将允许我们引用 `morseSymbols[i]` 而不是 `mappings[index][i]`。在功能上,它们是等效的,但我更喜欢更容易理解的代码。
在创建 `morseSymbols` 之后,我们使用 `strlen` 为一个 `count` 变量赋值,该变量表示当前摩尔斯电码字母包含的各个符号(点和划)的数量。然后,我们使用此计数变量循环遍历 `morseSymbols` 数组中的所有符号。
查看循环内部,我们可以看到我们做的第一件事是调用 `digitalWrite(13, HIGH)`。`digitalWrite` 是 Arduino 开发环境中的一个内置函数,而 `HIGH` 是一个内置常量。此函数调用会打开 Arduino 的 LED。
现在 LED 已经打开,我们希望等待一小段时间,然后再将其关闭。我们想等待多长时间取决于我们是想发送点还是划。所以,在打开 LED 后,我们检查当前符号是点还是划。如果是点,我们就调用 `delay(DOT)`。如果是划,我们就调用 `delay(DASH)`。正如您所料,`delay` 会导致 Arduino 暂停正在运行的程序的执行。`delay` 接受一个参数:一个整数,告诉 Arduino 暂停多少毫秒。
在为点或划等待适当的时间后,我们调用 `digitalWrite(13, LOW)`。这会关闭 LED。最后,我们调用 `delay(SYMBOL_SPACE)`。内部循环中这四行代码的净结果是:打开 LED,等待 250 毫秒(`DOT`)或 750 毫秒(`DASH`),然后关闭 LED 并等待 250 毫秒(`SYMBOL_SPACE`)。
在闪烁 LED 显示完一个字母的所有符号后,内部循环完成,我们调用 `delay(LETTER_SPACE)`。
然后,在显示完所有字母后,我们调用 `delay(WORD_SPACE)`。
一旦 `loop` 函数完成,Arduino 就会再次调用 `loop` 并从头开始整个过程。我们的 Arduino 将继续闪烁摩尔斯电码消息,直到永远,或者直到我们拔掉电源。
要在您的 Arduino 上运行此代码,请单击 Arduino IDE 中的上传按钮。它看起来像一个绿色箭头。
代码将上传到您的 Arduino,您将看到 SOS 摩尔斯电码在闪烁。
如果您使用的是 Tinkercad,请单击“开始模拟”按钮来启动您的程序。
然后……就是这样!我们已经对 Arduino 进行了编程,使其可以发送 SOS 信号。这是一个相当简单的项目,它只使用了 Arduino 功能的一小部分。不过,通常最好从小处着手。既然您已经品尝了 Arduino 编程的滋味,您可能会喜欢我关于使用 Arduino 硬件或 Tinkercad 创建二进制计数器的文章。
后续步骤
您可能会注意到我们的摩尔斯电码闪烁器存在一些缺陷。它没有做任何事情来处理包含空格的消息。而且,虽然我们的线性时间查找效率低下,但我们可以通过在程序首次启动时*一次性*完成 ASCII 到摩尔斯电码的转换来缓解这个问题。考虑到这些缺点,以下是一些您可以尝试的小挑战:
- 修改程序以处理包含空格的摩尔斯电码消息。
- 修改程序以在程序启动时一次性翻译 ASCII 字符。`setup` 函数是这样做的理想场所,因为 Arduino 只会调用它一次。
- 用 C 语言实现一个哈希表,或者找到一个您喜欢的预制哈希表实现。修改程序以使用哈希表将 ASCII 字符映射到摩尔斯电码字符,而不是进行数组查找。
- 获取一个面包板、一个扬声器、一些电阻器和一些跳线。创建一个将扬声器连接到 Arduino 的简单电路,并修改程序以使用扬声器播放摩尔斯电码消息,而不是通过 LED 闪烁。或者,为了更有趣,让您的摩尔斯电码消息同时通过 LED 和扬声器播放!