В прошлой статье мы начали работать с платой Teensy 4.1 не через сцепку из её «родных» среды разработки и библиотек (совместимых с Arduino), а через среду разработки и SDK, «родные» для установленного на ней микроконтроллера фирмы NXP. Мы убедились, что примеры от совершенно другой макетной платы, в принципе, могут быть запущены и на Teensy. После проделанных опытов нас уже есть USB-устройство, работающее по стандарту CDC, то есть виртуальный COM-порт.
![](https://habrastorage.org/r/w1560/webt/ou/zw/ge/ouzwge5ln6hc7j3_gcmqrp84m9m.png)
Но пока что мы просто учились пользоваться всем готовым. Внесённая правка была чисто символической. Сегодня мы научимся работать с UART (это очень важно, так как других средств отладки у платы Teensy 4.1 нет), поиграем с GPIO, разгоним работу с ним в десятки раз, просто подвигав «мышкой», а на закуску – уберём некоторые особенности примера виртуального COM-порта, о которых я говорил в конце прошлой статьи. Приступаем.
Я, как любитель контроллеров STM32, прекрасно знаю про систему создания и настройки проектов Cube MX. Дальше у ST-шников появился CubeIDE, куда всё это конфигурирование встроено. Не знаю, кто был первым, но в целом, фирма NXP пошла тем же путём: они встроили подсистему для конфигурирования в среду разработки. Эклипса позволяет переключать перспективы. Вот одна из перспектив как раз и предназначена для конфигурирования портов. Иконки, соответствующие перспективам, традиционно располагаются в правом верхнем углу Эклипсы. Вот они:
Нажимаем на ту, где изображена микросхема с ногами. Мы же будем настраивать ноги.
В открывшейся перспективе мы видим три представления: таблица слева, таблица снизу и картинка справа. Я не буду подробно рассказывать о таблицах. Есть документация, есть учебные видео. Все желающие могут посмотреть их. Документация живёт тут: 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.
Давайте просто попробуем пошевелить ножкой платы, отмаркированной как 32. Почему ею? Она с краю, её легко найти.
Сначала я повторю свою ошибку, чтобы потом эффектно её исправить. Итак. Смотрим схему.
![](https://habrastorage.org/r/w1560/webt/ox/ty/zb/oxtyzbg6xjmtp5uf7ighcepvzpo.png)
Это ножка контроллера 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():
Перейдя к этой функции, можно убедиться, что новая ножка действительно инициализируется:
Ну и прекрасно. Давайте сразу после инициализации сделаем бесконечный цикл, шевелящий этой ножкой:
Компилим, загружаем, запускаем…
6 мегагерц. Давайте попробуем разогнать…
Привычным движением руки делаем ассемблерный код и смотрим, как выглядит генератор нашего меандра:
Никакой оптимизации! Переключаемся в режим Release:
В свойствах проекта включаем максимальную оптимизацию, так как по умолчанию там была оптимизация по размеру, которая не подразумевает максимальной вставки inline повсюду и чуть-чуть ещё. Заодно ставим флажок Enable Link Time Optimization
Не забываем поправить Post Build Step, как мы делали для Debug сборки в прошлой статье. Ведь для Debug и Release сборок настройки разные! В прошлый раз мы это делали для Debug.
Собираем, смотрим новый ассемблерный код:
Отлично! Лучше уже не сделать! «Прошиваем», запускаем. Смотрим осциллограмму…
Нет, втрое мы, конечно, разогнали… Но для контроллера, работающего на частоте 600 МГц, это маловато. Напомню, что в одной из предыдущих статей я добавил разгона путём замены механизма «чтение-модификация-запись» на «сброс» и «установка». Правда, судя по ассемблерному коду, тут за нас это сделал оптимизатор. Но давайте я прекращу держать интригу и раскрою карты.
Шина – великое дело. Ещё в статье про DMA я показал, как латентность шины портит всё впечатление от хорошего механизма. Но в статьях про процессорную систему NIOS II мы уже встречались с такой удивительной шиной, которая подключается напрямую от процессорного ядра к устройству. Например, тут. Вот в контроллере, стоящем в Teensy 4.1, есть такие же шины. Их несколько. И порты могут быть подключены к обычной шине, а могут – к такой удивительной. Это я узнал отсюда.
Я выбрал порт номер два. Надо добавить пять… То есть, должен быть порт номер семь. Выбираем его (не забываем снять галку со второго, иначе будет конфликт):
Заново заполняем все поля порта в нижней таблице. Всё-таки это новое назначение… Не забываем сделать Update Code. Ну, и собираем, «прошиваем»…
Мой осциллограф показывает 142 мегагерца. Я обещал разгон в десятки раз? Вот он! Начинали мы с шести… А это у нас меандр 142 мегагерца, значит частота записи в порт у нас вдвое больше. Каково?
Ладно, удаляем этот бесконечный цикл while. Давайте переделаем работу с USB так, чтобы можно было делать замеры скорости. В прошлый раз я уже показывал место, где данные перекидываются из приёмника в передатчик:
Просто закомментируем этот цикл, выполняющий перенос…
Попутно я изменю VID/PID, чтобы устройство стало садиться на драйвер WinUSB, установленный при опытах для позапрошлой статьи:
Будет:
![](https://habrastorage.org/r/w1560/webt/mw/ll/52/mwll52uo78w2wawkn3j6lynrt9k.png)
Можете запустить… Не будет оно работать. Вот в этой функции обратного вызова:
Запрос следующего блока прописан не по событию «Принят блок», а по событию «Блок ушёл». Если точнее, то на приём тоже стоит, но только при нулевой длине. Логика такая: новый блок будет принимать, только если освободился буфер. Но если пришёл нулевой пакет – значит, буфер не занят, значит можно и при приёме запустить новый. Прекрасно. Закомментируем соответствующий код в районе отправки:
И уберём условие «если длина нулевая» в районе приёма:
Терминал после этого виснуть перестанет, а вот моя тестовая программа – продолжит. Они сделали так, что данные не обрабатываются, пока не взведён сигнал DTE:
Может, они и правы, но в STM такого нет, и всё работает. Поэтому я во всём файле virtual_com.c закомментировал все условия:
Делаю замер скорости, получаю недостаточно радующий глаз результат:
Половина теоретической скорости (примерно 25 мегабайт в секунду). Но причины всё те же, что и в позапрошлой статье. Пакет приходит, пока мы не подготовились к его приёму. Поэтому хост получает NAK. И библиотека NXP такова, что больше одного запроса отправить невозможно. Там в двух местах стоит защита от этого! Но к счастью, зато размер запроса может достигать вплоть до шестнадцати килобайт! Поставим, скажем, восемь.
Меняем:
![](https://habrastorage.org/r/w1560/webt/mc/3o/l-/mc3ol-9l4ksy2_eoqy42gufjcxe.png)
На:
![](https://habrastorage.org/r/w1560/webt/rq/ry/mt/rqrymtnpssoqsyebhnqd9vqlmbe.png)
Получаем совсем другую картину, которой я в своё время хвастался:
Собственно, у меня всё.
Мы научились работать с UART средствами библиотеки NXP. Также мы освоили GPIO и при оптимизации его работы выяснили, что в контроллере, установленном на плате Teensy 4.1 имеются не только медленные, но и сильно связанные с ядром шины. В следующий раз мы займёмся размещением кода и данных в памяти, подключённой именно через них, чтобы программа работала с максимально возможной скоростью.
![](https://habrastorage.org/webt/ou/zw/ge/ouzwge5ln6hc7j3_gcmqrp84m9m.png)
Но пока что мы просто учились пользоваться всем готовым. Внесённая правка была чисто символической. Сегодня мы научимся работать с UART (это очень важно, так как других средств отладки у платы Teensy 4.1 нет), поиграем с GPIO, разгоним работу с ним в десятки раз, просто подвигав «мышкой», а на закуску – уберём некоторые особенности примера виртуального COM-порта, о которых я говорил в конце прошлой статьи. Приступаем.
Как открыть нужный инструмент
Я, как любитель контроллеров STM32, прекрасно знаю про систему создания и настройки проектов Cube MX. Дальше у ST-шников появился CubeIDE, куда всё это конфигурирование встроено. Не знаю, кто был первым, но в целом, фирма NXP пошла тем же путём: они встроили подсистему для конфигурирования в среду разработки. Эклипса позволяет переключать перспективы. Вот одна из перспектив как раз и предназначена для конфигурирования портов. Иконки, соответствующие перспективам, традиционно располагаются в правом верхнем углу Эклипсы. Вот они:
![](https://habrastorage.org/webt/ui/pf/jd/uipfjdekwwyz9gtg2jjdix1ikyg.png)
Нажимаем на ту, где изображена микросхема с ногами. Мы же будем настраивать ноги.
Работаем с UART
В открывшейся перспективе мы видим три представления: таблица слева, таблица снизу и картинка справа. Я не буду подробно рассказывать о таблицах. Есть документация, есть учебные видео. Все желающие могут посмотреть их. Документация живёт тут: MCUXpresso Config Tools User's Guide (IDE).
В нижней таблице мы уже видим две готовые ножки UART. Где на плате Teensy их искать? Давайте разбираться.
![](https://habrastorage.org/webt/nv/iz/db/nvizdbce1q_d70izhyta06n1kly.png)
Мы видим, что линия 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. Это разные порты!!! Я сначала перепутал! Не повторяйте моих ошибок! Теперь, зная об этом, находим хоть нужные порты, хоть нужные ножки. Вот они:
![](https://habrastorage.org/webt/ag/xk/kx/agxkkxmlxnsodrs8m2pocsax4kc.png)
Получается, что нас интересуют контакты платы Teensy 4.1, отмаркированные как 24 и 25. Именно к ним подключены линии TX и RX соответственно. Берём какой-нибудь сторонний переходник USB-UART и подключаем его к этим ножкам.
Теперь надо узнать скорость порта, ведь это – настоящий UART. Наверное, она спрятана где-то в районе инициализации. Возвращаемся в обычную перспективу:
![](https://habrastorage.org/webt/he/7q/tx/he7qtx5aqe25r7xmyuzstpsj8fa.png)
И начинаем смотреть процесс старта. Вот что-то похожее на правду:
![](https://habrastorage.org/webt/6k/fv/_k/6kfv_kgkpkqhhdyfypsfslr8cfk.png)
Идём в эту функцию:
![](https://habrastorage.org/webt/yk/7z/08/yk7z08nehybprhmfwp5pjtfhud8.png)
Всё понятно. Открываем вновь подключённый переходник в терминале со скоростью 115200. Перезапускаем Teensy. Видим следующее:
![](https://habrastorage.org/webt/ll/6v/6v/ll6v6vt2lfcqdwm393gscvw08mg.png)
Это сработала следующая строка:
![](https://habrastorage.org/webt/vg/wf/pl/vgwfpl_cdctdw3z2bi7zrxzvifq.png)
usb_echo — это макрос, который раскрывается в DbgConsole_Printf. А в прошлой статье я специально ставил галочку, чтобы можно было работать и через обычный printf. Давайте проверим, работает ли этот механизм. Добавим вывод в функцию main:
![](https://habrastorage.org/webt/gn/4l/qe/gn4lqehmlhbn7oz6prz14_0n2ok.png)
Собираем, «прошиваем», запускаем…
![](https://habrastorage.org/webt/5p/ve/rj/5pverjej4atfohdfqkcrdsymnm4.png)
Ну, собственно, этого следовало ожидать. Переходим к более творческой части, к работе с GPIO.
Медленный GPIO
Давайте просто попробуем пошевелить ножкой платы, отмаркированной как 32. Почему ею? Она с краю, её легко найти.
![](https://habrastorage.org/webt/6o/qi/aq/6oqiaqvnjnskeavpwbfnwybditc.png)
Сначала я повторю свою ошибку, чтобы потом эффектно её исправить. Итак. Смотрим схему.
![](https://habrastorage.org/webt/ox/ty/zb/oxtyzbg6xjmtp5uf7ighcepvzpo.png)
Это ножка контроллера C10, порт B0_12. Прекрасно. Идём в ногастую перспективу, в левой таблице в поиск вбиваем B0_12:
![](https://habrastorage.org/webt/it/qw/7u/itqw7ublmonzokcbojrfhq71yse.png)
Кстати, тот случай. Я первый раз впал в ступор, увидев, что порт B0_12 уже занят. Но занят не B0_12, а AD_B0_12. Это видно по номерам ног. Щёлкаем по флажку слева от C10, получаем вот такой перечень возможных альтернатив:
![](https://habrastorage.org/webt/2p/hc/rp/2phcrp2-g7_x2agwfefs-ddl6sc.png)
И вот тут я сейчас и допущу ошибку. Я вижу GPIO2_IO12. Вот около этого порта и ставлю флажок. Я же просто к GPIO подключаюсь.
Новая ножка появилась в нижней строке
![](https://habrastorage.org/webt/de/7j/pi/de7jpindkfjpcbogmd8xtf8lixi.png)
Но она жёлтая. Её надо дозаполнить. Я сделаю это так:
Identifier — MyPin
Direction – Output
GPIO Initial State – Logical 0
Speed – Max
Slew Rate – Fast
Остальное оставлю без изменений.
![](https://habrastorage.org/webt/7c/bn/-q/7cbn-qvmqwgyh24ukeu80vjcnaa.png)
![](https://habrastorage.org/webt/u0/ff/wv/u0ffwvub2nnglve-jo_bqa6zba8.png)
Теперь надо щёлкнуть по кнопке Update Code
![](https://habrastorage.org/webt/bc/fj/iw/bcfjiwlucem1a-za6sjrogcf5aq.png)
По завершении обновления исходников нас автоматически выкинут в перспективу для программиста. Если интересно – в функции Main есть вызов функции BOARD_InitPins():
![](https://habrastorage.org/webt/if/e-/y6/ife-y6qoqsezm-pjjqscy_xxp4c.png)
Перейдя к этой функции, можно убедиться, что новая ножка действительно инициализируется:
![](https://habrastorage.org/webt/fl/-j/6r/fl-j6rmguieyzrnuas8wsmnuc9a.png)
Ну и прекрасно. Давайте сразу после инициализации сделаем бесконечный цикл, шевелящий этой ножкой:
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);
}
Компилим, загружаем, запускаем…
![](https://habrastorage.org/webt/t5/cl/eh/t5clehohlcwnhmuuh8dyhbmgrqk.png)
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 <main+0x2c>)
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 <main+0x2c>)
60002aa4: f000 ffec bl 60003a80 <__GPIO_PinWrite_veneer>
60002aa8: e7f4 b.n 60002a94 <main+0x14>
Никакой оптимизации! Переключаемся в режим Release:
![](https://habrastorage.org/webt/gv/7e/1a/gv7e1atqg_mtjfv1in6vy8poskk.png)
В свойствах проекта включаем максимальную оптимизацию, так как по умолчанию там была оптимизация по размеру, которая не подразумевает максимальной вставки inline повсюду и чуть-чуть ещё. Заодно ставим флажок Enable Link Time Optimization
![](https://habrastorage.org/webt/uz/63/tq/uz63tq1s03i0mc7xmyypunvam-m.png)
Не забываем поправить Post Build Step, как мы делали для Debug сборки в прошлой статье. Ведь для Debug и Release сборок настройки разные! В прошлый раз мы это делали для Debug.
Собираем, смотрим новый ассемблерный код:
186c: 4a1f ldr r2, [pc, #124] ; (18ec <main+0xcd0>)
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 <main+0xc56>
Отлично! Лучше уже не сделать! «Прошиваем», запускаем. Смотрим осциллограмму…
![](https://habrastorage.org/webt/j1/mo/va/j1movak1qvu2rev9qavzndbrsnc.png)
Нет, втрое мы, конечно, разогнали… Но для контроллера, работающего на частоте 600 МГц, это маловато. Напомню, что в одной из предыдущих статей я добавил разгона путём замены механизма «чтение-модификация-запись» на «сброс» и «установка». Правда, судя по ассемблерному коду, тут за нас это сделал оптимизатор. Но давайте я прекращу держать интригу и раскрою карты.
Использование порта, подключённого к другой шине
Шина – великое дело. Ещё в статье про DMA я показал, как латентность шины портит всё впечатление от хорошего механизма. Но в статьях про процессорную систему NIOS II мы уже встречались с такой удивительной шиной, которая подключается напрямую от процессорного ядра к устройству. Например, тут. Вот в контроллере, стоящем в Teensy 4.1, есть такие же шины. Их несколько. И порты могут быть подключены к обычной шине, а могут – к такой удивительной. Это я узнал отсюда.
Я выбрал порт номер два. Надо добавить пять… То есть, должен быть порт номер семь. Выбираем его (не забываем снять галку со второго, иначе будет конфликт):
![](https://habrastorage.org/webt/rm/4v/4v/rm4v4vgzijsa7iqehon8k_bjjvk.png)
Заново заполняем все поля порта в нижней таблице. Всё-таки это новое назначение… Не забываем сделать Update Code. Ну, и собираем, «прошиваем»…
![](https://habrastorage.org/webt/wz/xk/cr/wzxkcrn3octhv7bua5ubvi02oh0.png)
Мой осциллограф показывает 142 мегагерца. Я обещал разгон в десятки раз? Вот он! Начинали мы с шести… А это у нас меандр 142 мегагерца, значит частота записи в порт у нас вдвое больше. Каково?
Пара доработок USB
Ладно, удаляем этот бесконечный цикл while. Давайте переделаем работу с USB так, чтобы можно было делать замеры скорости. В прошлый раз я уже показывал место, где данные перекидываются из приёмника в передатчик:
![](https://habrastorage.org/webt/-e/or/qg/-eorqgd5hmqjcthrw3xcatin3tq.png)
Просто закомментируем этот цикл, выполняющий перенос…
![](https://habrastorage.org/webt/sg/eb/os/sgebosut8x0gd_dpmtb_ysakjjk.png)
Попутно я изменю VID/PID, чтобы устройство стало садиться на драйвер WinUSB, установленный при опытах для позапрошлой статьи:
![](https://habrastorage.org/webt/kj/jm/eg/kjjmegkeejw-sqisyl5_idvfz8u.png)
Будет:
![](https://habrastorage.org/webt/mw/ll/52/mwll52uo78w2wawkn3j6lynrt9k.png)
Можете запустить… Не будет оно работать. Вот в этой функции обратного вызова:
![](https://habrastorage.org/webt/sr/fj/yh/srfjyhgoligvgjc52yglhndqwwq.png)
Запрос следующего блока прописан не по событию «Принят блок», а по событию «Блок ушёл». Если точнее, то на приём тоже стоит, но только при нулевой длине. Логика такая: новый блок будет принимать, только если освободился буфер. Но если пришёл нулевой пакет – значит, буфер не занят, значит можно и при приёме запустить новый. Прекрасно. Закомментируем соответствующий код в районе отправки:
![](https://habrastorage.org/webt/sf/mn/7p/sfmn7pi3tyt_ka_b_dsqaes0--a.png)
И уберём условие «если длина нулевая» в районе приёма:
![](https://habrastorage.org/webt/xc/x2/s6/xcx2s6lhosh9zp3cbv_zxfnxtti.png)
Терминал после этого виснуть перестанет, а вот моя тестовая программа – продолжит. Они сделали так, что данные не обрабатываются, пока не взведён сигнал DTE:
![](https://habrastorage.org/webt/sb/i5/w_/sbi5w_ocqbwjfxr9tbsbsuaq-gm.png)
Может, они и правы, но в STM такого нет, и всё работает. Поэтому я во всём файле virtual_com.c закомментировал все условия:
(1 == s_cdcVcom.startTransactions)
Делаю замер скорости, получаю недостаточно радующий глаз результат:
![](https://habrastorage.org/webt/md/wh/wh/mdwhwh1j6at5qmc9bfc-59vg-vm.png)
Половина теоретической скорости (примерно 25 мегабайт в секунду). Но причины всё те же, что и в позапрошлой статье. Пакет приходит, пока мы не подготовились к его приёму. Поэтому хост получает NAK. И библиотека NXP такова, что больше одного запроса отправить невозможно. Там в двух местах стоит защита от этого! Но к счастью, зато размер запроса может достигать вплоть до шестнадцати килобайт! Поставим, скажем, восемь.
Меняем:
![](https://habrastorage.org/webt/mc/3o/l-/mc3ol-9l4ksy2_eoqy42gufjcxe.png)
На:
![](https://habrastorage.org/webt/rq/ry/mt/rqrymtnpssoqsyebhnqd9vqlmbe.png)
Получаем совсем другую картину, которой я в своё время хвастался:
![](https://habrastorage.org/webt/p4/ar/y9/p4ary9qvuy5n6jptfs_vqrtlopa.png)
Собственно, у меня всё.
Заключение
Мы научились работать с UART средствами библиотеки NXP. Также мы освоили GPIO и при оптимизации его работы выяснили, что в контроллере, установленном на плате Teensy 4.1 имеются не только медленные, но и сильно связанные с ядром шины. В следующий раз мы займёмся размещением кода и данных в памяти, подключённой именно через них, чтобы программа работала с максимально возможной скоростью.