07 - GPS Positioning Application
This chapter describes the GPS positioning application example — gps_display — on the Pico-G1 expansion board. The application demonstrates how to read NMEA-0183 data from a GPS module over a UART interface, parse the positioning information, and display it in real time on a TFT screen. It is a practical example for learning serial communication and protocol parsing, showing a complete technology stack from low-level UART operations to high-level data processing.
The application source code is located in the SDK directory source/app/07_gps_display/. It provides a complete implementation of UART character-device operations and NMEA protocol parsing, and is a valuable reference for learning embedded communication protocols.
1 Application Overview
1.1 Features
- UART serial communication: demonstrates serial communication through the Linux UART character-device interface
- NMEA protocol parsing: fully parses the NMEA-0183 protocol (GGA/RMC sentences)
- Positioning information display: displays latitude/longitude, time, date, speed, altitude, and more in real time
- TFT color display: drives an ST7789 display over the SPI interface to show positioning data
- PPS pulse detection: supports monitoring of the GPS pulse-per-second signal
- Multi-module compatibility: supports GPS ($GP), GNSS ($GN), BeiDou ($BD), and other modules
1.2 Technical Specifications
| Parameter | Value |
|---|---|
UART interface | UART1 (/dev/ttyAMA1) |
Baud rate | 9600 8N1 (configurable) |
NMEA protocol | NMEA-0183 standard |
Supported sentences | GGA (positioning data), RMC (recommended minimum data) |
TFT display | ST7789 240×240 RGB565 |
Refresh interval | 500 ms (configurable) |
PPS detection | GPIO4_6 input (optional) |
1.3 Test Case List
| index | Name | Test command | Expected result (success) | Possible causes of failure |
|---|---|---|---|---|
| 1 | Basic fixing | ./gps_display | Screen shows positioning info (lat/lon, time, etc.) | GPS module not connected, wrong UART config |
| 2 | Indoor test | ./gps_display | Shows "NO FIX" (cannot get a fix) | Normal behavior; GPS needs an outdoor signal |
| 3 | Outdoor test | Move outdoors | Shows "FIX VALID" + concrete positioning data | Faulty GPS module, antenna connection issue |
| 4 | NMEA data | cat /dev/ttyAMA1 | Shows $GPGGA/$GPRMC and similar sentences | UART connection failed, baud rate mismatch |
1.4 Directory Structure
source/app/07_gps_display/
├── Makefile # Build script
├── main.c # Main program
├── uart_hal.c # UART HAL layer implementation
├── uart_hal.h # UART HAL layer header
├── nmea.c # NMEA protocol parsing implementation
├── nmea.h # NMEA protocol parsing header
├── gpio_hal.c # GPIO HAL layer implementation
├── gpio_hal.h # GPIO HAL layer header
├── spi_hal.c # SPI HAL layer implementation
├── spi_hal.h # SPI HAL layer header
├── st7789.c # ST7789 driver implementation
├── st7789.h # ST7789 driver header
├── font8x16.h # 8×16 ASCII bitmap font
└── README.md # Documentation2 Hardware Connection
2.1 Pin Definitions
| Signal | On-board GPIO | iocfg pad | func | Device node |
|---|---|---|---|---|
| GPS TX → SoC RX | GPIO3_6 | 0x100C0004 | func3(UART1_RXD)=0x1003 | /dev/ttyAMA1 |
| GPS RX ← SoC TX | GPIO3_5 | 0x100C0000 | func3(UART1_TXD)=0x1003 | /dev/ttyAMA1 |
| PPS (optional) | GPIO4_6 | 0x100C0024 | func5(GPIO)=0x1005 | /dev/gpiochip4 line6 |
| Display | SPI2 | — | — | /dev/spidev2.0 |
| VCC / GND | 3.3V / GND | — | — | — |
Wiring notes
- Connect GPS-TX to GPIO3_6 (SoC UART1_RXD)
- Connect GPS-RX to GPIO3_5 (SoC UART1_TXD)
- TX/RX must not be swapped, otherwise communication fails
- PPS is the pulse-per-second signal; it is optional and used for precise time synchronization
2.2 Hardware Circuit
Standard GPS module wiring:
Pico-G1 GPS Module
┌───────────┐ ┌──────────────┐
│ │ │ │
│ GPIO3_6 ──┼────── RX ──┤ TX │
│ │ │ │
│ GPIO3_5 ──┼────── TX ──┤ RX │
│ │ │ │
│ GPIO4_6 ──┼────── PPS ──┤ PPS (optional)│
│ │ │ │
│ 3.3V ──┼─────────────┤ VCC │
│ │ │ │
│ GND ──┼─────────────┤ GND │
│ │ │ │
└───────────┘ └──────────────┘GPS module power supply
Most GPS modules are powered at 3.3V. Check the module specifications to avoid damage.
2.3 Pin Multiplexing
Pin multiplexing that needs to be configured:
| pad | Physical address | Value | Description |
|---|---|---|---|
| UART1_TXD (GPIO3_5) | 0x100C0000 | 0x1003 | func3 = UART1_TXD |
| UART1_RXD (GPIO3_6) | 0x100C0004 | 0x1003 | func3 = UART1_RXD |
| PPS (GPIO4_6) | 0x100C0024 | 0x1005 | func5 = GPIO (default JTAG_TRSTN) |
JTAG pin conflict
GPIO4_6 defaults to the JTAG function and must be configured as GPIO before it can be used as the PPS input.
3 Build and Deployment
3.1 Prerequisites
Before building this application, make sure the following preparations are done:
- SDK environment is set up: refer to Development Environment Setup to configure the cross-compilation toolchain and the SDK
- GPS module is connected: the GPS module is correctly connected to the UART1 interface
- Antenna is installed: the GPS module antenna is properly attached
3.2 Build the Application
# Set the toolchain path
export PATH=$PATH:<SDK>/tools/linux/toolchains/arm-gcc12.2.0-linux-uclibceabi/bin
# Enter the example directory
cd <SDK>/source/app/07_gps_display
# Build
make
# Clean
make cleanAfter a successful build, the executable gps_display is generated in the current directory.
3.3 Deploy to the Board
# Transfer to the development board with SCP
scp gps_display root@<board_ip>:/usr/bin/
# Or download via TFTP
tftp -g -r gps_display <board_ip>3.4 Run the Application
# Add execute permission
chmod +x /usr/bin/gps_display
# Run the GPS display example
/usr/bin/gps_displayAfter the application starts, the TFT screen shows the GPS positioning information and refreshes every 500 ms. Press Ctrl+C to exit.
3.5 Expected Output
Console output
/mnt # ./gps_display
[gps] pad 复用:UART1(3_5/3_6)->func3,GPIO4_6(PPS)->func5
[gps] pad 0x100C0000 -> 0x00001003
[gps] pad 0x100C0004 -> 0x00001003
[gps] pad 0x100C0024 -> 0x00001005
[gps] 初始化 SPI 屏(/dev/spidev2.0)...
[spi] pad 0x100C0028 -> 0x00001004
[spi] pad 0x100C002C -> 0x00001004
[spi] pad 0x100C0030 -> 0x00001000
[spi] pad 0x100C0020 -> 0x00001005
[spi] pad 0x100C001C -> 0x00001005
[spi] opening /dev/spidev2.0 ...
[spi] spidev opened, fd=3
[spi] spidev mode/bits/speed set (MODE3/8b/24MHz)
[spi] chardev request DC @ /dev/gpiochip4 line 5 ...
[spi] chardev request RES @ /dev/gpiochip4 line 4 ...
[spi] chardev request CS @ /dev/gpiochip5 line 1 ...
[spi] chardev-verify: DC=0(expect0) RES=1(expect1) CS=1(expect1) ==> OK(chardev 真驱动了引脚)
[spi] spi_hal_init done
[tft] init: SLPOUT
[tft] init: SLPOUT +120ms ok
[tft] init: config cmds ok
[tft] init: DISPON
[tft] init: DISPON ok
[tft] init: clear-flush start
[tft] flush #1 start
[tft] flush #1 done
[tft] init: clear-flush done
[gps] 打开 UART(/dev/ttyAMA1)...
[uart] /dev/ttyAMA1 open, 9600 8N1 raw (VMIN=0 VTIME=2)
[gps] 申请 PPS 输入(GPIO4_6)...
[gps] 接收 NMEA 中,每 500ms 刷新,Ctrl+C 退出。(室内通常 NO FIX,到窗户/室外定位)TFT screen display

Fixed and variable parts
- Fixed parts: the display format and layout (fixed by the code)
- Variable parts: the GPS positioning data (updated on every refresh)
4 Internal Execution Logic
4.1 Application Architecture
The application uses a layered design consisting of a hardware abstraction layer, a protocol parsing layer, and an application layer:
// Application layer (main.c)
int main(int argc, char *argv[])
{
// 1. Initialize pin multiplexing
padmux_init();
// 2. Initialize the UART HAL layer
uart_init();
// 3. Initialize the SPI TFT
spi_init();
tft_init();
// 4. Initialize GPIO (PPS)
gpio_init();
// 5. Main loop
while (1) {
// Read NMEA data
char line[256];
if (uart_read_line(line, sizeof(line)) > 0) {
// Parse the NMEA sentence
if (strncmp(line, "$GPGGA", 6) == 0 ||
strncmp(line, "$GNGGA", 6) == 0 ||
strncmp(line, "$BDGGA", 6) == 0) {
nmea_parse_gga(line, &gps_data);
}
else if (strncmp(line, "$GPRMC", 6) == 0 ||
strncmp(line, "$GNRMC", 6) == 0 ||
strncmp(line, "$BDRMC", 6) == 0) {
nmea_parse_rmc(line, &gps_data);
}
// Refresh the display
tft_display_gps_info(&gps_data);
}
usleep(500000); // 500 ms refresh interval
}
return 0;
}4.2 UART HAL Implementation
The UART HAL layer wraps Linux UART character-device operations:
// UART initialization
int uart_init(void)
{
int fd = open("/dev/ttyAMA1", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("打开 UART 设备失败");
return -1;
}
// Configure termios
struct termios options;
tcgetattr(fd, &options);
// Set the baud rate to 9600
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
// 8N1 configuration
options.c_cflag &= ~PARENB; // No parity
options.c_cflag &= ~CSTOPB; // 1 stop bit
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 8 data bits
// Raw mode (no processing)
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
// Set timeouts
options.c_cc[VTIME] = 1; // 0.1 s timeout
options.c_cc[VMIN] = 0; // Non-blocking
tcsetattr(fd, TCSANOW, &options);
return fd;
}
// Read NMEA data line by line
int uart_read_line(char *buffer, int max_len)
{
int pos = 0;
char ch;
while (pos < max_len - 1) {
if (read(uart_fd, &ch, 1) <= 0) {
break; // Read failed or timed out
}
if (ch == '\n') {
break; // End of line
}
if (ch != '\r') { // Skip carriage returns
buffer[pos++] = ch;
}
}
buffer[pos] = '\0';
return pos;
}4.3 NMEA Protocol Parsing
The NMEA protocol parsing layer implements GGA and RMC sentence parsing:
// NMEA data structure
typedef struct {
char time[16]; // HH:MM:SS
char date[16]; // YYYY/MM/DD
char lat[16]; // ddmm.mmmm N/S
char lon[16]; // dddmm.mmmm E/W
int fix_quality; // 0=no fix, 1=GPS, 2=DGPS
int num_sats; // Number of satellites
float altitude; // Altitude (meters)
float speed; // Speed (knots)
char status; // A=valid, V=warning
} nmea_data_t;
// Parse a GGA sentence
void nmea_parse_gga(const char *sentence, nmea_data_t *data)
{
char *token;
int field = 0;
// Copy the sentence for parsing
char buffer[256];
strncpy(buffer, sentence, sizeof(buffer));
// Split the fields
token = strtok(buffer, ",");
while (token != NULL && field < 15) {
switch (field) {
case 1: // Time
strncpy(data->time, token, sizeof(data->time) - 1);
break;
case 2: // Latitude
strncpy(data->lat, token, sizeof(data->lat) - 1);
break;
case 3: // Latitude direction
strcat(data->lat, " ");
strcat(data->lat, token);
break;
case 4: // Longitude
strncpy(data->lon, token, sizeof(data->lon) - 1);
break;
case 5: // Longitude direction
strcat(data->lon, " ");
strcat(data->lon, token);
break;
case 6: // Fix quality
data->fix_quality = atoi(token);
break;
case 7: // Number of satellites
data->num_sats = atoi(token);
break;
case 9: // Altitude
data->altitude = atof(token);
break;
}
token = strtok(NULL, ",");
field++;
}
}
// Parse an RMC sentence
void nmea_parse_rmc(const char *sentence, nmea_data_t *data)
{
// Similar to GGA parsing; handles time, date, status, speed, etc.
// ... implementation ...
}4.4 TFT Display Update
The TFT display layer visualizes the positioning information:
// Display the GPS positioning information
void tft_display_gps_info(const nmea_data_t *data)
{
char buffer[64];
// Clear the screen
st7789_clear();
// Title
st7789_text(0, 0, "GPS Display 240x240", WHITE, BLACK);
// Satellite count and time
snprintf(buffer, sizeof(buffer), "SAT: %02d TIME: %s",
data->num_sats, data->time);
st7789_text(0, 24, buffer, WHITE, BLACK);
// Date
snprintf(buffer, sizeof(buffer), "DATE: %s", data->date);
st7789_text(0, 48, buffer, WHITE, BLACK);
// Latitude
snprintf(buffer, sizeof(buffer), "LAT: %s", data->lat);
st7789_text(0, 72, buffer, WHITE, BLACK);
// Longitude
snprintf(buffer, sizeof(buffer), "LON: %s", data->lon);
st7789_text(0, 96, buffer, WHITE, BLACK);
// Altitude
snprintf(buffer, sizeof(buffer), "ALT: %.1f m", data->altitude);
st7789_text(0, 120, buffer, WHITE, BLACK);
// Speed
snprintf(buffer, sizeof(buffer), "SPD: %.1f knots", data->speed);
st7789_text(0, 144, buffer, WHITE, BLACK);
// Fix status
if (data->fix_quality > 0) {
st7789_text(0, 168, "FIX: VALID", GREEN, BLACK);
} else {
st7789_text(0, 168, "FIX: NO FIX", RED, BLACK);
}
// PPS status
int pps = gpio_get_value(PPS_GPIO);
snprintf(buffer, sizeof(buffer), "PPS: %d", pps);
st7789_text(0, 192, buffer, WHITE, BLACK);
// Refresh the display
st7789_refresh();
}5 Key Programming Points
5.1 UART Configuration Parameters
Key termios parameters:
// Baud rate (9600)
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
// 8N1 configuration
options.c_cflag |= CS8; // 8 data bits
options.c_cflag &= ~PARENB; // No parity
options.c_cflag &= ~CSTOPB; // 1 stop bit
// Raw mode
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);Common baud rates
- 9600: the default baud rate of GPS modules
- 4800: some older GPS modules
- 38400: some high-precision GPS modules
- 115200: some modern GPS modules
5.2 NMEA Sentence Format
GGA sentence format:
$GPGGA,hhmmss.ss,llll.llll,a,yyyyy.yyyy,a,x,xx,x.x,M,x.x,M,x.x,xxxx*hh
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Checksum
│ │ │ │ │ │ │ │ │ │ │ │ │ │ └───── Blank system ID
│ │ │ │ │ │ │ │ │ │ │ │ │ └─────────── Differential station ID
│ │ │ │ │ │ │ │ │ │ │ │ └─────────────── Altitude unit
│ │ │ │ │ │ │ │ │ │ │ └──────────────────── Geoid separation
│ │ │ │ │ │ │ │ │ └──────────────────────── Altitude
│ │ │ │ │ │ │ │ └────────────────────────── HDOP
│ │ │ │ │ │ │ └──────────────────────────── Fix quality
│ │ │ │ │ │ └───────────────────────────── Satellites in use
│ │ │ │ │ └──────────────────────────────────── Longitude direction
│ │ │ │ └────────────────────────────────────── Longitude
│ │ │ └──────────────────────────────────────── Latitude direction
│ │ └─────────────────────────────────────────────── Latitude
│ └────────────────────────────────────────────────── UTC time
└────────────────────────────────────────────────────────── Talker ID5.3 Fix Quality Interpretation
Meaning of the fix_quality field:
switch (data->fix_quality) {
case 0: // No fix
// Display "NO FIX"
break;
case 1: // GPS fix
// Display "GPS FIX"
break;
case 2: // DGPS fix
// Display "DGPS FIX"
break;
default: // Other
// Display "UNKNOWN"
break;
}5.4 PPS Pulse Detection
PPS (pulse per second) is used for precise time synchronization:
// Configure the PPS GPIO as input
gpio_set_direction(PPS_GPIO, GPIO_INPUT);
// Read the PPS state
int pps = gpio_get_value(PPS_GPIO);
if (pps) {
// Pulse high: record the precise time
gettimeofday(&pps_time, NULL);
}6 Code Customization
6.1 Changing the UART Baud Rate
Edit the baud rate settings in uart_hal.c:
// 9600 (GPS default)
cfsetispeed(&options, B9600);
cfsetospeed(&options, B9600);
// 4800 (older GPS)
cfsetispeed(&options, B4800);
cfsetospeed(&options, B4800);
// 38400 (high-precision GPS)
cfsetispeed(&options, B38400);
cfsetospeed(&options, B38400);6.2 Adding More NMEA Sentences
Support other NMEA sentences:
// Parse GSA sentences (DOP data)
void nmea_parse_gsa(const char *sentence, nmea_data_t *data)
{
// Parse PDOP/HDOP/VDOP accuracy information
}
// Parse GSV sentences (satellite information)
void nmea_parse_gsv(const char *sentence, nmea_data_t *data)
{
// Parse satellite azimuth, elevation, and SNR
}6.3 Implementing Data Logging
Record GPS track data:
// Open the track file
FILE *track_log = fopen("/mnt/sdcard/gps_track.log", "a");
// Record positioning data
if (data->fix_quality > 0) {
fprintf(track_log, "%s,%.8f,%.8f,%.1f,%.1f\n",
data->time,
parse_latitude(data->lat),
parse_longitude(data->lon),
data->altitude,
data->speed);
fflush(track_log);
}7 Troubleshooting
| Problem | Possible cause | Solution |
|---|---|---|
| Stuck on "NO FIX" | GPS cold start, weak indoor signal | Move outdoors or next to a window and wait for the cold start to finish |
| No data (SAT:00) | UART connection failed, baud rate mismatch | Check the TX/RX wiring, verify the baud rate config |
| Time shows "--:--:--" | NMEA data parsing failed | Check the NMEA sentence format, verify the parser |
| Lat/lon empty | No GPS fix, GGA sentences missing | Wait for a GPS fix, check for GGA sentences |
| Module unresponsive | Insufficient power, faulty module | Verify the 3.3V supply, replace the GPS module |
| Slow data updates | UART buffer issues, refresh interval too long | Optimize the read strategy, adjust the refresh rate |
GPS fix testing
The first fix of a GPS module can take from 30 seconds to several minutes. This is normal (cold start).
8 Advanced Extensions
8.1 Track Recording
Record and display the GPS movement track:
// Track point structure
typedef struct {
double lat;
double lon;
float altitude;
time_t timestamp;
} track_point_t;
// Track buffer
track_point_t track_buffer[1000];
int track_index = 0;
// Add a track point
void add_track_point(const nmea_data_t *data)
{
if (data->fix_quality > 0 && track_index < 1000) {
track_buffer[track_index].lat = parse_latitude(data->lat);
track_buffer[track_index].lon = parse_longitude(data->lon);
track_buffer[track_index].altitude = data->altitude;
track_buffer[track_index].timestamp = time(NULL);
track_index++;
}
}
// Display the track
void display_track(void)
{
for (int i = 0; i < track_index; i++) {
int x = longitude_to_x(track_buffer[i].lon);
int y = latitude_to_y(track_buffer[i].lat);
st7789_set_pixel(x, y, YELLOW);
}
st7789_refresh();
}8.2 Coordinate Conversion
Convert latitude/longitude to screen coordinates:
// Longitude/latitude to screen coordinates
int longitude_to_x(double lon)
{
// Simple linear mapping
return (int)((lon - LON_MIN) / (LON_MAX - LON_MIN) * TFT_WIDTH);
}
int latitude_to_y(double lat)
{
return (int)((lat - LAT_MIN) / (LAT_MAX - LAT_MIN) * TFT_HEIGHT);
}
// Draw the current-position marker
void draw_position_marker(double lon, double lat)
{
int x = longitude_to_x(lon);
int y = latitude_to_y(lat);
// Draw a cross marker
st7789_draw_hline(x - 5, x + 5, y, RED);
st7789_draw_vline(x, y - 5, y + 5, RED);
}8.3 NMEA Checksum Verification
Add NMEA checksum verification:
// Compute the NMEA checksum
uint8_t nmea_checksum(const char *sentence)
{
uint8_t checksum = 0;
int len = strlen(sentence);
for (int i = 1; i < len; i++) { // Skip the leading $
if (sentence[i] == '*') {
break; // End of the checksummed part
}
checksum ^= sentence[i];
}
return checksum;
}
// Verify the NMEA checksum
int nmea_verify(const char *sentence)
{
const char *asterisk = strrchr(sentence, '*');
if (!asterisk) {
return 0; // No checksum
}
uint8_t calculated = nmea_checksum(sentence);
uint8_t received = strtol(asterisk + 1, NULL, 16);
return (calculated == received);
}