ふとCWキーヤーを作ってみたくなりました。まずはソフトウェア的にどのような実装になるかを試してみたかったので、DitDahChat(実験版)に実装してみることにしました。次のXのポストに載せた動画のようになりました。シリアル経由でパドルも接続できます(DTR:コモン、DSR:短点、CTS:長点)。
新しいDitDahChatにエレキーを付けてみました
— DitDah Lab (@ditdahlab) 2025年6月12日
遅延が大きいので速度が速くなると実用にならないと思いますが
シリアル経由でパドルも接続できます(DTR:コモン、DSR:短点、CTS:長点) pic.twitter.com/aXSFDyumqX
この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