Программа радиоприемника 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.
Подробнее...