摘要:本文介绍一款基于Arduino NANO开发的,带2.4G无线收发报功能的摩尔斯电码训练器的系统设计思路。发射端通过NANO板的外部中断引脚采集电键输入的脉冲PPM序列同时驱动喇叭播放电键音,然后对采集到的PPM序列时序进行量化分析,解析得到当前输入的摩尔斯电码,在LCD屏幕上显示对应的字符。与此同时将量化好的脉位数据及字符编码、空格等信息利用串口数据透传模块发送出去。接收端接收到数据后,解析出原PPM序列及字符编码、空格信息,驱动喇叭播放电键音,并在LCD屏幕上显示对应的字符及空格。并且,以电键输入特定编码:1、可以实现国际通用字码及数字长码模式与数字短码模式的相互切换;2、可以启动国际通用字码及数字长码的自动播报。

1. 引言

摩尔斯电码是一种国际通用的信号代码,以"点"和"划"的组合对字母、数字、符号进行编码,它可以利用电台的载波进行传输,也可以利用声音、图形来进行信息传递。一直以来,摩尔斯码都是一种重要的通信方式,在航空、航天、航海等领域都有不可取代的地位,目前也是无线电爱好者进行通联"必备软件"。用电台进行摩尔斯码的抄报与发报需要进行大量的训练才能达到正常使用的水平,因此,摩尔斯电码练习器是不可或缺的。而目前市面上的练习器,主要有两种:一种只可以通过听来练习;一种可以听,可以解码显示,接电台可以进行发报。笔者在进行练习时想到,能不能开发一种可以听、可以显示,而且不接电台就可以进行模拟收发报的训练器(要持有电台并且使用电台要经历一个比较漫长的过程)。因此,笔者开发这一套基于Arduino的、带2.4G无线传输功能的摩尔斯电码练习器。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(1)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(2)

2. 系统硬件组成

本文使用的处理核心为Arduino NANO板,其搭载的ATmega328单片机足以完成系统功能。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(3)

由于系统需具备较高的实时性,电键信号使用NANO板的外部中断0即D2引脚来进行采集,电键实质上就是一个开关,因此构建一个开关电路,当电键按下时为高电平,放开时为低电平。为了防止由于电压波动产生误中断,在信号与地之间加入了滤波电容,如下图:

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(4)

屏幕显示选用了IIC接口的LCD2004液晶显示屏幕。该屏幕有4行显示区,每行可显示20个字符。因此将屏幕分为上下两个显示区,第1、2行显示发送或本机电键输入的电码字符,第3、4行显示接收到的电码字符,界面设计如下图所示:

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(5)

声音播放用NANO的D3口进行控制,即高电平时发声,低电平时停止发声。声源可由有源蜂鸣器提供。本文选用NE555芯片产生音频脉冲,D3口驱动继电器模块(本文中继电器使用是成品模块)通断,控制脉冲信号的通断以驱动喇叭发声,同时喇叭音量、音调可以通过电位器进行调节。电路原理图如下:

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(6)

无线传输部分使用的是UART接口的数据透明传输模块,工作频率为2.4GHz。该模块由于使用的是UART传输,只需要连接TX和RX,简化了硬件结构,同时程序设计也相对简化了,只要设置好波特率和通讯协议即可。接下来开始介绍软件设计思想。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(7)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(8)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(9)

制作过程

3. PPM采集到摩尔斯电码的转换

1) 摩尔斯码与PPM的联系

摩尔斯码由"点"和 "划"组合而成,例如字母"a",它对应的摩尔斯码为"·–",字母"B"对应的摩尔斯码为"–···"。而以电键输入摩尔斯码时,则以电键按下的时间长短来表示点和划。如输入"·"时按下电键的时间大约为0.04秒左右,得到的是一个宽度大约0.04秒的脉冲,输入"–"的时间大约是0.15秒,得到的是一个宽度为0.15秒的脉冲信号。因此,以电键输入一个摩尔斯电码,实质上就是形成了一串由多个宽度不一的脉冲组合而成的脉冲序列,这个脉冲序列就构成了一个PPM信号(脉冲位置调制信号)。如下图所示:

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(10)

不同的字母对应不同的点划组合,从而对应着各不相同的PPM序列,只要能够正确采样这些PPM信号,就可以解析出电键所输入的摩尔斯电码。

2) 采样PPM解析摩尔斯码

a) PPM采样方法

PPM采样的关键在于获得各高、低电平脉冲的宽度及其在脉冲序列中的位置。Arduino NANO的外部中断可以捕捉外部电平跳变,并且Arduino官方库中的micros()函数调用时可以读取到系统从开机到当前所经历的微秒数,有了这两个工具则可以非常方便地进行PPM采样。方法如下:首先,将Arduino的外部中断0的触发方式设置为跳变触发,即脉冲的上升沿和下降沿都能触发中断,每次中断到来,就调用micros()函数读取一个当前时刻,但这个时刻必须区分是上升沿时刻还是下降沿时刻,才能计算脉冲的宽度,例如第一个下降沿的时刻减去第一个上升沿的时刻,就能计算出第一个脉冲的宽度;第二个上升沿的时刻减去第一个下降沿的时刻,就能计算出第一个低电平间隔的宽度(或称为负脉冲)。值得注意的是,为什么还要采集低电平的时间呢?低电平对于解码的意义不大,但由于我们要在接收端"复现"发射端电键输入的声音,因此必须采集低电平间隔,才能在接收端"一比一"地复现电键音。为了实现PPM的完全采集,可以在进入中断后读取D2引脚上的电平:如果是高电平,则本次中断是上升沿触发;如果是低电平,则是下降沿触发,如此在不同的触发状态下分别记录上升沿时刻及下降沿时刻,则可以计算正负脉冲的宽度。同时,在每个上升沿置高D3引脚,在每个下降沿置低D3引脚,从而控制喇叭按照电键输入的电码播放声音。

接下来,继续深入地来看如何处理PPM。摩尔斯码是由点和划组合得来,但所有字母的点划组合中点和划的总数不是一定的,例如:A的点划总数是两个(·–),B的点划总数是四个(–···),D的点划总数是三个(–··),E的点划总数是一个(·),所有数字长码的点划总数为五个,一些标点符号为6个,那么每一次采样,应该如何来存储这些PPM数据呢?本文中使用的方法是将所有电码的点划总数按七个点划处理,增加的一位是为了方便加入功能转换编码(例如长码转数字短码,自动播报启动),将七个正脉冲数据存储于一个数组中(代码中的in_code[]),将间隔的6个负脉冲数据存储于另一个数组中(代码中的in_l_code[])。那么,是不是每一次采集都要等待采集7个正脉冲呢?比如电键输入"A",实质上只需采集到两个正脉冲及一个间隔负脉冲就完成了采集,假如紧接着输入"B",那么:1、输入A时如何判断输入完毕;2、接着输入B,B和A之间是否加入空格?为了解决这个问题,就必须建立两个判断机制,首先是判断字码输入完毕,然后是判断两个输入的字码之间是否存在空格。

这两个判断机制,可以建立在时间间隔的基础上。对于一个相对标准的发报手法,点和划的长度、点和划之间的间隔、字码间的间隔都可以得到一个比较固定的时间范围,因此,可以在每个下降沿到来后置位一个标志位,用于控制定时计数的启动,如果定时计数达到设定的阈值(设定好的字码间隔),那么表示该字码输入完毕;如果在正常的点划间隔内能够进入下一次中断(上升沿触发),那么定时计数标志位会被清除,停止计数;另外,在每一个下降沿到来时,用另一个计数值来记录采集到正脉冲的个数,如果定时计数被清除,那么正脉冲记录值从0加到6时则表示7个点划采集完毕,即字码输入完毕。字码输入完毕后,置位相应的标志为,进行解码、显示并且发送数据了。完成了一个字码的显示后,下一个字码的显示位置默认是加1,并且置位空格检测标志位,同样是利用定时计数的方法,如果计数未达到设定的阈值,由于上升沿触发中断,那么空格检测标志位及计数会被清零,那么这个字码就会紧挨着上一个字码显示;如果计数值达到了设定的阈值,则把在原有的显示位置上再加1,那么下一个输入的字码与上一个字码间会增加一个空格。以上的检测过程在一个1毫秒的定时中断函数中处理。经过大量数据样本采集及实际测试,字码间的间隔时间阈值设定为150毫秒比较合适,空格符加入的时间间隔阈值设定为200毫秒较为合适。调整这两个阈值,也可以起到调节发报速度的作用,对于初学发报的可以适当调大两个值。对于发报较为熟练的可以适当减小两个值。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(11)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(12)

发射及接收界面

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(13)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(14)

外部中断处理函数的代码

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(15)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(16)

定时中断函数代码

b) PPM数据解析摩尔斯码

将PPM数据解析为摩尔斯码,要经过两个步骤:1、将PPM按位置量化为二进制编码,每一个PPM序列量化为一个14个位的编码,并用一个16位的变量来存储这个编码;2、根据编码映射摩尔斯码。

为实现第一个步骤,笔者对点和划的脉冲宽度以及间隔时间进行了大量采样(数据样本来源于发报速度较为标准的资深无线电爱好者),下面是点和划的部分采样值,单位为微秒。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(17)

"点"的脉宽采样

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(18)

"划"的脉宽采样

从以上数据可以看到,"点"的脉宽在40000-60000微秒之间(0.04秒-0.06秒),"划"的脉宽在150000—300000微秒之间(0.15秒-0.3秒)。由于不同的使用者发报手法不同,得到的点划脉宽也不相同,为了提高系统的识别率,放宽了脉冲的范围:50—120000微秒(0.00005秒—0.12秒)识别为"点",大于120000但小于450000微秒(0.12秒—0.45秒)识别为"划",其余的都识别为错误输入。确定好了脉宽范围以后,接下来对PPM信号进行量化编码。在一个16位的二进制编码中,以0-13位(最低位为第0位)来存放编码,每两位的组合来表示一个点划位,以"01"表示"点","11"表示划,"00"表示空位,存放顺序为:电码先输入的放在二进制的低位,后输入的依次往高位存放,则字码"A"对应的二进制编码为:00 00 00 00 00 00 11 01 (·–),转换为十六进制为:0x000d;"B"对应的二进制编码为:00 00 00 00 01 01 01 11(–···),转换为十六进制为:0x0057。将所有字码按照这个规则转换为十六进制编码,当采集完一个PPM序列后按上述规则进行量化编码,将量化得到的编码放入到"switch"结构中进行判断,就可以快速地得出当前电键输入的字码,如经过对比没有找到对应的编码,则判断为输入错误,在屏幕上会显示"*"提示。下面是功能程序段:

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(19)

PPM—量化编码示例代码

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(20)

摩尔斯字码提取示例代码

4. 串口数据发送

电键PPM采集、解码显示的同时,还要将电键PPM数据、摩尔斯码以及空格信息发送到接收端,其中PPM数据用于"复现"发射端的电键音。在采集PPM数据时,采样的是脉冲宽度的微秒数,存储这些数据使用了"long int"长整形变量,这样的一个变量是32位的,占4个字节,而发送时一次是发送一个字节,为了提高效率,首先对PPM数据进行"压缩",一个简单的办法就是直接除以1000,这样就等同于发送PPM的毫秒数,接收端用毫秒数据来播报电键音与原电键音相差仅是微秒级的,不影响效果,并且经过压缩,一个脉冲宽度数据就可以缩减为16位,即两个字节,这样极大地减少了发送的数据量,更重要的是:有效数据位实际不足16位,因为正常的点划输入,脉冲宽度最多达到0.45秒,即450毫秒,将"450"转换为二进制为:111000010,只占到了9位,这样一来给通讯协议的制定带来了方便之处。

采集到的PPM数据包括7个正脉冲和6个负脉冲,经过"压缩"后放在13个16位的变量中,然后将它们"拆"成26个字节,并且按照制定好的通讯协议"摆放"到数据帧中。整个数据帧的长度为30个字节,其中第"0"字节为"帧头"—0xf0,第"1"字节到第"26"字节存放PPM数据,"27"-"28"字节存放摩尔斯字码的二进制编码,最后一个字节存放"显示空格标志",当其值为0x01时不空格,0x03时插入空格。为了让数据字节中不出现"0xf0",在"拆"数据时作了一些处理。由于PPM数据经压缩后实际有效位只有9位, 因此可以将这个数据的"低7位"提取出来(&0x007f)放到一个字节中,剩下的"右移7位"(先&0xff80,后>>7)放到另一个字节中,这样一来就可以保证每个数据字节的最高位保持为"0"即不会出现与帧头重复的"0xf0",以保证接收端能够准确地解析数据。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(21)

数据拆分示意图

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(22)

数据帧协议示意图

数据处理流程:

1、 从外部中断采集电键输入的PPM序列,将采集到的正负脉宽依次放置于数组"in_code[]"中,in_code[]的数据首先被用于解析摩尔斯码的二进制编码,放置于16位变量mos_code中;

2、将in_code[]数组(正脉冲)及in_l_code[]数组(负脉冲)中的数据"压缩",依次放置在数组"val[]"中,正负脉冲的排列顺序按照PPM的时序排列;

3、将val[]数组中的各元素及mos_code进行拆分提取,放置于"s_date[]"数组中(从第1字节至第28字节),第0字节放置帧头0xf0,末尾放置空格标志。然后依次从串口发送数据。数据发送完毕后,清空所有数组,准备进行下一轮数据处理。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(23)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(24)

数据处理及发送代码

5. 串口接收数据

制定好了通讯协议,串口收到数据后就可以准确无误地进行数据解析了。在Arduino官方库中,可以用串口事件来接收处理数据,类似于串口中断的效果。为了提高系统的实时性,串口接收数据利用串口事件来处理。当串口收到数据后,先判断串口缓冲区的字节数是否满足30个字节,如果满足这个条件,则进行数据判读。进入数据判读,首先是要寻找"帧头-0xf0",找到它之后,紧随其后的29个字节就是我们需要的数据字节。整个接收处理过程就是发送的"逆过程"。首先是将"整理"好的30个字节依次放入"r_date[]"中,然后按照帧协议将第1-26字节的数据"合成",放入"r_val[]"数组中,这个就是发送端采集到的PPM脉宽数据,这个数据用于"一比一"地播放发送端的电键音。第27、28字节则"合成"摩尔斯码的二进制编码,29字节决定了当前收到的字符前面是否加入空格,最后在LCD屏幕的接收区显示。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(25)

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(26)

串口接收、处理数据代码

从以上所有代码中可以看到,电键采集以及数据接收都受到一个标志位"by_flag"的限制,这个标志为0时,可以正常收发报,当电键输入特定的功能码时,它会被置1时,系统会进入字码自定播报模式,即自动播报"A-B,0-9"的摩尔斯码,此时不响应任何操作,自动播报完毕后该标志位会清0,恢复到正常收发报模式。另外,系统还支持国际通用字码及数字短码模式的切换,这些切换都是通过电键输入特定的功能编码实现的。

arduino脉冲计数器教程(基于Arduino的摩尔斯电码练习及无线收发报训练器)(27)

摩尔斯码及系统功能码表

6. 结束语

该训练器目前能够满足发报训练的要求,可以让初学者的发报速度接近标准速度。但系统还有很多地方可以改进优化。1、对于过长的"划"输入(电键按下不放)会造成数据帧的数据位溢出,从而在解码端出现电键音"失真",目前还需要找到更好的方法来解决这个问题。2、在电键连续输入字码时,发报的速度需与系统的识别速度高度吻合,发报过快会出现输入错误提示,太慢又会加入空格,此问题还需进一步优化解决。3、目前系统只支持手动键输入,后续还需增加自动键识别功能。

,