Aim of this project is to build a simple GNSS receiver, running on a RaspberryPi, that could then be used as a development test rig for GNSS applications.
- Assemble the RPi and GNSS receiver board
- Flash Ubuntu 22.04 LTS Server to microSD
- Configure Ubuntu Server for headless setup and remote access using SSH
- Configure I2C and serial/UART on RPi
- Configure Ubuntu to use GNSS board's external real-time clock (RTC) and remove fake-hwclock
- Test/check messages from the uBlox GNSS receiever
To use this RPI/GNSS as a development test rig it is useful to install a programming language (I am using Swift) and to perform some additional configuration steps.
This guide will also show how to configure the system so that we can edit code on a MacBook using Xcode, being able to keep the full benefit of the Xcode IDE.
- Install Swift for ARM
- (Optional) Install git
- Configure network sharing with Samba
- Connect to the network share on the RPi from macOS
- Create a
helloGPS
application, on macOS - Compile and run
helloGPS
on RPi
The equipment used for this guide is:
- RaspberryPi 4 Model B, with power supply
- RaspberryPi GPS/RTC Expansion Board
- Active GPS Antenna (with SMA Connector)
- microSD card (16GB min, suggested 32GB)
- microSD card reader
- PC/laptop, connected to your WiFi
There are many GPS expansion boards and antennas available for the RPi, the GPS/RTC expansion board and GPS antenna used in this guide are:
- Uputronics RPi GPS-RTC Expansion Board available from PiHUT or direct from Uputronics. I am actually using a slightly older version of this expansion board (without the USB-C header connector) but the procedure should be the same with the more modern version of the board.
- uBlox Multi-Band Active GPS Patch Antenna. Being multi-band (GPS/Galileo/GLONASS/BeuDou) this antenna is slightly more expensive that the single-band (GPS-only) versions, either antenna should work. Multi-band antenna available from Uputronics. Single-band antennas available from PiHUT or Uputronics.
For my setup I decided to buy a case to hold my RPi and GPS expansion board, not necessary for this project but it does help keep all the parts held together and saves possible damage.
The purchased my case from PiHut, GPS HAT Case for RPi
The final assembly.
I used the offical Raspberry Pi Imager app to flash an OS to the microSD card, available from raspberrypi.com/software. Other flash applications are available, such as balenaEtcher, the process to flash the OS will be very similar.
For the OS I don't want the overhead of a desktop on the RPi but I want to make the most of the 64-bit architecture on the RPi so will use Ubuntu Server 22.04 LTS (RPi 4, 64-bit).
- Connect the SD card reader to your laptop and insert a blank microSD card. N.B. If the microSD card is not blank, flashing the OS to the SD card will completely overwrite any data on the card.
On the RPi Imager app click...
- Choose OS
- Other general-purpose OS
- Ubuntu
- Ubuntu Server 22.04 LTS (RPi Zero/2/3/4/400), 64-bit for arm64
Next, choose which storage device to flash the OS image to, click...
- Choose Storage
- Select the microSD card from the list
Before flashing the OS, the RPi Imager application allows you to configure settings that will help for a headless installation. Click the gear icon then set the following...
- Image customization options: I suggest using
for this session only
, but if you intend to repeat this setup multiple times then selectingto use always
is OK. - Set hostname: OK to leave blank (default setting =
raspberrypi
) - Enable SSH: Enable the checkbox
- Use password authentication
- Set username and password: Enable the checkbox
- Username: {your_username} (default = pi)
- Password: {your_password}
- Configure wireless LAN: Enable the checkbox
- SSID: {your_wifi_ssid}
- Password: {your_wifi_password}
- Wireless LAN country: {your_country_code}
- Set locale settings: Enable the checkbox
- Time zone: Etc/UTC (we want of RPi's TZ to be the same as GPS's TZ)
- Keyboard layout: {your_country_keyboard}
Click SAVE
to save settings and dismiss the pop-up.
With all the settings done, click WRITE
, you will shown a warning to let you know that the any data on the SD card will be erased, to continue downloading the OS image file and flash the microSD card, clilck YES
.
Depending on your computer's security settings, you may be prompted for your username and password; this is to give the RPi Imager app permissions to write to the SD card.
The RPi Imager will download the OS image, write the OS to the SD card and then verify that it has copied to the SD card correctly, this could take some minutes to complete.
When the flash process has finished, click CONTINUE
, then eject and remove your microSD card from the reader.
Insert the microSD card into the RPi, then power-up the RPi.
When the RPi boots, it will connect to the WiFi using DHCP and the credentials provided during the microSD flash process. In order to connect to the RPi using SSH we need to know the IP address that the WiFi router has assigned to the RPi.
Open a terminal
window on your laptop/PC and scan the network for available devices using one or all of the following:
arp -a
Use arp
and grep
to return only IPs with name raspberry
or part of the RPi's MAC address
arp -a | grep raspberry
arp -a | grep dc:a6
IP range assignment may vary for individual routers, typically 192.168.1.0
is the base address of most routers with a mask of /24
to give access to a range of IPs from 192.168.1.0
to 192.168.1.255
nmap -sn 192.168.1.0/24
Check the list of devices to find the one with raspberry
in the description.
From a terminal window on your laptop/PC, connect to the RPi using ssh
, replace xx.xx.xx.xx
with the IP of the RPi. When prompted, enter the SSH password created during the microSD flash process.
Before making any changes to the RPi it is recommended to update the Ubuntu OS.
sudo apt update
The update process may take some time, then upgrade the OS.
sudo apt upgrade
The upgrade may take some time to complete, you may be prompted to restart some services or reboot.
Before we can use the GNSS expansion board with the RPi we need to configure the serial ports and I2C interfaces correctly.
By default Ubuntu is set to enable a serial-console, this must be disabled otherwise the root
user will constantly take ownership of the serial port, typically this will be on /dev/serial0
, a symlink to /dev/ttyS0
which is the UART serial port that the GPS board is using.
sudo nano /boot/firmware/cmdline.txt
Delete the entry console=serial0,115200
Save and exit nano.
Reboot sudo reboot
, login via SSH after approx. 1 minute.
Check that the pi
user is able to read data the GPS board is sending to the serial port.
cat /dev/serial0
The data may contain a lot of blank lines and/or 'unknown' data. For a cleaner output it may help to exclude these.
cat /dev/serial0 | grep -v -E "(unknown|^$)"
Example output:
$GNGSA,A,3,21,01,08,32,22,03,17,19,04,,,,1.13,0.69,0.89*13
$GNGSA,A,3,88,72,65,87,81,66,73,82,,,,,1.13,0.69,0.89*1F
$GPGSV,4,1,14,01,74,107,42,03,71,235,27,04,20,177,26,08,09,167,17*7B
$GPGSV,4,2,14,14,12,252,16,17,46,296,38,19,25,314,38,21,47,117,38*72
$GPGSV,4,3,14,22,36,060,36,28,,,32,31,06,090,14,32,20,042,37*4F
$GPGSV,4,4,14,36,23,142,,49,29,173,*7C
$GLGSV,3,1,11,65,68,059,35,66,45,199,25,72,19,037,23,73,12,005,23*6C
$GLGSV,3,2,11,74,00,047,,80,10,319,,81,64,317,38,82,14,316,35*6E
$GLGSV,3,3,11,87,11,134,30,88,57,134,41,95,10,319,*58
To use the GPS board fully and to make use of the hardware real-time clock (RTC) on the GPS board, we have to setup the I2C correctly and modify the hwclock
settings.
Install the necessary tools.
sudo apt install i2c-tools python3-smbus
Check the current configuration of I2C
sudo i2cdetect -y 1
Output should be...
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- 42 -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- 52 -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
The RTC is at addr 0x52
and the GPS at addr 0x42
on the I2C bus.
We need to modify the system config such that the RTC can be used by the OS.
sudo nano /boot/firmware/config.txt
At the end of the file, after [all]
add ...
dtoverlay=i2c-rtc,rv3028
The RTC on the Uputronics board is a Micro Crystal RV-3028-C7 real time clock module, rv3028
is the identifier for this RTC.
Reboot and SSH login again.
Check that the I2C bus has been updated correctly.
sudo i2cdetect -y 1
Output should be...
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- 42 -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- UU -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Now at adrr 0x52
should be UU
to indicate that the RTC is being used by the OS kernel driver.
Now the OS must be configured to use the hardware RTC instead of the fake hardware clock that is used by default.
sudo apt-get -y remove fake-hwclock
sudo update-rc.d -f fake-hwclock remove
sudo systemctl disable fake-hwclock
sudo nano /lib/udev/hwclock-set
Comment out the lines:
#if [ -e /run/systemd/system ] ; then
# exit 0
#fi
#
#/sbin/hwclock --rtc=$dev --systz
To read the hwclock
sudo hwclock -v -r
In order for correct NTP operation, the PPS from the GPS must also be configured.
sudo apt install pps-tools
sudo nano /boot/firmware/config.txt
At the end of the file, after [all]
add ...
dtoverlay=pps-gpio
We also need to update the system modules
sudo nano /etc/modules
At the end of the file, add...
pps-gpio
Reboot and SSH login again.
Check that the PPS is operating correctly.
sudo ppstest /dev/pps0
Output should be similar to...
N.B. Since V3.00 of the Ublox firmware the time pulse is not released until all time parameters are known including leap seconds. There it could be up to 12.5 minutes before time pulse is available however positional lock is achieved from cold in the expected sub 30 seconds.
- Run system update and upgrade, install
curl
sudo apt update && sudo apt upgrade
sudo apt install curl
- Run Swift lang quick install script
curl -s https://archive.swiftlang.xyz/install.sh | sudo bash
- Script will check for system compatibility, our config should be OK, when prompted select (1) latest release.
- Install Swift
sudo apt install swiftlang
- Check Swift version
swift --version
Git should be installed by default with this the current config, check that git is installed.
git --version
If git is not installed...
sudo apt install git
This shared folder will be used so that we can edit code on the RPi from another computer, specifically to use Xcode and have all the benefits of using that Xcode IDE whilst having all the code saved on the RPi.
We will create a code
folder in the pi user's home folder; create a user group coders
; then we will configure samba.
In the pi user home folder create a new folder code
.
cd ~
mkdir code
Create a new user group coders
, then add the pi
user to this group.
sudo groupadd coders
sudo usermod -aG coders pi
Set the coders
group to be the group owner of this folder, then give the coders
group read-write permissions.
sudo chgrp -R coders: ./code
sudo chmod -R g+rw ./code
With the share setup, we can install and configure samba.
sudo apt-get install -y samba
sudo nano /etc/samba/smb.conf
If your RPi needs to join a specific workgroup of domain, update the WORKGROUP
name as required.
workgroup = WORKGROUP
At the end of the smb.conf
file add...
[CODE]
path = /home/pi/code
valid users = @coders
browsable = yes
writable = yes
read only = no
Save and close the file.
Restart the samba service for the changes to take effect.
sudo systemctl restart smbd
Add the pi
user to Samba.
sudo smbpasswd -a pi
Confirm by entering the password for the pi
user when prompted. Then enable the user.
sudo smbpasswd -e pi
To connect to the share from macOS, use Finder. Select Go -> Connect To Server ...
. In the pop-up window enter the Samba location of the share, replace xx.xx.xx.xx with the IP address of the RPi.
smb://xx.xx.xx.xx/code
When prompted, enter the username and password we have created.
For the helloGPS
app we are going to make use of the existing SwiftyGPIO
repo.
Uraimo's UBloxGPS.swift
repo is very close to what we need, but is written to only parse GPS messages, our application must parse multi-GNSS message so we will create a UBloxGNSS parser based on UBloxGPS.swift; both repos created by uraimo.
On the RPi, create a new directory for the helloGPS
application.
cd ~/code
mkdir helloGPS
cd helloGPS
Optionally, enable git
for this project.
git init
git add .
Use Swift's package management (SPM) to create a blank executable package.
swift package init --type executable
Now that we have a blank Swift package and a open share on the RPi, we can write the code of our helloGPS
app using Xcode on macOS. Using Xcode, open the package.swift
file that we just created; should just need to open package.swift
, located in /Volumes/code/helloGPS/
in Finder.
In Package.swift
update the package dependencies to include the SwiftyGPIO
repo, this will download the SwiftyGPIO
package; then add this package as a target dependency.
import PackageDescription
let package = Package(
name: "helloGPS",
dependencies: [
.package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.1")
],
targets: [
.executableTarget(
name: "helloGPS",
dependencies: ["SwiftyGPIO"])
]
)
In helloGPS/Sources/helloGPS
create a new swift file, UBloxGNSS.swift
with the following code.
import Foundation
import SwiftyGPIO
public class UBloxGNSS{
var uart: UARTInterface
var updateThread: Thread?
var running = false
public var isDataValid = false
public var datetime = "N/A"
public var latitude: Double = 0
public var longitude: Double = 0
public var satellitesActiveNum = 0
public var altitude: Double = 0
public var altitudeUnit: String = "N/A"
// Internal fields for quadrant location used to compute lat/lon
var NS: Int = 1
var EW: Int = 1
public init(_ uart: UARTInterface) {
self.uart = uart
uart.configureInterface(speed: .S9600, bitsPerChar: .Eight, stopBits: .One, parity: .None)
print("Init'd")
}
public func startUpdating(){
//Ignored by Linux
guard #available(iOS 10.0, macOS 10.12, *) else {return}
if updateThread == nil {
updateThread = Thread{ [unowned self] in
self.update()
}
}
running = true
updateThread!.start()
}
public func stopUpdating(){
running = false
updateThread = nil
}
public func printStatus(){
print()
print("\tGNSS Values:",(isDataValid ? "valid." : "invalid."))
print("\tDate:", datetime)
print("\tLatitude:",latitude,"Longitude:",longitude)
print("\tAltitude:",altitude,altitudeUnit)
print("\tUsable satellites:",satellitesActiveNum)
print("\t-------------------------------------------------------- ctl-c to quit")
print()
}
private func update(){
while running {
let s = uart.readLine()
parseNMEA(s)
}
}
/// Parse the NMEA0183 protocol strings that follow the format:
///
/// $ttsss,d1,d2,...,dn<CR><LF>
///
/// - Parameter text: a string conforming to the protocol
///
private func parseNMEA(_ text: String){
let comp = text.components(separatedBy: ",")
switch comp[0] { //$ttsss
case "$GNRMC":
// time,valid,lat,NorS,lon,EorW,speed,course,date,magn,EorW,ck
// time= hhmmss.ss, date= ddmmyy
if comp[1].count > 0 {
datetime = comp[9]+" "+String(comp[1].dropLast(3))
}
isDataValid = (comp[2] == "A")
// quadrants, will be used to apply the right sign to lat/lon
NS = (comp[2] == "N") ? -1 : 1
EW = (comp[6] == "E") ? 1 : -1
// latitude and longitude in degrees+minutes format
if (comp[3].count > 0) && (comp[5].count > 0) {
latitude = Double(String(comp[3].prefix(2)))! +
Double(String(comp[3].dropFirst(2)))!/60
latitude *= Double(NS)
latitude = latitude.roundTo(places: 8)
longitude = Double(String(comp[5].prefix(3)))! +
Double(String(comp[5].dropFirst(3)))!/60
longitude *= Double(EW)
longitude = longitude.roundTo(places: 8)
}
case "$GNGGA":
// time,lat,NorS,lon,EorW,quality,numSats,Hdiluition,altitude,unitAltitude,geoidsep,unitGeoidsep,dataAge,[missing in ublox],ck
satellitesActiveNum = Int(comp[7]) ?? 0
altitude = Double(comp[9]) ?? 0
altitudeUnit = comp[10]
default:
//Unrecognized or ignored string
return
}
}
deinit{
stopUpdating()
}
}
extension Double {
func roundTo(places:Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}
Edit the main.swift
file as follows.
import Foundation
import SwiftyGPIO
var signalReceived: sig_atomic_t = 0
signal(SIGINT) { signal in
signalReceived = signal
}
let uarts = SwiftyGPIO.UARTs(for: .RaspberryPi4)!
var uart = uarts[0]
let gnss = UBloxGNSS(uart)
gnss.startUpdating()
while signalReceived == 0 {
#if os(Linux)
system("clear")
#endif
gnss.printStatus()
sleep(1)
}
gnss.startUpdating()
exit(signalReceived)
Save the Xcode project.
On the RPi, build the application in the terminal window.
swift build
Assuming there are no errors, then run the application.
swift run
The appliction will read the messages sent from the UBlox GNSS receiver and will decode the $GNRMC
and $GNGGA
messages, update the display every second, displaying the receiver position and usable satellite count.
swift build -c release
cd .build/release
sudo cp -f helloGPS /user/local/bin/hellogps
Invoke the application by running hellogps
That's All Folks!