In this post I’m going to write about my experiments with interfacing a low-cost serial GPS module directly to uLisp, to create projects such as a GPS clock:
I’ll also describe a GPS speedometer and odometer, and a simple navigator, in later posts.
The module I used is the GP-20U7, a small GPS module available for under $20 from SparkFun GPS Receiver - GP-20U7 (56 Channel), but almost any other GPS module should be suitable.
The GPS Clock and GPS Speedometer/Odometer will work on any version of uLisp, with sufficient memory, such as the Arduino Mega 2560. The simple navigator requires a 32-bit version of uLisp, running on a board such as the Adafruit ItsyBitsy M0.
Getting started
The first step is to connect the GPS module to your microcontroller’s Rx input; see Language reference - with-serial for details of which pin to use on your board. Then run the following echo program:
(defun echo ()
(with-serial (str 1 96)
(loop
(print (read-line str)))))
At first, before the GPS module has locked onto any satellites, you’ll see something like:
$GPRMC,,V,,,,,,,,,,N*53
If all is well, after a few seconds you should see lines with the time, followed after a minute or so by lines with location data:
$GPGSV,4,4,13,31,13,032,29*41
$GPGLL,5213.12861,N,00008.23883,E,111152.00,A,A*6D
$GPRMC,111153.00,A,5213.12851,N,00008.23852,E,0.408,,101119,,,A*7F
$GPVTG,,T,,M,0.408,N,0.756,K,A*2B
$GPGGA,111153.00,5213.12851,N,00008.23852,E,1,07,1.26,9.2,M,45.7,M,,*59
GPS modules output the GPS information as a series of text strings, called NMEA sentences. Each NMEA sentence starts with an identifier such as $GPRMC, to identify the sentence, followed by the GPS parameters such as latitude and longitude, separated by commas. The string is terminated by an asterisk and two-digit checksum.
The most useful NMEA sentence is the RMC (Recommended Minimum C) one:
$GPRMC,113211.00,A,5213.12667,N,00008.22177,E,4.955,266.36,101119,,,A*64
This contains all the most important navigational parameters: time, latitude, longitude, speed, course, and date. The fields in the above example are as follows:
- 113211.00 - Time is 11:32:11 and 00 milliseconds UTC
- A - Status, A=active or V=void
- 5213.12667,N - Latitude 52° 13.12667’ N
- 00008.22177,E - Longitude 0° 8.22177’ E
- 4.955 - Ground speed 4.955 knots
- 266.36 - Course 266.36°
- 101119 - Date 10th November 2019
- A - Mode, A=autonomous, D=differential, E=estimated
- *64 - The checksum
Note that most fields are fixed width, but the ground speed and course are variable width, and if the GPS module is stationary the course may be blank.
Selecting the RMC sentences
The next step is to select just the RMC sentences; here’s the revised version of echo :
(defun echo ()
(with-serial (str 1 96)
(loop
(let ((line (read-line str)))
(when (and (> (length line) 7) (string= (subseq line 0 7) "$GPRMC,"))
(print line))))))
You should now get just one RMC sentence printed every second.
Parsing the RMC sentences
The third step is to parse the parameters from the RMC sentence by extracting the substrings between successive commas. This new version of echo now calls a function parse on each RMC sentence:
(defun echo (fun)
(with-serial (str 1 96)
(loop
(let ((line (read-line str)))
(when (and (> (length line) 7) (string= (subseq line 0 7) "$GPRMC,"))
(parse line fun))))))
Here’s the definition of parse :
(defun parse (line fun)
(let ((start 7)
(end (length line))
i result)
(loop
(setq i start)
; Find comma
(loop
(when (or (= i end) (eq (char line i) #\,)) (return))
(incf i))
; Extract parameter
(push (if (= start i) nil (subseq line start i)) result)
(setq start (1+ i))
(when (= i end) (return)))
; Call function on result
(funcall fun (reverse result))))
It splits the string into a list of substrings, one for each parameter. Finally, it calls the function you provide as a parameter on the list of strings. For example, if you do:
(echo print)
it will simply print the list of strings for each RMC sentence, one per second:
("193409.00" "A" "5213.12667" "N" "00008.22177" "E" "2.881" nil "271119" nil nil "A*7D")
Our GPS applications can now simply get the appropriate GPS data from this list; for example the latitude is:
(third lst)
GPS Clock
The first project is a GPS clock that takes advantage of the accuracy of the atomic clocks used by the GPS system to provide an accurate time display in hours, minutes, and seconds on an eight digit 7-segment display.
Driving the 7-segment displays
An ideal display for the GPS clock is the 8-digit seven-segment display module available very cheaply from sites such as AliExpress MAX7219 8 Digit LED Display:
They are based on the MAX7219 display driver MAX7219 Datasheet and are easy to control using SPI.
The MAX7219 is specified as operating from 4.0 V to 5.5 V, but it seems to work fine from 3.3 V. For a brighter display when using it with a 3.3 V board connect VCC on the MAX7219 to the +5 V pin on the board.
Connecting the display
Connect the display using the appropriate SPI pins as follows:
VCC | +5V |
---|---|
GND | GND |
DIN | MOSI |
CS | Enable |
CLK | SCK |
For details of which pins are used for the SPI interface on different processors see Language reference: with-spi.
You can use any suitable pin as the Enable pin; specify it as follows:
(defvar en 10)
Display command
Every command written to the display is a 16-bit word, consisting of an address or command code followed by a data value:
(defun cmd (a d)
(with-spi (str en)
(write-byte a str)
(write-byte d str)))
Initialising the display
The following routine on turns on the display and sets the brightness; the parameter can be from 0 (dimmest) to 15 (brightest):
(defun on (bri)
(cmd #xF 0) ; Test mode off
(cmd #x9 #xFF) ; Code B mode
(cmd #xB 7) ; 8 digits
(cmd #xC 1) ; Enable display
(cmd #xA bri))
It set the display in “Code B” mode, which uses an internal lookup table to give the segments for the digits 0 to 9, “-”, and space.
Clearing the display
The following function clr clears the display:
(defun clr ()
(dotimes (d 8) (cmd (1+ d) #xF)))
Displaying a string
The following routine show writes a text string containing up to eight digits right-aligned on the display:
(defun show (text)
(let ((len (length text))
(d 1) (b 0))
(dotimes (i len)
(let ((c (char-code (char text (- len i 1)))))
(cond
((= (char-code #\.) c)
(incf b #x80))
(t
(cond
((<= (char-code #\0) c (char-code #\9))
(incf b (- c (char-code #\0))))
((= (char-code #\-) c)
(incf b #xA))
(t (incf b #xF)))
(cmd d b) (incf d) (setq b 0)))))))
It handles decimal points, dashes, and spaces in the string. For example, to display “1234.5678” give the command:
(show "1234.5678")
Displaying the time
To show the time on the 7-segment displays we simply need to call echo with a function that calls show on the first element of the RMC list:
(echo (lambda (lst) (show (first lst))))
To format the time into a more readable display, with the hours, minutes, and seconds separated by dashes, we can define this routine time:
(defun time (lst)
(let ((tim (first lst)))
(when tim
(show
(concatenate
'string
(subseq tim 0 2) "-"
(subseq tim 2 4) "-"
(subseq tim 4 6))))))
To display the time from each RMC sentence now call:
(echo time)
Here’s a program go that initialises the display and runs the GPS clock:
(defun go () (on 15) (sho "-- -- --") (echo time))
Making a stand-alone GPS clock
To make a stand-alone GPS clock from this project, proceed as follows:
Uncomment the following preprocessor statement from the uLisp source:
#define resetautorun
and upload uLisp to the ItsyBitsy M0 board, or whatever board you are using.
Enter the program for the GPS clock; here’s the full listing: GPS clock program.
Save the image using the command:
(save-image 'go)
The clock will now run automatically when you apply power to the board.