CWキーヤーを作る(1)

ふとCWキーヤーを作ってみたくなりました。まずはソフトウェア的にどのような実装になるかを試してみたかったので、DitDahChat(実験版)に実装してみることにしました。次のXのポストに載せた動画のようになりました。シリアル経由でパドルも接続できます(DTR:コモン、DSR:短点、CTS:長点)。


このCWキーヤーは、こちらのリンクから試すことができます。 ditdah.jp

次はハードウェアで作ってみます。電子工作の初心者ですので、勉強もかねてできるところまで作っていければと思っています。

実装はJH7UBCさんとyukilaboさんの実装を参考にさせていただきました。

電子工作の初心者にはシングルボードマイクロコントローラーを使うのがよさそうです。今回はRaspberry Pi Pico 2を使い、MircoPythonで手軽にはじめてみます。

まずはCWキーヤーの短点と長点の生成とモニター機能の実装からです。ブレッドボード上に次のように配線してみました。

最初は短点と長点の入力にタクトスイッチを使うことにしました。短点はGPIO 16、長点はGPIO 17とします。

DIT_PIN = 16
DAH_PIN = 17

dit_pin = Pin(DIT_PIN, Pin.IN, Pin.PULL_UP)
dah_pin = Pin(DAH_PIN, Pin.IN, Pin.PULL_UP)


モニター用に次のブザーでサイドトーンを聴けるようにします。 akizukidenshi.com

ブザーにはGPIO 15を使うことにします。Buzzerは音の再生/停止を行うクラスです。実装は後ほど説明します。

BUZZER_PIN = 15

buzzer = Buzzer(PWM(Pin(BUZZER_PIN)))
buzzer.frequency = 440


CWキーヤーの本体はKeyerクラスで実装しています。こちらのクラスも後ほど説明します。

keyer = Keyer(emitter)
keyer.wpm = 20


引数のEmitterはキーをオン/オフするタイミングを通知するためのものです。こちらも実装は後ほど説明します。次のように初期化します。キーをオンするタイミングでonイベントが発生し、Buzzerクラスのonメソッドを呼び出してブザーを鳴らします。キーをオフするタイミングではoffイベントが発生し、Buzzerクラスのoffメソッドを呼び出してブザーを停止させます。

emitter = Emitter()
emitter.on("on", buzzer.on)
emitter.on("off", buzzer.off)


メインの処理は次のように短点と長点の押下状態を定期的にKeyerに渡してキーイングします。GPIOはプルアップしているので値を反転させています。

def loop():
    keyer.handler(not dit_pin.value(), not dah_pin.value())

def main():
    while True:
        loop()
        sleep(0.001)

if __name__ == "__main__":
    main()


Buzzerクラスの実装は次の通りです。MicroPythonのPWMを使ってパッシブブザーを制御します。

class Buzzer:
    def __init__(self, pwm):
        self._pwm = pwm
        self._frequency = 600

    def on(self):
        self._pwm.freq(self._frequency)
        self._pwm.duty_u16(32768)

    def off(self):
        self._pwm.duty_u16(0)

    @property
    def frequency(self):
        return self._frequency

    @frequency.setter
    def frequency(self, value):
        self._frequency = value


Emitterクラスの実装は次の通りです。onメソッドでイベント名とイベントハンドラーを登録します。イベントをemitメソッドで発生させます。

class Emitter:
    def __init__(self):
        self._handlers = {}

    def on(self, event, handler):
        self._handlers[event] = handler

    def off(self, event):
        if event in self._handlers:
            del self.handlers[event]

    def emit(self, event, data):
        if event in self._handlers:
            self._handlers[event]()


最後にCWキーヤーの主要な部分であるKeyerクラスの実装です。まずメインから呼び出されるハンドラー部分は次のようになります。

class Keyer:

    def handler(self, dit_on, dah_on):
        if dit_on:
            self._dit_mem_on = dit_on
        if dah_on:
            self._dah_mem_on = dah_on

        if ticks_ms() < self._t1:
            return

        self.next_state(dit_on, dah_on)
        self.action()


dit_onとdah_onは短点と長点のキーの入力状態です。このCWキーヤーはアイアンビック(Iambic)Bモードとして実装しています。短点を送出中に押された長点、長点を送出中に押された短点をそれぞれdah_mem_onとdit_mem_onに記憶しておき、次の点としてそれを送出するために使います。

次の箇所は短点/長点の送出とスペースの送出を指定された時間継続するためのものです。

        if ticks_ms() < self._t1:
            return


next_stateメソッドは短点/長点キーの入力状態や記憶しておいた点、現在の状態をもとに次の状態に遷移させるメソッドです。

STATE_INIT      = 0
STATE_DIT       = 1
STATE_DAH       = 2
STATE_DIT_SPACE = 3
STATE_DAH_SPACE = 4

class Keyer:
    def __init__(self, emitter):
        self._emitter = emitter
        self._dit_mem_on = False
        self._dah_mem_on = False
        self._state = STATE_INIT
        self._t0 = 0
        self._t1 = 0
        self._wpm = 20
        self._duration = 1200 / self._wpm

    def next_state(self, dit_on, dah_on):
        if self._state == STATE_INIT:
            if dit_on:
                self._state = STATE_DIT
            elif dah_on:
                self._state = STATE_DAH
        elif self._state == STATE_DAH_SPACE:
            self._state = STATE_DIT if self._dit_mem_on else STATE_INIT
            self._dit_mem_on = False
            self._dah_mem_on = False
        elif self._state == STATE_DIT_SPACE:
            self._state = STATE_DAH if self._dah_mem_on else STATE_INIT
            self._dit_mem_on = False
            self._dah_mem_on = False
        elif self._state == STATE_DIT:
            self._state = STATE_DIT_SPACE
        elif self._state == STATE_DAH:
            self._state = STATE_DAH_SPACE
        elif self._state == STATE_DIT_SPACE:
            self._state = STATE_INIT
        elif self._state == STATE_DAH_SPACE:
            self._state = STATE_INIT


状態は初期状態(INIT)、短点送出(DIT)、長点送出(DAH)の他に、短点後のスペース(DIT_SPACE)、長点後のスペース(DAH_SPACE)を設けています。これはアイアンビックBモードに必要な短点中の長点、長点中の短点を扱うためのものです。

actionメソッドではnext_stateで求めた状態に応じた処理を行います。

    def action(self):
        d = 0

        if self._state == STATE_DIT:
            self._emitter.emit("on", None)
            d = self._duration
        elif self._state == STATE_DAH:
            self._emitter.emit("on", None)
            d = self._duration * 3
        elif self._state == STATE_DIT_SPACE or self._state == STATE_DAH_SPACE:
            self._emitter.emit("off", None)
            d = self._duration

        self._t0 = ticks_ms()
        self._t1 = self._t0 + d

短点(DIT)と長点(DAH)のときはonイベントを発生させてメイン側でブザーを鳴らします。スペースのときはoffイベントを発生させてブザーを停止させます。アクションを停止させる時間をt1に記憶しておき、前述の短点/長点またはスペースの送出の継続時間の判定に使います。


今後はパドルの接続、無線機のキーイング、送出速度などのパラメーターの変更などをハードウェアで実現できたらと考えています。

JI1JDI

ゆるく楽しくアマチュア無線とプログラミングを楽しんでいます。 scrapbox.io