ARM MBED OS. Работа с произвольным МК STM32 под PlatformIO

в 8:50, , рубрики: alglib, arm mbed os, cpu usage, platformio, random number generator, rtos, stm32, программирование микроконтроллеров

imageКогда в январе сего года я писал материал о файловой системе LittleFS (интегрированной в состав arm mbed os), то обещал в скорейшем времени описать создание проекта с arm mbed os для произвольного микроконтроллера STM32. Как известно, онлайн IDE от ARM (а точнее, выделенного подразделения Arm mbed) поддерживает, во-первых, строго определенное число отладочных плат, и число их невелико; во-вторых, экспортирует онлайн-примеры, на базе которых можно строить какие-то свои проекты, только для наиболее известных IDE: ARM, uVision KEIL и IAR. Более того, некоторые примеры не экспортируются вовсе. То есть, доступны для экспорта или только варианты для IAR, или только для KEIL, и так далее. Так что, как в то время показалось, научиться “прикручивать” arm mbed os к любому МК было бы не лишним вовсе.

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

Так или иначе, оставался неразрешенным один немаловажный вопрос. Мы все используем различные IDE и различные тулчейны. Процесс портирования довольно непрост, и требует определенных танцев с бубном. К примеру, ассемблер для GCC не поддерживает синтаксис x86 (там AT&T), поэтому самая первая и элементарная проблема, с которой тут столкнется программист – это ругань того же GCC-шного компилятора на ассемблерные вставки в исходных кодаx операционной системы Arm mbed.

Кто-то пользуется IAR, кто-то uVision, кто-то пишет в Sublime Text, а кто-то (как и я) – в Code::Blocks. Кто-то использует Windows, а кто-то – Linux. Объять необъятное и охватить неохватываемое мы не в силах, и при этом оставить один из вариантов без рассмотрения – значит оставить за бортом какую-то часть аудитории.

Решение пришло внезапно и оказалось весьма простым и универсальным.

PlatformIO IDE.

PlatformIO – кроссплатформенный тулчейн, написанный на python, наличие которого на машине пользователя является, пожалуй, единственным обязательным условием (не ниже версии 2.7).

По своему исполнению и использованному инструментарию PlatformIO мне напомнил несколько лет назад вышедшую IDE MicroEJ Studio, в которой можно было писать код для микроконтроллеров на Java. В дальнейшем в МК заливалась MicroJVM (написанная на С), и код исполнялся в ней. Широкого распространения, впрочем, среда не получила, и в массы не пошла.

PlatformIO может использоваться в составе ряда широко распространенных IDE и редакторов кода:

  • Atom;
  • Clion;
  • Eclipse;
  • Emacs;
  • NetBeans;
  • Qt Creator;
  • Sublime Text;
  • VIM;
  • Visual Studio;
  • VSCode и т.д.

Основной особенностью PlatformIO является использование конфигурационного файла “platformio.ini”, который используется для определения тарджет-платформы проекта, и последующей подгрузки библиотек и построения зависимостей в соответствии с тем описанием, что находится в этом конфигурационном файле.

Основными элементами являются PlatformIO IDE и PlatformIO Core.
В довольно-таки уже далеком 2016 году PlatformIO была кандидатом на награждение в номинации “Best IoT Software&Tools” в конкурсе 2016 IoT Awards.
Это в общих чертах. Подробную документацию можно изучить на сайте проекта platformio.org и в разделе Документация.
Наша же задача – установить требуемые средства разработки, создать проект, и что-то в нем сделать.

Atom vs. VS Code

На домашней странице к загрузке предлагаются два редактора: Atom и VS Code. Я попробовал оба, и сразу скажу: VS Code удобнее. Хотя бы потому, что в нем элементарно присутствует переход по коду. Забегая вперед, скажу: в проекте библиотек и исходников arm mbed os вы не увидите, они все сидят в локальном репозитории, поэтому в дереве проекта будут только ваши main.cpp и всё прочее, что создадите вы сами. Поэтому смотреть какие-то объявления, классы и их объекты, интерфейсы классов, придется сто процентов. А Atom такой возможности… не представляет! И при использовании Atom довольствоваться нужно будет только документацией mbed os. Согласитесь, это неудобно.

Итак, дальнейшее рассмотрение процесса я провожу в применении к VS Code. Нам необходимо проделать следующие шаги:

  1. Установить VS Code.
  2. Установить PlatformIO IDE.
  3. Настроить udev rules (для пользователей Линукс) – возможно, это не понадобится, но чтобы потом не подпрыгивать на стуле, нанесем превентивный удар.
  4. Создать проект и включить в него минимальный функционал. Удостовериться, что он собирается, грузится и отлаживается на плате (в качестве сервера используется OCD/GDB).

Устанавливаем VS Code, предварительно перейдя по ссылке, и скачав установщик для желаемой системы.

После установки запускаем редактор, открываем панель расширений (Extensions), и вводим в поиске “platformio”. Первым же вариантом выскочит “PlatformIO IDE”. Нажимаем “Install”, дожидаемся окончания установки, перезагружаем редактор.

image

Пользователям Linux можно сразу установить udev rules для адекватной работы отладчика. В принципе, этот шаг можно опустить, и вернуться к нему в том случае, если при старте отладчика терминал выдаст сообщение вроде “Remote communication error. Target disconnected.: Connection reset by peer.

Открываем терминал и пишем в нем:

sudo curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/scripts/99-platformio-udev.rules > /etc/udev/rules.d/99-platformio-udev.rules

Если терминал выдаст “Permission denied”, то скачиваем файл “99-platformio-udev.rules” по ссылке, и принудительно копируем файл в etc/udev:

sudo cp 99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules

Учтите, что после команды cp должен быть определен полный путь к файлу. Если файл .rules находится в папке, к примеру, “Downloads”, то терминальная команда будет выглядеть так:

sudo cp ~/Downloads/99-platformio-udev.rules /etc/udev/rules.d/99-platformio-udev.rules

Далее выполняем:

sudo usermod -a -G dialout $USER
sudo usermod -a -G plugdev $USER

где $USER – это имя вашего пользователя. К примеру, у меня это subdia.
После этого все проблемы с отладчиком, если они могли возникнуть, должны решиться.

Окружение и локальный репозиторий arm mbed os

После установки окружения не будет лишним понять, где находится локальный репозиторий arm mbed os (как я уже говорил, в дереве проекта вы его не увидите), где находятся все исходники mbed os, и куда сохраняется скомпилированный проект.

В процессе установки platformIO разворачивает локальный репозиторий arm mbed (и не только его) по пути $HOME/.platformio/packages. Вот, к примеру, arm mbed.

image

Файлы прошивки и прекомпилированные исходники находятся непосредственно в папке проекта.

image

Это всё, что нам нужно знать о том, что где хранится. Перейдем непосредственно к созданию проекта.

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

Вкратце о создаваемом проекте. По очевидным причинам, я принял решение создать проект для платы, которая не включена в состав поддерживаемых ARM online IDE, а именно STM32F4DISCOVERY.

В мире встраиваемых систем принято создавать демонстрационные проекты с миганием светодиодов. Мы этого делать не будем – это уже просто и неинтересно. PlatformIO подразумевает несколько типов проектов: cmsis, hal, rtos, и так далее. Так как речь сейчас идет об arm mbed os, то есть об операционной системе, создадим проект именно для rtos.

В проекте мы создадим и запустим три задачи (Task): первая будет выполнять перемножение массивов типа float (у нас же процессор Cortex-M4F, так воспользуемся FPU), вторая задача… ну ладно — мигать светодиодами (=)), а третья – определять степень загруженности процессора.

Итак, поехали.

Открываем VS Code. Первым делом откроется окошко PIO Home. Выбираем “New Project”.

image

В окне “Project Wizard” указываем название проекта (у нас пусть это будет “armmbed_F407_CPU_usage”), и выбираем плату в выпадающем списке “Board”. Для читателей, планирующих использовать материал при написании софта для своих авторских плат: да, привязка к конкретной плате, но все ноги и периферию можно перенастраивать. Далее я пару слов об этом скажу, не спешите расстраиваться. Итак, Board.

image

Выбираем STM32F4DISCOVERY, и переходим в окно “Framework”. Тут у нас несколько вариантов.

image

Так как мы условились использовать arm mbed os, то очевидно, что здесь выбираем вариант “mbed”. Жмем “finish” — готово. Мастер маленько подумает, и откроет свежесозданную болванку проекта. Взглянем на это.

image

Как я уже упоминал выше, в проекте всего две папки по умолчанию: lib (пустая) и src, содержащая единственный файл main.cpp. Всего исходного кода, напомню, здесь мы не увидим. Но тем не менее, мы имеем возможность использовать весь функционал arm mbed os. Чтобы использовать rtos, мы должны добавить флаг сборки в файл “platformio.ini”:

build_flags =-DPIO_FRAMEWORK_MBED_RTOS_PRESENT

Вообще, конфигурационный файл заслуживает отдельного рассмотрения. Мне этот подход напомнил TIRTOS/SYSBIOS от Texas Instruments с их конфиг-файлом .cfg, хоть в arm mbed все и проще намного. В конфигурационном файле можно декларировать многое — от аппаратных ресурсов до флагов сборки и отладки. К примеру, вот состав простейшего конфигурационного файла:

[env:disco_f407vg]
platform = ststm32
framework = mbed
board = disco_f407vg

А это — конфиг-файл нашего примера в окончательном его виде:

[env:disco_f407vg]
platform = ststm32
board = disco_f407vg
framework = mbed
build_flags =   -DPIO_FRAMEWORK_MBED_RTOS_PRESENT -O1 -Wl,-u_printf_float
  -D std=gnu99 -fno-builtin-printf -fexceptions -fpermissive
debug_flags = -D DEBUG=1 -DDEBUG_LEVEL=DEBUG_NONE
monitor_baud = 115200

Так что здесь есть что осваивать на досуге.

Итак, начинаем приводить проект в тот вид, который нам необходим.

Я буду приводить код блоками, и пояснять, что в нем происходит. Для начала, мы должны включить в исходник заголовочные файлы “mbed.h” и “rtos.h”. Думаю, понятно, зачем.

Функция “main” примет следующий вид:

/**************************************************************************/
int main (void) {

     Thread thread0;
     Thread thread1;
     Thread thread2; 
     Thread::attach_idle_hook (&sleeping_sun);
     thread0.start (&ledblink);
     thread1.start (&cpu_usage);
     thread2.start (&math_thread);
     while (true) {

               }
}
/**************************************************************************/

Сначала мы создаем объекты класса “Thread”, то есть по сути, наши задачи (Task, Thread), которые будут нам обеспечивать определенный функционал.
Если кто-то обратил внимание, то следующей строкой значится

Thread::attach_idle_hook (&sleeping_sun);

Это задача “idle” — то есть задача с пониженным приоритетом, которой отводится только то время, которое остается у процессора после выполнения задач с нормальным и повышенным приоритетами. Ну, в нашем случае эта задача останется голодной, так как у процессора не останется на нее времени. Я привел это здесь просто для примера.

Далее мы запускаем задачи по очереди методом “start”, передавая ему ссылки на функции задач, а именно на то, что будет выполняться в процессе. Это “ledblink” — шморгалка, “cpu_usage” — подсчет загрузки CPU, и самая тяжелая — “math_thread”, выполняющая перемножение массивов.

Посмотрим по очереди на каждую из задач. С “ledblink” все просто.

/**************************************************************************/
void ledblink (void) {
     while (true) {
              myled1 = !myled1;
              Thread::wait (500);
             }
}
/**************************************************************************/

Мы поочередно меняем состояние вывода со светодиодом на противоположное, и вызываем задержку в 500 мс. Кстати, декларация “myled1” выглядит так:

DigitalOut myled1(LED1);

Обратим теперь свое внимание на задачу “cpu_usage”.

/**************************************************************************/
void cpu_usage (void) {
     Timer tim; 
     CPU_Usage cpu(tim, 1); 
     cpu.working(); 
     uint8_t value = 0;
     while (true) {
              cpu.delay(0.25);
              value = cpu.update();
              pc.printf("CPU %i", value);
             }
}
/**************************************************************************/

Здесь уже все несколько сложнее. Вообще, дабы не выдумывать велосипед, я использовал готовую библиотеку, написанную одним веселым парнем еще в 2014 году для arm mbed, которая так и называется: CPU_Usage. Взять ее можно по ссылке, там же приводится краткое ее описание. Библиотека использует таймер (мы видим объект класса Timer tim). Сначала вызывается конструктор класса “cpu”, затем поочередно методы “working” (начало работы), и “update” — вычисление загрузки процессора в процентах.

Пожалуй, сейчас самый подходящий момент для демонстрации. Покажу скрин из режима отладки.

image

Слева вверху видим значение “value” = 95. Значит, процессор в тот момент был загружен на 95%. Вообще, по результатам эксперимента это значение при выполнении одних и тех же задач варьировалось от 87 до 98%.

Кстати, почему я демонстрирую скриншоты из дебаггера, а не из терминала? Все просто, у меня под рукой нет переходника UART-USB, поэтому я не могу использовать UART терминал (вот эта функция “pc.printf()” — это как раз вывод по UART, pc — объект класса Serial).

И последняя, и самая прожорливая для процессора – задача “math_thread”. Посмотрим на нее – сначала в “голом” виде, затем немножко дополним плюшками arm mbed.

/**************************************************************************/
void math_thread(void) {
         volatile uint16_t rand_num_dmassi1 = 0;
         volatile uint16_t rand_num_dmassi2 = 0;
         float result;
         while (true) {
                  rand_num_dmassi1 = RandomMassIndex();
                  rand_num_dmassi2 = RandomMassIndex();
                  result = (DigMas1[rand_num_dmassi1]*DigMas2[rand_num_dmassi2]);
                 }
}
/**************************************************************************/

Когда я придумывал, чем поднагрузить процессор, перемножение массивов сразу пришло мне на ум. И я вспомнил ситуацию, как заказчик, для которого я кое-что делал удаленно (и отлаживал тоже удаленно, через полмира), кричал мне в скайп: “Вы же программист, так загрузите процессор! Он же совсем холодный, сделайте так, чтобы он нагрелся!”. Давайте теперь уже таки сделаем так, чтобы наш MCU нагрелся. =)

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

Если посмотреть на задачу-вычислитель произведения, то мы увидим там два вызова “RandomMassIndex()”. Это как раз функция, возвращающая значение в диапазоне (у нас диапазон ограничен числом элементов массивов).

/**************************************************************************/
uint16_t RandomMassIndex (void){
          uint16_t randval;
          alglib_impl::ae_state mystate;
          randval = alglib_impl::ae_randominteger(18, &mystate);
          return randval;
}
/******************************END_OF_FILE*********************************/

Итак, что мы здесь делаем. Сначала инициализируем структуру “ae_state” (она используется для внутренних нужд), а затем – просто вызываем метод “ae_randominteger”, которому передаем ссылку на нашу структуру, и диапазон, в котором мы хотим получить сгенерированное случайное число (у нас это 0..18). Эта цифра должна быть меньше максимально генерируемого значения. Число элементов массива у нас – 20 (0..19), и максимальное число равно 19. Так что нам в качестве граничного аргумента 18 подойдет как нельзя лучше.

Кстати, можно посмотреть на результаты вызова этой функции.

image

Вверху слева – сгенерированные случайные индексы массивов, “rand_num_dmassi1” и “rand_num_dmassi2”. 13 и 12.

Прогоним еще один цикл, посмотрим – изменятся ли.

image

11 и 17. Изменились. Значит, все работает.

Раз уж мы заговорили об анализе ресурсов (в частности, об использовании процессорного времени), то немного уделим времени памяти и приоритетам задач. В arm mbed os реализован целый класс rtos::Thread для этих нужд.

Прямо в задачу “math_thread” добавим следующие строки:

osThreadId_t this_thread_id;
volatile uint32_t this_thread_stacksize;
volatile osPriority_t this_thread_priority;

this_thread_id = osThreadGetId();
this_thread_stacksize = osThreadGetStackSize(this_thread_id);
this_thread_priority = osThreadGetPriority(this_thread_id);

Здесь (и выше) я использовал ключевое слово volatile – чтобы переменную можно было отслеживать.

Итак, сначала мы получаем ID задачи для дальнейшего использования. Затем вызываем методы для определения стека задачи и ее приоритета. Приоритет можно менять на ходу – в некоторых применениях это востребовано.
Смотрим.

image

Видим, что размер стека задачи равен 4096 байт, а приоритет – osPriorityNormal. Нормальный, в общем, приоритет.

Кроме этого, мы можем оценить степень используемости, размер неиспользованного и использованного стека. Прямо в main добавляем:

volatile uint32_t threads_stack;
volatile uint32_t threads_max_stack;
volatile uint32_t free_stack;
volatile uint32_t used_stack;

И после запуска задач:

threads_stack = thread0.stack_size();
threads_stack = thread1.stack_size();
threads_stack = thread2.stack_size();

threads_max_stack = thread0.max_stack();
threads_max_stack = thread1.max_stack();
threads_max_stack = thread2.max_stack();

free_stack = thread0.free_stack();
free_stack = thread1.free_stack();
free_stack = thread2.free_stack();

used_stack = thread0.used_stack();
used_stack = thread1.used_stack();
used_stack = thread2.used_stack();

Здесь вызываются четыре метода. “Stack_size()” возвращает размер стека задачи (аналогично тому, что мы оценивали чуть ранее), “max_stack()” возвращает размер максимально использованного в процессе выполнения, “free_stack()” возвращает размер свободного места, а “used_stack()” — размер использованного. Возвращаемые значения – в байтах. Для всех трех наших задач эти величины будут одинаковыми.

Посмотрим, что нам покажут по телевизору в дебаггере.

image

Как видим, от 4096 байт мы откушали совсем немного – всего 64 байта, и имеем в запасе еще аж 4032 байта.

Пожалуй, на этом с экспериментами и анализом мы закончим – я и так заигрался.

Да, что еще хотел сказать по поводу авторских плат. Сейчас кто-то может сказать, мол, вот – взял F4Discovery, на ней поигрался в свое удовольствие, а у меня вообще плата самодельная, и светодиоды вообще висят на других ногах, и в целом, я хочу SPI на ней поднять. Так вот, в репозитории armbed, в папке “targets” (выбираем дальше уже свои вполне конкретные MCU – их там тьма), в директориях каждого микроконтроллера, есть чудесные хидеры с названиями “PinNames.h”, “PeripheralPins.h” и “PeripheralNames.h”. Редактируя эти файлы, можно добавлять/редактировать/удалять периферию.

На этом, пожалуй, я остановлюсь. Больше примеров для различных применений arm mbed (в том числе и не-rtos, а просто bare metal) можно клонировать или скачать архивом здесь.

Ссылку на архив (Google диск) с нашим созданным примером прикрепляю к материалу, а полный исходный код размещаю ниже под спойлером — для полноты охвата всей картины. Если что – добро пожаловать на почту subdia.subdia@gmail.com.

main.cpp

#include "mbed.h"
#include "rtos.h"
#include "CPU_Usage.h"
#include "alglibmisc.h"
#include "ap.h"

DigitalOut myled1(LED1);
DigitalOut myled2(LED2);

Timer tim;                        
CPU_Usage cpu(tim, 1);  
Serial pc(USBTX,USBRX,9600);

#define PRETTY_ENOUGH  20

float DigMas1[PRETTY_ENOUGH] = {0.1234, 1.1234, 2.1234, 3.1234, 4.1234, 5.1234, 6.1234, 7.1234, 8.1234, 9.1234, 10.1234, 11.1234, 12.1234, 13.1234, 14.1234, 15.1234, 16.1234, 17.1234, 18.1234, 19.1234};
float DigMas2[PRETTY_ENOUGH] = {0.5678, 1.5678, 2.5678, 3.5678, 4.5678, 5.5678, 6.5678, 7.5678, 8.5678, 9.5678, 10.5678, 11.5678, 12.5678, 13.5678, 14.5678, 15.5678, 16.5678, 17.5678, 18.5678, 19.5678};

uint16_t RandomMassIndex (void);
/**************************************************************************/
void math_thread(void) {
    volatile uint16_t rand_num_dmassi1 = 0;
    volatile uint16_t rand_num_dmassi2 = 0;
    float result;
    osThreadId_t this_thread_id;
    volatile uint32_t this_thread_stacksize;
    volatile osPriority_t this_thread_priority;
    
    while (true) {
        rand_num_dmassi1 = RandomMassIndex();
        rand_num_dmassi2 = RandomMassIndex();
        result = (DigMas1[rand_num_dmassi1]*DigMas2[rand_num_dmassi2]);
        
        this_thread_id = osThreadGetId();
        this_thread_stacksize = osThreadGetStackSize(this_thread_id);
        this_thread_priority = osThreadGetPriority(this_thread_id);
    }
}
/**************************************************************************/
void cpu_usage (void) {   
    uint8_t value = 0;
    while (true) {
        cpu.delay(0.25);
        value = cpu.update();
        pc.printf("CPU %i", value);
    }
}
/**************************************************************************/
void ledblink (void) {
    
    while (true) {
        myled1 = !myled1;
        Thread::wait (500);
    }
}
/**************************************************************************/
void sleeping_sun(void) {
    return;
}
/**************************************************************************/
int main (void) {

    Thread thread0;
    Thread thread1;
    Thread thread2;   
    
    volatile uint32_t threads_stack;
    volatile uint32_t threads_max_stack;
    volatile uint32_t free_stack;
    volatile uint32_t used_stack;

    Thread::attach_idle_hook (&sleeping_sun);
    thread0.start (&ledblink);
    thread1.start (&cpu_usage);
    thread2.start (&math_thread);

    threads_stack = thread0.stack_size();
    threads_stack = thread1.stack_size();
    threads_stack = thread2.stack_size();

    threads_max_stack = thread0.max_stack();
    threads_max_stack = thread1.max_stack();
    threads_max_stack = thread2.max_stack();

    free_stack = thread0.free_stack();
    free_stack = thread1.free_stack();
    free_stack = thread2.free_stack();

    used_stack = thread0.used_stack();
    used_stack = thread1.used_stack();
    used_stack = thread2.used_stack();
    cpu.working();
    while (true) {
        
    }
}
/**************************************************************************/
uint16_t RandomMassIndex (void){
    uint16_t randval;
    alglib_impl::ae_state mystate;
    randval = alglib_impl::ae_randominteger(18, &mystate);
    return randval;
}
/******************************END_OF_FILE*********************************/

Спасибо за внимание, всем удачного дня и хорошего настроения.

image

Автор: Денис Громов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js