
Программа радиоприемника 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.
На само деле это довольно просто.
- собираю нашу DLL в среде MS Visual Studio в конфигурации Debug. При этом генерируется дополнительная отладочная информация в файлах *.PDB.
- копируем готовый DLL в папку с HDSDR
- запускаем программу HDSDR
- в среде Visual Studio выбираем пункт меню Debug => Attach to Process

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

После того, как подключились к процессу для отладки можно ставить точки останова прямо в исходном тексте 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:

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

Честно говоря не знаю, что и сказать, про sdrsharp слышал, но не пробовал. Видимо нужно писать свой плагин?
Не подскажите как Ваше устройство с sdrsharp подружить
Должно быть memcpy(model,my _sdr_model,strl en(my_sdr_model )+1);