Программирование AVR: работа с USB V-USB без прерываний Thu, January 19 2017  

Поделиться

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

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

V-USB без прерываний Печать
Добавил(а) microsin   

Начиная с версии V2.0 появилась модификация Micronucleus [2] (это USB-загрузчик для ATtiny85, bootloader) без задействования прерываний на основе V-USB (программная реализация низкоуровневого протокола USB). Это дает большие преимущества загрузчику, потому что теперь не надо модифицировать вектор прерывания в таблице векторов для программы пользователя (ранее это было необходимо, потому что в микроконтроллерах серии ATtinyXX нет аппаратной поддержки секции загрузчика). Большой сюрприз - неожиданно скорость передачи V-USB увеличилась, что может также пригодиться и в других приложениях. Здесь я приведу попытку в общих чертах описать основную работу, которая привела к этому достижению (перевод статьи [1], опубликованной 2 марта 2014 года).

Предыдущие версии Micronucleus (и также Trinket bootloader) использовали изощренный механизм исправления вектора прерывания в программе пользователя. Поскольку этот метод работал очень хорошо, бутлоадер был еще несколько усложнен добавлением нескольких циклов в задержки прерывания, что перенесло риск порчи программы пользователя на очень редкие случаи. Удаление этого бремени позволяет значительно сократить код и повысить устойчивость.

Изначально V-USB использует вывод GPIO с настроенным прерыванием по изменению логического уровня (pin change interrupt) на сигнале D+, чтобы детектировать входящие передачи USB. Обработчик прерывания принимает, декодирует и подтверждает пакеты данных USB и сохраняет их в буфер приема (RX buffer) для его парсинга в основной программе (из главного цикла main). В случае, когда хост запрашивает исходящие данные, обработчик прерывания соответственно ответит данными, если они найдены в буфере передачи (TX buffer). Парсинг пакетов и создание исходящих пакетов возлагается на основную программу, путем периодических вызовов usbpoll() из главного цикла main.

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

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

do
{ 
   if (USB_INTR_PENDING & (1 << USB_INTR_PENDING_BIT))
   {
      USB_INTR_VECTOR(); // очищает INT_PENDING (см. se0: в asmcommon.inc)
      break;
   }
} while(1)

Все выглядит довольно просто, и к нашему удивлению, это даже иногда работало не некоторых компьютерах. Проблема состоит в том, что usbpoll() нужно вызвать в определенный момент, так как иначе приходящие передачи USB не обрабатываются, и с данными сделать ничего было нельзя. Одиночный вызов usbpoll() занимает около 45-90 мкс на тактовой частоте 16.5 МГц ATtiny. Поскольку мы не опрашиваем прерывания во время вызова функции, то не можем принять приходящие данные. Первый шаг исправить это состоит в определении периода тайм-аута, и вызывать usbpoll() только тогда, когда не было приходящих данных в некотором промежутке времени. Это улучшило работоспособность до такой степени, что стало возможно загружать и запускать программы через загрузчик micronucleus. Однако снова на некоторых компьютерах ничего не работало, и общее функционирование было довольно ненадежным. Стало ясно, что нужно применить некий более сложный алгоритм определения момента, когда можно блокировать CPU, и когда этого нужно избегать.

Я уже собрался разочароваться в идее отказа от использования прерывания. Но в этот момент заметил, что новый бета-релиз 1.1.18 программного обеспечения Saleae logic поставляется с интерпретатором протокола USB1.1. Это предоставило инструмент для понимания того, что на самом деле происходит.

V-USB-interrupt-free-detail int

Чтобы разобраться в основной проблеме, я сначала посмотрел на трафик USB, который обрабатывается стандартной библиотекой V-USB. Кроме сигналов трафика USB (D+, D-), я подключил просмотр двух сигналов GPIO, сконфигурированных для отображения информации о состоянии программы (INT active, TX active). Канал 2 показывает лог. 1, когда активно прерывание, канал 3 показывает лог. 1, когда микроконтроллер активно посылает данные. На картинке выше показана передача пакета SETUP, который запрашивает данные от клиента USB (этим клиентом является микроконтроллер AVR с работающей библиотекой V-USB, в терминах USB этот клиент также называется "функция USB"). Как Вы можете увидеть, прерывание корректно вызывается в начале передачи, и передача подтверждается пакетом ACK функции USB. Поскольку хост хочет принять данные от функции USB, он посылает пакет IN сразу после пакета настройки (пакет SETUP), при этом проходит всего несколько микросекунд. Можно также увидеть, что V-USB делает выход из прерывания в течение очень малого промежутка времени, которого недостаточно для обработки входящих данных и подготовки исходящих данных. Однако стандарт USB 1.1 требует от функции USB ответа на запрос в течение 7.5 битов времени, что составляет 5 мкс. Иначе хост регистрирует таймаут. V-USB обрабатывает эту ситуацию выдачей NACK, чем сигнализирует хосту, что функция USB пока не готова.

V-USB-interrupt-free-all int

Проблема еще в том, что NACK занимает все время CPU, что также не улучшает ситуацию. Как Вы можете увидеть на картинке выше, это может продолжаться довольно долго. Хост перестанет отправлять пакеты IN по окончании фрейма, чтобы избежать коллизии 1 мс импульса поддержки работы (keep alive pulse). Только после окончания этого хост оставляет для V-USB достаточно времени для обработки данных RX и подготовки данных буфера TX. Такие потери процессорного времени чрезвычайно вредны, поскольку менее 10% трафика шины USB реально переносят данные, и почти 90% времени CPU впустую тратятся на "успокоение" хоста. В худшем случае может быть обработана только 1 допустимая передача на фрейм (1 мс). Поскольку низкоскоростные пакеты данных USB могут переносить максимум 8 байт, это ограничивает теоретическую пропускную способность 8000 байт/сек, и она оказывается на практике значительно меньше из-за дополнительных вычислительных затрат на обработку протокола низкого уровня. Хотя это чрезвычайно уродливо, но именно так и работает на самом деле V-USB.

Все еще более усложняется с переходом на версию без прерываний. Как в этой ситуации можно узнать, что вызов usbpoll() сейчас безопасен? Как уже упоминалось, частичное решение проблемы было в том, чтобы обрабатывать данные только тогда, когда шина неактивна в течение 20..30 мкс.

V-USB-interrupt-free-resync

Рисунок выше показывает трафик шины от версии без прерываний, которая использует детектирование неактивности шины. Канал 2 в состоянии лог. 1, когда активна подпрограмма трансивера USB (выполняет все то, что ранее делал обработчик прерывания). Канал 3 в состоянии лог. 1, когда активен вызов usbpoll(). Лог стартует по окончании успешно принятого пакета. После того, как на шине нет активности около 20 мкс, вызывается usbpoll(). К сожалению, промежуток времени в 42 недостаточен для завершения usbpoll(), и она не может выполнить свою работу до прихода следующего пакета. Это приводит к тому, что трансивер пытается засинхронизироваться с пакетом. Он делает выход, поскольку не может детектировать маску синхронизации, которая предположительно должна быть в начале пакета. Хост детектирует таймаут, и посылает пакет второй раз. На этот раз трансивер успешно обрабатывает пересинхронизацию на следующем пакете.

V-USB-interrupt-free-resynch failed

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

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

Довольно легко определить пропуск пакета данных, потому что тогда флаг прерывания окажется установленным после выполнения usbpoll(). Но как детектировать момент, что мы достигли конца пакета? Моей первой попыткой было ожидать следующего события SE0, которое сигнализирует об окончании пакета. К сожалению это не получилось, потому что некоторые передачи приходили в нескольких пакетах (например OUT или SETUP, за которыми следует DATA). В этом случае трансивер неправильно подтвердил бы части данных без корректной их обработки.

Решением намного лучше стало ожидание неактивного состояния шины (idle bus). И снова, интервалы времени здесь очень критичные. Оказалось, что хороший компромисс ждать 10..10.5 мкс, поскольку это время даст функции возможность отправить ответ ACK, правильно интерпретируя последний пакет.

V-USB-interrupt-free-forced resync

На картинке выше показан трафик шины для V-USB без прерываний, построенной на последней описанной идее. Вместо того, чтобы ожидать свободного времени шины, просто делается вызов usbpoll() после каждого успешно принятого пакета. Если была детектирована коллизия, то происходит ожидание неактивности шины (idle bus), чтобы сделать пересинхронизацию. С использованием этого приема шина может быть остановлена на время до 90 мкс, поскольку минимальная длина пакета 45 мкс, и до 2 пакетов может быть отброшено по таймауту.

Этот хак привел к очень интересному эффекту, что передачи стали работать намного быстрее в сравнении со стандартной версией V-USB, которая основана на прерывании, потому что каждый пакет данных надо передавать заново только 1..2 раза вместо того, чтобы >10 раз отправлять NACK, как это делалось ранее.

Вот внутренний цикл реализации V-USB без прерываний:

do
{
   // Ожидание пакета данных и вызов трансивера
   do 
   {
      if (USB_INTR_PENDING & (1 << USB_INTR_PENDING_BIT))
      {
         USB_INTR_VECTOR();  // очищает INT_PENDING (см. se0: в asmcommon.inc)
         break;
      }
   } while(1);
   // Парсинг пакета данных и составление ответа
   usbpoll();
 
   // Проверка - был ли пропущен пакет данных. Если да, то ожидание idle bus.
   if (USB_INTR_PENDING & (1 << USB_INTR_PENDING_BIT))  
   {        
      uint8_t ctr;
 
      // Цикл на 5 тактов
      asm volatile(      
      " ldi %0,%1 \n\t"        
      "loop%=: sbic %2,%3 \n\t"        
      " ldi %0,%1 \n\t"
      " subi %0,1 \n\t"        
      " brne loop%= \n\t"   
      : "=&d" (ctr)
      :  "M" ((uint8_t)(10.0f*(F_CPU/1.0e6f)/5.0f+0.5)), "I" (_SFR_IO_ADDR(USBIN)), "M" (USB_CFG_DPLUS_BIT)
      );       
      USB_INTR_PENDING &= ~(1 << USB_INTR_PENDING_BIT);
   }
} while(1);

Мы на этом остановимся? Нет, есть еще несколько не очень важных вещей, которым надо уделить внимание:

• Обработка сброса по шине (bus reset) больше не выполняется в usbpoll(), поскольку эта подпрограмма вызывается только когда принят пакет. Вместо этого опрос детектирования сброса по шине также перемещен в главный цикл main.
• За пакетами SETUP и OUT немедленно следует пакет DATA. V-USB имеет специальный код для обработки этой ситуации, однако это детектирование иногда происходит неудачно, когда интервал между пакетами был слишком большим. Это не составляет проблему при использовании прерываний, потому что есть возможность "откладывания в стек". В случае версии без прерываний нужно добавить дополнительный код перед "handleSetupOrOut" в файле asmcommon.inc.
• Поскольку пакеты принимаются и обрабатываются по порядку, нет больше необходимости использовать двойной буфер RX. Удаление двойного буфера экономит память.

Уже сейчас Вы можете найти полную реализацию в тестовой ветке Micronucleus V2. Но предупреждаем, что она находится в активной разработке, так что многое может измениться. Текущая выложенная версия была протестирована многими людьми, которые убедились в её стабильности. Micronucleus V2 будет выпущена после того, как будет реализована поддержка нескольких устройств. Нужно еще добавить, что если Вы просто хотите получить хороший, маленький USB bootloader для ATtiny85, советую пока использовать текущий релиз Micronucleus V1.11.

[Ссылки]

1. Interrupt free V-USB site:cpldcpu.wordpress.com.
2. Micronucleus site:github.com - USB-загрузчик (bootloader) для микроконтроллеров ATtiny85.

 

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


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

Top of Page