Структура библиотеки Winrad DLL для SDR радио

vs logo

Программа радиоприемника HDSDR может взаимодействовать с аппаратными SDR приемниками разных производителей. Чтобы добавить поддержку нашей платы Марсоход2 как SDR приемника придется написать интерфейсную DLL со специальным набором экспортируемых функций. Этот набор функций — Winrad API.

На самом деле, Winrad API довольно хорошо описано http://www.winrad.org/bin/Winrad_Extio.pdf , там в PDF даже есть фрагмент кода.

Имеющийся код мне придется адаптировать под прием и передачу через последовательный порт и под протокол передачи, который мы сами себе придумали (5-ти байтные пакеты).

Вкратце расскажу, что же такое эта Winrad DLL. Winrad DLL - это динамически загружаемая библиотека, которая предоставляет программе HDSDR интерфейс для работы с аппаратным приемником. Библиотека Winrad DLL обязана предоставить некоторый минимальный набор функций.

Функция InitHW().

//called once at startup time
extern "C" bool __stdcall InitHW(char *name, char *model, int& type)
{
  type = 6; //the hardware does its own digitization and the audio data are returned to Winrad
  //via the callback device. Data must be in 32‐bit (int) format, little endian.
  char* my_sdr_name = "SDR M2 FPGA";
  char* my_sdr_model = "SDR M2 v1.0";
  memcpy(name, my_sdr_name, strlen(my_sdr_name)+1);
  memcpy(model,my_sdr_model,strlen(my_sdr_name)+1);

  Init( "\\.\\COM6" );
  return true;
}

Эта функция вызывается всего один раз и должна передать программе HDSDR строку наименование приемника, строку наименование модели приемника и тип данных, которые будут передаваться в потоке пар каналов I / Q. Я возвращаю тип 6, что обозначает, что мои данные будут целыми знаковыми 32-х битными. Если бы я вернул, скажем 7, то программа HDSDR интерпретировала мои данные как поток float чисел. Если вернуть type=3, то это будет значить, что передается 16-ти битное целое знаковое (не очень высокая точность).
Что касается наименование приемника, то его будет видно когда запустишь HDSDR приемник. В меню программы Options => Select Input можно выбрать источник сигнала и если наша DLL будет успешно загружена, то в списке источников будет «SDR M2 FPGA» - его и выбирать.

Кроме того, на функцию InitHW() можно возложить какие-то наши собственные специфические действия. Я, например, здесь же открываю файл последовательного порта для чтения и записи: Init( "\\.\\COM6" )
Извините, но моя DLL пока работает только с последовательным портом №6.

Идем дальше. Пара функций SetHWLO(long LOfreq) и GetHWLO(void).

extern "C" int __stdcall SetHWLO(long LOfreq)
{ 
  frequency = (int)LOfreq;
  send_freq(frequency);
  return 0; // return 0 if the frequency is within the limits the HW can generate
}

extern "C" long __stdcall GetHWLO(void)
{
  return (long)frequency; //LOfreq;
}

extern "C" long __stdcall GetHWLO(void)
{
  return (long)frequency; //LOfreq;
}

Программа HDSDR вызывает SetHWLO(long Lofreq), когда пользователь перестраивает приемник на другую частоту. Вот параметр Lofreq – это и есть частота на которую мы перестраиваемся. На самом деле функция SetHWLO вызовет другую функцию send_freq(frequency), которая отправит в плату Марсоход2 пятибайтный пакет с новой частотой тюнера.

void send_freq(int frequency)
{
  unsigned char pkt[5];
  int freq = frequency * 0x100000000 / OSC_FREQ;

  pkt[0] = 0x80;
  pkt[1] = (freq >> 0 ) & 0xFF;
  pkt[2] = (freq >> 8 ) & 0xFF;
  pkt[3] = (freq >> 16) & 0xFF;
  pkt[4] = (freq >> 24) & 0xFF;

  if(pkt[1]&0x80) pkt[0] |= 1;
  if(pkt[2]&0x80) pkt[0] |= 2;
  if(pkt[3]&0x80) pkt[0] |= 4;
  if(pkt[4]&0x80) pkt[0] |= 8;

  pkt[1]=pkt[1]&0x7F;
  pkt[2]=pkt[2]&0x7F;
  pkt[3]=pkt[3]&0x7F;
  pkt[4]=pkt[4]&0x7F;

  DWORD wr=0;
  WriteFile( hPort, pkt, sizeof(pkt), &wr, NULL);
}

Проект в ПЛИС примет пакет, возьмет оттуда значение новой частоты тюнера и передаст его на модуль NCO – Numerically Controlled Oscilator – цифровой перестраиваемый генератор синусоидальных / косинусоидальных колебаний.

Функция GetHWLO(void) не делает чего-то существенного — возвращает текущее значение ранее установленной частоты.

Очень фажная функция SetCallback()

extern "C" void __stdcall SetCallback(void (* Callback)(int, int, float, void *))
{
  ExtIOCallback = Callback;
  (*ExtIOCallback)(-1, 101, 0, NULL); // sync lo frequency on display
  (*ExtIOCallback)(-1, 105, 0, NULL); // sync tune frequency on display
  return; // this HW does not return audio data through the callback device
  // nor it has the need to signal a new sampling rate.
}

Программа HDSDR вызывает эту функцию и сообщает нашей DLL адрес колбэка — то есть адрес другой функции, которую нужно нам вызывать, чтобы передать программе HDSDR очередной блок принятых из платы Марсоход2 данных. Самое главное в этом вызове — запомнить адрес обратного вызова, колбэка (call back).

Идем дальше. Следующая важная пара функций StartHW() и StopHW()

extern "C" int __stdcall StartHW(long freq)
{
  DWORD dwID=0;
  if( hPort!=NULL && hPort!=INVALID_HANDLE_VALUE )
    hThread=CreateThread(0,64*1024,&ThreadStart,NULL,0,&dwID);

  return 512; // number of complex elements returned each
              // invocation of the callback routine
}

extern "C" void __stdcall StopHW(void)
{
  TerminateThread(hThread,0);
  return; // nothing to do with this specific HW
}

Когда пользователь нажимает в программе HDSDR кнопку «Start», то в нашей DLL происходит вызов функции StartHW(). Тут я запускаю отдельный поток (thread), который будет постоянно читать из последовательного порта, из платы FPGA данные. Потом пользователь нажмет кнопку «Stop» в программе HDSDR и DLL получит вызов StopHW(). Здесь, в этой функции я убиваю ранее созданный поток.

Несколько обособленно стоит функция порождаемого потока (thread).

ULONG __stdcall ThreadStart(void* lParam)
{
  unsigned char buffer[4096*2];
  unsigned char pkt[512*8];
  char str[256];
  DWORD sz = 512*10;
  DWORD got=0;
  DWORD idx;
  BOOL err_num=0;

  //drop all accumulated data in serial port buffers
  PurgeComm(hPort, PURGE_RXABORT|PURGE_TXABORT|PURGE_RXCLEAR|PURGE_TXCLEAR);
  int num_blocks=0;
  while(1)
  {
    //data is sent by 10-byte packets, find 1st byte in packet
    //first byte in packet has 7th bit set, other bytes have 7th bit reset
    while(1)
    {
      got=0;
      if( !ReadFile(hPort,buffer,1,&got,NULL) )
      {
        sprintf_s(str,sizeof(str),"Error read COM port: %08X\n",GetLastError());
        OutputDebugString(str);
        return -1;
      }

      if( got==1 )
      {
        if( buffer[0]&0x80 )
        {
          //ok, first byte in packet was found
          break; 
        }
      }
      else
      {
        Sleep(10);
        continue;
      }
    }

    idx=1;
    bool protocol_err=false;
    DWORD ticks=0;
    while(1)
    {
      //read whole block
      got=0;
      if( !ReadFile(hPort,&buffer[idx],sz-idx,&got,NULL) )
      {
        sprintf_s(str,sizeof(str),"Error read COM port: %08X\n",GetLastError());
        OutputDebugString(str);
        return -1;
      }
      idx+=got;
      if(idx!=sz) continue;

      idx=0;
      //repack
      for(int i=0; i<512; i++)
      {
        protocol_err=false;
        if((buffer[i*10+0]&0x80)==0) protocol_err=true;
        if( buffer[i*10+1]&0x80) protocol_err=true;
        if( buffer[i*10+2]&0x80) protocol_err=true;
        if( buffer[i*10+3]&0x80) protocol_err=true;
        if( buffer[i*10+4]&0x80) protocol_err=true;
        if(protocol_err)
        {
          err_num++;
          sprintf_s(str,sizeof(str),"Err: %d %d\n",i,err_num);
          OutputDebugString(str);
          printf(str);
          break;
        }
        //reconstruct received 10 bytes packets into 2 integer pairs of I/Q values
        //1st channel
        pkt[i*8+0]=buffer[i*10+1] | ((buffer[i*10+0]&1) ? 0x80 : 0 );
        pkt[i*8+1]=buffer[i*10+2] | ((buffer[i*10+0]&2) ? 0x80 : 0 );
        pkt[i*8+2]=buffer[i*10+3] | ((buffer[i*10+0]&4) ? 0x80 : 0 );
        pkt[i*8+3]=buffer[i*10+4] | ((buffer[i*10+0]&8) ? 0x80 : 0 );
        //2nd channel
        pkt[i*8+4]=buffer[i*10+6] | ((buffer[i*10+5]&1) ? 0x80 : 0 );
        pkt[i*8+5]=buffer[i*10+7] | ((buffer[i*10+5]&2) ? 0x80 : 0 );
        pkt[i*8+6]=buffer[i*10+8] | ((buffer[i*10+5]&4) ? 0x80 : 0 );
        pkt[i*8+7]=buffer[i*10+9] | ((buffer[i*10+5]&8) ? 0x80 : 0 );
      }
      if(protocol_err) break;
      (*ExtIOCallback)(512, 0, 0, pkt);
      num_blocks++;
      DWORD ticks2=GetTickCount();
      if(ticks2-ticks>1000)
      {
        ticks=ticks2;
        printf("%d Blocks/Sec\n",num_blocks);
        num_blocks=0;
      }
    }
  }
return 0;
}

Эта функция постоянно читает блоки данных из последовательного порта. Она должна найти пятибайтные пакеты нашего протокола передачи и реконструировать 32-х битные знаковые числа для обоих каналов I / Q. Сложить эти числа в чередующийся массив данных I32, Q32, I32, Q32, I32, Q32... всего не менее 512-ти пар чисел. Потом поток вызывает через колбэк (*ExtIOCallback)(512, 0, 0, pkt) программу HDSDR и программа уже знает, что с этими всеми данными делать.

Пожалуй еще скажу пару слов по отладке проекта Winrad DLL в среде MS Visual Studio. Поскольку мы пишем DLL, то кажется не очень понятным, как же произвести отладку этой программы? Любая DLL содержит функции, которые вызываются сторонними программами. В нашем случае — Winrad DLL вызывается программой HDSDR. Это значит, что и вести отладку нужно программы HDSDR.

На само деле это довольно просто.

  1. собираю нашу DLL в среде MS Visual Studio в конфигурации Debug. При этом генерируется дополнительная отладочная информация в файлах *.PDB.
  2. копируем готовый DLL в папку с HDSDR
  3. запускаем программу HDSDR
  4. в среде Visual Studio выбираем пункт меню Debug => Attach to Process

    MSVC dbg attach
  5. в появившемся диалоговом окне находим процесс HDSDR и подключаем к нему отладчик. Нажимаем кнопку Attach.

    MSVC dbg attach pr

После того, как подключились к процессу для отладки можно ставить точки останова прямо в исходном тексте DLL в среде Visual Studio. Например, поставьте точку останова (кнопка F9) на любую строку внутри функции SetHWLO() в среде Visual Studio. Теперь, когда в программе HDSDR вы попытаетесь перестроить приемник на другую частоту, будет вызов SetHWLO() и процесс будет остановлен отладчиком Visual Studio. Теперь уже можно по шагам пройти все строки функции, посмотреть состояние переменных в программе DLL, установить новые точки останова. Вообще MS Visual Studio имеет очень удобный инструмент отладки.

Есть еще один простой способ отладки, которым я часто пользуюсь — диагностические сообщения в консоль с помощью printf(). Есть только небольшая проблема — программы с GUI, то есть оконные программы Windows не имеют консоли по умолчанию. По этому сообщения printf() просто не куда показывать. Ну раз нет консоли — так давайте ее создадим сами!

В программе Winrad DLL в функции OpenHW() я как раз и создаю консоль для вывода моих информационных сообщений. Теперь можно вызывать printf() из других частей программы DLL и в консоли мы увидим все наши сообщения:

extern "C" bool __stdcall OpenHW(void)
{
  AllocConsole() ;
  AttachConsole( GetCurrentProcessId() ) ;
  freopen( "CON", "w", stdout ) ;
  printf("Hello SDR!\n");
  return true;
}

Вот скриншот программы HDSDR с подключенной моей DLL:

MSVC dbg console

Видите позади окна программы HDSDR консоль с сообщением из DLL?
Таким образом, используя диагностические сообщения из консоли или отладку DLL из Visual Studio можно произвести разработку программы Winrad DLL.

 

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