>

做了个PSK31 Beacon

2017-04-12 by Stavros

psk31 是业余无线电爱好者常用的通讯方式. psk31 的原理就不介绍了, 见链接: 哈罗 CQ 火腿社区 - HAM 软件、HAM 网站 - PSK31 簡介 BV3FG - Powered by phpwind

psk31 的调制如何具体实现呢? HAM 们常用的方式是用 digipan 等软件把要发送的消息调制到几百 Hz 的音频信号上, 再利用发射机的 SSB 模式把这个音频信号发射出去, 这里 SSB 实际上起了上变频的作用.

也可以把整个调制过程都放在 MCU 里进行, 直接从 MCU 的 DAC 输出调制好的音频信号.

ka7oei 则提供了一个偏硬件的直接调制方案, 见下图:

左下的振荡器振荡在所需频率的 4 倍上, 由计数器 74HC4017 接成四分频, 从 0 和 2 两脚输出一对相位相反的载波信号. MCU 的 PB2 通过 74HC00 选择相位. PA3 输出的 PWM 信号经低通滤波后, 再经 U2, Q4 调节 Q3 的漏极电压, 实现在输出 0 时将输出信号幅度逐渐降到零的瞬间切换相位. 这个方案很简洁, 我决定就拿这个图稍微改动一下. MCU 么, 用最简单的 ATTiny13 就好了.

首先得在 PC 上软件模拟一下. 程序很简单, 找个 psk31 的码表, 一般是类似这样:

const uint16_t VARICODE_TABLE[] = {
    0x8000,    /* ASCII = ' '    1                             */
    0xFF80,    /* ASCII = '!'    111111111                     */
    0xAF80,    /* ASCII = '"'    101011111                     */
    0xFA80,    /* ASCII = '#'    111110101                     */
...
    0xA000,    /* ASCII = 't'    101                           */
    0xDC00,    /* ASCII = 'u'    110111                        */
    0xF600,    /* ASCII = 'v'    1111011                       */
    0xD600,    /* ASCII = 'w'    1101011                       */
    0xDF00,    /* ASCII = 'x'    11011111                      */
    0xBA00,    /* ASCII = 'y'    1011101                       */
    0xEA80,    /* ASCII = 'z'    111010101                     */
};

存储格式是从高位到低位, 后面补零. 注意这个码表是从空格开始, 到小写 z 就结束了, 也就是 ASCII 码从 32 到 122 这 91 个字符, 这样能省下不少宝贵的 flash 空间. 毕竟 ATTiny13 的 flash 空间只有可怜的 1024 字节, 如果把完整的码表放进来, 一下子 512 字节就没有了. 发送一个字符的函数可以写成这样:

void psk31_tx(char c)
{
    uint16_t varc;
    for(varc = VARICODE_TABLE[c - ' ']; varc; varc <<= 1) {
        if(varc & 0x8000)
            printf("1");
        else
            printf("0");
    }
    printf("00");
}

然后随便找个字符串, 比如 Hello, world.

char msg[] = "Hello, world.";
char *p = msg;
while(*p) {
     psk31_tx(*p);
     p++;
}

编译运行, 就能看到效果了.

在 MCU 上实现时要稍微复杂一点. 发送 1 好办, 保持满幅输出不变, 挨过 32ms 时间就完事. 发送 0 就复杂一些, 需要按余弦函数把输出幅度逐渐减小为零, 切换相位, 再把幅度逐渐增加到最大. 把 printf("1") 和 printf("0") 分别换成这两个操作就行. 但是这样就破坏了可移植性. 解决办法就是函数指针, 把 psk31_tx 函数写成这样:

void psk31_tx(char c, void (*tx0)(void), void (*tx1)(void))
{
    unsigned short varc;
    for(varc = varicode_table[(c - ' ')]; varc; varc <<= 1) {
        if(varc & 0x8000)
            (*tx1)();
        else
            (*tx0)();
    }
    (*tx0)();
    (*tx0)();
}

把发送 1 和 0 的操作写成两个回调函数传给 psk31_tx(), 这样就完美了.

之后设计电路, 如图:

振荡这里用一只 74AHC1G00 加上 14.318M 的晶振就行了, 通过调节 KV1471 的偏压来微调频率. 四分频用了更常用的 74HC74, 两级二分频, 从第二级的 Q 和 / Q 脚输出一对相位相反的载波信号. 切换相位用一只模拟开关 74LVC1G3157 来实现. 其余部分基本保持 ka7oei 的设计不变.

成品照片:

末级电源电压这里只用到 5V, 实测连续发送 1 时在 50 欧假负载上可以得到 40mW / +16dBm 的功率. 如果把末级供电电压提到 12V, 同时把 MMBT2222 和 2N7002 换成封装大一点的管子, 估计能做到 1W 上下, 再大就得加后级了.

ATTiny13 的一大缺点是不能接晶振, 于是把它的 RC 时钟设到 4.8M, 通过 OSCCAL 寄存器微调到 4.096M 左右, 然后 8 分频作为定时器 0 的时钟源, 定时器 0 输出 PWM 的同时也提供了 4.096M / 8 / 256 = 2kHz 的时基信号. 余弦波形表用了 64 个点, 从而得到 2k / 64 = 31.25Hz. 输出 1 时只要什么都不做, 等定时器 0 中断 64 次就行了.

ATTiny13 的另一大缺点... 就是 RAM 只有可怜的 64 字节. 实测发现即使发送 "Hello, world." 也一样爆栈, 只好不用回调机制, 同时把 "Hello, world." 也放到 flash, 才总算是发送成功.

用 IC-R71E 接收, Digipan 和 MultiPSK 解调的效果图如下:

Digipan

MultiPSK

感觉 Multipsk 的解调效果要好一些, 可能是 Digipan 那边有些参数没设置好.

在这个 Beacon 的基础上可以玩出很多花样来, 看你怎么发挥了.