AVR: подключаем GPS приёмник

gps avrЕсли GPS приёмник имеет интерфейс rs232 (а его имеет большинство GPS модулей) то его можно легко подключить к микроконтроллеру avr atmega.

Описание протокола NMEA

Информация от GPS приёмника передаётся в текстовом виде по протоколу NMEA.

GPS приёмники могут передавать несколько типов сообщений, но большая часть необходимой информации содержится в RMС сообщениях. Этот тип сообщений имеет следующий формат:

$GPRMC,hhmmss.ss,A,GGMM.MM,P,gggmm.mm,J,v.v,b.b,ddmmyy,x.x,n,m*hh\r\n
  • «GP» — источник данных — GPS,
  • «RMC» — «Recommended Minimum sentence C»
  • «hhmmss.ss» — время UTC: «hh» — часы, «mm» — минуты, «ss.ss» — секунды.
  • «A» — статус: «A» — данные достоверны, «V» — недостоверны.
  • «GGMM.MM» — широта. «GG» — градусы, MM.MM — минуты
  • «P» — широта: «N» — северная , «S» — южная.
  • «gggmm.mm» — долгота. «ggg» — градусы, mm.mm — минуты
  • «J» — долгота: «E» — восточная ,  «W» — западная
  • «v.v» — горизонтальная составляющая скорости относительно земли в узлах.
  • «b.b» — путевой угол  Значение равное 0 соответствует движению на север, 90 — восток, 180 — юг, 270 — запад.
  • «ddmmyy» — дата:  «dd» — день,   «mm» — месяц,  «yy» — год
  • «x.x» — магнитное склонение в градусах
  • «n» — направление магнитного склонения
  • «m» — индикатор режима: «A» — автономный, «D» — дифференциальный, «E» — аппроксимация, «N» — недостоверные данные
  • «hh» — контрольная сумма.
  • «\r» — байт равен 0x0d, перевод каретки
  • «\n» — байт равен 0x0a, перевод строки

Схема подключения GPS приёмника к avr atmega

Схема с которой я работал построена на GPS модуле Fastrax IT500.
IT500 avr
Так как управлять GPS приёмником не обязательно, то его можно подключить только ко входу  Rx  AVR’а, а выход Tx микроконтроллера через преобразователь уровней (я использовал uart<->usb)   для отладки подключить к ПК.

Пример программы для avr, работающей с GPS приёмником

Для обмена по uart возьмем код из заметки AVR: обмен данными по uart c использованием прерываний и fifo

В функции main будем считывать из fifo принятые данные и складывать их буфер для последующего разбора. Когда приходит символ ‘\r’ или ‘\n’ вызываем функцию nmea_parser для разбора буфера. Результат парсинга выводим в консоль.

#define BUF_SIZE 128
char buf[BUF_SIZE];
unsigned char buf_cnt;
 
int main( void  )
{
 
  uart_init( 9600 ); //настраиваем uart на 9600
  sei();  
 
  puts( "GPS\r\n" );
 
  buf_cnt = 0;
  while( 1) {
     int c;
     //получаем байт из 
     if( (c = getchar()) != EOF ) {
 
       if( (c == '\r') || (c == '\n') ) { 
          //если приняли признак конца строки	  
          buf[buf_cnt] = '\0';
	  //то вызываем парсер
          if( nmea_parser( buf ) == 0 ) {
	    //если удачно распарсили и данные валидны
	    if( nmea_rmc_data.valid ) {
	      //то выводим их в консоль
	      printf( "utc time %f\r\n", nmea_rmc_data.utc_time );
	      printf( "latitude %f\r\n", nmea_rmc_data.latitude );
	      printf( "longitude %f\r\n", nmea_rmc_data.longitude );
	      printf( "speed %f\r\n", nmea_rmc_data.speed );
	      printf( "direction %f\r\n", nmea_rmc_data.direction );
	      printf( "data %lu\r\n\r\n", nmea_rmc_data.data );
 
	    }
	  }
          buf_cnt = 0;
       } 
       else if( buf_cnt &lt; (BUF_SIZE-1) ) {
	  //пока есть место в буфере помещаем туда принятые байты
          buf[buf_cnt] = c;
          buf_cnt++;
       }      
     }
  }
  return 0;
}

Функция nmea_parser сначала проверяет на корректность данные в буфере и затем ищет и вызывает функцию обработчик, подходящую именно для этого типа nmea сообщения.

#define ARRAY_SIZE( array )  (sizeof(array)/sizeof(array[0]))
 
unsigned char nmea_parser( char* buf )
{
  unsigned char i;
 
  if( *buf++ != '$' ) {
    return -1;
  }
 
  if( nmea_check_crc( buf ) != 0 ) {
    return -1; //CRC fail
  }
 
  for( i = 0; i&lt; ARRAY_SIZE( nmea_messages ); i++ ) {
    if( strncmp( buf, nmea_messages[i].id, 
	         strlen( nmea_messages[i].id ) ) == 0 ) {
      if( nmea_messages[i].parser ) {
        buf = nmea_next_field( buf );
        return nmea_messages[i].parser( buf, nmea_messages[i].data );
      }
      break;
    }
  }
  return -1;
}
}

Функция проверки контрольной суммы для nmea сообщения

char nmea_check_crc( char *buf )
{
  unsigned char crc, calc_crc = 0;
 
  while( *buf != '*' ) {
     if( *buf == '\0' ) {
       return -1;
     }    
     calc_crc ^= *buf++;
  }
 
  crc = hex2bin( *(buf+2) ) |  hex2bin( *(buf+1) ) &lt;&lt; 4;
 
  if ( crc  !=  calc_crc ) return -1;
 
  return 0;
}

функция hex2bin переводит из текстового hex формата в двоичный вид

unsigned char hex2bin( char c )
{
  if( ( c &gt;= 'A') &amp;&amp; ( c &lt;= 'F') ) {
    return 0x0A + c - 'A';       
  }
  else if( ( c &gt;= 'a') &amp;&amp; ( c &lt;= 'f') ) {
    return 0x0A + c - 'a';    
  }
  else {
    return c - '0';    
  }
}

структура, описывающая обработчик nmea сообщения

typedef struct _nmea_message {
  char * id; //идентификатор сообщения
  char (*parser)( char *buf, void *data ); //парсер сообщения
  void *data; //результат парсинга сообщения
} nmea_message_t;

массив обработчиков nmea сообщений, в данном примере в массиве только один обработчик, но можно по аналогии добавить обработчики и для других типов сообщений

nmea_message_t nmea_messages[] =
{
  {
    .id = "GPRMC",
    .parser = nmea_rmc_parser,
    .data = &amp;nmea_rmc_data,
  },
};

nmea_rmc_data это структура, в которой хранится результат разбора nmea сообщения

typedef struct gms_rmc {
  double utc_time;     //время в 0-ой зоне
  unsigned char valid; //признак достоверности данных
  double latitude;     //широта
  double longitude;    //долгота
  double speed;        //скорость в милях
  double direction;    //направление движения 
  unsigned long data;  //дата
} nmea_rmc_t;
 
nmea_rmc_t nmea_rmc_data;

Функция обработчик nmea RMC сообщения

char nmea_rmc_parser( char *buf, void *data )
{
  nmea_rmc_t* rmc = (nmea_rmc_t*)data;
  memset( rmc, 0, sizeof( nmea_rmc_t ) );
 
  rmc-&gt;utc_time = atof( buf );
  buf = nmea_next_field( buf );
 
  if( *buf == 'A') {
    rmc-&gt;valid = 1;
  }
 
  buf = nmea_next_field( buf );
  rmc-&gt;latitude = atof( buf );
  buf = nmea_next_field( buf );
  if( *buf == 'S') {
    rmc-&gt;latitude = -rmc-&gt;latitude;
  }
 
  buf = nmea_next_field( buf );
  rmc-&gt;longitude = atof( buf );
 
  buf = nmea_next_field( buf );
  if( *buf == 'W') {
    rmc-&gt;longitude = -rmc-&gt;longitude;
  }
 
  buf = nmea_next_field( buf );
  rmc-&gt;speed  = atof( buf );
 
  buf = nmea_next_field( buf );
  rmc-&gt;direction  = atof( buf );
 
  buf = nmea_next_field( buf );
  rmc-&gt;data  = atol( buf );
 
  return 0;
}

Функция поиска следующего поля nmea сообщения

char *nmea_next_field( char* buf)
{
   while( *buf++ != ',' );
   return buf;
}

Результат работы программы:

utc time 061732.000000
latitude 6001.597200
longitude 3012.735100
speed 0.040000
direction 0.000000
data 100613

Скачать исходники для avr-gcc (WinAvr) можно тут

post scriptum

Рассмотренный пример программы демонстрирует возможность разбора nmea сообщений, и содержит большой простор для оптимизации. К примеру, можно избавиться от работы с переменными типа float и duble, так как микроконтроллеры avr не имеют аппаратной поддержки чисел с плавающей запятой.

Запись опубликована в рубрике Микроконтроллеры avr с метками , . Добавьте в закладки постоянную ссылку.

13 комментариев: AVR: подключаем GPS приёмник

  1. Владимир говорит:

    Доброго времени суток. Нагуглил Вашу статью, появилось пару вопросиков. Нет кода ф-ции nmea_next_field — нет, это так и задумано? Случайно не делали разбор предлодения GSV? Если да, можете поделиться кодом? ;)

  2. Артём Двинин говорит:

    День добрый. Код функции nmea_next_field есть в архиве с исходниками, но на всякий случай добавлю и в заметку. Разобор GSV не делал.

  3. Владимир говорит:

    Возник ещё вопросик…
    К примеру есть поле, в предложении NMEA0183, со таким сожержанием:
    …,-147.2,M,…
    как его преобразовать в hex вид?
    Спасибо.

  4. Артём Двинин говорит:

    Не совсем понял, что значит «преобразовать в hex вид».

  5. Владимир говорит:

    к примеру, поле структуры имеет размерность int32_t, а в нмеа0183 данные в виде: …,-147.2,M,…, значит что-бы информацию «положить» в инт32, нужно умножить на 10, и за одно в бинарном виде, 1472 = 5C0 (в хексе), а сам процесс? atof ? или как-то иначе?

  6. Владимир говорит:

    поддержал сайт :)

  7. Артём Двинин говорит:

    Спасибо! С мира по нитке — наберем на хостинг :)

  8. Артём Двинин говорит:

    Да, самый простой способ atof, но можно и свою функцию преобразования написать. Вот два варианта.

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
     
    int32_t my_atof( char *str )
    {
      int8_t sign = 1;
      int32_t ret = 0;
     
      if( *str == '-' ) {
        sign = -1;
        str++;
      }
     
      while( *str != '' && *str != ',' ) {
        if( *str != '.' ) {
          ret *= 10;
          ret += *str - '0';    
        }
        str++;
      }
      return ret*sign;
    }
     
     
     
    int main( )
    {
      printf( "atof %drn", (int32_t)(atof("-123.4,")*10) );
      printf( "my_atof %drn", my_atof("-123.4,") );
      return 0;
    }
  9. Станислав говорит:

    Добрый день!
    Пролистал много Ваших статей, очень увлекательно и познавательно, как начинающему так и продвинутому микроконтроллерщику. Только один вопрос — почему Вы так не любите прерывания? В идеале main пустой и контроллер постоянно спит, пробуждаясь по соответствующему прерыванию, в крайнем случае в main идёт обработка сложной процедуры, по флагу всё того же прерывания.
    Это поможет сделать Ваш код встраиваемым в разносторонние задачи.
    Например, с библиотечкой delay.h, когда таймеры могут быть заняты чем-то полезным, далеко не уедешь…

  10. Артём Двинин говорит:

    Доброго времени суток! Спасибо за комментарий.
    Я совершенно не против прерываний, я двумя руками (и другими конечностями) за их использование. Просто в заметках пытаюсь больше сконцентрироваться на основной теме, и не усложнять второстепенными деталями. Программы которые приводятся в заметках — это пример использования а не завершенный программный продукт. Но вы правы, думаю стоит как-нибудь написать заметку об использовании прерываний и режимов энергосбережения.

  11. Владимир говорит:

    Доброго времени суток!
    Пользуюсь Вашим кодом, за это большое спасибо. Видел несколько разных парсеров, но Ваш вариант наиболее мне подходит :) Ещё раз спасибо.
    Ну и в догонку вопросец :)
    А не приходилось часом делать обратное? Т.е. не парсить NMEA предложения, а собирать их? Если будет возможность, буду очень рад вашей помощи в очередной раз :)
    Вашу ф-цию парсинга RMC слегонца подпилил под свои нужды:
    [code]uint8_t nmea_rmc_parser ( uint8_t *buf, void *data )
    {
    //$GPRMC,090916.00,A,4912.3898,N,03151.9493,E,00.00,024.3,120314,,,A*5C
    rmc_data_t* rmc = ( rmc_data_t* ) data;
    memset ( rmc, 0, sizeof ( rmc_data_t ) );

    rmc->data_availability = false;

    //090916.00,A,4912.3898,N,03151.9493,E,00.00,024.3,120314,,,A*5C
    rmc->time = atof ( buf );
    sscanf ( buf , «%02d %02d %02d», &rmc->timehh, &rmc->timemm, &rmc->timess);//, &nanosec );

    buf = nmea_next_field ( buf );
    rmc->status = *buf;

    buf = nmea_next_field ( buf );
    rmc->latitude = atof ( buf );
    sscanf ( buf , «%02d %02d .%04d», &rmc->latgr, &rmc->latmin, &rmc->latdecmin);

    buf = nmea_next_field ( buf );
    rmc->podcot_lat = *buf;
    if( *buf == ‘S’)
    {
    rmc->latitude = -rmc->latitude;
    rmc->latgr = -rmc->latgr;
    }

    buf = nmea_next_field ( buf );
    rmc->longitude = atof ( buf );
    sscanf ( buf , «%03d %02d .%04d», &rmc->longr, &rmc->lonmin, &rmc->londecmin);

    buf = nmea_next_field( buf );
    rmc->podcot_lon = *buf;
    if( *buf == ‘W’)
    {
    rmc->longitude = -rmc->longitude;
    rmc->longr = -rmc->longr;
    }

    buf = nmea_next_field( buf );
    rmc->speedN = atof ( buf );

    buf = nmea_next_field( buf );
    rmc->kurs = atof ( buf );

    buf = nmea_next_field( buf );
    rmc->date = atoi ( buf );
    sscanf ( buf , «%02d %02d %02d», &rmc->datedd, &rmc->datemm, &rmc->dateyy);

    buf = nmea_next_field ( buf );
    buf = nmea_next_field ( buf );

    buf = nmea_next_field ( buf );
    rmc->rejim = *buf;

    rmc->data_availability = true;

    return 0;
    }[/code]

  12. Владимир говорит:


    for (:::);

  13. Віталій говорит:

    Дякую за код, єдине код і статті різниться з тим що у вас в архіві, в статті робочіший варіант.
    Я якраз пишу свій код на прериваннях і перешукав купу прикладів парсення NMEA , всі вони в main посимвольно щось роблять, і тільки цей один приклад приймає готову строку і парсить її.

Добавить комментарий

Ваш e-mail не будет опубликован.

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>