Skip to content

feature: add PWM pin transmitter to the Morse code example#11

Open
deadprogram wants to merge 1 commit intomainfrom
morse-pinradio
Open

feature: add PWM pin transmitter to the Morse code example#11
deadprogram wants to merge 1 commit intomainfrom
morse-pinradio

Conversation

@deadprogram
Copy link
Member

@deadprogram deadprogram commented Jan 26, 2026

This PR adds a PWM pin transmitter to the Morse code example.

It uses amplitude modulation to transmit an audible 1200Hz tone on the broadcast frequency of 540KHz.

Comment on lines 101 to 106
func generateSineWave(samples []float32, freq uint64, sampleRate float64) {
for i := range samples {
v := float32(math.Sin(2.0 * math.Pi * float64(i) * float64(freq) / sampleRate))
samples[i] = (v + 1.0) / 2.0 // shift to [0, 1]
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the frequency, number of samples and sampleRate this function would generate different number of sine wave periods, and the last sample would not always be at the end of the wave period so when sending samples continuously this will potentially create big phase shifts. Should we instead generate samples for single (or multiple) sine wave period and store them in samples. That way, instead of defining sample rate, we would define number of samples per sine wave period and make samples array of that size (or multiple of that size) and store it in PinRadio. Also, instead of storing values from 0 to 1 as samples, we could directly calculate and store duty values for the PWM (of type uint32) so they don't need to be calculated each time.

Suggested change
func generateSineWave(samples []float32, freq uint64, sampleRate float64) {
for i := range samples {
v := float32(math.Sin(2.0 * math.Pi * float64(i) * float64(freq) / sampleRate))
samples[i] = (v + 1.0) / 2.0 // shift to [0, 1]
}
}
func generateSineWave(samples []uint32) {
top := pwm.Top()
min := top / 4
max := top
for i := range samples {
v := float32(math.Sin(2.0 * math.Pi * float64(i) / float64(len(samples))))
v = (v + 1.0) / 2.0 // shift to [0, 1]
samples[i] = uint32(float32(min) + v*float32(max-min))
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not what I want, however. The purpose is to generate an audible sine wave at a specific frequency (440Hz in this case), and then to transmit it over a different transmit frequency (540Khz) using amplitude modulation so it can be heard using an standard AM radio, or even a crystal radio.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deadprogram so If understand it correctly, you want 540Khz carrier frequency to be modulated with 440Hz sine wave signal that is modulated as morse code? If this is the case, I still don't see how is pwm generating 540Khz carrier frequency?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that is thanks to this: https://github.com/tinygo-org/wireless/blob/morse-pinradio/examples/morse/pwm.go#L21

So you are using PWM switching interval as a carrier frequency. I will have to get a refresher on my telecommunications theory, but it looks to me that you will need some specific band filter for it to work (or live with a lot of harmonics :) ).

This PWM period is defined independently of morse.NewMorse frequency parameter, and also samples are generated independent of Transmit frequency parameter so they are still ignored (which could be fine for the example purposes)

Regarding phase shift issue, I ploted the generated waveform from the example to clarify my point
image
as you can see, the phase at the beginning of the generated waveform is different than the one at the end, and since this waveform is played in the loop, there will be phase shift in each cycle:
image

That is why I suggested that generate function generates one (or multiple) complete sine waves.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks to me that you will need some specific band filter for it to work (or live with a lot of harmonics

That is true, hence the comment here: https://github.com/tinygo-org/wireless/pull/11/changes#diff-e9349dfe6339ee0a21f0420ebf324ce17c305b5ebb4888cd4e0233cd57bf7890R44

Regarding the phase, I think if I change the size of samples to some semi-clean division of sampleRate such as 344 the phase should mostly sync up.

stopChan chan struct{}
}

func (r *PinRadio) Transmit(freq uint64) error {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frequency in morse constructor (540_000) is ignored, and also the frequency argument to Transmit function. If we change generateSineWave like I suggested in other comment, we could calculate sample duration on each Transmit call (or only if the frequency changes) and use that duration for sleep between samples.

r.sampleDuration = time.Duration(float64(time.Second) / float64(freq) / float64(len(r.samples)))

Comment on lines 66 to 73
for _, v := range r.samples {
top := pwm.Top()
min := top / 4
max := top
duty := uint32(float32(min) + v*float32(max-min))
pwm.Set(r.transmitChannel, duty)
time.Sleep(time.Duration(1e9/sampleRate) * time.Nanosecond)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop would look like this:

Suggested change
for _, v := range r.samples {
top := pwm.Top()
min := top / 4
max := top
duty := uint32(float32(min) + v*float32(max-min))
pwm.Set(r.transmitChannel, duty)
time.Sleep(time.Duration(1e9/sampleRate) * time.Nanosecond)
}
for _, v := range r.samples {
r.pwm.Set(r.transmitChannel, v)
time.Sleep(r.sampleDuration)
}

Signed-off-by: deadprogram <ron@hybridgroup.com>
@deadprogram
Copy link
Member Author

@HattoriHanzo031 I did like some of your ideas on how to refactor the code, even if I still need to generate an audio tone, I think it is better now. Please take a look.

r.stop()
r.stopChan = make(chan struct{})

go func(stop <-chan struct{}) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead spinning a goroutine on each Transmit call, just spin one goroutine on PinRadio init (or on first Transmit call) and then just send it start and stop signals over the channel

samples := make([]uint32, 512)
generateSineWave(samples, 440, sampleRate)

m := morse.NewMorse(&PinRadio{transmitChannel: transmitChannel, samples: samples}, 540_000, 5)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think morse protocol should not be configured with carrier frequency (540kHz) but instead with frequency that is modulated by digital signal (440Hz in this case) like it is the case with other radio types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants