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

Azure rtos

Разработка защищённого 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 миллисекунд, что вполне приемлемо для интерфейса встраиваемого устройства.

Подробнее..

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.

Подробнее..

Категории

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

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