diff --git a/channel.py b/channel.py index 34bf4db..b895a77 100644 --- a/channel.py +++ b/channel.py @@ -16,6 +16,12 @@ pilot_value = 1 + 1j snr_db = 300 def sim(in_data): + """ + Simulate effects of communication channel. + + Convolves the channel response, and adds random noise to the signal. + """ + out_data = np.ndarray((len(in_data), len(in_data[0])), dtype=np.csingle) # noise stuff is straight copied from the DSP illustrations article @@ -34,6 +40,32 @@ def sim(in_data): # Again, most of this is stolen from the guide # this sort of stuff I had no idea about before I read the guide def estimate(in_data, pilots=0): + """ + Estimate channel effects by some cool properties. + + Since the effects of the channel medium is a convolution of the transmitted signal, + we can abuse this for some easy math. + + We take the time domain input signal and turn that into a frequency domain signal. + If we had a perfect channel, this frequency domain signal would exactly equal the signal + transmitted. But it doesn't. However, we are in the frequency domain, which means + convolution turns into multiplication, and to find the effect of the channel on each + subcarrier, we can simply use division. + + Unfortunately, we don't know what the original data was, so we use "pilot" subchannels, which transmit + known information. We can use this to get estimates for each of the pilot carriers, and finally, we can interpolate + these values to get an estimate for everything. + + There are a few issues with this method: + 1: It is very estimate-ey. We have to interpolate from a subset of the carriers. + 2: It is quite inefficient. We are sending useless information on every symbol. + + I think a better solution is to send a known symbol (or a set of known symbols) + at the beginnning of each transmission instead. This means we get a full channel estimate + every time. This also has the advantage of being able to synchronise the symbols. Since I + will be implementing some sort of protocol anyways, I think this will be a good idea. As well, + we move slow enough that the channel will not likely change significantly over a single packet. + """ all_carriers = np.arange(len(in_data[0])) diff --git a/main.py b/main.py index 66840a9..2ee78bc 100755 --- a/main.py +++ b/main.py @@ -7,7 +7,6 @@ Hopefully eventually this modem design makes it onto an fpga. TODO: Change channel estimation to pre-amble symbols Add comments for functions - Explain what the main function is doing Add more errors, like a shifted signal Add support for 16-QAM, 64-QAM, etc... Add some sort of payload support, i.e. be able to drop the padding at the end @@ -29,6 +28,14 @@ import qam from serpar import parallelise, serialise def cp_add(in_data, prefix_len): + """ + Adds a cyclic prefix to an array of symbols, with a specified length. + + This changes the linear convolution of the data into a circular convolution, + allowing easier equalization. + As well, it helps remove inter-symbol interference. + """ + out_data = np.ndarray((len(in_data), len(in_data[0]) + prefix_len), dtype=np.csingle) for i in range(len(in_data)): @@ -38,6 +45,10 @@ def cp_add(in_data, prefix_len): return out_data def cp_remove(in_data, prefix_len): + """ + Removes cyclic prefix from retrieved data. Naively assumes that data is correctly aligned. + """ + out_data = np.ndarray((len(in_data), len(in_data[0]) - prefix_len), dtype=np.csingle) for i in range(len(in_data)): @@ -54,25 +65,36 @@ if __name__ == '__main__': bytes = bytearray(data, 'utf8') + # Turn data into a parallelised form, able to be QAM-modulated parallel = parallelise(64, bytes) + # modulate data with a QAM scheme modulated = qam.modulate(parallel, pilots=20) + # Run IFFT to get a time-domain signal to send ofdm_time = np.fft.ifft(modulated) + # Add cyclic prefix to each symbol tx = cp_add(ofdm_time, 16) + # Simulate effects of a multipath channel rx = channel.sim(tx) + # Remove cyclic prefix from incoming symbols ofdm_cp_removed = cp_remove(rx, 16) + # Bring symbols back into frequency domain to get carrier channels to_equalize = np.fft.fft(ofdm_cp_removed) + # Find an estimate for channel effect H_est = channel.estimate(to_equalize, pilots=20) + # Equalise based on estimated channel to_decode = channel.equalize(to_equalize, H_est) + # Demodulate symbol into output data to_serialise = qam.demodulate(to_decode, pilots=20) + # Turn data back into string data = serialise(64, to_serialise) print(data) diff --git a/qam.py b/qam.py index 2871f59..7ce7a9b 100644 --- a/qam.py +++ b/qam.py @@ -16,6 +16,10 @@ def modulate(in_data, pilots=0): """ Modulates into 4-QAM encoding, might change that number later. + This turns input data into a "constellation" of complex numbers, + ready to be fed into an IFFT. This constellation could also be used + directly with an IQ modulator. + Parameters: in_data - m X n array, m symbols to run on pilots (optional) - number of pilot signals to intersperse into carriers @@ -54,6 +58,10 @@ def modulate(in_data, pilots=0): return out_data def demodulate(in_data, pilots=0): + """ + Demodulates incoming signal. + """ + all_carriers = np.arange(len(in_data[0]), dtype=int) if pilots > 0: