Modulation and generation of the AFSK signal is a fairly straight forward process. Working bottom up:
First step is to generate the
If we represent mark as +1 and space as -1, we can modulate the
Direct evaluation with Mathematica yields
Great!
A key goal of this project is performance on embedded systems lacking a floating point unit without access to purpose math libraries (eg. numpy). We utiluze integer only math combined with look-up tables (eg. sinusoidal generation).
First we will need a sin look-up table. From afsk/sin_table.py, we can easily generate an array (of type array such that we know the bytes are sequentially addressed as in a c-style array). The function may be memoized to file.
return array('h', (int((2**15-1)*math.sin(x)) for x in frange(0,2*math.pi,2*math.pi/size)))
The look-up table provides the values for the waveform
- Where the phase [0,PI) is mapped to look-up table phase indexes [0,1024)
- Where the frequency is determined by iterative step size.
To reproduce a mark tone (1200 Hz) at a sampling rate of 22050 Hz, with a lookup table of size 1024, the index step size is:
While this wouldn't normally be a problem if we are solving sin directly with floating point math, because we are using integer math, rounding
In our case, we pick a residue size of 10000, which leads us with a residue of 7279 per iteration (sampling rate step.)
In implementation, we will accumulate the phase (look-up table index) by 55 each iteration. In a seperate residue accumulator, we increment by the residue accumulator by 7279. The residue accumulator cycles with modulous 10000, incrementing phase index each time the accumulator wraps around. In code:
mark_step_int = 55
mark_residue = 7279
residue_size = 10000
markspace_index = 0 # the phase accumulator
markspace_residue_accumulator = 0
for t in range(tsim_array_size):
# append the sample from the lookup table,
r.append(sin_array[markspace_index%lookup_size])
# advanced the phase by the integer phase step size
markspace_index += mark_step_int
# advance the phase residue accumulator
markspace_residue_accumulator += mark_residue
# update the phase/residue accumulators if we exceed the residue size
markspace_index += markspace_residue_accumulator // residue_size
markspace_residue_accumulator %= residue_size
With this scheme, we are able to create the desired waveform, matching exact as well as floating point implementations.
Each bit will need to include one baud period's worth of samples. Similar to mark and space, you will need to track the phase and residue to account for the correct number of samples per bit. By retaining the phase and residue between adjacent mark/space bits, the output is a continous waveform.