Проводим нагрузочное тестирование скоростных USB-библиотек для STM32F103C8T6

В предыдущей статье я показывал предельную скорость шины USB у микроконтроллера STM32F103 со штатной библиотекой MiddleWare. В комментариях мне показали сразу две самодельных библиотеки, выжимающих из USB FS все соки. Но автор одной из библиотек высказал мысль, что быстро-то они работают быстро, а насколько надёжно — не ясно. Он считал, что было бы полезно провести нагрузочное тестирование с какими-то полезными данными. Только если они не потеряются и не исказятся, можно будет сказать, что библиотеки имеют право на жизнь.

urehu998qeegm2asj7-v_lqlcxu.png

Само собой, я еле дождался выходных, чтобы провести проверки. Давайте рассмотрим результаты испытаний. А чтобы было интересней, попутно рассмотрим технологию отображения переменных «на лету», без остановки процессорного ядра. Ну, и технологию визуальной отладки elf-файлов, собранных пакетными компиляторами.

Какое тестирование будем проводить


Суть тестирования была обсуждена в комментариях к предыдущей статье. Исходная программа для PC шлёт данные. Какие — не важно. Данные, и всё тут. Контроллер эти данные принимает и благополучно игнорирует. Потому что вопрос был в скорости передачи. Новые библиотеки теоретически могут либо пропустить часть данных, либо перепутать местами пару буферов. Поэтому поток должен стать меняющимся по времени. В таких случаях, шлют либо псевдослучайную последовательность, либо инкрементирующиеся данные.

Не имея желания тратить много времени на формирование псевдослучайной последовательности для источника и приёмника (алгоритм же должен совпадать), я ограничился инкрементирующимся 32-битным словом. Главный недостаток такого подхода — в соседних пакетах могут совпадать вплоть до трёх байтов из четырёх. Но один, причём самый первый — будет различаться. Так что для сегодняшнего теста мне это кажется допустимым.

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

В принципе, все желающие смогут переписать мой код на другой вариант тестовых данных… Сегодня же, я просто буду слать инкрементирующееся 32-битное слово, а при приёме — проверять, что инкремент идёт без нарушения последовательности.

Как мы будем отслеживать результат


Как мы будем выяснять, что всё работает? Одного светодиода будет мало. Ну, а чтобы добавлять UART, надо сильно корёжить чужой код. Можно внести свои ошибки. Давайте воспользуемся функциональностью, о которой я знаю давно, но всегда пользовался ею только в среде разработки Keil. Сегодня я покажу, как ею воспользоваться в Эклипсе. Из комментариев к прошлой статье, я понял, что об этой технологии знают не все.

Отладочный порт JTAG позволяет вести работу, только когда процессорное ядро остановлено. Это неприемлемо для USB. Там и при обычной-то работе остановки чреваты таймаутами, а в нашем случае — даже если мы не поймаем таймаут, то скорость может быть занижена. К счастью, отладочный порт SWD позволяет отслеживать память на лету. В далёком 2016-м я проверял при помощи осциллографа, позволяющего задавать синхронизацию по длительности импульса, доступ к памяти силами SWD практически не замедляет работу процессорного ядра. Но как мы им воспользуемся?

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

А второе — это то, что мы с ребятами недавно нашли. У нас в конторе этого не знал никто. Оказывается, если собрать проект пакетным компилятором, внедрив в него отладочную информацию Dwarf-2, то затем этот elf-файл можно открыть в Эклипсе на отладку и получить полную связь с его исходниками. При этом сами исходники к проекту подключать не требуется. Отладчик будет автоматически подтягивать пути к ним из отладочной информации. Теперь я всегда так делаю. Собран проект GCC или CLang, а я просто подключаю elf к Эклипсе и трассирую её, не тратя времени на присоединение к этой Эклипсе самого проекта. Мне иногда даже присылают elf-файлы, собранный тулчейнами, которых нет на моей машине. Даже собранные в Линуксе (а я работаю с Windows). Метод работает даже в этих случаях, лишь бы проект был прислан в полном комплекте: elf и его исходники. Сегодня нам это поможет не дорабатывать авторские проекты в плане их структуры. Я просто буду собирать всё на базе «родных» makefile, а затем — подключаться отладчиком к elf-файлу.

Тренируемся подключаться


Первое, что нам понадобится сделать в CubeIDE, проект для STM32F103. «Минуточку!», —воскликнет внимательный читатель… «Автор только что обещал, что не придётся ничего делать с оригинальным проектом!!!». Всё верно. Это бзик CubeIDE. Нам нужен проект для STM32F103. Любой. Главное, чтобы он был под STM32F103. Создаём его, собираем и забываем. Что в нём — не важно. Важен сам факт его существования в среде разработки.

Теперь в CubeIDE идём в настройки отладчика. Например, так:

vaektwuqmkqyl_s9gkbhjtbro-o.png

Нам бы не пришлось извращаться с созданием левого проекта, если бы мы выбрали пункт GDB Hardware Debugging. Я всегда выбираю его в обычных Эклипсах. Пробовал я его выбрать и тут:

fyamhtmak846xayzet4uj68wzqk.png

Увы. Левый проект будет не нужен, но зато про функциональность отображения переменных в реальном времени говорят, что она недоступна. Поэтому увы и ах. Выбираем STM32 Cortex-M C/C++ Application. У меня там уже есть две конфигурации. Сейчас, чтобы убедиться, что я вас не обманул, я создам третью. Для этого дважды щёлкаю сюда:

loxgehd8hqwxcqkpywjpa89oega.png

Назову конфигурацию Article:

83lor7scdfd0-sbj63vqk__rwi0.png

Надо выбрать путь к elf-файлу:

espcvsitqgem6uft7r87pzzkoz8.png

Я выбрал такой путь (нигде в пути не должно быть русских букв):

zq5km7p_gzthf6lptm4kv4kmgwm.png

И вот тут загорается ошибка. Вот она, красненькая:

uafy0wdtr3ogfxbikupalruoe0k.png

Чтобы её убрать, я должен выбрать проект, привязанный к STM32F103. Вот тут-то и надо ткнуть в Browse:

9pfbbdtvv6_bax7vprkbxrgqa2e.png

И выбрать заранее созданный левый проект.

kcmn-5pduv7abs5n8cicfvutbhg.png

Красненькое (признак ошибки) ушло:

oamwrf3qouu-61qfmnyyuxhbdsy.png

Ой! На этом рисунке видно, что у меня после выбора проекта спрыгнуло имя elf-файла. Прописался elf этого проекта. Пришлось выбрать нужный после указания проекта ещё раз. Не зря пробежался по всем пунктам.

Так как нам тут нечего собирать (мы всё собираем пакетно), надо поставить флажок так, чтобы система не пыталась заниматься бесполезной работой:

8ehgx-iemqez7gmu6lq5g8nfxqg.png

На этой вкладке — всё. Переходим на вкладку Debugger. Правда, тут менять ничего не надо. По крайней мере, если у вас всё настроено так же:

ptxgjiarijv1oeuvlj0ldbms_gw.png

Собственно, больше нигде ничего менять не надо. Ну что, запускаем отладку?

rz2yl5gprnqb1ukmuzh4ec52ozm.png

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

Проверяем первую библиотеку


Итак, скачиваем проект stm32samples/F1-nolib/CDC_ACM at master · eddyem/stm32samples · GitHub за авторством EddyEm.

Не забываем в makefile добавить формирование отладочной информации dwarf-2:

isxicw1fsx1zqaxl8rkzek4d6ya.png
То же самое текстом:

CFLAGS	+= -O2 -g -gdwarf-2 -D__thumb2__=1 -MD

Начинаем править код.

В функции main () есть бесконечный цикл. Я оставлю от него только рожки-да-ножки:

    while (1){
        IWDG->KR = IWDG_REFRESH; // refresh watchdog
        usb_proc();
        get_USB();
    }

Рабочую функцию get_USB () я сделаю такой:
uint32_t loop = 0;
uint32_t errors = 0;
uint32_t errState = 0;
int32_t lastData = 0;
int32_t show = 0;
int32_t pkt = 0;

#define USBBUF 63
char tmpbuf[USBBUF+1];
int32_t* pData = (int32_t*) tmpbuf;
// usb getline
char *get_USB()
{
    int x = USB_receive((uint8_t*)tmpbuf) / sizeof(uint32_t);
    int i;

    show += 1;

    if(!x) return NULL;

    pkt += 1;
    // Буфер начинается с нуля - это новый цикл замеров
    // Забыли про ошибки!
    if (pData [0] == 0)
    {
         lastData = 0;
         errState = 0;
         loop += 1;
    }
    // Предотвращаем снежный ком ошибок
    if (errState)
    {
         return NULL;
    }
    // проверяем факт искажений
    for (i=0;i

Куча глобальных переменных сделана для того, чтобы наблюдать за ними в реальном времени. Локальные при каждом запуске теряются. Глобальные видны вечно.

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

Переменная pkt покажет, что данные действительно приходят (поначалу они у меня не приходили). Она увеличится только если мы не вышли из-за того, что из USB не было ничего.

lastData покажет, до скольки мы уже досчитали в рамках теста. Она позволит убедиться, что мы действительно работаем с большими блоками данных. Значение этой переменной в конце теста показывает размер блока в двойных словах. Чтобы понять, сколько пробежало байт, надо умножить значение на 4.

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

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

errors — счётчик возникавших когда-то ошибок. Поначалу я сам ошибся в логике, тогда этот счётчик постоянно увеличивался. Но если всё хорошо — он увеличиваться не должен.

Чуть не забыл. Я ещё закомментировал проверку флага в функции USB_receive:

v2jjkfe39kqjd6keyzffsfpsrgu.png
То же самое текстом:

uint8_t USB_receive(uint8_t *buf){
    if(/*!usbON ||*/ !rxNE) return 0;
...

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

Ну что. Собираем проект пакетным компилятором и запускаем его в отладчике CubeIDE. Как мне объясняли, не всем читателям нравятся анимированные рисунки. Некоторых они отвлекают от чтения текста. Но тут реально приятно поглядеть.

hw5i39kgkinebbvtjnlnpxggovq.gif

Оно тикает! Оно тикает! Оно тикает!

Ну что ж. Добавляем в тестирующую программу заполнение массива:

    QByteArray data;
    data.resize(totalSize);

    uint32_t* dwPtr = (uint32_t*) data.constData();
    for (uint32_t i = 0;i

И прогоняем тест. Получаем вот такую красоту в переменных (ноль ошибок):

odxieriys9hi2--n6hvyp_r3vo8.png

И вот такой график скорости:

nuixjo0o6ynhud3ga7nt1cr2efm.png

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

Сферический конь в вакууме работает, а что с реальным?


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

Будем тестировать разные варианты? По уму — надо бы, но времени нет. Я сейчас по работе занят совсем с другим контроллером. И тут-то просто на выходных развлекался. Поэтому мы пойдём другим путём. Мы приклеим приём данных к факту их прихода.

Такие штучки делаются при помощи прерываний. Но в прошлой статье говорилось, что, если мы растянем обработчик прерывания USB, аппаратура начнёт слать NAKи и вся прелесть рассматриваемой библиотеки сойдёт на нет. Как нам получить прерывание, но не задерживаться в прерывании?

Ну, тут путь известный. Мы должны в обработчике USB прерывания сделать так, чтобы сразу после выхода из него, сработало бы тоже прерывание, но какое-то другое. И там мы быстренько, с гарантированно низкой задержкой, заберём данные из аппаратного буфера в свой внутренний. На какое бы прерывание сесть? Осматриваем startup код. А именно — обработчики прерываний. Наша задача найти неиспользуемый.

Вот интересующий нас файл
\stm32samples-master\F1-nolib\inc\startup\vector.c

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

[NVIC_USART3_IRQ] = usart3_isr, \

Зная имя, создаём в файле main.c функцию:

void usart3_isr()
{
    NVIC_ClearPendingIRQ(USART3_IRQn);
    get_USB();
}

Это будет этакая функция обратного вызова. И уже она вызовет нам тот код, который мы недавно написали. А вызов get_USB () в бесконечном цикле — закомментируем.

Теперь надо задать этому прерыванию приоритет пониже, чтобы оно никому не мешало. В реальной жизни, возможно, придётся подойти к выбору приоритета творчески. Но сегодня я просто возьму пятнадцатый. Дописываем в инициализирующую часть функции main () такой код:

    NVIC_SetPriority(USART3_IRQn, 15);
    NVIC_EnableIRQ(USART3_IRQn);

Ну, и теперь самое интересное. В обработчик USB прерывания добавляем провокацию на срабатывание прерывания USART3, если было обращение к нашей конечной точке:

b_ofgsruxuy7v0drh_glzszq508.png

То же самое текстом.
#include "stm32f10x.h"
…
void usb_lp_can_rx0_isr(){
   LED_off(LED0);
    if(USB->ISTR & USB_ISTR_RESET){
…
    }
    if(USB->ISTR & USB_ISTR_CTR){
        // EP number
        uint8_t n = USB->ISTR & USB_ISTR_EPID;

        if (n == 1)
        {
             NVIC_SetPendingIRQ(USART3_IRQn);
        }
        // copy status register
        uint16_t epstatus = USB->EPnR[n];
        // copy received bytes amount
…

Так как приоритет низкий — до конца работы USB прерывания ничего не произойдёт. Но как только оно завершится — тут же вызовут нас. Потому что у нас никаких других прерываний пока что нет. Даже с пятнадцатым приоритетом мы будем самыми важными персонами.

Запускаем. Сначала пугает, что переменная show не увеличивается. Но это нормально. Сейчас функция вызывается не безусловно, а только после фактического прерывания. Так что надо запустить тестирование.

На процесс тестирования можно смотреть вечно.

ixia3hkhear_mdmtpbedtq9luto.gif

А вот метрика скорости:

xfris7hsee_djojwylcqdzbppno.png

Проверяем вторую библиотеку


Теперь проверяем библиотеку usb/5.CDC_F1 at main · COKPOWEHEU/usb · GitHub за авторством COKPOWEHEU. Описание этой библиотеки можно посмотреть здесь: USB на регистрах: STM32L1 / STM32F1 /. Здесь нам предоставляются функции обратного вызова для обработки активности конечных точек. Вот её и поправим. Переменная show становится не нужна. Нас всегда вызывают по приходу данных. В остальном — получаем практически тот же самый код.
uint32_t loop = 0;
uint32_t errors = 0;
uint32_t errState = 0;
int32_t lastData = 0;
int32_t pkt = 0;

void data_out_callback(uint8_t epnum){
  int i;
uint8_t buf[ ENDP_DATA_SIZE ];
int32_t* pData = (int32_t*) buf;

  int len = usb_ep_read_double( ENDP_DATA_OUT, buf) / sizeof (uint32_t);
  if(len == 0)return;

    pkt += 1;
    // Буфер начинается с нуля - это новый цикл замеров
    // Забыли про ошибки!
    if (pData [0] == 0)
    {
         lastData = 0;
         errState = 0;
         loop += 1;
    }
    // Предотвращаем снежный ком ошибок
    if (errState)
    {
         return NULL;
    }
    // проверяем факт искажений
    for (i=0;i

При проверке у меня CubeIDE почему-то неверно определяла стартовый адрес. Возможно, имеет место какая-то несовместимость с тем самым «левым» проектом. Отложим это на отдельное исследование. Пока я не стал разбираться, а прямо при старте вписал правильное значение регистра PC. Код запустился и начал работать. Прогоняем тест. Количество ошибок тоже нулевое:

3pffrzq4gde_equm74mw2inhomo.png

Скорость — тоже приличная:

k7ddft7859omeb19k2q8aynfkdm.png

Заключение


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

Попутно мы освоили замену отладочного вывода путём отслеживания в реальном времени ряда переменных через порт SWD. Начерно мы освоили также отладку любых пакетно собранных приложений в Эклипсе, но по ходу работы, из-за смешивания двух проектов, у меня возникли некоторые трудности, которые пришлось преодолевать, правя регистр PC напрямую. Но в обычной Эклипсе такое смешивание не понадобится. И в конце концов, даже при помощи серпа, молота и какой-то матери, конечная цель всё равно была достигнута. Отладка была проведена. Исходные коды на Сях при этом всё равно отображались в Эклипсе.

Послесловие


Когда статья уже была написана, но ещё находилась в процессе выгрузки на Хабр, появился вот такой замечательный материал за авторством DSarovsky. Там тоже реализуется доступ к USB, но делается это через библиотеку, сделанную в моём любимом стиле — стиле Константина Чижова.

Я просто обязан отметить существование библиотеки, сделанной в таком красивом варианте. На текущий момент мы с её автором проверили быстродействие и выяснили, что пока что её скорость относится к типовой, а не к максимальной. Но возможно, когда вы читаете эти строки, она уже разогнана. Поэтому я оставлю ссылку на неё среди прочих. Она просто обязана взлететь! Библиотеки в таком стиле не могут не взлетать!

© Habrahabr.ru