Linux Tutorial: Serial Ports
| serial_test.c |
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 <iostream> #include <sys/time.h> // timers #include <signal.h> // timers / serial #include <termios.h> // serial #include <unistd.h> // serial, file #include <fcntl.h> // 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=" << start_pos << ";"; // cout << " end_pos=" << end_pos << ";"; // cout << " data.len=" << data.length() << ";"; // cout << endl << " data=" << data << ";"; // cout << endl << " serial_buffer=" << serial_buffer << ";"; // cout << endl; cout << data << endl; } } while (start_pos != string::npos && end_pos != string::npos && infinite_loop_preventer < 10000); // 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
|
|