24 April 2016

Процессор приложений в приёмниках ГНСС

Большинство современных ГНСС-приёмников строятся по следующей схеме:
  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 register
 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.

Application processing in GNSS

Most modern GNSS receivers share a similar architecture:
  1. Antenna & LNA;
  2. Front end;
  3. Baseband processing;
  4. Application processing.
In hardware it's usually implemented as a separate antenna device, front-end IC for down-conversion and primary filtering, ADCs and an ASIC. ASIC consists of multiple (nowadays up to several hundreds!) channels with correlators, heterodynes, NCOs and so on. Also it may or may not contain a general-purpose processor unit, such as ARM or PowerPC. As far as I know, there are no solutions using the x86 (due to license fees or the power requirements, who knows), but I'd love to create a receiver based on an Edison or something like that.

Application processing is a huge field in GNSS development and it's being used for such a things:
  1. Locked loops discriminators with feedback to the satellite channels. This is the heart of tracking;
  2. Calculating the PVT from the raw pseudoranges and pseudophases
  3. Monitoring the GNSS signal integrity and so on
Lately I've been developing a tool for ARM which prepares DSPs to work and launches them. When you have to prepare bare-metal hardware to work almost every time it's required to read/write to some registers, pull some GPIO pins and so on. 

Let's have a look at an abstract SoC. For example, we have 4 ADCs (GPS L1, GLN L1, GPS L2, GLN L2), and we want to start only two of them. Now we go to the manual and read that to enable ADC #1 and #3 we have to take a 32-bit register, and set the first and the third bit in it. 

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

Or even worse:

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

Why it's bad? Because it's not clear why the hell would I want to write an 0xA at some memory address (Here I'd like to greet my good friend, who is a C# developer and who literally turns grey when I speak about such "unsafe" things).

Is there a way to improve it? Sure, one may add a nice comment, something like this:

 //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;   

Well ok, now it's clear and good to create, test and run like there's no tomorrow. This is ok when someone will support your projects in five years time, but it becomes more and more complicated if there's a need to modify or update the code. So you have to add some more bits, somehow convert it to hex value.

The solution is a std::bitset container. It's used to implement an integer number (or a std::string like "00001010") as an array of bits. So now if we need to modify our code it's easier to do this way:

 #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());   

In the code above new_value is initialised with an old value, and then two bits are being set. And that's it. Also this containter extremely simplifies the bitwise programming questions on an interview. new_value.count() returns the number of the bits set, bitwise operations are simplified to the dumbest possible level.

The more I work with C++ the more I get amused by it. And not only by the C++11/14 features (which are great, check out the decltype(auto) functions), but also by the older STL and Boost stuff.

6 April 2016

Векторные генераторы сигналов

Если вкратце, то современные СВЧ-устройства восхитительны. В особенности если обучение прошло на старых ламповых советских генераторах, частотомерах (привет метрологии) и осциллографах. Количество возможностей просто поражает.


Например, вот некоторые из качеств, которые радуют при работе:
  1. Отменная стабильность в работе. 
  2. Возможность удалённого управления. Это может быть как какой-то заготовленный сценарий, так и единичная настройка с последующей работой с этим сигналом.
  3. Как продолжение предыдущего пункта: подобные устройства могут быть использованы для создания стендов для автоматизированного тестирования. Чтобы объяснить, что именно я имею в виду, необходимо пояснить цикл разработки нового оборудования.
Предположим, что у нас есть отличная аппаратно-программная платформа, в которой нет багов (что не всегда верно). Например, какая-нибудь SoC или ASIC. И мы хотим реализовать на ней новый алгоритм. Сначала разработчик должен провести теоретические исследования и создать правдоподобную модель. Модели просто отлаживать и они являются хорошим способом оценить производительность и качественные характеристики. Затем модель шаг за шагом модифицируется для максимального приближения или даже симуляции аппаратной платформы.

Когда эта стадия пройдена, самое время портировать алгоритм непосредственно на аппаратную платформу. И для тестирования необходимо создать подходящее окружение, т.е. серию тестов, каждый из которых требует собственного внешнего сигнала с генератора. Возможность удалённого управления позволяет создать тестовый сценарий (скрипт на Python, если вам это угодно, либо же программу на C++ с расписанной временной шкалой), запустить его на сбор информации (мы знаем внешнее воздействие и каков должен быть отклик на него. При совпадении ожиданий и действительности считается, что тест пройден), а сами идём играть в пинг-понг, пить кофе, либо же писать статью в блог о том, как здорово нынче быть инженером.

Сегодня я нашёл единственный недостаток в генераторе, с которым я работаю: невозможно запустить сигнал один раз без использования внешних триггеров. Сигнал может либо крутиться бесконечно много раз, либо запускаться по внешнему TTL-триггеру. Вопрос в том, как получить сигнал, который может являться триггером для генератора (желательно из Visual Studio)? Два часа поисков и тестирования различных вариантов из нашей коробки с хламом с применением WinAPI и мультиметра дало решение: USB-TTL программатор! Он стоит копейки (по сравнению со стоимостью аппаратной платформы и средств отладки), он нативно интегрируется в операционную систему как виртуальный COM-порт, а также показывает отличные результаты как триггер для генератора.

Конечно любое дополнительное оборудование это плохо. Есть ещё несколько подходов, которые я попробовал, но они либо не так хорошо работают, либо слишком сложны в исполнении.

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

 std::this_thred::sleep_for(std::chrono::microseconds(N))  

std::chrono это отличный и достаточно точный инструмент, но с этим методом начинаются проблемы, когда мы имеем дело с короткими и высокочастотными сигналами (до 1 мс). Дело в том, что TCP/IP слишком медленный для таких сигналов и не сможет остановить генератор в нужное время. Есть вариант заполнить массив модуляции нулями после полезного сигнала, но это потребует огромного количества нулей и крайне неэффективно в плане потребления памяти. Но, если честно, я бы выбрал именно это решение, если бы не нашёл вариант с USB-TTL программатором.

Ещё есть способ, при котором генерируются две равные секции (сигнал и нули) с последующей загрузкой их обеих в генератор. После чего записывается сценарий, при котором секции переключаются по внешнему триггеру. Далее создаётся маркер (исходящий триггер) в конце секции сигнала и мы соединяем триггерные выход и вход генератора. Вуаля, происходит магия. Когда сигнал заканчивается, он запустит проигрывание секции нулей на повторе (до 65 тысяч раз). Здорово, не правда ли? Но слишком трудозатратно. Если TTL-программатор не справится со своей задачей, я пойду по этому пути.

UPD: Упс, вот что случается, когда слишком рано пишешь запись в блог. Нашёл способ контролировать одиночные триггеры при помощи SCPI (из своей библиотеки управления генератором). Репутация векторных генераторов восстановлена, а я посыпаю голову пеплом и иду дальше читать руководства.

5 April 2016

Vector signal generators

Long story short: modern RF devices are awesome. Especially if you've learned everything you know on the old valve generators and scopes, there's just a huge amount of possibilities.



For example here are the features I personally find the most important and superior to the conventional generators:
  1. Excellent stability;
  2. Remote control. It may be setup-and-go case or some scenario;
  3. As an extension to the previous point: such devices may be used to create an automated test platform. To explain this idea I have to specify the developement cycle of the new equipment.
Let's assume that we have a great and bug-free (which isn't always true) hardware platform, some SoC or an ASIC, and we want to implement a brand-new algorithm. At first developer should do some theoretical investigation to make a plausible model. Models are easy to debug and are very important to estimate the performance and the qualitative characteristics. Then the model has to be modified step by step to approximate or even simulate the hardware platform.

When this stage is over it's time to migrate the algorithm to the external hardware. And to test our brand-new algorithm we create the suiting environment: i.e. series of tests every one on which requires different signal from the generator. Remote control allows us to set up the test script, launch it to acquire the information and have some time to play ping-pong, go for a coffee break, or write an article about how cool is it to be an engineer nowadays.

Today I've found the only flaw in the generator I'm working with: one can't simply run the signal once without an external trigger. The signal state may be set to either the free run (infinite number of repeats), or to the single run by a TTL-trigger. The question is how can I get this trigger (preferably within the Visual Studio)? Two hours of searches and testing stuff from our big box of junk with WinAPI and a multimeter gave me a solution: USB-TTL stick! It's cheap, it interacts with OS like a virtual COM port and it gives great trigger pulses for the generator. 

Of course any additional hardware is a bad decision. There are some other approaches I've tried, but they're either not working so well or too hard to implement. 

Pretty obvious solution is to start a generator, wait for the signal to end (because the samples quantity and the sampling frequency are known) and turn the generator (Arbitary waveform generator if precisely) off.  It may be done with something like this:

 std::this_thred::sleep_for(std::chrono::microseconds(N))  

std::chrono is a great tool, it's really precise, but the problems begin when we're dealing with the fast signals (up to 1 ms), because TCP/IP is extremely slow for this signal and won't stop the generator when it's required. The solution is to fill the space afterwards the signal with a lot (A LOT) of zeroes, but it's extremely memory inefficient. But, to be honest, I'd pick that option if I hadn't came up with a USB-TTL stick idea.

The other way is to generate two equal signal sections (true signal and zeroes), then upload them both to the generation and set up a sophisticated scenario to switch sections on the external trigger. Then make a marker (outcoming trigger) in the end of the signal section and wire the trigger output to the trigger input. Ta-da, magic happens. When the signal is over it'll trigger the zeroes section to run and repeat (up to 65k times according to the specifications). Pretty cool, eh? But too much work to do. If the TTL stick will prove itself inefficient that'll be the way I'll try.

UPD: oops, this is what happens when you write an article too soon. I've found a way to control the single burst signals via SCPI. The reputation of the vector signal generators is restored.

1 April 2016

Разработка для embedded-систем

Небольшое предупреждение: в рамках этого разговора под "embedded" подразумевается не совсем то, что принято называть. Сейчас Internet of Things (IoT) это очень быстрорастущая и развивающаяся область и разработчики часто подразумевают под ней модули вроде esp8226 и различные --duino платы.

Сегодня же я буду говорить о системах-на-кристалле (SoC), используемых в ГНСС. У них тоже очень низкое энергопотребление, относительно маломощные процессоры (ARM или PowerPC), зато они часто поставляются вместе в цифровыми сигнальными процессорами (DSP), что оказывается очень кстати в ситуации с ограниченными вычислительными мощностями.

В идеальном мире вся разница между разработкой для ПК и ГНСС-приёмников, которая видна программисту, должна заключаться в различии производительности. Но всё не так просто. Необходимо скачать сторонний тулчейн, новую IDE, купить JTAG-отладчик (жутко дорогой, кстати говоря) и т.д. И это ещё не всё: поскольку мы сменили компилятор, необходимо аккуратно пересмотреть весь написанный код. Больше нет возможностей C++11 (пока шаблоны, auto, nullptr), помимо этого я ещё не встречал SoC с реализованной стандартной библиотекой (больше никаких std::vector, только обычные статические массивы). Мне очень нравится идея, которую пропагандирует Microsoft с Windows 10, что есть одна общая ОС и одни средства разработки для всех устройств: компьютеров, смартфонов и планшетов. Но опять, мы живём не в идеальном мире. Так вот, не стоит верить компилятору. Обязательно стоит проверить объектный файл, перепроверить дважды, если есть какой-то баг, или существует некая неуверенность. Чем более узконаправлен компилятор, тем больше в нём скрытых багов и поведения, которого от него не ожидаешь. Мораль сей басни такова: не стоит верить компилятору!

Чтобы запустить приложение на голом железе необходимо его разместить по "нулевому адресу". После чего первое немаскируемое прерывание запустит программу. Звучит просто, не правда ли?

Сегодня я узнал, что не правда. Этот метод предполагает, что первая инструкция расположена ровно по 0x0, но я повторю опять: не стоит верить компилятору. Он может перемешать секцию кода, данных и другие как ему заблагорассудится и как он сочтёт нужным. А самое худшее в этом, что он даже ничего не скажет об этом, поскольку по внутреннему алгоритму сочтёт это оптимальным. Сегодня мне пришлось для борьбы с этим использовать костыль: ассемблерная функция, которая находится в отдельной секции и располагается ровно по нулевому адресу. Эта функция (pre_start) просто вызывает метку start, которая состоит из различных инициализаций, генерируемых компилятором. После этой функции start вызывается функция main() из C/C++ кода и мы можем начать отладку на голом железе.

Приложение на ARM, которое я отлаживал, сейчас занимается только контролем двух светодиодов, для включения которых мне необходимо установить соответствующие этим светодиодам уровни на GPIO в 0... Но это уже совсем другая история про отдел, который занимается разработкой плат.

P.S. Когда я говорю не верить компилятору я не апеллирую к голубой мечте всех программистов родом из стран бывшего СССР, которые все как один хотят написать свой компилятор. Я предлагаю писать код, который будет транслирован очевидным образом и, если этой возможности нет, перепроверить дизассемблированный код.