The overall goal of this project is to create an APRS beacon device using the HamWing carrier board by W8LID.
To do this, this project implements an AFSK "modem" via a low-pass filtered PWM signal. I'm using an Adafruit Feather RP2040 board as the microcontroller in this project. The sine wave is created by applying PWM output to a 3.1 kHz low-pass filter. Via software it is able to produce 2-FSK at 1200 bps using Bell 202 (-ish) 1200 Hz/2200 Hz tones.
Higher-level features are near the top lower-level are near the bottom. The hardware PWM at 1MHz is driven by a 64-sample wave table that alters the duty cycle from 0 to 100%.
- Sampling period The voltage generated by the PWM wave in the interval between two interrupts will be a constant value and this time period can be called sampling period.
- PWM cycle The duty cycle of the PWM, this is equivalent to the PWM off time + PWM on time. There will be many PWM cycles per sampling period.
The duty cycle of the PWM is increased in a sinusoidal way (e.g. 5%, 10%, 25%, etc.) at each sampling time (timer interrupt/callback).
The RP2040 has a built-in PWM peripheral that uses the system clock at full speed (no dividers). This yields about 125 MHz to work with. The PWM peripheral is controlled by two variables:
COUNTER_TOP
16-bit value that controls when the auto-incrementing counter rolls over. The PWM frequency then is given by:
Note: If you look at the datasheet, I'm not using phase-correct PWM nor am I using any divisors, so other values in the denominator are 1.
pwm_level
is the duty cycle of the PWM signal.
When the auto-incrementing counter reaches pwm_level
, the PWM output goes low.
pwm_level
is scaled to COUNTER_TOP
, so that COUNTER_TOP/2
yields a 50% duty cycle.
wav
is a table of 64 8-bit samples of an ideal sine wave.
This is what will make up our actual output signal.
Now we have to deal with generating the audio signal.
This involves determining the correct sampling period mentioned above.
We have 64 samples for our sine wave.
The higher frequency in Bell 202 is, zero (0) or space (as in mark and space) and it is 2,200 Hz.
We have to "play back" 64 samples per cycle, which happens 2,200 times per second.
If
Our interrupt should happen every 7 microseconds or so.
It's annoying to try to nail that frequency exactly. The Pico can easily set periodic tasks (interrupts) based on a whole number of microseconds. It's not clear to me right now how I'd directly set an interrupt to go off each 7.102 us. So, enter Direct Digital Synthesis. It took me a bit to grok the idea but it's roughly, subdivide a convenient periodic interrupt interval into many steps. Then on each interrupt an appropriate number of these smaller units are added into a phase accumulator so that overall (on average) the code steps through the sample table at the correct rate. If the frequency should be higher, add more steps on each interrupt. To go slower, add fewer steps each time.
To put numbers to it, I want to know how many steps I should increment for each interrupt.
I'm aiming for a 2,200Hz (f) sound, 64 samples (N), a 20us interrupt period (
To derive variables for the mark tone (representing a binary 1) of 1,200 Hz we can run through similar math.
Then, by similar reasoning as above, I came up with:
Higher level protocol details follow.
Non-return to zero-inverted is an encoding scheme that looks like the following:
During each period, if there is a one then the signal transitions whether or not it was high or low before. If the bit is a zero, then there is no transition.