Application processing in GNSS

Disclaimer: этот пост очень сильно устарел. Я напишу обновлённую версию как только появится время. Представленный тут подход нормальный, но я нашёл гораздо лучшее решение.

Большинство современных ГНСС-приёмников строятся по следующей схеме:

  1. Антенна и МШУ;
  2. Аналоговый тракт (оно же радиочасть, оно же РПУ, front end etc);
  3. Многоканальный цифровой коррелятор;
  4. Сигнальный процессор.

Если говорить о реализации в железе, то это обычно отдельная антенна, отдельная СБИС с преобразованием частоты и предварительной фильтрацией, набор АЦП и ASIC (application specific integrated circuit, СБИС специального назначения). ASIC содержит несколько (в самых современных приёмниках аж до нескольких сотен!) каналов с корреляторами, цифровыми гетеродинами и т.д. Та же СБИС может содержать (или он может быть вынесен в виде отдельной микросхемы) обычный процессор, такой как ARM или PowerPC. Насоклько мне известно, ещё не существует коммерческих решений на базе х86 (из-за лицензионных отчислений, потребления мощности или ещё чего-то), но я бы с радостью занялся разработкой приёмника на Edison или похожем устройстве.

Задачи, решаемые сигнальным процессором в ГНСС, достаточно обширны и многогранны:

  1. Дискриминаторы петель слежения с обратной связью в каналы слежения. Это сама суть слежения за сигналом;
  2. Решение навигационной задачи и расчёт PVT (position, velocity & time) с использованием сырых данных, т.е. псевдозадержек и псевдофаз.
  3. Дополнительные задачи, более интересные с исследовательской точки зрения. Например, исследование качества сигнала

В последнее время я занят разработкой утилиты, которая настраивает DSP, аналоговые тракты и всю обвязку СБИС и запускает её. Когда стоит задача настройки голого железа, практически всегда необходимо производить чтение/запись из каких-те регистров, дёргать GPIO пины и т.д.

Давайте представим некую абстрактную СБИС. Например, у нас есть 4 АЦП, по одному на каждый диапазон (GPS L1, GLN L1, GPS L2, GLN L2). И мы хотим использовать только два из них. Открываем документацию и видим, что для включения АЦП 1 и 3 необходимо использовать некий 32-битный регистр и выставить в нём первый и третий бит. Это обычно делается следующим образом:

uint32_t* start_adc_ptr = reinterpret_cast<uint32_t*>(0xfff88000);  
start_adc_ptr[0] = 0xA;  

Или даже хуже:

*reinterpret_cast<uint32_t*>(0xfff88000) = 0xA;  

Почему это плохо? Потому что непонятно, зачем вообще писать некое 0xA по непонятному адресу (Тут я хочу передать привет своему хорошему другу, который занимается разработкой на C# и который буквально седеет каждый раз, когда я говорю о прямой работе с памятью).

Есть ли способ улучшить? Конечно, можно добавить комментарий, объясняющий происходящее, вроде этого:

//ADC start control registers  
uint32_t* start_adc_ptr = reinterpret_cast<uint32_t*>(0xfff88000);   
//Start ADC 1 and 3: 0000_0000_0000_0000_0000_0000_0000_1010 = 0xA  
start_adc_ptr[0] = 0xA;   

Отлично, теперь понятно что и почему, можно спокойно написать, отладить, запустить и забыть. Оно не создаст проблем для человека, который будет поддерживать код через лет пять, но сильно усложняет задачу, если кто-то захочет модифицировать или улучшить код. Например, если новый разработчик захочет включить все АЦП, ему придётся добавить биты и каким-то образом конвертировать полученное значение в hex.

Решением этой проблемы является контейнер std::bitset. Он используется для представения целого числа (или std::string вида “00001010”) в качестве массива бит. Таким образом, если нужно модифицировать код, это можно сделать следующим образом:

#include <bitset>  

//ADC start control registers   
uint32_t* start_adc_ptr = reinterpret_cast<uint32_t*>(0xfff88000);    
//Start ADC 1 and 3: 0000_0000_0000_0000_0000_0000_0000_1010 = 0xA  
uint32_t old_value = 0xA;  
std::bitset<32> new_value(old_value);  
new_value[0] = 1;  
new_value[2] = 1;  
//new_value: Start ADC 0..3: 0000_1111  
start_adc_ptr[0] = static_cast<uint32_t>(new_value.to_ulong());   

В этом примере new_value инициализируется новым значением, после чего устанавливаются два новых бита. Вот и всё. Вдобавок этот контейнер значительно упрощает решение задач на битовые операции, которые так любят на различных собеседованиях. Например, new_value.count() возвращает количество установленных бит, побитовые операции упрощены настолько, как это только может быть.

Чем больше я работаю с C++, тем больше он меня поражает. И не только плюшки C++11/14 (которые, кстати, восхитительны, обратите внимание на decltype(auto) функции), но и более старые возможности STL и Boost.


© 2022. All rights reserved.

Powered by Hydejack v9.1.6