Русский
Русский
English
Статистика
Реклама

Stm32f103

USB Host, Blue Pill, метод деления отрезка пополам и цена на водку в СССР

19.03.2021 22:15:52 | Автор: admin

Написал недавно программный USB-HOST на esp32 для работы с клавиатурой/мышкой/джойстиком. Процессор быстрый, но нежный, 5 вольт на ножках не выдерживает. Поэтому решил переписать на stm32f103c8t6, широко известную в варианте отладочной платы "Blue Pill".

К сожалению , это весьма неторопливый по сегодняшним меркам процессор(72 MHz vs 240 у esp32 ), поэтому были сомнения , смогу ли я обеспечить необходимую точность временного интервала между битами при передаче (1.5 Mbps +/- 1.5%),что соответствует +/- 0.01uS то есть примерно один такт работы процессора. То есть процедура задежки типа :

static inline  void cpuDelay(uint32_t ticks){uint32_t stop =_getCycleCount32() + ticks;while((_getCycleCount32() - stop)&0x80000000u); // соntinue while overflow}

необходимую точность обеспечивать перестала, поэтому решил перейти к процедуре вида:

void cpuDelay(){__asm__ __volatile__("nop");__asm__ __volatile__("nop");__asm__ __volatile__("nop");__asm__ __volatile__("nop");}  

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

#define TNOP1  { __asm__ __volatile__("   nop"); }#define TNOP2  {TNOP1 TNOP1}#define TNOP4  {TNOP2 TNOP2}#define TNOP8  {TNOP4 TNOP4}#define TNOP16  {TNOP8 TNOP8}#define TNOP32  {TNOP16 TNOP16}#define TNOP64  {TNOP32 TNOP32}__volatile__ void cpuDelayBase(){TNOP64;TNOP64;TNOP64;TNOP64;END_BASE:;}void (*delay_pntA)() = &cpuDelayBase;#define cpuDelay(x) {(*delay_pntA)();}#define SIZEOF_NOP 2void setDelay(uint8_t ticks){delay_pntA = (&cpuDelayBase)+((256-ticks)*SIZEOF_NOP);}

Ну а теперь каждом запуске программы генерируется тестовая последовательность бит на 200,посылается, измеряется затраченное время и методом деления отрезка пополам процедура задержки приводится к требуемой и сохраняется в указателе delay_pntA.

Время передачи 6 бит должно составлять точно 4.0uS. 6 бит было удобно замерять на временной диаграмме передачи. В стартовой последовательности присутствует комбинация 6 фронтов , которые хорошо видно на диаграмме:

Диаграмма

При калибровке выяснилось, что есть небольшой оверхед на измерениях, и его нормированная величина 0.12uS отсюда целевое время 4.12 uS.

А причем здесь ВОДКА!!!???

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

Цену на водку люди знали лучше числа Пи. В частности, на пивзаводе где я подрабатывал электриком во время учебы, телефон фельдшера был 2-87, а электриков-механиков 3-62, а дирекции 4-12. И никто не забывал эти номера телефонов. Цену в 2.87 я не застал по причине слишком молодого возраста а 3.62 (Русская ) и 4.12 (Столичная) - уже вполне. Цена складывалась из цены собственно водки и залоговой цены бутылки в 12 копеек, поэтому такая и не круглая.

Водка русская ,этикетка:
Водка столичная , этикетка:

Это картинки отсюда

Итак 4.0 - содержание и 0.12-емкость итого 4.12 ---- Это цена водки Столичная в 1981 году.

результат прогона:

pins 8 9 1 1 is OK!cpu freq = 72.000000 MHzTIME_MULT = 43120 bits in 57.333332 uSec 2.093023 MHz  6 ticks in 2.866667 uS120 bits in 269.000000 uSec 0.446097 MHz  6 ticks in 13.450000 uS120 bits in 162.333328 uSec 0.739220 MHz  6 ticks in 8.116667 uS120 bits in 109.000000 uSec 1.100917 MHz  6 ticks in 5.450000 uS120 bits in 82.916664 uSec 1.447236 MHz  6 ticks in 4.145833 uS120 bits in 69.000000 uSec 1.739130 MHz  6 ticks in 3.450000 uS120 bits in 75.666664 uSec 1.585903 MHz  6 ticks in 3.783333 uS120 bits in 75.666664 uSec 1.585903 MHz  6 ticks in 3.783333 uS120 bits in 77.333336 uSec 1.551724 MHz  6 ticks in 3.866667 uS120 bits in 77.333336 uSec 1.551724 MHz  6 ticks in 3.866667 uSTRANSMIT_TIME_DELAY = 15 time = 4.145833 error = 0.627029%USB1: Ack = 0 Nack = 0 20 pcurrent->cb_Cmd = 2  state = 2 epCount = 0USB2: Ack = 0 Nack = 0 00 pcurrent->cb_Cmd = 0  state = 0 epCount = 0desc.bDeviceClass    = 00desc.bNumConfigurations = 01cfg.bLength         = 09cfg.wLength         = 59cfg.bNumIntf        = 02cfg.bCV             = 01cfg.bIndex          = 00cfg.bAttr           = a0cfg.bMaxPower       = 50pcurrent->epCount = 1pcurrent->epCount = 2desc.bDeviceClass    = 00desc.bNumConfigurations = 01cfg.bLength         = 09cfg.wLength         = 34cfg.bNumIntf        = 01cfg.bCV             = 01cfg.bIndex          = 00cfg.bAttr           = a0cfg.bMaxPower       = 50pcurrent->epCount = 1USB0: Ack = 6 Nack = 0 80 pcurrent->cb_Cmd = 14  state = 100 epCount = 2USB1: Ack = 6 Nack = 0 20 pcurrent->cb_Cmd = 14  state = 104 epCount = 1

P.S. Водка водкой, у нас в Израиле разрешили коноплю, забыл выложить исходники, вот.

P.P.S. не придирайтесь к коду. Это только proof of concept. Будет чиститься.

Подробнее..

Power-line communication. Часть 2 Основные блоки устройства

26.01.2021 02:10:54 | Автор: admin

Часть 1 Основы передачи данных по линиям электропередач

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

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

- Введение
- Мозги устройства микроконтроллер
- Основные требования к микроконтроллеру
- Выбор подходящего микроконтроллера
- Особенности питания устройства

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

Введение

Для начала кратко вспомним из части 1, как происходит передача данных. На изображении одна из фаз ЛЭП. Красное устройство передает, синие слушают. Биты данных один за одним передаются в виде синусоидальных сигналов различной частоты (FSK модуляция).

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

Для примера: если передается символ 0, то генерируется полезный сигнал в виде синусоиды 74 кГц. А если передается 1, то генерируется синусоида с частотой, например, 80 кГц. Номиналы частот не особо важны, просто выбираются любые из разрешенных диапазонов. Главное, чтобы приемник смог их различить.

В первой части статьи упоминалось про третий символ S, который означал начало кадра. Он также кодировался своей определенной частотой. Когда устройство получало символ S, входной буфер очищался. Для простоты в этой статье будут упоминаться только 0 и 1.

Передающие и принимающие устройства синхронизируются между собой с помощью отдельного блока устройства zero cross детектора.

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

Физически это значит, что за один синхросигнал из ZC детектора генерируется один полезный сигнал определенной частоты. В нашем случае это синусоиды 74 кГц или 80 кГц.

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

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

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

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

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

Мозги устройства микроконтроллер

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

Микроконтроллер это такой мини-компьютер, который в одном корпусе содержит процессор (ЦПУ), память (ПЗУ и ОЗУ), ввод-вывод и периферийные устройства. По сути, внутри уже все есть для работы: подаем питание и поехали. Дальше все зависит уже от программы прошивки, которую мы в него записали.

Рисунок с сайта digikey.comРисунок с сайта digikey.com

Сейчас выпускают микроконтроллеры с большим количеством различной встроенной периферии. Это очень удобно, так как меньше необходимости во внешних компонентах, что экономит место на печатной плате (и, конечно же, ваши денежки). Внутри может иметь ЦАП и АЦП, часы с календарем. Даже встроенный USB уже не удивляет.

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

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

Основные требования к микроконтроллеру

Производительность

Основной нагрузкой на ЦПУ будет обработка оцифрованного входного сигнала с помощью ДПФ для выяснения того, какой символ был закодирован в сигнале: 0 или 1. Далее этот символ будет отправляться в протокол на уровень выше. Больше всего вычислений будет происходить именно при подсчете гармоник в ДПФ.

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

Результат визуально можно представить в виде эквалайзера, на котором нарисованы полоски определенных частот разной высоты (амплитуды). Для подсчета высоты каждой отдельной полоски нужно сигнал прогонять через ДПФ.

После подсчета некоторого количества гармоник, делаются выводы о том, какой символ закодирован.

В самом простом случае можно просто сравнить амплитуды гармоник 74 и 80 кГц между собой. Если в сигнале преобладает гармоника с частотой 74 кГц, записываем в входной буфер бит 0.

Если в сигнале преобладает гармоника с частотой 80 кГц, записываем в входной буфер 1.

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

Задача же этого уровня просто, как конвейер, подавать 0 и 1 наверх, а дальше из них будут складываться правильные целостные кадры данных. Или не будут.

Также можно заморочиться и дополнительно рассчитывать амплитуды смежных гармоник и узнавать уровень шума относительно полезного сигнала. Можно программно фильтровать сигнал и тд. Вариантов много.

Суть в том, что считать, возможно, придется много. Успевать считать нужно гарантированно, так как это реалтайм-конвейер.

Если разложить всю нагрузку на которую ЦПУ тратит время друг за другом, то получим примерно это:

  • оцифровка сигнала

  • подсчет амплитуд гармоник через ДПФ и анализ результата

  • прочая нагрузка (обработка прерываний из интерфейсов USB или CAN, обработчики таймеров, моргания светодиодами, работа с памятью, какие-то вычисления по протоколу и т.д.)

Это должно циклично выполняться каждые 10 миллисекунд снова и снова. ЦПУ никогда не должен быть загружен на 100%, иначе есть риск не успеть посчитать что-то важное. Поэтому всегда нужно оставлять запас по производительности.

Энергоэффективность

Обратная сторона быстрых вычислений большее потребление энергии. Чем быстрее контроллер считает, тем больше он потребляет энергии. Поэтому нам не нужен слишком мощный процессор.

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

Должен быть достаточно быстрый АЦП

Нам нужно оцифровывать входной аналоговый сигнал и желательно, чтобы был встроенный АЦП. Точность тут не так важна, как скорость. Так как измеряемый сигнал имеет частоту до сотни килогерц. Для корректных вычислений гармоник есть условие.

Частота дискретизации должна быть минимум в два раза больше частоты измеряемого сигнала [Теорема Котельникова].

Это значит, что для распознавания сигнала нужно сделать от двух точек измерения на период. А по-хорошему 4-5. Посмотрим на примере.

Представим, что мы измеряем сигнал, в котором есть нужная нам гармоника частотой 80 кГц. У сигнала с частотой 80 кГц период 1/80000 = 12,5 микросекунд. Чтобы оцифровать 5 точек на период нужно успевать делать измерение раз в 2.5 микросекунды для адекватного распознавания сигнала.

С пятью точками измерений на период уже выглядит неплохо. ДПФ с этим отлично справится.

Но для чего брать с запасом? Что если измерять по минимуму, только две точки на период? Вот такой сигнал мы оцифруем при удачном попадании.

А так будет выглядеть оцифрованный сигнал, если попасть в момент, когда сигнал в нуле.

Не похоже на синусоиду.

Если интересно посмотреть, что будет, если проводить измерения частотой меньше двух точек за период, то поищите в гугле картинки Эффект алиасинга.

Должен быть достаточно быстрый ЦАП

Для полезного сигнала нужно сгенерировать синусоиду большой частоты. Чем больше точек на период синусоиды успеет генерировать ЦАП, тем плавнее будет сигнал на выходе (меньше лесенка, которую затем сгладит конденсатор).

Представим на примере синусоиды с частотой 80 кГц, период 12.5 микросекунд. Возьмем для начала 4 точки на период. Генерация каждые 3.125 микросекунды.

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

Увеличим количество точек вдвое. Генерация каждые 1.56 микросекунды.

Нужна достаточная скорость ЦАП для того, чтобы сигнал был хотя бы похож на синус. В нашем случае, с сигналом частотой до 80 кГц, будет достаточно чтобы ЦАП успевал менять уровень сигнала раз в 1.5 микросекунды. Если успеет быстрее, то еще лучше.

С выхода ЦАП этот угловатый сигнал проходит через пассивный фильтр нижних частот и в сглаженном виде идет на усилитель выходной цепи.

Если нет АЦП

Помню, в самом начале я проводил эксперименты на 8-битных AVR от Atmel серии ATmega8, и у них в распоряжении не было АЦП. Но на них было очень удобно начинать знакомство с миром микроконтроллеров. Низкий порог вхождения и никаких танцев с бубнами при запуске.

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

И этот оцифрованный сигнал прогонялся через ДПФ и вычислялось наличие нужных гармоник. Ни о какой точности или чувствительности речи и не шло, но это работало.

Если нет ЦАП

Аналогичная ситуация на ATmega8 была с ЦАП. Его там нет, и мне очень не хотелось заморачиваться с внешним ЦАП.

Оказалось, что можно пожертвовать логическими выходами микроконтроллера и подключить к ним резисторную матрицу R-2R. Таким образом из горстки резисторов собрать свой ЦАП с нужной разрядностью.

Картинка с сайта easyelectronics.ruКартинка с сайта easyelectronics.ru

Подавая 0 и 1 на выходы микроконтроллера, можно получать нужный уровень напряжения на выходе OUT. Чем больше выходов будет использовано, тем выше разрядность ЦАП. По схеме R-2R оставил ссылку в конце.

Выбор подходящего микроконтроллера

После экспериментов на ATmega8 мне захотелось улучшить то, что есть. Выбирая из разных вариантов, я положил глаз на STM32. А конкретно на STM32F103 это 32-битные микроконтроллеры на ядре ARM Cortex-M3 (до 72 MHz).

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

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

Производительность?

Схема тактирования позволяет работать ЦПУ на частоте 72 MHz, что после 8-битных на 20 MHz было с запасом. Хватало для более точных расчетов по алгоритму ДПФ.

Энергоэффективность?

При почти максимальной нагрузке потреблял около 40-50 мА. Дешевый стабилизатор напряжения в схеме питания на 100 мА с этим справлялся. Даже с учетом остальной маложрущей периферии этого было достаточно.

Достаточно быстрый АЦП?

Разобрался, как разогнать до максимальной скорости АЦП при частоте ЦПУ 72 MHz. Так как ранее было сказано, что полезный сигнал будет частотой в районе 80 кГц, то будем считать исходя из этого.

В доках для STM32 нашел, как вычислять минимальное время преобразования: нужно к настраиваемому времени семплирования (минимум 1.5 цикла) прибавить 12.5 машинных циклов. Получается 14 машинных циклов на одну точку измерения.

При определенной настройке схемы тактирования на модуль АЦП приходится 14 MHz. Если перевести в секунды, то 14 циклов при частоте тактирования 14 MHz это одно измерение в 1 микросекунду.

Идеально! Даже если полезный сигнал будет частотой 100 кГц, я смогу измерить 10 точек за один период сигнала. С минимальной точностью, но быстро.

Примерно так будет выглядеть оцифровка синусоиды 80 кГц.

Достаточно быстрый ЦАП?

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

Почитав документацию, я понял, что в ЦАП STM32F103 встроенный ОУ имеет ограничение в 1 MSPS. Получилось настроить генерацию каждой точки сигнала раз в 1 микросекунду.

Примерно так при этом будет выглядеть синусоида с частотой 80 кГц на выходе из ЦАП.

Периферия

Что еще мне понравилось в STM32F103 это наличие встроенного USB. Там есть режим эмуляции COM порта. Мне показалось это очень удобным, особенно после внешних преобразователей USB-UART.

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

Для экспериментов подключал два PLC устройства к двум компам, и они посылали друг другу ASCII символы, вводимые с клавиатуры. Получилось что-то вроде чата через розетку 220 В.

Особенности питания устройства

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

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

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

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

Стабильное питание микроконтроллера

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

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

Примерная картина потребления мощностиПримерная картина потребления мощности

При передаче кадра это происходит каждые 10 миллисекунд длиной в 1 миллисекунду.

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

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

Совет 1 - Разделить землю на аналоговую и цифровую

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

Для этого нужно разделить дорожки GND в самом начале схемы питания возле минуса блока питания. Ни в коем случае нельзя их пересекать или как-то замыкать в других частях схемы.

Для питания условно цифровых компонентов схемы (микроконтроллер, EEPROM память и т.д.) от самого блока питания должна идти отдельная линия, можно назвать её DGND.

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

Совет 2 - Не забыть про керамику

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

Картинка с сайта allexpress.comКартинка с сайта allexpress.com

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

С танталовыми осторожнее, они красиво взрываются :).

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

Совет 3 - Экранировать цифровые компоненты

Может получиться так, что недалеко от микроконтроллера расположен высокочастотный трансформатор, который во время передачи сигнала генерирует электромагнитные помехи.

Мне помогло расположение микроконтроллера на другой от ВЧ трансформатора стороне печатной платы и наличие земляного полигона под корпусом микроконтроллера.

Картинка с сайта caxapa.ru "Помехоустойчивые устройства, Алексей Кузнецов"Картинка с сайта caxapa.ru "Помехоустойчивые устройства, Алексей Кузнецов"

Подробнее можно почитать в статье по ссылке в конце.

Заключение

В этой части мы в общих чертах разобрали чем занимается микроконтроллер. Узнали некоторые особенности питания устройства и возможные проблемы.

Статья вышла довольно объемной. Я постарался максимально коротко передать основные моменты. Может сложиться ощущение незаконченности и это нормально. Для углубленного изучения оставлю ссылки внизу.

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

У кого был/есть какой-либо опыт в PLC обязательно делитесь этим с остальными в комментариях :)

Полезные ссылки

https://nag.ru/articles/article/24485/strasti-po-plc.html - интересная статья по истории PLC
https://www.electronshik.ru/catalog/interfeys-modemy-plc - заводские PLC микросхемы с datasheet (там много схем и характеристик)
https://ru.wikipedia.org/wiki/Частотная_манипуляция - FSK модуляция
http://www.atmega8.ru/ - про ATmega8

STM32
https://www.st.com/en/microcontrollers-microprocessors/stm32f103.html - STM32F103
https://themagicsmoke.ru/courses/stm32/led.html - Помигать светодиодом на stm32
https://blog.avislab.com/stm32-clock_ru - схема тактирования stm32
http://personeltest.ru/aways/habr.com/ru/post/312810/ - подробнее про ЦАП в stm32
https://blog.avislab.com/stm32-adc_ru/ - АЦП в stm32
https://blog.avislab.com/stm32-usb_ru/ - USB в stm32

Аналоговая часть
http://easyelectronics.ru/parallelnyj-cifro-analogovyj-preobrazovatel-po-sxeme-r-2r.html - преобразователь по схеме R-2R
http://caxapa.ru/lib/emc_immunity.html - "Помехоустойчивые устройства", Алексей Кузнецов
https://www.ruselectronic.com/passive-filters - пассивные фильтры

Подробнее..

Предельная скорость USB на STM32F103, чем она обусловлена?

24.05.2021 14:14:07 | Автор: admin
У данной статьи тяжёлая история. Мне надо было сделать USB-устройства, не выполняющие никакой функции, но работающие на максимальной скорости. Это были бы эталоны для проверки некоторых вещей. HS-устройство я сделал на базе ПЛИС и ULPI, загрузив туда прошивку на базе проекта Daisho. Для FS-устройства, разумеется, была взята голубая пилюля. Скорость получалась смешная. Прямо скажем, черепашья скорость.



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

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

Итак, давайте выясним, почему именно STM32F103C8T6 не может прокачать по шине USB данные на скорости 12 мегабит, заняв всю ширину предоставленного канала.

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

Подготовка проекта STM32


Создаём проект


Итак. Чтобы все могли повторить мои действия, я скачал самую свежую на момент написания статьи версию 6.2. Правда, они выходят с такой частотой, что на момент, когда всё будет выложено на Хабр, всё может уже измениться. Но так или иначе. Скачиваем, устанавливаем.

Создаём новый проект для STM32F103C8Tx. Добавляем туда USB-Device.



Теперь, когда есть USB, добавляем CDC-устройство. При этом заменим ему VID и PID. Дело в том, что у меня есть inf Файл, который ставит драйвер winusb именно на эту пару идентификаторов. При работе через драйвер usbser скорость была ещё ниже. Я решил исключить всё, что может влиять. Буду замерять скорость без каких-либо прослоек.



Теперь добавляем RCC для работы с тактовыми сигналами:



После всего этого (добавили USB и добавили RCC) можно и тактовые частоты настроить, но сначала спасём себя от самоотключающегося блока отладки. Он спрятан надёжно! Вот так сейчас всё выглядит по умолчанию:



А вот так надо



Прекрасно! Теперь можно настроить тактовые частоты. Я всегда это делаю опытным путём. Системная частота должна стать 72 МГц, а частота USB 48 МГц. Это я в состоянии запомнить. Остальное каждый раз заново вывожу.



Ну всё. Для тестового проекта настроек, вроде, достаточно. Заполняем свойства проекта и сохраняем. Лично я в формате MDK ARM. Он же Кейл. Мне так проще.

Надеюсь, я ничего не забыл сделать. Я специально показываю все шаги, чтобы внимательная общественность проверила меня.

Донастраиваем проект в среде разработки


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



Дописываем код проекта


Наш проект должен просто принимать данные из USB и И всё! Принимать, принимать, принимать! Не будем тратить время на какую-то обработку этих данных. Просто приняли и забыли, приняли и забыли. Обработчик события данные приняты в типовом CDC проекте живёт здесь:



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

Подготовка проекта под Windows


Вариант честной работы с UART мы опустим. Дело в том, что совсем скоро мы будем искать причины тормозов. А вдруг они вызваны драйвером usbser.sys? Нет. Мы возьмём проверенный временем драйвер winusb и будем работать с ним через библиотеку libusb. Кому нравится Linux сможет работать через эту же библиотеку там. Мы тренировались работать с нею в этой статье. А в этой учились работать с нею в асинхронном режиме.

Сначала я вёл работу через блокирующие функции, так как их написать было проще. Мало того, черновые замеры, которые я делал ещё до начала работы над текстом, были вполне красивые. Это ещё не всё, первая метрика, снятая для статьи, тоже была прекрасна и полностью отражала черновые результаты! Потом что-то случилось. График стал каким-то удивительным, правда, чуть ниже я эту удивительность объясню. При работе с блоками меньше чем 64 байта программа съедала 25% процессорного времени, а именно на блоке 64 байта был излом. Мне казалось, что кто-то обязательно напишет в комментариях, что сделай я всё на асинхронных функциях, всё станет намного лучше. В итоге, я взял и всё переписал на асинхронный вариант. Процент потребления процессорного времени на малых блоках действительно изменился. Теперь программа потребляет 28% вместо двадцати пяти Цифры скоростей же не изменились Но асинхронная работа более правильная сама по себе, так что я покажу именно её. Вся теория уже рассматривалась мною в тех статьях про libusb.

Я завожу всё ту же вспомогательную структуру:
    struct asyncParams    {        uint8_t* pData;        uint32_t dataOffset;        uint32_t dataSizeInBytes;        uint32_t transferLen;        uint32_t actualTranfered;        QElapsedTimer timer;        quint64       timerAfter;    };    asyncParams m_asyncParams;

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

Ну, и указатели на объекты передача имеются, куда же без них:
    static const int m_nTransfers = 32;    libusb_transfer* m_transfers [m_nTransfers]; 

Функция обратного вызова отличается от описанной в предыдущих статьях как раз тем, что она считывает показание таймера, если передавать больше нечего. Это произойдёт не единожды, а для каждой из передач (тридцати двух в случае мелких блоков, если блоки крупные их будет меньше, но всё равно не одна). Но на самом деле, это не страшно. Мы это значение будем анализировать только после последнего вызова этой функции. В остальном там всё то же, что и раньше, так что просто покажу код, не объясняя его. Объяснения все были в предыдущих статьях.
void MainWindow::WriteDataTranfserCallback(libusb_transfer *transfer){    MainWindow* pClass = (MainWindow*) transfer->user_data;    switch (transfer->status )    {    case LIBUSB_TRANSFER_COMPLETED:        pClass->m_asyncParams.actualTranfered += transfer->length;        // Still need transfer data        if (pClass->m_asyncParams.dataOffset < pClass->m_asyncParams.dataSizeInBytes)        {            transfer->buffer = pClass->m_asyncParams.pData+pClass->m_asyncParams.dataOffset;            pClass->m_asyncParams.dataOffset += pClass->m_asyncParams.transferLen;            libusb_submit_transfer(transfer);        } else        {            pClass->m_asyncParams.timerAfter = pClass->m_asyncParams.timer.nsecsElapsed();        }        break;/*    case LIBUSB_TRANSFER_CANCELLED:    {        pClass->m_cancelCnt -= 1;    }*/    default:        break;    }}

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

Ну, а параметр blockSize у функции это я в своих статьях уже набил оскомину высказыванием, что при работе с USB скорость зависит от размера блока. До определённого значения она ниже нормальной. Это связано с тем, что хост посылает пакеты медленнее, чем их может обработать устройство. Поэтому я всегда строю графики и смотрю, где они входят в насыщение. Сегодня я буду делать то же самое. Правда, сегодня график в дополнение к банальному росту, имеет непривычную для меня форму, что и сподвигло меня на переделку программы с блокирующего на асинхронный режим. Итак, функция, измеряющая скорость, выглядит так:
Её текст я скрыл под катом.
quint64 MainWindow::MeasureSpeed2(uint32_t totalSize, uint32_t blockSize, uint32_t avgCnt){    std::vector<qint64> gist;    gist.resize(avgCnt);    QByteArray data;    data.resize(totalSize);    m_asyncParams.dataSizeInBytes = totalSize;    m_asyncParams.transferLen = blockSize;    uint32_t nTranfers = m_nTransfers;    if (totalSize/blockSize < nTranfers)    {        nTranfers = totalSize/blockSize;    }    for (uint32_t i=0;i<avgCnt;i++)    {        m_asyncParams.dataOffset = 0;        m_asyncParams.actualTranfered = 0;        m_asyncParams.pData = (uint8_t*)data.constData();        // Готовим структуры для передач        for (uint32_t i=0;i<nTranfers;i++)        {            m_transfers[i] = libusb_alloc_transfer(0);            libusb_fill_bulk_transfer (m_transfers[i],m_usb.m_hUsb,0x02,//,0x01,                                       m_asyncParams.pData+m_asyncParams.dataOffset,m_asyncParams.transferLen,WriteDataTranfserCallback,                                            // No need use timeout! Let it be as more as possibly                                       this,0x7fffffff);            m_asyncParams.dataOffset += m_asyncParams.transferLen;        }        m_asyncParams.timerAfter = 0;        m_asyncParams.timer.start();        for (uint32_t i=0;i<nTranfers;i++)        {            int res = libusb_submit_transfer(m_transfers[i]);            if (res != 0)            {                qDebug() << libusb_error_name(res);            }        }        timeval tv;        tv.tv_sec = 0;        tv.tv_usec = 500000;        while (m_asyncParams.actualTranfered < totalSize)        {            libusb_handle_events_timeout (m_usb.m_ctx,&tv);        }        quint64 size = totalSize;        size *= 1000000000;        gist [i] = size/m_asyncParams.timerAfter;    }    for (uint32_t i = 0;i<nTranfers;i++)    {        libusb_free_transfer(m_transfers[i]);        m_transfers[i] = 0;    }    qint64 avgSpeed = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        avgSpeed += gist [i];    }    avgSpeed /= avgCnt;    if (avgCnt < 4)    {        return avgSpeed;    }    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist [i] < (avgSpeed * 3)/4)        {            gist [i] = 0;        }        if (gist [i] > (avgSpeed * 5)/4)        {            gist [i] = 0;        }    }    avgSpeed = 0;    int realAvgCnt = 0;    for (uint32_t i=0;i<avgCnt;i++)    {        if (gist[i]!= 0)        {            avgSpeed += gist [i];            realAvgCnt += 1;        }    }    if (realAvgCnt == 0)    {        return 0;    }    return avgSpeed/realAvgCnt;}


Сборку статистики в файл csv я делаю так:
void MainWindow::on_m_btnWriteStatistics_clicked(){    QFile file ("speedMEasure.csv");    if (!file.open(QIODevice::WriteOnly))    {        QMessageBox::critical(this,"Error","Cannot create csv file");        return;    }    QTextStream out (&file);    QApplication::setOverrideCursor(Qt::WaitCursor);    for (int blockSize=0x8;blockSize<=0x20000;blockSize *= 2)    {        quint64 speed = MeasureSpeed(0x100000,blockSize,10);        out << blockSize << "," << speed << Qt::endl;    }    out.flush();    file.close();    QApplication::restoreOverrideCursor();}


Первый результат


Итак. У нас есть прошитое устройство, у нас есть программа для его тестирования. Строим статистику для разных размеров блока, чтобы быть уверенными, что проблемы не зависят от этого значения.



Где-то после размера блока 4 килобайта, скорость упирается в 560 килобайт в секунду. Давайте я грубо умножу это на 8. Получаю условные 4.5 мегабита в секунду. Условность состоит в том, что на самом деле, там ещё бывают вставные биты, да и на пакеты оверхед имеется. Но всё равно, это отстоит очень далеко от 12 мегабит в секунду, положенных на скорости Full Speed (кстати, именно поэтому на вступительном рисунке стоит знак 120, он символизирует данный теоретический предел).

Почему результат именно такой


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


То же самое текстом.
*** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usACKD2 *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD *** +45 usNAK5A *** +4 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA14B 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usACKD2 *** +5 usOUT Addr: 16 (0x10), EP: 1E1 90 40 *** +3 usDATA0C3 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD BA 0D F0 AD*** +45 usNAK5A


И так до бесконечности на всех участках, что я смог осмотреть глазами, возвращается то ACK, то NAK. Причём в режиме FS каждая пачка данных передаётся целиком. Принялась она или нет, а всё равно передаётся целиком. Хорошо, что это не роняет всю шину USB, так как до последнего хаба данные бегут на скорости HS в виде SPLIT транзакции. А дальше уже хаб мелко шинкует её на пакеты по 64 байта и пытается отослать на скорости FS.

Подобные чередования ACK-NAK я нашёл и в сети, когда пытался проверить, у одного ли меня такой результат. Нет, не у одного.

Причины падения скорости, честно говоря, написаны прямо под носом. А именно в комментариях к функции, где нам следует вставлять обработку данных. Вот их текст:



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

А почему при блоке 64 байта скорость выше? Я долго думал, и нашёл следующее объяснение: 64 байта это тот размер блока, после которого при работе с FS-устройствами через HS-хабы начинается использование SPLIT-транзакций. Что это такое можно посмотреть в стандарте, там этому посвящён не один десяток страниц. Но если коротко: до хаба запрос идёт на скорости HS, а уже хаб обеспечивает снижение скорости и нарезание данных на FS-блоки. Основная шина при этом не тормозит.

Выше мы видели, что уже через 5 микросекунд после прихода примитива ACK, пошёл следующий пакет, который не был обработан контроллером. А что будет, если мы будем работать блоками по 64 байта? Я начну с примитива SOF.
Смотреть код.
*** +1000 usSOF 42.0 (0x2a)A5 2A 50 *** +3 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 *** +45 usACKD2 *** +37 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA14B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2 *** +43 usOUT Addr: 29 (0x1d), EP: 1E1 9D F0 *** +3 usDATA0C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*** +45 usACKD2


Вот и разгадка! Как видим, быстрее не всегда лучше. Время между пакетами больше. Особенность работы хоста такая.

По этой же причине, если мы воткнём между материнской платой и устройством дешёвый USB2-хаб, марку которого я не скажу, так как сильно поругался с магазином, владеющим именем бренда, но судя по ID, чип там VID_05E3&PID_0608, то статистика окажется намного лучше, чем при прямом подключении к материнке:



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

Пробуем двухбуферную систему


Будучи опытным программистом для микроконтроллеров, я знаю, что обычно такая проблема решается путём применения двухбуферной схемы. Пока обрабатывается один буфер, данные передаются во второй. А какая схема используется здесь? Ответ на мой вопрос мы получим из следующего кода:
USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev){  /* USER CODE BEGIN EndPoint_Configuration */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);  /* USER CODE END EndPoint_Configuration */  /* USER CODE BEGIN EndPoint_Configuration_CDC */  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);  HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);

Тут, везде написано PCD_SNG_BUF. Вообще, в статье про DMA я уже рассуждал, что если разработчики этой библиотеки что-то не используют, значит и не стоит этого использовать. Но всё же, я попробовал заменить SNG_BUF на DBL_BUF. Результат остался прежним. Тогда я нашёл в сети следующее утверждение:

The USB peripheral's HAL driver (PCD) has a known limitation: it directly maps EPnR register numbers with endpoint addresses. This works if all OUT and IN endpoints with the same number (e.g. 0x01 and 0x81) are the same type, and not double buffered. However, isochronous endpoints have to be double buffered, therefore you have to use an endpoint number that's unused in the other direction. E.g. in your case, set AUDIO_OUT_EP1 to 0x01, and AUDIO_IN_EP1 to 0x82. (Or you can drop this USB stack altogether as I did.)


Попробовал разнести точки результат тот же. В общем, анализ показал, что для точек типа BULK это если и возможно, то только путём переписывания MiddleWare. Короче, не зря там везде одиночные буферы выбраны. Разработчики знали, что делают.

Тормоза в обработчике прерывания


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

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



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

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

Но и это ещё не всё! Ради всё тех же каждых 64 байт, мы вызываем функцию USBD_CDC_SetRxBuffer(), а затем USBD_CDC_ReceivePacket(), которая также расслоится на кучку вызовов, каждый из которых всего лишь чуть-чуть перетасовывает данные. Оцените стек возвратов, когда мы находимся в самом глубоком слое!



А если вы попробуете рассмотреть код функции USB_EPStartXfer(), то вам совсем станет грустно. А ведь всё это безобразие вызывается ради каждого блока в 64 байта. Вот так 64 байта пробежало начинается чехарда с многократной (правда, в нашем случае с отсутствующим функционалом однократной) пересылкой этих данных, а также с вызовом кучи слоёв. Некоторые из них тратят массу тактов процессора на свою важную работу, но некоторые просто слои. Они просто зачем-то перекладывают байтики из структуры в структуру и передают управление дальше. А такты на это всё равно расходуются!

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

Проверяем теорию осциллографом


Имея стек возвратов, а также известные времянки посылки транзакций, мы можем проверить всё с другой стороны, чтобы быть уверенными, что нигде не ошиблись. Давайте я на входе в прерывание буду взводить какой-либо порт, а на выходе сбрасывать. Как всегда, я буду делать это через библиотеку mcucpp Константина Чижова.

Объявим, скажем, ножку Pa0 для этой цели:
#include <iopins.h>typedef Mcucpp::IO::Pa0 oscOut;//PC13

За что люблю эту библиотеку, так за её простоту. Инициализация ножки выглядит так:
oscOut::ConfigPort::Enable();oscOut::SetDirWrite();

Включили тактирование аппаратных блоков, назначили ножку на выход. Собственно, всё.

И добавим пару строк в функцию обработки прерывания (первая взведёт ножку в единицу, вторая сбросит в ноль):


То же самое текстом.
void USB_LP_CAN1_RX0_IRQHandler(void){  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 0 */oscOut::Set();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 0 */  HAL_PCD_IRQHandler(&hpcd_USB_FS);  /* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 1 */oscOut::Clear();  /* USER CODE END USB_LP_CAN1_RX0_IRQn 1 */}


Типичный период следования прерываний от 70 до 100 микросекунд. При этом в прерывании мы находимся чуть меньше чем 19 микросекунд. На первом рисунке показан случай, когда данные идут помедленнее (расстояния велики), на втором побыстрее (расстояния меньше). Обе осциллограммы сняты при подключении через хаб.





А вот хорошая и не очень хорошая ситуации, снятые при прямом подключении к материнке. Собственно, на плохом варианте видно, что расстояния удваиваются. Один из пакетов уходит с NAKом, в прерывание мы не попадаем.





Осталось понять, как эти прерывания располагаются относительно USB-примитивов. В этом нам поможет Reference Manual. Момент прихода прерывания я выделил.



Вот теперь всё сходится. После формирования ACKа мы не готовы принимать новые пакеты на протяжении 18 микросекунд. Когда они приходят через 3-5 микросекунд (а именно это мы видим в текстовом логе анализатора выше для плохого случая), контроллер их просто игнорирует, посылая NAK. Когда через 30-40 (что мы наблюдаем в текстовом логе для случая хорошего, хотя, точно подойдёт любое значение, больше чем 19) обрабатывает.

Плюс из текстовых логов мы видим, что влево от прерывания около пятидесяти микросекунд занимает сама OUT-транзакция на аппаратном уровне. Кстати. У нас же скорость FS. Такие сигналы можно ловить обычным осциллографом. Давайте я добавлю активность на шине USB в виде голубого луча. Что получим?

Вот тут мне удалось поймать SOF (в правой части экрана). Видно, что сразу после него начинают идти данные. В конце кадра (то есть, от последнего пакета прошлого кадра до SOFа) имеется тишина (там по стандарту что-то такое требуется).



Вот такая картинка была, когда я получил производительность 814037 байт в секунду. Извините, но быстрее никак. Либо по шине идут данные, либо мы обрабатываем прерывание. Простоев нет!




Причём 64 байта, с учётом, что я передаю все нули, а значит там может быть вставленный бит это примерно 576 бит. При частоте 12 МГц их передача займёт 48 миллисекунд. То есть, когда между пакетами примерно по 50 миллисекунд, мы имеем дело с пределом скорости. Тут даже NAKов нет.




NAKи вот они, когда хост пытается что-то передавать, пока мы заняты, что совпадает с выводами, сделанными при анализе текстовых логов. Вот тут прямо видны два пакета, первый из которых начался, когда аппаратура ещё не готова его принимать.




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

Мне показалось, что там они решили проблему, полностью переделав логику работы Middleware USB. Они там рассуждают, что дико сократили время пребывания внутри прерывания. Но если честно, у меня не стояло задачи выжать из этого контроллера всё. Так что я даже сильно не вчитывался в доводы авторов. Правда, при работе через хаб всё попроще. Короче, результаты у китайских специалистов если и есть, они также будут работать только на определённой конфигурации, ведь на паузы от хаба повлиять не сможет никто.

Неожиданное следствие из снятой осциллограммы


И раз уж я снял осциллограммы, предлагаю посмотреть на процесс, когда пакеты идут по шине, с точки зрения не оптимизатора, а простого программиста. Вот осциллограмма. Единичное значение контроллер находится в обработчике прерывания. Все остальные задачи в это время не могут работать. Каково?



А при коротких пакетах, бывает и такое:



Правда, это только во время передачи данных. Если передача не идёт, мы видим только SOFы (на предыдущей осциллограмме короткая иголка слева это как раз SOF). Но когда идёт обмен данными, процентов 20 времени контроллер не готов нас обслуживать. Он находится в контексте прерывания. Так что если и обслужит, то только прерывания с бОльшим приоритетом.

Выводы


Ну что, пришла пора делать выводы. Как видим, контроллер STM32F103C8T6 не может выжать всю производительность даже из шины USB 2.0 FS.

Хорошо это или плохо? Ни то, ни другое. Есть тысяча и одна задача, где не надо гнаться за производительностью USB, и этот копеечный контроллер прекрасно с ними справляется. Вот там его и надо использовать. (Дополнение: пока статья дежала в столе, на Хабре появилась статья, что уже не копеечный. Цена у местных поставщиков, согласно той статье, выросла в 10 раз. Надеюсь, это временное явление.)

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

А для производительных вещей я в ближайшее время собираюсь изучить контроллер, у которого USB обрабатывается по стандарту EHCI. Там всё на дескрипторах. Заполнил адрес, длину Когда пришло прерывание данные уже готовы Надеюсь Если будет что-то интересное сделаю статью. А здесь сам подход к обработке приходящих данных (они помещаются в выделенную память, а затем программно изымаются оттуда, причём в контексте прерывания) не даёт развить высоких скоростей. По крайней мере, на кристалле F103.

Следующий вывод: добавленный в систему дешёвый USB-хаб даёт неожиданный прирост производительности. Это связано с тем, что он шлёт пакеты с паузами 20-25 микросекунд (в статье подтверждающий лог не приводится для экономии места, но его можно скачать здесь для самостоятельного изучения). Получаем грубо 20 микросекунд задержки, 50 микросекунд передачи. Итого 5/7 от полной производительности. Как раз 700-800 килобайт в секунду при теоретическом максимуме 1000-1100. Так что любой FS-контроллер, включённый через этот хаб, не сможет выдать больше.

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

И, наконец, мы выяснили, что для FS устройств на базе STM32 нет чёткого критерия оптимальной работы со стороны прикладной программы. Одна и та же система, в которую то добавляли, то исключали внешний USB-хаб, работала с максимальной производительностью либо при длине блока 64 байта (без хаба), либо более четырёх килобайт (с хабом). При разработке прикладных программ для PC, требующих высокой производительности, следует учитывать и этот аспект. Вплоть до калибровки параметров под конкретную конфигурацию оборудования.

Послесловие. А что там в режиме HS?


Ради интереса приведу результаты, снятые для HS-устройства на базе ПЛИС. Второй столбец скорость при прямом подключении к материнской плате, третий через тот самый дешёвый хаб. Мне уже интересно, почему там хаб даёт такую просадку, но с этим я разберусь как-нибудь в другой раз.



А пока статья лежала в столе, я взял типовой пример CDC-прошивки от NXP, немного доработал его (без доработки он зависнет при односторонней передаче), залил в плату Teensy 4.1 и снял метрики там. У него контроллер EHCI и скорость HS.



Причина та же самая. Аппаратура, вроде, позволяет поставить в очередь несколько запросов (как для асинхронной работы), но увы, программная часть это явно запрещает:
usb_status_t USB_DeviceCdcAcmRecv(class_handle_t handle, uint8_t ep, uint8_t *buffer, uint32_t length){    if (1U == cdcAcmHandle->bulkOut.isBusy)    {        return kStatus_USB_Busy;    }    cdcAcmHandle->bulkOut.isBusy = 1U;

Правда, там, за счёт резервов аппаратуры, удалось с минимальной доработкой улучшить графики до таких:



Но это уже тема для другой статьи.
Подробнее..

Из песочницы STM32 и LCD2004A без I2C интерфейса

11.10.2020 12:19:43 | Автор: admin
Недавно начал изучать STM32 контроллеры и понадобилось взаимодействие с LCD дисплеем. Из дисплеев нашел у себя только 2004A, причем без I2C интерфейса. О нем и пойдет речь в этой статье.

Для начала необходимо подключить дисплей к контроллеру. Подключаем по схеме:

image

PB0 PB7 выводы контроллера.

Назначение выводов дисплея :
Номер вывода Сигнал Назначение сигнала
1 GND Земля (общий провод)
2 VCC Питание + 5 В
3 VEE Управление контрастностью дисплея. Подключается средний вывод делителя напряжения. Обычно это подстроечный резистор 10-20 кОм, но я распаял на плате дисплея резисторы.
4 RS Выбор регистра: 0 регистр команд; 1 регистр данных.
5 R/W Направление передачи данных:
0 запись;
1 чтение.
Как правило чтение из дисплея не используется, поэтому сажаем вывод на землю.
6 EN Строб операции шины. При спадающем фронте данные, находящиеся на шине данных защелкиваются в регистр.
7 DB0 Младшие биты восьми битного режима. При четырех битном интерфейсе не используются и обычно сажаются на землю.
8 DB1
9 DB2
10 DB3
11 DB4 Старшие биты восьми битного режима или биты данных четырех битного интерфейса.
12 DB5
13 DB6
14 DB7
15 A Анод питания подсветки (+)
16 K Катод питания подсветки (-). Ток должен быть ограничен.


Итак, дисплей подключили. Самое время научить микроконтроллер работать с ним. Я решил создать свою библиотеку для того, чтобы можно было ее использовать в разных проектах. Она состоит из двух файлов lcd_20x4.h и lcd_20x4.c

Начнем с заголовочного файла.

#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_#define LCD_LCD_20X4_2004A_LCD_20X4_H_#include "stm32f1xx.h"#include "delay.h"

В начале подключаем файл библиотеки CMSIS stm32f1xx.h так как у меня камень STM32F103C8T6. Следующим включением подключаем файл delay.h это моя библиотека для работы с задержками на основе системного таймера. Здесь ее описывать не буду, вот ее код:

Файл delay.h
#ifndef DELAY_DELAY_H_#define DELAY_DELAY_H_#include "stm32f1xx.h"#define F_CPU 72000000UL#define US F_CPU/1000000#define MS F_CPU/1000#define SYSTICK_MAX_VALUE 16777215#define US_MAX_VALUE SYSTICK_MAX_VALUE/(US)#define MS_MAX_VALUE SYSTICK_MAX_VALUE/(MS)void delay_us(uint32_t us); // до 233 мксvoid delay_ms(uint32_t ms); // до 233 мсvoid delay_s(uint32_t s);#endif /* DELAY_DELAY_H_ */


Файл delay.с
#include "delay.h"/* Функции задержек на микросекунды и миллисекунды*/void delay_us(uint32_t us){ // до 233016 мксif (us > US_MAX_VALUE || us == 0)return;SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // запретить прерывания по достижении 0SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // ставим тактирование от процессораSysTick->LOAD = (US * us-1); // устанавливаем в регистр число от которого считатьSysTick->VAL = 0; // обнуляем текущее значение регистра SYST_CVRSysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // запускаем счетчикwhile(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // ждем установку флага COUNFLAG в регистре SYST_CSRSysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;// скидываем бит COUNTFLAGSysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // выключаем счетчик}void delay_ms(uint32_t ms){ // до 233 мсif(ms > MS_MAX_VALUE || ms ==0)return;SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;SysTick->LOAD = (MS * ms);SysTick->VAL = 0;SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;}void delay_s(uint32_t s){for(int i=0; i<s*5;i++) delay_ms(200);}


Дисплей 2004A основан на контроллере фирмы HITACHI HD44780. Поэтому заглянем в даташит на данный контроллер. В таблице 6 есть система команд, а так же тайминги выполнения этих команд.

image

Перепишем нужные команды в макроопределения в заголовочном файле:

// display commands#define CLEAR_DISPLAY 0x1#define RETURN_HOME 0x2#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift#define DISPLAY_ON 0xC // non cursor#define DISPLAY_OFF 0x8#define CURSOR_SHIFT_LEFT 0x10#define CURSOR_SHIFT_RIGHT 0x14#define DISPLAY_SHIFT_LEFT 0x18#define DISPLAY_SHIFT_RIGHT 0x1C#define DATA_BUS_4BIT_PAGE0 0x28#define DATA_BUS_4BIT_PAGE1 0x2A#define DATA_BUS_8BIT_PAGE0 0x38#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS#define SET_DDRAM_ADDRESS 0x80

Теперь необходимо настроить выводы контроллера для работы с дисплеем. Определяем положение битов в порте ODR контроллера. Следует обратить внимание на PIN_D4. У меня там прописан 10-й бит вместо 4. На моем контроллере не работает 4-й вывод. Не знаю с чем это связано, но в регистре ODR этот бит всегда единица, даже до начала инициализации тактирования контроллера. Не знаю с чем это связано, возможно камень не оригинальный.

// положение битов в порте ODR#define PIN_RS 0x1#define PIN_EN 0x2#define PIN_D7 0x80#define PIN_D6 0x40#define PIN_D5 0x20#define PIN_D4 0x400

Далее настраиваем управляющие регистры для выводов. Я решил это сделать в виде макросов препроцессора:

#define     LCD_PORT               GPIOB#defineLCD_ODR LCD_PORT->ODR#define     LCD_PIN_RS()     LCD_PORT->CRL &= ~GPIO_CRL_CNF0; \LCD_PORT->CRL |= GPIO_CRL_MODE0;    // PB0  выход тяни-толкай, частота 50 Мгц#define     LCD_PIN_EN()            LCD_PORT->CRL &= ~GPIO_CRL_CNF1;\ LCD_PORT->CRL |= GPIO_CRL_MODE1;        // PB1#define     LCD_PIN_D7()            LCD_PORT->CRL &= ~GPIO_CRL_CNF7;\ LCD_PORT->CRL |= GPIO_CRL_MODE7;          // PB7#define     LCD_PIN_D6()            LCD_PORT->CRL &= ~GPIO_CRL_CNF6;\ LCD_PORT->CRL |= GPIO_CRL_MODE6;       // PB6#define     LCD_PIN_D5()            LCD_PORT->CRL &= ~GPIO_CRL_CNF5;\ LCD_PORT->CRL |= GPIO_CRL_MODE5;         // PB5#define     LCD_PIN_D4()            LCD_PORT->CRH &= ~GPIO_CRH_CNF10;\ LCD_PORT->CRH |= GPIO_CRH_MODE10;         // PB10#define     LCD_PIN_MASK   (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экрана

В завершении заголовочного файла определяем функции работы с дисплеем:

void portInit(void); // инициализация ножек порта под экранvoid sendByte(char byte, int isData);void lcdInit(void); // инициализация дисплеяvoid sendStr(char *str, int row ); // вывод строки#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */

С заголовочным файлом закончили. Теперь напишем реализации функций в файле lcd_20x4.c
Первым делом нужно настроить выводы для работы с дисплеем. Это делает функция void portInit(void):

void portInit(void){//----------------------включаем тактирование порта----------------------------------------------------if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;else return;//--------------------- инициализация пинов для LCD-----------------------------------------------------LCD_PIN_RS();// макроопределения в заголовочном файлеLCD_PIN_EN();LCD_PIN_D7();LCD_PIN_D6();LCD_PIN_D5();LCD_PIN_D4();lcdInit(); // функция инициализации дисплеяreturn ;}

Что касается функции lcdInit() это функция инициализации дисплея. Напишем и ее. Она основана на блок-схеме инициализации дисплея из даташита:

image

//--------------------- инициализация дисплея-----------------------------------------------------------void lcdInit(void){delay_ms(15); // ждем пока стабилизируется питаниеsendByte(0x33, 0); // шлем в одном байте два 0011delay_us(100);sendByte(0x32, 0); // шлем в одном байте  00110010delay_us(40);sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 битdelay_us(40);sendByte(DISPLAY_OFF, 0); // выключаем дисплейdelay_us(40);sendByte(CLEAR_DISPLAY, 0); // очищаем дисплейdelay_ms(2);sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещаетсяdelay_us(40);sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсорdelay_us(40);return ;}

Функция инициализации использует функцию void sendByte(char byte, int isData). Напишем ее реализацию. Она основана на временной диаграмме из даташита:

image

void sendByte(char byte, int isData){//обнуляем все пины дисплеяLCD_ODR &= ~LCD_PIN_MASK;if(isData == 1) LCD_ODR |= PIN_RS; // если данные поднимаем RSelse LCD_ODR &= ~(PIN_RS);   // иначе скидываем RS      LCD_ODR |= PIN_EN; // поднимаем пин E// ставим старшую тетраду на портif(byte & 0x80) LCD_ODR |= PIN_D7;if(byte & 0x40) LCD_ODR |= PIN_D6;if(byte & 0x20) LCD_ODR |= PIN_D5;if(byte & 0x10) LCD_ODR |= PIN_D4;LCD_ODR &= ~PIN_EN; // сбрасываем пин ЕLCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);//обнуляем все пины дисплея кроме RS     LCD_ODR |= PIN_EN;// поднимаем пин E// ставим младшую тетраду на портif(byte & 0x8) LCD_ODR |= PIN_D7;if(byte & 0x4) LCD_ODR |= PIN_D6;if(byte & 0x2) LCD_ODR |= PIN_D5;if(byte & 0x1) LCD_ODR |= PIN_D4;LCD_ODR &= ~(PIN_EN);// сбрасываем пин Еdelay_us(40);return;}

Теперь мы умеем отсылать байт на дисплей по 4-битной шине. Этим байтом может быть как команда так и символ. Определяется передачей в функцию переменной isData. Пришло время научиться передавать строки.

Дисплей 2004A состоит из 4 строк по 20 символов, что отражается в названии. Дабы не усложнять функцию я не буду реализовывать обрезку строк до 20 символов. В функцию будем отправлять строку символов и строку в которой ее вывести.

Для отображения символа на экране нужно записать его в память DDRAM. Адресация DDRAM соответствует таблице:

image

void sendStr(char *str, int row ){char start_address;switch (row) {case 1:start_address = 0x0; // 1 строкаbreak;case 2:start_address = 0x40; // 2 строкаbreak;case 3:start_address = 0x14; // 3 строкаbreak;case 4:start_address = 0x54; // 4 строкаbreak;}sendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки  в DDRAMdelay_ms(4);while(*str != '\0'){// пока не встретили конец строкиsendByte(*str, 1);str++;}// while}

Вот и все, библиотека для дисплея готова. Теперь настало время ее использовать. В функции main() пишем:

portInit();// инициализация портов под дисплейsendStr("    HELLO, HABR", 1);sendStr("     powered by", 2);sendStr("   STM32F103C8T6", 3);sendStr("Nibiru", 4);

И получаем результат:

image

В заключение приведу полный листинг файлов:

lcd_20x4.h
#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_#define LCD_LCD_20X4_2004A_LCD_20X4_H_#include "stm32f1xx.h"#include "delay.h"// display commands#define CLEAR_DISPLAY 0x1#define RETURN_HOME 0x2#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift#define DISPLAY_ON 0xC // non cursor#define DISPLAY_OFF 0x8#define CURSOR_SHIFT_LEFT 0x10#define CURSOR_SHIFT_RIGHT 0x14#define DISPLAY_SHIFT_LEFT 0x18#define DISPLAY_SHIFT_RIGHT 0x1C#define DATA_BUS_4BIT_PAGE0 0x28#define DATA_BUS_4BIT_PAGE1 0x2A#define DATA_BUS_8BIT_PAGE0 0x38#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS#define SET_DDRAM_ADDRESS 0x80// положение битов в порте ODR#define PIN_RS 0x1#define PIN_EN 0x2#define PIN_D7 0x80#define PIN_D6 0x40#define PIN_D5 0x20#define PIN_D4 0x400#define     LCD_PORT               GPIOB#defineLCD_ODR LCD_PORT->ODR#define     LCD_PIN_RS()     LCD_PORT->CRL &= ~GPIO_CRL_CNF0; \LCD_PORT->CRL |= GPIO_CRL_MODE0;    // PB0  выход тяни-толкай, частота 50 Мгц#define     LCD_PIN_EN()            LCD_PORT->CRL &= ~GPIO_CRL_CNF1;\LCD_PORT->CRL |= GPIO_CRL_MODE1;        // PB1#define     LCD_PIN_D7()            LCD_PORT->CRL &= ~GPIO_CRL_CNF7;\LCD_PORT->CRL |= GPIO_CRL_MODE7;          // PB7#define     LCD_PIN_D6()            LCD_PORT->CRL &= ~GPIO_CRL_CNF6;\LCD_PORT->CRL |= GPIO_CRL_MODE6;       // PB6#define     LCD_PIN_D5()            LCD_PORT->CRL &= ~GPIO_CRL_CNF5;\LCD_PORT->CRL |= GPIO_CRL_MODE5;         // PB5#define     LCD_PIN_D4()            LCD_PORT->CRH &= ~GPIO_CRH_CNF10;\LCD_PORT->CRH |= GPIO_CRH_MODE10;         // PB10#define     LCD_PIN_MASK   (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экранаvoid portInit(void); // инициализация ножек порта под экранvoid sendByte(char byte, int isData);void lcdInit(void); // инициализация дисплеяvoid sendStr(char *str, int row ); // вывод строки#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */


lcd_20x4.c
#include "lcd_20x4.h"// посылка байта в порт LCDvoid sendByte(char byte, int isData){//обнуляем все пины дисплеяLCD_ODR &= ~LCD_PIN_MASK;if(isData == 1) LCD_ODR |= PIN_RS; // если данные ставмим RSelse LCD_ODR &= ~(PIN_RS);   // иначе скидываем RS// ставим старшую тетраду на портif(byte & 0x80) LCD_ODR |= PIN_D7;if(byte & 0x40) LCD_ODR |= PIN_D6;if(byte & 0x20) LCD_ODR |= PIN_D5;if(byte & 0x10) LCD_ODR |= PIN_D4;// поднимаем пин ELCD_ODR |= PIN_EN;LCD_ODR &= ~PIN_EN; // сбрасываем пин Е//обнуляем все пины дисплея кроме RSLCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);// ставим младшую тетраду на портif(byte & 0x8) LCD_ODR |= PIN_D7;if(byte & 0x4) LCD_ODR |= PIN_D6;if(byte & 0x2) LCD_ODR |= PIN_D5;if(byte & 0x1) LCD_ODR |= PIN_D4;// поднимаем пин ELCD_ODR |= PIN_EN;//delay_us(10);// сбрасываем пин ЕLCD_ODR &= ~(PIN_EN);delay_us(40);return;}// функция тактирует порт под дисплей и задает пины на выход тяни толкай и частоту 50 Мгцvoid portInit(void){//----------------------включаем тактирование порта----------------------------------------------------if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;else return;//--------------------- инициализация пинов для LCD-----------------------------------------------------LCD_PIN_RS();LCD_PIN_EN();LCD_PIN_D7();LCD_PIN_D6();LCD_PIN_D5();LCD_PIN_D4();lcdInit();return ;}//--------------------- инициализация дисплея-----------------------------------------------------------void lcdInit(void){delay_ms(15); // ждем пока стабилизируется питаниеsendByte(0x33, 0); // шлем в одном байте два 0011delay_us(100);sendByte(0x32, 0); // шлем в одном байте  00110010delay_us(40);sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 битdelay_us(40);sendByte(DISPLAY_OFF, 0); // выключаем дисплейdelay_us(40);sendByte(CLEAR_DISPLAY, 0); // очищаем дисплейdelay_ms(2);sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещаетсяdelay_us(40);sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсорdelay_us(40);return ;}void sendStr(char *str, int row ){char start_address;switch (row) {case 1:start_address = 0x0; // 1 строкаbreak;case 2:start_address = 0x40; // 2 строкаbreak;case 3:start_address = 0x14; // 3 строкаbreak;case 4:start_address = 0x54; // 4 строкаbreak;}sendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки  в DDRAMdelay_ms(4);while(*str != '\0'){sendByte(*str, 1);str++;//delay_ms(100);}// while}

Подробнее..

STM32 и бесконтактный датчик температуры MLX90614. Подключение по I2C

18.11.2020 22:19:29 | Автор: admin

Датчик MLX90614 - это датчик с бесконтактным считыванием температуры объекта посредством приема и преобразования инфракрасного излучения. Он умеет работать в трех режимах: термостат, ШИМ выход и SMBus. В режиме термостат датчику не требуется контроллер, он просто держит температуру в заданных пределах, управляя драйвером нагрузки открытым стоком. В режиме ШИМ на выходе датчика появляется сигнал ШИМ, скважность которого зависит от температуры. В целях подключения к контроллеру наиболее интересен режим SMBus. Так как этот протокол электрически и сигнально совместим с I2C мы будем работать с датчиком, используя аппаратный I2C. О нем и пойдет речь в данной статье. Все режимы датчика настраиваются записью в определенные ячейки EEPROM. По умолчанию датчик находится в режиме SMBus.


Внешний вид и схема подключения

В подключении датчика нет ничего сложного. У меня есть плата "Синяя таблетка" с контроллером STM32F103C8T6 на борту, вот к ней и будем подключать датчик. У этого контроллера 2 аппаратных интерфейса I2C. Для датчика будет использоваться первый на выводах по умолчанию. Это PB6 - SCL, PB7 - SDA. При подключении необходимо не забыть подтянуть эти выводы к питанию внешними резисторами, у меня их сопротивление 4.7 кОм.

Программная часть

Весь код я решил оформить в виде библиотеки, состоящей из двух файлов: mlx90614.h и mlx90614.c . Также в проекте используется библиотека системного таймера для задержек и библиотека LCD дисплея A2004 для вывода температуры. Они описаны в прошлой статье.

Начнем с заголовочного файла.

#ifndef I2C_DEVICES_I2C_H_#define I2C_DEVICES_I2C_H_#include "stm32f1xx.h"#include <stdio.h>#include "delay.h"#define F_APB1 36 // частота шины APB1#define TPCLK1 ( 1000/F_APB1 ) // период частоты APB1 ns. ~ 28#define CCR_VALUE ( 10000 /(TPCLK1 * 2 ) ) // значение регистра CCR Для 36 Мгц ~ 179#define TRISE_VALUE ( 1000 / TPCLK1)

В начале подключаем заголовочный файл для своего контроллера. У меня это stm32f1xx.h. Стандартная библиотека СИ stdio.h нужна для того, чтобы переводить дробные числа в char массив для вывода на LCD. delay.h - библиотека для организации задержек.

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

Далее идем в даташит и узнаем, что датчик имеет две разные памяти: RAM и EEPROM. RAM используется для считывания температуры. Внутренний процессор датчика считывает температуру с сенсоров, обрабатывает и кладет в ячейки RAM температуру в Кельвинах. В первых двух ячейках хранится "сырая температура". Я не понял что она из себя представляет. В следующих ячейках температура кристалла датчика, температура первого и второго сенсора. Датчики MLX90614 бывают с одним и двумя датчиками. У меня с одним, поэтому температура будет читаться с первого сенсора. В EEPROM конфигурируется работа датчика. Для удобства запишем адресацию памяти в заголовочном файле в виде макроопределений.

//---------------- RAM addresses --------------------------------------------#define MLX90614_RAW_IR_1 0x04// сырые данные с сенсоров#define MLX90614_RAW_IR_2 0x05#define MLX90614_TA 0x06// температура кристалла датчика#define MLX90614_TOBJ_1 0x07// температура с первого ИК сенсора#define MLX90614_TOBJ_2 0x08// температура со второго ИК сенсора//--------------- EEPROM addresses ------------------------------------------#define MLX90614_TO_MAX 0x00#define MLX90614_TO_MIN 0x01#define MLX90614_PWM_CTRL 0x02#define MLX90614_TA_RANGE 0x03#define MLX90614_EMISSIVITY 0x04#define MLX90614_CONFIG_REGISTER_1 0x05#define MLX90614_SMBUS_ADDRESS 0x0E // LSByte only#define MLX90614_ID_NUMBER_1 0x1C#define MLX90614_ID_NUMBER_2 0x1D#define MLX90614_ID_NUMBER_3 0x1E#define MLX90614_ID_NUMBER_4 0x1F

Также из документации к датчику переносим команды, с которыми он умеет работать.

//--------------- Commands ------------------------------------------------#define MLX90614_RAM_ACCESS 0 // доступ к RAM#define MLX90614_EEPROM_ACCESS 0x20 // доступ к EEPROM#define MLX90614_READ_FLAGS 0xF0 // чтение флагов#define MLX90614_ENTER_SLEEP_MODE 0xFF // режим сна#define MLX90614_READ 1// режим чтения из датчика#define MLX90614_WRITE 0// режим записи в датчик

С макроопределениями закончили. Теперь нужно определить функции, с помощью которых контроллер будет взаимодействовать с датчиком. Чтобы комфортно работать с датчиком нужно уметь считывать температуру и считывать и изменять адрес устройства, так как на шине I2C может быть несколько таких датчиков, а адрес по умолчанию у всех одинаковый - 5A. Я задумывал в своем устройстве использовать два таких датчика, но почитав форумы понял, что они для моих целей не подходят. Так как датчики уже были у меня я решил написать под них библиотеку на будущее.

Итак определяем функции:

void mlx90614Init( void );double getTemp_Mlx90614_Double( uint16_t address, uint8_t ram_address );void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf );uint16_t getAddrFromEEPROM( uint16_t address );int setAddrToEEPROM ( uint16_t address, uint16_t new_address );uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address );void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data );#endif /* I2C_DEVICES_I2C_H_ */

void mlx90614Init( void )

Инициализация I2C для работы с датчиком

double getTempMlx90614Double( uint16t address, uint8t ram_address )

Возвращает температуру в формате double приведенную к градусам Цельсия. Применяется, если нужна дальнейшая обработка численного значения. Принимает адрес датчика и адрес RAM памяти из которого читать данные. В зависимости от адреса RAM вернет температуру кристалла, сенсора 1 или 2.

void getTempMlx90614CharArray( uint16t address, uint8t ram_address, char* buf )

Аналогичная предыдущей функции, за исключение того, что возвращает значение в char массив, переданный по ссылке. Удобно применять для вывода температуры на LCD

uint16t getAddrFromEEPROM( uint16t address )

Возвращает адрес датчика, записанный в его EEPROM. Принимает текущий адрес датчика.

int setAddrToEEPROM ( uint16t address, uint16t new_address )

Записывает адрес датчика в EEPROM. Применяется для изменения адреса датчика.

uint16t readEEPROM( uint16t address, uint16t eepromaddress )

Универсальная функция чтения EEPROM

void writeEEPROM ( uint16t address, uint16t eepromaddress, uint16t data )

Универсальная функция записи в EEPROM

С заголовочным файлом закончили. Самое время написать реализации функций.

void mlx90614Init(void){ delay_ms(120);// на стабилизацию питания и переходные процессы в датчикеRCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// тактируем портRCC->APB1ENR |= RCC_APB1ENR_I2C1EN;// тактируем i2c1GPIOB->CRL |= GPIO_CRL_MODE6 | GPIO_CRL_MODE7;// выход 50 мгцGPIOB->CRL |= GPIO_CRL_CNF6 | GPIO_CRL_CNF7; // альтернативная ф-я открытый стокI2C1->CR2 &= ~I2C_CR2_FREQ; // скидываем биты частоты шины тактирования  APB1I2C1->CR2 |= F_APB1; // устанавливаем частоту шины APB1 от которой тактируется I2C модульI2C1->CR1 &= ~I2C_CR1_PE; // выключаем модуль I2C для настройки регистра CCRI2C1->CCR &= ~I2C_CCR_CCR;I2C1->CCR |=  CCR_VALUE;I2C1->TRISE |=  TRISE_VALUE;I2C1->CR1 |= I2C_CR1_ENPEC; // разрешаем отсылку PECI2C1->CR1 |= I2C_CR1_PE;// включаем модуль I2CI2C1->CR1 |= I2C_CR1_ACK; // разрешаем ACK}

Из комментариев в функции понятно что в ней делается. Единственное следует обратить внимание на F_APB1, CCR_VALUE и TRISE_VALUE они берутся из заголовочного файла, там и рассчитываются исходя из заданной частоты тактирования. Так же важно отключить модуль I2C перед настройкой регистра CCR ( это указано в документации на контроллер ) и разрешить ACK после запуска модуля I2C , иначе ACK работать не будет.

double getTemp_Mlx90614_Double( uint16_t address,uint8_t ram_address){ uint16_ttemp ; // температура uint8_ttemp_lsb ;// младшие биты температуры double temp_result ; // результирующая пересчитанная температура double temp_double ;// температура, приведенная к формату double address = address<<1; // сдвигаем на один влево (так датчик принимает           // адрес плюс 1-й бит запись/чтение) I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= ram_address;// передача адреса RAM  датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->CR1 |= I2C_CR1_START;// повторный старт while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_READ;// обращение к датчику для чтения while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; while(!(I2C1->SR1 & I2C_SR1_RXNE)){} temp_lsb = I2C1->DR;// читаем младший байт while(!(I2C1->SR1 & I2C_SR1_RXNE)){} temp = I2C1->DR;// читаем старший байт I2C1->CR1 |= I2C_CR1_STOP; temp = (temp & 0x007F) << 8;// удаляем бит ошибки, сдвигаем в старший байт temp |= temp_lsb; temp_double = (double) temp; // приводим к формату double temp_result =  ((temp_double * 0.02)- 0.01 );// умножаем на разрешение измерений temp_result = temp_result - 273.15; // и приводим к град. Цельсияreturn temp_result; }

Здесь следует обратить внимание, что адрес датчика сдвигается на 1 бит влево, а потом в первый бит адреса записывается 1 для чтения, 0 - для записи. В документации на датчик не очень очевидно освещен процесс передачи адреса по I2C. И там не понятно почему обращаемся по адресу 5A, а на временной диаграмме B4 для записи и B5 для чтения. Принимая во внимание тот факт, что мы сдвигаем адрес влево и прибавляем бит режима доступа, все встает на свои места. Еще есть одна тонкость. В старшем бите старшего байта передается бит ошибки. Его необходимо удалить перед дальнейшей обработкой, что мы и делаем перед сдвигом в старший байт - (temp & 0x007F).

Получить значение температуры конечно хорошо, но еще лучше вывести это значение на LCD, например. Для этого есть простенькая функция void getTempMlx90614CharArray, которая просто преобразует полученное значение из предыдущей функции в char массив, используя для этого функцию стандартной библиотеки СИ sprintf(), которая объявлена в файле stdio.h

void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf){double t;t = getTemp_Mlx90614_Double(address,ram_address); sprintf(buf, "%.1f",t);return ;}

Общий алгоритм чтения из RAM датчика выглядит так:

  1. START

  2. Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи - 0

  3. Передаем адрес RAM откуда читать плюс команда доступа к RAM. Адресом RAM может быть температура кристалла датчика или температура одного из двух инфракрасных сенсоров.

  4. Повторный START

  5. Передаем адрес датчика сдвинутый на 1 влево плюс бит чтения.

  6. Читаем младший байт

  7. Читаем старший байт

  8. STOP

Теперь мы умеем читать температуру из датчика. Осталось реализовать возможность менять адрес, чтобы можно было вешать несколько датчиков на шину. Но перед этим напишем две вспомогательные функции для работы с EEPROM для записи и чтения.

Алгоритм чтения из EEPROM выглядит следующим образом:

  1. START

  2. Передаем адрес датчика, сдвинутый на 1 бит влево плюс бит записи (первый). Для записи - 0

  3. Передаем адрес EEPROM откуда читать плюс команда доступа к EEPROM ( определена в заголовочном файле )

  4. Повторный START

  5. Передаем адрес датчика плюс бит чтения

  6. Читаем младший байт

  7. Читаем старший байт

  8. STOP

Функция чтения из EEPROM

uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address ){ uint16_tdata_msb; uint16_tdata_lsb; uint16_t data_result; address = address<<1; // сдвигаем на один влево (так датчик принимает адрес + 1 бит чтение/запись)  I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;// передача адреса EEPROM датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->CR1 |= I2C_CR1_START;// повторный старт while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_READ;// обращение к датчику для чтения while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; //I2C1->CR1 &= ~I2C_CR1_ACK; while (!(I2C1->SR1 & I2C_SR1_RXNE)){}; data_lsb = I2C1->DR;// читаем младший байт while(!(I2C1->SR1 & I2C_SR1_RXNE)){} data_msb = I2C1->DR;// читаем старший байт I2C1->CR1 |= I2C_CR1_STOP;data_result = ((data_msb << 8) | data_lsb) ; return data_result;}

Чтение из EEPROM осуществляется по аналогичному алгоритму чтения из RAM. Разница только в командах выбора памяти и адресах этой памяти.

С записью немного иначе. Алгоритм следующий:

  1. START

  2. Передаем адрес датчика, сдвинутый влево плюс бит записи

  3. Передаем адрес EEPROM плюс команда выбора EEPROM памяти

  4. Передаем младший байт

  5. Передаем старший байт

  6. Передаем PEC (байт контрольной суммы )

  7. STOP

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

Функция записи в EEPROM

void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data ){ address = address<<1; // сдвигаем на один влево (т.к. датчик принимает адрес + 1 бит чтение/запись)  I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;// передача адреса EEPROM датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->DR =  ( uint8_t ) ( data & 0x00FF );// пишем младший байт while(!(I2C1->SR1 & I2C_SR1_BTF)){} I2C1->DR = ( uint8_t ) ( data >> 8 );// пишем старший байт while(!(I2C1->SR1 & I2C_SR1_BTF)){} I2C1->CR1 |= I2C_CR1_PEC;// посылаем PEC I2C1->CR1 |= I2C_CR1_STOP; return ;}

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

Функция чтения адреса датчика

uint16_t getAddrFromEEPROM ( uint16_t address ){uint16_t addr_eeprom;addr_eeprom = readEEPROM( address, MLX90614_SMBUS_ADDRESS );return addr_eeprom;}

Тут все просто. Функция принимает текущий адрес датчика, читает с помощью readEEPROM() текущий адрес из EEPROM и возвращает его.

С записью нового адреса в EEPROM немного сложнее. Даташит на MLX90614 рекомендует следующий алгоритм записи в EEPROM:

  1. Включение питания

  2. Запись в ячейку нулей, тем самым эффективно стирая ее

  3. Ждем 10 миллисекунд

  4. Пишем новое значение ячейки

  5. Ждем еще 10 миллисекунд

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

  7. Выключаем питание

От себя дополню. Новый адрес датчика будет использоваться только после выключения и повторного включения питания датчика. В связи с этим необходимо предусмотреть отдельный пин контроллера, который будет управлять включением и выключением датчика через, например, транзистор. Здесь возникает вопрос - зачем вообще нужен режим сна, если все равно для работы с EEPROM необходимо управлять питанием датчика? Не проще ли тогда просто отключать питание? Поэтому я не стал реализовывать режим сна, так как в таких обстоятельствах он не имеет смысла. Так же в этой статье я не рассматриваю управление питанием, так как реализовать его не сложно в реальном проекте.

Функция записи в EEPROM

int setAddrToEEPROM ( uint16_t address, uint16_t new_address ){uint16_t addr;writeEEPROM ( address, MLX90614_SMBUS_ADDRESS, 0x0); // стираем ячейкуdelay_ms(10);writeEEPROM (address, MLX90614_SMBUS_ADDRESS, new_address ); // пишем новый адресdelay_ms(10);addr = readEEPROM ( address, MLX90614_SMBUS_ADDRESS ); // читаем для сравненияif ( addr == new_address){return 1;}else return 0;}

И наконец пришло время опробовать работу библиотеки. Для этого пишем небольшой скетч в main() функции проекта

#include "main.h"#include <stdio.h>int main (void){clk_ini(); // запускаем тактирование переферииlcd_2004a_init(); // инициализация дисплея a2004mlx90614Init(); // инициализация I2C для датчикаuint16_t geted_eeprom_address;char char_eeprom_address[20];char crystal_temp[10];   // массив для строки температурыchar first_sensor_temp[10];// читаем адрес датчика из EEPROM и выводим на LCDgeted_eeprom_address = getAddrFromEEPROM( 0x5A );sprintf(char_eeprom_address, "%x", (uint8_t) geted_eeprom_address);sendStr("addr value:", 3, 0);sendStr (char_eeprom_address, 3, 14 );setAddrToEEPROM (0x5A , 0xA); // записываем новый адрес// снова читаем адрес и выводим на LCDgeted_eeprom_address = getAddrFromEEPROM( 0x5A );sprintf(char_eeprom_address, "%x", (uint8_t) geted_eeprom_address);sendStr("new addr :", 4, 0);sendStr (char_eeprom_address, 4, 14 );while(1){// читаем и выводим температуру кристалла и сенсора датчика getTemp_Mlx90614_CharArray ( 0x5A,  MLX90614_TA, crystal_temp ); sendStr( "Crystal Temp :", 1, 0 ); sendStr( crystal_temp, 1, 14 );    delay_s(1);     getTemp_Mlx90614_CharArray ( 0x5A,  MLX90614_TOBJ_1, first_sensor_temp ); sendStr( "Sensor Temp  :", 2, 0 ); sendStr( first_sensor_temp, 2, 14 );    delay_s(1);}}

В main.h подключаем

#ifndef CORE_INC_MAIN_H_#define CORE_INC_MAIN_H_#include "stm32f1xx.h"#include "clk_ini.h" // тактирование контроллера#include "delay.h"// функции задержки#include "lcd_20x4.h" // функции для работы с LCD A2004#include "mlx90614.h" // функции работы с датчиком#endif /* CORE_INC_MAIN_H_ */

У меня получилось вот так

В заключение полный листинги проекта

mlx90614.h
#ifndef I2C_DEVICES_I2C_H_#define I2C_DEVICES_I2C_H_#include "stm32f1xx.h"#include <stdio.h>#include "delay.h"#define F_APB1 36 // частота шины APB1#define TPCLK1 ( 1000/F_APB1 ) // период частоты APB1 ns. ~ 28#define CCR_VALUE ( 10000 /(TPCLK1 * 2 ) ) // значение регистра CCR Для 36 Мгц ~ 179#define TRISE_VALUE ( 1000 / TPCLK1)//---------------- RAM addresses --------------------------------------------#define MLX90614_RAW_IR_1 0x04// сырые данные с сенсоров#define MLX90614_RAW_IR_2 0x05#define MLX90614_TA 0x06// температура кристалла датчика#define MLX90614_TOBJ_1 0x07// температура с первого ИК сенсора#define MLX90614_TOBJ_2 0x08// температура со второго ИК сенсора//--------------- EEPROM addresses ------------------------------------------#define MLX90614_TO_MAX 0x00#define MLX90614_TO_MIN 0x01#define MLX90614_PWM_CTRL 0x02#define MLX90614_TA_RANGE 0x03#define MLX90614_EMISSIVITY 0x04#define MLX90614_CONFIG_REGISTER_1 0x05#define MLX90614_SMBUS_ADDRESS 0x0E // LSByte only#define MLX90614_ID_NUMBER_1 0x1C#define MLX90614_ID_NUMBER_2 0x1D#define MLX90614_ID_NUMBER_3 0x1E#define MLX90614_ID_NUMBER_4 0x1F//--------------- Commands ------------------------------------------------#define MLX90614_RAM_ACCESS 0 // доступ к RAM#define MLX90614_EEPROM_ACCESS 0x20 // доступ к EEPROM#define MLX90614_READ_FLAGS 0xF0 // чтение флагов#define MLX90614_ENTER_SLEEP_MODE 0xFF // режим сна#define MLX90614_READ 1// режим чтения из датчика#define MLX90614_WRITE 0// режим записи в датчикvoid mlx90614Init( void );double getTemp_Mlx90614_Double( uint16_t address, uint8_t ram_address );void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf );uint16_t getAddrFromEEPROM( uint16_t address );int setAddrToEEPROM ( uint16_t address, uint16_t new_address );uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address );void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data );#endif /* I2C_DEVICES_I2C_H_ */
mlx90614.c
#include "mlx90614.h"/******************************************************************************************** * Функция инициализирует I2C интерфейс для работы с датчиком MLX90614   * *  * ********************************************************************************************/ void mlx90614Init(void){ delay_ms(120);// на стабилизацию питания и переходные процессы в датчикеRCC->APB2ENR |= RCC_APB2ENR_IOPBEN;// тактируем портRCC->APB1ENR |= RCC_APB1ENR_I2C1EN;// тактируем i2c1GPIOB->CRL |= GPIO_CRL_MODE6 | GPIO_CRL_MODE7;// выход 50 мгцGPIOB->CRL |= GPIO_CRL_CNF6 | GPIO_CRL_CNF7; // альтернативная ф-я открытый стокI2C1->CR2 &= ~I2C_CR2_FREQ; // скидываем биты частоты шины тактирования  APB1I2C1->CR2 |= F_APB1; // устанавливаем частоту шины APB1 от которой тактируется I2C модульI2C1->CR1 &= ~I2C_CR1_PE; // выключаем модуль I2C для настройки регистра CCRI2C1->CCR &= ~I2C_CCR_CCR;I2C1->CCR |=  CCR_VALUE;I2C1->TRISE |=  TRISE_VALUE;I2C1->CR1 |= I2C_CR1_ENPEC; // разрешаем отсылку PECI2C1->CR1 |= I2C_CR1_PE;// включаем модуль I2CI2C1->CR1 |= I2C_CR1_ACK; // разрешаем ACK}/******************************************************************************************** * Функция возвращает значение температуры в град. Цельсия и типом double. * *   * * Входные данные:    * * address - адрес датчика MLX90614   * *    * * ram_address RAM-адрес для чтения ( см. константы в .h файле ) :    * *    * * MLX90614_TA - температура кристалла датчика   * * MLX90614_TOBJ_1 - температура первого ИК сенсора   * * MLX90614_TOBJ_2 - температура второго ИК сенсора  *  *******************************************************************************************/double getTemp_Mlx90614_Double( uint16_t address,uint8_t ram_address){ uint16_ttemp ; // температура uint8_ttemp_lsb ;// младшие биты температуры double temp_result ; // результирующая пересчитанная температура double temp_double ;// температура, приведенная к формату double address = address<<1; // сдвигаем на один влево (так датчик принимает           // адрес плюс 1-й бит запись/чтение) I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= ram_address;// передача адреса RAM  датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->CR1 |= I2C_CR1_START;// повторный старт while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_READ;// обращение к датчику для чтения while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; while(!(I2C1->SR1 & I2C_SR1_RXNE)){} temp_lsb = I2C1->DR;// читаем младший байт while(!(I2C1->SR1 & I2C_SR1_RXNE)){} temp = I2C1->DR;// читаем старший байт I2C1->CR1 |= I2C_CR1_STOP; temp = (temp & 0x007F) << 8;// удаляем бит ошибки, сдвигаем в старший байт temp |= temp_lsb; temp_double = (double) temp; // приводим к формату double temp_result =  ((temp_double * 0.02)- 0.01 );// умножаем на разрешение измерений temp_result = temp_result - 273.15; // и приводим к град. Цельсияreturn temp_result; }/******************************************************************************************** * Функция записывает в, переданный по ссылке, массив типа char температуру в град. Цельсия * * * * Входные данные: * * address - адрес датчика MLX90614* * * * ram_address RAM-адрес для чтения ( см. константы в .h файле ) : * * * * MLX90614_TA - температура кристалла датчика* * MLX90614_TOBJ_1 - температура первого ИК сенсора* * MLX90614_TOBJ_2 - температура второго ИК сенсора* * * * *buf - ссылка на массив* *******************************************************************************************/void getTemp_Mlx90614_CharArray( uint16_t address, uint8_t ram_address, char* buf){double t;t = getTemp_Mlx90614_Double(address,ram_address); sprintf(buf, "%.1f",t);return ;}/******************************************************************************************** * Чтение  EEPROM датчика по произвольному адресу* * Входные данные:* * address - адрес датчика* * eeprom_address - адрес в EEPROM* * * * Выходные данные:* * значение в ячейке EEPROM формат uint16_t* ** * ******************************************************************************************/uint16_t readEEPROM( uint16_t address, uint16_t eeprom_address ){ uint16_tdata_msb; uint16_tdata_lsb; uint16_t data_result; address = address<<1; // сдвигаем на один влево (так датчик принимает адрес + 1 бит чтение/запись)  I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;// передача адреса EEPROM датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->CR1 |= I2C_CR1_START;// повторный старт while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_READ;// обращение к датчику для чтения while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; //I2C1->CR1 &= ~I2C_CR1_ACK; while (!(I2C1->SR1 & I2C_SR1_RXNE)){}; data_lsb = I2C1->DR;// читаем младший байт while(!(I2C1->SR1 & I2C_SR1_RXNE)){} data_msb = I2C1->DR;// читаем старший байт I2C1->CR1 |= I2C_CR1_STOP;data_result = ((data_msb << 8) | data_lsb) ;//& 0x1F; return data_result;}/******************************************************************************************** * Запись в EEPROM по произвольному адресу     * *     * * Входные данные:    * * address - адрес датчика    * * eeprom_address - адрес в EEPROM    * * data - данные    * ********************************************************************************************/void writeEEPROM ( uint16_t address, uint16_t eeprom_address, uint16_t data ){ address = address<<1; // сдвигаем на один влево (т.к. датчик принимает адрес + 1 бит чтение/запись)  I2C1->CR1 |= I2C_CR1_START; while (!(I2C1->SR1 & I2C_SR1_SB)){} (void) I2C1->SR1; I2C1->DR = address | MLX90614_WRITE; // режим записи , передача адреса MLX90614 while (!(I2C1->SR1 & I2C_SR1_ADDR)){} (void) I2C1->SR1; (void) I2C1->SR2; I2C1->DR= eeprom_address | MLX90614_EEPROM_ACCESS;// передача адреса EEPROM датчика MLX90614 while (!(I2C1->SR1 & I2C_SR1_TXE)){} I2C1->DR =  ( uint8_t ) ( data & 0x00FF );// пишем младший байт while(!(I2C1->SR1 & I2C_SR1_BTF)){} I2C1->DR = ( uint8_t ) ( data >> 8 );// пишем старший байт while(!(I2C1->SR1 & I2C_SR1_BTF)){} I2C1->CR1 |= I2C_CR1_PEC;// посылаем PEC I2C1->CR1 |= I2C_CR1_STOP; return ;}/******************************************************************************************** * Чтение адреса датчика из EEPROM* * * * Входные данные:* * address - адрес датчика* ** * Выходные данные:* * адрес в формате uint8_t* *  * *******************************************************************************************/uint16_t getAddrFromEEPROM ( uint16_t address ){uint16_t addr_eeprom;addr_eeprom = readEEPROM( address, MLX90614_SMBUS_ADDRESS );return addr_eeprom;}/******************************************************************************************** * Запись нового адреса датчика в EEPROM* * * * Входные данные:* * address - текущий адрес * * new_address -новый адресс* * * * Возвращает 1 - успешно/ 0 - неудача* ********************************************************************************************/int setAddrToEEPROM ( uint16_t address, uint16_t new_address ){uint16_t addr;writeEEPROM ( address, MLX90614_SMBUS_ADDRESS, 0x0); // стираем ячейкуdelay_ms(10);writeEEPROM (address, MLX90614_SMBUS_ADDRESS, new_address ); // пишем новый адресdelay_ms(10);addr = readEEPROM ( address, MLX90614_SMBUS_ADDRESS ); // читаем для сравненияif ( addr == new_address){return 1;}else return 0;}
clk_ini.h
#ifndef INC_CLK_INI_H_#define INC_CLK_INI_H_#include "stm32f1xx.h"int clk_ini(void);#endif /* INC_CLK_INI_H_ */
clk_ini.c
#include "clk_ini.h"int clk_ini(void){RCC->CR |= (1 << RCC_CR_HSEON_Pos);__IO int startCounter;for(startCounter = 0; ; startCounter++){if(RCC->CR & (1 << RCC_CR_HSERDY_Pos)){break;}// ifif(startCounter > 0x1000){RCC->CR &= ~(1 << RCC_CR_HSEON_Pos);return 1;}}// forRCC->CFGR |= (0x07 << RCC_CFGR_PLLMULL_Pos) // PLL x9  |(0x01 << RCC_CFGR_PLLSRC_Pos); // start clocking PLL of HSERCC->CR |= (1 << RCC_CR_PLLON_Pos);for(startCounter = 0; ; startCounter++){if(RCC->CR & (1 << RCC_CR_PLLRDY_Pos)){break;}//ifif(startCounter > 0x1000){RCC->CR &= ~(1 << RCC_CR_HSEON_Pos);RCC->CR &= ~(1 << RCC_CR_PLLON_Pos);return 2;}// if}// for////////////////////////////////////////////////////////////  //Настраиваем FLASH и делители  ////////////////////////////////////////////////////////////  //Устанавливаем 2 цикла ожидания для Flash  //так как частота ядра у нас будет 48 MHz < SYSCLK <= 72 MHz  FLASH->ACR |= (0x02<<FLASH_ACR_LATENCY_Pos);  //Делители  RCC->CFGR |= (0x00<<RCC_CFGR_PPRE2_Pos) //Делитель шины APB2 равен 1            | (0x04<<RCC_CFGR_PPRE1_Pos) //Делитель нишы APB1 равен 2            | (0x00<<RCC_CFGR_HPRE_Pos); //Делитель AHB отключен  RCC->CFGR |= (0x02<<RCC_CFGR_SW_Pos); //Переключаемся на работу от PLL  //Ждем, пока переключимся  while((RCC->CFGR & RCC_CFGR_SWS_Msk) != (0x02<<RCC_CFGR_SWS_Pos))  {  }  //После того, как переключились на  //внешний источник такирования  //отключаем внутренний RC-генератор  //для экономии энергии  RCC->CR &= ~(1<<RCC_CR_HSION_Pos);  //Настройка и переклбючение сисемы  //на внешний кварцевый генератор  //и PLL запершилось успехом.  //Выходимreturn 0;}
delay.h
#ifndef DELAY_DELAY_H_#define DELAY_DELAY_H_#include "stm32f1xx.h"#define F_CPU 72000000UL#define US F_CPU/1000000#define MS F_CPU/1000#define SYSTICK_MAX_VALUE 16777215#define US_MAX_VALUE SYSTICK_MAX_VALUE/(US)#define MS_MAX_VALUE SYSTICK_MAX_VALUE/(MS)void delay_us(uint32_t us); // до 233 мксvoid delay_ms(uint32_t ms); // до 233 мсvoid delay_s(uint32_t s);
delay.c
#include "delay.h"/* Функции задержек на микросекунды и миллисекунды*/void delay_us(uint32_t us){ // до 233016 мксif (us > US_MAX_VALUE || us == 0)return;SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // запретить прерывания по достижении 0SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // ставим тактирование от процессораSysTick->LOAD = (US * us-1); // устанавливаем в регистр число от которого считатьSysTick->VAL = 0; // обнуляем текущее значение регистра SYST_CVRSysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // запускаем счетчикwhile(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)); // ждем установку флага COUNFLAG в регистре SYST_CSRSysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;// скидываем бит COUNTFLAGSysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // выключаем счетчикreturn;}void delay_ms(uint32_t ms){ // до 233 мсif(ms > MS_MAX_VALUE || ms ==0)return;SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;SysTick->LOAD = (MS * ms);SysTick->VAL = 0;SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;while(!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk));SysTick->CTRL &= ~SysTick_CTRL_COUNTFLAG_Msk;SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;return;}void delay_s(uint32_t s){for(int i=0; i<s*5;i++) delay_ms(200);return;}
lcd_20x4.h
#ifndef LCD_LCD_20X4_2004A_LCD_20X4_H_#define LCD_LCD_20X4_2004A_LCD_20X4_H_#include "stm32f1xx.h"#include "delay.h"// display commands#define CLEAR_DISPLAY 0x1#define RETURN_HOME 0x2#define ENTRY_MODE_SET 0x6 // mode cursor shift rihgt, display non shift#define DISPLAY_ON 0xC // non cursor#define DISPLAY_OFF 0x8#define CURSOR_SHIFT_LEFT 0x10#define CURSOR_SHIFT_RIGHT 0x14#define DISPLAY_SHIFT_LEFT 0x18#define DISPLAY_SHIFT_RIGHT 0x1C#define DATA_BUS_4BIT_PAGE0 0x28#define DATA_BUS_4BIT_PAGE1 0x2A#define DATA_BUS_8BIT_PAGE0 0x38#define SET_CGRAM_ADDRESS 0x40 // usage address |= SET_CGRAM_ADDRESS#define SET_DDRAM_ADDRESS 0x80// положение битов в порте ODR#define PIN_RS 0x1#define PIN_EN 0x2#define PIN_D4 0x1000#define PIN_D5 0x2000#define PIN_D6 0x4000#define PIN_D7 0x8000#define     LCD_PORT               GPIOB#defineLCD_ODR LCD_PORT->ODR#define     LCD_PIN_RS()     LCD_PORT->CRL |= GPIO_CRL_MODE0_0;\LCD_PORT->CRL &= ~GPIO_CRL_CNF0; // PB0  выход тяни-толкай, частота 10 Мгц#define     LCD_PIN_EN()            LCD_PORT->CRL |= GPIO_CRL_MODE1_0;\LCD_PORT->CRL &= ~GPIO_CRL_CNF1;        // PB1#define     LCD_PIN_D4()            LCD_PORT->CRH |= GPIO_CRH_MODE12_0;\LCD_PORT->CRH &= ~GPIO_CRH_CNF12;          // PB7#define     LCD_PIN_D5()           LCD_PORT->CRH |= GPIO_CRH_MODE13_0;\LCD_PORT->CRH &= ~GPIO_CRH_CNF13;      // PB6#define     LCD_PIN_D6()            LCD_PORT->CRH |= GPIO_CRH_MODE14_0;\LCD_PORT->CRH &= ~GPIO_CRH_CNF14;         // PB5#define     LCD_PIN_D7()            LCD_PORT->CRH |= GPIO_CRH_MODE15_0;\LCD_PORT->CRH &= ~GPIO_CRH_CNF15;         // PB10#define     LCD_PIN_MASK   (PIN_RS | PIN_EN | PIN_D7 | PIN_D6 | PIN_D5 | PIN_D4) // 0b0000000011110011 маска пинов для экранаvoid lcd_2004a_init(void); // инициализация ножек порта под экранvoid sendByte(char byte, int isData);void sendStr(char *str, int row , int position); // вывод строки#endif /* LCD_LCD_20X4_2004A_LCD_20X4_H_ */
lcd_20x4.c
#include "lcd_20x4.h"// посылка байта в порт LCDvoid lcdInit(void); // инициализация дисплеяvoid sendByte(char byte, int isData){//обнуляем все пины дисплеяLCD_ODR &= ~LCD_PIN_MASK;if(isData == 1) LCD_ODR |= PIN_RS; // если данные ставмим RSelse LCD_ODR &= ~(PIN_RS);   // иначе скидываем RS// поднимаем пин ELCD_ODR |= PIN_EN;// ставим старшую тетраду на портif(byte & 0x80) LCD_ODR |= PIN_D7;if(byte & 0x40) LCD_ODR |= PIN_D6;if(byte & 0x20) LCD_ODR |= PIN_D5;if(byte & 0x10) LCD_ODR |= PIN_D4;LCD_ODR &= ~PIN_EN; // сбрасываем пин Е//обнуляем все пины дисплея кроме RSLCD_ODR &= ~(LCD_PIN_MASK & ~PIN_RS);// поднимаем пин ELCD_ODR |= PIN_EN;// ставим младшую тетраду на портif(byte & 0x8) LCD_ODR |= PIN_D7;if(byte & 0x4) LCD_ODR |= PIN_D6;if(byte & 0x2) LCD_ODR |= PIN_D5;if(byte & 0x1) LCD_ODR |= PIN_D4;// сбрасываем пин ЕLCD_ODR &= ~(PIN_EN);delay_us(40);return;}// функция тактирует порт под дисплей и задает пины на выход тяни толкай и частоту 50 Мгцvoid lcd_2004a_init(void){//----------------------включаем тактирование порта----------------------------------------------------if(LCD_PORT == GPIOB) RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;else if (LCD_PORT == GPIOA) RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;else return;//--------------------- инициализация пинов для LCD-----------------------------------------------------LCD_PIN_RS();LCD_PIN_EN();LCD_PIN_D7();LCD_PIN_D6();LCD_PIN_D5();LCD_PIN_D4();lcdInit();return ;}//--------------------- инициализация дисплея-----------------------------------------------------------void lcdInit(void){delay_ms(200); // ждем пока стабилизируется питаниеsendByte(0x33, 0); // шлем в одном байте два 0011delay_us(120);sendByte(0x32, 0); // шлем в одном байте  00110010delay_us(40);sendByte(DATA_BUS_4BIT_PAGE0, 0); // включаем режим 4 битdelay_us(40);sendByte(DISPLAY_OFF, 0); // выключаем дисплейdelay_us(40);sendByte(CLEAR_DISPLAY, 0); // очищаем дисплейdelay_ms(2);sendByte(ENTRY_MODE_SET, 0); //ставим режим смещение курсора экран не смещаетсяdelay_us(40);sendByte(DISPLAY_ON, 0);// включаем дисплей и убираем курсорdelay_us(40);return ;}void sendStr( char *str, int row , int position ){char start_address;switch (row) {case 1:start_address = 0x0; // 1 строкаbreak;case 2:start_address = 0x40; // 2 строкаbreak;case 3:start_address = 0x14; // 3 строкаbreak;case 4:start_address = 0x54; // 4 строкаbreak;}start_address += position; // к началу строки прибавляем позицию в строкеsendByte((start_address |= SET_DDRAM_ADDRESS), 0); // ставим курсор на начало нужной строки  в DDRAMdelay_ms(4);while(*str != '\0'){sendByte(*str, 1);str++;}// while}
Подробнее..

USB на регистрах STM32L1 STM32F1

21.03.2021 16:08:22 | Автор: admin

Еще более низкий уровень (avr-vusb)
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: interrupt endpoint на примере HID
USB на регистрах: isochronous endpoint на примере Audio device

С программным USB на примере AVR мы уже познакомились, пришла пора взяться за более тяжелые камни stm32. Подопытными у нас будут классический STM32F103C8T6 а также представитель малопотребляющей серии STM32L151RCT6. Как и раньше, пользоваться покупными отладочными платами и HAL'ом не будем, отдав предпочтение велосипеду.

Раз уж в заглавии указано два контроллера, стоит рассказать об основных отличиях. В первую очередь это резистор подтяжки, говорящий usb-хосту, что в него что-то воткнули. В L151 он встроен и управляется битом SYSCFG_PMC_USB_PU, а в F103 нет, придется впаивать на плату снаружи и соединять либо с VCC, либо с ножкой контроллера. В моем случае под руку попалась ножка PA10. На которой висит UART1 А другой вывод UART1 конфликтует с кнопкой замечательную плату я развел, не находите? Второе отличие это объем флеш-памяти: в F103 ее 64 кБ, а в L151 целых 256 кБ, чем мы когда-нибудь и воспользуемся при изучении конечных точек типа Bulk. Также у них немного отличаются настройки тактирования, да и лампочками с кнопочками могут на разных ногах висеть, но это уже совсем мелочи. Пример для F103 доступен в репозитории, так что адаптировать под него остальные эксперименты с L151 будет несложно. Исходные коды доступны тут: github.com/COKPOWEHEU/usb

Общий принцип работы с USB


Работа с USB в данном контроллере предполагается с использованием аппаратного модуля. То есть мы ему говорим что делать, он делает и в конце дергает прерывание я готовое!. Соответственно, из основного main'а нам вызывать почти ничего не надо (хотя функцию usb_class_poll я на всякий случай предусмотрел). Обычный цикл работы ограничен единственным событием обмен данными. Остальные сброс, сон и прочие события исключительные, разовые.

В низкоуровневые подробности обмена я на этот раз углубляться не буду. Кому интересно, может почитать про vusb. Но напомню, что обмен обычными данными идет не по одному байту, а по пакету, причем направление передачи задает хост. И названия этих направлений диктует тоже он: IN передача означает что хост принимает данные (а устройство передает), а OUT что хост передает данные (а мы принимаем). Более того, каждый пакет имеет свой адрес номер конечной точки, с которой хост хочет общаться. Пока что у нас будет единственная конечная точка 0, отвечающая за устройство в целом (для краткости я еще буду называть ее ep0). Для чего нужны остальные я расскажу в других статьях. Согласно стандарту, размер ep0 составляет строго 8 байт для низкоскоростных устройств (к каковым относится все тот же vusb) и на выбор 8, 16, 32, 64 байта для полноскоростных вроде нашего.

А если данных слишком мало и они не заполняют буфер полностью? Тут все просто: помимо данных в пакете передается и их размер (это может быть поле wLength либо низкоуровневая комбинация сигналов SE0, обозначающая конец передачи), так что даже если нам надо через ep0 размером 64 байта передать три байта, то переданы будут именно три байта. В результате расходовать пропускную способность, гоняя ненужные нули, мы не будем. Так что не стоит мельчить: если можем себе позволить потратить 64 байта, тратим не раздумывая. Помимо прочего это несколько уменьшит загруженность шины, ведь передать за раз кусок в 64 байта (плюс все заголовки и хвосты) проще, чем 8 раз по 8 байт (к каждому из которых опять-таки заголовки и хвосты).

А если данных напротив слишком много? Тут сложнее. Данные приходится разбивать по размеру конечной точки и передавать порциями. Скажем, размер ep0 у нас 8 байт, а передать хост пытается 20 байт. При первом прерывании к нам придут байты 0-7, во втором 8-15, в третьем 16-20. То есть чтобы собрать посылку целиком нужно получить целых три прерывания. Для этого в том же HAL придуман хитрый буфер, с которым я попытался разобраться, но после четвертого уровня пересылки одного и того же между функциями, плюнул. Как результат, в моей реализации буферизация ложится на плечи программиста.

Но хост хотя бы всегда говорит сколько данных пытается передать. Когда же данные передаем мы, надо как-то хитро дернуть низкоуровневые состояния ножек чтобы дать понять что данные закончились. Точнее, дать понять модулю usb, что данные закончились и что надо дернуть ножки. Делается это вполне очевидным способом записью только части буфера. Скажем, если буфер у нас 8 байт, а мы записали 4, то очевидно, данных у нас всего 4 байта, после которых модуль пошлет волшебную комбинацию SE0 и все будут довольны. А если мы записали 8 байт, это значит что у нас всего 8 байт или что это только часть данных, которая влезла в буфер? Модуль usb считает что часть. Поэтому если мы хотим остановить передачу, то после записи 8-байтного буфера должны записать следом 0-байтный. Это называется ZLP, Zero Length Packet. Про то, как это выглядит в коде, я расскажу чуть позже.

Организация памяти


Согласно стандарту, размер конечной точки 0 может достигать 64 байт. Размер любой другой аж 1024 байт. Количество точек также может отличаться от устройства к устройству. Те же STM32L1 поддерживают до 7 точек на вход и 7 на выход (не считая ep0), то есть до 14 кБ одних только буферов. Которые в таком объеме скорее всего никому никогда не понадобятся. Непозволительный расход памяти! Вместо этого модуль usb отгрызает себе кусок общей памяти ядра и пользуется ей. Эта область называется PMA (packet memory area) и начинается с USB_PMAADDR. А чтобы указать где внутри нее располагаются буферы каждой конечной точки, в начале выделен массив на 8 элементов каждый со следующей структурой, и только потом собственно область для данных:
typedef struct{    volatile uint32_t usb_tx_addr;    volatile uint32_t usb_tx_count;    volatile uint32_t usb_rx_addr;    volatile union{      uint32_t usb_rx_count;      struct{        uint32_t rx_count:10;        uint32_t rx_num_blocks:5;        uint32_t rx_blocksize:1;      };    };}usb_epdata_t;


Здесь задаются начало буфера передачи, его размер, потом начало буфера приема и его размер. Обратите внимание во-первых, что usb_tx_count задает не собственно размер буфера, а количество данных для передачи. То есть наш код должен записать по адресу usb_tx_addr данные, потом записать в usb_tx_count их размер и только потом дернуть регистр модуля usb что данные мол записаны, передавай. Еще большее внимание обратите внимание на странный формат размера буфера приема: он представляет собой структуру, в которой 10 бит rx_count отвечают за реальное количество прочитанных данных, а вот остальные уже действительно за размер буфера. Надо же железке знать докуда писать можно, а где начинаются чужие данные. Формат этой настройки тоже довольно интересный: флаг rx_block_size говорит в каких единицах задается размер. Если он сброшен в 0, то в 2-байтных словах, тогда размер буфера равен 2*rx_num_blocks, то есть от 0 до 62. А если выставлен в 1, то в 32-байтных блоках, соответственно размер буфера тогда оказывается 32*rx_num_blocks и лежит в диапазоне от 32 до 512 (да, не до 1024, такое вот ограничение контроллера).

Для размещения буферов в этой области будем использовать полудинамический подход. То есть выделять память по запросу, но не освобождать ее (еще не хватало malloc / free изобретать!). На начало неразмеченного пространства будет указывать переменная lastaddr, изначально указывающая на начало области PMA за вычетом таблицы структур, рассмотренной выше. Ну а при каждом вызове функции настройки очередной конечной точки usb_ep_init() она будет сдвигаться на указанный там размер буфера. И в соответствующую ячейку таблицы будет вносится нужное значение, естественно. Значение этой переменной сбрасывается по событию ресета, после чего тут же следует вызов usb_class_init(), в котором точки настраиваются заново в соответствии с юзерской задачей.

Работа с регистрами приема-передачи


Как было только что сказано, на прием мы читаем сколько же реально данных пришло (поле usb_rx_count), потом читаем сами данные, потом дергаем модуль usb что буфер свободен, можно принимать следующий пакет. На передачу наоборот: записываем данные в буфер, потом выставляем в usb_tx_count сколько же записано и наконец дергаем модуль что буфер заполнен, можно передавать.

Первые грабли начинаются при собственно работе с буфером: он организован не по 32 бита, как весь остальной контроллер, и не по 8 бит, как можно было ожидать. А по 16 бит! В результате запись и чтение в него осуществляются по 2 байта, выровненные по 4 байта. Спасибо, ST, что сделали такое извращение! Как бы скучно без этого жилось! Теперь обычным memcpy не обойтись, придется городить специальные функции. Кстати, если кто любит DMA, то оно такое преобразование делать вроде умеет самостоятельно, хотя я это не проверял.

И тут же вторые грабли с записью в регистры модуля. Дело в том, что за настройку каждой конечной точки за ее тип (control, bulk и т.д.) и состояние отвечает один регистр USB_EPnR, то есть просто так в нем бит не поменяешь, надо следить чтобы не попортить остальные. А во-вторых, в этом регистре присутствуют биты аж четырех типов! Одни доступны только на чтение (это замечательно), другие на чтение и запись (тоже нормально), третьи игнорируют запись 0, но при записи 1 меняют состояние на противоположное (начинается веселье), а четвертые напротив игнорируют запись 1, но запись 0 сбрасывает их в 0. Скажите мне, какой наркоман додумался в одном регистре сделать биты, игнорирующие 0 и игнорирующие 1?! Нет, я готов предположить что это сделано ради сохранения целостности регистра, когда к нему обращаются и из кода, и из железа. Но вам что, лень было поставить инвертор чтобы биты сбрасывались записью 1? Или в другом месте инвертор чтобы другие биты инвертировались записью 0? В результате выставление двух битов регистра выглядит так (еще раз спасибо ST за такое извращение):
#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)


Ах да, чуть не забыл: доступа к регистру по номеру у них тоже нет. То есть макросы USB_EP0R, USB_EP1R и т.д. у них есть, но вот если номер пришел в переменной, то увы. Пришлось изобретать свой USB_EPx() а что поделать.

Ну и для соблюдения формальностей укажу, что флаг готовности к чтению (то есть что предыдущие данные мы уже прочитали) задается битовой маской USB_EP_RX_VALID, а к записи (то есть что данные мы записали полностью и их можно передавать) маской USB_EP_TX_VALID.

Обработка IN и OUT запросов


Возникновение прерывания USB может сигнализировать о разных вещах, но сейчас мы сосредоточимся на запросах обмена данными. Флагом такого события будет бит USB_ISTR_CTR. Если увидели его, можем разбираться с какой точкой хочет общаться хост. Номер точки скрывается под битовой маской USB_ISTR_EP_ID, а направление IN или OUT под битами USB_EP_CTR_TX и USB_EP_CTR_RX соответственно.

Поскольку точек у нас может быть много, и каждая со своим алгоритмом обработки, заведем им всем callback-функции, которые бы вызывались по соответствующим событиям. Например, послал хост данные в endpoint3, мы прочитали USB->ISTR, вытащили оттуда что запрос у нас OUT и что номер точки равен 3. Вот и вызываем epfunc_out[3](3). Номер точки в скобках передается если вдруг юзерский код захочет повесить один обработчик на несколько точек. Ах да, еще в стандарте USB принято входные точки IN помечать взведенным 7-м битом. То есть endpoint3 на выход будет иметь номер 0x03, а на вход 0x83. Причем это разные точки, их можно использовать одновременно, друг другу они не мешают. Ну почти: в stm32 у них настройка типа (bulk, interrupt, ...) общая и на прием, и на передачу. Так что та же 0x83-я точка IN будет соответствовать callback'у epfunc_in[3](3 | 0x80).

Тот же принцип используется и для ep0. Разница только в том, что ее обработка происходит внутри библиотеки, а не внутри юзерского кода. Но что делать если нужно обрабатывать специфичные запросы вроде какого-нибудь HID не лезть же ковырять код библиотеки? Для этого предусмотрены специальные callback'и usb_class_ep0_out и usb_class_ep0_in, которые вызываются в специальных местах и имеют специальный формат, рассказывать про который я буду ближе к концу.

Стоит упомянуть еще про один не очень очевидный момент, связанный с возникновением прерываний обработки пакетов. С OUT запросами все просто: данные пришли, вот они. А вот IN прерывание генерируется не тогда, когда хост послал IN запрос, а когда в буфере передачи пусто. То есть по принципу действия это прерывание аналогично прерыванию по опустошению буфера UART. Следовательно, когда мы хотим что-то передать хосту, мы это просто записываем данные в буфер передачи, ждем прерывания IN и дописываем что не поместилось (не забываем про ZLP). И ладно еще с обычными endpoint'ами, ими программист управляет, можно пока не обращать внимание. Но вот через ep0 обмен идет всегда. Поэтому и работа с ней должна быть встроена в библиотеку.

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

Ну а сам обработчик устроен довольно просто: записывает очередной кусок данных в буфер передачи, сдвигает адрес начала буфера и уменьшает количество оставшихся для передачи байтов. Отдельный костыль связан с тем самым ZLP и необходимостью на некоторые запросы отвечать пустым пакетом. В данном случае конец передачи обозначается тем, что адрес данных стал NULL. А пустой пакет что он равен константе ZLPP. И то и другое происходит при равенстве размера нулю, так что реальной записи не происходит.

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

Логика общения по USB


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

Обработка SETUP запросов: DeviceDescriptor


Человек, хоть немного ковырявший USB, уже давно должен был насторожиться: COKPOWEHEU, ты говоришь про запросы IN и OUT, но ведь в стандарте прописан еще и SETUP. Да, так и есть, но это скорее разновидность OUT запроса, специально структурированная и предназначенная исключительно для конечной точки 0. Об ее структуре и особенностях работы и поговорим. Сама структура выглядит следующим образом:
typedef struct{  uint8_t bmRequestType;  uint8_t bRequest;  uint16_t wValue;  uint16_t wIndex;  uint16_t wLength;}config_pack_t;


Поля этой структуры рассмотрены во множестве источников, но все же напомню.
bmRequestType битовая маска, биты в которой означают следующее:
7: направление передачи. 0 от хоста к устройству, 1 от устройства к хосту. Фактически, это тип следующей передачи, OUT или IN.
6-5: класс запроса
0x00 (USB_REQ_STANDARD) стандартный (обрабатывать пока будем только их)
0x20 (USB_REQ_CLASS) специфичные для класса (до них дойдем в следующих статьях)
0x40 (USB_REQ_VENDOR) специфичные для производителя (надеюсь, не придется их трогать)
4-0: собеседник
0x00 (USB_REQ_DEVICE) устройство в целом
0x01 (USB_REQ_INTERFACE) отдельный интерфейс
0x02 (USB_REQ_ENDPOINT) конечная точка

bRequest собственно запрос
wValue небольшое 16-битное поле данных. На случай простых запросов, чтобы не гонять полноценные пересылки.
wIndex номер получателя. Например, интерфейса, с которым хост хочет пообщаться.
wLength размер дополнительных данных, если 16 бит wValue недостаточно.

Первым делом при подключении устройства хост пытается узнать что же именно в него воткнули. Для этого он посылает запрос со следующими данными:
bmRequestType = 0x80 (запрос на чтение) + USB_REQ_STANDARD (стандартный) + USB_REQ_DEVICE (к устройству в целом)
bRequest = 0x06 (GET_DESCRIPTOR) запрос дескриптора
wValue = 0x0100 (DEVICE_DESCRIPTOR) дескриптор устройства в целом
wIndex = 0 не используется
wLength = 0 дополнительных данных нет
После чего шлет запрос IN, куда устройство должно положить ответ. Как мы помним, запрос IN от хоста и прерывание контроллера слабо связаны, так что записывать ответ будем сразу в буфер передатчика ep0. Теоретически, данные из этого, да и всех прочих, дескрипторов привязаны к конкретному устройству, поэтому помещать их в ядро библиотеки бессмысленно. Соответствующие запросы передаются функции usb_class_get_std_descr, которая возвращает ядру указатель на начало данных и их размер. Дело в том, что некоторые дескрипторы могут быть переменного размера. Но DEVICE_DESCRIPTOR к ним не относится. Его размер и структура стандартизованы и выглядят так:
uint8_t bLength; //размер дескриптораuint8_t bDescriptorType; //тип дескриптора. В данном случае USB_DESCR_DEVICE (0x01)uint16_t bcdUSB; //число 0x0110 для usb-1.1, либо 0x0200 для 2.0. Других значений я не встречалuint8_t bDeviceClass; //класс устройстваuint8_t bDeviceSubClass; //подклассuint8_t bDeviceProtocol; //протоколuint8_t bMaxPacketSize0; //размер ep0uint16_t idVendor; // VIDuint16_t idProduct; // PIDuint16_t bcdDevice_Ver; //версия в BCD-форматеuint8_t iManufacturer; //номер строки названия производителяuint8_t iProduct; //номер строки названия продуктаuint8_t iSerialNumber; //номер строки версииuint8_t bNumConfigurations; //количество конфигураций (почти всегда равно 1)

В первую очередь обратите внимание на первые два поля размер дескриптора и его тип. Они характерны почти для всех дескрипторов USB (кроме HID, пожалуй). Причем если bDescriptorType это константа, то bLength приходится чуть ли не считать вручную для каждого дескриптора. В какой-то момент мне это надоело и был написан макрос
#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x

Он считает размер переданных ему аргументов и подставляет вместо первого. Дело в том, что иногда дескрипторы бывают вложенными, так что один, скажем, требует размер в первом байте, другой в 3 и 4 (16-битное число), а третий в 6 и 7 (снова 16-битное число). Точные значения аргументов макросам безразличны, но хотя бы количество совпадать должно. Собственно, макросы для подстановки в 1, в 3 и 4, а также в 6 и 7 байты там тоже есть, но их применение я покажу на более характерном примере.
Пока же рассмотрим 16-битные поля вроде VID и PID. Понятное дело что в одном массиве смешать 8-битные и 16-битные константы не выйдет, да плюс endiannes в общем, на выручку снова приходят макросы: USB_U16( x ).

В плане выбора VID:PID вопрос сложный. Если планируется выпускать продукцию серийно, все же стоит купить персональную пару. Для личного же пользования можно подобрать чужую от похожего устройства. Скажем, у меня в примерах будут пары от AVR LUFA и STM. Все равно хост определяет по этой паре скорее специфичные баги реализации, чем назначение. Потому что назначение устройства подробно расписывается в специальном дескрипторе.

Внимание, грабли! Как оказалось, Windows привязывает к этой паре драйвера, то есть вы, например, собрали устройство HID, показали системе и установили драйвера. А потом перепрошили устройство под MSD (флешку), не меняя VID:PID, то драйвера останутся старые и, естественно, работать устройство не будет. Придется лезть в управление оборудованием, удалять драйвера и заставлять систему найти новые. Я думаю, ни для кого не станет неожиданностью, что в Linux такой проблемы нет: устройства просто подключаются и работают.

StringDescriptor


Еще одной интересной особенностью дескрипторов USB является любовь к строкам. В шаблоне дескриптора они обозначаются префиксом i, как например iSerialNumber или iPhone. Эти строки входят во многие дескрипторы и, честно говоря, я не знаю, зачем их так много. Тем более что при подключении устройства видны будут только iManufacturer, iProduct и iSerialNumber. Как бы то ни было, строки представляют собой те же дескрипторы (то есть поля bLength и bDescriptorType в наличии), но вместо дальнейшей структуры идет поток 16-битных символов, похожих на юникод. Смысл данного извращения мне опять непонятен, ведь подобные названия даются все равно обычно на английском, где и 8-битного ASCII хватило бы. Ну хорошо, хотите расширенный набор символов, так UTF-8 бы взяли. Странные люди Для удобного формирования строк удобно применять угадайте что правильно, макросы. Но на этот раз не моей разработки, а подсмотренные у EddyEm. Поскольку строки являются дескрипторами, то и запрашивать их хост будет как обычные дескрипторы, только в поле wValue подставит 0x0300 (STRING_DESCRIPTOR). А вместо младшего байта будет собственно индекс строки. Скажем, запрос 0x0300 это строка с индексом 0 (она зарезервирована под язык устройства и почти всегда равна u"\x0409"), а запрос 0x0302 строка с индексом 2.

Внимание, грабли! Сколь бы ни был велик соблазн засунуть в iSerialNumber просто строку, даже строку с честной версией вида u''1.2.3'' не делайте этого! Некоторые операционные системы считают, что там должны быть только шестнадцатеричные цифры, то есть '0'-'9', 'A'-'Z' и все. Даже точек нельзя. Наверное, они как-то считают от этого числа хэш чтобы идентифицировать при повторном подключении, не знаю. Но проблему такую заметил при тестировании на виртуальной машине с Windows 7, она считала устройство бракованным. Что интересно, Windows XP и 10 проблему не заметили.

ConfigurationDescriptor


С точки зрения хоста устройство представляет набор отдельных интерфейсов, каждый из которых предназначен для решения какой-то задачи. В дескрипторе интерфейса описывается его устройство и привязанные конечные точки. Да, конечные точки описываются не сами по себе, а только как часть интерфейса. Обычно интерфейсы со сложной архитектурой управляются SETUP запросами (то есть через ep0), в которых поле wIndex номеру интерфейса и соответствует. Максимум позволяется прикарманить конечную точку для прерываний. А от интерфейсов данных хосту нужны только описания конечных точек и обмен будет идти через них.

Интерфейсов в одном устройстве может быть много, причем очень разных. Поэтому чтобы не путаться где заканчивается один интерфейс и начинается другой, в дескрипторе указывается не только размер заголовка, но и отдельно (обычно 3-4 байтами) полный размер интерфейса. Таким образом интерфейс складывается подобно матрешке: внутри общего контейнера (который хранит размер заголовка, bDescriptorType и полный размер содержимого, включая заголовок) может находиться еще парочка контейнеров поменьше, но устроенных точно так же. А внутри еще и еще. Приведу пример дескриптора примитивного HID-устройства:
static const uint8_t USB_ConfigDescriptor[] = {  ARRLEN34(  ARRLEN1(    bLENGTH, // bLength: Configuration Descriptor size    USB_DESCR_CONFIG,    //bDescriptorType: Configuration    wTOTALLENGTH, //wTotalLength    1, // bNumInterfaces    1, // bConfigurationValue: Configuration value    0, // iConfiguration: Index of string descriptor describing the configuration    0x80, // bmAttributes: bus powered    0x32, // MaxPower 100 mA  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_INTERFACE, //bDescriptorType    0, //bInterfaceNumber    0, // bAlternateSetting    0, // bNumEndpoints    HIDCLASS_HID, // bInterfaceClass:     HIDSUBCLASS_NONE, // bInterfaceSubClass:     HIDPROTOCOL_NONE, // bInterfaceProtocol:     0x00, // iInterface  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_HID, //bDescriptorType    USB_U16(0x0101), //bcdHID    0, //bCountryCode    1, //bNumDescriptors    USB_DESCR_HID_REPORT, //bDescriptorType    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength  )  )};


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

Как можно видеть, данный дескриптор состоит из заголовка USB_DESCR_CONFIG (хранящего полный размер содержимого включая себя!), интерфейса USB_DESCR_INTERFACE (описывающего подробности устройства) и USB_DESCR_HID, в общих чертах говорящего что же именно за HID мы изображаем. Причем именно что в общих чертах: конкретная структура HID описывается в специальном дескрипторе HID_REPORT_DESCRIPTOR, рассматривать который я здесь не буду, просто потому что слишком плохо его знаю. Так что ограничимся копипастом из какого-нибудь примера.

Вернемся к интерфейсам. Учитывая, что у них есть номера, логично предположить, что в одном устройстве интерфейсов может быть много. Причем они могут отвечать как за одну общую задачу (скажем, интерфейс управления USB-CDC и интерфейс данных), так и за принципиально несвязанные. Скажем, ничто не мешает нам (кроме отсутствия знаний пока) на одном контроллере реализовать два переходника USB-CDC плюс флешку плюс, скажем, клавиатуру. Очевидно, что интерфейс флешки знать не знает про COM-порт. Впрочем, тут есть свои подводные камни, которые, надеюсь, когда-нибудь рассмотрим. Еще стоит отметить, что один интерфейс может иметь несколько альтернативных конфигураций (bAlternateSetting), отличающихся, скажем, количеством конечных точек или частотой их опроса. Собственно, для того и сделано: если хост считает, что лучше пропускную способность поберечь, он может переключить интерфейс в какой-нибудь альтернативный режим, который ему больше понравился.

Обмен данными с HID


Вообще говоря, HID-устройства имитируют объекты реального мира, у которых есть не столько данные, сколько набор неких параметров, которые можно измерять или задавать (запросы SET_REPORT / GET_REPORT) и которые могут уведомлять хост о внезапном внешнем событии (INTERRUPT). Таким образом, собственно для обмена данными данные устройства не предназначены но кого это когда останавливало!

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

Начнем с более простого чтения по запросу HIDREQ_GET_REPORT. По сути это такой же запрос, как и всякие DEVICE_DESCRIPTOR, только специфичный именно для HID. Плюс этот запрос адресован не устройству в целом, а интерфейсу. То есть если мы реализовали в одном устройстве несколько независимых HID-устройств, их можно различить по полю wIndex запроса. Правда, именно для HID это не лучший подход: проще сам дескриптор сделать составным. В любом случае до таких извращений нам далеко, так что даже не будем анализировать что и куда хост пытался послать: на любой запрос к интерфейсу и с полем bRequest равным HIDREQ_GET_REPORT будем возвращать собственно данные. По идее, такой подход предназначен чтобы возвращать дескрипторы (со всеми bLength и bDescriptorType), но в случае HID разработчики решили все упростить и обмениваться только данными. Вот и возвращаем указатель на нашу структуру и ее размер. Ну и небольшая дополнительная логика вроде обработки кнопок и счетчика запросов.

Более сложный случай запрос на запись. Это первый раз, когда мы сталкиваемся с наличием дополнительных данных в SETUP запросе. То есть ядро нашей библиотеки должно сначала прочитать сам запрос, и только потом данные. И передать их юзерской функции. А буфера у нас, напоминаю, нет. В результате некоторой низкоуровневой магии был разработан следующий алгоритм. Callback вызывать будем всегда, но укажем ему с какого по счету байта данные сейчас лежат в буфере приема конечной точки (offset) а также размер этих данных (size). То есть при приеме самого запроса значения offset и size равны нулю (данных-то нет). При приеме первого пакета offset все еще равен нулю, а size размеру принятых данных. Для второго offset будет равен размеру ep0 (потому что если данные пришлось разбивать, делают это по размеру конечной точки), а size размеру принятых данных. И так далее. Важно! Если данные приняты, их надо считать. Это может сделать либо обработчик вызовом usb_ep_read() и возвратом 1 (мол я там сам считал, не утруждайся), либо просто вернув 0 (мне эти данные не нужны) без чтения тогда очисткой займется ядро библиотеки. По этому принципу и построена функция: она проверяет в наличии ли данные и если да, то читает их и зажигает светодиоды.

Софт для обмена данными


Тут я велосипед изобретать не стал, а взял готовую программу из предыдущей статьи.

Заключение


Вот, собственно, и все. Основы работы с USB при помощи аппаратного модуля в STM32 я рассказал, некоторый грабли тоже пощупал. Учитывая значительно меньший объем кода, чем тот ужас, что генерирует STMCube, разобраться в нем будет проще. Собственно говоря, в Cube'ической лапше я так и не разобрался, уж больно много там вызовов одного и того же в разных комбинациях. Гораздо лучше для понимания вариант от EddyEm, от которого я отталкивался. Конечно, и там не без косяков, но хотя бы пригодно для понимания. Также похвастаюсь, что размер моего варианта едва ли не в 5 раз меньше ST'шного (~2.7 кБ против 14) при том, что оптимизацией я не занимался и наверняка можно еще ужать.

Отдельно хочу отметить разницу поведения различных операционных систем при подключении сомнительного оборудования. Linux просто работает, даже если в дескрипторах ошибки. Windows XP, 7, 10 при малейших ошибках ругаются что устройство поломанное, я с ним работать отказываюсь. Причем XP иногда даже в BSOD падала от негодования. Ах да, еще они постоянно выводят устройство может работать быстрее, что с этим делать я не знаю. В общем, как бы хорош Linux не был для разработки, он прощает слишком многое, тестировать надо и на менее юзер-френдли системах.

Дальнейшие планы: рассмотреть остальные типы конечных точек (пока что был пример только с Control); рассмотреть другие контроллеры (скажем, у меня еще at90usb162 (AVR) и gd32vf103 (RISC_V) валяются), но это совсем далекие планы. Также хорошо бы поподробнее разобраться с отдельными USB-устройствами вроде тех же HID, но тоже не приоритетная задача.
Подробнее..

USB на регистрах isochronous endpoint на примере Audio device

23.05.2021 14:09:40 | Автор: admin
image<картинка с платой и наушниками>
Еще более низкий уровень (avr-vusb): habr.com/ru/post/460815
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: interrupt endpoint на примере HID

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

Как ни странно, этот тип конечной точки оказался самым мозговыносящим (и это после всего, что я успел повидать с stm'ками!). Тем не менее, сегодня мы сделаем аудиоустройство и заодно чуть-чуть допилим ядро библиотеки USB. Как обычно, исходные коды доступны:
github.com/COKPOWEHEU/usb/tree/main/4.Audio_L1
github.com/COKPOWEHEU/usb/tree/main/4.Audio_F1

Доработка ядра


Допиливать ядро нужно потому, что у STM изохронные точки могут быть только с двойной буферизацией, то есть, грубо говоря, нельзя сделать 0x01 изохронной, а 0x81 управляющей. То есть в дескрипторе USB это прописать, конечно, можно, но внутренности контроллера это не изменит, и реальный адрес точки просто будет отличаться от видимого снаружи. Что, естественно, повысит риск ошибок, поэтому в эту сторону извращаться не будем.

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

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

Прием и передача пакетов отличается не так сильно, хотя и отняла гораздо больше времени сначала на попытки понять как же она должна работать по логике ST, потом на подгонку заклинания из интернета чтобы все-таки заработало. Как говорилось раньше, если для обычной точки два буфера независимы и отличаются направлением обмена, то для буферизованной они одинаковы и отличаются только смещением. Так что немножко изменим функции usb_ep_write и usb_ep_read чтобы они принимали не номер точки, а номер смещения. То есть если раньше эти функции предполагали существование восьми сдвоенных точек, то теперь 16 одинарных. Соответственно, номер новой полуточки на запись равен всего лишь номеру обычной, умноженному на два, а для usb_ep_read надо еще добавить единицу (см. распределение буферов в PMA). Собственно, это и делается инлайн-функциями usb_ep_write и usb_ep_read для обычных точек. А вот логику буферизованных рассмотрим чуть подробнее.

Согласно документации, один буфер такой точки доступен для железа, второй для софта. Потом они переключаются и снова не мешают друг другу. Для OUT точки флагом со стороны железа является бит USB_EP_DTOG_RX, который нужно прочитать чтобы понять в какой из буферов только что закончилась запись и откуда соответственно софт может читать. Когда он прочитал свой буфер, нужно дернуть бит USB_EP_DTOG_TX, что собственно переключит буферы. Не уверен, что подразумевалось именно это, но оно, по крайней мере, работает.

Симметричная ситуация должны была быть с IN точками. Но на практике оказалось, что и проверять, и дергать надо USB_EP_DTOG_RX. Почему не TX я так и не понял Спасибо пользователю kuzulis за ссылку на github.com/dmitrystu/libusb_stm32/edit/master/src/usbd_stm32f103_devfs.c

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

В результате конечные точки научились работать и в буферизованном режиме если не дышать на них слишком сильно.

Для пользователя разница невелика: вместо usb_ep_init использовать usb_ep_init_double, а вместо usb_ep_write и usb_ep_read соответственно usb_ep_write_double и usb_ep_read_double.

Устройство AudioDevice


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

Согласно стандарту USB аудиоустройство представляет собой набор сущностей (entity), соединенных друг с другом в некую топологию, по которой и проходит аудиосигнал. Каждая сущность имеет свой уникальный номер (bTerminalID, он же UnitID), по которому к ней могут подключаться другие сущности или конечные точки, по нему же обращается хост, если хочет изменить какие-то параметры. И он же считается единственным выходом данной сущности. А вот входов может вообще не быть (если это входной терминал), а может быть и больше одного (bSourceID). Собственно записью в массив bSourceID номеров сущностей, от которых текущая получает аудиосигнал, мы и описываем всю топологию, которая в результате может получиться весьма резвесистой. Для примера приведу топологию покупной USB-звуковой карты (цифрами показаны bTerminalID / UnitID):

lsusb и его расшифровка
Bus 001 Device 014: ID 0d8c:013c C-Media Electronics, Inc. CM108 Audio Controller#Тут пока ничего интересногоDevice Descriptor:  bLength                18  bDescriptorType         1  bcdUSB               1.10  bDeviceClass            0   bDeviceSubClass         0   bDeviceProtocol         0   bMaxPacketSize0         8  idVendor           0x0d8c C-Media Electronics, Inc.  idProduct          0x013c CM108 Audio Controller  bcdDevice            1.00  iManufacturer           1   iProduct                2   iSerial                 0   bNumConfigurations      1  #интересное начинается тут  Configuration Descriptor:    bLength                 9    bDescriptorType         2    wTotalLength       0x00fd    bNumInterfaces          4  # общее количество интерфейсов    bConfigurationValue     1    iConfiguration          0     bmAttributes         0x80      (Bus Powered)    MaxPower              100mA    #интерфейс 0 - описание топологии    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        0      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      1 Control Device      bInterfaceProtocol      0       iInterface              0       AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      1 (HEADER)        bcdADC               1.00        wTotalLength       0x0064        bInCollection           2  # ВАЖНО! количество интерфейсов данных (2)        baInterfaceNr(0)        1  #номер перовго из них        baInterfaceNr(1)        2  #номер второго ##### Топологоия ###### 1 InputTerminal (USB, на динамик)       AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             1  # Вот номер данной сущности        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bNrChannels             2  # Здесь задается количество каналов        wChannelConfig     0x0003  # А здесь - их расположение в пространстве          Left Front (L)          Right Front (R)        iChannelNames           0         iTerminal               0         # 2 InputTerminal (микрофон)      AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             2        wTerminalType      0x0201 Microphone        bAssocTerminal          0        bNrChannels             1        wChannelConfig     0x0001          Left Front (L)        iChannelNames           0         iTerminal               0         # 6 OutputTerminal (динамик), вход соединен с сущностью 9      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             6        wTerminalType      0x0301 Speaker        bAssocTerminal          0        bSourceID               9  # Номера входов указываются здесь        iTerminal               0         # 7 OutputTerminal (USB), вход соединен с сущностью 8      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             7        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bSourceID               8        iTerminal               0         # 8 Selector, входы соединены только с сущностью 10      AudioControl Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      5 (SELECTOR_UNIT)        bUnitID                 8        bNrInPins               1  # У сущностей с несколькими входами указывается их количество        baSourceID(0)          10  # а потом номера        iSelector               0         # 9 Feature, вход соединен с сущностью 15      AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                 9        bSourceID              15        bControlSize            1        bmaControls(0)       0x01          Mute Control        bmaControls(1)       0x02          Volume Control        bmaControls(2)       0x02          Volume Control        iFeature                0         # 10 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                10        bSourceID               2        bControlSize            1        bmaControls(0)       0x43          Mute Control          Volume Control          Automatic Gain Control        bmaControls(1)       0x00        iFeature                0         # 13 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                13        bSourceID               2        bControlSize            1        bmaControls(0)       0x03          Mute Control          Volume Control        bmaControls(1)       0x00        iFeature                0         # 15 Mixer, входы соединены с сущностями 1 и 13      AudioControl Interface Descriptor:        bLength                13        bDescriptorType        36        bDescriptorSubtype      4 (MIXER_UNIT)        bUnitID                15        bNrInPins               2  # Снова массив входов        baSourceID(0)           1  # и их номера        baSourceID(1)          13        bNrChannels             2        wChannelConfig     0x0003          Left Front (L)          Right Front (R)        iChannelNames           0         bmControls(0)        0x00        iMixer                  0 ##### конец топологии ###### Интерфейс 1 (основной) - заглушка без конечных точек    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 1 (альтернативный) - рабочий с одной конечной точкой    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           1        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             2        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x01  EP 1 OUT        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x00c8  1x 200 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         1 Milliseconds          wLockDelay         0x0001          # Интерфейс 2 (основной) - заглушка    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 2 (альтернативный)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           7        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             1        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x82  EP 2 IN        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x0064  1x 100 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         0 Undefined          wLockDelay         0x0000##### Конец описания аудиоинтерфейсов ###### Интерфейс 3 "Клавиши громкости и всего остального" (не интересно)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        3      bAlternateSetting       0      bNumEndpoints           1      bInterfaceClass         3 Human Interface Device      bInterfaceSubClass      0       bInterfaceProtocol      0       iInterface              0         HID Device Descriptor:          bLength                 9          bDescriptorType        33          bcdHID               1.00          bCountryCode            0 Not supported          bNumDescriptors         1          bDescriptorType        34 Report          wDescriptorLength      60         Report Descriptors:            ** UNAVAILABLE **      Endpoint Descriptor:        bLength                 7        bDescriptorType         5        bEndpointAddress     0x87  EP 7 IN        bmAttributes            3          Transfer Type            Interrupt          Synch Type               None          Usage Type               Data        wMaxPacketSize     0x0004  1x 4 bytes        bInterval               2



image

Мы же будем делать нечто более простое (заготовку брал отсюда):

image

Здесь видно две независимых ветки распространения сигнала: либо от USB через фичу к динамику, либо из микрофона через другую фичу к USB. Микрофон и динамик не просто так взяты в кавычки: на моей отладочной плате их нет, поэтому вместо собственно звука будем пользоваться кнопками и светодиодами. Впрочем, ничего нового. Фичи в моем случае ничего не делают и добавлены скорее для красоты.

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

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

1. Входной терминал (Input Terminal)
Как следует из названия, именно через него в аудиоустройство попадает звуковой сигнал. Это может быть USB, может быть микрофон обыкновенный, микрофон гарнитурный, даже микрофонный массив.

2. Выходной терминал (Output Terminal)
Тоже вполне очевидно то, через что звук покидает наше устройство. Это может быть все тот же USB, может быть динамик, гарнитура, динамик в мониторе, динамики различных частот и куча других устройств.

3. Микшер (Mixer Unit)
Берет несколько входных сигналов, усиливает каждый на заданную величину и складывает то, что получилось, в выходной канал. При желании можно задать усиление в ноль раз, что сведет его к следующей сущности.

4. Селектор (Selector Unit)
Берет несколько входных сигналов и перенаправляет один из них на выход.

5. Фильтр (Feature Unit)
Берет единственный входной сигнал, меняет параметры звука (громкость, тембр и т.п.) и выдает на выход. Естественно, все эти параметры одинаковым способом прикладываются ко всему сигналу, без взаимодействия логических каналов внутри него

6. Processing Unit
А вот эта штука уже позволяет проводить манипуляции над отдельными логическими каналами внутри каждого входного. Более того, позволяет сделать количество логических каналов в выходном не равным количеству во входных.

7. Extension Unit
Весь набор нестандартных сущностей, чтобы больной фантазии производителей оборудования было раздолье. Соответственно, и поведение, и настройки будут зависеть от этой самой фантазии.

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

Грабли в дескрипторе


В отличие от предыдущих USB-устройств, здесь дескриптор сложный, многоуровневый и склонный пугать виндоусы до BSOD'а. Как мы видели выше, топология у аутиоустройства может быть весьма сложной и развесистой. Под ее описание выделяется целый интерфейс. Очевидно, endpoint'ов он содержать не будет, зато будет содержать список дескрипторов сущностей и описаний к чему подключены их входы. Тут особо описывать смысла не вижу, проще посмотреть в коде и документации. Отмечу только главные грабли: здесь описывается какие интерфейсы с соответствующими конечными точками относятся именно к данному устройству. Скажем, если вы захотите изменить мою конфигурацию и убрать оттуда динамик, придется не просто удалить половину сущностей (слава макросам, хотя бы с подсчетом длины дескриптора проблемы не будет), но и уменьшить поле bInCollection до 1, после чего из следующего за ним массива bInterfaceNr убрать номер лишнего интерфейса.

Дальше находятся интерфейсы, отвечающие за обмен данными. В моем случае 1-й интерфейс отвечает за микрофон, а 2-й за динамик. Здесь стоит обратить внимание в первую очередь на два варианта каждого из этих интерфейсов. Один с bAlternateSetting равным 0, второй с 1. Они отличаются наличием конечной точки. То есть если наше устройство в данный момент не используется, хост просто переключается на тот альтернативный интерфейс, который конечной точкой не оборудован, и уже не тратит на нее пропускную способность шины.

Вторая особенность интерфейсов данных это формат аудиосигнала. В соответствующем дескрипторе задается тип кодирования, количество каналов, разрешение и частота дискретизации (которая задается 24-битным числом). Вариантов кодирования предусмотрено довольно много, но мы будем использовать самый простой PCM. По сути это просто последовательность значений мгновенной величины сигнала без какого-либо кодирования, причем величина считается целым числом со знаком. Разрешение сигнала задается в двух местах (зачем непонятно): в поле bSubFrameSize указывается количество байтов, а в bBitResolution количество битов. Вероятно, можно указать, что диапазон нашей звуковой карты не доходит до полного диапазона типа данных, скажем, int16_t и составляет всего 10 бит.

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

Ах да, чуть не забыл упомянуть очередную пачку BSOD'ов при тестировании неправильных дескрипторов. Еще раз напоминаю: количество интерфейсов данных должно соответствовать числу bInCollection, а их номера следующему за ним массиву!
Скрытый текст
Как представлю отладку подобного кода под виндами, с этими постоянными вылетами, да еще без нормальной консоли. бр-р-р.


Логика работы устройства


Как я уже говорил, для тестов не имеет смысла городить на отладочную плату навесные компоненты, поэтому все тестирование будет осуществляться тем, что уже установлено кнопки да светодиоды. Впрочем, в данном случае это проблемы не составляет: микрофон может просто генерировать синусоиду частотой, скажем, 1 кГц, а динамик включать светодиод при превышении порогового значения звука (скажем, выше числа 10000: при указанных 16 битах разрешения, что соответствует диапазону -32768 +32767, это примерно треть).

А вот с тестированием возникла небольшая проблема: я не нашел простого способа перенаправить сигнал с микрофона на stdin какой-нибудь программы. Вроде бы раньше это делалось просто чтением /dev/dsp, но сейчас что-то поломалось. Впрочем, ничего критичного, ведь есть всякие библиотеки взаимодействия с мультимедией SDL, SFLM и другие. Собственно на SFML я и написал простенькую утилиту для чтения с микрофона и, если надо, визуализации сигнала.

Особое внимание уделю ограничениям нашего аудиоустройства: насколько я понял, изохронный запрос IN отправляется один раз в миллисекунду (а вот OUT'ов может быть много), что ограничивает частоту дискретизации. Допустим, размер конечной точки у нас 64 байта (учитывая буферизацию, в памяти она занимает 128 байт, но хост об этом не знает), разрешение 16 бит, то есть за раз можно отправить 32 отсчета. Учитывая интервал в 1 мс получаем теоретический предел 32 кГц для одного канала. Самый простой способ это обойти увеличить размер конечной точки. Но тут надо помнить, что размер общего буфера PMA у нас всего 512 байт. Минус таблица распределения точек, минус ep0, получаем максимум 440 байт, то есть 220 байт на единственную точку с учетом буферизации. И это теоретический предел.

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

Заключение (общее для цикла)


Ну вот мы и познакомились с устройством USB в контроллерах STM32F103 и STM32L151 (и других с аналогичной реализацией), поудивлялись логике некоторых архитектурных решений (особенно меня впечатлил регистр USB_EPnR, впрочем двойная буферизация тоже не отстает), рассмотрели все типы конечных точек и проверили их, построив соответствующие устройства. Так что можно сказать, что данный цикл статей подошел к логическому заключению. Хотя это, конечно, не значит, что я заброшу контроллеры или USB: в отдаленных планах еще разобраться с составными устройствами (пока что выглядит несложно, но ведь и изохронные точки тоже проблем не предвещали) и USB на контроллерах других семейств.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru