Showing posts with label ru. Show all posts
Showing posts with label ru. Show all posts

26 July 2016

Автоматизированная сборка C++ проектов в Gitlab CI

Новый адрес: MKlimenko.github.io

Для начала: GitLab это git-сервис с открытым исходным кодом. Около года назад, когда я спросил о наличии каких-либо развёрнутых систем контроля версий, админы дали мне доступ к нашему серверу git. Достаточно долго я использовал лишь малую часть его возможностей, в основном просто push, чтобы сохранить проделанную работу. Однако, этому было суждено измениться. Сейчас я работаю над достаточно сложным проектом, который включает в себя несколько типов программных продуктов (программы для DSP и ARM, API, графическое приложение), которые в свою очередь требуют множество сред и тулчейнов для сборки. И после того как пришлось ждать три часа, пока скачается и установится Qt (привет корпоративным прокси!), я решил, что было бы здорово настроить build-сервер, который бы при каждом новом коммите стягивал последние исходники и собирал их. Беглое гугление рассказало про Jenkins, но в тот день мне было лениво его настраивать. Позднее в тот же день я зашел в веб-интерфейс нашего git и один пункт меню приковал моё внимание: “Builds”. Если не вдаваться в детали, это интегрированный планировщик задач, который запускается при каждом новом коммите (а так же merge request).

Чтобы начать использовать CI (continuous integration), необходимо всего-либо расположить в корне репозитория один файл. Он предоставляет много возможностей, например, произвести В, только если А пройдёт успешно/неуспешно и т.д. Я не слишком серьезно настроил эти скрипты, в моём случае перед сборкой скачиваются подмодули для получения последних версий библиотек, после чего осуществляется сборка. Если сборка провалилась, автор коммита получает письмо на почту, что он сломал билд. Когда я тестировал различные возможности CI, я получал по 50 писем в час (простите, админы).

В сети много примеров содержимого файла .gitlab-ci.yml для популярных языков, однако я не нашёл пример для проектов Visual Studio (кроме C#, но это другая песня) или Qt. Если вы пришли сюда за этим, надеюсь вам это поможет.

MSBuild:

 Job_name: 
  script: 
  - 'setlocal' 
  - 'chcp 65001' 
  - 'call "%VS120COMNTOOLS%..\..\vc\vcvarsall.bat" x86_amd64' 
  - 'msbuild.exe make\vs120\Project_name.sln /t:Rebuild /p:Configuration=Release /p:Platform="x64" /m' 
  - 'if not exist "%BUILDS%\Project_name" (mkdir "%BUILDS%\Project_name")' 
  - 'copy make\vs120\x64\Release\Project_name.exe "%BUILDS%\Project_name"' 

Строка “Job_name:” является обязательной и обозначает, как будет называться задача runner-а. Также под этим именем она будет отображена в веб-интерфейсе.

  - 'chcp 65001'” необходмо для корректного отображения кириллицы, печатаемой при выводе MSBuild.

  - 'call "%VS120COMNTOOLS%..\..\vc\vcvarsall.bat" x86_amd64'” добавляет необходимые переменные окружения чтобы cmd нашёл MSBuild, компилятор и т.д.

Последние две строчки помогают мне всегда иметь в собранном виде последнюю версию программ. Это очень помогает, когда кому-то требуется что-то из моих разработок.

Достаточно просто, да? Давайте перейдём к более интересным вещам, а в частности проектам из Qt.

Another_Job_name: 
  script: 
  - 'setlocal' 
  - 'chcp 65001' 
  - 'call "%VS120COMNTOOLS%..\..\vc\vcvarsall.bat" x86_amd64' 
  - 'cd make\qt5' 
  - 'call "%QT_ROOT_x86_64%\bin\qmake.exe" Qt_project.pro -r -spec win32-msvc2013' 
  - 'call "%QT_CREATOR%\bin\jom.exe" -f Makefile.Release'  
  - 'rd /s/q deploy' 
  - 'mkdir deploy' 
  - 'copy release\Qt_project.exe deploy' 
  - 'set curr_dir=%cd%' 
  - 'cd /d "%QT_ROOT_x86_64%\bin"' 
  - 'windeployqt.exe "%curr_dir%\deploy\Qt_project.exe" -no-translations' 
  - 'cd /d %curr_dir%' 
  - 'if not exist "%BUILDS%\Qt_project" (mkdir "%BUILDS%\Qt_project")' 
  - 'xcopy /s /y deploy "%BUILDS%\Qt_project"' 

Вот это уже интереснее. Несколько схожих команд, затем вызывается qmake для генерации makefile-ов из файла проекта .rpo, а затем jom (многопоточный make) производит сборку Release-версии.

Приложения, написанные в Qt требуют множества библиотек во время работы, соответственно для распространения нам необходимо, чтобы они все находились в директории с программой. Вместе с Qt поставляется утилита windeployqt, которая анализирует исполняемый файл и кладёт все его зависимости рядом с ним. До сих пор не понимаю почему, но мне не удалось заставить её работать без смены директории (ругалась на Python2.7), хотя какая разница. xcopy используется для копирования всего содержимого внутренней директории в каталог с общим доступом.

Осмысление того, как всё необходимо осуществлять с qmake, jom и windeployqt заняло некоторое время, но опять же, ничего слишком сложного. А теперь я вам представляю скрипт для сборки проектов под ARM.

 ARM_project_job: 
  script: 
  - 'setlocal' 
  - 'chcp 65001' 
  - 'set command=""%DS-5_DIR%\sw\eclipse\eclipsec.exe" -nosplash --launcher.suppressErrors -application org.eclipse.cdt.managedbuilder.core.headlessbuild -data "%ARM_WORKSPACE%" -import make\eclipse\arm_project -cleanBuild arm_project"'  
  - 'echo "%command%" | "%DS-5_DIR%\bin\cmdsuite.exe" 2> error.txt' 
  - 'for %%A in (error.txt) do set fileSize=%%~zA' 
  - 'del /f /q error.txt'  
  - 'if not %fileSize%==0 (exit /b 1)' 
  - 'if not exist "%BUILDS%\arm_project" (mkdir "%BUILDS%\arm_project")' 
  - 'copy make\eclipse\arm_project\Release\arm_project.axf "%BUILDS%\arm_project"' 

Самая интересная часть тут это перенаправление вывода %command% в cmdsuite.exe. Cmdsuite это командная строка DS-5, т.н. batch job, в котором происходит некоторая внутренняя магия с лицензиями и конфигурированием баз данных. Я называю это магией в связи с тем, что мне не удалось экспортировать все переменные окружения и настройки в основную командную строку. Вопрос в том, как передать команду в созданную командную строку из родителя? Каким-то образом это работает через символ |, т.н. pipe. Сама по себе команда — это просто вызов Eclipse без логотипа с командой для сборки проекта в режиме командной строки. Внимание. Если у вас в workspace уже имеется одноимённый проект, вы не сможете его импортировать и собрать. После этого я осуществляю переадресацию вывода ошибок сборки в текстовый файл с последующим чтением в основном потоке (командной строке). Это необходимо в связи с тем, что вывод batch job подавлен и недоступен для анализа из gitlab CI. Если файл не пуст, принимается решение о наличии ошибок и возвращается код 1, что отмечает коммит, как не прошедший сборку. Иначе в общую директорию кладётся новейшая программа под ARM.


8 July 2016

Управление потоками в Qt

Я писал небольшой пост о том, как осуществляется удалённый сброс по питанию, но он как-то слишком долго пишется. Так что пока я поделюсь некоторыми соображениями на тему многопоточности в приложениях с UI. Предположим, что мы разрабатываем простое клиент-серверное приложение, от которого требуется вызывать некую callback-функцию каждый раз, когда приходят новые данные. Отсюда следует необходимость постоянного прослушивания порта, что невозможно реализовать в основном потоке, если есть и другие задания. Тут и пригождается многопоточность: мы создаём слушающий поток, в то время как главный ожидает команд от пользователя.

Существует два способа создания параллельных приложений на C++: на основе потоков и на основе задач, std::thread и std::async соответственно. Лично я предпочитаю std::thread по той простой причине, что std::async требует модификатора std::launch::async для полноценной отвязки потока. Хотя тут дело скорее в том, что я просто не оценил по достоинству все возможности std::async в действии.

Предположим, у нас есть некий класс:

class Foo {
private:
       std::thread RxThread;
       //…
       void InfiniteRead(std::function<void(uint8_t*, size_t &)> callback) {
             for (;;) {
                    //Read Some Data
                    if(read) {
                           callback(data, size_of_data);
                    }
             }
       }
       //…
public:
       void Init() {
             RxThread = std::thread(&Foo::InfiniteRead, this, callback);
       }
}

Вуаля, мы породили поток, который в вечном цикле слушает порт и вызывает некую функцию при наличии данных. Проблемы начинаются, когда становится необходимо обновить UI в соответствии с полученными данными. Считается плохим тоном изменять что-либо в интерфейсе из других потоков по той простой причине, что из потока неизвестно, происходит ли обновление интерфейса в данный момент, что приводит к очень противным и трудно отлавливаемым ошибкам.

В Qt следует использовать event-ы для обновления графического интерфейса из другого (как бы) потока. Сначала, следует создать класс, который будет эти самые события обрабатывать.

class MyEvent : public QEvent{
public:
    struct event_msg{
        //some custom struct with data
    };

  MyEvent(const event_msg& message) : QEvent(QEvent::User) {_message = message;}
 ~MyEvent() {}

  event_msg message() const {return _message;}

private:
  event_msg _message;
};

Далее объявляется простой метод в заголовочном файле класса графического интерфейса:

bool event(QEvent* event);

Этому методу соответствует следующая имплементация:

bool UI_Class::event(QEvent* event){
    if (event->type() == QEvent::User){

        MyEvent* postedEvent = static_cast<MyEvent*>(event);
        //some code
    }
    return QWidget::event(event);
}

В перегруженном методе обработки событий мы проверяем тип события и, если он совпадает с тем, что создали мы, производятся некоторые команда. Иначе событие отправляется дальше по пищевой цепочке. Ну и напоследок вкратце о том, как посылать эти события:

void blabla(){
    //...
    MyEvent::event_msg event;
    //fill event_msg with data
    MyEvent* e = new MyEvent(event);
    QCoreApplication::postEvent(parent, e);
    }

Готово, вот так просто. Ещё очень хорошей идеей является уведомление потока через std::condition_variable о том, что были получены некоторые данные. 

27 May 2016

Программа для отображения цифровых сигналов

Мы живём в цифровую эру. Даже если вы не имеете понятия, в чём разница между процессором и оперативном памятью, GSM и GPS, вам всё равно нужно смириться с тем, что подавляющая часть окружающих вещей содержит в себе что-то цифровое. Это может быть простая RFID-метка на пакете молока или в билете на общественный транспорт, либо же сложное устройство, упакованное в красивый корпус.

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

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

Однажды на собеседовании меня спросили, был ли у меня опыт и умею ли я читать и анализировать сигнал в двоичном виде, и этот вопрос меня очень сильно удивил, потому что кому захочется это делать "на пальцах", когда гораздо проще построить сигнал и посмотреть на него?

Вот почему я решил написать приложение, построенное на библиотеке, которую я упоминал ранее. Приложение тоже не перегружено функционалом и делает ровно одну вещь: отображает с указанными параметрами файл, который был ей передан. Например, вот результат передачи двух файлов с разной структурой (сверху комплексный 16-битовый сигнал, снизу действительный 8-битовый) в программу:



Необходимое приведение типов тут реализовано достаточно просто, программа читает файл в виде массива байт (uint8_t, если быть точнее), а затем указатель на этот массив передаётся в библиотеку через reinterpret_cast.

Есть у программы и одна нерешенная проблема, а именно работа с упакованными сигналами. Сигналам ГНСС требуется всего 1-2 бита для эффективного квантования, так зачем тратить целый байт на каждый отсчёт? Несколько раз я видел оцифровки сигналов, которые были упакованы насколько это позволяет логика. Это сигнал с приёмника GPS/ГЛОНАСС диапазонов L1 и L2, в котором каждый байт выглядел следующим образом (если делить на биты): 

|GPS L1 Inphase|GPS L1 Quadrature|GLN L1 Inphase|GLN L1 Quadrature|GPS L2 Inphase|GPS L2 Quadrature|GLN L2 Inphase|GLN L2 Quadrature|

На данный момент я не могу придумать способ, как пользователю лучше сообщить способ упаковки сигнала программе. У меня есть пара идей, но они достаточно сложные и запутанные для конечного пользователя и я не хочу их использовать. Так что на данный момент перед отображением сигнала его нужно распаковать.

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

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.

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 (из своей библиотеки управления генератором). Репутация векторных генераторов восстановлена, а я посыпаю голову пеплом и иду дальше читать руководства.

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. Когда я говорю не верить компилятору я не апеллирую к голубой мечте всех программистов родом из стран бывшего СССР, которые все как один хотят написать свой компилятор. Я предлагаю писать код, который будет транслирован очевидным образом и, если этой возможности нет, перепроверить дизассемблированный код.

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. С небольшими изменениями этот алгоритм можно будет использовать и для этих целей.