Teensy 4.1 через MCUXpresso. Часть 2. Осваиваем GPIO и UART
В прошлой статье мы начали работать с платой Teensy 4.1 не через сцепку из её «родных» среды разработки и библиотек (совместимых с Arduino), а через среду разработки и SDK, «родные» для установленного на ней микроконтроллера фирмы NXP. Мы убедились, что примеры от совершенно другой макетной платы, в принципе, могут быть запущены и на Teensy. После проделанных опытов нас уже есть USB-устройство, работающее по стандарту CDC, то есть виртуальный COM-порт.
Но пока что мы просто учились пользоваться всем готовым. Внесённая правка была чисто символической. Сегодня мы научимся работать с UART (это очень важно, так как других средств отладки у платы Teensy 4.1 нет), поиграем с GPIO, разгоним работу с ним в десятки раз, просто подвигав «мышкой», а на закуску — уберём некоторые особенности примера виртуального COM-порта, о которых я говорил в конце прошлой статьи. Приступаем.
Как открыть нужный инструмент
Я, как любитель контроллеров STM32, прекрасно знаю про систему создания и настройки проектов Cube MX. Дальше у ST-шников появился CubeIDE, куда всё это конфигурирование встроено. Не знаю, кто был первым, но в целом, фирма NXP пошла тем же путём: они встроили подсистему для конфигурирования в среду разработки. Эклипса позволяет переключать перспективы. Вот одна из перспектив как раз и предназначена для конфигурирования портов. Иконки, соответствующие перспективам, традиционно располагаются в правом верхнем углу Эклипсы. Вот они:
Нажимаем на ту, где изображена микросхема с ногами. Мы же будем настраивать ноги.
Работаем с UART
В открывшейся перспективе мы видим три представления: таблица слева, таблица снизу и картинка справа. Я не буду подробно рассказывать о таблицах. Есть документация, есть учебные видео. Все желающие могут посмотреть их. Документация живёт тут: MCUXpresso Config Tools User’s Guide (IDE).
В нижней таблице мы уже видим две готовые ножки UART. Где на плате Teensy их искать? Давайте разбираться.
Мы видим, что линия RX подключена к ножке L14, которой соответствует порт AD_B0_13, а TX — к ножке K14, порт AD_B0_12.
Запрашиваем у Гугля Teensy 4.1 wiring diagram, получаем ссылку сюда: Teensy and Teensy++ Schematic Diagrams.
Первое, на что стоит обратить внимание: есть порты B0_XX, а есть AD_B0_XX. Это разные порты!!! Я сначала перепутал! Не повторяйте моих ошибок! Теперь, зная об этом, находим хоть нужные порты, хоть нужные ножки. Вот они:
Получается, что нас интересуют контакты платы Teensy 4.1, отмаркированные как 24 и 25. Именно к ним подключены линии TX и RX соответственно. Берём какой-нибудь сторонний переходник USB-UART и подключаем его к этим ножкам.
Теперь надо узнать скорость порта, ведь это — настоящий UART. Наверное, она спрятана где-то в районе инициализации. Возвращаемся в обычную перспективу:
И начинаем смотреть процесс старта. Вот что-то похожее на правду:
Идём в эту функцию:
Всё понятно. Открываем вновь подключённый переходник в терминале со скоростью 115200. Перезапускаем Teensy. Видим следующее:
Это сработала следующая строка:
usb_echo — это макрос, который раскрывается в DbgConsole_Printf. А в прошлой статье я специально ставил галочку, чтобы можно было работать и через обычный printf. Давайте проверим, работает ли этот механизм. Добавим вывод в функцию main:
Собираем, «прошиваем», запускаем…
Ну, собственно, этого следовало ожидать. Переходим к более творческой части, к работе с GPIO.
Медленный GPIO
Давайте просто попробуем пошевелить ножкой платы, отмаркированной как 32. Почему ею? Она с краю, её легко найти.
Сначала я повторю свою ошибку, чтобы потом эффектно её исправить. Итак. Смотрим схему.
Это ножка контроллера C10, порт B0_12. Прекрасно. Идём в ногастую перспективу, в левой таблице в поиск вбиваем B0_12:
Кстати, тот случай. Я первый раз впал в ступор, увидев, что порт B0_12 уже занят. Но занят не B0_12, а AD_B0_12. Это видно по номерам ног. Щёлкаем по флажку слева от C10, получаем вот такой перечень возможных альтернатив:
И вот тут я сейчас и допущу ошибку. Я вижу GPIO2_IO12. Вот около этого порта и ставлю флажок. Я же просто к GPIO подключаюсь.
Новая ножка появилась в нижней строке
Но она жёлтая. Её надо дозаполнить. Я сделаю это так:
Identifier — MyPin
Direction — Output
GPIO Initial State — Logical 0
Speed — Max
Slew Rate — Fast
Остальное оставлю без изменений.
Теперь надо щёлкнуть по кнопке Update Code
По завершении обновления исходников нас автоматически выкинут в перспективу для программиста. Если интересно — в функции Main есть вызов функции BOARD_InitPins ():
Перейдя к этой функции, можно убедиться, что новая ножка действительно инициализируется:
Ну и прекрасно. Давайте сразу после инициализации сделаем бесконечный цикл, шевелящий этой ножкой:
int main(void)
#else
void main(void)
#endif
{
BOARD_ConfigMPU();
BOARD_InitPins();
BOARD_BootClockRUN();
BOARD_InitDebugConsole();
while (1)
{
GPIO_PinWrite(BOARD_INITPINS_MyPin_PERIPHERAL, BOARD_INITPINS_MyPin_CHANNEL, 0);
GPIO_PinWrite(BOARD_INITPINS_MyPin_PERIPHERAL, BOARD_INITPINS_MyPin_CHANNEL, 1);
}
Компилим, загружаем, запускаем…
6 мегагерц. Давайте попробуем разогнать…
Традиционный разгон
Привычным движением руки делаем ассемблерный код и смотрим, как выглядит генератор нашего меандра:
while (1)
{
GPIO_PinWrite (BOARD_INITPINS_MyPin_PERIPHERAL, BOARD_INITPINS_MyPin_CHANNEL, 0);
60002a94: 2200 movs r2, #0
60002a96: 210c movs r1, #12
60002a98: 4804 ldr r0, [pc, #16] ; (60002aac )
60002a9a: f000 fff1 bl 60003a80 <__GPIO_PinWrite_veneer>
GPIO_PinWrite (BOARD_INITPINS_MyPin_PERIPHERAL, BOARD_INITPINS_MyPin_CHANNEL, 1);
60002a9e: 2201 movs r2, #1
60002aa0: 210c movs r1, #12
60002aa2: 4802 ldr r0, [pc, #8] ; (60002aac )
60002aa4: f000 ffec bl 60003a80 <__GPIO_PinWrite_veneer>
60002aa8: e7f4 b.n 60002a94
Никакой оптимизации! Переключаемся в режим Release:
В свойствах проекта включаем максимальную оптимизацию, так как по умолчанию там была оптимизация по размеру, которая не подразумевает максимальной вставки inline повсюду и чуть-чуть ещё. Заодно ставим флажок Enable Link Time Optimization
Не забываем поправить Post Build Step, как мы делали для Debug сборки в прошлой статье. Ведь для Debug и Release сборок настройки разные! В прошлый раз мы это делали для Debug.
Собираем, смотрим новый ассемблерный код:
186c: 4a1f ldr r2, [pc, #124] ; (18ec )
186e: f44f 5380 mov.w r3, #4096 ; 0x1000
1872: f8c2 3088 str.w r3, [r2, #136] ; 0x88
1876: f8c2 3084 str.w r3, [r2, #132] ; 0x84
187a: e7fa b.n 1872
Отлично! Лучше уже не сделать! «Прошиваем», запускаем. Смотрим осциллограмму…
Нет, втрое мы, конечно, разогнали… Но для контроллера, работающего на частоте 600 МГц, это маловато. Напомню, что в одной из предыдущих статей я добавил разгона путём замены механизма «чтение-модификация-запись» на «сброс» и «установка». Правда, судя по ассемблерному коду, тут за нас это сделал оптимизатор. Но давайте я прекращу держать интригу и раскрою карты.
Использование порта, подключённого к другой шине
Шина — великое дело. Ещё в статье про DMA я показал, как латентность шины портит всё впечатление от хорошего механизма. Но в статьях про процессорную систему NIOS II мы уже встречались с такой удивительной шиной, которая подключается напрямую от процессорного ядра к устройству. Например, тут. Вот в контроллере, стоящем в Teensy 4.1, есть такие же шины. Их несколько. И порты могут быть подключены к обычной шине, а могут — к такой удивительной. Это я узнал отсюда.
Я выбрал порт номер два. Надо добавить пять… То есть, должен быть порт номер семь. Выбираем его (не забываем снять галку со второго, иначе будет конфликт):
Заново заполняем все поля порта в нижней таблице. Всё-таки это новое назначение… Не забываем сделать Update Code. Ну, и собираем, «прошиваем»…
Мой осциллограф показывает 142 мегагерца. Я обещал разгон в десятки раз? Вот он! Начинали мы с шести… А это у нас меандр 142 мегагерца, значит частота записи в порт у нас вдвое больше. Каково?
Пара доработок USB
Ладно, удаляем этот бесконечный цикл while. Давайте переделаем работу с USB так, чтобы можно было делать замеры скорости. В прошлый раз я уже показывал место, где данные перекидываются из приёмника в передатчик:
Просто закомментируем этот цикл, выполняющий перенос…
Попутно я изменю VID/PID, чтобы устройство стало садиться на драйвер WinUSB, установленный при опытах для позапрошлой статьи:
Будет:
Можете запустить… Не будет оно работать. Вот в этой функции обратного вызова:
Запрос следующего блока прописан не по событию «Принят блок», а по событию «Блок ушёл». Если точнее, то на приём тоже стоит, но только при нулевой длине. Логика такая: новый блок будет принимать, только если освободился буфер. Но если пришёл нулевой пакет — значит, буфер не занят, значит можно и при приёме запустить новый. Прекрасно. Закомментируем соответствующий код в районе отправки:
И уберём условие «если длина нулевая» в районе приёма:
Терминал после этого виснуть перестанет, а вот моя тестовая программа — продолжит. Они сделали так, что данные не обрабатываются, пока не взведён сигнал DTE:
Может, они и правы, но в STM такого нет, и всё работает. Поэтому я во всём файле virtual_com.c закомментировал все условия:
(1 == s_cdcVcom.startTransactions)
Делаю замер скорости, получаю недостаточно радующий глаз результат:
Половина теоретической скорости (примерно 25 мегабайт в секунду). Но причины всё те же, что и в позапрошлой статье. Пакет приходит, пока мы не подготовились к его приёму. Поэтому хост получает NAK. И библиотека NXP такова, что больше одного запроса отправить невозможно. Там в двух местах стоит защита от этого! Но к счастью, зато размер запроса может достигать вплоть до шестнадцати килобайт! Поставим, скажем, восемь.
Меняем:
На:
Получаем совсем другую картину, которой я в своё время хвастался:
Собственно, у меня всё.
Заключение
Мы научились работать с UART средствами библиотеки NXP. Также мы освоили GPIO и при оптимизации его работы выяснили, что в контроллере, установленном на плате Teensy 4.1 имеются не только медленные, но и сильно связанные с ядром шины. В следующий раз мы займёмся размещением кода и данных в памяти, подключённой именно через них, чтобы программа работала с максимально возможной скоростью.