diff --git a/README.md b/README.md index eb68e95..32af2d8 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,4 @@ https://en.wikipedia.org/wiki/WSPR_(amateur_radio_software) - si5351 - SX126X +- PWM pin (limited range, should be used with a appropriate low-pass filter) diff --git a/examples/morse/featherwing.go b/examples/morse/featherwing.go index e88c0b5..8ce2172 100644 --- a/examples/morse/featherwing.go +++ b/examples/morse/featherwing.go @@ -40,7 +40,7 @@ func initRadio() *morse.Morse { println("setting OOK modulation") dev.SetModulationType(sx127x.SX127X_OPMODE_MODULATION_OOK) - m := morse.NewMorse(&sx127xRadio{device: dev}, 11_400, 20) + m := morse.NewMorse(&sx127xRadio{device: dev}, 540_000, 20) m.Configure() return m diff --git a/examples/morse/go.mod b/examples/morse/go.mod index 098a77d..73e87a2 100644 --- a/examples/morse/go.mod +++ b/examples/morse/go.mod @@ -7,6 +7,7 @@ replace tinygo.org/x/wireless => ../.. replace tinygo.org/x/wireless/examples/audio => ../audio require ( + github.com/chewxy/math32 v1.11.1 tinygo.org/x/drivers v0.34.1-0.20260107195827-c21cd39813be tinygo.org/x/wireless v0.0.0-20260121153201-f0d8647de68c tinygo.org/x/wireless/examples/audio v0.0.0-00010101000000-000000000000 diff --git a/examples/morse/go.sum b/examples/morse/go.sum index 347ce9b..30fedf1 100644 --- a/examples/morse/go.sum +++ b/examples/morse/go.sum @@ -1,3 +1,5 @@ +github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= +github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/oto/v3 v3.1.0 h1:9tChG6rizyeR2w3vsygTTTVVJ9QMMyu00m2yBOCch6U= diff --git a/examples/morse/main.go b/examples/morse/main.go index 84f4956..ed44302 100644 --- a/examples/morse/main.go +++ b/examples/morse/main.go @@ -1,8 +1,9 @@ // FSK4 modem example // -// tinygo flash -size short -tags=si5351 -target=pico -monitor ./examples/morse -// tinygo flash -size short -tags=featherwing -target=pybadge -monitor ./examples/morse -// go run ./examples/morse +// tinygo flash -size short -tags=si5351 -target=pico -monitor . +// tinygo flash -size short -tags=featherwing -target=pybadge -monitor . +// tinygo flash -size short -tags=pwm -target=pico -monitor . +// go run . package main import ( @@ -27,7 +28,7 @@ func main() { println("Transmitting data:", message) radio.Write(message) - time.Sleep(10 * time.Second) + time.Sleep(5 * time.Second) } time.Sleep(2 * time.Second) diff --git a/examples/morse/noradio.go b/examples/morse/noradio.go index 1d45deb..c885cf3 100644 --- a/examples/morse/noradio.go +++ b/examples/morse/noradio.go @@ -1,4 +1,4 @@ -//go:build !si5351 && !featherwing +//go:build !si5351 && !featherwing && !pwm package main diff --git a/examples/morse/pwm.go b/examples/morse/pwm.go new file mode 100644 index 0000000..b1b67d1 --- /dev/null +++ b/examples/morse/pwm.go @@ -0,0 +1,107 @@ +//go:build pwm + +package main + +import ( + "machine" + "time" + + "github.com/chewxy/math32" + "tinygo.org/x/wireless/morse" +) + +var ( + pwm = machine.PWM0 + pin = machine.GPIO16 + sampleRate float32 = 11_025.0 // suitable for AM radio +) + +func initRadio() *morse.Morse { + err := pwm.Configure(machine.PWMConfig{ + Period: 1854, // 1854 ns, approx 540 kHz + }) + + transmitChannel, err := pwm.Channel(pin) + if err != nil { + println("failed to configure channel") + return nil + } + + samples := make([]uint32, 512) + generateSineWave(samples, 440, sampleRate) + + m := morse.NewMorse(&PinRadio{transmitChannel: transmitChannel, samples: samples}, 540_000, 5) + m.Configure() + + return m +} + +// PinRadio implements a simple radio using PWM on a GPIO pin. +// +// It uses PWM to generate an audio tone that is transmitted using amplitude +// modulation (AM). +// +// NOTE: You should use a low-pass filter connected to the output pin to get +// a cleaner signal and avoid harmonics that will cause interference for both +// yourself and others. +// +// See https://github.com/tudbut/picoAM for the original inspiration. +type PinRadio struct { + transmitChannel uint8 + samples []uint32 + stopChan chan struct{} +} + +func (r *PinRadio) Transmit(freq uint64) error { + r.stop() + r.stopChan = make(chan struct{}) + + go func(stop <-chan struct{}) { + for { + select { + case <-stop: + return + default: + } + for _, v := range r.samples { + pwm.Set(r.transmitChannel, v) + time.Sleep(time.Duration(1e9/sampleRate) * time.Nanosecond) + } + } + }(r.stopChan) + + return nil +} + +func (r *PinRadio) Standby() error { + r.stop() + pwm.Set(r.transmitChannel, 0) + + return nil +} + +func (r *PinRadio) Close() error { + r.stop() + pwm.Set(r.transmitChannel, 0) + + return nil +} + +func (r *PinRadio) stop() { + if r.stopChan != nil { + close(r.stopChan) + r.stopChan = nil + } +} + +func generateSineWave(samples []uint32, freq uint64, sampleRate float32) { + top := pwm.Top() + min := top / 4 + max := top + + for i := range samples { + v := math32.Sin(2.0 * math32.Pi * float32(i) * float32(freq) / sampleRate) + v = (v + 1.0) / 2.0 // shift to [0, 1] + samples[i] = uint32(float32(min) + v*float32(max-min)) + } +} diff --git a/examples/morse/si5351.go b/examples/morse/si5351.go index 2ec4004..634e1e1 100644 --- a/examples/morse/si5351.go +++ b/examples/morse/si5351.go @@ -18,7 +18,7 @@ func initRadio() *morse.Morse { panic(err) } - m := morse.NewMorse(&Si5351Radio{device: dev}, 11_400, 20) + m := morse.NewMorse(&Si5351Radio{device: dev}, 540_000, 5) m.Configure() return m