Writing device dll for miniscope v3

This tutorial will guide you though process of creating custom input device library for miniscope v3.
I will refer to hidpic.dll and Generic HID Demo from Microchip as example device.

Make sure what capabilities are offered by device


Our example device is Generic HID Demo from Microchip running on PICDEM USB or similar hardware. Firmware will be not modified.
This device has no ability to change sensitivity and coupling type.
This device interface consists only of get single single sample function, so sampling frequency, triggering and buffer size is fully determined by host.

Choose compiler/IDE

Simplest choice of compiler/IDE would be Turbo C++ 2006 Explorer since it is used to build miniscope and thus could make easier modifying miniscope and it's dll same time. It has field proven integrated CodeGuard runtime error checking that can highly speed up locating memory and resources managements related errors. Last by not least it's free and it has no commercial usage restrictions.

Basic requirement for input device dll is no exported functions name decoration. Virtually any C/C++ compiler can be used, but some compiler/linkers may require additional configuration.

For hidpic.dll MinGW/Code::Blocks was choosen. It comes with HID headers and libraries so no DDK download is required - that's big advantage.

Project setup

Device dll interface is described by MiniscopeDevice.h header. This file belongs to miniscope project, but dll projects should include it. Single interface description helps to keep projects consistent. I would recommend to place dll projects on the same directory tree level that miniscope is placed.
Use Code::Blocks project wizard:
Code::Blocks dll project wizard

Modify project settings to add function name export without any mangling, add -Wl,--add-stdcall-alias to linker options for every target created:
Code::Blocks dll project options - linker

Create library interface

Project wizard has created generic dll. It's time to define functions exported by library. Library interface is described inside files MiniscopeDevice.h and MiniscopeDeviceCapabilities.h that are part of miniscope project. Before including MiniscopeDevice.h _EXPORTING macro has to be defined, otherwise header will work as import, not export declaration. If miniscope and dll project directories are placed next to each other relative path can be used:

#define _EXPORTING
#include "..\miniscope\device\MiniscopeDevice.h"
#include "..\miniscope\device\MiniscopeDeviceCapabilities.h"

Miniscope is (yet) not dead project and some changes can be made to dll interface sooner or later. To avoid problems with dll interface incompatibility (especially tricky ones like function argument lists mismatch that can manifest after "successfull" loading library as access violation) dll has simple versioning scheme. After loading library (that is checking for pointers to its functions by miniscope) GetMiniscopeInterfaceDescription function is called. Returned by this dll function version is compared with version from MiniscopeDevice.h used by miniscope. If miniscope and library were compiled with different versions of MiniscopeDevice.h (with different interface version numbers) miniscope will abort loading library. GetMiniscopeInterfaceDescription function must remain consistent in every interface version, but that should not be problem - this function has only one simple purpose.
You should not use hard coded version numbers but always use DLL_INTERFACE_MAJOR_VERSION, DLL_INTERFACE_MINOR_VERSION macros from MiniscopeDevice.h.

static const struct S_DEVICE_DLL_INTERFACE dll_interface =

void __stdcall GetMiniscopeInterfaceDescription(struct S_DEVICE_DLL_INTERFACE* interf)
	interf->majorVersion = dll_interface.majorVersion;
	interf->minorVersion = dll_interface.minorVersion;

The rest of this tutorial applies to interface version 0.3 (MAJOR.MINOR). Please check DLL_INTERFACE_MAJOR_VERSION and DLL_INTERFACE_MINOR_VERSION in your copy or MiniscopeDevice.h.

Miniscope needs informations about device ADC resolution, sampling frequency, sensitivity, etc. This information is passed by calling GetDeviceCapabilities function.

static float fSampl[] = { 100e-3, 50e-3, 20e-3, 10e-3, 5e-3 };
static unsigned int iBuf[] = { 512, 1024, 2048, 4096 };

void __stdcall GetDeviceCapabilities(struct S_SCOPE_CAPABILITIES **caps)
	static float fSens[] = { 4.8828e-3 };
	static enum E_ACQUISITION_MODE eAcqMode[] = { ACQUISITION_1SHOT };
	static enum E_TRIGGER_TYPE eTriggerType[] = { TRIGGER_MANUAL };
	static enum E_COUPLING_TYPE eCouplingType[] = { COUPLING_DC };
	static struct S_SCOPE_CAPABILITIES capabilities =
		11,		//unsigned int iBitsPerSample;
		sizeof(fSens)/sizeof(fSens[0]), fSens,
		sizeof(fSampl)/sizeof(fSampl[0]), fSampl,
		sizeof(eTriggerType)/sizeof(eTriggerType[0]), eTriggerType,
		sizeof(eAcqMode)/sizeof(eAcqMode[0]), eAcqMode,
		sizeof(eCouplingType)/sizeof(eCouplingType[0]), eCouplingType,
		sizeof(iBuf)/sizeof(iBuf[0]), iBuf,
		0		//unsigned int iOffset;  ///< is offset setting available?
	*caps = &capabilities;

This device will have five sampling period values (fSampl), from 0.1 s to 5 ms and four possible buffer sizes from 512 B to 4 kB (meaning we can see from 512 to 4096 samples at once). These tables will be needed later outside GetDeviceCapabilities by SetSamplingPeriod and SetBufferSize functions. Other settings will have no effect on device. Device has sensitivity of 4.8828 mV per bit, single-shot acquisition mode, direct current coupling and can be triggered only by host.
Physical device has no data buffer, all buffering is done inside dll, so we can choose virtually any buffer size (limited by our patiency when waiting for filling the buffer).
Why are we reporting 11 bits resolution to miniscope? Our ADC has 10 bits, but miniscope assumes that input voltage range is symmetric with respect to 0 V level and this ADC can measure only positive voltage, so we've added one bit to reported resolution. Reported value of resolution is not critical - if we report wrong value it could be fixed by zooming in or out our plot.

To pass information about new events from dll to miniscope four callbacks are used. Initially we should declare pointers to this callbacks as invalid.

// add text to miniscope log
// send info when device is connected or disconnected
// send info when new data arrives
// send info when device is triggered (new data frame starting)
// opaque pointer used by callbacks
void *callbackCookie;

Miniscope is making callbacks pointers valid by calling SetCallbacks function.

void __stdcall SetCallbacks(void *cookie, CALLBACK_LOG lpLog, CALLBACK_CONNECT lpConnect,
	lpLogFn = lpLog;
	lpConnectFn = lpConnect;
	lpDataFn = lpData;
	lpTriggerFn = lpTrigger;
	callbackCookie = cookie;

CallbackCookie is opaque pointer used by miniscope to distinguish callback sources. It is added to every callback call, i.e. lpLogFn(callbackCookie, "hidpic.dll loaded!").

Dll hast to export function to show it's options window. We don't need any settings in hidpic.dll, so we will just show informative message.

void __stdcall ShowSettings(HANDLE parent)
	MessageBox(NULL, "This dll has no settings.\n"
        "This dll works with device running Generic HID demo from MCHPUSB 2.5.",
        "Device DLL", MB_ICONINFORMATION);

For device connection state control Connect() and Disconnect() functions are used. Actual connection state if passed as parameter of CALLBACK_CONNECT, Connect() function does not have to block program flow until device is found and connection is established. In this implementation real action associated with these function is hidden inside HidScope object.

int __stdcall Connect(void)
	return hidScope->Connect();
int __stdcall Disconnect(void)
	return hidScope->Disconnect();

Other functions required by miniscope when loading dll are SetAcqMode, AcquireStart, AcquireStop. They are related to continuous acquisition and we leave their implementation empty. These functions may be changed to optional in upcoming interface versions.

int __stdcall SetAcqMode(int iId)
	return 0;
int __stdcall AcquireStart(void)
	return 0;
int __stdcall AcquireStop(void)
	return 0;

We declared that our device has options to change sampling frequency, buffer size and has manual trigger. We must than define additional functions. Again, their implementation is hidden inside HidScope class.

int __stdcall SetSamplingPeriod(int iId)
	if (iId < 0 || iId >= (int)(sizeof(fSampl)/sizeof(fSampl[0])))
		return -1;
	return hidScope->SetSampling((int)((float)1000 * fSampl[iId] + 0.5));
	return 0;
int __stdcall SetBufferSize(int iId)
	return hidScope->SetBufferSize(iBuf[iId]);
int __stdcall ManualTrigger(void)
	return hidScope->ManualTrigger();

There are few other optional functions: storing and retrieving settings and calibration, choosing trigger type. We do not have to export them.

Connect dll with your device

Any communication with PIC microcontroller is hidden inside HidScope class. This class uses also callbacks provided by miniscope to inform about device connection or disconnection and send new samples.
You can download full source code of hidpic.dll from miniscope v1 page. There is also standalone console application to test HID communication with Microchip Generic HID Demo project.

 "Cookie monsters": 2678857    Parse time: 0.001 s