Average Speed Head-up Display Unit - Giantboard

Photo of Al

Made by Al / Automotive / Bikes / Vehicles

About the project

GPS to HUD for in-car use. To make sure a road's average speed cameras do not send you a letter in the post!

Project info

Difficulty: Moderate

Platforms: Linux

Estimated time: 5 hours

License: GNU General Public License, version 3 or later (GPL3+)

Items used in this project

Hardware components

Giant Board Giant Board x 1
GPS receiver GPS receiver TTL serial data output x 1
Monochrome 0.96" OLED Graphic Display Monochrome 0.96" OLED Graphic Display I2C bus, 128 x 64 pixels, 3.3V x 1

Software apps and online services

Putty Putty To run on a PC
SD-Card image Debian-Linux for Giant Board SD-Card image Debian-Linux for Giant Board I use giantboard-debian-9.7-1GB-1-17-2020

Hand tools and fabrication machines

soldering iron soldering iron I use a Weller TCP-1 and PU-1D. x 1
wire-wrap tool wire-wrap tool hand tool x 1


Giant Board arrived 14th April 2020.

Someone else was having loads of trouble so I measured the current consumption of a bare Giant Board with an 8GB SD inserted. During the two minutes or so it took to boot up, the 5V USB current varied from 30mA to 80mA according to my Thurlby PSU's meter, subsequently discovered to be somewhat innaccurate. 

The LEDs showed this:

Green: Fully on, showing power.

Yellow: Flashes at about 4Hz during first 25 seconds of boot-up.

Orange: Intermittent flashing. After boot-up, very brief flashes at 1 second intervals then brighter flash at 30s intervals.

GPS/GNSS Average Speed HUD - Giant Board project

Average speed-limited areas are now more common, both along stretches undergoing road maintenance and there are also permanent areas, one of which is on the road to the Office!

Cars and sat-navs indicate current speed, but not average speed. HUD means head-up display or heads-up display and the original plan was to reflect the display off the windscreen. Speed data comes from a satellite navigation receiver. Originally called GPS but it is now referred to as GNSS (Global Navigation Satellite System), which covers GPS, GLONASS, Galileo, Beidou etc. I have mostly used the term GPS because this project happens to use a very old receiver!


a. GNSS/GPS receiver

b. Giant Board

c. SSD1306 128x64 pixel OLED 3V I2C-bus display

d. DIP switch

e. Press button

f. NPN transistors (2), LED and some resistors.

g. micro-USB power/data lead.

h. FTDI232 module

I. 9-way D-type connector if required.

Component Notes:

a. GNSS/GPS receiver. I happen to have bought a Garmin GPS18LVC puck-style one at my local radio club's junk sale, therefore it was used.

There are better choices available e.g. the smaller PCB-mounted devices with integrated antenna are ideal providing they offer serial data with TTL levels, and can be powered from 3.3 or 5V. The 4,800Bd data from my Garmin is similar to RS232 but without the +/- 12V voltages therefore it requires inverting to get the correct resting logic high state, hence the two transistor interface. My transistor interface circuit also reduces the 5V signalling levels from the GPS receiver down to 3.3V with a potential divider.

For a previous project, 'Teddy in Space' (HAM-1 hab), we used a Lassen receiver connected to an Arduino.

Other receivers may not require inversion and/or voltage reduction.

I have experimented with a GNSS 5 Click Board (shown above) and an external active GPS antenna; it works very nicely. With only a micro-USB lead between the Click board and my PC, to provide both power and data, I could immediately get serial data at a default 9,600Bd on COM6: using Putty. It streams default sentences, but not the GPRMC one that I use on the Garmin. Because the Click board is a GNSS receiver it uses slightly different sentences, so GPRMC is renamed GNRMC. Its GNRMC sentence has an extra comma towards the end, so I have modified my software accordingly and it will decode both. Oddly its speed reporting is not zero but dithers around 0.02kts when the antenna is stationary. Another comparison between receivers is that the Click board reports speed with ten times the resolution. The Click board may need setting up to decode the other satellites. I have yet to try that, and I have not trained the Click unit to send just one sentence.

A test this morning shows the Click board takes under 10mA at 5V. 

b. Giant Board.

This requires an SD card with a special image containing an ATSAMA5D27 version of Debian/Blinka/CircuitPython. Note that no connectivity via Wi-Fi/Ethernet is catered for in my project, therefore I use the software that came on the SD-card image exactly 'as-is', giantboard-debian-9.7-1GB-1-17-2020, and no extra library code has been added.   I have also not attempted to modify the Linux kernel.

The Giant Board’s SD card is used for booting and logging all GPRMC data, its I2C port drives a display, its secondary UART gets GPS data and two digital inputs are used for switches. Digital outputs are used for testing. Here is a photo of my MSO showing these digital outputs being toggled as various software methods run in relation to GPS data, the top trace:

c. SSD1306 OLED. The 128x64 blue monochrome display seems to work nicely and consumes very little power. It has an internal 3 to 5V inverter and the adafruit_ssd1306 library code is happy, using default settings, to initialise the board's internal registers including the inverter controller.

For a previous Raspberry-Pi-based teleprompter project I used PyGame which was great as it allowed a display surface to be flipped and rotated. However the built-in libraries for the SSD1306 do not appear to do this, so I added my own large character fonts and python code to write pixels directly to the frame buffer either normally or in reflected mode. These fonts are available from here: A link to Fonts etc

Unfortunately, experiments have shown that the OLED is simply not day-readable when reflected off a car's windscreen; it is just not bright enough. A UK source of OLED displays is

To make a reflected display appear at infinity requires a small lens but I have not investigated this. It would need some careful thought to prevent sunlight from setting fire to the OLED...

d. DIP switch. This selects normal or reflected display mode. The CPU has internal pull-up resistors but the built-in libraries do not give access to this facility so external pull-up resistors are fitted as that is considerably easier to do as opposed to trying to fork the kernel. Only one section of my dual-switch is actually connected. The spare half is for 'future-proofing'.

e. Press button input. This multi-function button is used to clear the average speed accumulators, select average or current speed, and exit the program. Once the program has stopped, power can be removed or a PC connected perhaps to copy log files to its SD card's /boot/uboot, or software changes made and the program re-run. Again, an external pull-up resistor has been fitted. The push button may be duct-taped to the gear stick.

Originally the push button had two functions: if held for 5 seconds or more it clears the average speed.

If held for 10 seconds or more, it stops the program. But it was found on a test drive that displaying one's current speed to 0.1mph resolution could be another useful feature to add, because the car's (mechanical) speedometer accuracy is ultimately dependent on tyre/tire diameter, air pressure, temperature, a spring and a magnet.

My Button Logic is now: press and release for approximately half a second to clear average speed. A press and release over about 2 seconds toggles average to current speed display, and a hold down for >10 seconds hold exits the program.

f. Transistor interface. This is only required for serial data inversion and/or >=5 volt signalling. Often one can safely apply 5V signal levels to some I/O pins, via current-limiting resistors, as for example most 3.3V PICs have some 5V-tolerant input pins. Alas, the ATSAMA5D27 data sheet does not mention this. The transistors chosen were NPN types BC337 because they were in the junk box, but of course anything similar will work as they are not critical.

h. FTDI232 module. This item is optional. I added an FTDI232 module so that the Linux boot process could be monitored. The module converts 3.3V serial data from the Giant Board’s TX pin (ttyS0) at 115200Bd to USB, and my PC sees it as COM5. It is not required for normal operation. The RX pins is not connected.


The project is built on a piece of Veroboard(tm) and some of the Giant Board's edge pins were wire-wrapped to strip-pins soldered onto the perf-board. This allows modifications to be made without needing to de-solder. All other components were soldered onto the Veroboard. My GPS cable happened to have a male 9-way ‘D’ connector, so I used a 9-way female connector as a convenient way to plug and unplug for testing. The wiring was non-standard. There is often confusion as to the sex of ‘D’ connectors. If a connector has 9 pins then I call it male.

Eagle-eyed people will notice the lack of a driver transistor on the Veroboard implementation. My original interface was built in bird’s-nest style, in the ‘D’ connector’s shell. But that didn’t look neat. As I had set-up the GPS receiver already, I did not put the extra components on the Veroboard to save space.

Viewed from the top:

Software Details.

This project takes an NMEA sentence, specifically the GPRMC one, from a GPS receiver and decodes current speed, averages it and displays the result on an OLED display.

The Garmin's default settings cause it to send loads of different sentences over a two second cycle. We only need one sentence so a Python script,, was written to allow the user to train the receiver to only send out one particular sentence at a much more useful one-second update rate. This is achieved by clearing all sentence reporting and enable the required one, GPRMC. Of course this could be automated in the main program but to make life easier I decided that, because it only runs once, use a separate Python script. The GPS receiver's NV ram stores the settings.

All code is here:

More modern receivers may not offer the GPRMC sentence but there will be an equivalent e.g. GNRMC, that outputs date, time, lat, long and speed. It is very easy to modify this program as required. All the relevant information on each field is freely available on-line. 

It took the Giant Board about 2 minutes 30 seconds to boot up its Linux OS. To reduce this try:

sudo systemctl disable connman-wait-online.service.

I have modified /etc/rc.local to call my software on power up so a keyboard is not necessary. That is the *nux equivalent of Autoexec.bat. This starts the app in about 27 seconds.

Here is rc.local

#!/bin/sh -e

python3 /home/debian/ &

exit 0

Conversely, in order to run the program from the Gadget Port's terminal (ttyGS0), type this from Putty on your PC, obviously once you have logged in first(!):

sudo python3

Because the program uses hardware features it must be run under sudo. When the program starts, its software version 'AV1' is displayed on the OLED for a second. 

After setting up the UART and I/O pins, the main program then waits until the GPS unit replies with valid data. GPS serial data is fed from the Garmin via a transistor interface into the Aux serial port /dev/ttyS1 on pin AD3. Even though the Giant Board has pins marked TX and RX, this port /dev/ttyS0 is only useable to display Linux boot output, at 115kBd, and cannot be used to receive data due to permissions errors. Unfortunately the kernel would require forking to sort that issue out.

The OLED initially shows GV which means no GPRMC block and Invalid data. Here is a typical GPRMC sentence, note the status is 'V' which means Not Valid(!?!):


It may take a few more minutes for the first fix data to become valid.

Now this is a valid sentence; status is now 'A':


For comparison, the Click Board's NEO-M8N module replies:


The main reason to use the Giant Board, as opposed to perhaps a PIC, is the ability to use Python3. A few lines of that can do loads of useful work, and one does not have to worry about cross-compilers, (in)correct libraries etc. 

The incoming valid GPRMC sentence is read by this line:

  1. line_in = tty1.readline() # get the sentence

The data (in CSV format) is split by commas. This is one of the many very powerful Python functions and makes life so easy!

  1. str_in = str (line_in) # convert to a string
  1. data = str_in.split(',') # split at each comma
  2. speed = data[7] # for example, this is speed.

It doesn't get easier than that! Those 4 lines of code are the nuts and bolts of the software. Full code is to be found at the end of this story.

Speed is accumulated in spd_acc and a number of samples counter, spd_ctr, incremented. Because Python3 doesn't care about an object's length (i.e. byte or word or long) this number can get larger and larger; it will still work, yet another advantage of Python.

Invalid blocks do not get counted e.g. when in a tunnel. GPS speed info comes in as a 4 digit floating point number starting at 000.0 in knots. Thus it is quite accurate and is even useful at walking speeds. GNSS receivers such as the Click's NEO offer even higher resolution.

In Run mode, the main loop runs every 200ms, very approximately. The 200ms consists of a non-blocking 100ms wait time for serial data and a 100ms sleep time at the end of the loop. I used my HP MSO to see how long each method took by bit-banging output pins, and the data splitting etc. takes 50ms and the method takes 100ms.

The OLED displays 3 large digits and will show walking speeds to a resolution of 0.01mph or driving speeds to 0.1mph below 100mph. Sadly I have not been able to test above a ton. The decimal point moves automatically. My fonts are on a 32 bit x 32 bit matrix. To fit the OLED's 64 pixel height. I scale each character in the disp_txt method. That method writes pixels either normally or inverted.

Each time a new average speed sample is displayed, a moving line of 64 pixels is rotated clockwise around the display's border. A video of this is here: YouTube link to Average Speed display

Then it is possible to know if GPS updates have stopped or Linux has gone AWOL. When the current speed mode is operational, the border toggles between two vertical lines or two horizontal lines so it is immediately obvious whether the display is showing average or current speed. A video of this is here: YouTube link to Current Speed display

As another feature, each incoming sentence is logged on the SD card in the directory /home/debian/logs. The log filename is taken from the GPS date, so up to 24 hour's worth of data can be appended to one file. That limits its size to a sensible level. Invalid blocks are also logged, but to a different file; the filename has an ‘x’ appended to the date.

Now when plugged into a PC, via the USB Gadget serial port, the SD card is visible as /boot/uboot. If Putty is used on that PC, at 115,200Bd, one can use the terminal and copy log files over from /log to the common /boot/uboot directory. The uboot directory has a size of about 40MB and that is more than adequate to transfer a single day's log because they cannot get that large. A spreadsheet programme running on the PC can then plot all sorts of useful stuff about the day's trips by simply importing this CSV data.

Current consumption of the Giant Board, Garmin receiver and OLED varies from 80 to 100mA during the boot process and may be powered from a car's cigar lighter socket with one of those USB power adaptors. Conversely a 5V/4000mAh mobile phone power bank works well and has the capacity to run the project for several days continuously. This means a back-pack person-portable version is feasible.

At the start of this project, we were having restrictions on car use that limited one to essential journeys only, so for testing purposes a back-pack version was built so I could take a hike and test the software for bugs. This worked really well and my average walking speed was 3.71mph.

To keep the puck upright I modified a polythene 2.272L/4pint milk container which could then be placed inside a rucksack. A short (5mm) M3 bolt holds the GPS puck onto the lid, then the lid is screwed back onto the container. This could also be used by cyclists. Note my board sitting on the right hand side and Click GNSS module on left.


The use of Python3 is very powerful and enjoyable to learn. A few lines of Python can do lots of work. I have not written the code in 'Pythonic' language so it may be easily understood and modified by all. Debug info can be printed from the program via the Gadget serial port. The software starts with 

  1. Debug_mode = False # could be True to print debug messages

My program was typed in with Nano, as it is not that long, under 300 lines. Home-made font files were copied over from a PC using the boot directory. The Giant Board plus OLED take only about 80-120mA from the USB power supply during boot-up, and the current drops dramatically once my app starts. It then takes only 60mA with no GPS device attached, and 130mA for the Giant Board, OLED  and GPS. My loop ends with

  1. time.sleep(0.1)

and that may well power-down the CPU to its 200uA figure. (Figures supplied by Groboard seem to show minimum power is over 20mA)

On the downside, a boot time of 27 seconds is not very good. I know that some versions of embedded Linux can boot in 10 seconds but whether that is from an SD card or not I don't know. But the USB gadget port needed yet another 60 seconds before the login prompt appeared. That one is not yet sorted...

The lack of being able to use the pins TX and RX is worth fixing. The inability to enable internal pull-up/down resistors is not ideal especially for any large quantity production – resistors cost money! Again. these issues require forking the kernel I believe.

There are some notes by Microchip on using the various interfaces under Linux, but the CAN bus is not there.

Finally, the HUD mode has had to be abandoned due to the lack of visibility of the OLED. But if the OLED is placed in front of the windscreen it is easy to see. For a proper HUD it really need 3 high-brightness LEDs but there are not enough I/O pins on the Giant Board to run segments individually. I try to avoid multiplexing large currents because of the EMC problems that can be caused. One alternative is to use three latched 7447 BCD to 7-segment decoders and wire the LEDs up for an inverted display. That would need 7 I/O lines. But there is no easy way to invert the display should that be required.

A more elegant solution would be to drive the 3 LEDs from a 40 or 44-pin 5V PIC. The SPI port could communicate to the PIC. Then software could be used to invert the display and brightness could be controlled via PWM.

Many years ago I have used an MM5450 34-output LED driver and thought they were now obsolete, but a swift look at Farnell's website has proved me wrong, and they are still manufactured by Microchip. But the PIC solution would be very slightly cheaper.

The software has now been improved after many miles of testing. My GPS receiver is confused by heavy foliage and that results in invalid data blocks for up to 2% of the journey. The ability to see the 'V' displayed is handy; before that change was made, the display mysteriously froze for about 20 seconds, because sync was lost, and now I realise why that was.

The code for GPSSetup: 

  1. #! /usr/bin/python3
  2. # above line is 'shebang line'
  3. # GPS Test Program
  4. # APRW 29-04-2020
  6. # set Garmin to output only GPRMC NMEA sentence.
  7. # read data
  8. # generate average speed
  9. # display average speed
  11. # Gadget Port commands:
  12. # c = clear av speed
  13. # m = monitor GPS input
  14. # wr/w1 = write to GPS unit : r reset output/ 1 = allow RMC only
  15. # n = no monitor output
  16. # gga = send GPGGA sentences
  17. # gsv = send GPGSV
  18. # rmc = send GPRMC
  19. # f = flush input
  20. import serial
  21. # no module called: import getch
  23. sv_speed_kts=0.0
  24. av_speed_samples=0
  25. av_speed_acc=0.0
  27. def clear_av_speed():
  28. av_speed_kts=0.0
  29. av_speed_samples=0
  30. av_speed_acc=0.0
  31. print ('Averages cleared.')
  33. def reset_GPS():
  34. # send $PGRMO,,2<cr>,<lf> to ttyS1
  35. tty1.write(b'$PGRMO,,2*75rn')
  36. print ('Sent $PGRMO,,2*75 cmd')
  38. def send_RMC():
  39. # send $PGRMO,GPRMC,1<cr><lf> to ttyS1 ( tty1)
  41. tty1.write(b'$PGRMO,GPRMC,1*3Drn')
  42. print ('Sent PGRMO,GPRMC,1*3D cmd')
  44. def send_GSV():
  45. tty1.write(b'$PGRMO,GPGSV,1*23rn')
  47. def send_GGA():
  48. tty1.write(b'$PGRMO,GPGGA,1*20rn')
  50. #main
  51. tty1=serial.Serial('/dev/ttyS1',4800,timeout=1)
  52. print ('GPSX1 GPS Average Speed Program. rnOpening ttyS1...%s'
  53. run_mode=True
  54. tty1.flushInput()
  56. while run_mode == True:
  57. print('Enter cmd: c/wr/w1/m/n/x/X:')
  58. cmd=input() # renamed input from raw_input
  59. if (cmd=='x' or cmd=='X'):
  60. run_mode=False
  61. elif (cmd=='c'):
  62. clear_av_speed()
  63. elif (cmd=='wr'):
  64. reset_GPS()
  65. elif (cmd=='gga'):
  66. send_GGA()
  67. elif (cmd=='gsv'):
  68. send_GSV()
  69. elif (cmd=='rmc'):
  70. send_RMC()
  71. elif (cmd=='f'):
  72. tty1.flushInput()
  73. line_in= tty1.readline()
  74. print ('Input: %s' %line_in)
  75. tty1.close()

The above only has to run once!

And (latest version with fonts etc is here:

  1. #! /usr/bin/python3
  3. #
  4. # Font ses QBI 24-point bitmap to display average speed.
  5. # Hardware: GPS on serial UART and SSD1306 on I2C bus, button on IO port
  6. # Feed GPS serial data into AD3.
  7. # Wed 13th May 2020 APRW
  8. # Mon 18th May 2020 APRW Add digital in on AD4.
  9. # Tue 19th May 2020 APRW Add button on AD1, log data into logs directory.
  10. # $GPRMC,time,A/V,Lat,Long,spd, dir, date, offsets, csum
  11. # 0 1 2 3 5 7 8 9 10 12 # split
  12. # display is 128 x 64 dots.
  13. # DIP Switch inverts display for HUD mode.
  14. # Press button clears average speed if pressed for a second or so
  15. # Press button exits program if held for 10 secs or so.
  16. # Tue 2nd June 2020 APRW New feature: toggle average/current speed display.
  17. # Tue 2nd June 2020 APRW Press button mods to change mode.
  18. # Wed 3rd June 2020 APRW try time.monotonic to check loop time.
  19. # Thu 4th June 2020 APRW Manifest constant Debug Mode to
  20. # stop/allow debug print
  21. # Thu 11th June 2020 APRW Add state machine.
  22. # Thu 11th June 2020 APRW Try digital outputs.
  23. # Sun 14th June 2020 APRW getline() mods for split data. L.
  24. # Mon 15th June 2020 APRW Check input is GPRMC and has >=11 commas
  25. # Tue 16th June 2020 APRW Store valid data in one file, junk in another.
  27. Debug_mode=False
  29. import board
  30. import adafruit_ssd1306
  31. import serial
  32. import busio
  33. import time
  34. import binascii
  35. import digitalio
  36. from enum import Enum
  39. class ModeEnum(Enum):
  40. START = 0
  41. RUN = 1
  42. END = 2
  45. # convert offset to 32-bit mask as my font is not in bit order.
  46. # Each 32-bit font row is in byte order e.g.
  47. # ....**** **...... .**...** *....... (left to right pixels)
  48. # 0x0F 0xC0 0x63 0x80
  49. lut_dict={0:0x80,1:0x40,2:0x20,3:0x10,4:0x8,5:0x4,6:0x2,7:1,
  50. 8:0x8000,9:0x4000,10:0x2000,11:0x1000,12:0x800,13:0x400,14:0x200,15:0x100,
  51. 16:0x800000,17:0x400000,18:0x200000,19:0x100000,20:0x80000,21:0x40000,22:0x20000,23:0x10000}
  54. # These constants relate to my 36-pt font and 24-pt font in a 32 bit x 32 bit matrix
  55. FontBytesPerRow = 4
  56. FontCharPitch = 128
  57. FontRowsPerChar = 4
  58. FontBitsPerCharRow = 24
  59. FontTopMargin = 24
  60. FontCharScanLines = 16
  61. row_offset = 1
  63. # display one large character at a time, xpos 0 for top left.
  64. # siz = 2 is 2 dots in row and 2 in column expansion
  65. # siz = 3 is 2 dots wide and 3 high expansion
  67. # inv_bool is true/false to invert characters
  68. def disp_txt(ch, xpos, siz, inv_bool):
  70. fhfont= open("/home/debian/24REGA.RES","rb") # full path required
  71. res_offset = ord(ch) # find offset into font file
  72. res_offset -= ord(' ') # remove space as font starts at ch = 20h
  73. res_offset *= FontCharPitch
  74. res_offset += FontTopMargin # now we know exactly where to seek
  75. # read out 4 bytes at this file offset, as there are 4 bytes per dot row.
  76. ( res_offset, 0 ) # 0 is SEEK from start of file
  77. dat =
  78. ldat = int.from_bytes(dat,"little", signed = False) # convert to a long
  79. # print (hex(ldat)) ...uncomment to see what top row looks like
  80. for row in range (0, FontCharScanLines,1):
  81. if (inv_bool == True ): row = FontCharScanLines - row
  82. row += row_offset # to fit smaller characters centrally
  83. for col in range (0, FontBitsPerCharRow,1):
  84. if (ldat & lut_dict[col]):
  85. if siz==1:
  86. oled.pixel((col+xpos), row, 1)
  87. if (siz==2):
  88. oled.pixel(xpos+col*2 + 1, row*2, 1)
  89. oled.pixel(xpos+col*2 + 1, row*2,1)
  90. oled.pixel(xpos+col*2, row*2 + 1,1)
  91. oled.pixel(xpos+col*2 + 1, row*2 + 1,1)
  92. if (siz==3):
  93. oled.pixel(xpos+col*2, row*3, 1) # these could be a pythonic
  94. oled.pixel(xpos+col*2, row*3 + 1, 1) # rewrite
  95. oled.pixel(xpos+col*2, row*3 + 2, 1)
  96. oled.pixel(xpos+col*2 + 1, row*3, 1)
  97. oled.pixel(xpos+col*2 + 1, row*3 + 1, 1)
  98. oled.pixel(xpos+col*2 + 1, row*3 + 2, 1)
  101. dat = # read another 4 bytes
  102. ldat = int.from_bytes(dat,"little", signed = False)
  104. fhfont.close()
  107. def disp_curspd(cspd, invert): # display current speed
  108. # toggle horizontal or vertical border on and off
  109. global edge
  110. # global size
  111. oled.fill(0) # clear display buffer
  112. fcspd = float(cspd) # to a float from 000.0
  113. spd = fcspd*1.15078 # convert kts to mph
  114. if (spd>0.0):
  115. spd_str = str(spd)
  116. else:
  117. spd_str = '0.00' # convert to string
  118. digits = len (spd_str) # how many to display
  119. # print (spd_str+' '+str(digits)) # debug only
  120. if digits>4:
  121. digits=4
  122. for i in range (digits):
  123. if spd_str[i]=='.':
  124. disp_txt(spd_str[i],30*i+21,size, invert)
  125. else:
  126. disp_txt(spd_str[i],30*i+6, size, invert)
  127. if edge>1:
  128. edge=0
  129. oled.line(0,0,127,0,1) # top line
  130. oled.line(0,63, 127,63,1) # bottom line
  131. else:
  132. oled.line(0,0,0,63,1) # left line
  133. oled.line(127,0,127,63,1) # right line
  134. # do later in state_machine 11-6-2020 as it takes 100ms
  135. edge+=1
  138. def disp_av(acc, ctr, invert): # display average speed
  139. global edge
  140. x1=(0,64,127,63,0,0) # 4 tuples for edge border
  141. x2=(64,127,127,127,63,0) # so oled.line can be used to
  142. y1=(0,0,0,63,63,0) # draw the moving border
  143. y2=(0,0,63,63,63,63)
  144. oled.fill(0) # clear the display
  146. if (ctr==0):
  147. ctr=1 # prevent divide by zero
  148. av_spd_kts = acc/ctr # divide accumulator by number of samples
  149. av_spd_mph = av_spd_kts * 1.15078 # convert from knots to mph
  150. av_spd_str = str(av_spd_mph) # Convert float to string
  151. digits = len(av_spd_str) # how many digits to display?
  153. if digits>4:
  154. digits=4
  155. for i in range (digits):
  156. if (av_spd_str[i]=='.'):
  157. disp_txt(av_spd_str[i],30*i+21,size, invert)
  158. else:
  159. disp_txt(av_spd_str[i],30*i+6,size,invert)
  160. if (edge >5):
  161. edge = 0
  162. oled.line(x1[edge],y1[edge],x2[edge],y2[edge],1)
  164. edge += 1 # move the 64 pixel border line along for next time
  165. # in state_machine 11-6-20 # display digits plus border line
  167. def log_data(date, strdata):
  168. logf = open('/home/debian/logs/'+date, "a+") # generate absolute path
  169. logf.write(strdata) # must be str not bytes...
  170. logf.write("n") # whack a newline at the end of each row
  171. logf.close()
  173. #
  174. # void main(void) # start here!...
  175. #
  176. i2c = busio.I2C(board.SCL, board.SDA)
  177. oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c)
  178. oled.fill(0) # clear the display buffer
  179. # GPS serial data input on this port...
  180. tty1 = serial.Serial('/dev/ttyS1',4800,timeout=0.1) # timeout 100ms
  181. # operator push button...
  182. button = digitalio.DigitalInOut(board.AD1)
  183. button.direction = digitalio.Direction.INPUT
  184. # button.pull = digitalio.Pull.UP ! error not implemented !
  185. # button.switch_to_input(pull=digitalio.Pull.UP) ! ditto !
  187. # invert display switch on AD4...
  188. inv_sw = digitalio.DigitalInOut(board.AD4)
  189. inv_sw.direction = digitalio.Direction.INPUT
  191. PWM1_Out = digitalio.DigitalInOut(board.PWM1) # to time functions
  192. PWM1_Out.direction = digitalio.Direction.OUTPUT
  194. PWML1_Out = digitalio.DigitalInOut(board.PWML1)
  195. PWML1_Out.direction = digitalio.Direction.OUTPUT
  197. PWM2_Out = digitalio.DigitalInOut(board.PWM2)
  198. PWM2_Out.direction = digitalio.Direction.OUTPUT
  200. PWM3_Out = digitalio.DigitalInOut(board.PWM3)
  201. PWM3_Out.direction = digitalio.Direction.OUTPUT
  204. oled.rect(0,0,127,63,1) # a border...
  205. # Show s/w version on oled...
  206. invert = inv_sw.value
  207. size = 3
  209. edge = 0 # display rotating border when running using edge
  210. disp_txt("A",6, size, invert)
  212. time.sleep(1)
  213. disp_txt("V",36, size, invert)
  215. time.sleep(1)
  216. disp_txt("1",64, size, invert)
  218. time.sleep(1)
  219. disp_txt("N",90, size, invert)
  221. time.sleep(1)
  224. # flush GPS serial buffer as lots of stuff could be in queue...
  225. tty1.flushInput()
  226. time.sleep(1)
  227. spd_acc=0.0 # accumulate speed, kts
  228. spd_ctr=0 # count samples
  230. run_mode = ModeEnum.RUN
  231. oled.fill(0) # clear oled
  232. disp_txt("*",6,size,invert)
  234. disp_mode=0 # for av speed (0) or current speed (1)
  235. button_down_ctr = 0 # see how long button is pressed.
  236. state_machine = 0 # split read data / print / update OLED
  237. str_in='' # clear line in so it can be concatenated
  238. while run_mode == ModeEnum.RUN:
  239. if button.value == 0: # first of all check the push button
  240. button_down_ctr += 1
  241. # print (str(button_down_ctr))
  242. if button_down_ctr>=30 : # 10 seconds - ish! 3 loops per second
  243. run_mode = ModeEnum.END # end program if button held for > 10 secs
  244. oled.fill(0) # clear the display
  246. else:
  247. if button_down_ctr>0: # had been pressed so check time
  248. if button_down_ctr<3: # under 1 second so clear average speed
  249. spd_acc=0
  250. spd_ctr=0
  251. if Debug_mode: print ("Clr...")
  252. elif button_down_ctr>=6: # 2 seconds at 250ms per loop - ish!
  253. disp_mode^=1 # XOR current speed bit
  254. if Debug_mode: print ("Disp"+str(disp_mode))
  255. button_down_ctr = 0
  256. # end of read button section.
  257. # Now see if serial data sentence in from GPS unit...
  258. if state_machine == 0:
  259. PWML1_Out.value = True # time the function
  260. line_in = str(tty1.readline())
  261. PWML1_Out.value = False
  262. if len(line_in)>1:
  263. PWM1_Out.value = True # time when data rxd
  264. if Debug_mode:
  265. disptime = time.monotonic()
  266. print('d')
  267. print (disptime) # see how long sections take to run
  268. print (line_in)
  269. str_in=line_in # no need to concatenate
  270. if len(str_in)>60:
  271. if str_in.find('$GPRMC')>0: # or str_in.find('$GNRMC')>0: # or GNRMC of course!
  272. state_machine = 1
  273. else:
  274. str_in='' # clear and start again
  275. PWM1_Out.value = False
  276. elif state_machine ==1: # we have got a sentence so split on commas
  277. PWM2_Out.value = True # time this function with MSO
  278. invert = inv_sw.value
  279. line_in=''
  280. # print ( str_in) # uncomment to print all GPS info to GadgetPort
  281. # print ('commas='+str(str_in.count(','))) # should be 11 commas
  282. if str_in.count(',') < 11:
  283. oled.fill(0)
  284. disp_txt("C", 6, size, invert) # 'C' means comma count error
  285. state_machine = 2 # display it
  287. else:
  288. data = str_in.split(',')
  289. if data[2]=='A': # A=valid data
  290. spd_acc+= float(data[7]) # speed in knots from GPS
  291. spd_ctr+=1 # (1 second per) sample
  292. if disp_mode & 1:
  293. disp_curspd(data[7], invert)
  294. else:
  295. disp_av(spd_acc, spd_ctr, invert)
  296. state_machine = 2 # display on next loop
  297. log_data(data[9], str_in) # log good blocks here
  298. duff_data = False
  299. # log_data(data[9],str_in) # log valid data, log filename = date
  300. elif (data[2]=='V'): # debug only!!! ZZZ
  301. oled.fill(0) # clear display
  302. disp_txt("V", 6, size, invert) # invalid data
  303. state_machine = 2
  304. duff_data = True
  305. else:
  306. oled.fill(0)
  307. disp_txt("?", 6, size, invert) # something wrong!
  308. state_machine = 2 # get more data as sentence not valid.
  309. duff_data = True
  310. # log_data(data[9],str_in) # log all rxd data. Filename = date (data[9])
  311. PWM2_Out.value = False # end timing, MSO says 50ms
  312. if duff_data is True:
  313. log_data(data[9]+'x',str_in) # attempt to log in date+x file
  314. elif state_machine ==2:
  315. PWM3_Out.value = True # see how long takes
  316. # MSO says this takes exactly 100ms at 100kHz I2C speed
  317. PWM3_Out.value = False
  318. str_in = ''
  319. state_machine = 0
  320. time.sleep(0.1) # delay of 100ms for a loop time of 200ms
  321. if Debug_mode: # remember wait for serial data is blocked for up to 100ms.
  322. looptime=time.monotonic()
  323. print (looptime)
  324. tty1.close()
  325. # the end...

Schematics and circuit diagrams

GPS - Giant Board - OLED display

Two transistor interface - see text.


Python3 code for GPS average speed display unit with Giant Board

Latest version


Python3 code to train GPS unit to send one sentence only.


Photo of Al


Been a keen radio amateur for many years. HF CW activity when I get the chance...homebrew radios and gadgets from valve amplifiers using EL84s etc in the past to a TV sound-bar using an Arduino and Class-D amplifier. I have been trying to build an electronic organ for a while but technology changes before I get anywhere!


Leave your feedback...