AVR-USB-MEGA16: быстрая разработка USB приложений на C# при помощи класса-обертки ATMega16 Печать
Добавил(а) Сергей Кухтецкий   

При разработке систем на базе микроконтроллеров (МК), подключенных каналом связи к персональному компьютеру (ПК), разработчик сталкивается с проблемой одновременной (параллельной) разработки как ПО хоста, так и firmware для микроконтроллера. Это утомительный, итерационный процесс, связанный с переключением внимания на различные среды разработки.

Поскольку при работе с реальными сигналами и устройствами средства эмуляции не всегда годятся, возникает еще одно звено в этом процессе – запись firmware в память МК (т. е. «прошивка»). Таким образом, разработчик постоянно находится в процессе переключения между различными видами деятельности и приложениями. Например, внес изменения в программу firmware в текстовом редакторе типа блокнот. Затем скомпилировал, набрав «make hex» в командной строке. Поправил, если что не так. Далее - перешел к программатору и «прошил» МК новоиспеченным hex-файлом. Потом подключил МК к каналу связи (USB в нашем случае) и пошел в средство разработки хоста (например, Visual Studio). Внес изменения в программу хоста, откомпилировал, запустил и понял, что в программе firmware все еще что-то не так. И опять пошел в блокнот, редактировать firmware, попутно испытав стресс, перейдя из Visual Studio в блокнот с командной строкой. В общем, что-то с этим нужно было делать…

Одна из идей, которая помогла существенно облегчить этот процесс, заключается в следующем. Нужно сделать одну универсальную прошивку и класс-обертку для функций этой прошивки на C#, которые позволили бы решать все задачи, связанные со сбором данных, управления внешними устройствами и ресурсами микроконтроллера непосредственно из программы на C# (не выходя из Visual Studio). Т. е. один раз «прошил» МК, и все действия далее - только из хоста. Потом, если, конечно это нужно, часть отлаженного программного обеспечения хоста можно будет перенести в firmware для ускорения процессов или «разгрузки» канала связи или ПК.

Идея заманчивая, но, конечно, в общем случае такая задача вряд ли разрешима. Главная проблема такого тотального «интерпретатора» - катастрофическая потеря производительности и перегруженность канала связи. Кроме этого, часть процессов МК в принципе нельзя (или очень трудно) «перенести» на хост. Например, - обработка прерываний, высокоприоритетные, критические по времени выполнения операции и т.п.

Тем не менее оказалось, что для достаточно широкого круга задач такую прошивку и соответствующий класс-обертку можно сделать. Описанию этого решения и посвящена данная статья.

Для работы нам понадобятся следующие программы и «железо»:

1. Средства разработки хоста - свободно доступная Visual Studio 2008 Express Edition, которую можно скачать с сайта Microsoft.
2. Для работы с USB на стороне хоста используется свободная библиотека LibUSB [1].
3. Макетная плата AVR-USB-MEGA16 [6].

Если не хотите разрабатывать firmware и сами прошивать микроконтроллер этой платы, Вы можете при покупке платы попросить записать её нужной прошивкой. В противном случае понадобится средство разработки (если хотите сами прошивку скомпилировать). Чтобы записать прошивку-firmware в плату, программатор не нужен, так как макетная плата AVR-USB-MEGA16 имеет встроенный USB-загрузчик (bootloader USBasp).

4. Для самостоятельной компиляции прошивок (если это Вам понадобится) нужен свободно доступный пакет WinAVR [1].

Исходники и скомпилированные бинарники firmware и ПО хоста, описанные в статье, можно скачать по ссылке [2].

[Firmware]

Круг задач, для которых разработана прошивка и класс-обертка, так или иначе связан с управлением измерительным или исполнительным оборудованием в системах автоматизации, вводом и обработкой полученной информации. Анализ большого количества примеров на эту тему показывает, что если процессы не очень быстрые (т. е. не требуется немедленной реакции микроконтроллера по аппаратным или программным прерываниям), то всю работу с внешними устройствами можно свести к установке или чтению значений портов и регистров ввода/вывода, управляющих периферией микроконтроллера. Если просмотреть файл iom16.h (например, в WinAVR\avr\include\avr), то становится ясно - все эти операции реализованы через макросы _SFR_IO8(адрес) для байтов или _SFR_IO16(адрес) для слов, причем последний макрос обычно дублируется парой байтовых. Например, регистр данных АЦП:

...
#define ADCW _SFR_IO16(0x04)
#define ADCL _SFR_IO8(0x04)
#define ADCH _SFR_IO8(0x05)
...

или порт D (для вывода данных):

...
#define PORTD _SFR_IO8(0x12)
...

Поэтому при разработке firmware для микроконтроллера ATMega16 в методе usbFunctionSetup() мы можем реализовать всего два (!) запроса, чтобы получить возможность работать с любым портом или регистром ввода/вывода. Наше firmware должно уметь по команде, пришедшей по USB, записать байт по указанному адресу регистров ввода/вывода микроконтроллера и уметь считать байт по указанному адресу.

Давайте так и поступим. Возьмем самый первый и простой пример из пакета AVR-USB – «custom class» и модернизируем его следующим образом (см. файл Main.c).

1. Определим две константы в начале программы в качестве команд:

... 
typedef unsigned char byte; 
#define RQ_IO_READ 0x11 
#define RQ_IO_WRITE 0x12 
...

2. Выкинем из метода usbFunctionSetup() все «потроха» и напишем туда следующее:

usbMsgLen_t usbFunctionSetup(uchar data[8]) 
{
   usbRequest_t *rq = (void *)data;
   byte addr = rq->wIndex.bytes[0]; // Адрес порта или регистра 
   byte val = rq->wValue.bytes[0];  // Значение для записи 
   switch(rq->bRequest)
   {//---------------------------------------------------------- 
    // Работа с портами и регистрами ввода/вывода. 
    //---------------------------------------------------------- 
   case RQ_IO_READ:                 // Чтение байта с порта ввода/вывода 
      dataBuffer[0] = _SFR_IO8(addr); 
      usbMsgPtr = dataBuffer; 
      return 1; 
   case RQ_IO_WRITE:                // Запись байта в порт ввода/вывода 
      _SFR_IO8(addr) = val; 
      return 0; 
   }
   // default для не реализованных запросов: на хост обратно
   // данные не возвращаются
   return 0;
}

3. И это – все. Скомпилируйте и прошейте микроконтроллер полученной прошивкой. Ни компилятор C для AVR, ни программатор нам больше не понадобятся. Действительно, теперь мы можем прямо из хоста делать много полезного. В частности:

• «Дергать» любой доступный разряд любого порта (PORTX), управлять направлением потока данных (DDRX) или читать с портов (PINX).
• Работать с любыми регистрами ввода/вывода. Например, проинициализировать, запустить и считать данные с АЦП или настроить и запустить аппаратный ШИМ на таймере TIMER1 и так далее (можно пойти почти по всей периферии микроконтроллера, там где не является обязательным условием использование прерываний).

Для этого в приложении хоста нам нужно всего лишь вызывать функцию usb_control_msg() библиотеки libusb с нужными параметрами. В параметрах этой функции мы должны указать тип запроса (собственно, направление передачи данных - RQ_IO_READ или RQ_IO_WRITE), адрес регистра или порта (addr) и еще значение (val), если мы пишем в порт или регистр. При чтении данные передаются в хост через первый байт буфера (тоже параметр функции usb_control_msg()), поскольку в данной реализации мы работаем с 16-разрядными регистрами только побайтно. Другие элементы firmware (циклы, ветвления, вычисления и т. п.) вполне можно делать и на стороне ПО хоста, тем более что вычислительные ресурсы хоста существенно превышают ресурсы микроконтроллера.

Однако у такого подхода есть и серьезные ограничения. Он не будет работать, если нам нужна точная синхронизация процессов или их жесткая временнАя привязка к каким-нибудь внешним или внутренним событиям микроконтроллера. Если, например, нам необходимо немедленно реагировать по прерываниям или просто выполнять команды очень быстро (в этом случае будет сильно тормозить реализация USB в AVR-USB-MEGA16), то придется расширять firmware соответствующими модулями и обработчиками. Но, как показал опыт, для многих задач этого может и не потребоваться.

Прошивку вместе с исходниками вы можете взять из [2] (см. папку firmware\phmeter-fw в общем архиве).

Итак, половина дела (firmware) сделано. Мы превратили микроконтроллер платы AVR-USB-MEGA16 в интеллектуальное устройство ввода/вывода с богатой периферией, которое настраивается и управляется хостом по USB. Перейдем теперь к программному обеспечению хоста.

[ПО хоста - класс-обертка ATMega16]

При наличии достаточных вычислительных ресурсов объектно-ориентированный подход существенно «просветляет» мозги разработчика, освобождая его от рутины и давая возможность оперировать более крупными сущностями на более высоком уровне абстрагирования. Вычислительных ресурсов у нас в хосте достаточно. Поэтому для работы с микроконтроллером на языках .NET, будем использовать класс-обертку на C# - ATMega16. Этот класс я сконструировал так, что в программе хоста микроконтроллер выглядит как некое логическое устройство (объект класса ATMega16). Свойства этого устройства - порты и регистры ввода/вывода микроконтроллера. Кроме того, в этом классе инкапсулированы вспомогательные переменные, константы и вызовы, необходимые для работы с USB при помощи библиотеки libusb. Т. е. про USB пользователь (точнее - разработчик) ничего не знает (канал абсолютно прозрачен). Таким образом, в программе хоста на C# или Visual Basic мы можем создать объект класса ATMega16, передав конструктору параметры VID и PID. После создания объекта (пусть его зовут - dev), мы можем изменять его свойства (например, dev.PORTB |= 0x01) или читать значения этих свойств (например, bool flag = (dev.PORTC & 0x08) != 0). В реальности при этих присваиваниях будет производиться передача команды по USB, дешифрация её микроконтроллером, запись в порт или чтение из него и передача данных по USB обратно в ПО хоста, но в программе это выглядит просто как работа со свойствами объекта dev.

Исходный текст базового варианта класса-обертки можно взять в архиве [2] (C#\Thermo\ATMega16.cs). Рассмотрим структуру класса чуть подробнее.

Пространство имен, в котором существует класс – AvrUsbDevice. Сам класс объявлен с ключевым словом unsafe, потому что в нем используются структуры и методы библиотеки libusb-win32, содержащей небезопасный код. В связи с этим при компиляции приложений, содержащих класс-обертку ATMega16, необходимо разрешить компилятору использовать небезопасный код. Для этого в свойствах проекта на закладке Build нужно включить флажок Allow unsafe code.

Класс ATMega16 состоит из двух больших блоков. В первом – все что связано с библиотекой libusb-win32 и USB. Он состоит из трех разделов – определения констант, определения на C# структур данных библиотеки libusb, импорт и объявление нужных функций этой библиотеки.

Второй блок содержит все необходимое для работы с микроконтроллером. В первом разделе – адреса портов, регистров ввода/вывода, имена портов и битов регистров ATMega16. В конструктор класса передаются два параметра. Это - VID и PID USB-устройства. Далее в конструкторе происходит инициализация USB и поиск устройства. Успешность инициализации USB можно проверить вызовом метода IsOpen().

Следующие разделы содержат определения свойств класса, связанных с портами и регистрами периферии микроконтроллера (в данной версии класса-обертки - только АЦП и 16-разрядный таймер). Вы можете самостоятельно дополнить класс своими необходимыми свойствами (например – работой с памятью EEPROM, FLASH и RAM, firmware при этом нужно соответственно доработать – добавить команды по аналогии с RQ_IO_READ и RQ_IO_WRITE). В методах set/get этих свойств как раз используются вызовы функции usb_control_msg() с соответствующими параметрами.

При разработке приложения необходимо включить класс-обертку ATMega16 в разрабатываемый проект и сделать доступным пространство имен AvrUsbDevice. Кроме этого, должна быть установлена библиотека libusb-win32 (файл libusb0.dll должен быть в папке исполняемого файла приложения или в другой папке, указанной в переменной окружения PATH).

Ниже представлена пошаговая инструкция по созданию простого приложения с использованием класса ATMega16.

[Разработка простейшего приложения – мигаем светодиодом по USB]

1. Запускаем Visual C# 2008 Express Edition.

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

2.1. Щелкаем меню File/New Project

2.2. На панели шаблонов New Project выбираем «Windows Forms Application» и снизу панели вводим имя проекта «LedTest» и нажимаем «Ok».

2.3. Через некоторое время на экране появится пустая форма.

2.4. Сохраним весь проект, выбрав пункт меню File/Save All. На панели диалога сохранения файла указываем папку, в которой мы хотим сохранить проект. Жмем Ok и в этой папке у нас появится новая папка с названием LedTest, в которой будут сохранены все файлы нашего проекта. Файл LedTest.sln это, собственно, файл проекта, а все наши программистские действия в данной простой программе будут находиться в файле Form1.cs.

2.5. Скопируем в эту папку файл с классом-оберткой ATMega16.cs и подключим этот объект к нашему проекту. Для этого щелкнем пункт меню Project/Add Existing Item… В открывшемся диалоге выберем файл ATMega16.cs и нажмем кнопку Add. Можно видеть, что в окне Solution Explorer появился новый объект ATMega16.cs.

2.6. Выполним настройку проекта – разрешим использование небезопасного кода. Это необходимо, поскольку класс ATMega использует небезопасный код, что и указано ключевым словом unsafe в объявлении класса (см. файл ATMega16.cs). Для этого откроем панель свойств проекта (меню Project/LedTest Properties), перейдем на закладку Build и поставим флажок в свойстве «Allow unsafe code».

3. Теперь приступим к содержательной части задачи. В окне Solution Explorer щелкните правой кнопкой мыши на объекте Form1.cs. В контекстном меню выберите пункт View Code. В главном окне вы увидите текст кода нашей главной формы.

3.1. Добавим пространство имен AvrUsbDevice, в котором находится класс-обертка ATMega16. Для этого после строки

using System.Windows.Forms;

добавьте строку

using AvrUsbDevice;

3.2. Сразу же после заголовка класса Form1 объявим переменные:

bool ledOn = false; // Если ledOn равен true, то светодиод 
                    // на плате включен 
ushort vid = 0x16C0, pid = 0x05DC; // VID и PID 
ATMega16 dev;       // Объявляем объект типа ATMega16

3.3. Создание объекта dev лучше всего выполнить при загрузке главной формы приложения. При этом генерируется событие Load. В обработчик этого события (метод Form1_Load()) мы и вставим создание объекта dev. Для этого перейдем на закладку дизайнера формы в главном окне (Form1.cs [Design]) и сделаем двойной щелчок по форме нашего будущего приложения. В коде главной формы появится шаблон:

private void Form1_Load(object sender, EventArgs e) 
{
}

Внутрь этого шаблона (в фигурные скобки функции Form1_Load) мы вставим следующий код:

dev = new ATMega16(vid, pid); // Создаем объект dev класса ATMega16
if (!dev.IsOpen())
{
   // Если есть проблемы с USB – сообщим и выйдем
   MessageBox.Show(String.Format(
         "Невозможно найти устройство vid = 0x{0:X}, pid = 0x{1:X}", vid, pid),
      "Ошибка USB",
      MessageBoxButtons.OK, MessageBoxIcon.Error); 
   Close(); 
} 
else 
{
   // Если все хорошо, настроим микроконтроллер по USB 
   dev.DDRB |= 0x01;  // Разряд 0 порта B - на вывод – там светодиод 
   dev.PORTB &= 0xFE; // Выключим светодиод на плате 
}

3.4. Теперь разместим на форме кнопку, при помощи которой мы будем включать и выключать светодиод на плате. Для этого опять перейдем в дизайнер формы и из Toolbox-а перетащим на форму кнопку.

3.5. Создадим обработчик события нажатия кнопки. Для этого сделаем двойной щелчок по изображению кнопки на форме. В коде формы появится шаблон

private void button1_Click(object sender, EventArgs e) 
{
}

3.6. Внутрь этого шаблона вставим код:

ledOn = !ledOn;
if (ledOn) 
   dev.PORTB |= 0x01; // Включим светодиод на плате, PB0 = 1 
else 
   dev.PORTB &= 0xFE; // Выключим светодиод на плате, PB0 = 0

4. Теперь проверим работу нашей программы и прошивки.

4.1. Подключим прошитую плату AVR-USB-MEGA16 к разъему USB персонального компьютера. На хосте-компьютере должна быть предварительно установлена библиотека libusb.

4.2. Компилируем и запускаем на выполнение наше приложение (ну просто нажимаем F5). Если все правильно воткнуто, прошито, собрано и написано, то на экране появится главное окно приложения с одной кнопкой. Нажимая на эту кнопку, мы сможем включать и выключать светодиод, расположенный на плате AVR-USB-MEGA16 (см. Рис.1).
classCsharp-mega16-01.jpg
Рис.1. Вид работающего приложения и горящего светодиода

Полностью готовый проект можно найти в архиве C#\ThermoSimple [2]. Там также находятся и более сложные примеры использования расширенного класса-обертки ATMega16 и расширенного firmware (см. файл readme.txt [2] и статьи [4, 5]).

Байты перемычек (fuse bits) ATmega16 должны быть 0xFF (low) и 0x09 (high). 

S.V.Kukhtetskiy, 2009
Кухтецкий Сергей Владимирович,
лаборатория проблем материаловедения,
Институт химии и химической технологии СО РАН, Красноярск
E-mail: ku@icct.ru

[Решение возможных проблем]

Здесь описаны некоторые типичные проблемы, которые возникают при использовании прошивки Сергея Кухтецкого. Решение других проблем см. в статье AVR-USB-MEGA16, V-USB, FAQ: переписка по вопросам программирования.

1. Записал в платку прошивку Сергея Кухтецкого с помощью загрузчика USB и программы Khazama, но платка не определяется как устройство USB. Если снова установить перемычку для активации загрузчика USB, то платка определяется, и загорается светодиод. В чем проблема? Ответ: платка у Вас исправна, но Вы записали в неё не ту прошивку. Например, прошивка соответствует другой частоте кварца, не той что используется на Вашей макетной плате AVR-USB-MEGA16. Прошейте HEX-файл прошивки Сергея Кухтецкого, рассчитанный на нужную частоту кварца, который стоит у Вас. Частоту кварца можно прочитать на его корпусе, например если на корпусе написано 12.000, то кварц на 12 МГц.

2. При подключении платки к компьютеру (в платку записана прошивка Сергея Кухтецкого) система Windows запрашивает драйвер. Что это за драйвер, где его брать? Ответ: это драйвер библиотеки LibUSB, скачайте его по ссылке [7].

3. На моей системе Windows драйвер LibUSB не устанавливается, и о причине система не сообщает. Как быть? Ответ: такое нередко бывает на урезанных версиях Windows. Причина может быть в том, что не отключена система проверки цифровой подписи драйвера, либо пакет драйвера неполон (например, если Вы скачали драйвер по ссылке из статьи на сайте vanoid.ru, то там не хватает некоторых файлов, требуемых для 64-битных версий Windows). Поэтому, во-первых, перезагрузите систему, и при загрузке отключите проверку цифровой подписи драйвера (нажимайте при старте F8, и на черном экране выберите соответствующую нижнюю строчку загрузочного меню). Во-вторых, устанавливайте полную версию драйвера LibUSB (скачайте его по ссылке [7]).

4. Скомпилированное ПО хоста на C# не запускается с ошибкой. Что делать? Ответ: возможно не установлена библиотека .NET 3.5 от Microsoft. В этом случае могут выскакивать разные ошибки (наподобие The application failed to initialize properly 0xc0000135), но программа в любом случае не запустится. Чтобы решить проблему, скачайте и установите эту библиотеку (строка для поиска Microsoft .NET Framework 3.5 site:microsoft.com). Инсталлятор для библиотеки можете скачать по ссылке [7] (инсталлятор требует подключения к интернет, так как он закачивает недостающие модули).

[Ссылки]

1. Разработка устройства USB - как начать работу с библиотеками AVR USB и libusb.
2. 101022V-USB-Csharp-libusb.zip - здесь я собрал в одном архиве все исходники программ, описанные в статье (ПО хоста и firmware), а также скомпилировал прошивки на все варианты частот кварцев для микроконтроллеров ATmega16 и ATmega32.
3. Класс-обертка для AVR-USB-MEGA16 с поддержкой событий.
4. AVR-USB-MEGA16: измеряем и контролируем температуру.
5. AVR-USB-MEGA16: новая жизнь старого «Милихрома».
6. Макетная плата AVR-USB-MEGA16.
7140808LibUSB-driver.zip - архив с драйвером библиотеки LibUSB, он нужен для корректной работы прошивки Сергея Кухтецкого совместно с операционными системами Windows. Драйвер подходит для всех версий операционных систем Windows XP (32 bit и 64 bit), Windows 7 (32 bit и 64 bit), Windows 8 (32 bit и 64 bit). В этом же архиве найдете инсталлятор для библиотеки .NET 3.5 (требуется для приложений ПО хоста, написанных на Visual Studio C# 2010).