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

Микроконтроллер

Перевод Клоны STM32 хороший, плохой, злой

09.11.2020 12:16:47 | Автор: admin
После того, как некий продукт становится популярным, у компаний, которые могли бы его создать, но не сделали этого, неизбежно возникает желание прокатиться на волне его популярности. Это лишь вопрос времени. Именно этот феномен в ответе за то, что было создано так много ужасных детских игрушек и компьютерных игр. Проявляется он и в мире электроники. Поэтому неудивительным должно выглядеть то, что произошло с чрезвычайно успешной серией микроконтроллеров (Microcontroller Unit, MCU) STMicroelectronics, основанных на ARM. На долю этих контроллеров выпало немалое количество имитаций, клонов и явных подделок.



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

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

Анатомия фальшивки


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


Слева настоящая микросхема STM32, а справа поддельная. На её корпусе есть дополнительные углубления

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


STM32FEB это не настоящий STM32

Куда коварнее, возможно, чипы, которые можно назвать полуподделками. Они, на первый взгляд могут выглядеть как настоящие. Но их выдаёт маркировка. Выглядит она как STM32FEBKC6. У компании STMicroelectronics нет компонентов с такой маркировкой. Наличие на чипе подобной маркировки служит важным признаком его поддельности. Это клон STM32, столкнувшись с которым, можно нажить себе кучу проблем. Хотя подобные микроконтроллеры и работоспособны, они представляют собой урезанную версию STM32F103, в которой реализованы не все возможности настоящих чипов. О них, кроме того, очень сложно найти подробные сведения.

Копии талантливых подражателей


А тут мы поговорим о самых хитрых клонах, представленных в виде MCU CS32F103. Такие клоны работают точно так же, как настоящие чипы, на них нормально запускается код Blinky, скомпилированный для STM32F103. Некоторые из них могут быть даже маркированы как микросхемы производства STMicroelectronics. Это усложняет их уверенную идентификацию.


CS32F103 более честный клон

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

Главное отличие этих копий от оригиналов становится заметным при получении сообщения об ошибке UNEXPECTED idcode: 0x2ba01477 в ходе загрузки кода на них. Правда, эту проблему можно решить, например, используя соответствующий конфигурационный скрипт в OpenOCD.

Клоны производства GigaDevice


Возможно, самый известный создатель клонов STM32 это компания GigaDevice со своими микроконтроллерами GD32. Как отмечено в этом материале, GD32F103, по-видимому, является более быстрой и функциональной версией STM32F103. GD32F103 имеет более высокую тактовую частоту и более быструю флеш-память. А изучение разобранного чипа показало, что в корпусе имеются две микросхемы. Одна это основной чип, а вторая это флеш-хранилище. А такое внутреннее устройство GD32F103 позволяет довольно гибко менять размер памяти чипов, производимых GigaDevice.


Разобранный GD32F103. Поверх основной микросхемы видна отдельная флеш-микросхема

На первый взгляд чипы GD32 выглядят привлекательнее, чем STM32F1. Их тактовая частота выше (108 против 72 Мгц), в них более быстрая память. Хотя флеш-хранилище GD32 должно было бы работать очень медленно, так как это SPI ROM, в микроконтроллере используется SRAM главного чипа в роли кеша для флеш-хранилища, что приводит к тому, что это хранилище быстрее того, которое входит в состав основного чипа. Оно не пребывает в состоянии ожидания даже при работе микроконтроллера на полной доступной ему тактовой частоте.

Минус использования SRAM в связке с флеш-памятью заключается в том, что это повышает энергопотребление устройства. Это, кроме того, вызывает задержку при загрузке (небольшую), вызванную тем, что содержимое SPI ROM копируется в SRAM до того, как сможет загрузиться прошивка. В зависимости от конкретной ситуации это может быть и достоинством, и недостатком. Это, естественно, тот же подход, который используется в микроконтроллер ESP8266, где для хранения прошивки тоже используется внешняя SPI ROM.

Правда, если говорить о других устройствах GD32, то, видимо, их разработчики не так сильно стремились к созданию прямых клонов. Так, микроконтроллер GDF303 использует ту же периферию, что и GDF103, хотя периферия STM32F3, пожалуй, лучше. Это, кроме того, не позволяет использовать GDF303 на платах, рассчитанных на STM32F3xx. На решение вопроса об использовании GD32 может повлиять отношение того, кто решает этот вопрос, к периферии STM32F1.

Они повсюду



Настоящий MCU CH32F103 на плате Blue Pill

Хотя я и знала о вышеописанных подделках и клонах, я, тем не менее, недавно встретилась с кое-чем новым из этой сферы. Я купила несколько плат Blue Pill с чипами STM32F103 у крупного немецкого продавца, занимающегося импортом подобных вещей. Я этим не горжусь, но мне нужны были дешёвые платы для датчиков BlackMagic, а предложение выглядело очень уж заманчивым. В комментариях к товарам на Amazon одни говорили, что получили платы с настоящими чипами, другие же говорили о подделках.

Меня влекло нездоровое любопытство, я получила несколько таких плат, после чего, со смесью ужаса и удовольствия заметила, что на них установлены вовсе не обещанные контроллеры STM32F103C8T6. На них стояли чипы CH32F103C8T6. Хорошо хоть они не прикидывались настоящими STM32.

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

Присоединив плату к ST-Link/V2 и подключившись к ней с помощью OpenOCD, я получила ту же ошибку CPUTAPID, которую выдают CS32F103 при использовании конфигурационного файла, рассчитанного на STM32F1xx. После того, как я отредактировала файл stm32f1xx.cfg, последовав найденным в интернете советам, я смогла без проблем прошить пример Blinkу из моего STM32-проекта Nodate.

Это говорит о том, что, по крайней мере, базовое управление тактированием и сбросом, GPIO и системный таймер достаточно похожи на то, что имеется в STM32. В результате система смогла пройти этот простой тест. Ещё мне хотелось бы исследовать то, как тут работают USART, DMA, SPI, I2C и I2S, узнать, соответствует ли их функционирование тому, что показывают STM32F103, которые имеются на нескольких моих платах. Если эти микроконтроллеры это нечто вроде CS32F103, то я думаю, что они, скорее всего, будут работать так же, как STM32F103.

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

Пришло время для паники?


Внимательный читатель, возможно, заметил, что практически все рассмотренные клоны имитируют первое поколение микроконтроллеров STMicroelectronics на Cortex-M (серию STM32F1). В результате оказывается, что происходящее может обеспокоить лишь тех, кому нужно покупать платы Blue Pill для коммерческих проектов. Вряд ли это особо потревожит тех, для кого возня с STM32 это хобби, и тех, кто держит у себя несколько дешёвых плат с Cortex-M3 на борту для разных домашних проектов. Если заказывать микроконтроллер и платы у надёжных поставщиков, вроде Digikey и Mouser, то о подделках тоже можно не волноваться.

Платы Blue Pill в последнее время претерпели некоторые улучшения, что выражается в выходе их новой версии с MCU STM32F4, которую, из-за её чёрного цвета, называют Black Pill. Хотя эти новые платы и немного дороже тех, что основаны на STM32F103, они дают разработчику значительно больше ресурсов и гораздо более интересную (как мне кажется) периферию. Это может повлиять на рынок плат, основанных на STM32F103, в результате чего ослабеет поток бесчисленных клонов, подделок и копий STM32F103.

Правда, так будет лишь до того момента, пока на рынок не попадут первые партии поддельных, клонированных и скопированных MCU STM32F401 и STM32F411. Вряд ли с этим можно что-то сделать.

А вам попадались поддельные STM32?



Подробнее..

Перевод Практический взгляд на Raspberry Pi Pico с точки зрения STM32

19.06.2021 14:07:57 | Автор: admin
Сравнительно недавно Raspberry Pi Foundation выпустила плату Raspberry Pi Pico, основанную на микроконтроллере (Micro Controller Unit, MCU) RP2040. Эта плата привлекла большое внимание членов сообщества разработчиков различных электронных устройств. Появилось довольно много проектов, в которых используются программируемые модули ввода-вывода (Programmable I/O, PIO) Raspberry Pi Pico. Например, это проект PicoDVI, в котором конечные автоматы PIO используются для вывода DVI-сигнала.



Но с появлением Raspberry Pi Pico связано не только радостное возбуждение разработчиков электроники. Это событие заставило сообщество задаться важным вопросом о том, окажет ли появление платы какое-то ощутимое влияние на тех, кто пользуется STM32, SAM и другими микроконтроллерами, основанными на Cortex-M. Станет ли микроконтроллер RP2040 жизнеспособным выбором для некоторых из проектов, в которых используются похожие MCU? Учитывая то, что в состав RP2040 входит двухъядерный процессор ARM Cortex-M0+, кажется справедливой идея использования этого микроконтроллера там же, где применяются 32-битные MCU от ведущих производителей компонентов такого рода, в частности, от STMicroelectronics.

Сможет ли небольшой проект Raspberry Pi Foundation показать инженерам STM как надо делать микроконтроллеры, или создателям платы на RP2040 стоит пересмотреть некоторые из своих гипотез? Сложно ли будет портировать на RP2040 низкоуровневый код, рассчитанный на STM32?

Сложно ли перенести STM32-проект на RP2040?


Короче говоря, когда я обратила внимание на RP2040, мне подумалось, что будет интересно попытаться портировать на новый микроконтроллер мой C++-фреймворк для STM32. Правда, эта идея меня заинтересовала не из-за двухъядерного ARM Cortex-M0+. У меня есть двухъядерные микроконтроллеры STM32H7 (M4 и M7), которые, за счёт более совершенных характеристик, легко обойдут RP2040. Сильнее всего меня заинтриговали программируемые модули ввода-вывода RP2040, возникало такое ощущение, что они достойны того, чтобы познакомиться с ними поближе.


Плата Raspberry Pi Pico, основанная на RP2040 подключена к одноплатному компьютеру Raspberry Pi, играющему роль SWD-адаптера (оригинал)

Основываясь на опыте работы с STM32 я поняла, что смогу быстро портировать некоторые файлы, создав в репозитории проекта ветку RP, рассчитанную на другую архитектуру, и принявшись за дело. Ведь и в основном проекте, и в новой ветке код будет рассчитан на Cortex-M. Обычно работа с новым ARM-микроконтроллером заключается в том, чтобы найти даташит, справочное руководство и CMSIS-файлы для соответствующего устройства. А потом существующий низкоуровневый код можно легко адаптировать под новый подход к именованию периферийных устройств и под новую схему регистров, учитывая то, что фундаментальные компоненты нового и старого микроконтроллеров (SysTick, NVIC и так далее) ничем не отличаются.

Может, я поступила слишком опрометчиво, но я заказала плату Raspberry Pi Pico, даже не поинтересовавшись тем, есть ли для неё CMSIS-файлы, и даже не взглянув в справочное руководство по ней. Позже я, к своему удивлению, выяснила, что пока нет даже и речи о наличии CMSIS-файлов для Raspberry Pi Pico, или хотя бы о возможности взаимодействия RP2040 с другими устройствами из экосистемы Cortex-M. Но при этом SVD-файл для MCU RP2040 имеется в Pico SDK, а на основе этого файла можно создать заголовочный файл для устройства. Благодаря проекту cmsis-pi-pico в моём распоряжении, в итоге, оказалось рабочее решение.

Решение моей задачи можно было бы и упростить


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


Последовательность загрузки RP2040 (даташит RP2040, рисунок 15) (оригинал)

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

Первая сложность, которую нужно было преодолеть для того чтобы научиться работать с RP2040, заключалась в понимании особенностей цепочечного процесса загрузки микроконтроллера. Тут всё очень похоже на то, как в прошлом, на обычных компьютерах, была организована загрузка с дискет, или то, как устроена загрузка с HDD/SSD. А именно внешняя QSPI Flash ROM рассматривается MCU лишь как устройство, которое, возможно, содержит загрузочные данные. Загрузчик первой фазы загрузки интегрирован в MCU и располагается в ROM по адресу 0x0000 0000. Он обращается к интерфейсу QSPI и пытается загрузить из него 256 байт данных. Потом будет проверен CRC32-хеш этих данных. Если проверка пройдёт успешно, они будут признаны загрузчиком второй фазы загрузки.

Загрузчик второй фазы может решать множество задач, при этом некоторые задачи он должен решать в обязательном порядке. Этот процесс, реализованный в RP2040, если сравнить его с аналогичным процессом в некоторых знаменитых клонах STM32, тоже имеющих SPI ROM (вроде отличных клонов компании GigaDevice), не так понятен, не так хорошо документирован, не так прозрачен, как мог бы быть. В нём много такого, в чём можно запутаться.

Говорят, что хорошие художники копируют


У меня ушло достаточно много времени на то, чтобы понять, как подход к управлению тактированием периферийных устройств, принятый в STM32, соотносится с системной архитектурой RP2040. Я внимательно читала даташит RP2040 и всех вокруг спрашивала об этом. Как оказалось, RP2040-версия системы управления тактированием периферии называется RESETS. Эта система является полной противоположностью той, что применяется в STM32. А именно, нужно установить условие сброса периферийного блока в 0 для того чтобы включить его тактирование. Так, чтобы включить тактирование GPIO, нужно переключить бит 8 в RESETS_RESET (PADS_BANK0).


Функциональная схема GPIO-пина RP2040 (оригинал)

Когда я это поняла, я посмотрела раздел документации по GPIO-периферии (раздел 2.19). И кое-что тут же бросилось мне в глаза. А именно, то, что я там увидела, совершенно не похоже на то, как устроена практически вся GPIO-периферия, с которой я когда-либо сталкивалась. В частности, речь идёт о периферии STM32, AVR и SAM.

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

А теперь, когда я через всё это прошла, полагаю, можно будет просто переписать мой код, после чего он заработает на RP2040?

Причуды загрузки


Как уже было сказано, загрузчик второй фазы загрузки должен быть расположен в начале образа прошивки. Я считала, что это должен быть какой-то достаточно стандартный код, поэтому просто взяла готовый ASM-код, который выдал официальный Pico SDK, и использовала его при сборке примера Blinky. Добавив этот код к RP2040-порту моего проекта Nodate, я смогла без проблем собрать Blinky.

Запись результирующего ELF-бинарника в RP2040 стала ещё одним приключением. Дело в том, что на плате Raspberry Pi Pico нет встроенного SWD-адаптера, чего-то в духе ST-Link. А микроконтроллеру на двухъядерном Cortex-M нужен могоканальный SWD-адаптер. Единственным подобным устройством, которое было у меня под рукой, оказался адаптер, интегрированный в плату Nucleo-STM32H7. Поэтому я решила использовать кастомный форк OpenOCD, созданный Raspberry Pi Foundation. Его я запустила на Raspberry Pi.

После столь основательной подготовки мне удалось успешно прошить RP2040, но ничего не заработало. Беглая проверка вызвала у меня такое ощущение, что в ходе загрузки мне не удалось выйти за пределы исходного загрузчика и добраться до прошивки, находящейся в SPI ROM. Сейчас мне сложно дать ответ о причинах происходящего. Это могла быть проблема с ASM-кодом второй фазы загрузки, это могла быть ошибка в экспериментальных CMSIS-файлах RP2040, которые создавала не я. Это могло быть и что-то совершенно иное.

Продолжение следует?



Raspberry Pi Pico (оригинал)

После того, как я потратила много часов на то, чтобы завести RP2040 с использованием CMSIS-файлов и файлов загрузчика второй фазы загрузки, мне кажется, что можно немного отстраниться от ситуации и переоценить происходящее. А именно, с того момента, когда начинала формироваться моя точка зрения на Raspberry Pi Pico, в запросе по поводу CMSIS-файлов появились сведения о том, что официальные CMSIS-файлы, возможно, появятся в Pico SDK 1.2.0. Это довольно-таки приятно.

Полагаю, любому, кто хочет поближе познакомиться с RP2040, пользуясь инструментами, ставшими индустриальным стандартом, имеет смысл дождаться этого релиза Pico SDK. А после того, как в моём распоряжении окажутся официальные CMSIS-файлы, я, вероятно, начну с переделывания примера Nodate Blinky, а потом попробую поработать с PIO. Перспектива создавать собственные интерфейсы кажется мне весьма привлекательной. И хотя возможности Raspberry Pi Pico не так мощны, как возможности CPLD или FPGA, они, всё равно, способны лечь в основу интереснейших проектов.

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

Полагаю, тех, кто захочет написать для Raspberry Pi Pico что-то своё, не особенно порадуют такие особенности платы, как запутанная работа с GPIO-периферией, сложный процесс загрузки, необходимость в загрузчике второй фазы загрузки, непрозрачность внешней ROM. В общем тому, кому интересна плата Raspberry Pi Pico, пока приходится ориентироваться на официальный SDK.

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

Пользовались ли вы Raspberry Pi Pico?


Подробнее..

Распределенный LED Контроллер управления светом (12V 6A)

04.08.2020 18:23:50 | Автор: admin

Предисловие


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

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

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

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

image

Единственный минус когда светильников становится много ими уже неудобно управлять!

Управляем с пульта!


Так как большинство светильников 12 Вольтовые (так и проще и резервирование от аккумулятора удобнее реализовывать), то сразу возникает мысль изготовить контроллер для управления освещением!

Хотя есть тонкости = тащить кучу проводов от каждого светильника к контроллеру (который желательно расположить на видном месте, что-бы не тыкать пультом куда попало) не всегда удобно. Да и НЕкорректно. Куда проще сгруппировать светильники территориально и управлять ими при помощи нескольких контроллеров, общающихся друг с другом.

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

Получилось примерно так:

  • головной контроллер, с приёмником ИК команд, звуковой индикацией и четырехсимвольным экранчиком.

    image
  • один из подчинённых контроллеров, там только контроль приёма/передачи и состояния каналов при помощи обычных светодиодов, большего ничего и не нужно, да и спрятаны вспомогательные контроллеры от глаз.

    image

Плата головного контроллера (если интересно) выглядит так:

image

Опосля травления и лужения (оужосЪнах!)

image

И уже после сборки, перед нанесением защитного покрытия (и это будет НЕ "Богомерзкий" лак! Что-то халяльное! Так и знайте!!!)

image

В данный момент каждый из контроллеров рассчитан на 9 каналов. Каждый из каналов способен тянуть по 6 Ампер нагрузки, при этом НЕзависимо у каждого из каналов свой персональный уровень яркости, от выключено до максимума 15 уровней яркости. Больше, думаю, и не надо, вполне достаточно.

Зачем так много каналов? Ну помимо основных светильников, еще есть и RGB (полноцвет), RGBW (полноцвет + модуляция яркости). И светильники с заданной цветовой температурой (от холодного до теплого оттенка). А все это масса каналов. И еще у меня есть аквариумы с фитосветильниками, вольер с шиншиллами, свет подкроватный и прикроватный На самом деле это всё очень удобно и вызывает привыкание похлеще никотина

Плюс еще такие интересные штуки, как автономный перелив всеми цветами для RGB и RGBW полноцветных светильников, типа торшера, например

Пока все управляется с обычных пультов ДУ. Но так даже удобнее.

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

Да так много возможностей = большой и НЕудобный пульт с кучей кнопок.

image

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

Вот пример на видео, так сказать


Благодарю за уделенное внимание!

Дальше будет
Подробнее..

Как управлять CNC-роутером, не привлекая внимания

27.09.2020 18:10:26 | Автор: admin
Мой CNC-роутер служил верой и правдой два года, но что-то пошло не так слетела прошивка, а был это woodpecker 0.9.

Сначала я хотел ее просто перезалить, и, с этой целью раздобыл исходные коды Grbl CNC Project. Но любопытство пересилило и я погрузился в изучение этих исходников

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



Собственно идея контроллера для CNC-машины довольна проста и интересна. Есть несколько потоков обработки один читает данные (gcode) и разбирает их, второй превращает команды в блоки исполнения и третий (stepper) собственно исполняет эти блоки. Вот об этом третьем потоке и пойдет речь.

Степпер имеет дело со списком отдельных команд вида сделай (X,Y,Z) шагов для всех трёх (как минимум) шаговых двигателей, причем за указанное время и в заданном направлении (ну это так упрощенно). Надо сказать, что шаговый двигатель со своим драйвером довольно простая в управлении штука задаешь (0 или 1) направление вращения и затем по положительному перепаду входа (0 -> 1) двигатель пытается сделать один шаг (а всего на оборот обычно 200 шагов). Данные уже подготовлены, так что надо просто как-то соотнести 3 целых числа с заданным временем.

В оригинале у автора использован контроллер atmega328p, но практически без изменений все легко переносится на arm (например, stm32). Но вот сам алгоритм не может не вызывать вопросов.

С одной стороны, использован весьма совершенный алгоритм Брезенхэма, а точнее его разновидность Adaptive Multi-Axis Step-Smoothing. Но с другой стороны, как-то это все сложно и главное, плавность хода шагового мотора и точность работы роутера прямо зависят от точности выдачи сигналов управления. В данном случае это обуславливается частотой на которой работает таймер и временем обработки прерываний а это дает не более 40-50 кГц в лучшем случае, а обычно и того менее ну то есть точность задания управления 20-50 мксек.

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

Так как я рассматривал переход на cortex-m (ну точнее на stm32h750, который я очень люблю и который очень подешевел), то такая задача может быть решена вовсе без привлечения CPU только лишь с использованием двух каналов DMA и одного 32-битного счетчика.

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

Получится что-то вроде такого.

Обработка по прерыванию переключение на новый буфер (двойная буфферизация).

#define MAX_PGM 32typedef struct _pgm_buffer {        uint32_t data[MAX_PGM];        uint32_t delta[MAX_PGM];} pgm_buffer;pgm_buffer buf[2];uint32_t current_buf = 1;uint32_t flags = 0;void program_down(DMA_HandleTypeDef *_hdma) {        TIM2->CR1 &= ~TIM_CR1_CEN;        if ((flags & BUF_RUNNING) == 0)                return;        current_buf ^= 1;        DMA1_Channel5->CCR &= ~1;        DMA1_Channel2->CCR &= ~1;        DMA1_Channel5->CNDTR = MAX_PGM;        DMA1_Channel2->CNDTR = MAX_PGM;        DMA1_Channel5->CMAR = (uint32_t) (buf[current_buf].delta);        DMA1_Channel2->CMAR = (uint32_t) (buf[current_buf].data);        DMA1_Channel5->CCR |= 1;        DMA1_Channel2->CCR |= 1;        TIM2->CNT = 0;        TIM2->ARR = 8;        TIM2->EGR |= TIM_EGR_UG;        TIM2->CR1 |= TIM_CR1_CEN;}


Инициировать можно так:

       HAL_DMA_RegisterCallback(&hdma_tim2_up, HAL_DMA_XFER_CPLT_CB_ID,                        program_down);        HAL_DMA_Start_IT(&hdma_tim2_up, buf, &GPIOA->BSRR, MAX_PGM);        DMA1_Channel5->CCR &= ~1;        DMA1_Channel5->CPAR = &TIM2->ARR;        DMA1_Channel5->CCR |= 1;        TIM2->CCR1 = 1;        TIM2->DIER |= TIM_DIER_UDE | TIM_DIER_CC1DE;        flags |= BUF_RUNNING;


Ну а старт это:

        program_down(NULL);


Что это дает? Давайте подсчитаем на примере того же stm32h750. Таймер (TIM2) там работает на частоте 200 МГц, минимальное время задержки два такта, но DMA не может переслать данные быстрее 50МГц, то есть между двумя командами на переключение порта можно положить (с учетом возможной занятости шины) 40 нсек (25МГц) это в 1000 раз лучше исходной реализации!

С другой стороны, ширина порта 16 бит, так что можно одновременно управлять 8 шаговыми двигателями вместо 3 еще бы знать зачем

При этом заполнение собственно данных не вызывает проблем (с таким-то разрешением!) простая линейная интерполяция по каждому двигателю отдельно с объединением (для оптимизации) событий ближе 40 нсек.

Собственно выводы.

В мастерской лежит готовый CNC-станок с размером 1.2 метра на 0.8 метра с двигателями и драйверами, но без контроллера. Похоже, надо завершить работу и попробовать на нем, насколько это будет эпично. Если сделаю обязательно напишу продолжение. А пока я не понимаю, почему это контроллеры делают на atmega и они пищат на всех 3d-принтерах и cnc-роутерах на этих грубых прерываниях

Ну и конечно, наверно имея мощь Cortex-M7, можно реализовать более плавное управление траекторией движения со всеми ограничениями, но это уже совсем другая статья.
Подробнее..

Разработка защищённого WEB интерфейса для микроконтроллеров

19.04.2021 12:12:03 | Автор: admin

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

Выбор аппаратной платформы.

Платформа в виде платы должна иметь чип или модуль Wi-Fi, микроконтроллер, желательно SD карту или чип внешней памяти ёмкостью не менее нескольких мегабайт.

Выбор RTOS

В проектируемом устройстве WEB сервер играет вторичную роль. Кроме него будут работать ещё десятки задач. Организовать WEB сервер так чтобы его присутствие не влияло на работу остальной функциональности можно с помощью RTOS.
От middleware RTOS нам нужен развитый набор сервисов синхронизации, вытесняющая многозадачность, надёжный менеджер динамической памяти, быстрая файловая система для SD или eMMC карт, стек TCP/IP протоколов и очень желательно сервисные инструменты отладки.
Все это есть в Azure RTOS. Эта RTOS имеет очень длинную и славную историю с тех пор, когда ещё называлась ThreadX. Она нашла применение в нескольких миллиардах устройств. Её отличает надёжность, компактность и хорошая документация. Долгое время была коммерческой и очень дорогой. Впервые в открытом виде появилась в пакете ПО Synergy от Renesas для микроконтроллеров Synergy на базе ARM Cortex-M4, и сразу получила широкую популярность благодаря исключительно богатой палитре предоставляемого middleware. Теперь портированное middleware Azure RTOS доступно бесплатно и для STM32.

Выбор WEB сервера.

Верхним протоколом, на котором непосредственно базируется WEB сервер является HTTP.
Azure RTOS уже имеет в своём составе HTTP сервер, работающий поверх стека сетевых протоколов Azure RTOS NetX Duo. HTTP Azure, согласно документации, требует всего от 3.0 KB до 9.5 KB FLASH и от 0.5 KB до 2 KB ОЗУ. Ниже будет дана более реалистичная оценка необходимого объема ОЗУ.
Поскольку NetX Duo обладает интерфейсом BSD, то есть возможность достаточно легко адаптировать другие сторонние WEB сервера, хотя тесная связь таких серверов с нижележащим API делает такой выбор малоэффективным.

Свойства WEB сервера реализованного в Azure RTOS:
- Поддержка 2-х типов авторизации: basic (передача пароля в открытом виде) и digest (передача хэша пароля),
- Работа по протоколам HTTP и HTTPS (HTTP защищённый с помощью TLS)
- Чтение файлов страниц с SD карты или другого носителя с FAT32
- Обработка запросов: GET, POST, HEAD, PUT, DELETE
- Поточная передача без указания размера данных: Content-Length
- Работа нескольких подключений одновременно.

Полностью документация на WEB сервер здесь.

Технология работы WEB интерфейса

Способ работы сервера очень простой. Устройство получает через TCP соединение текстовую строку запроса от браузера пользователя сформированную согласно спецификации HTTP.
Устройство также должно ответить строкой по спецификации HTTP. Поэтому WEB сервер в Azure RTOS называется HTTP сервером. HTTP сервер Azure RTOS сразу готов отдавать по запросам браузера файлы с SD карты устройства. Но статические страницы мало интересны.

Самой распространённой технологией в WEB серверах встраиваемых устройств является технология Server Side Includes (SSI). В Azure WEB сервере такой технологии нет. Видимо она считается устаревшей. Действительно, SSI позволял придать динамичность страницам, когда в браузере была отключена возможность исполнять JavaScript. Теперь же JavaScript включён повсеместно, без него сложно добиться адаптируемости.
Более продвинутым считается способ взаимодействия браузера и сервера с помощью технологии AJAX. Т.е. браузер с помощью JavaScript после загрузки страницы в отдельном потоке запрашивает дополнительные данные из устройства. Сервер в устройстве на лету формирует блок запрашиваемых данных и отправляет браузеру в таком же виде как он посылал страницы т.е. по спецификации HTTP.

Никто, конечно, не мешает реализовать и SSI в сервере Azure. Это делается очень просто и выльется в конечном итоге в длинную цепочку операторов if else с проверками на вхождение строк-директив. Но такой подход вызовет слишком тесную связность между страницами и кодом в микроконтроллере.
Гораздо привлекательней выглядит AJAX, причём с передачей файлов в JSON кодировке.
Дело в том, что JSON является форматом внутреннего представления объектов JavaScript в WEB страницах. Нет ничего проще для WEB разработчика чем передавать и принимать данные в формате JSON. В JSON можно выполнить сериализацию буквально всего: параметров, таблиц параметров, баз данных параметров, иерархических деревьев параметров, представления параметров в виде виджетов и прочее.

JSON кодировка довольно простая. JSON в конечном счёте просто строка. Внутри неё нельзя использовать байт 0, поэтому эта строка легко интерпретируется как C-и строка. Одновременно это же обстоятельство позволяет без перекодировки вставлять JSON в HTTP строку ответа.
Немного сложнее дела обстоят с парсингом JSON. Парсинг JSON необходим когда браузер клиента пришлёт устройству запрос например с отредактированными настройками. Простые JSON строки можно парсить и средствами языка C-и, но большие JSON строки уже требуют серьёзных парсеров.
Здесь можно посоветовать проект Jansson. Надо только знать что Jansson активно использует динамическую память, и если передавать в JSON около сотни числовых и строковых параметров, то для парсера может понадобиться около 50 Кбайт ОЗУ, зависит от длины имён переменных и длины самих переменных.

Дополнительные замечания по HTTP серверу Azure.

MIME типы
Сервер Azure HTTP не поддерживает HTTP pipelining, но поддерживает передачу файлов один за другим в одном TCP соединении. WEB страницы как правило содержат ссылки на файлы стилей, скриптов, картинок и прочего. Все эти файлы скачиваются последовательно один за другим браузером клиента. Чтобы получить файл браузер посылает HTTP запрос. Сервер на старте отправки каждого файла отправляет HTTP заголовок, например такой:

HTTP/1.1 200 OKContent-Type: text/htmlConnection: keep-aliveContent-Length: 13210Date: Sun, 17 May 2020 00:59:19 GMTCache-Control: max-age=1Last-Modified: Sun, 17 May 2021 00:59:19 GMT

Здесь имеет большое значение содержание поля Content-Type. Если его указать неправильно браузер может неправильно отобразить страницу. Сервер Azure содержание Content-Type устанавливает в соответствии с расширением передаваемого файла. Но список самих таких известных расширений у сервера небольшой, поэтому в файле nx_web_http_server.c дополняем массив _nx_web_http_server_mime_maps следующим образом:

/* Define basic MIME maps. */static NX_WEB_HTTP_SERVER_MIME_MAP _nx_web_http_server_mime_maps[] ={    {"html",     "text/html"},    {"htm",      "text/html"},    {"txt",      "text/plain"},    {"css",      "text/css"},    {"js",       "application/javascript"},    {"gif",      "image/gif"},    {"jpg",      "image/jpeg"},    {"png",      "image/png"},    {"ico",      "image/x-icon"},};

Способы размещения контента. Для более удобного и быстрого размещения статического контента и сопутствующих файлов на WEB сервере устройства можно применить FTP сервер. FTP сервер имеется в поставке Azure RTOS. Такие среды разработки как Adobe Dreamweaver способны автоматически обновлять контент на целевом FTP сервере содержащем контент WEB сайта.

Ограничение видимости для WEB сервера Azure.
По умолчанию корневой директорией HTTP сервера Azure является корневая директория SD карты.
Чтобы сервер мог считывать файлы только из определенной поддиректории ему надо установить функцией fx_directory_local_path_set локальный путь для задачи сервера. Это удобно делать при первом вызове в callback функции перехватчика запросов сервера. Указатель на callback функцию передаётся в задачу сервера при его создании с помощью функции nx_web_http_server_create. Весь HTTP сервер выполняется в одной задаче, поэтому такой метод работает.

Сжатие контента. Современные браузеры поддерживают компрессию передаваемых страниц и сопутствующих файлов. Компрессия типичных HTML файлов, скриптов и стилей позволяет уменьшит объем передаваемых данных в 3-4 раза. Сервер HTTP Azure не поддерживает компрессию на лету, для микроконтроллеров компрессия может оказаться более продолжительным процессом чем передача несжатого контента. Есть возможность выполнить компрессию статических файлов сразу же при подготовке контента. И хранить файлы на SD карте уже сжатыми, однако HTTP сервер Azure не поддерживает распознавание сжатых файлов и соответствующую модификацию заголовков HTTP.
Проблема не так актуальна как может показаться. Например одностраничные приложения выполненные во фреймворках скачивают статичный контент всего один раз, и гораздо больший трафик может создать динамический контент. Статический контент также можно разместить на сторонних быстрых серверах способных сжимать. И можно конечно же реализовать распознавание сжатых файлов в HTTP сервере собственными силами если сжатие действительно сильно улучшит отзывчивость сервера.

Неточность в исходниках Azure HTTP сервера при базовой авторизации.
Исходные тексты содержат неточность в файле nx_web_http_server.c после строки 3689.
В таком виде базовая авторизация затягивается на 10 сек., поскольку не разрывается соединение после неавторизированного запроса. Туда следует вставить вот такой фрагмент:

if (status == NX_WEB_HTTP_BASIC_AUTHENTICATE)        {          _nx_web_http_server_connection_reset(server_ptr,server_ptr -> nx_web_http_server_current_session_ptr , NX_WEB_HTTP_SERVER_TIMEOUT_SEND);          return;        }

После такого исправления пользователь сразу, а не через 10 сек. увидит диалог с вводом имени и пароля.
Не забыть также вставить этот фрагмент для методов POST и DELETE.

Выбор WEB фреймворка

Выбор фреймворка очень важен. Фреймворки это такое сочетание JavaScript библиотек и файлов стилей CSS. Бывает просто одна библиотека, как например jQuery, бывает библиотека со стилями, как например jQuery UI. Фреймворк значительно облегчает создание адаптируемых, интуитивных и привлекательных WEB страниц, в противовес использованию голого HTML и JavaScript с архаичными стилями. С помощью фреймворков даже самый примитивный WEB сервер выполненный на Arduino, может предоставить удивительно стильный WEB интерфейс. Фреймворки автоматически адаптируют страницы так чтобы они оставались в приемлемом качестве на экранах любых размеров и в любых браузерах.

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

Большинство фреймворков вводят новые элементы синтаксиса разметки в страницы HTML, добиваясь таким образом новых способов выражения дизайна. Это влечёт за собой необходимость дополнительно изучать кроме HTML, JavaScript, CSS и DOM модели ещё и уникальный язык и архитектурный подход фреймворка. Это ещё более усложняет задачу выбора. Поэтому об оптимальном выборе речь не идёт.
Как локальный оптимум выбираем jQuery mobile. Он использует одну из старейших и проверенных библиотек jQuery и интегрирован в WYSIWYG среду разработки Adobe Dreamweaver, что позволяет более удобно конструировать интерфейс по сравнению со способами без WYSIWYG. jQuery mobile как следует из названия предназначен в первую очередь для мобильных устройств, и это как раз то что нужно, поскольку большинство пользователей скорее всего захотят работать с WEB интерфейсом через мобильные устройства

Защита WEB сервера, генерация и инсталляция сертификатов.

Защищённый WEB сервер работает по протоколу HTTPS на порту 433. В этом случае используется протокол шифрования и аутентификации Transport Layer Security (TLS). В Azure RTOS реализован протокол TLS версии 1.3 и также более ранние его версии.

По умолчанию сервер просит от клиента вот такой набор алгоритмов:
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)

Для того чтобы WEB c TLS работал устройство должно ещё иметь в своих недрах сертификат сервера и секретный ключ сервера. Здесь предлагается их хранить во Flash памяти в виде массивов. Сертификат сервера должен содержать подпись доверенного центра (это стоит денег и времени), поэтому проще всего сгенерировать самоподписанный сертификат.

И ключ и сертификат должны быть представлены в двоичном формате DER.

Для примера: ключ занимает 1192 байта, один самоподписанный сертификат занимает 879 байт.

Чтобы сгенерировать самоподписанный сертификат и секретный ключ надо скачать файлы openssl.exe, libeay32.dll, ssleay32.dll и выполнить в их директории несколько команд:

1. Сгенерировать корневой секретный ключ ca.key -
openssl genrsa -out ca.key 2048

2. Сгенерировать корневой сертификат CA.crt -
openssl req -config CA.conf -new -x509 -sha256 -key ca.key -days 3650 -out CA.crt

3. Сгенерировать секретный ключ сервера srv.key
openssl genrsa -out srv.key 2048

4. Сгенерировать запрос сертификата сервера с использованием корневого сертификата srv.csr -
openssl req -new -config Server.conf -out srv.csr -key srv.key

5. Верифицировать и сгенерировать сертификат сервера srv.crt -
openssl x509 -req -in srv.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out srv.crt -days 3650

6. Конвертировать сертификаты и ключ в формат DER -
openssl x509 -in CA.crt -out CA.der -outform DER
openssl x509 -in srv.crt -out srv.der -outform DER
openssl rsa -inform pem -in srv.key -outform der -out srv_key.der

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

Сколько ОЗУ потребует защищённый WEB сервер

Задача http сервера в Azure RTOS называется TCPSERVER Thread.
Про этому названию её легко найти в списке запущенных задач в отладчике IDE IAR Embedded Workbench for Arm.
По наблюдениям на реальном проекте размер занятого этой задачей стека не превышал 2500 байта.
Движок TLS требует нескольких объёмных структур данных:
Структура NX_SECURE_X509_CERT имеет размер 304 байта
Массив crypto_metadata должен иметь размер не менее 17596 байта на каждую сессию. У нас выбрано до 2 сессий одновременно. Следовательно надо 35192 байта
Массив tls_packet_buffer требует не менее 2000 байт и не более 64 Кбайт. Задаём ему 4000 байта. Его размер определяется размером сертификата сервера. Сертификат может сопровождаться цепочкой сертификатов поэтому надо определять этот размер каждый раз по обстоятельствам.

Итого защищённый сервер потребует не менее 42 Кбайт. Это сравнительно немного. В целом на весь TCP стек с сервером потребуется около 100 Кбайт ОЗУ. Это с учётом буфера пакетов TCP/IP пакетов, JSON парсера для минимальных структур и прочих расходов на протоколы. Если иметь в виду ещё файловую систему, то размер возрастёт на 30-70 Кбайт в зависимости от того какое быстродействие хотим получить.

Пример разработанной страницы WEB интерфейса.

Используя фреймворк jQuery mobile сконструирована вот такая страница:

Если бы фреймворка не было, то эта же страница выглядела бы так:

Содержимое HTML страницы
<!doctype html><html lang="en">  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">    <title>IoT Logger</title>    <link href="jquery-mobile/jquery.mobile.theme-1.3.0.min.css" rel="stylesheet" type="text/css">    <link href="jquery-mobile/jquery.mobile.structure-1.3.0.min.css" rel="stylesheet" type="text/css">    <script src="jquery-mobile/jquery-1.11.1.min.js">    </script>    <script src="jquery-mobile/jquery.mobile-1.3.0.min.js">    </script>    <style>        @media only screen and (device-width: 300px), only screen and (max-width:300px) {            .css-element {                yourcsscode:;            }        }        input.cb_larger {            width: 25px;            height: 25px;            margin: -12px 0 0 -10px;        }        .tbl_hdr {            font-size: 14px;            font-style: oblique;            font-weight: normal;            text-align: left;            background-color: #DEDEDE        }    </style>  </head>  <body>    <div align="center" data-role="page" id="page">      <div data-role="header" data-position="fixed">        <p id="page_header1" style="padding: 0px 0px 0px 0px; margin: 5px 0px 0px 0px ">IoT Logger</p>        <p style="font-size: 10px; padding: 0px 0px 0px 0px; margin: 0px 0px 5px 5px; text-align: left ">        <span>ID:</span>        <em id="page_header2" style="color:deepskyblue">?</em>        <span id="page_header3" style="margin-left: 10px">?</span>        </p>        <div data-role="navbar" style="width: 400px">          <ul>            <li></li>            <li></li>            <li><li><button type="submit" id="reset_log" onclick="location.assign('device_log.html');" data-theme="b">Show log</button></li></li>          </ul>        </div>      </div>      <div data-role="content">        <div style="overflow:auto;">        <table id="ap_list" style="width:800px">          <caption style="font-size: 16px; font-weight: bold; text-align: left">          Available Access Points list for Station mode          </caption>          <tr>            <th class="tbl_hdr"></th><th class="tbl_hdr">Enable</th>            <th class="tbl_hdr">Access Point SSID</th>            <th class="tbl_hdr">Password</th>            <th class="tbl_hdr">Use DHCP</th>            <th class="tbl_hdr">IP address</th>            <th class="tbl_hdr">IP mask</th>            <th class="tbl_hdr">IP gateway</th>          </tr>          <tr>            <td>1</td>            <td><input type="checkbox" class="cb_larger" id="ssiden1" name="SSIDEN1"></td>            <td><input type="text" id="ssid1" name="SSID1" value=""></td>            <td><input type="password" id="pass1" name="PASS1" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp1" name="DHCP1"></td>            <td><input type="text"    id="ipaddr1"    name="IPADDR1" value=""></td>            <td><input type="text"    id="ipmask1"    name="IPMASK1" value=""></td>            <td><input type="text" id="ipgateway1" name="IPGATEWAY1" value=""></td>          </tr>          <tr>            <td>2</td>            <td><input type="checkbox" class="cb_larger" id="ssiden2" name="SSIDEN2"></td>            <td><input type="text" id="ssid2" name="SSID2" value=""></td>            <td><input type="password" id="pass2" name="PASS2" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp2" name="DHCP2"></td>            <td><input type="text"    id="ipaddr2"    name="IPADDR2" value=""></td>            <td><input type="text"    id="ipmask2"    name="IPMASK2" value=""></td>            <td><input type="text" id="ipgateway2" name="IPGATEWAY2" value=""></td>          </tr>          <tr>            <td>3</td>            <td><input type="checkbox" class="cb_larger" id="ssiden3" name="SSIDEN3"></td><td>            <input type="text" id="ssid3" name="SSID3" value=""></td>            <td><input type="password" id="pass3" name="PASS3" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp3" name="DHCP3"></td>            <td><input type="text"    id="ipaddr3"    name="IPADDR3" value=""></td>            <td><input type="text"    id="ipmask3"    name="IPMASK3" value=""></td>            <td><input type="text" id="ipgateway3" name="IPGATEWAY3" value=""></td>          </tr>          <tr>            <td>4</td>            <td><input type="checkbox" class="cb_larger" id="ssiden4" name="SSIDEN4"></td>            <td><input type="text" id="ssid4" name="SSID4" value=""></td>            <td><input type="password" id="pass4" name="PASS4" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp4" name="DHCP4"></td>            <td><input type="text"    id="ipaddr4"    name="IPADDR4" value=""></td>            <td><input type="text"    id="ipmask4"    name="IPMASK4" value=""></td>            <td><input type="text" id="ipgateway4" name="IPGATEWAY4" value=""></td>          </tr>          <tr>            <td>5</td>            <td><input type="checkbox" class="cb_larger" id="ssiden5" name="SSIDEN5"></td>            <td><input type="text" id="ssid5" name="SSID5" value=""></td>            <td><input type="password" id="pass5" name="PASS5" value=""></td>            <td><input type="checkbox" class="cb_larger"  id="dhcp5" name="DHCP5"></td>            <td><input type="text"    id="ipaddr5"    name="IPADDR5" value=""></td>            <td><input type="text"    id="ipmask5"    name="IPMASK5" value=""></td>            <td><input type="text" id="ipgateway5" name="IPGATEWAY5" value=""></td>          </tr>        </table>        </div>        <table id="ap_selector">          <tr>            <th>            <div style="text-align:left ; padding-top: 10px">              Selected operational mode            </div>            </th>          </tr>          <tr>            <td>            <input type="radio" id="md_sta" name="wifi_mode" value="0">            <label id="md_sta_lb" for="md_sta">            Station mode <br>            <span style="font-weight: normal; font-size: 14px">            (The device will connect to the someone <br>            of access points listed in the table above)            </span>            </label>            </td>          </tr>          <tr>            <td>            <input type="radio" id="md_ap" name="wifi_mode" value="1">            <label id="md_ap_lb" for="md_ap">            Access Point mode<br>            <span style="font-weight: normal; font-size: 14px">(The device will wait for someone to join with it)</span>            </label>            </td>          </tr>        </table>        <fieldset class="ui-grid-a" style="width: 300px">          <div class="ui-block-a">            <button type="submit" id="submit_data" data-theme="a">Submit</button>          </div>          <div class="ui-block-b">            <button type="submit" id="reset_dev" data-theme="e">Reset device</button>          </div>        </fieldset>        <div data-role="footer" data-position="fixed">          <p id="status_msg" style="font-weight:normal" hidden=true>          </p>        </div>      </div>    </div>  </body>  <script>    // Глобальные переменные для хранения полученной из дивайса информации    var params;    var dev;    var apl;    var wrl;    // Вывод строки статуса выполнения операции по нажатию кнопок    function Show_cmd_status(data, status)    {      $("#status_msg").text("Data sending status: " + status); $("#status_msg").show();      setTimeout(function() { $("#status_msg").hide(); }, 2000);    }    // Функция извлечение интересующих параметров из объекта data и помещение их в поля ввода    function Data_accept(data, status)    {      params = data;                  var v = params.find(function(item, i) { if (item["Client_AP_list"] != undefined) return true; });      apl = v["Client_AP_list"];      if (apl != undefined)      {        $("#ssid1").val(apl[0][1]);        $("#pass1").val(apl[0][2]);        $("#ipaddr1").val(apl[0][4]);        $("#ipmask1").val(apl[0][5]);        $("#ipgateway1").val(apl[0][6]);        $("#ssid2").val(apl[1][1]);        $("#pass2").val(apl[1][2]);        $("#ipaddr2").val(apl[1][4]);        $("#ipmask2").val(apl[1][5]);        $("#ipgateway2").val(apl[1][6]);        $("#ssid3").val(apl[2][1]);        $("#pass3").val(apl[2][2]);        $("#ipaddr3").val(apl[2][4]);        $("#ipmask3").val(apl[2][5]);        $("#ipgateway3").val(apl[2][6]);        $("#ssid4").val(apl[3][1]);        $("#pass4").val(apl[3][2]);        $("#ipaddr4").val(apl[3][4]);        $("#ipmask4").val(apl[3][5]);        $("#ipgateway4").val(apl[3][6]);        $("#ssid5").val(apl[4][1]);        $("#pass5").val(apl[4][2]);        $("#ipaddr5").val(apl[4][4]);        $("#ipmask5").val(apl[4][5]);        $("#ipgateway5").val(apl[4][6]);        if (apl[0][0] == 1) $("#ssiden1").attr("checked", true);        if (apl[1][0] == 1) $("#ssiden2").attr("checked", true);        if (apl[2][0] == 1) $("#ssiden3").attr("checked", true);        if (apl[3][0] == 1) $("#ssiden4").attr("checked", true);        if (apl[4][0] == 1) $("#ssiden5").attr("checked", true);        if (apl[0][3] == 1) $("#dhcp1").attr("checked", true);        if (apl[1][3] == 1) $("#dhcp2").attr("checked", true);        if (apl[2][3] == 1) $("#dhcp3").attr("checked", true);        if (apl[3][3] == 1) $("#dhcp4").attr("checked", true);        if (apl[4][3] == 1) $("#dhcp5").attr("checked", true);      }      v = params.find(function(item, i) { if (item["Parameters"] != undefined) return true; });      if (v != undefined)      {        wrl = v["Parameters"].find(function(item, i) { if (item[0] == "wifi_role") return true; });        if (wrl != undefined)        {          if (wrl[1] == 0)          {            $("#md_sta_lb").click();          } else          {            $("#md_ap_lb").click();          }        }      }      v = params.find(function(item, i) { if (item["Device"] != undefined) return true; });      dev = v["Device"];      if (dev["HW_Ver"] != undefined)      {        $("#page_header1").html(dev["HW_Ver"]);        $("#page_header2").html(dev["CPU_ID"]);        $("#page_header3").html(dev["CompDate"] + " " + dev["CompTime"]);      }    }    // Считываем параметры из полей ввода, записываем их в объект JSON    // Сериализируем объект и отправляем устройству методом POST    function Data_send()    {            apl[0][1] = $("#ssid1").val();      apl[0][2] = $("#pass1").val();      apl[0][4] = $("#ipaddr1").val();      apl[0][5] = $("#ipmask1").val();      apl[0][6] = $("#ipgateway1").val();      apl[1][1] = $("#ssid2").val();      apl[1][2] = $("#pass2").val();      apl[1][4] = $("#ipaddr2").val();      apl[1][5] = $("#ipmask2").val();      apl[1][6] = $("#ipgateway2").val();      apl[2][1] = $("#ssid3").val();      apl[2][2] = $("#pass3").val();      apl[2][4] = $("#ipaddr3").val();      apl[2][5] = $("#ipmask3").val();      apl[2][6] = $("#ipgateway3").val();      apl[3][1] = $("#ssid4").val();      apl[3][2] = $("#pass4").val();      apl[3][4] = $("#ipaddr4").val();      apl[3][5] = $("#ipmask4").val();      apl[3][6] = $("#ipgateway4").val();      apl[4][1] = $("#ssid5").val();      apl[4][2] = $("#pass5").val();      apl[4][4] = $("#ipaddr5").val();      apl[4][5] = $("#ipmask5").val();      apl[4][6] = $("#ipgateway5").val();      if ($("#ssiden1").prop("checked") == true) apl[0][0] = 1;      else apl[0][0] = 0;      if ($("#ssiden2").prop("checked") == true) apl[1][0] = 1;      else apl[1][0] = 0;      if ($("#ssiden3").prop("checked") == true) apl[2][0] = 1;      else apl[2][0] = 0;      if ($("#ssiden4").prop("checked") == true) apl[3][0] = 1;      else apl[3][0] = 0;      if ($("#ssiden5").prop("checked") == true) apl[4][0] = 1;      else apl[4][0] = 0;      if ($("#dhcp1").prop("checked") == true) apl[0][3] = 1;      else apl[0][3] = 0;      if ($("#dhcp2").prop("checked") == true) apl[1][3] = 1;      else apl[1][3] = 0;      if ($("#dhcp3").prop("checked") == true) apl[2][3] = 1;      else apl[2][3] = 0;      if ($("#dhcp4").prop("checked") == true) apl[3][3] = 1;      else apl[3][3] = 0;      if ($("#dhcp5").prop("checked") == true) apl[4][3] = 1;      else apl[4][3] = 0;      if ($("#md_sta").prop("checked") == true) wrl[1] = "0";      else wrl[1] = "1";      // Преобразуем объект JavaScript в строку JSON      json_str = JSON.stringify(params);      // Отправляем устройству методом POST строку JSON с отредактированными параметрами      $.post("data.json", json_str, Show_cmd_status);    }    // Перейти на страницу отображения лога     function Show_log()    {      location.assign("device_log.html");    }    // По клику на кнопке с id = "submit_data" посылаем отредактированные данные обратно устройству      $("#submit_data").click(Data_send);    // По клику на кнопке с id = "reset_dev" посылаем команду сброса устройства    $("#reset_dev").click(function() {$.post("reset", "", Show_cmd_status)});    // Здесь сразу запрашиваем у устройства по протоколу AJAX текущие настройки     $.get("data.json", Data_accept);  </script></html>

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

Содержимое JSON файла c данными
[ {  "Device": {   "CPU_ID": "5301646835393735C86643535454227D",   "SW_Ver": "V0.0.2",   "HW_Ver": "IoT Logger 1.0.0",   "CompDate": "Apr 16 2021",   "CompTime": "13:03:54"  } }, {  "Parameters": [   [    "leds_mode",    "1"   ],   [    "wifi_role",    "1"   ]  ] }, {  "Client_AP_list": [   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ],   [    1,    "SSID",    "PASS",    0,    "192.168.1.1",    "255.255.255.0",    "192.168.1.254"   ]  ] }]

И наконец посмотрим отзывчивость WEB интерфейса на примере платформы Sinergy

Диаграмма времени закачки страницы по протоколу HTTP Диаграмма времени закачки страницы по протоколу HTTP Диаграмма времени закачки страницы по протоколу HTTPSдерДиаграмма времени закачки страницы по протоколу HTTPSдер

Здесь надо отметить что для Sinergy у Azure RTOS есть драйвер аппаратного криптографического модуля, поэтому скорость HTTP и HTTPS отличаются всего лишь в два раза.
В случае программной реализации отличие будет более значительным.
Как обстоят дела с драйверами для криптографической периферии STM32 ещё предстоит выяснить.

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

Подробнее..

Перетягивание диода или устраиваем соревнование между CANNY 3 TINY PRO и Arduino

08.06.2021 02:17:27 | Автор: admin
Arduino vs CANNY перетягивание диодаArduino vs CANNY перетягивание диода

В предыдущей статье, посвящённой моим попыткам погрузится в увлекательный мир программирования микроконтроллеров, я грозился сделать обзор на "обновку". К сожалению, мне сейчас не хватает навыков и времени чтобы сделать, что-то достойное полноценного обзора. Однако, я всё-таки решил подготовить забавы ради, короткую статью на тему игрушечного соревнования CANNY 3 TINY PRO и неоригинальной Arduino Nano. Соревноваться контроллеры будут в своеобразном аналоге перетягивания каната, на роль которого был выбран двухцветный светодиод марки BL-L2519EGW.

Итак в сегодняшнем материале мы подключим оба контроллера к светодиоду и будем подавать случайный сигнал на его выводы. Правила простые кто большее напряжение подаст у того и кристалл в светодиоде загорится ярче. Попутно мы воспользуемся ЦАП на контроллере CANNY и доработаем стандартный ГСЧ с помощью составного функционального блока.

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

Оглавление:

  1. Введение

  2. Схема

  3. Программа

  4. Заключение

Введение

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

Для того, чтобы повторить "дуэль века" вам потребуется:

  1. Двухцветный светодиод - я использовал BL-L2519EGW, но марка не особо критична.

  2. Контроллер CANNY 3 TINY PRO именно у этого контроллера есть ЦАП, к тому же он самый доступный по цене в линейке контроллеров CANNY.

  3. Совместимый с Arduino контроллер - я использовал неоригинальную Nano, но можно было и UNO.

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

  5. Соединительные провода и макетная плата.

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

Предвосхищая некоторую критику, сразу скажу, что само собой раз контролеры Кэнни стоят в "Дакаровских" Камазах, наверное и CANNY 3 TINY PRO явно предназначен для чего-то большего, чем просто моргать светодиодом. Собственно Arduino Nano тоже может намного больше. Просто я ничего сложнее в этот раз собрать не смог, а поделится впечатлением хотелось.

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

Другие статьи цикла

Схема

Схема соединенийСхема соединений

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

Первым делом объединим "Землю" у двух контроллеров соединив выводы "GND".

Затем, от каждого из контроллеров "подведем" к светодиоду выводы от канала ЦАП.
У Arduino - "D5", у CANNY - "C2" (это единственный выход с ЦАП).
В нашем случае светодиод имеет два вывода и работает он примерно так: если на левой ноге напряжение больше, чем на правой то загорается красный кристалл. Чем больше разница потенциалов, тем ярче он загорится. И наоборот, если на правой ноге напряжение больше, чем на левой, то загорится зеленый. При примерном равенстве потенциалов светодиод вообще не будет светится.

Мы будем "перетягивать канат" 2.5 секунды, нам важно, чтобы контроллеры подали сигнал более-менее синхронно. Для этого (а также для нашей тренировки) контроллеры подадут друг другу сигналы. Выход "D7" Arduino подаст логическую единицу на вход "C6" CANNY. В свою очередь, CANNY с выхода "C4" подаст логическую единицу на вход "D3" Arduino. В программе каждого из контроллеров предусмотрим проверку наличия сигнала, при успешном прохождении которой подается напряжение на светодиод.

Для того, чтобы узнать какое напряжение подали оба контроллера, мы с помощью АЦП CANNY померим напряжение поданное Arduino. Для этого подсоединим выход "C10" CANNY к резистору.

Кстати лично я очень рад что у Canny 3 TINY PRO для того, чтобы включить режим АЦП канала не нужно паять перемычку, как в случае с обычным CANNY 3 TINY.

В итоге должно получится примерно вот-так:

Фото схемыФото схемы

Я правда использовал специальные резисторы из набора, но в остальном схема как на картинке.

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

Перейдем к программной части.

Программа

Обе программы можно скачать с GitHub.

Программа для Arduino очень простая, думаю нет смысле её комментировать:

/*Synhronized Randomize DAC.See more http://personeltest.ru/aways/habr.com/ru/post/561148/*/int ADC_pin = 5;int input_pin = 3;int output_pin = 7;int synch_signa = 0;int v_min = 10;int v_max = 2550;int synch_signal = 0;float rand_voltage = 0;// the setup function runs once when you press reset or power the boardvoid setup() {  pinMode(ADC_pin, OUTPUT);  pinMode(output_pin, OUTPUT);  digitalWrite(output_pin,HIGH);  pinMode(input_pin, INPUT);  Serial.begin(9600);}// the loop function runs over and over again forevervoid loop() {synch_signal = digitalRead(input_pin);      // read signal from another deviceif (synch_signal) { rand_voltage=random(v_min, v_max) / 10; analogWrite(ADC_pin, rand_voltage); Serial.println(rand_voltage);  delay(2500);   // wait for seconds}else{ delay(500);   // wait for seconds}                }

Программа для контроллера CANNY:

Функциональная диаграммаФункциональная диаграмма
  • При включении контроллер устанавливает на выходе "С4" логическую "1" для синхронизации с Arduino.

  • Канал "C10" в режиме АЦП измеряет напряжение от Arduinio и с помощью функции MAP переводит его в удобный для чтения вид.

  • ШИМ-генератор в сочетании с детектором переднего фронта раз в 2.5 секунды дают сигнал для записи случайного значения в канал "C2".

    • Значение при этом запишется, только если на входе "С6" есть логическая "1" от Arduino.

    • Значение напряжения для подачи на светодиод генерируется случайным образом. Поскольку у CANNY нет встроенного блока для сброса ГСЦ, "случайность" сигнала обеспечивается, сложением "истории" сигналов, поступивших от Arduino.

    • Для уменьшения количества элементов на основной диаграмме, реализация функции "Random" перенесена в одноименный составной блок (об этом чуть позже).

  • Значения напряжений на выводах светодиода от обоих контроллеров передается в виртуальный COM-порт ПК. Данный порт мы можем мониторить в любой программе, например в Hterm, но я для простоты решил использовать Arduino IDE.

    • Чтобы не "заморачиваться" с лишними символами, напряжение контроллеров выводится без точки, например, "c=45" значит, что напряжение на выводе ЦАП CANNY = 4.5В, соответственно "a=27" значит, что на ЦАП выводе Arduino = 2.7В.

Рассмотрим составной блок "Random (min...max)":

Составной блокСоставной блок

В данном блоге мы используем встроенный ГСЧ и функцию MAP для того чтобы выводить не просто числа от 0 до 65000, а в нужном нам диапазоне. Данный блок можно использовать, как библиотечный элемент и повторно использовать в других схемах. Более подробно о том, как работать с составными функциональными блоками, я писал в этой статье.

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

Заключение

Если вы все собрали верно, то получится примерно такой результат:

Различные варианты поданного на СИД напряженияРазличные варианты поданного на СИД напряжения

Слева направо:

  1. У контроллеров ничья, напряжение примерно равно.

  2. CANNY немножко выигрывает.

  3. CANNY ощутимо выигрывает.

  4. Arduino ощутимо выигрывает.

Пример вывода данных из монитора COM-порта (несвязанный с картинкой выше):

Монитор COM-портаМонитор COM-порта

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

Ну и еще под конец хочу сказать, что чертовски любопытно иногда погрузится в другую парадигму программирования. Я получил удовольствие, когда своими руками из функциональных блоков собрал простенький аналог функции "Random". Сейчас подумываю сделать еще несколько "библиотечных" элементов реализующих распространенные функции, которых порой не хватает среди готовых блоков CANNY и набросать об этом статью.

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

Подробнее..

Azure RTOS. Часть 1 обзор и запуск (STM32 CubeIDE HAL)

06.08.2020 16:13:59 | Автор: admin

На недавно прошедшем Microsoft Build 2020 многократно упоминалась Azure RTOS как специализированная ОС жесткого реального времени для микроконтроллеров.


В данном материале мы последовательно разберемся в том, что это за операционная система, какое место она занимает в продукции Microsoft для встраиваемых систем, а также установим планировщик ОС на один из микроконтроллеров STM32.


Кому не интересен обзор, а нужна практическая часть переходите сразу к ней.


Что это вообще такое?


Микроконтроллер это специализированная микросхема, объединяющая микропроцессор, память и периферийные устройства в одном корпусе. В отличие от "большого" компьютера имеет ограниченные объемы собственно этой самой памяти: типовые значения и для RAM, и для ROM десятки-сотни килобайт.


Как правило, микроконтроллер не имеет MMU (хотя есть и исключения, но это именно исключения, которые правильнее будет уже отнести к совершенно другой категории систем-на-кристалле), то есть отсутствует аппаратная поддержка механизма виртуальной памяти, что не позволяет использовать "полновесные" ОС даже при расширении объема встроенной памяти внешними микросхемами.


В связи с перечисленным, код под микроконтроллеры разрабатывается особым образом, в специализированных IDE, а операционные системы вообще выделены в особый класс. Основной функцией ОС для микроконтроллера является реализация многозадачности, а бонусом обычно идут разные стеки сети, файловых систем и т.д. Ни о каком окружении и вспомогательных утилитах, как в настольных ОС, здесь речи не идет. Так, например, в ОС для микроконтроллеров нет процессов, есть только задачи = нити = потоки, а сама ОС, как правило, компонуется с пользовательским кодом в единую микропрограмму ("прошивку"). Для понимания особенностей таких ОС рекомендуем статью. Отметим, что в ThreadX, несмотря на прямое отсутствие процессов, есть их аналог модули.


Впрочем, ограниченные объемы ресурсов никак не мешают использовать микроконтроллеры для решения узкоспециализированных задач. Более того, по меркам микроконтроллера, 128 КБ ROM и 64 КБ RAM уже довольно внушительные цифры. Микроконтроллер, несмотря на отсутствие "большой" ОС, успешно может записывать файлы на USB флешку, обмениваться данными по сети, а некоторые реализации содержат специальные инструкции для цифровой обработки сигналов, то есть могут решать достаточно "тяжелые" задачи.


Зачем вообще нужна ОС в микроконтроллере, ведь есть альтернативные варианты архитектуры типа "суперцикла"? Вопрос не очень простой и до сих пор вызывающий что-то типа религиозных войн. Достаточно подробный ответ на него дан в этой статье. Если совсем вкратце, то это упрощение кода в целом, что позволяет не только разработать, но и успешно модифицировать уже написанный код. Конечно, есть и минусы в виде потребления ресурсов "железа" на саму ОС и необходимости понимания основ написания потокобезопасного кода.


Все изложенное описывает картину достаточно укрупненно, так как везде есть исключения и оговорки. Например, на микроконтроллерах без MMU можно запустить ucLinux порт "большого" Linux специально для микроконтроллеров без MMU (без защиты памяти, естественно, со всеми вытекающими последствиями). Как правило, для этого потребуются дополнительные микросхемы памяти, так как встроенной хватит только для загрузчика этой самой ucLinux.


Что есть у Microsoft?


Microsoft традиционно занимается "большими" ОС, среди которых тоже есть специализированные решения в виде Windows 10 IoT Enterprise LTSC, значительно дешевле настольных систем и со специальными возможностями встраивания. Windows 10 IoT Enterprise требует практически полноценного (хоть и промышленного и малогабаритного) компьютера для запуска. Впрочем, есть редакция Windows 10 IoT Core, ориентированная только на приложения UWP, где требования к системе ниже: она успешно запускается на Raspberry Pi 2.


Здесь же нельзя не упомянуть класс операционных систем Windows Embedded Compact, которые могут работать на системах, по вычислительным возможностям находящимся где-то между полноценными компьютерами и микроконтроллерами. Compact отдельный класс ОС, не совместимых с "настольной" Windows, требующих особых средств разработки. Последний выпуск датируется 2013-м годом, далее ОС развития не получила, но все еще продается и поддерживается, как и несколько предыдущих версий.


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


Первым таким решением был .Net Micro Framework, который позволял разрабатывать для микроконтроллеров на языке C#. Вводную информацию можно найти в статье, а репозитории проекта по ссылке. К сожалению, на всех репозиториях стоит метка "Archive", а последние изменения датируются 2018-м годом. .Net Micro Framework достаточно интересен именно реализацией C#, что позволяет применить все преимущества данного языка на таких ограниченных системах, как микроконтроллеры. Реализация C# с его механизмами управления памятью представляет собой значительный "оверхед" для систем с ограниченными ресурсами, хотя из личного опыта работает достаточно хорошо и надежно (несмотря на часто встречающиеся едкие комментарии к тематическим статьям). Существуют и коммерческие проекты на .Net Micro Framework.


На данный момент доступны и сторонние реализации среды выполнения для C#: https://www.nanoframework.net/, https://www.wildernesslabs.co/. Отметим, что последняя аппаратная платформа вполне подходит и для запуска ucLinux, так что к выбору ОС следует относиться, как к выбору инструмента для решения задачи: что удобнее, то и применяем.


В 2019 году Microsoft поглощает Express Logic, и среди решений для микроконтроллеров от Microsoft появляется Azure RTOS, которая раньше называлась X-WARE IoT Platform. В Azure RTOS входит ядро ThreadX вместе с дополнительными компонентами, а также добавлены средства подключения к Azure IoT Hub и Azure IoT Central. Само название Azure RTOS подчеркивает применение совместно с сервисами Azure для устройств Интернета вещей.


В состав Azure RTOS входят:


  • сама ОС ThreadX, а именно, ядро, планировщик, реализующий многозадачность и синхронизацию задач;
  • стек TCP/IP NetX/NetX Duo;
  • стек FAT FileX;
  • стек USB Host/Device/OTG USBX;
  • реализация графического интерфейса GUI: GUIX и инструмент разработки (GUIX Studio);
  • реализация равномерного износа флеш-памяти для FileX: LevelX;
  • система трассировки событий TraceX;
  • SDK для Azure IoT поверх NetX Duo готовые средства для подключения устройства к службам Azure.

Нельзя не отметить одно из специализированных решений высокой готовности: Azure Sphere. Это безопасная платформа для приложений интернета вещей со встроенными механизмами коммуникаций и безопасности. Она представляет собой микроконтроллер (скорее даже SoC) с установленным ядром Linux, а также готовыми облачными сервисом для доставки обновлений безопасности.


Реальное время


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


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


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


Среди всех перечисленных продуктов к системам жесткого реального времени относятся:


  • Все семейство Windows Embedded Compact ориентировано на промышленные компьютеры;
  • Azure RTOS/ThreadX ориентирована на микроконтроллеры;
  • Azure Sphere специализированное решение, в котором операционная система (Azure Sphere OS) не является системой реального времени, но тем не менее предоставляются механизмы запуска пользовательских приложений реального времени.

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


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


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


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


Таким образом, для микроконтоллеров на данный момент Azure RTOS единственное решение жесткого реального времени от Microsoft. В принципе, сюда же (с некоторыми оговорками) можно было бы отнести и Azure Sphere, но это уникальный продукт с уникальными возможностями, поэтому его мы обсудим в отдельной статье.


Чем уникальна Azure RTOS


Исследование показывает, что данная ОС является одной из наиболее часто применяемых (более 6 миллионов инсталляций). В основном она используется в специализированном оборудовании, таком, как устройства беспроводной связи, принтеры, модемы, устройства хранения данных, медицинские устройства, интеллектуальные датчики.


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


  • Малый размер. Минимальная система занимает 2 КБ ROM. Размер увеличивается автоматически по мере использования возможностей ОС.
  • Поддерживаются различные методы реализации многопоточности, как вытесняющая, так и кооперативная многопоточность.
  • Детерминированное время переключения контекста (меньше 100 циклов), быстрая загрузка (меньше 120 циклов), опциональная проверка ошибок, пикоядро без "слоев".
  • Поддержка большого количества микроконтоллеров и IDE для разработки.
  • Порог вытеснения (Preemption threshold) порог вытеснения N означает, что данный поток может быть вытеснен только потоками с приоритетом выше N, т.е. от 0 до (N 1) включительно, а потоки с приоритетом ниже N (т.е. больше N включительно) не могут вытеснять данный поток. Правильное использование данной возможности уменьшает количество переключений контекста, а также уменьшает время реакции на внешние события. Подробную информацию можно найти в статье.
  • Сцепление событий (Event chaining) позволяет объединить несколько событий в единый сигнал синхронизации для потока, что позволяет синхронизироваться сразу по нескольким событиям, причем в разных комбинациях (И, ИЛИ).
  • Наследование приоритета (Priority inheritance) позволяет избежать негативных последствий ситуации инверсии приоритетов. Описание ситуации инверсии приоритетов тема для целой статьи, отдельно с данной проблемой многозадачных систем можно ознакомиться здесь.
  • Оптимизированная обработка прерываний от аппаратных таймеров;
  • Модули (Modules). ThreadX позволяет "обернуть" один или несколько потоков приложения в "модуль", который может быть динамически загружен и запущен на целевом устройстве. Модули позволяют производить обновление "в полях" с целью исправления ошибок. Также при помощи модулей можно разбить микропрограмму на сегменты и динамически определять набор выполняемых потоков, чтобы сэкономить память.
  • Встроенная трассировка событий и аналитика стека. Подбор размера стека потока является одной из самых важных задач при разработке с использованием ОС для микроконтроллера. Нельзя сделать слишком маленький стек, т.к. в отсутствие защиты памяти при переполнении стека произойдет порча областей памяти других задач. Слишком большой стек также недопустим, т.к. приведет к излишнему расходованию памяти, а она ограничена.

Также рекомендуем интересное сравнение ThreadX с FreeRTOS от инженера, работающего с обеими ОС, а также данную книгу.


Лицензирование


Azure RTOS коммерческая ОС с соответствующими требованиями к применению в производстве. Однако в ряде случаев платить за ее использование не понадобится.


  • Вам не требуется лицензия, если вы используете код не для производства, а для изучения, разработки, тестирования, портирования, адаптации системы для вашего решения;
  • Лицензия на использование в производстве включена автоматически при развертывании ОС на любой из микроконтроллеров из данного списка. На август 2020 года список еще не заполнен в связи с тем, что процедуры лицензирования еще не завершены, но уже есть соответствующий issue, в котором упомянуты микросхемы Microchip, NXP, Renesas, ST, и Qualcomm;
  • В ином случае вам нужно приобрести платную лицензию.

Во всех случаях ОС поставляется с исходным кодом.


Запуск ThreadX на STM32


Для понимания зависимостей между компонентами Azure RTOS приводим соответствующий рисунок из документации:



Как видим, ThreadX является основой для всего остального. Ядро ThreadX обеспечивает многопоточность, переключение контекста, синхронизацию потоков. Поддерживаются таймеры, очереди сообщений, семафоры, мьютексы, флаги событий, пулы байтов и блоков (в каком-то смысле аналог кучи в C++, но с потокобезопасным управлением).


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


Будем использовать относительно недорогую и популярную плату STM32F4Discovery, но весь процесс можно с успехом повторить на любом микроконтроллере, например, на сверхдешевом и доступном STM32F103C8T6.


STM32F4Discovery удобна тем, что уже имеет встроенный отладчик ST-Link, большое количество периферии для экспериментов, а все выводы микроконтроллера (STM32F407VGT6) выведены на контакты.



Инструменты, которые нам понадобятся


Эксперименты будем проводить на Windows 10 (подойдет также любая, начиная с 7).


Будем также использовать STM32 HAL набор универсальных API для микроконтроллеров STM32. Есть много мнений и "за", и "против" использования HAL. На наш взгляд, HAL, внося некоторый "оверхед", все же позволяет получить хорошо читаемый и модифицируемый код. HAL не требует скачивания, все необходимые библиотеки будут загружены автоматически при создании проекта.


  1. Скачиваем и устанавливаем STM32CubeIDE бесплатная IDE от STMicroelectronics на базе открытых инструментов.
  2. Загружаем исходный код ThreadX c GitHub. Существуют, конечно, "правильные" способы использования репозитория с исходным кодом в виде клонирования репозитория или создания форка, но для простоты описания просто скачиваем его как архив: зеленая кнопка "Clone", затем "Download zip".
  3. Подключаем плату STM32F4Discovery через разъем Mini-USB к компьютеру, проверяем наличие устройства "ST Link" в диспетчере устройств. Плата питается по этому же кабелю.


Создание и правка проекта


Запускаем STM32CubeIDE. При запуске нас попросят указать директорию для хранения Workspace можно оставить директорию по умолчанию.


Создаем новый проект, выбрав на главном экране Start New STM32 Project.



В появившемся окне в поле 1 набираем STM32F4DISCOVERY, выбираем плату в списке плат 2 (она там будет одна) и нажимаем Next (3):



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



Далее выбираем "Copy only necessary files..." и нажимаем Finish.



На появляющиеся вопросы отвечаем Yes:




Открывается окно Project Explorer, в нем находится иерархия файлов проекта. Находим файл threadx_test.ioc и переходим к нему (двойной клик):



В открывшемся окне переходим на вкладку Clock Configuration и убеждаемся, что система тактирования настроена следующим образом:



Заметим, что SYSCLK = 168 МГц, это нам понадобится далее для настройки таймера SysTick.


Вернемся на закладку Pinout & Configuration, где развернем пункт Connectivity, выберем USB_OTG_FS (1) и выключим его (2), затем в группе выводов (3) всем выводам выставим состояние Reset_State (4).



Мы отключаем USB OTG, так как в соответствующем коде используется функция HAL_Delay, которая, выполняя задержку, не позволяет планировщику ОС правильно переключать потоки. Код нужно адаптировать для использования с ThreadX, создав отдельный поток и заменив функцию задержки из HAL на функцию задержки из ThreadX, которая на время задержки передает управление другим потокам. Но для простоты примера мы этого не делаем, а просто отключаем USB OTG.


Аналогично в группе System Core перейдите к SYS и выберите таймер TIM7 в качестве Timebase Source для Serial Wire Debug:



SysTick Timer нам понадобится для ядра ThreadX.


Нам также нужно активировать прерывание на выводе PA0, к которому на плате подключена кнопка. Смотрим в User Manual к плате, как подключена кнопка:



Соответственно, нам понадобится прерывание по восходящему фронту. Подтягивающий вниз резистор на плате уже есть, встроенная подтяжка не потребуется. Соответствующие настройки делаем в группе System Core GPIO:



В группе System Core NVIC включаем прерывание EXTI0:



На этой же странице в группе Priority group установите значение "4 bits...":



Там же, но на вкладке Code Generation, отключим генерацию кода для Pendable request for system serivice, System tick timer, так как соответствующий код уже есть в порте ThreadX для ядра Cortex-M4, причем обработчики определены уже с нужными именами:



Сохраним проект и согласимся с появившимся предложением о генерации кода.


Далее распаковываем содержимое скачанного архива в каталог threadx_test\Middlewares нашего workspace. В Project Explorer нажимаем F5 и видим, что все необходимые файлы появились в дереве:



Нажимаем на название проекта threadx_test правой кнопкой мыши и выбираем Properties. Переходим к разделу C/C++ General Paths and Symbols. Нажимаем кнопку Add и добавляем путь


Middlewares/threadx-master/ports/cortex_m4/gnu/inc

Не забыв установить все флажки, как на рисунке:



Если вы делаете эксперименты на микроконтроллере с другим ядром, в вашем случае нужно включить заголовочные файлы для этого ядра, выбирайте путь соответственно (доступны cortex_m0, 3, 4, 7)


Аналогично добавляем путь


Middlewares/threadx-master/common/inc

Нажимаем Apply and Close и соглашаемся с перестроением индекса.


Теперь по пути Middlewares/threadx-master/ports в Project Explorer исключим из сборки:


  • весь каталог cortex_m0
  • весь каталог cortex_m3
  • весь каталог cortex_m7
  • cortex_m4/gnu/src/tx_vector_table_sample.S (таблица векторов прерываний уже есть в нашем стартовом коде)

Для этого кликаем по каждому каталогу/файлу правой кнопкой мыши, выбираем Properties и снимаем галочку Exclude resource from build:



Также исключите из сборки (или можете просто удалить):


  • Middlewares/threadx-master/cmake
  • Middlewares/threadx-master/docs
  • Middlewares/threadx-master/samples

И убедитесь, что следующие каталоги НЕ исключены из сборки:


  • Middlewares/threadx-master/common
  • Middlewares/threadx-master/ports/cortex_m4

Перейдем к скрипту компоновщика STM32F407VGTX_FLASH.ld и найдем строчки


  ._user_heap_stack :  {    . = ALIGN(8);    PROVIDE ( end = . );    PROVIDE ( _end = . );    . = . + _Min_Heap_Size;    . = . + _Min_Stack_Size;    . = ALIGN(8);  } >RAM

и после строки


    . = ALIGN(8);

добавляем строку


    __RAM_segment_used_end__ = .;

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


Если планируете запуск из RAM, можете то же самое сделать в файле STM32F407VGTX_RAM.ld.


Теперь в Project Explorer разворачиваем ветку Middlewares/threadx-master/ports/cortex_m4/gnu/src и открываем файл tx_initialize_low_level_sample.S.


Находим строку


SYSTEM_CLOCK      =   6000000

и меняем значение 6000000 на 168000000 в соответствии с частотой SYSCLK.


В следующей строке


SYSTICK_CYCLES    =   ((SYSTEM_CLOCK / 100) -1)

меняем значение 100 на 1000. Тем самым мы изменим частоту системных тиков со 100 до 1000 Гц: удобнее будет задавать задержки для соответствующих функций ThreadX, задержка в тиках будет равна задержке в миллисекундах.


Конкретное значение частоты тиков планировщика подбирается в зависимости от решаемой задачи.


В Core/Src/main.c в начале файла включим


/* USER CODE BEGIN Includes */#include "tx_api.h"/* USER CODE END Includes */

а до кода


  /* USER CODE END 2 */  /* Infinite loop */  /* USER CODE BEGIN WHILE */  while (1)  {    /* USER CODE END WHILE */    /* USER CODE BEGIN 3 */  }  /* USER CODE END 3 */

добавим вызов


  tx_kernel_enter();

тем самым передав управление планировщику.


Перейдем к Core/Startup/startup_stm32f407vgtx.s


После


.global  Default_Handler

Добавим


.global _vectors

А после


g_pfnVectors:

Также добавим


_vectors:

Тем самым скомпонуем нашу таблицу векторов с кодом ThreadX.


В Core/Src/ создайте файл demo_threadx.c и скопируйте в него код ниже.


#include "tx_api.h"#include "main.h"/* Размер стека каждого демо-потока в байтах */#define DEMO_STACK_SIZE         1024/* Размер пула памяти, из которого будет выделяться память для демо-потоков, в байтах */#define DEMO_BYTE_POOL_SIZE     10240/* Количество демо-потоков */#define THREAD_COUNT 4/* Массив структур, каждая из которых хранит информацию о потоке (thread control block) */TX_THREAD               thread_x[THREAD_COUNT];/* Структура, хранящая информацию о пуле памяти */TX_BYTE_POOL            byte_pool_0;/* Группа флагов событий */TX_EVENT_FLAGS_GROUP    evt_group;/* Область памяти для пула TX_BYTE_POOL */UCHAR                   memory_area[DEMO_BYTE_POOL_SIZE];/* Флаг события (маска) для потока N */#define EVT_KEYPRESS_THREAD(n) (1 << n)/* Время задержки после переключения режима светодиода */#define LED_PAUSE_AND_DEBOUNCE_TIME_MS 100/* Описание светодиодов для каждого потока */static const struct{    GPIO_TypeDef* GPIOx;    uint16_t GPIO_Pin;    uint32_t blink_delay_ms;    char thread_name[10];} BoardLedsSettings[THREAD_COUNT] ={    { LD3_GPIO_Port, LD3_Pin, 250,  "orange" },    { LD4_GPIO_Port, LD4_Pin, 500,  "green" },    { LD5_GPIO_Port, LD5_Pin, 750,  "red" },    { LD6_GPIO_Port, LD6_Pin, 1000, "blue" }};/* Callback внешнего прерывания (кнопки) */void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){    if(GPIO_Pin == GPIO_PIN_0)    {        for (int i = 0; i < THREAD_COUNT; i++) tx_event_flags_set(&evt_group, EVT_KEYPRESS_THREAD(i), TX_OR);    }}/* Функция-worker каждого из 4 потоков */void thread_entry(ULONG thread_input){    /* Поток управления одним светодиодом на плате */    uint8_t led_state = GPIO_PIN_RESET;    uint32_t cur_delay_ms = BoardLedsSettings[thread_input].blink_delay_ms;    ULONG actual_flags_ptr;    while(1)    {        HAL_GPIO_WritePin(BoardLedsSettings[thread_input].GPIOx, BoardLedsSettings[thread_input].GPIO_Pin, led_state);        led_state = !led_state;        if (TX_SUCCESS == tx_event_flags_get(&evt_group, EVT_KEYPRESS_THREAD(thread_input),            TX_AND_CLEAR, &actual_flags_ptr, cur_delay_ms))        {            /* Установлен флаг события "кнопка нажата". Выключаем светодиод */            HAL_GPIO_WritePin(BoardLedsSettings[thread_input].GPIOx, BoardLedsSettings[thread_input].GPIO_Pin, GPIO_PIN_RESET);            /* Пауза, что было видно, что светодиод погас */            tx_thread_sleep(LED_PAUSE_AND_DEBOUNCE_TIME_MS);            /* Дополнительно очистим флаг события, на случай, если оно произошло еще раз за время задержки (антидребезг) */            tx_event_flags_get(&evt_group, EVT_KEYPRESS_THREAD(thread_input), TX_AND_CLEAR, &actual_flags_ptr, TX_NO_WAIT);            /* Светодиод будет включен в следующей итерации цикла */            led_state = GPIO_PIN_SET;            /* Изменяем задержку */            cur_delay_ms = (cur_delay_ms == BoardLedsSettings[thread_input].blink_delay_ms) ?                cur_delay_ms * 2 : BoardLedsSettings[thread_input].blink_delay_ms;        }    }}/* Инициализация приложения */void tx_application_define(void *first_unused_memory){    CHAR    *pointer = TX_NULL;    /* Создаем byte memory pool, из которого будем выделять память для стека каждого потока */    tx_byte_pool_create(&byte_pool_0, "byte pool", memory_area, DEMO_BYTE_POOL_SIZE);    /* Создаем группу событий */    tx_event_flags_create(&evt_group, "event group");    /* Создаем в цикле 4 потока, каждый из которых получает в качестве параметра индекс данных из структуры BoardLedsSettings */    for (int i = 0; i < THREAD_COUNT; i++)    {        /* Выделяем стек для потока i */        tx_byte_allocate(&byte_pool_0, (void **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);        /* Создаем поток i */        tx_thread_create(&thread_x[i], BoardLedsSettings[i].thread_name, thread_entry, i, pointer, DEMO_STACK_SIZE,            1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);    }}

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


Выберите Project Build All и убедитесь, что сборка проекта прошла успешно.


После этого выберите Run Debug Configurations, кликните правой кнопкой мыши на STM32 Cortex-M C/C++ и выберите New Configuration:



Оставьте значения по умолчанию (там выбран ST-LINK уже с нужными параметрами) и нажмите кнопку Debug. Согласитесь с переключением перспективы.


Отладка остановится на строке


  HAL_Init();

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


Что происходит в данном примере


Приведенное приложение классический пример "лампочки и кнопочки" для RTOS. На плате распаяно 4 светодиода, и задача приложения мигать ими, причем у каждого из них должна быть своя частота этого мигания. Без RTOS это сделать достаточно сложно и неудобно. Также на плате имеется кнопка, и ее мы используем для демонстрации обработки внешнего прерывания в RTOS. Очень плохой практикой является обработка непосредственно в обработчике прерывания (наша функция-callback HAL_GPIO_EXTI_Callback() выполняется непосредственно в контексте прерывания), поэтому в самом обработчике мы устанавливаем флаг соответствующего события. В дальнейшем по этому флагу оно будет обработано в потоке.


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


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


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


Выделенная область стека передается в функцию tx_thread_create() в виде указателя и размера области памяти в байтах. Обратите внимание, что в нашем примере достаточно было просто объявить массив нужной длины и передать указатель на массив в эту функцию, что означало бы статическое выделение памяти для стека. Но мы пошли более сложным путем, чтобы показать, как в ThreadX устроено динамическое управление памятью. Мы статически создали массив для пула байтов (byte_pool_0), создали сам пул в строке


tx_byte_pool_create(&byte_pool_0, "byte pool", memory_area, DEMO_BYTE_POOL_SIZE);

Затем выделили из этого пула память для стека каждого потока в строке


tx_byte_allocate(&byte_pool_0, (void **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);

И передали соответствующий указатель (pointer) в функцию создания потока:


tx_thread_create(&thread_x[i], BoardLedsSettings[i].thread_name, thread_entry, i, pointer, DEMO_STACK_SIZE,    1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);

Обратим внимание на следующее:


  • Поскольку мы выделяли память динамически, мы также можем ее и освободить, например, после уничтожения потока. Память вернется в пул и может быть в дальнейшем использована повторно. В ThreadX уже решена проблема фрагментации возвращенной в пул памяти, поэтому проблем с повторным выделением не будет.
  • Все созданные потоки запускаются автоматически (параметр TX_AUTO_START).
  • Параметр TX_NO_TIME_SLICE отключает механизм time-slice для создаваемого потока. Это означает, что квант времени на исполнение процесса мы не задаем, а вместо этого полагаемся на планировщик.
  • Данный код не подходит для производства, поскольку для упрощения примера не производится анализ возвращенного значения функций на предмет возникновения ошибок.
  • ThreadX достаточно гибко конфигурируется путем применения директив препроцессора. Для упрощения примера мы их не рассматривали. Подробная информация доступна здесь.

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


Выводы


Мы рассмотрели лишь базовую часть Azure RTOS и (пока) не использовали расширенных функций, а также стеков FileX, NetX и т.д. Это тема следующих статей.


Мы убедились, что работу с ThreadX можно начать достаточно быстро, буквально в пределах рабочего дня. Microsoft также предлагает руководство пользователя к ОС, а если у вас еще остались вопросы по Azure RTOS или другим встраиваемым операционным системам Microsoft обращайтесь к нам в Кварта Технологии.


Автор статьи Сергей Антонович, ведущий инженер Кварта Технологии. Связаться с ним можно по адресу sergant (at) quarta.ru.

Подробнее..

Бинарный поиск в микроконтроллере

10.02.2021 08:12:41 | Автор: admin

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

Одно время мы выпускали несложный контроллер облачной СКУД на 100 пользователей. В его основе лежал микроконтроллер PIC18F46K22. В качестве памяти для хранения кодов ключей пользователей использовалась FLASH-память с интерфейсом I2C ёмкостью 64 кБ. Сама флешка довольно быстрая, но на шине I2C находилась ещё микросхема часов DS1307, которая работает на скорости не выше 100 кбит/сек. Высокой скорости работы нам не требовалось, поэтому в итоге вся шина была запущена на частоте 100 кГц.

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

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

Решить данную проблему можно двумя способами. Первый аппаратный вариант, предполагающий использование более быстрого интерфейса с флешкой, например, SPI. Скорости чтения здесь на два порядка выше, чем при использовании I2C. Это, так сказать, решение в лоб. Неудобство лишь состоит в том, что приходится закладывать в устройство новый компонент, переделывать код и печатную плату. Плюс потребуются дополнительные линии микроконтроллера для подключения такой микросхемы памяти.

Но есть и другой способ программный. Заключается он в переделке самой процедуры поиска. Изначально она выглядела так:

typedef struct{    uint32_t COD;    uint8_t nSchedule;} TSKUD_User;bool skudFindUserByCode(uint32_t pCOD){  TSKUD_User user;  for (uint8_t i = 0; i < SKUD_USERS_COUNT; i++)  {    skudReadUser(i, &user);    if (user.COD == pCOD)      return 1;  }  return 0;}

Функция skudReadUser считывала блок данных из I2C памяти, далее осуществлялась проверка на совпадение кода.

При ста пользователях в худшей случае (когда код находился в самом конце массива данных) время поиска занимало порядка 0,1 сек. При переходе же к 3000 пользователей время выросло 3 сек!

Поэтому для ускорения функция была переписана следующим образом:

bool skudFindUserByCode(uint32_t pCOD){  TSKUD_User user;  int16_t m, beg, end;  beg = 0;  end = SKUD_USERS_COUNT - 1;  while (beg <= end)  {    m = (beg + end) / 2;    skudReadUser(m, &user);    if (pCOD == user.COD)      return true;    if ((pCOD < user.COD) || (user.COD == 0))      end = m - 1;    else      beg = m + 1;  }  return false;}

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

О различных частных случаях при реализации бинарного поиска можно почитать в статье:Я не могу написать бинарный поиск (http://personeltest.ru/aways/habr.com/ru/post/146228).

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

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

  2. Если номер искомой карты меньше, то следует его искать в левой части массива. Тут мы отбрасываем сразу половину заведомо не подходящих вариантов. Индекс end теперь будет равен m 1.

  3. Если номер искомой карты меньше, то следует его искать в правой части массива. Так же отбрасываем сразу половину заведомо не подходящих вариантов, но меняем индекс beg (он будет равен m + 1).

Если в массиве данных вообще нет искомого значения ключа, то нам нужно выйти из цикла. Условием выхода является beg > end.

Очень важным является дополнительное условие user.COD == 0 в строке:

    if ((pCOD < user.COD) || (user.COD == 0))

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

Индекс

Значение

0

1307131

1

1308780

2

1318001

3

2174082

4

2290467

5

2291521

...

0

2996

0

2997

0

2998

0

2999

0

Можно было бы записывать туда значения 0xFFFFFFFF, но его мы используем в качестве сервисного для служебных нужд системы. Поэтому дополнительное условие user.COD== 0 всегда заставляет алгоритм искать код в левой половине массива данных.

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

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

А вот при бинарном поиске количество сравнений будет составлять log23000 11 шт!

Интересно, что если записей будет аж 4 миллиарда, то количество сравнений при использовании бинарного поиска увеличится всего лишь до 32!

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

Итерация

beg

end

m

Код искомой карты

Код карты в массиве по индексу m

1

0

2999

1499

2174082

0

2

0

1498

749

2174082

0

3

0

748

374

2174082

0

4

0

373

186

2174082

0

5

0

185

92

2174082

0

6

0

91

45

2174082

0

7

0

44

22

2174082

0

8

0

21

10

2174082

0

9

0

9

4

2174082

2290467

10

0

3

2

2174082

1318001

11

3

3

3

2174082

2174082

В итоге мы за 11 итераций нашли искомое значение. Важно понимать, что 11 итераций это максимальное время поиска. В вышеприведённом примере в случае, если бы мы искали значение 2290467, то количество итераций было бы равно 9.

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

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

Вот работа линейного поиска:

А вот бинарного:

Результат, как говориться, на лицо!

Подробнее..

Передача аналогового тв сигнала с помощью STM32

03.10.2020 22:04:09 | Автор: admin
Помните как некто cnlohr запустил передачу ТВ сигнала на ESP8266?

Недавно мне попалось к просмотру это видео, стало интересно как это возможно и выяснил что автор видео разогнал частоту I2S до телевизионного диапазона, а затем с помощью DMA генерировал AM сигнал. Мне захотелось повторить это, но или прошивка криво собирается, или ESP модуль оказался неподходящий. Запустить передачу телесигнала не получалось.

Затем я вспомнил что STM32 умеет выводить свой тактовый сигнал на один из пинов.

Введение


Современные микроконтроллеры могут работать на частотах в сотни МГц, они попадают в диапазон работы FM приемников и аналоговых телевизоров. Практически все они имеют возможность вывода своей тактовой частоты на один из пинов. В микроконтроллерах STM32 эта функция называется Master Clock Output.

Если выбрать источником тактирования PLL, частоту на выходе можно менять в широких пределах. Так-же выход MCO можно включить и выключить простым переключением режима пина в регистре GPIO. Это побудило меня к экспериментам над возможностями формирования радиосигналов с помощью микроконтроллера.

В наличии была отладочная плата с микроконтроллером STM32F407. Его максимальная частота ядра равна 168МГц, а максимальная частота переключения GPIO равна 84мгц.

Для начала нужно было понять на какую частоту настраивать MCO чтобы его мог поймать телевизор. Поиск привел меня на страницу с таблицей частот всех тв каналов. Наибольшую частоту ядра без превышения максимальной частоты переключения GPIO можно достичь выбрав 3 канал.
Частота ядра при этом будет равна 154.5МГц, а тактовый выход необходимо поделить на 2, чтобы получить искомые 77.25МГц.

Начало экспериментов


Чтобы долго не изучать мануалы, для генерации инициализационного кода был использован cubeMX. В нем настроил PLL на частоту 154.5МГц, вывод MCO1 сделал с источником от PLL предделителем на 2. К выводу PA8 подсоединил кусок провода.

Настройки тактирования в CubeMX


Скомпилировал проект, залил прошивку в плату и экран на телевизоре стал темным. Это означает что телевизор понимает импровизированную несущую как сигнал.

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

Результат программного управления MCO





Но как вывести на него изображение?

Использование DMA



Если осуществлять управление с помощью ядра, практически всё его время будет использовано только для переключения тактового сигнала и любое прерывание будет сбивать тайминги. Поэтому единственным способом управления осталось использовние DMA с буфером.

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

В cubeMX это выглядит так:
Настройки таймера



В ходе эксперимента выяснилось что к регистрам GPIO возможен и побайтовый доступ как самим ядром, так и через DMA, что позволило тратить всего 1 байт на пиксель:
#define GPIOA_MODER_8_11 (((uint8_t*)(&(GPIOA->MODER)))[2])#define MCO_ON()  (GPIOA_MODER_8_11) = 2#define MCO_OFF() (GPIOA_MODER_8_11) = 0

В начальной поставке библиотеки HAL, адресом назначения DMA является регистр таймера ARR. Пришлось написать функцию, позволяющую задать произвольный адрес назначения. Этим адресом является биты [16:23] регистра GPIOA->MODER.

Так-как DMA имеет 16 битный счетчик элементов, размер буфера ограничен в 64кб. Но можно настроить DMA на работу с двойным буфером, что позволяет увеличить количество элементов в 2 раза.

// Изначальная функция, которая принимает в качестве аргумента лишь источник данных, а назначением является регистр TIM->ARR (регистр предзагрузки)// HAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint16_t Length);// Добавлен аргумент - адрес назначения данныхHAL_StatusTypeDef HAL_TIM_Base_Start_DMA(TIM_HandleTypeDef *htim, uint32_t *pData, uint32_t *pDest, uint16_t Length);// Запуск в режиме двойного буфераHAL_StatusTypeDef HAL_TIM_Base_Start_DMA_DoubleBuffer(TIM_HandleTypeDef *htim, uint32_t *pData, uint32_t *pData2, uint32_t *pDest, uint16_t Length);


Формирование кадра



В стандарте PAL/SECAM видеосигнал имеет частоту кадров равную 25гц и 625 строк на кадр. В случае отказа от черезстрочной развертки остается 312 строк изображения с частотой полей 50гц. При максимальном размере буфера в 128кб получится: 131072/312 = 420 пикселей на строку.
Значит фреймбуфер получится размером 312x410.

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

Перед запуском передачи происходит заполнение фреймбуфера синхроимпульсами, а уровень черного обеспечивается заполнением ШИМ в 25%.
Заполнение буфера кадра
// заполнение синхроимпульсамиvoid init_fb(){  // кадровый синхроимпульс находится в начале буфера,  // чтобы прерывание конца передачи DMA (Vsync) приходилось  // на начало обратного хода луча  for (int i=0;i<24;i++)    for (int j=0;j<(WIDTH);j++)      frameBuf[i][j] = 2;    // строчный синхроимпульс  for (int i=0;i<312;i++)    for (int j=(WIDTH - 30);j<(WIDTH);j++)      frameBuf[i][j] = 2;}// приведение буфера кадра к уровню черногоvoid clear_fb(){    for (int i=24;i<312;i++)    {      // смещение начала строки      int offset = (i * WIDTH);      // ядро cortex-m4 позволяет работать с невыровненными данными,      // 32 битный доступ позволяет ускорить работу      uint32_t* data_ptr = (uint32_t*)(&((uint8_t*)(frameBuf))[offset]);      // диагональные линии менее заметны на экране чем вертикальные      uint32_t pattern = 0x02020202;      pattern &= ~( 2 << (((i)%4)*8) );            for (int j=0;j<(390);j+=4)      {        *(data_ptr++) = pattern;      }    }}



На этом этапе записью значений в frameBuf можно формировать изображение на экране телевизора.
Графическая библиотека была портирована из демо проекта от другой платы discovery. С ее помощью можно генерировать различные графические примитивы и символы. Так-же был портирован когда-то мной написанный клон игры Space Invaders и 3D шары из проекта от ESP8266.

Результат получился следующий:
Полученный результат





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

Эксперименты с созданием промежуточных уровней сигнала



Что если управлять тактовым выходом не просто включая и выключая его, а менять значение регистра OSPEEDR? Этим регистром управляется крутизна фронта переключения вывода. Было интересно, возможно ли меняя его значение создать на экране телевизора больше чем 2 градации яркости.

Написал код, который в бесконечном цикле перебирает 5 вариантов:
MCO выключен через MODER
OSPEEDR = 0
OSPEEDR = 1
OSPEEDR = 2
OSPEEDR = 3

На экране появилось 4 полосы с разной яркостью. Состояние с минимальной крутизной фронтов и выключенным совсем MCO никак не отличается для телевизора.
Полосы с градациями яркости



Использование регистра OSPEED вместо MODER позволило увеличить четкость изображения.
Изображение при использовании модуляции с помощью OSPEEDR



Так-же пытался использовать I2S, но безуспешно.
Выше примерно скорости 20 Мбит/с при дальнейшем увеличении частоты тактирования интерфейса, появляется нестабильность в работе. А на стабильных частотах если принимать гармоники сигнала, изображение едва отличимо от шума. SPI1 может работать на частотах выше, но качество сигнала тоже остается плохим.

Видео демонстрации работы прилагаю, код на гитхабе

github.com/rus084/F4Discovery-tv-transmitter



Как насчет частотной модуляции?



В STM32 в регистре RCC_CR есть биты HSITRIM, отвечающие за подстройку частоты HSI генератора.
Если настроить PLL со входом от HSI и выходной частотой, попадающей в FM диапазон, можно получить радиосигнал, который будет приниматься FM приемником. Модуляция осуществляется изменением значения битов HSITRIM.

Доказательство работоспособности показано в этом видео. Исходники тоже прилагаются.

github.com/rus084/F4Discovery-fm-transmitter



p.s. Да, код ужасный, но как proof of concept сгодится
Подробнее..

Категории

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

  • Имя: Макс
    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