Кафедра света и электричества

Общение с контроллером по USB

Имеющийся в контроллере интерфейс USB можно использовать не только для заливки в него программ, но и для связи с компьютером. Спецификация USB достаточно сложна, но мы будем использовать только часть его возможностей, и пользоваться готовым кодом от Objective Development Software GmbH. Первая задача выглядит так: передать произвольную стркутуру данных от PC контроллеру и обратно. Такой способ обмена несколько ограничен в возможностях, но имеет целый ряд полезных свойств. Во-первых, на контроллере экономится оперативная память. Ее и так там немного. Во-вторых, код получается чуть меньше. В третьих, используется простая и понятная идея межпроцессного взаимодействия - общая область памяти, которая по запросу с PC копируется на контроллер или с контроллера на PC. Напомним, что USB не работает по прерываниям, любая инициатива по передаче данных исходит от PC.

Использование интерфейса USB осложняется двумя обстоятельствами. Во-первых, для любого нестандартного устройства (а наш контроллер или то, что мы делаем на его основе - явно нестандартное устройство) нужен драйвер, разработка которого - не слишком приятное занятие. Вторая проблема состоит в том, что устройства USB распознаются на основании кода производителя, который нужно покупать у организации, заведующей протоколом USB.

К счастью, из этого безвыходного положения имеется простой выход. Для стандартного класса устройств, который называется HID (Human Interface Device, к нему относятся мышь, клавиатура и прочие не очень быстрые устройства) стандартом определены команды передачи настроечных параметров. То есть, фактически, любых не очень больших блоков данных. Это-то нам и надо ! Наше устройство будет заявлять себя как HID, но понимать только команды установки параметров SetFeature и чтения параметров GetFeature. С точки зрения HID интерфейса это будет совершенно бессмысленное устройство, но проверку на осмысленность пока что никто в стандарт не вводил.

Проблема с идентификаторами решается тоже просто. Фирма Objective Development Software GmbH заплатила один раз денег, зарегистрировала код, и теперь предлагает его всем желающим совершенно бесплатно. При условии соблюдения лицензии GNU GPL. Возникает вопрос: как же различать разные устройства, сделанные под одним и тем же кодом производителя ? У каждого USB устройства, кроме кода, есть еще строка, идентифицирующая устройство. Помните, при подключении новой флешки появляется сообщение, идентифицирующее изготовителя и само устройство ? Это оно и есть. Для того, чтобы строки были уникальными, Objective Development Software GmbH предлагает использовать либо ваше доменное имя, либо ваш адрес e-mail в качестве USB_CFG_VENDOR_NAME. Этот параметр надо прописать в файле usbconfig.h

Опишем для начала структуру данных.

struct dataexchange_t	// управление шаговыми двигателями
{
  uint8_t  timems;		// время шага в ms
  uint16_t cmd[16];     // количество шагов для каждого мотора
};

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

Теперь нужно сделать программы для компьютера и для контроллера. В контроллере все происходит следующим образом:

#include "usbdrv.h"
#include "oddebug.h"

#define CMDSIZE   16

struct dataexchange_t
{
  uint8_t  flags;
  uint8_t  timems;
  uint16_t switches;
  uint16_t cmd[CMDSIZE];
} pdata;

/* ----------------------------- USB interface ----------------------------- */

PROGMEM char usbHidReportDescriptor[22] = {		/* USB report descriptor */
    0x06, 0x00, 0xff,              			// USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    			// USAGE (Vendor Usage 1)
    0xa1, 0x01,                    			// COLLECTION (Application)
    0x15, 0x00,                    			//   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              			//   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    			//   REPORT_SIZE (8)
    0x95, sizeof(struct dataexchange_t),		//   REPORT_COUNT
    0x09, 0x00,                    			//   USAGE (Undefined)
    0xb2, 0x02, 0x01,              			//   FEATURE (Data,Var,Abs,Buf)
    0xc0                           			// END_COLLECTION
};
/* Обычно используется несколько типов параметров устройства (feature reports),
 * для каждого из которых нужен свой идентификатор. Но здесь используется всего 
 * один тип параметра, и идентификаторы не нужны. Фактически переданный блок данных
 * должен содержать на 1 байт больше - это и есть идентификатор параметра, в нашем 
 * случае он будет равен 0. Feature report попадет по назначению при соблюдении двух
 * условий: он должен иметь заданную длину и первым байтом должен быть 0.
 */

Обратите внимание, что используется название структуры. Массив usbHidReportDescriptor описывает данные, которые будут передаваться по USB. Нас в нем интересует единственный элемент, отвечающий за размер передаваемых данных. Все остальные элементы имеют некие разумные значения по умолчанию.

Далее следует совершенно неизменная часть кода.

static uchar    currentAddress;
static uchar    bytesRemaining;

void readdata();    // эта функция будет вызвана, когда компьютер запросит данные
void writedata();   // эта функция будет вызвана, после того, как компьютер запишет данные
struct dataexchange_t pdata; // а это сами данные

/* usbFunctionRead() вызывается, когда USB хочет прочитать часть данных  */

uchar   usbFunctionRead(uchar *data, uchar len)
{
    uint8_t i;
    if(len > bytesRemaining)
        len = bytesRemaining;
    uchar *buffer=(uchar*)&pdata;
	if(!currentAddress) /* ни один кусок данных еще не прочитан */
	{
	  readdata();
	}
    for(i=0;i < len;i++)
	  data[i]=buffer[i+currentAddress];
    currentAddress += len;
    bytesRemaining -= len;
    return len;
}

/* usbFunctionRead() вызывается, когда USB хочет записать часть данных  */

uchar   usbFunctionWrite(uchar *data, uchar len)
{
    uint8_t i;
    if(bytesRemaining == 0)
        return 1;               /* конец передачи */
    if(len > bytesRemaining)
        len = bytesRemaining;
    uchar *buffer=(uchar*)&pdata;
    for(i=0;i < len;i++)
	  buffer[i+currentAddress]=data[i];
    currentAddress += len;
    bytesRemaining -= len;
    if(bytesRemaining == 0) /* все данные получены */
    {
        writedata();
    }
    return bytesRemaining == 0; /* 0 означает, что есть еще данные */
}

Функция readdata() будет вызвана, когда компьютер запросит данные. Она должна заполнить структуру pdata, и потом данные из этой структуры будут переданы компьютеру, возможно, за несколько приемов. Аналогично, функция writedata() будет вызвана после получения всех данных структуры. Программа должна учитывать, что могут возникать ситуации, когда данные еще не переданы до конца.
Еще одна важная функция называется usbFunctionSetup. Она отвечает за общение с драйвером USB.

usbMsgLen_t usbFunctionSetup(uchar data[8])
{
usbRequest_t    *rq = (void *)data;

    if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS){    /* HID устройство */
        if(rq->bRequest == USBRQ_HID_GET_REPORT){  
            bytesRemaining = sizeof(struct dataexchange_t);
            currentAddress = 0;
            return USB_NO_MSG;  
        }else if(rq->bRequest == USBRQ_HID_SET_REPORT){
            bytesRemaining = sizeof(struct dataexchange_t);
            currentAddress = 0;
            return USB_NO_MSG; 
        }
    }else{
        /* остальные запросы мы просто игнорируем */
    }
    return 0;
}

Кроме этого, нужна еще и самая главная функция - main().

int main(void)
{
   	usbInit();
   	usbDeviceDisconnect();  /* вызываем перенумерацию устройств USB. Прерывания должны быть запрещены ! */
   	_delay_ms(250);         /* вызываем таймаут */
   	usbDeviceConnect();     /* подсоединяемся к шине USB */
   	sei();
   	for(;;)
	{                
       	usbPoll();          /* эту функцию надо вызывать не реже, чем раз в 50 миллисекунд */
 	}
    return 0;
}

Все остальные действия программы происходят либо по прерываниям, либо в функциях readdata() и writedata(). Тот факт, что драйвер от Objective Development не использует таймер является его большим достоинством. Это позволяет программе использовать таймеры как угодно и для каких угодно целей, лишь бы обработка прерываний не занимала слишком много времени.

Теперь перейдем к программе, которая будет выполняться на PC. Напишем универсальный класс при помощи шаблонов. Параметром шаблона будет служить та самая структура, которую мы собираемся передавать контроллеру. Начнем с определения некоторых типов:

typedef void (WINAPI* t_HidD_GetHidGuid)( OUT LPGUID );
typedef BOOLEAN (WINAPI* t_HidD_GetManufacturerString)(IN HANDLE, OUT PVOID, IN ULONG);
typedef BOOLEAN (WINAPI* t_HidD_GetProductString)(IN HANDLE, OUT PVOID, IN ULONG);
typedef BOOLEAN (WINAPI* t_HidD_GetFeature)(IN HANDLE, OUT PVOID, IN ULONG);
typedef BOOLEAN (WINAPI* t_HidD_SetFeature)(IN HANDLE, IN PVOID, IN ULONG);

А вот так будет выглядеть инициализация переменных:

  hDLL = LoadLibrary("HID.DLL");
  if(hDLL != NULL)
  {
    HidD_GetHidGuid = (t_HidD_GetHidGuid)GetProcAddress(hDLL, "HidD_GetHidGuid");
    HidD_GetManufacturerString = (t_HidD_GetManufacturerString)GetProcAddress(hDLL, "HidD_GetManufacturerString");
    HidD_GetProductString = (t_HidD_GetProductString) GetProcAddress(hDLL, "HidD_GetProductString");
    HidD_GetFeature = (t_HidD_GetFeature) GetProcAddress(hDLL, "HidD_GetFeature");
    HidD_SetFeature = (t_HidD_SetFeature) GetProcAddress(hDLL, "HidD_SetFeature");
    if(HidD_GetHidGuid)
    {
      HidD_GetHidGuid(&m_hidguid);
    }
  }

Зачем все это надо ? Дело в том, что файлы hid.h и hid.lib не входят в комплект SDK. Более того, эти функции в разных версиях hid.dll имеют разные номера! (Возможно, именно поэтому hid.lib и нет в SDK). Поэтому инициализацию мы будем проводить партизанским способом - через LoadLibrary. Функции для посылки и приема данных выглядят достаточно просто.

template<typename T> int HIDLibrary<T>::SendData(T* data)
{
   char vpath[datasize+16];
   vpath[0]=0;
   int len=datasize+1;
   HANDLE h = CreateFile(m_ConnectedDevice.c_str(),GENERIC_READ,0,0,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0);
   if(h != INVALID_HANDLE_VALUE)
   {
		memcpy(vpath+1,data,datasize);
		int err = HidD_SetFeature(h, vpath , len);
		CloseHandle(h);
		return err;
   }
   else
	    return 0;
}

template<typename T> int HIDLibrary<T>::ReceiveData(T* data)
{
    char vpath[datasize+16];
	memset(vpath,0,sizeof(vpath));
	int len=datasize+1;
	HANDLE h = CreateFile(m_ConnectedDevice.c_str(),GENERIC_READ,0,0,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0);
   if(h != INVALID_HANDLE_VALUE)
   {
		int err = HidD_GetFeature(h, vpath , len);
		memcpy(data,vpath+1,datasize);
		CloseHandle(h);
		return err;
   }
   else
	    return 0;
}

Полностью файлы проекта можно скачать тут. А посмотреть, как работают шаговые двигатели под управлением программы, использующей этот класс, можно на YouTube.