Delphi работа с usb

Программирование USB-устройств в Delphi

Что такое HID-устройство

Свойства HID-устройства

Организация обмена данными между HID-устройством и компьютером

Чтобы описать взаимодействие HID-устройства с компьютером, употребим термин «хост». В данном случае под ним понимается управляющее устройство в общей физической архитектуре взаимодействия по USB-протоколу. Так, все порты в компьютере — хосты. К ним можно подключать различные USB-устройства (флэшки, мыши, веб-камеры, фотоаппараты и проч.), которые хоста не имеют. Хост обеспечивает обнаружение, подключение, отключение, конфигурирование устройств, а также сбор статистики и управление энергопотреблением.

HID-устройство может само установить частоту опроса, во время которого выясняется наличие в нем каких-либо новых данных. Значит, даже на таком низком уровне программист может довериться системе, поскольку частота опроса и другие параметры обмена данными должны быть заранее заданы в программе контроллера HID-устройства. Этим протокол HID отличается от общего описания USB 1.1 или USB 2.0, в котором нет жестких требований к организации протокола. Однако при специфических задачах, требующих повышенного уровня безопасности, может оказаться довольно сложно избавиться от циклических опросов, когда постоянно передаются почти одни и те же блоки данных.

Особенности программирования HID-устройств

HID-устройства имеют специальные дескрипторы. Когда хост определит, что устройство принадлежит к классу HID, он передает управление им соответствующему драйверу. Предполагается, что дальнейший обмен данными ведется под его руководством.

В Windows за доступ к HID-устройствам отвечает системная служба HidServ. Подробнее о функциях запросов к HID-устройствам и других особенностях работы с HID-драйвером рассказывается в работе П.?В. Агурова «Интерфейс USB. Практика использования и программирования» (СПб.: БХВ-Петербург, 2005).

Программирование HID-устройств на «верхнем уровне»

Нелегкую жизнь «прикладных» программистов, работающих на Паскале, облегчает проверенный модуль HID. PAS, программная оболочка для hid. dll (Hid User Library — как указано в свойствах файла). В комментариях к файлу сообщается, что в основе его лежат модули hidsdi.h и hidpi.h корпорации Microsoft. А сам файл HID. PAS — часть пакета JEDI (http://jvcl.sourceforge.net ).

Для работы с HID-устройством в среде Delphi for win32 применяется компонент TJvHidDeviceController, представляющий собой удобный глобальный менеджер для доступа к HID-устройствам. А уже на его базе можно получить объектный экземпляр для работы с конкретным устройством.

Основные свойства и события компонента TJvHidDeviceController

Рассмотрим компонент TJvHidDeviceController более подробно. Событие OnArrival срабатывает на поступление (подключение) в систему HID-устройства, доступ к устройству предоставляется в обработчике этого события через экземпляр класса TJvHidDevice. Простое событие OnDeviceChange реагирует на изменение состояния устройства, оно только сигнализирует об изменениях в системе. Событие OnDeviceData срабатывает при поступлении данных от одного из HID-устройств и передает обработчику следующее: HidDev: TJvHidDevice; — устрой-ство, от которого были получены данные;

Событие OnDeviceDataError уведомляет об ошибке передачи данных, передавая в процедуру обработки параметры H >

Для последовательного перечисления имеющихся в системе HID-устройств по вызову метода Enumerate предназначено событие OnEnumerate, т. е. в обработчике события найденные устройства последовательно передаются в виде объектов. Это событие принудительно инициируется методом Enumerate, использующимся для «проведения» имеющихся HID-устройств через обработчик, например при ревизии состояния HID-устройств по инициативе хоста (компьютера).

Событие OnRemoval срабатывает на физическое извлечение устройства из системы и имеет тот же тип обработчика TJvHidUnplugEvent, что и для OnDeviceUnplug. Функция CountByProductName выдает количество устройств, удовлетворяющих указанному в аргументе имени продукта, а CountByVendorName — указанному в аргументе имени производителя.

Основные свойства и события класса TJvHidDevice

Класс TJvHidDevice — виртуальное представление отдельно взятого HID-устройства. Новый объект этого класса можно получить, как было уже сказано, из события OnArrival или OnEnumerate. Функционал классов TJvHidDeviceController и TJvHidDevice частично дублируется, поскольку в первом из них интегрированы общий инструментарий для работы с набором имеющихся в системе HID-устройств и механизм доступа к одному из них.

Устройство можно однозначно идентифицировать по свойствам SerialNumber, ProductName и VendorName. Чтобы получить сведения о поступлении данных с применением такого объекта, можно воспользоваться событием OnData. Отсылка данных ведется через метод WriteFile (в строгом смысле — через функцию). WriteFile — это оболочка системной функции WriteFile (kernel32).

Чтобы проконтролировать факт извлечения устройства, следует присвоить свой обработчик событию OnUnplug. Перед началом обмена данными с HID-устройством нужно удостовериться в самой возможности такого обмена с помощью HasReadWriteAccess. В этом классе на возникновение ошибки обмена данными даже есть отдельное событие OnDataError.

А теперь рассмотрим фрагменты кода из «живого» проекта, реализующего тестовое клиентское приложение для организации обмена данными с нестандартным устройством — пластиковыми чип-картами на базе HID. В борьбе за реализм автор взял на себя смелость не выкидывать из листингов «лишние» технологические обвязки кода.

Метод ScanDevices (листинг 1) предназначен для инициирования процесса поиска в системе необходимого HID-устройства. Большая часть кода, за исключением вызова метода Enumerate, необязательна и обеспечивает гибкость приложения, например, для того, чтобы в эту же тестовую программу можно было добавить возможность работы по интерфейсу, отличному от HID. Метод AddError выводит в окно отладочную информацию в процессе работы программы.

Читайте также:  Установка mac os по сети

Листинг 1. Инициирование диска поиска устройства

В листинге 2 приведен обработчик события OnEnumerate для поиска необходимого внешнего устройства. Для простоты будем считать, что программа может работать только с одним устройством нужного ей типа.

Листинг 2. Обработчик поиска

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

Дело в том, что здесь разработчику предоставляется возможность реализовать свои творческие способности. Вернее, разработчикам, потому что процесс создания нового протокола очень часто бывает двусторонним, и при этом первую скрипку играет тот, кому труднее реализовывать алгоритм обмена. В общем, каким бы ни был протокол обмена, всегда приятно делать каждую программную сущность максимально наглядной и самодостаточной, пусть даже в ущерб некоторым общепринятым традициям.

Ибо лучшее решение — то, которое будет реализовано в сжатые сроки с минимальной привязкой к программной среде и с большими возможностями дальнейшего развития. На основе этих принципов был создан протокол обмена верхнего уровня, где главное понятие — «команда». Из листинга 3 видно, насколько автор любит строковые данные, не раз спасавшие его при отладке программных модулей. Как же замечательно, что у нас вообще есть тип String! Все команды протокола делятся на категории (классы), внутри которых существует код команды, однозначно характеризующий ее назначение. Параметр edParam служит для отсылки данных в устройство, а параметр edAnswerData содержит в себе полученные от устройства данные.

Строковый тип описанных членов записи позволяет свободно и наглядно манипулировать данными в формате HEX-строки. И что самое приятное, формат описанной записи идеологически стоит где-то посередине между ее непосредственным назначением и различными формами ее представления (INI, HEX, XML и т. д.)

Листинг 3. Пример структуры отсылки данных для описания алгортимов обмена данными с внешним устройством.

// структура соответствует протоколу

// верхнего уровня,
// оговоренному для специфического устройства.
TedCommand = RECORD Caption: String; // метка команды
INDEX:Integer;
// индекс команды Enabled:Boolean; // флаг пригодности команды
WT:Cardinal; // время обработки
edClass: String [2] ;// HBX-строка класса команды
edCode:String[2];// HEX-строка кода команды
edLength:String[2]; // длина
edParam: String; // HEX-CTpoкa (параметр отсылки)
edAnswerdta:String;// HEX-CTpoкa
// полученных данных
IsAnswerdta:Boolean;
// флаг новых полученных данных ERROR:String; // поле для последней ошибки
WORKED:Boolean; //флаг обработки команды
end;

Выполнение команды, т. е. отсылка данных в устройство, реализовано с применением отсылки пакетов данных длиной 8 байт (листинг 4). Эта длина — не единственное решение, такой выбор продиктован требованиями протокола верхнего уровня и в каждом конкретном случае может быть другим. Это, что называется, дело вкуса. Странный флаг IsUSBMode в методе ExecuteCommand (листинг 5 на «Мир ПК-диске») оставлен как напоминание о том, что вместо работы с USB нам может потребоваться использовать COM-порт или какой-то другой интерфейс. В начале отсылаемой группы данных в устройство передается синхросерия произвольно выбранного формата (например, 3E3E3E2B), сообщающая устройству, что у него на входе вполне легальные данные. Напомню, что в данном случае речь идет не столько о HID, сколько о специфическом протоколе верхнего уровня, идеологически оторванном от «железа» и предназначенном для решения особых прикладных задач.

Листинг 4. Отсылка данных

В обработчике GetDataExecutor полученных от устройства данных (пакет по 8 байт) использовано специально созданное событие OnNewInputData для передачи первично обработанных данных на дальнейшую обработку, причем с указанием их старого и нового значений (листинг 6 на «Мир ПК-диске»). Таким образом, события поступления необработанных данных и указание на дальнейшую обработку развязываются, позволяя добавлять какой-то специфический алгоритм предупреждения на раннем этапе ошибочной, повторной или ненужной входной информации.

Представленные здесь примеры работы с HID-устройством иллюстрируют общую идею статьи — относительную простоту программирования нестандартных HID-устройств средствами Delphi.

Научитесь программировать микроконтроллеры прямо сейчас — получить видеокурс по микроконтроллерам!

Нашел на просторах рунета код для отлова подключения к усб порту

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

Инжектю длл, выходит сообщение good , значит заинжектилось норм, но вот при вставке флэшки не выходит сообщение tada! , хотя на форме все работает хорошо. Я думаю, что проблема в регистрации ( USBRegister ) мб хэндл не тот, можете мне подсказать ?

1 ответ 1

Представленный в вопросе код содержит ошибки:

  1. При инициализации dll (между begin и end, в секции initialization используемых модулей) крайне не рекомендуется использовать что-то "тяжелое". Объяснение легко тянет на отдельную статью (например — статья Gunsmoker-а )
  2. Уведомления о подключении устройств к компьютеру используют штатный механизм сообщений Windows , т.е. код, реализующий этот функционал, должен выполняться в потоке, который реализует цикл выборки сообщений (Message loop).

Собственно, код из вопроса не работает "благодаря" пункту 2.

С учетом обоих замечаний, код должен выполнять следующие действия:

  1. Максимально быстро завершить работу между "главными" begin и end кода dll.
  2. После "отложенной" инициализации нужно организовать цикл выборки сообщений, который должен работать до момента выгрузки dll (или любых других условий, на ваш выбор).
  3. По завершению работы цикла выборки сообщений — провести деинициализацию.
Читайте также:  Ati radeon старые видеокарты

Пункт 1 будем выполнять при помощи потока. Т.е. при инициализации dll создаем свой поток и уже в нем проводим основную работу. Этот метод тоже не очень хорош, посему — если есть возможность, то при внедрении dll лучше вызвать отдельную функцию, экспортируемую этой dll и уже в ней производить указанные манипуляции.

Итого, общий код dll будет выглядеть примерно так:

Этот код выполняет действия №1 (не делать ничего "тяжелого" при инициализации dll, максимально быстро отдать управление) и №3 (деинициализация).

Теперь нужно реализовать основное действие №2. Объявление класса потока оставим на ваше усмотрение, здесь же остановимся именно на реализации. Код организации выборки сообщений для работы всех окон (созданных в этом потоке) может выглядеть так:

В главном потоке обычного VCL-приложения выборкой сообщений (используя подобный цикл) занимается объект Application: TApplication , а конкретнее — его метод ProcessMessages (лирическое отступление: если вам приходится принудительно вызывать этот метод у себя в приложении — скорее всего, вы что-то делаете неправильно).

В продолжение темы о создании собственного USB-гаджета.
Создание простого устройства.

Раз устройство планируется подключать к ПК, значит вероятнее всего потребуется передача данных между устройством и ПК.
Начнем писать прошивку и софт, наладив связь между ними.

Самым простым вариантом передачи данных является использование класса коммуникационных устройств USB (CDC).
При таком подключении устройство будет видно в системе как обычный виртуальный COM-порт.
Плюсом такого подключения является отсутствие необходимости писать собственные драйвера.
Так же радует простота приема и передачи данных: для работы с портом в Windows достаточно открыть его как текстовый файл и производить обычные операции чтениязаписи.

Железо.

Возьмем схему с минимальной обвязкой МК.

На этот раз нам нужно добавить только 4 контакта к USB и одну кнопку (кнопка нужна только для бутлоадера: куда проще нажать ее и заменить прошивку в устройстве по USB, нежели переставлять чип в программатор).

Не сильно стараясь сделать красиво, разводка может выглядеть так:

Но при желании часто экспериментировать с подключаемыми компонентами лучше сразу развести каждую ногу МК сделав аналог ардуино — Jaluino.

Прошивка

include 18f2455 — библиотека для используемого МК

enable_digital_io () — переключение всех входов на цифровой режим

alias Button is pin_B7 — раз уж у нас подключена кнопка, объявим ее
pin_B7_direction = input — кнопка у нас работает на вход

— одна строчка — и у нас есть все необходимое для работы с USB CDC
include usb_serial — бибилотека для работы с usb

usb_serial_init () — —инициализируем USB CDC
forever loop — основной цикл, выполняется постоянно
usb_serial_flush () — обновление usb. Данная процедура выполняет все необходимые
— действия для поддержания соединения с ПК
end loop

Скомпилировав данный код, записав полученный HEX файл в МК при помощи бутлоадера и запустив устройство можно будет наблюдать как в системе опрделится новое устройство: Виртуальный сom-порт.

Теперь, когда устройство уже работает, научим его общаться.

Для чтения принятого байта существует функция usb_serial_read(byte):boolean. При наличии полученного байта она заносит его в указанную переменную и возвращает true, иначе возвращает false.

Для отправки байта существует процедура usb_serial_data. Она замаскирована под переменную, потому для отправки байта достаточно присвоить ей значение отправляемого байта.

Объявим переменную размером в байт до основного цикла, в основном цикле будем проверять наличие полученных байт, и при их наличии отправлять их обратно.

include 18f2455

enable_digital_io ()

alias Button is pin_B7
pin_B7_direction = input


include usb_serial

usb_serial_init ()
var byte ch — объявляем переменную
forever loop — основной цикл
usb_serial_flush ()
if ( usb_serial_read ( ch ) ) then — если байт получен, он будет записан в ch
usb_serial_data = ch — отправляем полученный байт обратно
end if
end loop

Компилируем, зажимаем кнопку, передергиваем питание, запуская бутлоадер, меняем прошивку, запускаем.
Устройство снова определилось в системе, теперь нам нужен софт, дабы протестировать работу устройства.

Пока у нас нет своего, используем готовый терминал: я использовал программу RealTerm.
Открываем порт с нужным номером и отправляем данные.


И нам в ответ приходит то, что мы отправили. Значит, все работает как надо.

Итак, наш микроконтроллер умеет принимать байты и тут же отправлять их обратно. Теперь напишем свой софт для общения с ним (я буду использовать Delphi).

Создаем новый проект, раскидываем по форме необходимые компоненты:
SpinEdit1 — для указания номера порта
Button1 — для установки соединения
Button2 — для разрыва соединения
SpinEdit2 — для ввода байта в десятичном виде
Button3 — для отправки байта
Memo1 — для вывода принятой информации.

Как уже было сказано выше, с com-портом нужно работать так же, как и с обычным текстовым файлом: используя функции CreateFile, WriteFile и ReadFile.

Дабы не вдаваться в подробности, возьмем готовую библиотеку для работы с com-портом: ComPort.

Вешаем на каждую кнопку необходимую задачу и получаем конечный код:

uses
Windows, Messages, SysUtils, Variants, >Graphics , Controls, Forms,
Dialogs, StdCtrls, Spin,ComPort;

type
TForm1 = class (TForm)
SpinEdit1: TSpinEdit;
Button1: TButton;
Button2: TButton;
SpinEdit2: TSpinEdit;
Button3: TButton;
Memo1: TMemo;
procedure OnRead(Sender: TObject; ReadBytes: array of Byte );
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
< Private declarations >
Port: TComPort;
public
< Public declarations >
end;

Читайте также:  Как правильно подобрать процессор к материнской плате

var
Form1: TForm1;
num: integer;
implementation

procedure TForm1.Button1Click(Sender: TObject);
begin
Port := TComPort.Create(SpinEdit1.Value, br115200); //создаем соединение
Port.OnRead := OnRead; //создаем поток чтения принятых данных
Button2.Enabled := true ; //активируем кнопку закрытия соединения
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
Port.Free; //закрываем соединение
Button2.Enabled := false ; //отключаем кнопку
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
if Button2.Enabled then Port.Write([SpinEdit2.Value]);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
if Button2.Enabled then
Port.Free;
end;

procedure TForm1.OnRead(Sender: TObject; ReadBytes: array of Byte );
var
i:integer;
begin
for i := Low(ReadBytes) to High(ReadBytes) do //проходим по массиву принятых байт
begin
Memo1.Text := Memo1.Text + ‘.’ +InttoHex(ReadBytes[i],2); //добавляем его HEX значение в окно
inc(num); //считаем колв-о принятых байт
end;
if num > 10 then begin
Memo1.Lines.Add( » ); //переносим строку
num := 0;
end;
end;

Запускаем, устанавливаем соединение, отправляем байты:

Вот и готов наш самый простой терминал для работы с самым простым usb-устройством.

Как видно, чтение и запись происходит динамическими массивами байт.

Обрабатывая получаемую информацию можно составить необходимый протокол обмена, подходящий для текущей задачи.

include 18f2455

enable_digital_io ()

alias Button is pin_B7
pin_B7_direction = input


include usb_serial

usb_serial_init ()
var byte ch
var byte i — объявляем вторую переменную
forever loop — основной цикл
usb_serial_flush ()
if ( usb_serial_read ( ch ) ) then — если байт получен выполняем необходимые действия
case ch of — перебираем номер байта
0 : usb_serial_data = 0xff
1 : usb_serial_data = Button — отправка состояния кнопки
OTHERWISE block — если получено что-то иное
for 16 using i loop — отправляем 10 байт с данными
usb_serial_data = ch + i — от ch до ch+15
end loop
end block
end case
end if
end loop

Дополнительные возможности

Если на этом остановиться, получится обычная статья с подробным описанием примера использования библиотеки, коих на просторах сети достаточно. Потому добавлю немного более углубленной информации.

Упрощение отправки данных

Отправлять информацию по одному байту — не всегда удобно. Очень часто может пригодиться библиотека print. Она содержит процедуры по отправке данных всевозможной длины всевозможными форматами: byte,hex,dec,bin,boolean что может упростить вывод данных в программе.

> include print
.
var dword data
print_dword_hex ( usb_serial_data , data )

Название всех команд можно посмотреть в файле библиотеки.

Ожидание подключения к ПК

Если перед стартом основного цикла микроконтроллера необходимо предварительно установить соединение с ПК, то можно дописать перед ним строчки

while ( usb_cdc_line_status () == 0x00 ) loop
end loop

Привязываем к устройству номер порта

Если оставить все как есть, система при каждом новом подключении будет выделять первый свободный номер порта. А это значит что за ним придется всегда следить.
Для того, что бы этого не происходило, необходимо устройству присвоить уникальное значение серийного номера до подключения библиотеки usb:
Номер может быть любой длины и содержать различные символы.

Меняем имя устройства на свое

Поменять имя устройства, видимое в системе до установки драйверов можно объявив массив с именем, как и серийный номер, это необходимо сделать до подключения библиотеки USB.

Но увы, после установки драйверов устройство поменяет имя на указанное в .inf файле, потому поменяем имя и там

Организуем автоподключение устройства

Увы, никаких прямых путей выполнить данную задачу нет, потому придется исхитриться.

Прежде всего необходимо присвоить своему устройству уникальное значение производителя и продукта, дабы легко определять его среди сотен других стандартных CDC-прошивок.
VID и PID выдаются за денюжку, потому пойдем по пуути китайцев: втихую возьмем себе заведомо свободные значения.

Прошивка:
В прошивке необходимо объявить две переменные до подключения библиотеки USB

const word USB_SERIAL_PRODUCT_ID = 0xFF10
const word USB_SERIAL_VENDOR_ID = 0xFF10

Вместо FF10 можно вставить любые два слова (2 байта). Конечный результат содержится в прилагаемом архиве.

Драйвера:
Так как драйвера не предназначены для нашей комбинации VID и PID, допишем наши значения в .inf файл вручную:

[DeviceList]
%DESCRIPTION%=DriverInstall, USBVID_FF10&PID_FF10

[DeviceList.NTamd64]
%DESCRIPTION%=DriverInstall, USBVID_FF10&PID_FF10

Софт:
Для отлова событий подключенияотключения устройства подключим библиотеку ComponentUSB. Не считаю нужным пояснять каждую строчку: все изменения можно увидеть в прилагаемом проекте.

Результат

На скриншоте сложно разглядеть, но кнопка отправки активна только в момент наличия подключенного устройства, при этом каждые 50мс программа подает запрос на получение состояния кнопки (что, впрочем, неправильно, потому как нажатие кнопки должно обрабатываться на МК).

Как видно, организовать обмен данными между МК и ПК через USB — не самое сложное занятие. Полученное соединение можно использовать не только для конечынх целей: оно так же подходит для отладки программы. Ведь отправить на компьютер результаты расчетов, текущие состояния регистров и переменных куда нагляднее, чем моргать парой светодиодов азбукой морзе.

И напоследок: советую заглянуть в исходный код лампы настроения. Там можно найти довольно-таки хороший вариант обработки принимаемых данных для организации удобного протокола обмена.