I wanted to read a GPS module using mbed 6 using a serial interrupt. I decided to sort the data into the different NEMA sentences inside of the interrupt sub routine which works well on my Nucleo 767ZI but was wondering if I am going to run into problems with a slower board of when I add more sensors to the system.
Hello Eric,
Thank you for publishing your nice GPS library!
was wondering if I am going to run into problems with a slower board of when I add more sensors to the system.
In Mbed OS 6 by default, there are four threads running after boot:
- the ISR/scheduler thread,
- the idle thread,
- the timer thread and
- the main application thread.
RTOS, including Mbed OS, needs a mechanism for counting the time and scheduling tasks. A timer that generates periodic interrupts, called system tick timer, usually does this. Under Mbed OS, we call this mechanism the RTOS ticker. The RTOS ticker runs in the timer thread . Keeping the MCU to spin in one thread without enabling the other threads can disrupt the RTOS ticker and consequently all the other RTOS functions depending on it.
The CMSIS_RTOS tutorial recommends the following solution:
With an RTOS application it is best to design the interrupt service code as a
thread within the RTOS and assign it a high priority. The first line of code in the
interrupt thread should make it wait for a signal flag. When an interrupt occurs,
the ISR simply sets the signal flag and terminates. This schedules the interrupt
thread which services the interrupt and then goes back to waiting for the next
signal flag to be set.
If you apply such design to your GPS library then you shouldn’t worry.
That could be done for example as follows.
GPS.h
:
#include "mbed.h"
...
class GPS : public UnbufferedSerial {
public :
...
private :
...
Thread interruptThread;
EventFlags interruptFlags;
void interruptHandler();
void interruptTask();
};
GPS.cpp
:
#include "GPS.h"
#define SERIAL_RX_INTERRUPT_FLAG (1UL << 0)
GPS::GPS(PinName serialTX, PinName serialRX) :
UnbufferedSerial(serialTX, serialRX),
interruptThread(osPriorityAboveNormal)
{
interruptThread.start(callback(this, &GPS::interruptTask));
attach(callback(this, &GPS::interruptHandler));
rxTimer.start();
}
void GPS::interruptHandler()
{
interruptFlags.set(SERIAL_RX_INTERRUPT_FLAG);
}
void GPS::interruptTask()
{
while(true) {
interruptFlags.wait_all(SERIAL_RX_INTERRUPT_FLAG);
readData();
}
}
...
No modification is needed to the main.cpp
.
Thank you so much. I will add those changes to my library and test it out.
I think the concern I was having with have a large number of operations in the interrupt is that if the next byte came in as it was doing those operations. If I put a printf in a serial interrupt I always get an error.
My current method of checking if the information transmission is complete is to periodically check the rxTimer to see if I haven’t received data in a defined amount of time. Is that how the interruptFlags.wait_all works?
Unfortunately, not. Since there is now one more thread (the interrupt task) running in parallel with the main one some sort of synchronisation is needed.
One way how to synchronize threads is to use event flags. Because there exist 19 various GPS messages you can set up 19 event flags. For example:
#define GPBOD (1UL << 1)
#define GPBWC (1UL << 2)
#define GPGGA (1UL << 3)
#define GPGLL (1UL << 4)
#define GPGSA (1UL << 5)
#define GPGSV (1UL << 6)
#define GPHDT (1UL << 7)
#define GPR00 (1UL << 8)
#define GPRMA (1UL << 9)
#define GPRMB (1UL << 10)
#define GPRMC (1UL << 11)
#define GPRTE (1UL << 12)
#define GPTRF (1UL << 13)
#define GPSTN (1UL << 14)
#define GPVBW (1UL << 15)
#define GPVTG (1UL << 16)
#define GPWPL (1UL << 17)
#define GPXTE (1UL << 18)
#define GPZDA (1UL << 19)
This will allow the interrupt thread (task) to signal, by setting the corresponding event flag, to the main thread that a certain GPS message has been received.
The main thread can wait in the while(true)
loop for the selected event/message. For that purpose you can design a suitable GPS function. For example as below:
bool GPS::waitFor(uint32_t flag, uint32_t millisec)
{
return eventFlags.wait_all(flag, millisec);
}
Such function can wait in the main thread forever while letting the interrupt task to run in parrallel without blocking it. You can also set a maximum wait time in case you’d like to perform more tasks in the main thread.
The main thread and the interrupt task share some variables (i.e. resources). To prevent data from being scrambled, access to those variables has to be protected by a mutex (mutual exclusion). For example:
GPS.cpp
void GPS::updateCheck()
{
...
if (updates & GPGGA) {
mutex.lock();
sscanf
(
gpgga,
",%f,%f,%c,%f,%c,%d,%d,%f,%f,%c,%f",
&time,
&latitude,
&ns,
&longitude,
&ew,
&lock,
&sats,
&hdop,
&alt,
&unit,
&geoid
);
int_time = static_cast<int>(time);
mutex.unlock();
//printf("GPGGA ");
eventFlags.set(GPGGA);
}
...
}
float GPS::getLat()
{
float result;
mutex.lock();
result = latitude;
mutex.unlock();
return result;
}
This is necessary because it could happen that the GPS::updateCheck()
function is called (writes data to the latitude
variable) in the interruptTask
at the moment when the main task is reading the latitude
variable by executing the GPS::getLat()
function.
For the sake of completeness, find below the code which compiled fine for the LPC1768 target. However, it has not been tested on real hardware:
GPS.h
#include "mbed.h"
#ifndef GPS_H
#define GPS_H
#define GPBOD (1UL << 1)
#define GPBWC (1UL << 2)
#define GPGGA (1UL << 3)
#define GPGLL (1UL << 4)
#define GPGSA (1UL << 5)
#define GPGSV (1UL << 6)
#define GPHDT (1UL << 7)
#define GPR00 (1UL << 8)
#define GPRMA (1UL << 9)
#define GPRMB (1UL << 10)
#define GPRMC (1UL << 11)
#define GPRTE (1UL << 12)
#define GPTRF (1UL << 13)
#define GPSTN (1UL << 14)
#define GPVBW (1UL << 15)
#define GPVTG (1UL << 16)
#define GPWPL (1UL << 17)
#define GPXTE (1UL << 18)
#define GPZDA (1UL << 19)
//
#define RX_MAXTIME 2000 // Maximum time gap in milliseconds between two GPS messages
#define PACKET_SIZE 80
class GPS :
public UnbufferedSerial
{
public:
GPS(PinName serialTX, PinName serialRX, int baud = 9600);
bool waitFor(uint32_t flag, uint32_t millisec = osWaitForever);
int getTime();
float getLat();
float getLong();
int getSats();
float getHDOP();
float getSpeed();
private:
int gpsRxLen;
int updates; //the 19 message types to be masked
bool uart_error = false;
int msg_type;
char gpbod[PACKET_SIZE];
char gpbwc[PACKET_SIZE];
char gpgga[PACKET_SIZE];
char gpgll[PACKET_SIZE];
char gpgsa[PACKET_SIZE];
char gpgsv[PACKET_SIZE];
char gphdt[PACKET_SIZE];
char gpr00[PACKET_SIZE];
char gprma[PACKET_SIZE];
char gprmb[PACKET_SIZE];
char gprmc[PACKET_SIZE];
char gprte[PACKET_SIZE];
char gptrf[PACKET_SIZE];
char gpstn[PACKET_SIZE];
char gpvbw[PACKET_SIZE];
char gpvtg[PACKET_SIZE];
char gpwpl[PACKET_SIZE];
char gpxte[PACKET_SIZE];
char gpzda[PACKET_SIZE];
float time, latitude, longitude, hdop, vdop, pdop, alt, geoid;
float track, mag, speedN, speedM;
char mode, T, M, N, K;
char ns, ew, unit;
int lock, sats, checksum;
int int_time;
Thread interruptThread;
EventFlags interruptFlags;
EventFlags eventFlags;
Mutex mutex;
void interruptHandler();
void interruptTask();
void readData();
void updateCheck();
};
#endif
GPS.cpp
#include "GPS.h"
#define GPS_SERIAL_RX_INTERRUPT_FLAG (1UL << 0)
/**
* @brief
* @note
* @param
* @retval
*/
GPS::GPS(PinName serialTX, PinName serialRX, int baud /*= 9600*/ ) :
UnbufferedSerial(serialTX, serialRX, baud),
interruptThread(osPriorityNormal)
{
interruptThread.start(callback(this, &GPS::interruptTask));
attach(callback(this, &GPS::interruptHandler));
}
/**
* @brief
* @note
* @param
* @retval
*/
void GPS::interruptHandler()
{
attach(nullptr);
interruptFlags.set(GPS_SERIAL_RX_INTERRUPT_FLAG);
}
/**
* @brief
* @note
* @param
* @retval
*/
void GPS::interruptTask()
{
while (true) {
interruptFlags.wait_all(GPS_SERIAL_RX_INTERRUPT_FLAG);
readData();
}
}
/**
* @brief
* @note
* @param
* @retval
*/
void GPS::readData()
{
char gps_byte;
static int step;
static int array_title;
read(&gps_byte, 1);
attach(callback(this, &GPS::interruptHandler));
// '$' begin-packet symbol
if (gps_byte == '$') {
updateCheck();
step = 0;
gpsRxLen = 0;
updates = 0;
}
switch (step) {
case 0:
step++;
break;
case 1:
if (gps_byte == 'G') {
step++;
}
break;
case 2:
if (gps_byte == 'P') {
step++;
}
break;
case 3:
msg_type = gps_byte << 16;
step++;
break;
case 4:
msg_type = msg_type + (gps_byte << 8);
step++;
break;
case 5:
msg_type = msg_type + gps_byte;
if (msg_type == 4345668) {
//BOD
array_title = 0;
updates |= GPBOD;
step++;
}
else
if (msg_type == 4347715) {
//BWC
array_title = 1;
updates |= GPBWC;
step++;
}
else
if (msg_type == 4671297) {
//GGA
array_title = 2;
updates |= GPGGA;
step++;
}
else
if (msg_type == 4672588) {
//GLL
array_title = 3;
updates |= GPGLL;
step++;
}
else
if (msg_type == 4674369) {
//GSA
array_title = 4;
updates |= GPGSA;
step++;
}
else
if (msg_type == 4674390) {
//GSV
array_title = 5;
updates |= GPGSV;
step++;
}
else
if (msg_type == 4736084) {
//HDT
array_title = 6;
updates |= GPHDT;
step++;
}
else
if (msg_type == 5386288) {
//R00
array_title = 7;
updates |= GPR00;
step++;
}
else
if (msg_type == 5393729) {
//RMA
array_title = 8;
updates |= GPRMA;
step++;
}
else
if (msg_type == 5393730) {
//RMB
array_title = 9;
updates |= GPRMB;
step++;
}
else
if (msg_type == 5393731) {
//RMC
array_title = 10;
updates |= GPRMC;
step++;
}
else
if (msg_type == 5395525) {
//RTE
array_title = 11;
updates |= GPRTE;
step++;
}
else
if (msg_type == 5526086) {
//RRF
array_title = 12;
updates |= GPTRF;
step++;
}
else
if (msg_type == 5461070) {
//STN
array_title = 13;
updates |= GPSTN;
step++;
}
else
if (msg_type == 5653079) {
//VBW
array_title = 14;
updates |= GPVBW;
step++;
}
else
if (msg_type == 5657671) {
//VTG
array_title = 15;
updates |= GPVTG;
step++;
}
else
if (msg_type == 5722188) {
//WPL
array_title = 16;
updates |= GPWPL;
step++;
}
else
if (msg_type == 5788741) {
//XTE
array_title = 17;
updates |= GPXTE;
step++;
}
else
if (msg_type == 5915713) {
//ZDA
array_title = 18;
updates |= GPZDA;
step++;
}
else {
uart_error = true;
}
break;
case 6:
if (gpsRxLen < PACKET_SIZE) {
switch (array_title) {
case 0:
gpbod[gpsRxLen++] = gps_byte;
break;
case 1:
gpbwc[gpsRxLen++] = gps_byte;
break;
case 2:
gpgga[gpsRxLen++] = gps_byte;
break;
case 3:
gpgll[gpsRxLen++] = gps_byte;
break;
case 4:
gpgsa[gpsRxLen++] = gps_byte;
break;
case 5:
gpgsv[gpsRxLen++] = gps_byte;
break;
case 6:
gphdt[gpsRxLen++] = gps_byte;
break;
case 7:
gpr00[gpsRxLen++] = gps_byte;
break;
case 8:
gprma[gpsRxLen++] = gps_byte;
break;
case 9:
gprmb[gpsRxLen++] = gps_byte;
break;
case 10:
gprmc[gpsRxLen++] = gps_byte;
break;
case 11:
gprte[gpsRxLen++] = gps_byte;
break;
case 12:
gptrf[gpsRxLen++] = gps_byte;
break;
case 13:
gpstn[gpsRxLen++] = gps_byte;
break;
case 14:
gpvbw[gpsRxLen++] = gps_byte;
break;
case 15:
gpvtg[gpsRxLen++] = gps_byte;
break;
case 16:
gpwpl[gpsRxLen++] = gps_byte;
break;
case 17:
gpxte[gpsRxLen++] = gps_byte;
break;
case 18:
gpzda[gpsRxLen++] = gps_byte;
break;
}
break;
}
}
}
/**
* @brief
* @note
* @param
* @retval
*/
void GPS::updateCheck()
{
if (updates & GPBOD) {
//printf("GPBOD ");
eventFlags.set(GPBOD);
}
if (updates & GPBWC) {
//printf("GPBWC ");
eventFlags.set(GPBWC);
}
if (updates & GPGGA) {
mutex.lock();
sscanf
(
gpgga,
",%f,%f,%c,%f,%c,%d,%d,%f,%f,%c,%f",
&time,
&latitude,
&ns,
&longitude,
&ew,
&lock,
&sats,
&hdop,
&alt,
&unit,
&geoid
);
int_time = static_cast<int>(time);
mutex.unlock();
//printf("GPGGA ");
eventFlags.set(GPGGA);
}
if (updates & GPGLL) {
//printf("GPGLL ");
eventFlags.set(GPGGA);
}
if (updates & GPGSA) {
//printf("GPGSA ");
eventFlags.set(GPGGA);
}
if (updates & GPGSV) {
//printf("GPGSV ");
eventFlags.set(GPGSV);
}
if (updates & GPHDT) {
//printf("GPHDT ");
eventFlags.set(GPHDT);
}
if (updates & GPR00) {
//printf("GPR00 ");
eventFlags.set(GPR00);
}
if (updates & GPRMA) {
//printf("GPRMA ");
eventFlags.set(GPRMA);
}
if (updates & GPRMB) {
//printf("GPRMB ");
eventFlags.set(GPRMB);
}
if (updates & GPRMC) {
//printf("GPRMC ");
eventFlags.set(GPRMC);
}
if (updates & GPRTE) {
//printf("GPRTE ");
eventFlags.set(GPRTE);
}
if (updates & GPTRF) {
//printf("GPRTF ");
eventFlags.set(GPTRF);
}
if (updates & GPSTN) {
//printf("GPSTN ");
eventFlags.set(GPSTN);
}
if (updates & GPVBW) {
//printf("GPVBW ");
eventFlags.set(GPVBW);
}
if (updates & GPVTG) {
//printf("GPVTG ");
mutex.lock();
sscanf(gpvtg, ",%f,%c,,%c,%f,%c,%f,%c,%c", &track, &T, &M, &speedN, &N, &speedM, &K, &mode);
mutex.unlock();
eventFlags.set(GPVTG);
}
if (updates & GPWPL) {
//printf("GPWPL ");
eventFlags.set(GPWPL);
}
if (updates & GPXTE) {
//printf("GPXTE ");
eventFlags.set(GPXTE);
}
if (updates & GPZDA) {
//printf("GPZDA ");
eventFlags.set(GPZDA);
}
uart_error = false;
}
/**
* @brief
* @note
* @param
* @retval
*/
bool GPS::waitFor(uint32_t flag, uint32_t millisec)
{
eventFlags.wait_all(flag, millisec);
return true;
}
/**
* @brief
* @note
* @param
* @retval
*/
int GPS::getTime()
{
int result;
mutex.lock();
result = int_time;
mutex.unlock();
return result;
}
/**
* @brief
* @note
* @param
* @retval
*/
float GPS::getLat()
{
float result;
mutex.lock();
result = latitude;
mutex.unlock();
return result;
}
/**
* @brief
* @note
* @param
* @retval
*/
float GPS::getLong()
{
float result;
mutex.lock();
result = longitude;
mutex.unlock();
return result;
}
/**
* @brief
* @note
* @param
* @retval
*/
int GPS::getSats()
{
float result;
mutex.lock();
result = sats;
mutex.unlock();
return result;
}
/**
* @brief
* @note
* @param
* @retval
*/
float GPS::getHDOP()
{
float result;
mutex.lock();
result = hdop;
mutex.unlock();
return result;
}
/**
* @brief
* @note
* @param
* @retval
*/
float GPS::getSpeed()
{
float result;
mutex.lock();
result = speedM;
mutex.unlock();
return result;
}
main.cpp
/* mbed Microcontroller Library
* Copyright (c) 2019 ARM Limited
*
SPDX
-License-Identifier: Apache-2.0
*/
#include "mbed.h"
#include "GPS.h"
GPS gps(PG_14, PG_9);
/**
* @brief
* @note
* @param
* @retval
*/
int main()
{
printf("Starting...\r\n");
// Initialise the digital pin LED1 as an output
DigitalOut led(LED1);
while (true) {
if (gps.waitFor(GPGGA, RX_MAXTIME)) {
int time = gps.getTime();
int latitude = static_cast<int>(gps.getLat() * 100000.0f);
int longitude = static_cast<int>(gps.getLong() * 100000.0f);
int speed = static_cast<int>(gps.getSpeed() * 100.0f);
int hdop = static_cast<int>(gps.getHDOP() * 10.0f);
int sats = gps.getSats();
printf("time:%d lat:%d long:%d speed:%d hdop:%d sats:%d\r\n", time, latitude, longitude, speed, hdop, sats);
led = !led;
}
ThisThread::sleep_for(1000ms);
}
}
I copied this back into mbed studio and changed the pin assignment back to (PG_14, PG_9) for my board and got an ISR Queue overflow error message. If I restart the board with the RX pin disconnected it will print its message with the values of 0 and then show the error as soon as I plug the RX pin back in. The error also occurs when I comment out the printf statement.
++ MbedOS Error Info ++
Error Status: 0x80020126 Code: 294 Module: 2
Error Message: CMSIS-RTOS error: ISR Queue overflow
Location: 0x800C09F
Error Value: 0x2
Current Thread: rtx_idle Id: 0x20006238 Entry: 0x800C171 StackSize: 0x380 StackMem: 0x20006308 SP: 0x2007FEC0
For more info, visit: mbedos-error
– MbedOS Error Info –
= System will be rebooted due to a fatal error =
= Reboot count(=2) reached maximum, system will halt after rebooting =
Sorry, I forgot that the UART rx interrupt register flag is not cleared automatically. So when the program execution returns from the interrupt handler a new UART rx interrupt is triggered immediately (because the flag in the interrupt register is still on) without having received a new byte. This is repeated until the ISR Queue overflows. To prevent that the rx interrupt handler should be detached in the interrupt handler:
void GPS::interruptHandler()
{
attach(nullptr); // detach the rx interrupt handler (this function)
interruptFlags.set(GPS_SERIAL_RX_INTERRUPT_FLAG);
}
To clear the UART rx interrupt flag in the interrupt register we have to read the received byte. After that we can safely re-attach the rx interrupt handler:
void GPS::readData()
{
char gps_byte;
volatile int step;
volatile int array_title;
read(&gps_byte, 1);
attach(callback(this, &GPS::interruptHandler));
...
}
This does clear the ISR error. However the values printed are all 0 and they are printing once every 10 seconds instead of every 1 second.
I have to MTK3339 GPS chip. The 1 second timing is the default message stream update rate so with the original program I posted the rxTimer expires once per second. Each message starts with a “$” but you get what I am seeing as 3 to 7 of them with no break in between. Where I am sitting I get GPGGA in most streams but not every time but I always get GPVTG.
I am an amateur at C++ so I don’t understand how wairFor works here.
if (gps.waitFor(GPGGA, RX_MAXTIME)) {
is in the main.cpp. The program builds and runs without error but GPGGA and RX_MAXTIME are not defined in the main. I tried hard coding those in
if (gps.waitFor((1UL << 3), 10000LL)) {
but I got the same result.
This does clear the ISR error. However the values printed are all 0 and they are printing once every 10 seconds instead of every 1 second.
-
The
void GPS::readData()
function doesn’t work correctly because thestep
andarray_title
are declared asvolatile
. This design implies that the values stored in these variables are lost when the program execution leaves the function. To retain the values for next call they shall be declared asstatic
(I wonder how could your original program work?). -
In the original code the RX_MAXTIME represented a time period in microseconds. In the new one it represents the maximum time period, in milliseconds, to wait for the GPGGA message to be completely received.
The program builds and runs without error but GPGGA and RX_MAXTIME are not defined in the main.
- They are defined in the
GPS.h
header file which is included (copied) into themain.cpp
by the
C preprocessor :
main.cpp
:
...
#include "GPS.h"
...
Additional changes:
-
I have modified the program so that the
GPS::updateCheck()
function is now getting called when abegin-packet
($
) symbol is received. -
The
GPS
class constructor was also extended by an additional parameter which enables to set the UART’s bit rate (the default value is 9600 bits per second).
The modified source codes are available in my earlier post above (I replaced the old codes by the new ones).
I can’t find the original topic I used for reading UART sensors but it was for mbed 2 and the sensorRxLen was set as volatile and I didn’t know why but I always copied it. When I originally made this and tried to sort the data into the different message types I set array_title as an int found that the array_title switch would only go to 0. After adding volatile it worked.
In the original code the rxTimer is reset every time a byte is received. The timer is then checked periodically to see if a byte has not been received in a while. I have tried to change RX_MAXTIME and it seems like the printf occurs at whatever time that is set at. If I am receiving data every second and the RX_MAXTIME is more than one second shouldn’t it never print?
A note about changing the baud rate and other configuration options is that I was at one point changing the baud and data rate which is something I would like to put back into this and found that you have to put a delay in before sending the configuration messages. It also resets every time the GPS is power cycled so you have to set the baud to 9600, tell the GPS chip at 9600 to be a different baud, and then change the mbed device baud to the new rate to continue communication.
The sensorRxLen was set as volatile and I didn’t know why but I always copied it.
- Because the C++/C compiler doesn’t know anything about interrupt service routines (the UART interrupt is triggered by hardware rather than by software) it concludes, based on the code structure, that the ISR is never called and that’s why in the optimization phase the function is not included into the final binary program. To cope with this problem the variables modified inside the ISR are declared as
volatile
to indicate that their values can change any time (for a reason unknown for the compiler). This prevents the compiler from optimizing away such variables as well the related function.
If I am receiving data every second and the RX_MAXTIME is more than one second shouldn’t it never print?
The GPS::waitFor(uint32_t flag, uint32_t millisec)
function is waiting in the main thread for the flag
to be set by the interruptThread
(in the interruptTask
). If that happens the waitFor
returns immediately regardless of the millisec
parameter. However, if you for example did not want to wait in the main thread at this point forever in case when no GPGGA
messages are arriving you can specify a timeout limit by setting the millisec
parameter to an adequate value (which should be usually greater than the normal interval). Actually, there is no need to use the if
condition. The code below will work too.
main.cpp
...
while (true) {
gps.waitFor(GPGGA, RX_MAXTIME); // wait until GPGGA is set or RX_MAXTIME elapsed
int time = gps.getTime();
int latitude = static_cast<int>(gps.getLat() * 100000.0f);
int longitude = static_cast<int>(gps.getLong() * 100000.0f);
int speed = static_cast<int>(gps.getSpeed() * 100.0f);
int hdop = static_cast<int>(gps.getHDOP() * 10.0f);
int sats = gps.getSats();
printf("time:%d lat:%d long:%d speed:%d hdop:%d sats:%d\r\n", time, latitude, longitude, speed, hdop, sats);
led = !led;
ThisThread::sleep_for(1000ms);
}
- Have you tested the last version of the modified code? Does it work?
Thank you for the help!
I thought I had made the changes you suggested this morning but I must have missed something. I just copied and pasted the whole thing and this does work now.
I’m glad that it works. Now it should be safe to include printf
into any GPS function, the GPS::interruptTask()
included, except the GPS::interruptHandler(). And the code should run on any target that is slower than the NUCLEO_F767ZI provided it has enough RAM for the Mbed RTOS.