Программирование PC Класс-обертка для AVR-USB-MEGA16 с поддержкой событий Tue, October 15 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Класс-обертка для AVR-USB-MEGA16 с поддержкой событий Печать
Добавил(а) Даниил Захаров   

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

[Использование событий в классе-обертке для AVR-USB-MEGA16]

Сергей Кухтецкий предложил универсальную прошивку для микроконтроллеров AVR с поддержкой USB – она принимает по USB команды записи и чтения с указанием адреса (куда писать или откуда читать) и значения (если выбрана запись). В случае операции чтения результат отправляется хосту (компьютеру, к которому микроконтроллер подключен), а там с ним можно делать что угодно. Таким образом, это принципиальное решение можно распространить и на круг задач, не связанных с применением микроконтроллеров с ядром AVR.

ИМХО, эта схема подходит для управления любым микроконтроллером, удовлетворяющим следующим требованиям:

- реализована поддержка протокола USB (аппаратно или программно – неважно);
- управление необходимыми функциями микроконтроллера сводится к записи определенного числа байт по определенному адресу.

Как происходит идентификация устройства в системе, и управление периферией - подробно описано в статье Сергея, поэтому не буду повторяться: при необходимости читатель может обратиться туда или в другие источники [1, 2]. Сосредоточимся на том, что уже сделано и чего бы хотелось видеть. Итак, класс-обертка от Сергея Кухтецкого уже умеет:

- находить устройство в системе;
- сохранять внутри себя ссылку на него;
- читать байт по адресу;
- писать байт по адресу.

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

[Вводим исключения для отслеживания ошибок]

Когда я начал разбираться с устройством AVR и областью их применения, меня сразу начали переполнять идеи. Они кружились в моей голове, не давая сосредоточиться на какой-то одной достаточно долго, и поэтому я начал быстренько оформлять их в виде грубых быстрых и кривых набросков кода. При этом в самом начале мне подвернулся упомянутый класс-обертка (моя благодарность Сергею за отличную работу), что очень упростило жизнь и сэкономило кучу времени.

Однако в каждом проекте (а все они достаточно простые и подразумевают работу по USB) приходилось явно реализовывать в своем коде отслеживание статуса подключения устройства. Не устраивал вариант, когда программа заканчивалась в момент отключения микроконтроллера от порта USB, поэтому я решил добавить try/catch блоки и организовать обработку исключений. Выглядело это примерно так (выдержка из одного проекта, где я использовал обертку от С. Кухтецкого собственной модификации):

     set
     {
         try
         {
             usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
                             RQ_IO_WRITE, value, aDDRA, null, 0, 5000);
         }
         catch (Exception e)
         {
             throw new Exception(USB_ERROR); 
         }
     }

То есть вызовы функций LibUSB я помещал в охраняемый блок try/catch, а при возникновении ошибки выбрасывал новое исключение, которое позволяло мне обрабатывать событие отключения. Это оказалось годным решением, но не идеальным, поэтому я пошел дальше. В процессе работы над материалом статьи и бурного обсуждения его c microsin родилась идея отслеживания кодов ошибки. В результате был описан новый класс исключений:

    /// < summary >
    /// Исключение, выбрасывающееся при ошибке чтения/записи USB
    /// < /summary >
    public class ATMegaUSBErrorException : Exception
    {
        public ATMegaUSBErrors ErrorCode;
 
        public ATMegaUSBErrorException(ATMegaUSBErrors ErrorCode)
        {
            this.ErrorCode = ErrorCode;
        }
    }
 
    /// < summary >
    /// Перечисление, определяющее коды ошибок
    /// < /summary >
    public enum ATMegaUSBErrors
    {
        Disconnected
    }

Тогда тот же кусок, но с использованием нового класса:

    try
    {
        usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
                       (int)ATMegaMessageTypes.RQ_IO_WRITE,val, addr, null, 0, 5000);
    }
    catch (Exception e)
    {
        throw new ATMegaUSBErrorException(ATMegaUSBErrors.Disconnected);
    }

Согласитесь, этот вариант уже гораздо более информативен, чем предыдущий. Представим, к примеру, что кроме состояний «Подключено» и «Отключено» устройство может находиться в состоянии «Занято». Каким образом это будет реализовано – в текущем контексте неважно. Рассмотрим только модификацию описанного исключения (а заодно добавим и определение для попытки записи в поле, доступное только для чтения). Точнее, само исключение модифицировать не нужно: достаточно добавить еще два определения в описанное перечисление ATMegaUSBErrors:

    /// < summary >
    /// Перечисление, определяющее коды ошибок
    /// < /summary >
    public enum ATMegaUSBErrors
    {
        Disconnected,
        FieldIsReadOnly,
        Busy
    }

Тогда в момент, когда зафиксирован факт «устройство занято, а пользователь пытается читать/писать в него», нужно вставить строку

    throw new ATMegaUSBErrorException(ATMegaUSBErrors.Busy);

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

    try
    {
        dev.WriteByte(ATMegaAddresses.ADCH, 1);
    }
    catch (ATMegaUSBErrorException MyException)
    {
        switch (MyException.ErrorCode)
        {
        case ATMegaUSBErrors.Busy:
            // Код пользователя, если устройство занято
            ...
            break;
        case ATMegaUSBErrors.Disconnected:
            // Код кользователя, если устройство отключено
            ...
            break;
        case ATMegaUSBErrors.FieldIsReadOnly:
            // Код пользователя, если пытались писать в поле "только для чтения"
            ...
            break;
        }
    }

[Избавляемся от мусора в классе]

Знакомый с классом-оберткой читатель уже, наверное, подметил непривычный вызов функции LibUSB. Смутить его должен был параметр кода операции, записанный следующим образом:

     ATMegaMessageTypes.RQ_IO_WRITE

Имя использованной константы (да и ее значение, в принципе, тоже) осталось неизменным, из чего становится понятно, что передается команда на запись по адресу, указанном в другом параметре. Однако почему-то вместо константы используется член перечисления ATMegaMessageTypes. Откуда и зачем оно появилось? Постараюсь пояснить в этом разделе статьи. 

В традиционном процедурном программировании не было понятия «класс». Был только набор подпрограмм, переменные, константы и точка входа. Поэтому все подобные постоянные значения описывались в виде констант с использованием ключевого слова const. При этом таких констант могло насчитываться большое число, и они оказывались как бы «свалены в кучу» (несмотря на настройки видимости). Поиск нужной константы стал отнимать все больше и больше времени (тем больше, чем крупнее проект). Однако все (или подавляющее их большинство) константы можно было разбить на небольшое число групп (что и делалось, но не средствами языка и даже не средствами среды разработки).

Когда на смену процедурному пришло объектно-ориентированное программирование, привыкшие к старой схеме программисты стали объявлять константы в статической части классов. Это частично решило проблему (константы «разбрелись» по классам), но все равно иногда статические части классов оказывались настолько перегруженными константами, что найти среди них процедуру стало целым приключением. Особенно если не помнить её имя.

При этом в современных языках есть понятие перечисления enum, которое может очень помочь в данной ситуации. Все определения целочисленных констант могут быть вынесены за пределы класса, классифицированы и отсортированы. Более того: если использовать в аргументах функций вместо целочисленных типов перечисления (где это возможно), среда будет еще больше «облегчать» нам поиск нужной константы, автоматически предлагая ввод константы соответствующего типа.

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

    /// < summary >
    /// Перечисление допустимых типов запросов,
    /// отправляемых микроконтроллеру
    /// < /summary >
    public enum ATMegaMessageTypes : byte
    {
        RQ_IO_READ = 0x11,
        RQ_IO_WRITE = 0x12
    }

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

[Новый класс-обертка или модификация старого?]

Оглянувшись на то, что уже сделано, и прикинув то, что еще хотелось бы сделать, я задумался: нужно ли мне продолжать «расковыривать» класс Сергея или просто написать свой с нуля. Тогда же меня посетила мысль, что при таком подходе (если реализовывать все приходящие в голову мысли) класс получится совсем уж непохожим на своего родителя. Поэтому я отбросил все сомнения и открыл новый проект. Поместил туда определения структур для LibUSB (не нашел, чего там поменять), определения внешних функций (они определения и есть) и два уже разработанных решения. Решил также позаимствовать у Кухтецкого код для поиска устройства и определения приватных членов. Все это было скопировано в новый проект в модуль MCUControl.cs.

[Читаем и записываем в порты микроконтроллера]

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

Сергей Кухтеций подошел к решению этой проблемы со стороны описания адресов портов. Он описал каждый порт как поле с настройками get и set, благодаря чему пользователь получил возможность читать из портов и писать в них свои значения. Однако для простого и универсального класса этих полей скопилось многовато: снаружи видно только их и ничего больше (позволю себе немного преувеличения).

Мне показалось более логичным реализовать операции функциями: по методу для каждой. Например, что нужно для того, чтобы прочесть значение? Только адрес. А для чтения – адрес и значение. Это гораздо проще, чем вызов функции с параметрами на две-три строки кода, но все же менее удобно, чем обращение к полю класса. Однако я нашел это более логичным для класса-обертки, чем решение Сергея.

Приведу код функций чтения и записи из нового класса:

    /// < summary >
    /// Читает байт из указанного адреса. Выбрасывает ATMegaUSBErrorException при неудаче
    /// < /summary >
    /// < param name="Address" > Адрес порта < /param >
    /// < returns > Значение порта < /returns >
    public byte ReadByte(ATMegaAddresses Address)
    {
        byte addr = (byte)Address;
 
        try
        {
            usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN,
                           (byte)ATMegaMessageTypes.RQ_IO_READ, 0, addr, buffer, 1, 5000);
        }
        catch (Exception e)
        {
            throw new ATMegaUSBErrorException(ATMegaUSBErrors.Disconnected);
        }
        return buffer[0];
    }
 
    /// < summary >
    /// Пишет байт в указанный порт. Выбрасывает ATMegaUSBErrorException при неудаче
    /// < /summary >
    /// < param name="Address" > Адрес порта < /param >
    /// < param name="val" > значение < /param >
    public void WriteByte(ATMegaAddresses Address, byte val)
    {
        byte addr = (byte)Address;
 
        try
        {
            usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
                           (int)ATMegaMessageTypes.RQ_IO_WRITE,val, addr, null, 0, 5000);
        }
        catch (Exception e)
        {
            throw new ATMegaUSBErrorException(ATMegaUSBErrors.Disconnected);
        }
    }

Пример использования методов «снаружи» был немного выше, не буду останавливаться на этом подробнее.

[Организация событий в классе MCUControl]

Теперь поговорим о самом интересном и полезном – автоматическое отслеживание подключения и отключения USB устройств. Для начала еще немного теории. Если использовать конструктор, как это делалось при работе с классом Сергея Кухтецкого, то всякая работа с событиями класса теряет смысл: после уничтожения объекта ссылки на обработчики событий теряются, и их нужно подключать снова. Ко всему прочему это означает еще и то, что мы не сможем полноценно использовать события: ведь подключать их нужно своевременно, еще до того, как событие наступит.

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

Такой ход дела меня не устроил. Напрашивалось решение, которое и было воплощено: экземпляр класса-обертки инициируется один раз, а переподключение происходит как бы «на лету» вызовом специального метода. Тогда мы можем обрабатывать событие Connected почти корректно и можем абсолютно корректно генерировать событие Disconnected. Почему «почти корректно»? Потому что проблема первого подключения все равно остается.

Есть два решения. Первое: передавать конструктору обработчик события для того, чтобы его можно было подключить до наступления события. Но с этим связаны другие проблемы. Для пояснения приведу кусок кода из проекта, вложенного в статью. Сразу скажу, что описано помимо этих двух событий было еще два (небольшое отступление от повествования): ConnectedChanged (генерируется каждый раз при подключении или отключении) и TryReconnect (генерируется при каждой попытке восстановить связь). Используя первое событие, удобно обрабатывать однотипные действия (например, отображение статуса подключения), а используя второе удобно отслеживать число попыток восстановить соединение. Например, в одном из моих высоконагруженных приложений, где контроллер выполнял третьестепенную роль, начинал тормозить постоянный опрос виртуального порта libusb, и мне приходилось после некоторого числа попыток увеличивать интервал между попытками, чтобы разгрузить систему. Для этого очень хорошо подошло событие TryReconnect.

Итак, возвращаясь к теме повествования, приведу обработчик события ConnectedChanged, который можно передать одной из перегрузок конструктора для первого генерирования события Connected:

    /// < summary >
    /// метод вызовется каждый раз, когда устройство будет отключено или подключено
    /// < /summary >
    /// < param name="sender" > < /param >
    /// < param name="e" > < /param >
    private void dev_ConnectedChanged(object sender, EventArgs e)
    {
        // Приведения типа необходимо, так как при первой инициализации событие
        // вызывается раньше, чем ссылка на dev сформируется в классе формы.
        // Для того, чтобы можно было использовать dev.ConnectionStatus,
        // Пользуйтесь первой предложенной в Form1_Load схемой инициализации объекта dev
        if (((MCUControl)sender).ConnectionStatus)
        {
            StatusBar.Items[0].Text = MCUControl.USB_OK;
            StatusBar.ForeColor = Color.Green;
        }
        else
        {
            StatusBar.Items[0].Text = MCUControl.USB_ERROR;
            StatusBar.ForeColor = Color.Red;
        }
    }

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

    if (dev.ConnectionStatus)

Для того, чтобы это обеспечить, нужно поступить следующим образом: сначала объявить экземпляр класса-обертки без подключения к устройству, затем подключить к нему всех слушателей и только потом подключиться к микроконтроллеру. Соответствующий пример кода [3] можно найти в разделе ссылок.

Как читатель мог заметить, в классе реализовано поле ConnectionStatus, которое позволяет определить, подключено наше устройство USB, или нет. Можно задать логичный вопрос: почему бы не избавиться от обработки исключений и читать/писать только при значении в ней true? Причины две: во-первых, сбой может наступить в момент выполнения операции, и тогда проверка нам не поможет; во-вторых, это не позволяет обрабатывать коды ошибок, как мы это делали выше.

Другое поле, реализованное в классе – AllowReconnect. Если перевести его в значение false, то автоматические попытки переподключиться к устройству не будут предприниматься. При этом такую попытку можно предпринять вручную, вызвав метод ConnectToDev экземпляра класса.

Как организованы события, рассмотрено в [4], поэтому здесь я останавливаться подробнее не буду. При желании пользователь может в пошаговом режиме отследить работу программы, и это даст ему в десятки раз больше, чем любой дополнительный текст здесь. Отмечу только, что перед запуском делегата на выполнение необходимо проверить, что он не пуст, иначе получим ошибку неинициализированного объекта.

Для определения факта отключения микроконтроллера можно использовать факт возникновения ошибки в процессе операции чтения-записи. Если поймали – на реконнект, и все дела. Но не все так просто: к своему стыду, не могу сказать почему, но библиотека lib-usb иногда «подвисает». Когда мы пытаемся читать из порта, откуда недавно читали, и если при этом устройство отключилось, то мы не получим исключения, а функция библиотеки вернет нам предыдущее значение (хотя по факту мы не можем знать, что там – соединение-то потеряно).

В поиске причины этого досадного недоразумения я потратил несколько дней на отладку и после всех мучений пришел к следующему алгоритму проверки соединения:

- выбираем интервал опроса устройства равным N миллисекунд;
- каждые N миллисекунд мы пишем в устройство случайно число и затем тут же считываем его;
- если результат не совпадет (библиотека вернет старое значение) – дисконнект, и все последующие действия читателю уже известны.

Оставался еще один вопрос: куда писать? Какая часть микроконтроллера будет использоваться пользователем реже всего? На мой взгляд, это регистры работы с EEPROM. Я выбрал регистр EEDR, но если пользователь все же будет вынужден его использовать, он может переключить опрос этого регистра на любой другой. Для этого достаточно изменить значение переменной, которая описана в классе MCUControl следующим образом:

    private ATMegaAddresses ReconnectAddress = ATMegaAddresses.EEDR;

Спасибо всем, кто дочитал до этого места. Во второй части статьи собираюсь рассказать о том, какие пути модификации класса-обертки я могу предложить, и о том, какую пользу влечет за собой использование ООП вместо процедурного программирования. В завершение статьи приведу код опроса регистра EEDR, фиксирующий факт отключения (повесил на таймер, объявленный внутри класса):

    /// < summary >
    /// Производим запись случайного числа в EEDR, затем читаем
    /// и проверяем результат. Если не совпало - ошибка USB.
    /// Если исключение - ошибка USB
    /// < /summary >
    /// < param name="sender" > < /param >
    /// < param name="e" > < /param >
    private void ReconnectTimer_Tick(object sender, EventArgs e)
    {
        Random r = new Random();
        byte temp = (byte)r.Next(0, 255);
 
        try
        {
            byte buf = this.ReadByte(ReconnectAddress);  // сохраним старое значение
            this.WriteByte(ReconnectAddress, temp);      // запись
            if (this.ReadByte(ReconnectAddress) != temp) // проверка
            {
                Reconnect(false);                        // ошибка
                return;
            }
            this.WriteByte(ReconnectAddress, buf);       // норма, вернем старое значение на место
        }
        catch
        {
            Reconnect(false);                            // ошибка
        }
    }

MCUControl-easy-example-application

[Для чего использовался класс-обертка C# совместно с AVR-USB-MEGA16]

При проведении эргономических экспертиз рабочих мест (чего угодно: от человека за компьютером/станком до пилотов и космонавтов) необходимо оценивать области обзорности и досягаемости. Например, если Вы едете в автомобиле, то часть наружной обстановки (улица, дорога) Вам видна сквозь стекло, а часть заслоняется переборками, арматурой и т.п. Однако это очень важный вопрос человеческого фактора: согласитесь, что вероятность попадания в аварию выше, если Вам почти ничего не видно сквозь стекло. Отсюда встает вопрос: как оценить "обзорность"? То же самое с досягаемостью: а можете ли Вы дотянуться до того или иного органа управления? Если да, то насколько это сложно?

Для этого производятся замеры в полярной системе координат: снимаются "контрольные" точки (например, по три на каждый изгиб и по одной между каждыми двумя такими) и по ним строится диаграмма (см. скриншот программы во вложении). Затем эта диаграмма обрабатывается в соответствии с тонной ГОСТов, ОСТов и внутренних стандартов, уникальных для каждой фирмы.

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

Исключение - некоторые государственные организации и предприятия, которые получили кучу государственных аккредитаций и частных заказов. Они смогли позволить себе начать подобные разработки (хотя начальникам отделов все равно приходится доплачивать из своего кармана). В частности, для компании N я разрабатываю вот эту самую головку. Однако в ТЗ на нее не описано никаких требований к доступности: это должна быть "коробочка", которая подключается к их головке. Это такая большая тяжелая металлическая конструкция с двумя дисками, выполняющими роль транспортиров (на них нанесены деления, с которых вручную переписывают значения углов). К ней прикрутили два потенциометра и пару кнопок, которые подключены к моей AVR-USB-MEGA16 и по USB передаются на хост, где уже и обрабатываются. Вот, собственно, и все, что я для них сделал. Главная фишка, конечно же, не в электронике, а в ПО хоста, реализующее все их требования по оформлению отчетности и проведению экспертиз.

Класс-обертка использовалась для считывания координат углового направления лазера и управления установкой. На фотографиях ниже фото имитатора, который я использовал для отладки приложения. Это просто коробочка с двумя потенциометрами и тремя кнопочками. Имеется также рабочий вариант, который я использую для презентаций и построения реальных диаграмм. Она собран из лазера, старого джойстика и вакуумной присоски для крепления. Нашел еще коробочку от старой электроники, расковырял и выкинул оттуда все потроха, запихнул туда макетную плату, развел выводы головки на COM-коннектор с обеих сторон. Вот, в принципе, и все. Здесь стоит выразить благодарность сотрудникам эргономического отдела завода M, которые помогли с монтажом: мой график (учеба+работа+диплом+разработки+статьи) не оставляли на это времени.

simulator-IMG 3271 simulator-IMG 3272 laser-control-IMG 3276
laser-control-IMG 3277 laser-control-IMG 3279 laser-control-IMG 3281

Сейчас система прошла отладочное тестирование, и я со своей командой - дизайнер, конструктор и консультант-практик (эргономист) - готовим РКД для запуска серийного производства (ибо вариант, который остался на заводе M меня совсем не устраивает). Устройство будет дешевым и эффективным.

[Как компилировать проекты с использованием класса-обертки MCUcontrol]

В макетную плату AVR-USB-MEGA16 должна быть прошита оригинальная прошивка Сергея Кухтецкого, она не претерпела изменений. Прошивку ATmega32A и кварц 12 МГц можете взять из архива [3], см. папку MCUControl.example\HEX, или на другие частоты кварцев см. папку phmeter-fw\bin.

Проект [3], как и другие проекты C#, использующие класс-обертку с поддержкой событий, следует компилировать в среде Microsoft Visual Studio 2010. Распакуйте архив [3], зайдите в папку MCUcontrol.example и откройте файл MCUControl.sln. Далее компилируйте (меню Построение -> Построить решение) и запускайте (меню Отладка -> Начать отладку) как обычно.

Внимание: в проекте с классом-оберткой MCUcontrol должна использоваться версия .NET Framework 3.5 Client Profile (настраивается в свойствах проекта). Для тест-программы [3] все уже настроено, но если будете делать собственный проект, то этой настройке в среде Visual Studio 2010 нужно уделить внимание. Если оставить версию по умолчанию (.Net 4.0), то класс обертка корректно работать не будет. Связана настройка с тем, что в версии .Net 4.0 изменились дефолтные значения атрибутов при импорте внешних dll (и, возможно, что-нибудь еще). На практике неправильная работа выражается в том, что при вызове функций libusb начинают появляться ошибки (хотя бы то же самый PInvokeStackImbalance, который легко фиксится указанием соглашения о вызовах равным cpp).

Автор статьи Даниил Захаров (harikomp@list.ru), Кафедра ЭИИС ("Эргономика и информационно-измерительные системы", eiis@mail.ru, +7 (495) 915-07-28), МАТИ-РГТУ им. К. Э. Циолковского, г. Москва.

[Ссылки]

1. AVR-USB-MEGA16: быстрая разработка USB приложений на C# при помощи класса-обертки ATMega16.
2. Разработка устройства USB - как начать работу с библиотеками V-USB и libusb.
3. 130331MCUcontrol-public.zip - проект C#: тест-программа на основе класса обертки для управления светодиодом на макетной плате AVR-USB-MEGA16, документация, исходный код прошивки микроконтроллера, готовая прошивка. В проекте используется новый класс MCUcontrol, поддерживающий отслеживание событий отключения и подключения макетной платы через USB.
4. Идеология C#: события и делегаты.

 

Добавить комментарий


Защитный код
Обновить

Top of Page