Programmer's Information: Digimode DLLs for Spectrum Lab ------------------------------------------------------------------------ File: ..\SpecLab\digi_dll.txt Date: March 10, 2001 Maintained by: Wolfgang Buescher, DL4YHF 1. Preface ----------- The 'Digimode DLL' feature has been implemented in Spectrum Lab to allow other programmers to implement experimental communication modes of their own without having to mess around with windows and the sound interface too much. In short terms, it provides an interface from Spectrum Lab to the implementation of a 'new' mode which does not depend on the programming languages used on both sides. 2. Basics of the DLL interface ------------------------------ 2.1 Loading a Digimode DLL by the application ---------------------------------------------- A Digimode DLL is linked dynamically into the application (using LoadLibrary and GetProcAddress from the Win API). After the User selects a Digimode DLL, the main program must find out the addresses of all required routines in the DLL itself ( which is very different from STATICALLY linked DLLs, where Windows will adjust all calling addresses automatically during program start). To make sure the application can find all routines in the DLL, proper routine names and spelling are important (see chapter 'Implementation'). 2.1 Initialisation, Mode settings --------------------------------- There is one interface routine in the digimode DLL which will be called to adjust some 'mode' parameters from the terminal's user interface. All input data are passed in a 'digimode control struct' (thats a RECORD in pascal). The DLL's internal code does not have to use all components in the control struct, because some of them may not be useful for the implemented mode. In the 'InitMode'-call, SpecLab passes the address of the control struct to the digimode DLL. The digimode DLL should keep a copy of its ADDRESS internally, because this control structure also contains some flags for switching between RX and TX (explained later). 2.2 Real-Time processing ------------------------- The 'Decode' or 'Transmit'-routine in the DLL will be called periodically from the audio progressing thread to process or generate a 'chunk' of received audio samples. (Calling the DLL for every single audio sample would be a waste of time.) The chunk size may vary from 512 to 4096 samples per call. The processing time for each chunk is much less than (chunk_size / sample_rate). Example: chunk_size = 4096 audio samples sample_rate = 11025 samples per second -> processing time MUCH LESS than 0.37 seconds per chunk. Most decoders have to do 'more complex processing' from time to time (for example, calling a Viterbi decoder for every SYMBOL but not for every audio sample). If -using this hypothetical example- the Viterbi decoder needs 1 second per call, it should not be called from the sound processing thread because samples would be lost. There are some of possibilities to come across this problem: - Make the decoder fast enough, so the Decode()-routine returns within 0.2 seconds or less, or - Use the 'DIGM_Decode()' routine only to copy the audio samples into a buffer of your application, and process the 'buffered' data in an own medium-priority thread, or - Use the 'DIGM_Decode()' routine only to copy the audio samples into a buffer of your application, and process the 'buffered' data in the DIGM_MainHandler()-routine which is called every XXX Milliseconds from the main program (so you don't have to launch your own thread which can be tough in some programming languages). 2.3 RECEIVE mode ---------------------- The decoder routine in the DLL will do the first (time-crititcal) processing decoding tasks (or -possibly- just copy the received audio into an own buffer). This is a time-critical routine, it should be faster than the time needed to acquire the audio samples in real-time. Because this can be tough for some decoders, less frequent processing steps may be done in the routine 'MainHandler' (see below). There are different ways how the DIGM_Receive()-routine can handle the received audio data: - If decoding is simple, and can always be done completely in real-time, it may immediately decode everything down to the level of 'characters'. - If decoding is complicated (or too slow to be done in real-time at certain processing steps), it may do just the low-level processing in real time (like carrier recovery, I/Q mixing, decimation, bit sync..). At a certain point of processing, the received data (let's call it bitbitstream for example) may be placed in a local buffer. The final detection stages will then be done in a less time-critical routine called DIGM_MainHandler(). The MainHandler is *NOT* called from the sound-processing thread but from a low-priority thread. It will be interrupted by the sound thread, so be careful to synchronize buffer accesses where needed. Every decoded character is placed in a 'RX character' buffer. The characters are periodically taken out of this buffer and printed to the screen by the terminal. 2.4 TRANSMIT mode ------------------------ The terminal will fill up a 'TX character' buffer whenever possible (and characters are available from the TX window). The DLL's 'Transmit' routine is called periodically from the audio processing thread to generate a continuous audio stream. Similar to RX mode, processing also takes place in 'audio chunks'. The generation of the transmit audio stream can be split into 2.5 RX/TX changeover -------------------- Switching between RX and TX is not as simple as you may expect, because some modes require the transmission of a 'preamble' and a 'postamble' (like PSK31). Other modes (like AMTOR-ARQ) may require switching between RX and TX for the sake of the transmission protocol. For this reason, there are FOUR flags to controll RX/TX changeover (all located in the 'digimode control struct/record): trm_tx_req : Transmit Request. WRITTEN by the terminal, READ by the DLL. trm_rx_req : Receive Request. WRITTEN by the terminal, READ by the DLL. dll_tx_req : Transmit Request. WRITTEN by the DLL, READ by the terminal. dll_rx_req : Transmit Request. WRITTEN by the DLL, READ by the terminal. In the Idle state, these four flags are all zero(0). A complete changeover cycle may work like this: A. Terminal (or user) wants to change from RX to TX and sets 'trm_tx_req=1' in the digimode control structure. B. After a while, the digimode algorithm decides that it's time now to switch from RX to TX (maybe WITHOUT STEP A for ARQ modes!). For this purpose, it sets the Flag 'dll_tx_req'. C. As soon as the control function in SpecLab detects 'dll_tx_req=1', it sets its own 'trm_tx_req' to zero and switches the hardware from RX to TX (like setting the PTT line for the transceiver). From this moment on, the sound processing thread calls the 'Transmit()'- routine in the DLL. The DLL should set dll_tx_req=0 now. D. Transmission in progess. After a while, the 'Transmit()' routine wants to switch back from TX to RX (maybe because the TX buffer is empty, acknowledge in RX mode required, or the terminal has set 'trm_rx_req=1'). The DLL sets 'dll_rx_req=1' to indicate that it wants to switch back into RX mode. E. The control function in SpecLab detects 'dll_rx_req=1', sets its own trm_rx_req to 0 and switches the hardware from TX to RX. From this moment on, the sound processing thread call the 'Receive()'- routine in the DLL. The DLL should set dll_rx_req=0 now. Sooner or later, the complete changeover cycle continues at step A. 2.6 A timebase for real-time processing --------------------------------------------- If your decoder needs some kind of time-reference, DO NOT USE THE PC's REAL-TIME CLOCK in the Decode()-routine ! Reason: You never know (exactly) when DIGM_Decode() will be called and how many microseconds have passed between recording an audio sample and processing it. Instead, you should use a 'sample counter' in combination with the precise sample rate to derive a kind of relative timebase (for bit synchronisation etc). If you need the ABSOLUTE time when a sample has been received, use the parameter 'time_of_recording' which is passed in every call to DIGM_Decode(). This parameter is the timestamp of the FIRST sample in an audio 'chunk'. It is measured in double-precision floating-point format as "Seconds elapsed since 00:00:00 GMT(=UTC), January 1, 1970". Together with the precise (calibrated!) sample rate, you can calculate the timestamp for every sample in the audio buffer. For european users, SpectrumLab has a built-in decoder for longwave time signals (from the UK, Switzerland and Germany) which can be used to set the PC's internal clock to an accuracy of 20..50 milliseconds, depending a little on the signal strength and the PC's speed. 3. Implementation of a new DIGIMODE DLL ----------------------------------------- Look at the provided 'DLL skeletons'. There is one in C++, one in DELPHI, and maybe other programming languages. Most of the 'red tape' is included there. First, try to compile one of the skeletons with your compiler. You'll have to tell your compiler somehow that you want a DLL instead of an EXE file. Second, try to load the new compiled DLL with one of the DLL test pro- grams (in the same directories like as the skeletons). The DLL test program will look for some of the interface routines in the DLL and try to call them. If everything is ok (Routine addresses found AND calling the routine via pointer), you are lucky. If not, try to adjust the 'Project Options' until it works. Third, add a few calls from the DLL skeleton into your source modules. You don't have to implement all routines mentioned in this document in your DLL. If the terminal application cannot find a routine, it assumes that the routine is not needed and will not try to call it (this is possible because the DLL is 'dynamically' loaded. If the DLL was 'statically' loaded, Windows would show an error message and refuse to start the application !) 3.1 Details for implementing a digimode DLL in DELPHI -------------------------------------------------------- Create a DLL without Borland's VCL and without the need for BORLNDMM.DLL: - Do not use large strings. Use the PChar type instead, which is the same as a null-terminated "C"-string. - Do not include "Visual Components" in your project. 3.2 Details for implementing a digimode DLL in C++Builder ----------------------------------------------------------- Create a DLL without Borland's VCL-stuff inside: - Use the "Console Wizard" , **NOT** "File..New..DLL" ! - See Borlands Help System on: "Creating DLLs in C++Builder". - Exported functions in the code should be identified with the __EXPORT or __declspec(dllexport) modifier (in the header file) Note: Borland sometimes uses two underscores to be incompatible with Microsoft and/or to make some programmers angry. Thanks. 3.3 More notes about "C" function prototypes in DLLs ----------------------------------------------------------- - The modifier "extern C" prevents C++ 'name mangling' which would compiler-dependent. The linker usually uses mangled names to verify a proper parameter list, so we must be very careful at this point ! - The "_stdcall" modifier for a function defines how the parameters are passed. In DELPHI (Pascal) the identifier is 'stdcall' after the function declaration. The _stdcall calling convention is also used for calls of the WinAPI routines (INTERNALLY!), so other compilers (VBasic, Java) will also support this calling convention. 3.4 Writing an own user-interface ------------------------------------ If you want to implement a special dialog to control all parameters of your algorithm during run-time, you must generate an own application (which can run side-by-side with the terminal). Use the Digimode-DLL as a small 'interface' in this case. Contact me (DL4YHF) for further details. Revision history (latest modification first) ----------------------------------------------------- March 10, 2001: First attempt to dynamically load DLLs: - load a DLL generated with DELPHI into a C++Builder program - load a DLL generated with C++Builder into a DELPHI program - first draft of the DLL interface documentation