Random Quote Board
Recovering the GPS Coarse Acquisition Code in Gnu Radio Companion
This is going to be a lot of information. Frankly, if this makes it as one post, I'll be surprised. It will probably take two. Maybe three.
Let's start with a bit of background: I just finished attending Dan Boschen's Digital Signal Processing for Wireless Communications class. One of the key takeaways from the class is that "correlation is key to understanding a lot of signal processing". (Sidenote: I wholeheartedly agree with this statement. )
ANYWAY, one of the examples he used was the coarse acquisition of the GPS, aka "the C/A code". This is the correlation of a chip stream with the signal from the GPS satellites. I decided to see if I could create a correlator in Gnu Radio Companion (GRC), then correlate it to actual GPS signals. Mr. Boschen even provided a small snippet (20 msec) of a GPS signal.
GPS Overview
GPS is a set of 24 active satellites in orbit around the Earth. Those 24 satellites are constantly transmitting a set of different signals on a few frequencies. The one signal with which we are interested is the C/A code, transmitted on the L1 frequency of 1.57542 GHz. (NOTE: The base clock frequency for GPS is 10.23 MHz. Pretty much everything is derived from this. The L1 frequency is 10.23 MHz x 154. In other words, it is the 154th harmonic of the base clock frequency.) The GPS works on direct sequence spread spectrum. This means that it uses "spreading codes" to spread the energy over a larger amount of spectrum than it would otherwise require. When talking about such things, we don't talk about "bits" but "chips". Just think of a "chip" as "a bit that is being used to spread in frequency another bit sequence".
Receiving the GPS Signal
Look, spread spectrum is an entire field unto itself. We're talking math of an insane order. Euler-phi functions and totients and maximal length and stages and states and primitive polynomials and... you get the idea. What I'm going to cover here are the following:
- Generating the C/A spreading codes.
- Correlating the spreading code with the GPS signal (the one provided in the class).
- Collecting GPS signals using basic SDRs.
- Synchronizing the code with the signal in order to recover the data.
Frankly, I think I'll be able to accomplish the first three. The fourth is going to be... iffy.
Let's get started.
Creating the Spreading Code
Let's look at what we need to do. Each satellite transmits its own C/A code. They're all derived from the same circuit, however. The GPS specification provides a nice diagram, shown below. First thing I had to figure out was could I actually create this circuit in Gnu Radio Companion (GRC). Without this, why bother with the rest?
For my circuit, I will need the following:
- Two linear feedback shift registers (LFSR).
- That are synchronized.
- And that I can tap off of two stages from one of them.
Creating the Linear Feedback Shift Register
There are two, general ways to create LFSRs. These are with Fibonacci shift registers or with Galois field shift register. The diagram shown above uses the setup for a Fibonacci-based linear shift register.
If you've read any of my posts on Gnu Radio, I prefer to create circuits using basic components. A LFSR would use a bunch of "Delay" blocks and one or more "XOR" blocks. It would actually be straightforward. But it would also require using feedback from the last delay block back into at least one of the other blocks (depending on the type of LFSR). Such feedback is not allowed in Gnu Radio flowgraphs. The only option is a GRC block called the "GLFSR", which stands for "Galois Linear Feedback Shift Register".
Synchronizing the LFSRs
This is a "good news / bad news" situation. Let's start with the good news. With the same set of feedback taps, both a Fibonacci LFSR and a Galois LFSR will output the same sequence. Even though we don't have a Fibonacci-based generator, we can still create the necessary sequences with the Galois block in GRC based on the provided feedback taps.
Just to make sure that the sequences were correct, I calculated the first couple of dozen chips for both the G1 and G2 generators. For the G1, I took out a couple of pieces of scrap notebook paper (I keep a large pile under my desk for just such an emergency.) and, by hand, calculated what the first 20-ish chips or so should be.
I repeated the same thing for the G2 generator. For this, I created a Libreoffice Calc page to do the calculations. (Full disclosure: I did it by hand at first, but due to the number of feedback taps, I made a lot of mistakes. I created the Calc page out of frustration. Turns out it was much easier.)
Using the same feedback taps ("Mask") as stated for the G1 and G2 generators in the GLFSR block means we should get these sequences.
Now comes the bad news. They will not start at the same point in the sequence. The two generators, G1 and G2, need to be synchronized. Given an initial fill and the fact that they're based on Fibonacci-based generators, the first 10 chip output from each should be 10 1s. A 10-stage Galois-based generator will have an output of the first 10 chips of... who knows? If I can figure out where the 10 1s occur in the Galois-based output, I can use a Delay to shift the sequence to that point. But how do I figure that out? I could look for some math somewhere that might tell me, or I could just create a flowgraph and measure it.
I think I'll do the latter.
I started with the G1 generator. I created a simple flowgraph that takes the output of the GLFSR with the requisite number of taps ("Degree", in the block parameters), the correct feedback taps ("Mask" in the parameters) and initialization vector ("Seed" in the parameters). According to the specification, this LFSR should be maximal length, meaning that it will run for 210-1=1023 chips before it starts to repeat. I used a "QT GUI Time Sink" to look at the output. I set the time sink to a length of 1023 and a sample rate of 1. (NOTE: By setting the sample rate to "1" and ignoring the units, any measured values will simply be "chips" or "bit" numbers.) If the chip sequence coming out of the "GLFSR Source" block was, indeed, maximal length, then the chip sequence should appear static in the time sink. The following are the flowgraph and the result when I ran it.
The time sink display was static when I ran the flowgraph. So this shift register is maximal length (1023 chips). Now I need to get the sequence aligned so that the first 10 chips are all 1s (the same as the initial fill). Zooming in on the time sink, I can see that the sequence in the time sink actually starts within the initial fill, with 7 1s before the beginning (meaning that they show up at the very end of the time sink) and 3 1s at the beginning of the time sink. Adding a "Delay" at the output of the GLFSR will allow me to shift the sequence to the right. Thus, by using a delay of 7 samples, the sequence should start precisely at the beginning of the initial fill just as I need it.
Okay, that worked. I now have the G1 generator creating the code I need and at the correct point in time. On to the second generator, G2. Here's the flowgraph and results using the same method as for the G1.
The G2 sequence is now aligned properly and it matches the sequence as generated in the Calc page.
So that works. Which is nice.
Full GPS C/A Generator
We now have the two chip sequences, G1 and G2, and they are synchronized. Next problem, as stated in the specification:
The effective delay of the G2 sequence to form the G2i sequence is accomplished by combining the output of two stages of the G2 shift register by Modulo-2 addition
In other words, I need to tap off two of the 10 stages from the G2 LFSR. The purpose is to create yet another sequence. Tapping and XORing two registers gives us a certain sequence. Regardless of the pairs selected, the sequence will be identical, only shifted in phase.
But I can't access the G2 generator registers directly. The "GLFSR Source" block does not provide for that. So, Plan B. Run the output through a set of 10 delays and treat these delays as the stages of the G2 LFSR. This should at least give me the access to the correct registers in the correct order to create the XOR of the G2 register pair. Note that this only works since the specification uses a Fibonacci-based generator. It wouldn't work if the original specification said, "Use a Galois-based generator". This is because the Fibonacci-based generator dumps the XORed chips into the first stage. They don't change from there until the output. Therefore, using 10 "Delay" blocks should give me the exact, same sequence as if I'd been able to tap directly off of the linear shift register.
The delays give me access to the various stages of the G2 generator, as desired. The downside is that this screws up the timing between the G1 and G2. All of these delays effectively shift the G2 sequence to the right 10 chips. Fortunately, the delay directly at the output of the G2 "GLFSR Source" block is pretty high (320). By lowering this number by 10 (to 310), the output from the delays should be aligned with the G1.
Comparing the output from the last "Delay" block with what the output should be for the G2 generator, they are identical. Thus, I now have a working G2 generator and this will allow me to access the stages of the G2 generator, as needed.
I've added two "Virtual Source" blocks to provide for the taps from the stages of the G2 generator. Combining these with an XOR should give me the needed sequence from the G2 generator. Then, I combined this XOR output into another XOR, of which the second input was the G1 generator.
Next step: I need to check that the output is correct. Fortunately, the specification helps with this by providing the first 10 chips of each G2 tap pair. Comparing the output of the XOR of the S1+S2 XOR with the G1 generator, the first 10 chips are correct for each of the first 10 sequences. That's enough for me. I'm confident that the generator works, as desired.
Correlating with a Real GPS Signal
I wanted to test out these codes. One of the benefits of attending Mr. Boschen's class was that he provided a nice, small file of GPS data (two of them, actually!) that he'd collected. I decided to try out my codes against his data set. If it didn't work with that data, then I'd need to relook at my work.
Let me add a little tidbit. There are 32 codes for the original GPS system. Each one requires 1023 chips. Even storing them as one chip per byte (the way that is easiest in Gnu Radio Companion), that's only 32 kB of storage. That's just a rounding error on the storage space most systems have available. You can even lower that by packing 8 chips/byte and the total storage required is 4 kB. That's literally 1 sector on older hard drives. (No idea if sectors are still a thing with .M2s and SSDs.) For now, I recorded each spreading code in a separate file, 32 in all.
Now that I have the spreading codes figured out, time to tackle the next problem. That problem is Doppler shift. GPS satellites are constantly trucking along. They're moving at roughly 3.87 km/sec. Given a person standing on Earth and working through the math, this means a person will see a maximum Doppler shift of roughly +/-4900 Hz.
I created a GRC flowgraph that would account for both the spreading code and the Doppler shift. It would read in the signal samples and one spreading code, cross-correlate them, and provide an output showing if correlation occurred. Further, I added the ability to step through the Doppler offsets most likely to be encountered.
Mr. Boschen showed a cross-correlation of his signal with the spreading code for SV24. It showed very strong, very clear spikes. So I used my SV24 spreading code against Boschen's data. And it worked.
Compare this with two, other codes, SV01 (no correlation) and SV06 (small correlation).
Now that I knew the circuit worked, I added another feature: automatically scanning through all of the Doppler shifts. This was a feature that Dan Boschen put in his circuit. I simply converted the idea to use in a Gnu Radio flowgraph.
This gives me a Gnu Radio flowgraph that I can use to test against GPS signals that I'll try to collect next. I'll stop this post here. As expected, this will require multiple posts.