Как я уже несколько раз упоминал в прошлых статьях, я один из разработчиков сервиса All Hardware, через который любой желающий может удалённо поработать с различными отладочными платами, которые туда выкладывают производители микроконтроллеров. По умолчанию, в каждую плату загружено типовое демонстрационное приложение. Проект для самостоятельной сборки этого приложения можно скачать, чтобы начать опыты не с чистого листа. Всё бы ничего, но разные пользователи предпочитают разные среды разработки. Всё многообразие, разумеется, охватить невозможно, но хотя бы Eclipse, а значит, вообще GNU (в случае STM32 — это скорее спецсборка STM32 Cube IDE) и Keil c IAR — стоит. Собственно, мне было поручено произвести хоть какую-то унификацию демонстрационных проектов для плат STM32. В статье я расскажу сначала, как быть простому пользователю, который зашёл на сервис и скачал пример. Что нужно сделать, чтобы собрать его. Ну, а уже затем, будет немножко мемуаров, обосновывающих выбранное решение, а также просто описывающих впечатления о работе.
Давайте сначала посмотрим, что за приложения предлагаются. Входим на сайт https://all-hw.com/.
Там надо авторизоваться (регистрация – свободная). После чего будет выдан перечень доступных плат.
Первая из них не имеет экрана, так что неудобна как иллюстрация к статье. Вторая банальная, она есть у всех, про неё просто не интересно рассказывать. Выберу третью — STM32F469I Discovery.
Пройдя несколько шагов, я попадаю вот на такую страницу:
Собственно, слева мы видим плату, которую снимает камера. И там как раз работает то самое приложение. Просто тикает счётчик. А если в терминале справа ввести строку, она отобразится на экране, так как терминал связан с отладочным портом UART. Ну, вот я ввёл Just Test.
Вроде, приложение для контроллера, чтобы реализовать эту функциональности, не сложное, но… Но… Но повторю, берём число видов ST-шных плат и умножаем минимум на три. И опять же, очень важное условие: время разработки этого приложения не должно быть большим. Надо разработать его, как можно быстрее. И оно должно собираться минимум тремя компиляторами.
Первое, что приходит на ум, это разработать типовое приложение в STM32Cube MX. Там можно и сгенерить всё под три среды разработки, и быстро добавить типовые устройства. Так-то оно так, но:
Собственно, этого достаточно, чтобы отложить такой вариант в резерв и осмотреться, есть ли более простые решения… Оказывается, есть, и они также связаны с Cube MX. Все, кто работал с этой средой, знают, что она состоит из основного кода и пакетов, обслуживающих конкретное семейство контроллеров. Что представляет из себя пакет? Это ZIP-файл. А давайте его скачаем и распакуем. Возьмём, скажем, пакет для новенького STM32H747. У меня это файл en.stm32cubeh7_v1-8-0.zip.
И вот такое богатство мы в нём видим:
Это типовые решения для разных макетных плат с данным типом контроллера. Хорошо. Входим в наш каталог STM32H747I-DISCO. Там есть отдельно готовые приложения и отдельно – примеры для работы с блоками контроллера. Тем, кто просто хочет собрать пример, тут нет ничего интересного, а вот разработчикам типового демо приложения стоит изучить содержимое каталога UART.
А из приложений, ну, разумеется, STemWin. Причём самое простейшее. Hello World. Каталог, в котором оно располагается, будет интересовать всех пользователей.
Свой пример будем делать на базе этого приложения. Почему? Входим в каталог STemWin_HelloWorld и видим:
Программисты фирмы ST всё сделали за нас. Они создали исходники, которые могут быть собраны из Кейла, из ИАРа и из Eclipse (коей по сути является Cube-IDE). Таким образом, достаточно поправить эти исходники, и задача будет решена без правки файлов, зависящих от сред разработки! Ну, а проект Hello World, он же выводит тексты на экран. Достаточно добавить к нему поддержку UART, и всё заработает. Именно поэтому я выше отметил, что разработчикам пример с UARTом тоже полезен.
Причём в данном конкретном случае, я наступаю на горло собственной песне. Если кто-то читал мои предыдущие статьи, он знает, что я терпеть не могу HAL. HAL и оптимизация – две вещи несовместные. В реальных проектах я использую либо прямую работу с железом, либо драйверы Константина Чижова (библиотеку mcucpp). Но в данном конкретном случае, всё совсем не так. Мы просто делаем программку, которая отображает тексты и работает с COM-портом. На контроллере с тактовой частотой в сотни мегагерц. Если честно, во времена оны с этим вполне справлялась обычная БК-шка, процессор которой работал на частоте в 3 МГц. Причём у БК-шки не было RISC-команд, не было даже конвейера. Зато были мультиплексированная шина (то есть, несколько тактов на цикл) и асинхронное динамическое ОЗУ без кэширования. Короче, производительность была всего 300 тысяч операций регистр-регистр в секунду. И этого для решения задачи вывода текстов и работы с UART (через блок ИРПС) хватало. То есть, оптимальности HAL-кода для данной задачи на современных STM32 для поставленных задач тоже вполне хватит. А вот скорость разработки при использовании HAL будет выше всех.
Появится новая плата, к ней точно будет приложен HAL с унифицированными вызовами. Мы подправим в типовом примере инициализацию, согласно приложенному примеру UART, а работа, она всегда одна и та же будет. Скорость разработки – десятки минут. Даже не часы. То есть, для решения данной задачи, однозначно лучше всего использовать именно HAL, хоть для боевых случаев я его и не люблю.
Всё. Минимум теории, без которого нельзя переходить к практике, я рассказал. Более детальные теоретические вещи я расскажу уже после лабораторной работы. Так что переходим к опытам.
Итак. Вы не собираетесь сразу творить что-то своё, а сначала хотите поиграть с готовым примером, взятым с сайта All-Hardware. Что надо, чтобы собрать и запустить его? Сначала надо скачать библиотеки для конкретной платы. Работая с Open Source решениями, я уже сталкивался с тем, что мало скачать проект. Надо следом ещё поставить 100500 инструментов и скачать 100500 сторонних библиотек. Здесь же надо скачать и распаковать всего один файл. Правда, размер у него гигантский. Но и содержимое просто замечательное. Итак. Нам нужны пакеты для STM32 CubeMX. Сейчас прямая ссылка на их хранилище выглядит так:
www.st.com/en/development-tools/stm32cubemx.html#tools-software
Какой именно пакет следует скачать, показано в таблице ниже.
Структура пакетов внутри одинаковая, так что дальше идём в каталог со следующей иерархией:
<Имя пакета>\Projects\<Имя платы>\Applications\STemWin.
Копируем каталог с примером туда. Получаем примерно такой вид:
У данной платы нет экрана, а значит – в фирменном пакете для неё нет каталога STemWin. Поэтому проект следует поместить на следующий уровень:
\STM32Cube_FW_G4_V1.3.0\Projects\B-G474E-DPOW1\Examples\UART
Как просто собрать пример, ясно. Теперь рассмотрим, как эти примеры делаются. Возможно, эта информация пригодится тем, кто хочет сделать что-то, отличное от простого вывода текстов на экран. Ну, и когда через несколько месяцев надо будет внедрять очередную плату, а я уже всё забуду, я сам открою эту статью, чтобы освежить инструкцию в памяти.
В типовом примере STemWin Hello World предсказуемо нет UART. Его надо добавить. Казалось бы, берём типовой код и добавляем. Увы, в жизни всё сложнее. В рабочей функции мы делаем всё именно так. А вот при инициализации есть нюансы. Начнём с того, что на разных макетных платах разные порты подключены к переходнику USB-UART, встроенному в JTAG адаптер. Какой именно подключён, надо смотреть в описании на плату. Обычно, конечно, USART1, но лучше перепроверить.
Дальше разнообразие вносит то, что по мере развития контроллеров, в аппаратуру UART добавляются разные полезные функции. Их тоже надо инициализировать в HAL новых плат. В частности, работу с FIFO.
Поэтому я предлагаю следующие типовые шаги:
С хэндлом всё понятно и универсально. Это будет выглядеть как-то так:
Остальное смотрим в примере их каталога \<Имя Пакета>\Projects\<Имя Платы>\Examples\UART.
Например, проект \STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO\Examples\UART\UART_WakeUpFromStopUsingFIFO.
В функции main() есть инициализация самого UART (надо только убедиться, что скорость 9600, если не так – вписать её). Связь с ножками же настраивается в другом файле, в функции
void HAL_UART_MspInit(UART_HandleTypeDef *huart).
Я предпочитаю располагать всё это в одной функции. Так как время ограничено, то не глядя скомпоновал из этих материалов такую функцию, поместив её в файл main.c и не забыв вызвать её из функции main().
В данном случае, из файла
\STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO\Examples\UART\UART_WakeUpFromStopUsingFIFO\CM7\Src\stm32h7xx_it.c
в файл
\STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO \Projects\STM32H747I-DISCO\Applications\STemWin\Demo_H747\CM7\Core\Src\stm32h7xx_it.c
переносим фрагмент:
Всё.
С основной рабочей функцией, в принципе, всё проще. Хотя, не в принципе, а в частности – не совсем, но об этом мы поговорим ниже. Пока же просто смотрим, какая функция вызывается в конце функции main. В рассматриваемом случае это:
Значит, в ней и творится основная работа. Просто заменяем её тело на типовой пример, взятый с нашего сайта.
Дальше – можно таскать примеры другой аппаратуры, как мы только что поступили с аппаратурой UART. Правда, контроль работы этой аппаратуры выходит за рамки данной статьи. Как видно, типовой контроль на WEB-странице есть только для изображения и для одного UARTа. Остальное пробрасывается чуть сложнее. Тем не менее, я не могу не рассказать, как однажды мне поручили за один вечер сделать генератор морзянки на базе платы STM32G474RE DPOW1 Discovery. Один вечер на разработку с нуля на незнакомой плате не располагает к чтению документации на сотни страниц. Еcли бы проект делался на века, я бы просто начал доказывать руководству, что оно не право, и что всё надо тщательно изучить. Но проект был тоже с краткосрочным жизненным циклом. Поэтому я решил пойти по пути выдёргивания примеров.
Итак, для морзянки нужен синус частотой 1 КГц… Привычным движением руки распаковываем файл en.stm32cubeg4_v1-3-0.zip и осматриваем каталог D:\tmp\STM32Cube_FW_G4_V1.3.0\Projects\B-G474E-DPOW1\Examples… И ничего путного там не находим…
В каталоге DAC нет ничего полезного.
Всё, конец? Не совсем. Посмотрим примеры от других плат с таким же кристаллом (пусть и в других корпусах) … И вот такую красоту мы находим для платы Nucleo!
Красота заключается в том, что внутри файла main.c есть таблица для формирования синуса:
Проверяем и убеждаемся, что да. Этот пример формирует на выходе ЦАП синус или треугольник. Причём как раз с частотой 1 КГц. Ну и отлично! Так как время был ограничено, я даже не стал читать никакую теорию. Просто убедился, что всё формирование идёт на аппаратном уровне, бегло осмотрев код. После чего в проекте заменил контроллер на стоящий в требуемой плате, собрал, залил, запустил и, пробежавшись щупом осциллографа по ножкам, нашёл ту, на которой тот синус присутствует. Дальше – подключил её ко входу колонок, заменил генерацию синуса или треугольника на генерацию синуса или тишины (да, я сделал ещё одну таблицу из одних нулей) … Ну, а написать прикладную часть с морзянкой было проще простого. Вспомнил молодость, военную кафедру, полковника Павлова…
В общем, методика «найти пример, вставить его в свой код», вполне себе действующая. А подход «чтобы собрать типовой пример – скачай огромную библиотеку» этому способствует, ведь все эти фирменные примеры являются её частью.
Один мой коллега любит цитировать следующее философское высказывание:
«В теории нет разницы между теорией и практикой. На практике она есть».
Вот при работе с кучей разных STM32 я регулярно вспоминал его. Про неизбежно разные UARTы я уже сказал, но, казалось бы, унифицированный StemWin не должен преподносить никаких сюрпризов… Преподнёс!
Надпись Hello World рисуется. Но когда переношу рабочий код – вижу синий экран. Просто у нас сначала секунду рисуется красный экран, потом – секунду зелёный, потом – секунду синий, потом – начинается работа. Если поставить точку останова сразу после инициализации, ещё до любой отрисовки, при её срабатывании на экране видны показания таймера от прошлого запуска. Потом они затираются тем самым синим экраном. Что такое?
Убираю вывод трёх цветов по секунде, добавляю сразу работу. Оно работает, но быстро замирает навсегда. Постепенно выясняю, что замирает через 37 миллисекунд. Что за волшебное время такое? Одна миллисекунда – это ясно. Системный тик. Но 37. Да хоть что-то круглое, близкое…
Долго ли, коротко ли, а выясняю, что всё отображается в обработчике прерывания HAL_DSI_IRQHandler(DSI_HandleTypeDef *hdsi). Он вызывается, всё отображается, после чего его вызовы прекращаются. В буфере всё формируется, но на экран не попадает. Если точнее, то на экран всё попадает два раза в жизни программы. При инициализации (тот самый артефакт из прошлой жизни) и через 37 мс. Всё, что было между, никто не увидит.
По уму — надо изучить документацию и разобраться, что там к чему. Но по факту — времени на задачу отпущено не мало, а очень мало. Ясно, что прерывание надо спровоцировать, но как? Честно пытаюсь вызывать GUI_Exec(), хотя там и без того вызывается GUI_Delay()… Не помогает.
Пример мёртвый. Вернее, там забавно. Hello World выводится, так как это происходит в первые 37 мс. А потом – мёртвый пример. Хорошо, беру из того же каталога пример с анимацией. Он работает. Постепенно выясняю, что надо перетащить из него, чтобы заработал наш пример… Вот так выглядит типовой наш код:
Ну логично же! И ведь работает!.. На других платах… А вот после такой правки он хоть как-то зашевелился и на H747:
Но в целом-то работает, а в частности – красный и зелёный экран держатся по секунде, а синий – мелькает и сразу появляется рабочий экран. После небольшого количества опытов выяснилось, что всё начинает полноценно работать в таком виде:
Вот вам и унификация… Возникло подозрение, что виновата настройка:
Но эксперименты с её правкой не дали идеального результата, а изучать детали не позволило время. Может, кто-то в комментариях расскажет, как надо было действовать верно, а данный раздел показывает, как можно действовать в условиях ограниченного времени на разработку. Пока же код остался в таком ужасном виде. Благо, это не учебник, а просто пример работающего кода.
Эта плата старинная, что с нею может быть не так? Рудименты! У старинной платы вылезают отростки, которые имеются, но давно перестали работать. Запускаем пример Hello World и видим:
Изображение повёрнуто на 90 градусов относительно типового. Что может быть проще? В далёком 2016-м году, перетаскивая прошивку 3D принтера MZ3D с Ардуинки на STM32F429, я лично повернул картинку простыми настройками. Ну-ка. Как тут дела обстоят? А вот подходящие настройки!
Пробуем их менять, не помогает. Проверяем, где они используются… А нигде! Они просто объявляются. Подозреваю, что их перестали обрабатывать при внедрении DMA2D, но не поручусь. Тем не менее, есть же функция. Вот совет с десятков форумов. Именно этой функцией пользовался я в 2016-м:
Некоторые ещё добавляют константу отзеркаливания… Но не суть. Не работает эта функция в 2020-м! Не работает и всё тут! А библиотека поставляется в объектном виде, почему не работает – никто не расскажет.
Хорошо, я же всё-таки в экранах разбираюсь. Иду в файл \STM32F429-DISC1\Drivers\BSP\Components\ili9341\ili9341.c (да, он защищён от записи, но это же легко снимается). Там настраивается графический чип. Меняем.
на
Изображение, конечно, поворачивается… Но буфер явно настроен несколько неверно:
Вот эти размеры:
тоже никак не влияют на работу. То же касается и этого участка:
А вот участок поинтереснее:
Я ради интереса добавил отрицание:
Получил вот такую красоту.
Подобную, но не точно такую красоту я получал и при прочих опытах, уже не помню при каких… Короче, мы приходим всё к тому же неутешительному выводу. Надо садиться и разбираться… Но времени на разбирательство не выделено. Что делать? К счастью, мне удалось выпытать ответ на этот вопрос у Гугля. В итоге, типовой код в демо приложении выглядит так:
А для F429 его надо привести к такому виду:
Используется функция вывода повёрнутого текста. Правда, для этого приходится добавлять сущность «прямоугольник». Ну, что ж поделать. И счётчик приходится выводить не функцией печати числа, а сначала формируя строку, а уже затем – выводя её в повёрнутом виде. Но в остальном – всё получилось почти универсально. Можно даже наоборот, выводить тексты везде таким образом, но не везде указывать флаг поворота. Но мне кажется, что это уже будет извращением.
Мы рассмотрели методику разработки типовых программ под различные платы STM32 как с точки зрения разработчика, так и с точки зрения пользователя, который просто скачал демо приложение под свою плату и хочет просто собрать его, не задумываясь о физике. Также мы рассмотрели примеры, показывающие, что, к сожалению, степень унификации кода высока, но не достигает ста процентов. Полученные знания можно проверить, работая поочерёдно с разными платами через сервис All-Hardware, не тратя деньги на их покупку.
Статья показывает, как разобраться во всём довольно поверхностно. Это позволяет быстро освоить конкретную плату. Но, разумеется, чем дольше вы с нею будете разбираться, тем более глубокие знания получите. При решении реальных задач это чрезвычайно важно, так как сложные программы, в которых автор не понимает физики процессов, – путь к ошибкам в алгоритмах. Я желаю всем освоить всё достаточно глубоко. Да-да, используя аппаратуру с сервиса All Hardware.
1. Что за приложения
Давайте сначала посмотрим, что за приложения предлагаются. Входим на сайт https://all-hw.com/.
Там надо авторизоваться (регистрация – свободная). После чего будет выдан перечень доступных плат.
Первая из них не имеет экрана, так что неудобна как иллюстрация к статье. Вторая банальная, она есть у всех, про неё просто не интересно рассказывать. Выберу третью — STM32F469I Discovery.
Пройдя несколько шагов, я попадаю вот на такую страницу:
Собственно, слева мы видим плату, которую снимает камера. И там как раз работает то самое приложение. Просто тикает счётчик. А если в терминале справа ввести строку, она отобразится на экране, так как терминал связан с отладочным портом UART. Ну, вот я ввёл Just Test.
Вроде, приложение для контроллера, чтобы реализовать эту функциональности, не сложное, но… Но… Но повторю, берём число видов ST-шных плат и умножаем минимум на три. И опять же, очень важное условие: время разработки этого приложения не должно быть большим. Надо разработать его, как можно быстрее. И оно должно собираться минимум тремя компиляторами.
2. Возможные пути
Первое, что приходит на ум, это разработать типовое приложение в STM32Cube MX. Там можно и сгенерить всё под три среды разработки, и быстро добавить типовые устройства. Так-то оно так, но:
- Если посмотреть на примеры работы с приложениями, созданными через CubeMX, то видно, что там после автоматического создания всё равно надо много что дорабатывать напильником. То есть, время хоть и экономится, но всё-таки не максимально.
- Под каждую среду разработки надо создавать проект индивидуально, хоть все три и окажутся в одном и том же рабочем каталоге.
- В современной среде Cube MX я не вижу возможности добавить работу через STemWin, а тексты быстрее всего выводить через неё (уже когда статья была загружена на Хабр, но ещё не опубликована, я выяснил, что неугомонные STшники заменили STemWin на TouchGFX, но всё равно предыдущие два других пункта – тоже весомые).
Собственно, этого достаточно, чтобы отложить такой вариант в резерв и осмотреться, есть ли более простые решения… Оказывается, есть, и они также связаны с Cube MX. Все, кто работал с этой средой, знают, что она состоит из основного кода и пакетов, обслуживающих конкретное семейство контроллеров. Что представляет из себя пакет? Это ZIP-файл. А давайте его скачаем и распакуем. Возьмём, скажем, пакет для новенького STM32H747. У меня это файл en.stm32cubeh7_v1-8-0.zip.
И вот такое богатство мы в нём видим:
Это типовые решения для разных макетных плат с данным типом контроллера. Хорошо. Входим в наш каталог STM32H747I-DISCO. Там есть отдельно готовые приложения и отдельно – примеры для работы с блоками контроллера. Тем, кто просто хочет собрать пример, тут нет ничего интересного, а вот разработчикам типового демо приложения стоит изучить содержимое каталога UART.
А из приложений, ну, разумеется, STemWin. Причём самое простейшее. Hello World. Каталог, в котором оно располагается, будет интересовать всех пользователей.
Свой пример будем делать на базе этого приложения. Почему? Входим в каталог STemWin_HelloWorld и видим:
Программисты фирмы ST всё сделали за нас. Они создали исходники, которые могут быть собраны из Кейла, из ИАРа и из Eclipse (коей по сути является Cube-IDE). Таким образом, достаточно поправить эти исходники, и задача будет решена без правки файлов, зависящих от сред разработки! Ну, а проект Hello World, он же выводит тексты на экран. Достаточно добавить к нему поддержку UART, и всё заработает. Именно поэтому я выше отметил, что разработчикам пример с UARTом тоже полезен.
Причём в данном конкретном случае, я наступаю на горло собственной песне. Если кто-то читал мои предыдущие статьи, он знает, что я терпеть не могу HAL. HAL и оптимизация – две вещи несовместные. В реальных проектах я использую либо прямую работу с железом, либо драйверы Константина Чижова (библиотеку mcucpp). Но в данном конкретном случае, всё совсем не так. Мы просто делаем программку, которая отображает тексты и работает с COM-портом. На контроллере с тактовой частотой в сотни мегагерц. Если честно, во времена оны с этим вполне справлялась обычная БК-шка, процессор которой работал на частоте в 3 МГц. Причём у БК-шки не было RISC-команд, не было даже конвейера. Зато были мультиплексированная шина (то есть, несколько тактов на цикл) и асинхронное динамическое ОЗУ без кэширования. Короче, производительность была всего 300 тысяч операций регистр-регистр в секунду. И этого для решения задачи вывода текстов и работы с UART (через блок ИРПС) хватало. То есть, оптимальности HAL-кода для данной задачи на современных STM32 для поставленных задач тоже вполне хватит. А вот скорость разработки при использовании HAL будет выше всех.
Появится новая плата, к ней точно будет приложен HAL с унифицированными вызовами. Мы подправим в типовом примере инициализацию, согласно приложенному примеру UART, а работа, она всегда одна и та же будет. Скорость разработки – десятки минут. Даже не часы. То есть, для решения данной задачи, однозначно лучше всего использовать именно HAL, хоть для боевых случаев я его и не люблю.
Всё. Минимум теории, без которого нельзя переходить к практике, я рассказал. Более детальные теоретические вещи я расскажу уже после лабораторной работы. Так что переходим к опытам.
3. Что делать конечному пользователю
3.1 Скачиваем пакет
Итак. Вы не собираетесь сразу творить что-то своё, а сначала хотите поиграть с готовым примером, взятым с сайта All-Hardware. Что надо, чтобы собрать и запустить его? Сначала надо скачать библиотеки для конкретной платы. Работая с Open Source решениями, я уже сталкивался с тем, что мало скачать проект. Надо следом ещё поставить 100500 инструментов и скачать 100500 сторонних библиотек. Здесь же надо скачать и распаковать всего один файл. Правда, размер у него гигантский. Но и содержимое просто замечательное. Итак. Нам нужны пакеты для STM32 CubeMX. Сейчас прямая ссылка на их хранилище выглядит так:
www.st.com/en/development-tools/stm32cubemx.html#tools-software
Какой именно пакет следует скачать, показано в таблице ниже.
Плата | Пакет |
---|---|
STM32F429I Discovery | STM32CubeF4 |
STM32F469I Discovery | STM32CubeF4 |
STM32G474RE DPOW1 Discovery (не имеет экрана) | STM32CubeG4 |
STM32F746G Discovery | STM32CubeF7 |
STM32H747I Discovery | STM32CubeH7 |
3.2 Копируем проект и приступаем к работе
Структура пакетов внутри одинаковая, так что дальше идём в каталог со следующей иерархией:
<Имя пакета>\Projects\<Имя платы>\Applications\STemWin.
Копируем каталог с примером туда. Получаем примерно такой вид:
Положение каталога важно, так как пути к библиотекам прописаны в относительном формате. Если проект не собирается из-за огромного числа отсутствующих файлов, вы не угадали с его расположением в иерархии каталогов.Входим в каталог, выбираем вариант проекта для одной из сред разработки, открываем проект, работаем с ним… Конец инструкции!
3.3 Особенность платы STM32G474RE DPOW1 Discovery
У данной платы нет экрана, а значит – в фирменном пакете для неё нет каталога STemWin. Поэтому проект следует поместить на следующий уровень:
\STM32Cube_FW_G4_V1.3.0\Projects\B-G474E-DPOW1\Examples\UART
4. Как проекты создаются
Как просто собрать пример, ясно. Теперь рассмотрим, как эти примеры делаются. Возможно, эта информация пригодится тем, кто хочет сделать что-то, отличное от простого вывода текстов на экран. Ну, и когда через несколько месяцев надо будет внедрять очередную плату, а я уже всё забуду, я сам открою эту статью, чтобы освежить инструкцию в памяти.
4.1 Добавление и инициализация UART
В типовом примере STemWin Hello World предсказуемо нет UART. Его надо добавить. Казалось бы, берём типовой код и добавляем. Увы, в жизни всё сложнее. В рабочей функции мы делаем всё именно так. А вот при инициализации есть нюансы. Начнём с того, что на разных макетных платах разные порты подключены к переходнику USB-UART, встроенному в JTAG адаптер. Какой именно подключён, надо смотреть в описании на плату. Обычно, конечно, USART1, но лучше перепроверить.
Дальше разнообразие вносит то, что по мере развития контроллеров, в аппаратуру UART добавляются разные полезные функции. Их тоже надо инициализировать в HAL новых плат. В частности, работу с FIFO.
Поэтому я предлагаю следующие типовые шаги:
- Добавить хэндл порта.
- Добавить инициализацию аппаратуры UART.
- Добавить инициализацию ножек UART.
- Добавить обработчик прерывания UART.
С хэндлом всё понятно и универсально. Это будет выглядеть как-то так:
UART_HandleTypeDef huart1;
Остальное смотрим в примере их каталога \<Имя Пакета>\Projects\<Имя Платы>\Examples\UART.
Например, проект \STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO\Examples\UART\UART_WakeUpFromStopUsingFIFO.
В функции main() есть инициализация самого UART (надо только убедиться, что скорость 9600, если не так – вписать её). Связь с ножками же настраивается в другом файле, в функции
void HAL_UART_MspInit(UART_HandleTypeDef *huart).
Я предпочитаю располагать всё это в одной функции. Так как время ограничено, то не глядя скомпоновал из этих материалов такую функцию, поместив её в файл main.c и не забыв вызвать её из функции main().
Код такой функции
static void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
GPIO_InitTypeDef GPIO_InitStruct;
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
// __HAL_RCC_LPUART1_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart1.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart1, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart1, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_EnableFifoMode(&huart1) != HAL_OK)
{
Error_Handler();
}
/* Enable the UART RX FIFO threshold interrupt */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXFT);
/* Enable the UART wakeup from stop mode interrupt */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_WUF);
/* USER CODE BEGIN USART3_Init 2 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*##-2- Configure peripheral GPIO ##########################################*/
/* UART TX GPIO pin configuration */
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* UART RX GPIO pin configuration */
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* NVIC for USART */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 1);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE END USART1_Init 2 */
}
Обратите внимание, что в фирменных примерах часто используются порты, которые не выведены на адаптер JTAG. Сверяйте номера портов со схемой макетной платы. При несовпадении — правьте на тот порт и те ножки, которые подключены именно к JTAG адаптеру, так как именно он на сервере смотрит в терминал. В наших примерах всё уже настроено корректно.Ну, а прерывания – там всё просто. Ищем файл с суффиксом _it.c в примере и переносим UART-овские строчки в файл с суффиксом _it нашего проекта.
В данном случае, из файла
\STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO\Examples\UART\UART_WakeUpFromStopUsingFIFO\CM7\Src\stm32h7xx_it.c
в файл
\STM32Cube_FW_H7_V1.8.0\Projects\STM32H747I-DISCO \Projects\STM32H747I-DISCO\Applications\STemWin\Demo_H747\CM7\Core\Src\stm32h7xx_it.c
переносим фрагмент:
extern UART_HandleTypeDef huart1;
…
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
Всё.
4.2 Правка основной рабочей функции
С основной рабочей функцией, в принципе, всё проще. Хотя, не в принципе, а в частности – не совсем, но об этом мы поговорим ниже. Пока же просто смотрим, какая функция вызывается в конце функции main. В рассматриваемом случае это:
MainTask();
Значит, в ней и творится основная работа. Просто заменяем её тело на типовой пример, взятый с нашего сайта.
4.3 Что дальше?
Дальше – можно таскать примеры другой аппаратуры, как мы только что поступили с аппаратурой UART. Правда, контроль работы этой аппаратуры выходит за рамки данной статьи. Как видно, типовой контроль на WEB-странице есть только для изображения и для одного UARTа. Остальное пробрасывается чуть сложнее. Тем не менее, я не могу не рассказать, как однажды мне поручили за один вечер сделать генератор морзянки на базе платы STM32G474RE DPOW1 Discovery. Один вечер на разработку с нуля на незнакомой плате не располагает к чтению документации на сотни страниц. Еcли бы проект делался на века, я бы просто начал доказывать руководству, что оно не право, и что всё надо тщательно изучить. Но проект был тоже с краткосрочным жизненным циклом. Поэтому я решил пойти по пути выдёргивания примеров.
Итак, для морзянки нужен синус частотой 1 КГц… Привычным движением руки распаковываем файл en.stm32cubeg4_v1-3-0.zip и осматриваем каталог D:\tmp\STM32Cube_FW_G4_V1.3.0\Projects\B-G474E-DPOW1\Examples… И ничего путного там не находим…
В каталоге DAC нет ничего полезного.
Всё, конец? Не совсем. Посмотрим примеры от других плат с таким же кристаллом (пусть и в других корпусах) … И вот такую красоту мы находим для платы Nucleo!
Красота заключается в том, что внутри файла main.c есть таблица для формирования синуса:
/* Sine wave values for a complete symbol */
uint16_t sinewave[60] = {
0x07ff,0x08cb,0x0994,0x0a5a,0x0b18,0x0bce,0x0c79,0x0d18,0x0da8,0x0e29,0x0e98,0x0ef4,0x0f3e,0x0f72,0x0f92,0x0f9d,
0x0f92,0x0f72,0x0f3e,0x0ef4,0x0e98,0x0e29,0x0da8,0x0d18,0x0c79,0x0bce,0x0b18,0x0a5a,0x0994,0x08cb,0x07ff,0x0733,
0x066a,0x05a4,0x04e6,0x0430,0x0385,0x02e6,0x0256,0x01d5,0x0166,0x010a,0x00c0,0x008c,0x006c,0x0061,0x006c,0x008c,
0x00c0,0x010a,0x0166,0x01d5,0x0256,0x02e6,0x0385,0x0430,0x04e6,0x05a4,0x066a,0x0733};
Проверяем и убеждаемся, что да. Этот пример формирует на выходе ЦАП синус или треугольник. Причём как раз с частотой 1 КГц. Ну и отлично! Так как время был ограничено, я даже не стал читать никакую теорию. Просто убедился, что всё формирование идёт на аппаратном уровне, бегло осмотрев код. После чего в проекте заменил контроллер на стоящий в требуемой плате, собрал, залил, запустил и, пробежавшись щупом осциллографа по ножкам, нашёл ту, на которой тот синус присутствует. Дальше – подключил её ко входу колонок, заменил генерацию синуса или треугольника на генерацию синуса или тишины (да, я сделал ещё одну таблицу из одних нулей) … Ну, а написать прикладную часть с морзянкой было проще простого. Вспомнил молодость, военную кафедру, полковника Павлова…
В общем, методика «найти пример, вставить его в свой код», вполне себе действующая. А подход «чтобы собрать типовой пример – скачай огромную библиотеку» этому способствует, ведь все эти фирменные примеры являются её частью.
5. Проблемы в унификации
Один мой коллега любит цитировать следующее философское высказывание:
«В теории нет разницы между теорией и практикой. На практике она есть».
Вот при работе с кучей разных STM32 я регулярно вспоминал его. Про неизбежно разные UARTы я уже сказал, но, казалось бы, унифицированный StemWin не должен преподносить никаких сюрпризов… Преподнёс!
5.1 STM32H747
Надпись Hello World рисуется. Но когда переношу рабочий код – вижу синий экран. Просто у нас сначала секунду рисуется красный экран, потом – секунду зелёный, потом – секунду синий, потом – начинается работа. Если поставить точку останова сразу после инициализации, ещё до любой отрисовки, при её срабатывании на экране видны показания таймера от прошлого запуска. Потом они затираются тем самым синим экраном. Что такое?
Убираю вывод трёх цветов по секунде, добавляю сразу работу. Оно работает, но быстро замирает навсегда. Постепенно выясняю, что замирает через 37 миллисекунд. Что за волшебное время такое? Одна миллисекунда – это ясно. Системный тик. Но 37. Да хоть что-то круглое, близкое…
Долго ли, коротко ли, а выясняю, что всё отображается в обработчике прерывания HAL_DSI_IRQHandler(DSI_HandleTypeDef *hdsi). Он вызывается, всё отображается, после чего его вызовы прекращаются. В буфере всё формируется, но на экран не попадает. Если точнее, то на экран всё попадает два раза в жизни программы. При инициализации (тот самый артефакт из прошлой жизни) и через 37 мс. Всё, что было между, никто не увидит.
По уму — надо изучить документацию и разобраться, что там к чему. Но по факту — времени на задачу отпущено не мало, а очень мало. Ясно, что прерывание надо спровоцировать, но как? Честно пытаюсь вызывать GUI_Exec(), хотя там и без того вызывается GUI_Delay()… Не помогает.
Пример мёртвый. Вернее, там забавно. Hello World выводится, так как это происходит в первые 37 мс. А потом – мёртвый пример. Хорошо, беру из того же каталога пример с анимацией. Он работает. Постепенно выясняю, что надо перетащить из него, чтобы заработал наш пример… Вот так выглядит типовой наш код:
GUI_SetBkColor(GUI_RED);
GUI_Clear();
GUI_Delay(1000);
GUI_SetBkColor(GUI_GREEN);
GUI_Clear();
GUI_Delay(1000);
GUI_SetBkColor(GUI_BLUE);
GUI_Clear();
GUI_Delay(1000);
Ну логично же! И ведь работает!.. На других платах… А вот после такой правки он хоть как-то зашевелился и на H747:
То же самое текстом.
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_RED);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_Delay(1000);
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_GREEN);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_Delay(1000);
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_BLUE);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_Delay(1000);
Но в целом-то работает, а в частности – красный и зелёный экран держатся по секунде, а синий – мелькает и сразу появляется рабочий экран. После небольшого количества опытов выяснилось, что всё начинает полноценно работать в таком виде:
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_RED);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_Delay(1000);
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_GREEN);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_Delay(1000);
GUI_MULTIBUF_Begin();
GUI_SetBkColor(GUI_BLUE);
GUI_Clear();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_MULTIBUF_Begin();
GUI_MULTIBUF_End();
GUI_Delay(1000);
Вот вам и унификация… Возникло подозрение, что виновата настройка:
/* Define the number of buffers to use (minimum 1) */
#define NUM_BUFFERS 3
Но эксперименты с её правкой не дали идеального результата, а изучать детали не позволило время. Может, кто-то в комментариях расскажет, как надо было действовать верно, а данный раздел показывает, как можно действовать в условиях ограниченного времени на разработку. Пока же код остался в таком ужасном виде. Благо, это не учебник, а просто пример работающего кода.
5.2 STM32F429
Эта плата старинная, что с нею может быть не так? Рудименты! У старинной платы вылезают отростки, которые имеются, но давно перестали работать. Запускаем пример Hello World и видим:
Изображение повёрнуто на 90 градусов относительно типового. Что может быть проще? В далёком 2016-м году, перетаскивая прошивку 3D принтера MZ3D с Ардуинки на STM32F429, я лично повернул картинку простыми настройками. Ну-ка. Как тут дела обстоят? А вот подходящие настройки!
#define LCD_SWAP_XY 1
#define LCD_MIRROR_Y 1
Пробуем их менять, не помогает. Проверяем, где они используются… А нигде! Они просто объявляются. Подозреваю, что их перестали обрабатывать при внедрении DMA2D, но не поручусь. Тем не менее, есть же функция. Вот совет с десятков форумов. Именно этой функцией пользовался я в 2016-м:
GUI_SetOrientation(GUI_SWAP_XY)
Некоторые ещё добавляют константу отзеркаливания… Но не суть. Не работает эта функция в 2020-м! Не работает и всё тут! А библиотека поставляется в объектном виде, почему не работает – никто не расскажет.
Хорошо, я же всё-таки в экранах разбираюсь. Иду в файл \STM32F429-DISC1\Drivers\BSP\Components\ili9341\ili9341.c (да, он защищён от записи, но это же легко снимается). Там настраивается графический чип. Меняем.
То же самое текстом.
ili9341_WriteReg(LCD_MAC);
ili9341_WriteData(0xC8);
на
То же самое текстом.
ili9341_WriteReg(LCD_MAC);
ili9341_WriteData(0x48|0x20);
Изображение, конечно, поворачивается… Но буфер явно настроен несколько неверно:
Вот эти размеры:
#define XSIZE_PHYS 240
#define YSIZE_PHYS 320
тоже никак не влияют на работу. То же касается и этого участка:
#define ILI9341_LCD_PIXEL_WIDTH ((uint16_t)240)
#define ILI9341_LCD_PIXEL_HEIGHT ((uint16_t)320)
А вот участок поинтереснее:
if (LCD_GetSwapXYEx(0)) {
LCD_SetSizeEx (0, YSIZE_PHYS, XSIZE_PHYS);
LCD_SetVSizeEx(0, YSIZE_PHYS * NUM_VSCREENS, XSIZE_PHYS);
} else {
LCD_SetSizeEx (0, XSIZE_PHYS, YSIZE_PHYS);
LCD_SetVSizeEx(0, XSIZE_PHYS, YSIZE_PHYS * NUM_VSCREENS);
}
Я ради интереса добавил отрицание:
То же самое текстом.
if (!LCD_GetSwapXYEx(0)) {
LCD_SetSizeEx (0, YSIZE_PHYS, XSIZE_PHYS);
LCD_SetVSizeEx(0, YSIZE_PHYS * NUM_VSCREENS, XSIZE_PHYS);
} else {
LCD_SetSizeEx (0, XSIZE_PHYS, YSIZE_PHYS);
LCD_SetVSizeEx(0, XSIZE_PHYS, YSIZE_PHYS * NUM_VSCREENS);
}
Получил вот такую красоту.
Подобную, но не точно такую красоту я получал и при прочих опытах, уже не помню при каких… Короче, мы приходим всё к тому же неутешительному выводу. Надо садиться и разбираться… Но времени на разбирательство не выделено. Что делать? К счастью, мне удалось выпытать ответ на этот вопрос у Гугля. В итоге, типовой код в демо приложении выглядит так:
GUI_DispStringHCenterAt("www.all-hw.com", xSize / 2, 20);
А для F429 его надо привести к такому виду:
GUI_RECT Rect = {20-10, 0, 20+10, xSize};
GUI_DispStringInRectEx("www.all-hw.com", &Rect,
GUI_TA_HCENTER | GUI_TA_VCENTER,
20, GUI_ROTATE_CCW);
Используется функция вывода повёрнутого текста. Правда, для этого приходится добавлять сущность «прямоугольник». Ну, что ж поделать. И счётчик приходится выводить не функцией печати числа, а сначала формируя строку, а уже затем – выводя её в повёрнутом виде. Но в остальном – всё получилось почти универсально. Можно даже наоборот, выводить тексты везде таким образом, но не везде указывать флаг поворота. Но мне кажется, что это уже будет извращением.
6. Заключение
Мы рассмотрели методику разработки типовых программ под различные платы STM32 как с точки зрения разработчика, так и с точки зрения пользователя, который просто скачал демо приложение под свою плату и хочет просто собрать его, не задумываясь о физике. Также мы рассмотрели примеры, показывающие, что, к сожалению, степень унификации кода высока, но не достигает ста процентов. Полученные знания можно проверить, работая поочерёдно с разными платами через сервис All-Hardware, не тратя деньги на их покупку.
Статья показывает, как разобраться во всём довольно поверхностно. Это позволяет быстро освоить конкретную плату. Но, разумеется, чем дольше вы с нею будете разбираться, тем более глубокие знания получите. При решении реальных задач это чрезвычайно важно, так как сложные программы, в которых автор не понимает физики процессов, – путь к ошибкам в алгоритмах. Я желаю всем освоить всё достаточно глубоко. Да-да, используя аппаратуру с сервиса All Hardware.