Cell phone RTK/PPK -How important is a ground plane?

I often stress in my posts the importance of using a ground plane under the antenna when collecting observations for precision GNSS solutions to reduce the effect of multipath. This is true for traditional ceramic patch antennas but is even more important when collecting data with a cell phone.

A ceramic patch antenna typically used with a low cost GNSS receiver has some built in protection against multipath. It is directional with maximum gain in the vertical direction and is also circularly polarized which helps because the reflected signals have the opposite polarization of the direct signals. In this case, adding a ground plane under the antenna reduces multipath by attenuating signals from low elevation angles and eliminating signals from below the horizon, but it is only part of the multipath mitigation.

A cell phone antenna on the other hand is omni-directional and most likely linearly polarized. Hence it has little or no built-in protection against the reflected signals. The only defense in this case against multipath is a ground plane, and so it becomes that much more critical.

I recently stumbled across a very nice cell phone data set shared online by the same authors of the data I used in my very first post on cell phone solutions from a couple years ago. It is taken with an Xiaomi Mi8 phone and includes 40 days of static measurements, each measurement between 10 and 15 minutes long. There are measurements each day from three locations with varying sky visibility; open sky, partially forested, and forested. What makes this data particularly interesting is that for half of the 40 days, the measurements were taken with a ground plane underneath the phone and the other half were taken without a ground plane. The authors then went on to show that the solutions derived from the ground plane data were significantly more accurate than those derived from the data without ground planes. Unfortunately their paper is not available without logging into a service, but a short summary is available here.

In this post I will try to repeat their analysis using their data to confirm I get the same result. They actually used an earlier version of the demo5 RTKLIB for their analysis but I will use the latest version of the code (b34d) and a more recent configuration file. For this exercise I will analyze only the open sky measurements and will leave the more challenging measurements for another post.

The data set is very complete and includes observations from a nearby base station as well as downloaded broadcast navigation data. This last item is important, because as I’ve noted before, the Mi8 Galileo navigation data either has a bug in it or is just incompatible with RTKLIB. For this reason, I ran all solutions with the downloaded broadcast navigation data rather than the navigation data collected by the phone.

I started with the config file I described in my last cell phone post but made a few changes. I have appended the contents of the final config file I ended up with to the end of this post. I’ll describe the changes I made below.

First of all, this data is static, unlike the previous data, so I changed the solution mode from “kinematic” to “static”.

Next, looking at the residuals from an initial solution plotted with RTKPLOT, I noticed a significant difference in the magnitude of the pseudorange residuals between L1 and L5. In the plots below, the left plot shows the L1 residuals and the right plot shows the L5 residuals. The carrier phase residuals are similar but the pseudorange residuals are much smaller for L5. This is not unexpected because the newer L5 signal has improved structure and higher power. The details are explained in this article.

Solution residuals for L1(left) and L5(right) observations

To take advantage of this difference, I adjusted the code/carrier phase error ratio for L1 (stats-eratio1) from 300 to 1500 and left the L5 error ratio (stats-eratio5) at 300. This will cause the kalman filter to weight the L5 pseudorange observations more heavily than the L1 observations. In RTKPOST, this parameter is set in the Statistics tab of the Options menu. Note that prior to the b34d code, the L5 error ratio parameter existed but was not configurable through the GUI interface in RTKPOST, but that is fixed now.

Next, based on the SNR of the observations, plotted below, I chose to reduce the minimum SNR threshold from 34 dbHz to 24 dbHz. This is a somewhat arbitrary setting and may not be optimal, but leaving it at 34 dbHz for this data set would have thrown out too many usable satellites. It could also have been adjusted separately for L1 and L5 but I did not choose to do this. It is interesting that the L5 observations do not appear to have higher SNR than L1 despite the increased signal power mentioned above. This may be due to limitations of the antenna.

SNR of observations, L1 (left) and L5(right)

The last change I chose to make was to change the solution type (Filter mode in the RTKPOST options menu) from “Combined to “Combined-no phase reset”. This is a relatively new option in the demo5 code. The official 2.4.3 code always resets the phase bias estimates between the forward and backward solutions and this is a more conventional approach. Earlier versions of the demo5 code reset the phase estimates if ambiguity resolution was set to fix-and-hold but did not reset them otherwise. The reason to reset them is to insure the forward and backward solutions independent, but especially for shorter data sets, it can be advantageous to use the converged estimates as a starting point for the backwards solution. Since, in this experiment, the measurements are fairly short (10-15 minutes), I chose to not reset the biases.

The next step was to run the solution on all 40 days of data. RTKPOST is nice for working with a single data set and experimenting with different parameter settings, but to run many solutions quickly, it is easier to batch process them with the RNX2RTKP command line version of RTKPOST. I used a simple python script to run RNX2RTKP ( the CLI version of RTKPOST) in multiple simultaneous threads to make this process faster since a single instance of an RTKLIB app will only use a single processor . I’ll discuss this script in more detail in my next post.

I then used python to plot the error in the solutions, 40 points in all, 2o with ground planes, 20 without. To calculate the errors, I subtracted the ground truth included with the data set from each solution position.

Static PPK solutions for open sky data, with and without ground plane

Note that the ground truth was generated from a survey grade receiver but is only approximate because the exact location of the antenna within the phone was unknown. Cell phone antennas can also have large offsets between the mechanical center of the antenna (ARP) and the electrical center of the antenna (APC). No attempt was made to compensate for these.

As expected, the ground plane solutions were significantly more accurate than the no-ground plane solutions. In fact, 19 out of the 20 measurements solutions for data using a ground plane were within a few centimeters of each other. Only 7 of the 20 measurements without ground planes met the same criteria. Overall, I would say this data makes quite a compelling argument for using a ground plane. I understand it’s not always possible in a cell phone application to do this, but it’s important to at least understand the significance of this choice.

The results were also consistent enough to suggest that cell phones, despite their low performance antennas, are becoming a viable option for practical measurements, at least in open sky environments. It’s worth noting as well, that, although the errors were much larger without the ground plane, even the errors in these measurements were consistently well below a meter.

It is important to remember that this data was all taken with an open sky view and that you should not expect such good results in more challenging conditions. It’s also still true that low cost receivers with higher quality antennas will still give much more robust and reliable solutions.

There’s plenty more data in this data set that I didn’t look at in this post but hope to take a closer look at in the future.

Config file used for this experiment:

# rtkpost options for b34d code, static Mi8 cell phone
pos1-posmode       =static     # (0:single,1:dgps,2:kinematic,3:static,4:static-start,5:movingbase,6:fixed,7:ppp-kine,8:ppp-static,9:ppp-fixed)
pos1-frequency     =l1+l2+l5   # (1:l1,2:l1+l2,3:l1+l2+l5,4:l1+l2+l5+l6)
pos1-soltype       =combined-nophasereset # (0:forward,1:backward,2:combined,3:combined-nophasereset)
pos1-elmask        =15         # (deg)
pos1-snrmask_r     =on         # (0:off,1:on)
pos1-snrmask_b     =on         # (0:off,1:on)
pos1-snrmask_L1    =24,24,24,24,24,24,24,24,24
pos1-snrmask_L2    =34,34,34,34,34,34,34,34,34
pos1-snrmask_L5    =24,24,24,24,24,24,24,24,24
pos1-dynamics      =on         # (0:off,1:on)
pos1-tidecorr      =off        # (0:off,1:on,2:otl)
pos1-ionoopt       =brdc       # (0:off,1:brdc,2:sbas,3:dual-freq,4:est-stec,5:ionex-tec,6:qzs-brdc)
pos1-tropopt       =saas       # (0:off,1:saas,2:sbas,3:est-ztd,4:est-ztdgrad)
pos1-sateph        =brdc       # (0:brdc,1:precise,2:brdc+sbas,3:brdc+ssrapc,4:brdc+ssrcom)
pos1-posopt1       =off        # (0:off,1:on)
pos1-posopt2       =off        # (0:off,1:on)
pos1-posopt3       =off        # (0:off,1:on,2:precise)
pos1-posopt4       =off        # (0:off,1:on)
pos1-posopt5       =off        # (0:off,1:on)
pos1-posopt6       =off        # (0:off,1:on)
pos1-exclsats      =           # (prn ...)
pos1-navsys        =45         # (1:gps+2:sbas+4:glo+8:gal+16:qzs+32:bds+64:navic)
pos2-armode        =fix-and-hold # (0:off,1:continuous,2:instantaneous,3:fix-and-hold)
pos2-gloarmode     =fix-and-hold # (0:off,1:on,2:autocal,3:fix-and-hold)
pos2-bdsarmode     =on         # (0:off,1:on)
pos2-arfilter      =on         # (0:off,1:on)
pos2-arthres       =3
pos2-arthresmin    =3
pos2-arthresmax    =3
pos2-arthres1      =0.1
pos2-arthres2      =0
pos2-arthres3      =1e-09
pos2-arthres4      =1e-05
pos2-varholdamb    =0.1        # (cyc^2)
pos2-gainholdamb   =0.01
pos2-arlockcnt     =5
pos2-minfixsats    =4
pos2-minholdsats   =5
pos2-mindropsats   =10
pos2-rcvstds       =off        # (0:off,1:on)
pos2-arelmask      =15         # (deg)
pos2-arminfix      =10
pos2-armaxiter     =1
pos2-elmaskhold    =15         # (deg)
pos2-aroutcnt      =20
pos2-maxage        =30         # (s)
pos2-syncsol       =off        # (0:off,1:on)
pos2-slipthres     =0.05       # (m)
pos2-rejionno      =1          # (m)
pos2-rejgdop       =30
pos2-niter         =1
pos2-baselen       =0          # (m)
pos2-basesig       =0          # (m)
out-solformat      =llh        # (0:llh,1:xyz,2:enu,3:nmea)
out-outhead        =on         # (0:off,1:on)
out-outopt         =on         # (0:off,1:on)
out-outvel         =off        # (0:off,1:on)
out-timesys        =gpst       # (0:gpst,1:utc,2:jst)
out-timeform       =hms        # (0:tow,1:hms)
out-timendec       =3
out-degform        =deg        # (0:deg,1:dms)
out-fieldsep       =
out-outsingle      =off        # (0:off,1:on)
out-maxsolstd      =0          # (m)
out-height         =ellipsoidal # (0:ellipsoidal,1:geodetic)
out-geoid          =internal   # (0:internal,1:egm96,2:egm08_2.5,3:egm08_1,4:gsi2000)
out-solstatic      =all        # (0:all,1:single)
out-nmeaintv1      =0          # (s)
out-nmeaintv2      =0          # (s)
out-outstat        =residual   # (0:off,1:state,2:residual)
stats-weightmode   =elevation  # (0:elevation,1:snr)
stats-eratio1      =1500
stats-eratio2      =300
stats-eratio5      =300
stats-errphase     =0.006      # (m)
stats-errphaseel   =0.006      # (m)
stats-errphasebl   =0          # (m/10km)
stats-errdoppler   =1          # (Hz)
stats-snrmax       =52         # (dB.Hz)
stats-stdbias      =30         # (m)
stats-stdiono      =0.03       # (m)
stats-stdtrop      =0.3        # (m)
stats-prnaccelh    =3          # (m/s^2)
stats-prnaccelv    =1          # (m/s^2)
stats-prnbias      =0.001      # (m)
stats-prniono      =0.001      # (m)
stats-prntrop      =0.0001     # (m)
stats-prnpos       =0          # (m)
stats-clkstab      =5e-12      # (s/s)
ant1-postype       =llh        # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw)
ant1-pos1          =0          # (deg|m)
ant1-pos2          =0          # (deg|m)
ant1-pos3          =0          # (m|m)
ant1-anttype       =
ant1-antdele       =0          # (m)
ant1-antdeln       =0          # (m)
ant1-antdelu       =0          # (m)
ant2-postype       =rinexhead  # (0:llh,1:xyz,2:single,3:posfile,4:rinexhead,5:rtcm,6:raw)
ant2-pos1          =0          # (deg|m)
ant2-pos2          =0          # (deg|m)
ant2-pos3          =0          # (m|m)
ant2-anttype       =
ant2-antdele       =0          # (m)
ant2-antdeln       =0          # (m)
ant2-antdelu       =0          # (m)
ant2-maxaveep      =1
ant2-initrst       =on         # (0:off,1:on)
misc-timeinterp    =on         # (0:off,1:on)
misc-sbasatsel     =0          # (0:all)


22 thoughts on “Cell phone RTK/PPK -How important is a ground plane?”

      1. Hi G01. The broadcast navigation data RTKLIB is getting from the receiver navigation messages incude satellite health which I believe should be equivalent to the satellite health you are seeing in the Trimble planning tool. By the way, I really like that tool, especially the interactive ionsphere activity maps.

        The smartphone observation data generally includes a higher percentage of dual-freq measurements from Galileo than the other constellations which probably explains your better fix results.


  1. Hi , thank you for sharing research, I was inspired a lot . However, I was confused about ‘b34d’ recently. Since I tried to compare the positioning results of ‘b34d’ resource code (compiled by VS2017) with that of the executable (rtkpost.exe). At the beginning, I expected to see the Exactly same results, but the positioning resluts solved by the resource code are fairly ‘worse’. The configuration file used was generated by the rtkpost.exe (b34d), and the obs and nav files are also same for VS2017 and rtkpost.exe. Therefore, could you offer me some hints.


    1. Hi Chengkai. VS2017 is not capable of compiling the RTKPOST code so I assume you were comparing the demo5 RNX2RTKP (the CLI version of RTKPOST) built with VS2017 to the demo5 version of RTKPOST available for download which was compiled with the Embarcadero compiler. I haven’t built the code with a VS compiler in a long time but I would expect it to provide the same results as long as the build options were the same. I would verify that you have all the constellations enabled in the build and the NFREQ build option set to 3 for L1,L2, and L5. The next step would be to set debug trace level to 3 in the config options, run both versions on the same data with the same config file and compare the debug trace files. If that doesn’t give you the answer, please send me the two debug trace files and I will take a look.

      Liked by 1 person

      1. Hi, Thanks for your reply, I’ve attempt to find the problem with your guidance, however, I still failed. For more details, you can find some files in your gmail and take a look, If it’s convenient for you. Really thanks for you help!


        1. Just for follow-up for other readers, the build files for the VS compiler were removed in the b34 RTKLIB code, but with a slight modification to account for a directory structure change between the two, the b33 VS build files work fine to build the b34 code.


  2. good afternoon ! thank you for a very interesting research! I see that with a dual-frequency phone it is quite possible to achieve an accuracy of less than 10 cm !!! this is an amazing achievement for the phone !!


    1. Hi Костя
      Beware of generalizing these results on every smartphone. For example, it is quite complicated to find a smartphone able to collect usable phase measurements (mandatory for this level of accuracy). And even if you find one, the results can be significantly different. The huge diversity in Android hardware is on very little benefit here… E.g. we are repeating the experiment with a Pixel 5 phone (3-4 generations newer than the examined Xiaomi Mi 8) and I’m quite disapointed so far.

      Liked by 1 person

        1. These are the measures of achieved internal accuracy. In statistical terms (if I understand correctly) this is more about precision (statistical variability around some average value) than accuracy (as the average value can be quite different from reference). From my experience, the relation (agreement) between these estimated errors and true accuracy is quite dependent on the type of solution (auto, float, fix – from the lowest to the highest reliability in this order).


          1. understood thanks !! Yes, that’s right – the reliability of the assessment depends on the type of the result obtained (fixed, floating, etc.) .. Thank you Master!


        2. What receiver were you using ?
          AFAIK there is no RTKLib Android version that works with the internal receiver of a smartphone unfortunately. Data has to be logged and then processed with RTKLib for PCs.

          Liked by 1 person

      1. good evening hobby colleague!
        you wrote that pixel 5 disappointed you in terms of accuracy, in comparison with xiaomi mi 8. I now choose a phone for this purpose. and choose between xiaomi mi 8 or pixel 4xl. which one would you recommend to buy?


        1. Hi, I must correct my statement – with the use of Tim’s most recent code and config I achieved sub-decimeter accuracy with Pixel 5 and 20 minutes static (in most cases). I have no experience with Pixel 4XL, so I cannot recommend it. With Xiaomi Mi8 you can see the results, but from my point of view, it is already a bit aged phone. You can find a very incomplete list of suitable phones here https://developer.android.com/guide/topics/sensors/gnss Look for yes in ADR…

          Liked by 1 person

          1. good evening ! in general, I got an unexpected (for me, at least!) option – oneplus 7t. judging by that table, it does not seem to give out phase measurements … please tell me, is this possibility – giving out phase measurements – can it depend on the firmware version on the phone? if you flash, for example, lineage os – will it be possible to receive phase measurements?
            thank you for your help and advice!


          2. good day ! you write that it is impossible to obtain phase measurements with oneplus 7t … but why? if this functionality is not in the firmware from the manufacturer – I can flash the lineage os … or some other! I have full access and the bootloader is unlocked .. I can flash the “brick” and restore it if necessary!
            In general, I’m ready for experiments, if you or someone else is sorting out the device for smartphones firmware, the structure of the built-in gps – module, wants to help enable or add support for phase measurements in oneplus 7t !!


          3. Hi Костя. While I suspect it is unlikely that you will be able to enable phase measurements on the 7T, I know very little about low level coding on an Android phone, so I would certainly not say it is impossible. Hoepfully, someone else who knows more about this subject can give a more definitive answer.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: