Wednesday, May 21, 2008

Using serial ports in Linux,Read data from GPS device

Using serial ports in Linux is easy, but there are some tricks that are hard to know when to use. This tutorial is specifically about how to write a program that can read NMEA data (such as from a GPS device) and to parse the data that is returned. The following code was compiled successfully under Slackware 10 on a Mini-ITX board connected to a GPS device.
To Compile: g++ tut_linux_serial.cpp -g -o tut_linux_serial -O3
-g to allow gdb to work
-o tut_linux_serial is the output name
-O3 is the optimization level

using namespace std;
#define SERIALPORT "/dev/ttyS0" // port the device is plugged in to
#define BAUDRATE B38400 // baud rate the device spits out at
#define UPDATE_RATE 20 // update speed in Hz (probably set anywhere from 10-50

#include
#include // timers
#include // timers / serial
#include // serial
#include // serial, file
#include // serial, file

string serial_buffer; // Unprocessed data off the serial port
int fd_serial;

void timer_handler(int x); // used in automation
void serial_handler(int status); // interrupt function called on new data (position isn't guaranteed), pass to ReadSerial()


int main() {
struct termios tty; // will be used for new port settings
struct termios oldtty; // will be used to save old port settings

fd_serial = open(SERIALPORT, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd_serial < 0) {
printf("\nUnable to write to serial port (%s), are you root?\n", SERIALPORT);
_exit(1);
}
tcgetattr(fd_serial, &oldtty); // save current port settings
bzero(&tty, sizeof(tty)); // Initialize the port settings structure to all zeros
tty.c_cflag = BAUDRATE | CS8 | CLOCAL | CREAD | CRTSCTS; // 8N1
tty.c_iflag = IGNPAR;
tty.c_oflag = 0;
tty.c_lflag = 0;
tty.c_cc[VMIN] = 0; // 0 means use-vtime
tty.c_cc[VTIME] = 1; // time to wait until exiting read (tenths of a second)

tcflush(fd_serial, TCIFLUSH); // flush old data
tcsetattr(fd_serial, TCSANOW, &tty); // apply new settings
fcntl(fd_serial, F_SETOWN, getpid()); // enable our PID to receive serial interrupts
fcntl(fd_serial, F_SETFL, FASYNC);


struct sigaction saio; // set the serial interrupt handler
saio.sa_handler = serial_handler; // to this function
sigemptyset(&saio.sa_mask); // clear existing settings
saio.sa_flags = 0; // make sure sa_flags is cleared
saio.sa_restorer = NULL; // no restorer
sigaction(SIGIO, &saio, NULL); // apply new settings


// set up the main timer
struct itimerval timer1; // set up the timers
signal(SIGALRM, timer_handler); // call this function on rollover
timer1.it_interval.tv_sec = 0; // reset val
timer1.it_interval.tv_usec = (__suseconds_t)((1.0/(double)UPDATE_RATE)*1000000); // reset val (converts UPDATE_RATE from Hz into microseconds
timer1.it_value.tv_sec = timer1.it_interval.tv_sec; // initial val
timer1.it_value.tv_usec = timer1.it_interval.tv_usec; // initial val
setitimer(ITIMER_REAL, &timer1, NULL); // apply new settings


while (1) { } // press ctrl-c to exit the program

write(fd_serial, "t", 1); // where t is the test char to send

tcsetattr(fd_serial, TCSANOW, &oldtty); // restore the old port settings before quitting
}



void serial_handler (int status) {
// this function is called whenever there is new data to be read off the serial port.
// It has to execute quickly, so the data processing is *not* done here.
char temp_buffer[256*2]; // max chars to read at once, if you don't read the entire buffer then the function will get called again
int len = read(fd_serial, temp_buffer, sizeof(temp_buffer)); // do the actual read
temp_buffer[len] = 0; // null terminate the string **important**
serial_buffer += temp_buffer; // append what we read to the serial_buffer of unprocessed data
}




void timer_handler(int x) {
// this function gets called UPDATE_RATE times per second, it handles the data processing off the serial port
// ** note that serial_buffer gets populated from serial_handler
signal(SIGALRM, timer_handler); // clear the interrupt flag, so it will trigger again when we exit

int infinite_loop_preventer = 0; // sanity check to make sure this section won't run forever
if (serial_buffer.length() > 0) { // unprocessed data exists
int start_pos; // position of the first $ char (all packets must start with a $
int end_pos_n; // position of the first \n char following the first $
int end_pos_r; // position of the first \r char following the first $
int end_pos; // set to the lesser of end_pos_n or end_pos_r

do {
infinite_loop_preventer++;
// 1) find start char
// 2) find end char (\n or \r)
// 3) process that data
// loop while there's more data

start_pos = serial_buffer.find("$", 0);
end_pos_n = serial_buffer.find("\n", start_pos);
end_pos_r = serial_buffer.find("\r", start_pos);
end_pos = end_pos_n < end_pos_r ? end_pos_n : end_pos_r; // choose the lesser of the two ending points

if (start_pos != string::npos && end_pos != string::npos) {
string data = serial_buffer.substr(start_pos, end_pos-start_pos); // just the current packet
serial_buffer = serial_buffer.substr(end_pos+1); // remove parsed section
// cout << "Debug: ";
// cout << " start_pos=" <<>
// cout << " end_pos=" <<>
// cout << " data.len=" <<>
// cout << data=" << data << ">
// cout << serial_buffer=" << serial_buffer << ">
// cout <<>
cout << data << endl;
}
} while (start_pos != string::npos && end_pos != string::npos && infinite_loop_preventer < color="#008800"> // keep processing if buffer has end char in it

}
}

Sample Output:

GPGLL,3354.4970,N,11759.5354,W,025604,V,S*52
GPGLL,3354.4980,N,11759.5353,W,025605,V,S*55
GPGLL,3354.4990,N,11759.5355,W,025606,V,S*22

0 comments: