31 March 2016

Код Неймана-Хоффмана в Beidou (Compass)

Сигнал Beidou, в принципе, очень похож на сигнал GPS C/A L1 [src].


В системе также используется кодовое разделение каналов, при помощи фазовой манипуляции на несущую накладываются следующие двоичные последовательности:
  1. Дальномерный код с чиповой частотой 2.046 Мбит/с;
  2. Навигационное сообщение с частотой 50 бит/с;
  3. А также дополнительный код Неймана-Хоффмана (в дальнейшем NH-код) длительностью 20 бит с частотой 1000 бит/с

Первые две последовательности (если не обращать внимания на чиповую скорость) совпадают по структуре с сигналом GPS C/A L1. Сигнал ГЛОНАСС L1 отличается лишь наличием дополнительно наложенного меандра с периодом 10 мс.

Так зачем нужен NH-код? Он позиционируется как средство для обеспечения символьной синхронизации в приёмнике, а также как дополнительное средство расширения спектра.

Нашей целью является снять модуляцию, полученную в результате получения этого кода. Когда сигнал достаточно мощный, в этом нет никаких проблем: приёмник запоминает последние 20 знаков I-компоненты с выхода ФАП, сравнивает их со сдвинутой битовой маской и, если есть совпадение, то ура, мы нашли смещение и задача решена!

      uint64_t NH_code = 0x72B20;  
      uint64_t input = 0xB208D;  
      for (uint64_t shift = 0; shift < 20; ++shift){  
           //...  
           //Generate shifted code  
           if(shifted_code == input){  
                return shift;  
           }            
      }  

Выглядит здорово и очевидно, не правда ли? Этот вариант является быстрым (даже очень быстрым), тратит не так много памяти, и, что самое главное, он является простым. Его просто понять и, следовательно, просто поддерживать.

Но он не покрывает два крайне важных сценария:
  1. Смена знака навигационного сообщения;
  2. Ошибки ФАП из-за низкого отношения сигнал/шум, ведущие к ошибке в знаке I-компоненты,
Если мы хотим придерживаться алгоритма на битовых масках, эти случаи будут стоить нам достаточно сильной головной боли. Как только мы начинаем брать в расчёт возможность смены знака, алгоритм сразу усложняется, поскольку нам нужно сравнить входной вектор не с одной маской, а сразу с четырьмя. И это для каждого возможного сдвига!

А второй сценарий делает картину ещё хуже. Мы больше не можем рассчитывать на условие  if(shifted_code == input). Теперь приёмник должен на каждом шагу запоминать (больше никакого низкого потребления памяти) количество различающихся бит между входным вектором и опорной последовательностью. И так четыре раза для всех комбинаций знаков навигационного сообщения.

Звучит не очень-то эффективно. Поэтому я предлагаю алгоритм, построенный на согласованном фильтре. Ему на вход подаётся 30 бит (секунд) с выхода ФАП, после чего генерируется вектор длиной 20 элементов, в котором оссуществляетсся поиск максимума. Положение максимума является сдвигом, а его знак — знаком навигационного сообщение. Реализация этого алгоритма примерно следующая:

 #include <cstdint>  
 #include <cmath>  
 namespace{  
      void MatchedFilter1ms(const int16_t *src, size_t src_len,   
           const int16_t *stretched_code, size_t stretched_code_len, int16_t *dst){  
           for (size_t i = 0; i < src_len; ++i) {  
                dst[i] = 0;  
                for (size_t j = 0; j < stretched_code_len; j++)  
                     dst[i] += stretched_code[j] * src[(i + j) % src_len];  
           }  
      }  
 }  
 void main(){  
      MatchedFilter1ms(src, SAMPLES, NH_code, NH_SAMPLES, matched);  
      for (size_t el = 0; el < SAMPLES; ++el){  
           if (abs(matched[el]) > max){  
                max = abs(matched[el]);  
                imax = el;  
           }  
      }  
      int16_t sign = matched[imax] > 0 ? 1 : -1;  
 }  

Да, он использует больше памяти, что не очень-то приветствуется в встраиваемых системах, но он немного быстрее аналога на битовых масках с использованием оптимизации в компиляторе (-O2), а также гораздо надёжнее, как видно на графике снизу.


На этом пока всё. В этом алгоритме ещё есть пространство для оптимизации, но я рад и с его нынешней производительностью. Ещё, насколько я знаю, укороченный NH-код длительностью всего 10 бит будет использован в сигнале ГЛОНАСС L3. С небольшими изменениями этот алгоритм можно будет использовать и для этих целей.

Введение

Здравствуйте.


Меня зовут Михаил и я инженер-программист. Я закончил кафедру со специализацией по ГНСС и пытаюсь использовать полученные знания в реальных приёмниках.
Хм, звучит как-то слишком официально.

Этот блог является попыткой записать и упорядочить различные моменты, которые я посчитал интересными. В принципе, тут будет в основном про программирование, но посмотрим, как пойдёт жизнь.

Надеюсь, что это поможет моему читателю (да, бот гугла, я говорю про тебя), а также окажется интересным и познавательным.

30 March 2016

Development for embedded systems

Disclaimer: In this case my meaning for the "embedded systems" is a bit different from what one might think. Nowadays, Internet of Things (IoT) is a rapidly growing market and developers often refer to modules like esp8226 and other --duino boards.
Today I'll talk more about the GNSS receivers SoCs, which also have very low power requirements, relatively weak CPUs (ARM or PowerPC), but often come with DSP, which comes in handy in the situation of very limited computational resourses.

In the perfect world the only difference one should see between the development for the PC and GNSS SoCs is the performance. But we're not there yet. You have to download the external toolchain, new IDE, buy the JTAG probe (God, they're expensive) and so on. And there's more: because you've changed the compiler, you have to revise the code very carefully. No more C++11 (goodbye templates, auto, nullptr), I haven't yet worked with SoC with implemented STL (no more std::vector, only static arrays). I really love the idea of Microsoft with their Windows 10, that there's one OS and the development tools for every device: laptop, smartphone, tablet and PC. But again, we're not in the perfect world. So don't trust the compiler toolkit. Check the obj file, double check if you have some strange bug or you're not sure. The more specialized compiler is the more unknown bugs and undefined behaviour it contains. Morale: don't trust the compiler!

To run bare-metal application, you should place it to the zero-offset memory. Then the first non-maskable interrupt will start the application. Sounds easy, eh?

Today I learned, that it's not. This method assumes that the first instruction is located exactly at the 0x0. But once again: don't trust the compiler. It may rearrange the code, data and the others sections by his own means. And the worst thing that it won't even tell you this. So today I had to use some duct tape code: assemly function that is placed at zero-offset memory. That function (pre_start) just calls the start. Start consists of some initializations, that are created by the compiler. After the start the main() is called and we can proceed with our bare-metal debug.

The ARM application is now only used to control two LEDs, and to turn them ON I have to write 0 to the GPIO pin... But that's another story about our circuit design department.

27 March 2016

Beidou (Compass) NH-code

Beidou signal is pretty similar to the GPS C/A L1 signal [src].

It is CDMA, and the carrier is being modulated via BPSK with the next binary sequences:

  1. Ranging code with the chip rate of 2.046 Mchips/s;
  2. Navigational message which is modulated on the carrier at 50 bps;
  3. Additional 20-bit long Neuman-Hoffman code (NH-code) with the 1000 bps rate
Carrier with the first two sequences is (if you mind the chip rate) like the GPS C/A L1 signal. To get the GLONASS L1 signal you just need to add the square wave signal with the 10 ms period.

So why adding the NH-code? It is explained to assist the navigation message symbol synchronization in the receiver as well as the additional spectrum spreading. 

Our goal is to remove the modulation caused by this code. When the signal is relatively strong, there is no problem: the receiver just saves the 20 bits from the PLL output, then compares it with the shifted bit mask, and, if it matches, yay, we've found it!


      uint64_t NH_code = 0x72B20;  
      uint64_t input = 0xB208D;  
      for (uint64_t shift = 0; shift < 20; ++shift){  
           //...  
           //Generate shifted code  
           if(shifted_code == input){  
                return shift;  
           }            
      }  

Looks pretty great and obvious, isn't it? It's fast (I mean really fast), it's very low on the memory consumption and, the most important, it's simple. Easy to understand, easy to support.

But it doesn't cover two very important cases:
  1. Change of the sign of the navigational message bit;
  2. PLL errors due to the low SNR.
If we want to stick with the bitwise algorithm, these cases will give us some serious headache. Taking the possibility of the sign change into account immediately makes the algorithm more complicated, because we have to compare the input not with one mask, but with four for every shift!

And the second case makes it even worse. We can no longer rely on the if(shifted_code == input) condition. Now the receiver on every step has to write (memory consumption, remember?) the difference betweed the input and the shifted code. Four times for every sign combination. 

That's not very efficient. That's why I propose the correlation-base algorith. It takes 30 bits (seconds) from the PLL, then generates 20-bit long output and searches for the max value. The position of the maximum is the shift, and the sign of it is the sign of the navigational message bit. It looks something like this:

 #include <cstdint>  
 #include <cmath>  
 namespace{  
      void MatchedFilter1ms(const int16_t *src, size_t src_len,   
           const int16_t *stretched_code, size_t stretched_code_len, int16_t *dst){  
           for (size_t i = 0; i < src_len; ++i) {  
                dst[i] = 0;  
                for (size_t j = 0; j < stretched_code_len; j++)  
                     dst[i] += stretched_code[j] * src[(i + j) % src_len];  
           }  
      }  
 }  
 void main(){  
      MatchedFilter1ms(src, SAMPLES, NH_code, NH_SAMPLES, matched);  
      for (size_t el = 0; el < SAMPLES; ++el){  
           if (abs(matched[el]) > max){  
                max = abs(matched[el]);  
                imax = el;  
           }  
      }  
      int16_t sign = matched[imax] > 0 ? 1 : -1;  
 }  

It uses more memory, which is not what you always want on a embedded systems, but it's sligtly faster with -O2, and much, MUCH, more reliable, as you can see below.


That's it for now. There are still some opportunities to improve this algorithm, but I'm happy with it for now. Also, as far as I know, the truncated NH-code (only 10 bits long) is going to be used in the GLONASS L3 signal. With minor changes this code may be used for it.

An introduction

Hello.

My name is Michael and I'm a software engineer. I have a degree in GNSS and I'm trying to implement my knowledge on the real recievers.
Sounds a bit too official, eh?

Well, my goal with this blog is to categorize and record some stuff that is interesting for me. Basically, it would be about the software developement, but who knows.

Hope you'll find this interesting and educational.