481 lines
12 KiB
C
481 lines
12 KiB
C
/*
|
|
* gps_task.c
|
|
*
|
|
* Created on: Jan 20, 2018
|
|
* Author: chris.l
|
|
*/
|
|
|
|
#include "esp_gps_ntp.h"
|
|
|
|
#include "driver/uart.h"
|
|
#include "freertos/timers.h"
|
|
#include "sys/time.h"
|
|
#include "minmea.h"
|
|
|
|
static const char *TAG = "GPS";
|
|
|
|
#define RX_BUF_SIZE 255
|
|
|
|
#define ESP_INTR_FLAG_DEFAULT 0
|
|
|
|
// simple versions - we don't worry about side effects
|
|
#define MAX(a, b) ((a) < (b) ? (b) : (a))
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
|
|
#define VALIDITY_TIMER_MS 1010
|
|
#define PPS_VALID_COUNT 10 // must have at least this many "good" PPS interrupts to be valid
|
|
|
|
#define PPS_PIN (GPIO_NUM_19)
|
|
#define TXD_PIN (GPIO_NUM_17)
|
|
#define RXD_PIN (GPIO_NUM_16)
|
|
#define LED_PIN (GPIO_NUM_15)
|
|
|
|
#define GPS_UART (UART_NUM_2)
|
|
|
|
static TimerHandle_t timer;
|
|
static volatile uint32_t timeouts;
|
|
static volatile time_t seconds;
|
|
static volatile micros_t last_micros;
|
|
static volatile micros_t min_micros;
|
|
static volatile micros_t max_micros;
|
|
static volatile bool gps_valid;
|
|
static volatile bool valid;
|
|
static volatile uint32_t valid_in;
|
|
static volatile time_t valid_since;
|
|
static volatile uint32_t valid_count;
|
|
static volatile time_t initial_valid;
|
|
static uint32_t dispersion;
|
|
static volatile char reason[128];
|
|
|
|
typedef struct gps_status
|
|
{
|
|
bool valid;
|
|
int sat_count;
|
|
int fix_quality;
|
|
} GPSStatus;
|
|
|
|
static GPSStatus gps_status;
|
|
|
|
|
|
|
|
#ifdef LED_PIN
|
|
#define DEBUG_LED_ON() gpio_set_level(LED_PIN, 1)
|
|
#define DEBUG_LED_OFF() gpio_set_level(LED_PIN, 0)
|
|
#define DEBUG_LED_INIT() init_debug_led()
|
|
static void init_debug_led()
|
|
{
|
|
//
|
|
// set up the LED pin
|
|
//
|
|
|
|
gpio_config_t io_conf;
|
|
//interrupt disabled
|
|
io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
|
|
//set as input mode
|
|
io_conf.mode = GPIO_MODE_OUTPUT;
|
|
//bit mask of the pins that you want to set,e.g.GPIO18/19
|
|
io_conf.pin_bit_mask = (1ULL<<LED_PIN);
|
|
//disable pull-down mode
|
|
io_conf.pull_down_en = 0;
|
|
//disable pull-up mode
|
|
io_conf.pull_up_en = 0;
|
|
//configure GPIO with the given settings
|
|
gpio_config(&io_conf);
|
|
|
|
DEBUG_LED_OFF();
|
|
}
|
|
#else
|
|
#define DEBUG_LED_ON()
|
|
#define DEBUG_LED_OFF()
|
|
#define DEBUG_LED_INIT()
|
|
#endif
|
|
|
|
|
|
uint32_t getDispersion()
|
|
{
|
|
return dispersion;
|
|
}
|
|
|
|
bool isTimeValid()
|
|
{
|
|
return valid;
|
|
}
|
|
|
|
void getTime(time_t *secs, time_t *usecs)
|
|
{
|
|
*secs = seconds;
|
|
*usecs = micros() - last_micros;
|
|
}
|
|
|
|
void getGPSTaskStatus(GPSTaskStatus* status)
|
|
{
|
|
status->timeouts = timeouts;
|
|
status->seconds = seconds;
|
|
status->min_micros = min_micros;
|
|
status->max_micros = max_micros;
|
|
status->gps_valid = gps_valid;
|
|
status->valid = valid;
|
|
status->valid_in = valid_in;
|
|
status->valid_since = valid_since;
|
|
status->valid_count = valid_count;
|
|
status->initial_valid = initial_valid;
|
|
status->sat_count = gps_status.sat_count;
|
|
status->fix_quality = gps_status.fix_quality;
|
|
}
|
|
|
|
static void IRAM_ATTR pps_isr_handler(void* arg)
|
|
{
|
|
DEBUG_LED_ON();
|
|
micros_t cur_micros = micros();
|
|
// restart the validity timer
|
|
xTimerReset(timer, 0);
|
|
|
|
//
|
|
// trigger a display update each second
|
|
//
|
|
triggerDisplay();
|
|
|
|
//
|
|
// don't trust PPS if GPS is not valid.
|
|
//
|
|
if (!gps_valid)
|
|
{
|
|
DEBUG_LED_OFF();
|
|
return;
|
|
}
|
|
|
|
//
|
|
// if we are still counting down then keep waiting
|
|
//
|
|
if (valid_in)
|
|
{
|
|
--valid_in;
|
|
if (valid_in == 0)
|
|
{
|
|
// clear stats and mark us valid
|
|
min_micros = 0;
|
|
max_micros = 0;
|
|
valid = true;
|
|
valid_since = seconds;
|
|
++valid_count;
|
|
reason[0] = '\0';
|
|
if (initial_valid == 0)
|
|
{
|
|
initial_valid = seconds;
|
|
}
|
|
}
|
|
}
|
|
|
|
seconds++;
|
|
//
|
|
// the first time around we just initialize the last value
|
|
//
|
|
if (last_micros == 0)
|
|
{
|
|
last_micros = cur_micros;
|
|
DEBUG_LED_OFF();
|
|
return;
|
|
}
|
|
|
|
uint32_t micros_count = cur_micros - last_micros;
|
|
last_micros = cur_micros;
|
|
|
|
if (min_micros == 0 || micros_count < min_micros)
|
|
{
|
|
min_micros = micros_count;
|
|
}
|
|
|
|
if (micros_count > max_micros)
|
|
{
|
|
max_micros = micros_count;
|
|
}
|
|
|
|
DEBUG_LED_OFF();
|
|
}
|
|
|
|
//
|
|
// read a single NMEA record
|
|
//
|
|
static char *readNMEA()
|
|
{
|
|
static uint8_t line[RX_BUF_SIZE+1];
|
|
int size;
|
|
uint8_t *p = line;
|
|
*p = '\0';
|
|
while(1)
|
|
{
|
|
if (p >= line+RX_BUF_SIZE)
|
|
{
|
|
ESP_LOGI(TAG, "Read buffer overflow! size %d bytes: '%s'", p-line, line);
|
|
}
|
|
size = uart_read_bytes(GPS_UART, (unsigned char *)p, 1, portMAX_DELAY);
|
|
if (size == 1)
|
|
{
|
|
if (*p == '$')
|
|
{
|
|
//
|
|
// '$' starts a record so reset pointer to start
|
|
//
|
|
p = line;
|
|
*p = '$';
|
|
}
|
|
else if (*p == '\r' || line[0] != '$')
|
|
{
|
|
//
|
|
// skip newlin and any chars if buffer does not start with '$'
|
|
continue;
|
|
}
|
|
else if (*p == '\n')
|
|
{
|
|
//
|
|
// got a record!
|
|
//
|
|
*p = 0;
|
|
break;
|
|
}
|
|
p++;
|
|
}
|
|
}
|
|
|
|
return (char*)line;
|
|
}
|
|
|
|
|
|
|
|
static void IRAM_ATTR invalidate(const char* fmt, ...)
|
|
{
|
|
va_list argp;
|
|
va_start(argp, fmt);
|
|
if (reason[0] == '\0')
|
|
{
|
|
vsnprintf((char*)reason, sizeof(reason)-1, fmt, argp);
|
|
}
|
|
va_end(argp);
|
|
|
|
if (valid)
|
|
{
|
|
valid_since = seconds;
|
|
}
|
|
|
|
valid = false;
|
|
gps_valid = false;
|
|
last_micros = 0;
|
|
valid_in = 0;
|
|
}
|
|
|
|
static void init_pps()
|
|
{
|
|
//
|
|
// set up the PPS interrupt
|
|
//
|
|
|
|
gpio_config_t io_conf;
|
|
//interrupt of falling edge
|
|
io_conf.intr_type = GPIO_PIN_INTR_POSEDGE;
|
|
//set as input mode
|
|
io_conf.mode = GPIO_MODE_INPUT;
|
|
//bit mask of the pins that you want to set,e.g.GPIO18/19
|
|
io_conf.pin_bit_mask = (1ULL<<PPS_PIN);
|
|
//disable pull-down mode
|
|
io_conf.pull_down_en = 0;
|
|
//enable pull-up mode
|
|
io_conf.pull_up_en = 1;
|
|
//configure GPIO with the given settings
|
|
gpio_config(&io_conf);
|
|
|
|
//hook isr handler for specific gpio pin
|
|
gpio_isr_handler_add(PPS_PIN, pps_isr_handler, (void*)0);
|
|
}
|
|
|
|
static void init_uart()
|
|
{
|
|
//
|
|
// initialize the uart attached to the GPS module
|
|
//
|
|
const uart_config_t uart_config = {
|
|
.baud_rate = 9600,
|
|
.data_bits = UART_DATA_8_BITS,
|
|
.parity = UART_PARITY_DISABLE,
|
|
.stop_bits = UART_STOP_BITS_1,
|
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
|
|
};
|
|
uart_param_config(GPS_UART, &uart_config);
|
|
uart_set_pin(GPS_UART, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
|
// We won't use a buffer for sending data.
|
|
uart_driver_install(GPS_UART, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
|
|
}
|
|
|
|
static void IRAM_ATTR invalidateTime()
|
|
{
|
|
// only count timeouts after we have been valid once.
|
|
if (initial_valid != 0)
|
|
{
|
|
++timeouts;
|
|
}
|
|
xTimerStart(timer, 0);
|
|
invalidate("TIMEOUT");
|
|
//
|
|
// trigger a display update each timeout
|
|
//
|
|
triggerDisplay();
|
|
}
|
|
|
|
static void init_timer()
|
|
{
|
|
timer = xTimerCreate("MyTimer", pdMS_TO_TICKS(VALIDITY_TIMER_MS), pdFALSE, NULL, &invalidateTime);
|
|
xTimerStart(timer, 0);
|
|
}
|
|
|
|
static void gps_task()
|
|
{
|
|
static bool last_valid = false;
|
|
static bool last_gps_valid = false;
|
|
static int timewarps = 0;
|
|
|
|
DEBUG_LED_INIT();
|
|
init_uart();
|
|
|
|
//install gpio isr service
|
|
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
|
|
|
|
init_pps();
|
|
init_timer();
|
|
|
|
while (1) {
|
|
|
|
//
|
|
// Print valid or invalid if status has changed.
|
|
//
|
|
if ((last_valid && !valid) || (last_gps_valid && !gps_valid))
|
|
{
|
|
ESP_LOGW(TAG, "INVALID: '%s'", reason);
|
|
reason[0] = '\0';
|
|
}
|
|
else if (!last_valid && valid)
|
|
{
|
|
ESP_LOGI(TAG, "VALID!");
|
|
}
|
|
last_valid = valid;
|
|
last_gps_valid = gps_valid;
|
|
|
|
char* line = readNMEA();
|
|
|
|
switch (minmea_sentence_id(line, true))
|
|
{
|
|
case MINMEA_SENTENCE_RMC:
|
|
{
|
|
struct minmea_sentence_rmc frame;
|
|
if (minmea_parse_rmc(&frame, line))
|
|
{
|
|
ESP_LOGD(TAG, "$RMC: seconds: %lu timeouts: %u valid: %s", seconds, timeouts, frame.valid ? "true" : "false");
|
|
gps_status.valid = frame.valid;
|
|
|
|
if (frame.valid)
|
|
{
|
|
struct timespec ts;
|
|
minmea_gettime(&ts, &frame.date, &frame.time);
|
|
if (valid && seconds > ts.tv_sec)
|
|
{
|
|
timewarps += 1;
|
|
ESP_LOGD(TAG, "$RMC: ignoring timewarp back! delayed serial? %lu -> %lu", seconds, ts.tv_sec);
|
|
if (timewarps > 1)
|
|
{
|
|
invalidate("time warped backwards too many (%d) times!", timewarps);
|
|
timewarps = 0;
|
|
}
|
|
}
|
|
else if (seconds != ts.tv_sec)
|
|
{
|
|
ESP_LOGW(TAG, "$RMC: adjusting seconds %lu -> %lu", seconds, ts.tv_sec);
|
|
seconds = ts.tv_sec;
|
|
}
|
|
else
|
|
{
|
|
timewarps = 0;
|
|
}
|
|
|
|
//
|
|
// if gps was not valid, it is now
|
|
//
|
|
if (!gps_valid)
|
|
{
|
|
gps_valid = true;
|
|
valid_in = PPS_VALID_COUNT;
|
|
ESP_LOGI(TAG, "gps valid!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (valid)
|
|
{
|
|
invalidate("GPS $RMC");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "$RMC: failed to parse line: %s", line);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MINMEA_SENTENCE_GGA:
|
|
{
|
|
struct minmea_sentence_gga frame;
|
|
if (minmea_parse_gga(&frame, line)) {
|
|
ESP_LOGD(TAG, "$GGA: seconds: %lu timeouts: %u fix: %d sats: %d", seconds, timeouts, frame.fix_quality, frame.satellites_tracked);
|
|
gps_status.sat_count = frame.satellites_tracked;
|
|
gps_status.fix_quality = frame.fix_quality;
|
|
}
|
|
else
|
|
{
|
|
ESP_LOGE(TAG, "$GGA: failed to parse line: %s", line);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MINMEA_SENTENCE_GSV:
|
|
case MINMEA_SENTENCE_GSA:
|
|
case MINMEA_SENTENCE_GLL:
|
|
case MINMEA_SENTENCE_GST:
|
|
case MINMEA_SENTENCE_VTG:
|
|
case MINMEA_SENTENCE_ZDA:
|
|
ESP_LOGV(TAG, "IGNORING: %s", line);
|
|
break;
|
|
|
|
case MINMEA_INVALID:
|
|
ESP_LOGE(TAG, "INVALID: '%s'", line);
|
|
break;
|
|
|
|
case MINMEA_UNKNOWN:
|
|
ESP_LOGW(TAG, "UNKNOWN: '%s'", line);
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Recompute dispersion periodically
|
|
//
|
|
static time_t last_seconds;
|
|
if (seconds != last_seconds)
|
|
{
|
|
double disp = us2s(MAX(abs(MICROS_PER_SEC-max_micros), abs(MICROS_PER_SEC-min_micros)));
|
|
dispersion = (uint32_t)(disp * 65536.0);
|
|
#if 0
|
|
ESP_LOGI(TAG, "min: %llu max: %llu jitter: %llu sats: %d fix: %d valid_count: %u valid_in: %u valid: %s",
|
|
min_micros, max_micros, max_micros - min_micros, gps_status.sat_count, gps_status.fix_quality, valid_count, valid_in, valid ? "true" : "false");
|
|
#endif
|
|
}
|
|
last_seconds = seconds;
|
|
|
|
|
|
}
|
|
}
|
|
|
|
void start_gps()
|
|
{
|
|
esp_log_level_set(TAG, ESP_LOG_INFO);
|
|
|
|
xTaskCreatePinnedToCore(gps_task, "gps_task", 1024*2, NULL, GPS_TASK_PRI, NULL, 1);
|
|
|
|
}
|