STM32, C++ и FreeRTOS. Разработка с нуля. Часть 1

от автора


Введение

Не так давно мой отдел столкнулся с трудностями поиска новых инженеров программистов для разработки встроенного ПО. Опытным и умным не нравился уровень зарплаты, а молодых просто нет в нашем городе. Поэтому под патронажем нашей доблестной глобальной компании со штаб квартирой где-то в Сент Луисе, мы начали сначала набирать студентов в интернатуру, а потом, решили пойти другим путем и сделать целых два курса по разработке ПО, а уже там самим выбирать самых “толковых” если понадобятся вдруг новые сотрудники. Это намного дешевле и позволяет охватить максимальное количество претендентов.
Немного отступлю от темы, сам я программировал последний раз очень давно, и вообще больше на С#, а последний глобальный проект на микроконтроллере (PIC16 на зыке Си) был сделан в далеком 2007 году.
Поэтому мне предстояло разобраться с современными микронроллерами, языком С++ и операционной системой реального времени.
Конечно все наши проекты уже сейчас используют ОСРВ и пишутся на С++, но как разработчик я в них не учувствую, а занимаюсь тунеядством управлением проектами разработки такого ПО.

Выбор

Времени у меня на все про все было дано 1 месяц. С начала июня 2015 до начала июля 2015, потому что потом я собирался в отпуск, а после отпуска обычно полно работы. Надо было делать все быстро и четко.
Немного проконсультировавшись с коллегами, выяснил, что модное направление ARM Cortex различные ядра и из доступных отладочных плат можно заказать Olimex STM32P152 которые стоили 25 долларов. Они пришли очень быстро — 6 плат по цене примерно 2000 рублей. Стоит заметить, что эти платы были закуплены нами для университета, где собственно и будет проходить этот курс.
image

Исходные данные: Цель

Первым делом, нужно было определиться с тем, какую информацию и что давать студентам. Для себя я решил, что главной целью будет — показать весь процесс разработки ПО для устройств с использованием микропроцессора STM32, языка программирования С++ и операционной системой реального времени FreeRTOS, начиная от требований и разработки архитектуры и заканчивая кодированием.

Исходные данные: Ограничения

Итак задачей нашего проекта является создание демо приложения для отладочной платы Olimex STM32P152, по стандартам кодирования (который я тут приводить не буду), написанным на языке С++ с использование FreeRTOS.
Приложение должно быть понятным, простым и ненавязчивым, без заумных конструкций присущих языку С++. Архитектура должна быть описана на языке UML.

Исходные данные: Функциональные требования

Устройство должно измерять три параметра (иметь три переменных): Температуру микропроцессора, Напряжение VDDA, Напряжение с переменного резистора
SR1: Устройство должно выводить значение этих переменных на индикатор.
SR2: Единицы измерения для Температуры микропроцессора — градусы Цельсия, для остальных параметров — вольты.
SR3: При нажатии на кнопку 1, на индикаторе должен показываться экран со следующей измеряемой переменной,
SR4: При нажатии на кнопку 1 Светодиод 1 должен изменять свое состояние
SR5: При нажатии на кнопку 2, на индикаторе должен поменяться режим отображения переменных с постоянного показывания переменной на последовательное (менять экраны раз в 1.5 секунды) при следующем нажатии с последовательного на постоянное,
SR6: При нажатии на кнопку 2 светодиод 2 должен менять свое состояние.
SR7: Светодиод 3 должен моргать раз в 1 секунду.

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

Разработка: начало работы

И так требования готовы, можно приступать. Начнем с инфраструктуры. Для начала я создал проект в IAR для C++, ничего нового тут нет. Вот эта статья все описывает Создание проекта на C++ в IAR для STM32. Здесь останавливаться не будем.

Разработка: обертка для FreeRTOS

Поскольку я собрался использовать С++, а операционная система написана на Си, то мне нужна С++ обертка для FreeRTOS. В идеале можно было бы написать обертку так, чтобы она подходила под большинство ОСРВ, и тогда в проектах, можно было не зависеть от типа ОСРВ, но заморачиваться с этим я не стал и в данной ситуации выбрал только те функции операционной системы, которые мне нужны, и только их и обернул, тупо скопировав сигнатуру методов.
Также был добавлен статический метод static void run(void *parameters); Это как раз та функция, указатель на которую будет использоваться при создании задачи.
Для того, чтобы было возможно вызывать метод экземпляра класса в задаче, был сделан интерфейс iActiveObject с виртуальной функцией virtual void run(void) = 0; и глобальным атрибутом для хранения указателя на задачу.

iActiveObject.h

#include "types.h"          //Стандартные типы проекта class iActiveObject {   public:                   virtual void run(void) = 0;     void *taskHandle; }; 

Любой объект, который хочет быть задачей (активным) должен наследовать этот интерфейс и реализовывать метод run(). Указатель на этот объект передается в функцию run() обертки в качестве параметра.
На бумаге это выглядело это так:
image
Задачу реализации этой картины я поручил одаренному молодому специалисту, который успешно справился с ней и через пару дней выдал вот такой пустой рабочий проект в IAR 6.50
Пустой проект с оберткой FreeRTOS в IAR 6.50

Разработка: Общая архитектура

Пока молодой специалист делал обертку, я прикидывал архитектуру ПО. Для себя я выделил 3 пакета:
AHardware — пакет содержащий классы для работы и управления аппаратурой (Светодиоды, Индикатор, АЦП и так далее)
Application — пакет содержащий классы прикладного уровня и по задумке ничего не знающий про железо, поэтому может быть портрирован на любой микроконтроллер без изменения, ну при условии, что обертка разработана не человеком снежинкой. А в данном случае это не так:)
image
FreeRTOS — пакет с портированной операционкой и оберткой для неё.
Немного порисовав получилась следующая картина:
image

Разработка: Моргание светодиодом

Как обычно разработка начинается с самого интересного и сложного 🙂 — реализации требования SR7: Светодиод 3 должен моргать раз в 1 секунду, показалась мне именно той задачей, освоив которую я смогу двигаться вперед.
Первым делом нужно было разобраться с портами микроконтроллера и настройкой частоты. И надо сказать, что за прошедшие 8 лет с моего последнего микроконтроллерного проекта, многе изменилось и это показалось совсем не изи. Пришлось очень внимательно прочитать датащит по этим разделам и в конце концов понять, что все довольно тривиально, просто настроек в разы больше (чем в PIC 16).
В итоге вся настройка портов была выкинута в __low_level_init(). И вообще всю настройку железа, которая не будет по ходу работы программы меняться я поместил в эту функцию. Она вызывается перед main(), до инициализации всех переменных и исполнения конструкторов глобальных экземпляров классов.

Настройка портов для светодидов

   //Настройка портов ввода-вывода    //PE.10, PE.11 - светодиоды stat3 и stat4    //PA.4, PA.5 - светодиоды stat1 и stat 2    //Настраиваем порты PE.10, PE.11, PA.4, PA.5 на выход, cтр.174 CD00240194.pdf    GPIOE->MODER |=  GPIO_MODER_MODER10_0;    GPIOE->MODER |=  GPIO_MODER_MODER11_0;    GPIOA->MODER |=  GPIO_MODER_MODER4_0;    GPIOA->MODER |=  GPIO_MODER_MODER5_0; 

Ну что же железо настроено и я опять сел за рисование, на этот раз — класса управления светодидами. Через пол часа получилось вот это:
image
И тут же реализация сего чуда:

LedsDriver.h

#include "types.h"            //Стандартные типы проекта tU8 #define LEDS_NUMBER    4 class cLedsDriver {   public:     explicit cLedsDriver(void);     void ledOn(const tU8 led);     void ledOff(const tU8 led);     void ledToggle(const tU8 led);   private:     static tPort ledsPort[LEDS_NUMBER];     static const tU16 ledsPin[LEDS_NUMBER]; }; 

ledsdriver.cpp

#include "ledsdriver.h"       // Определение класса и тип tLeds #include <stm32l1xx.h>        //Регистры STM32 #include "susuassert.h"       // for ASSERT #include "types.h"            // для типов tPort, tU16, tU8 #include "bitutil.h"          // для макросов работы с битами  #define LED1_PIN        GPIO_OTYPER_ODR_4 #define LED1_PORT	GPIOA #define LED2_PIN        GPIO_OTYPER_ODR_5 #define LED2_PORT       GPIOA #define LED3_PIN        GPIO_OTYPER_ODR_10 #define LED3_PORT       GPIOE #define LED4_PIN        GPIO_OTYPER_ODR_11 #define LED4_PORT       GPIOE tPort cLedsDriver::ledsPort[LEDS_NUMBER] = {LED1_PORT, LED2_PORT, LED3_PORT, LED4_PORT}; const tU16 cLedsDriver::ledsPin[LEDS_NUMBER] = {LED1_PIN, LED2_PIN, LED3_PIN, LED4_PIN}; /******************************************************************************* * Function:  constructor * Description:  ******************************************************************************/ cLedsDriver::cLedsDriver(void)  { }  /******************************************************************************* * Function:  ledOn * Description: Зажигает выбранный светодид ******************************************************************************/ void cLedsDriver::ledOn(const tU8 led) {   ASSERT(led < LEDS_NUMBER);    SETBIT(this->ledsPort[led]->ODR, this->ledsPin[led]);    }  /******************************************************************************* * Function:  ledOff * Description: Гасит выбранный светодид ******************************************************************************/ void cLedsDriver::ledOff(const tU8 led) {   ASSERT(led < LEDS_NUMBER);    CLRBIT(this->ledsPort[led]->ODR, this->ledsPin[led]); } /******************************************************************************* * Function:  ledToggle * Description: Меняет состояние выбранного светодиода ******************************************************************************/ void cLedsDriver::ledToggle(const tU8 led) {   ASSERT(led < LEDS_NUMBER);    TOGGLEBIT(this->ledsPort[led]->ODR, this->ledsPin[led]); } 

Теперь снова нужно немного порисовать, чтобы сделать класс управления логикой работы светодиодов — класса cLedsDirector. Это будет активный класс, т.е. его функция run(), будет запускаться в задаче. Как я уже писал выше, все мои активные классы должны наследовать интерфейс iActiveObject. Поэтому рисунок выглядит тоже не сложно.
image
Опять же реализация тоже простая и по силам человеке снежинке, вроде меня:

ledsdirector.h

#include "ledsdriver.h"    //для cLedsDriver #include "iactiveobject.h"  //для iActiveObject typedef enum  {   LD_led1 = 0,   LD_led2 = 1,   LD_led3 = 2,   LD_led4 = 3,   LD_none = 4 } tLeds; class cLedsDirector: public iActiveObject {   public:     explicit cLedsDirector(void);     void run(void);         private:     cLedsDriver* pLedsDriver; }; 

ledsdirector.cpp

#include "ledsdirector.h"      // Определение класса #include "frtoswrapper.h"   // для oRTOS #include "types.h"             // lля типов  #define LED_DELAY (tU32)500/portTICK_PERIOD_MS /******************************************************************************* * Function:  constructor * Description: Создает объект класса cLedsDriver ******************************************************************************/ cLedsDirector::cLedsDirector(void)   {   this->pLedsDriver =  new cLedsDriver();    } /******************************************************************************* * Function:  runTask * Description: Задача управления ледами. led3 просто моргает ******************************************************************************/ void cLedsDirector::run(void) {   for(;;)   {     oRTOS.taskDelay(LED_DELAY);     this->pLedsDriver->ledToggle(LD_led3);       } } 

Ну что же все написано, осталось самая малость — создать экземпляр класса cLedsDirector и задачу в main() и проверить как все это работает.

функция main()

#define LEDSDIRECTOR_STACK_SIZE configMINIMAL_STACK_SIZE #define LEDSDIRECTOR_PRIORITY (tU32)2 // Не охота было заморачиваться с синглтоном, сделал oRTOS глобальным объектом // можно было конечно сделать сRTOS статическим, но че-то тоже заморочек много // зато просто, все равно всем нужен  cRTOS oRTOS; .... void main(void) {     cLedsDirector *pLedsDirector = new cLedsDirector();   oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds");    oRTOS.startScheduler(); }  

Запускаем на плате, и о чудо — работает с первого раза. Значит все сделал правильно. Есть реализация первого требования. Будем идти дальше. А пока сохраним проект
Проект морганием светодиодом в IAR 6.50

Разработка: Кнопки

Светодиоды работают, теперь можно реализовать требования SR6: При нажатии на кнопку 2 светодиод 2 должен менять свое состояние и SR4: При нажатии на кнопку 1 светодиод 1 должен менять свое состояние.
Кнопки я решил делать без прерываний, да и вообще прерывания в этом проекте использовать не буду, хотя ничто не запрещает использовать прерывания, но просто я так решил.
Согласно процедуре рисуем класс cButtonsDriver.
image
Тут надо сказать одну вещь, что с первого раза у меня кнопки не заработали, точнее заработала только одна. Вторая не работала. Разобравшись, я понял, что одна кнопка подтянута к нулю, а вторая к единице. Поэтому нажатие определяется у них по разному. Для определения нажатия я и ввел дополнительный атрибут -buttonsTrigger (которого изначально в архитектуре не было). Он показывает по какому значению считается нажата кнопка по 0 или по 1. И вот после этого, все стало работать как часы.
Реализация очень простая.

buttonsdriver.h

#include <stm32l1xx.h>        //Регистры STM32 #include "types.h"            //Стандартные типы проекта для tU16 и tU8 #define BUTTONS_NUMBER    2 typedef enum {   BS_buttonNotPressed = 0,   BS_buttonPressed = 1 } tButtonState; class cButtonsDriver {   public:     explicit  cButtonsDriver();     tButtonState getButtonState(const tU8 button);   private:     static tPort buttonsPort[BUTTONS_NUMBER];     static const tU16 buttonsPin[BUTTONS_NUMBER];     static const tBoolean buttonsTrigger[BUTTONS_NUMBER];  }; 

buttonsdriver.cpp

#include "buttonsdriver.h"    // Определение класса и тип tLeds #include <stm32l1xx.h>        //Регистры STM32 #include "susuassert.h"       //for ASSERT #include "types.h"            //для типов tPort, tU16, tU8 #define BUTTON1_PIN         GPIO_OTYPER_IDR_13 #define BUTTON1_PORT	    GPIOC #define BUTTON2_PIN         GPIO_OTYPER_IDR_0 #define BUTTON2_PORT        GPIOA tPort cButtonsDriver::buttonsPort[BUTTONS_NUMBER] = {BUTTON1_PORT, BUTTON2_PORT}; const tU16 cButtonsDriver::buttonsPin[BUTTONS_NUMBER] = {BUTTON1_PIN, BUTTON2_PIN}; // первая кнопка нажата, когда во входном регистре 0, вторая когда 1 const tBoolean cButtonsDriver::buttonsTrigger[BUTTONS_NUMBER]  =  {FALSE, TRUE};  /******************************************************************************* * Function:  constructor * Description:  ******************************************************************************/ cButtonsDriver::cButtonsDriver() { } /******************************************************************************* * Function:  getButtonState * Description: Возвращает состояние кнопки, нажата или нет ******************************************************************************/ tButtonState cButtonsDriver::getButtonState(const tU8 button) {   tButtonState eState = BS_buttonNotPressed;     ASSERT(button < BUTTONS_NUMBER);    //У нас кнопки работают по разному, одна подтянута к 1, вторая к 0,   //Поэтому тут танцы с бубном, вначале смотрим состояние копки, а потом   //чтобы не городить много ифов и кейзов просто через массив состояний кнопки   //с помощью исключаещего или определяем её нажати.     tBoolean isLogicalZero = !(this->buttonsPort[button]->IDR &                               this->buttonsPin[button]);   if(isLogicalZero ^ this->buttonsTrigger[button])   {     eState = BS_buttonPressed;   }     return eState;    } 

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

Нажатие на кнопку должно каким-то образом оповещать те задачи, которым нужно об этом знать. Можно сделать это несколькими способами: Через события, либо использовать очередь, либо воспользоваться последним достоянием freeRTOS нотификацией (Notify).
Проанализируем требования. По нажатию кнопки у нас должно осуществляться две вещи: Во-первых, менять свои состояния светодиоды 1 и 2, а во-вторых, на индикаторе менятся режим вывода и экраны. Проблема заключается в том, что очереди и события могут быть приняты только одной задачей, после чего вторая об этом не узнает, и тогда архитектура будет усложнена. Придется переадресовывать события, скажем что задача Светодиодов должна будет переадресовывать событие или очередь от кнопок задаче Индикатора. Что-то в этом меня не устроило, и я решил сделать нотификацию конктретной задаче. Т.е наша задача опроса кнопок после определения нажатия будет нотифицировать только те задачи, которым эти события нужны.
Для этого и был заведен массив указателей pTaskToNotify на задачи, которые необходимо оповещать.
Теперь реализация:

buttonscontroller.h

#include "types.h"            //Стандартные типы проекта tU32 #include "iactiveobject.h"    //для iActiveObject #include "buttonsdriver.h"    //для cButtonsDriver #include "frtosWrapper.h"     // для tTaskHandle typedef enum  {   BT_button1 = 0,   BT_button2 = 1,   BT_none = 2 } tButtons; class cButtonsController: public iActiveObject {   public:     explicit  cButtonsController(const tTaskHandle *pTaskToNotify,                                   const tU32 countOfNotifiedTask);     tButtons  getPressedButton(void) const { return pressedButton; };     void run(void);   private:     cButtonsDriver* pButtonsDriver;     tButtons getButton(void);     tButtons pressedButton;     const tTaskHandle *pTaskToNotify;     tU32 countOfNotifiedTask;     }; 

buttonscontroller.cpp

#include "buttonscontroller.h" // Определение класса #include <stm32l1xx.h>         //Регистры STM32 #include "susuassert.h"       // для ASSERT #include "types.h"            // для типов tPort, tU16, tU8 #include "bitutil.h"          // для макросов работы с битами   #define BUTTON_TASK_DELAY   (tU32) 50/portTICK_PERIOD_MS #define NEXT_PRESS_DELAY    (tU32) 500/portTICK_PERIOD_MS  /******************************************************************************* * Function:  constructor * Description: Инициализируем список задач для нотификации и количество задач *              для нотификации. А также создаем драйвер кнопок. ******************************************************************************/ cButtonsController::cButtonsController(const tTaskHandle *pTaskToNotify,                                         const tU32 countOfNotifiedTask) {   ASSERT(pTaskToNotify != NULL);     this->pButtonsDriver =  new cButtonsDriver();   this->pTaskToNotify = pTaskToNotify;   this->countOfNotifiedTask = countOfNotifiedTask;   } /******************************************************************************* * Function:  run * Description: Определеяет нажатие кнопок и посылает нотификацию нужным  *              нужным задачам ******************************************************************************/ void cButtonsController::run(void) {    tRtosStatus eStatus = RS_fail;    tButtons eButtonPreviousState = BT_none;    tButtons  eButtonCurrentState = BT_none;    const tTaskHandle *pTaskHandle;    tU32 i = 0;    for(;;)    {      eButtonPreviousState = this->getButton();      if (eButtonPreviousState != BT_none)      {        //еще раз проверяем для антибребезга        oRTOS.taskDelay(BUTTON_TASK_DELAY);        eButtonCurrentState = this->getButton();        if (eButtonPreviousState == eButtonCurrentState)        {          pTaskHandle = this->pTaskToNotify;          i = 0;          //пробегаем по списку задач, которые нужно оповестить и оповещаем их          while ((pTaskHandle != NULL) && (i != countOfNotifiedTask))          {            eStatus = oRTOS.taskNotify(*(pTaskHandle), (tU32)eButtonCurrentState,                                        eSetValueWithOverwrite);            if(eStatus == RS_fail)             {              ;//Обработка ошибки            }                       pTaskHandle++;            i++;          }          //следующее нажатие будет определяться только после задержки  в 0.5 сек          oRTOS.taskDelay(NEXT_PRESS_DELAY);        }      }      oRTOS.taskDelay(BUTTON_TASK_DELAY);    } } /******************************************************************************* * Function:  getPressedButton * Description: Определеяет какая из конопок нажата ******************************************************************************/ tButtons cButtonsController::getButton(void) {   tButtons eButton =  BT_none;   if (BS_buttonPressed == this->pButtonsDriver->getButtonState(BT_button1))   {     eButton = BT_button1;       }   else if (BS_buttonPressed == this->pButtonsDriver->getButtonState(BT_button2))   {     eButton = BT_button2;       }   this->pressedButton = eButton;   return eButton; } 

Теперь в main нужно создать список задач для оповещения и новую задачу для опроса кнопок:

main.cpp

#include <stm32l1xx.h>          // Регистры STM2 #include "ledsdirector.h"       // Для класса cLedsDirector #include "buttonscontroller.h"  // Для класса cButtonsController #include "types.h"              // Для типов проекта #include "frtoswrapper.h"       // для cRtos #define LEDS_TASK_HANDLE_INDEX          0 #define BUTTON_TASKS_NOTYFIED_NUM       1 #define LEDSDIRECTOR_STACK_SIZE configMINIMAL_STACK_SIZE #define LEDSDIRECTOR_PRIORITY (tU32)2 #define BUTTONSCONTROLLER_STACK_SIZE 256//configMINIMAL_STACK_SIZE #define BUTTONSCONTROLLER_PRIORITY (tU32)3 // Не охота было заморачиваться с синглтоном, сделал oRTOS глобальным объектом // можно было конечно сделать сRTOS статическим, но че-то тоже заморочек много // зато просто, все равно всем нужен :) cRTOS oRTOS; .. void main( void ) {     //задача ButtonControllera должна оповещать другие задачи о нажатии   //на кнопку, и передавать её значение. Для этого заводим массив указателей на    //задачи, которые надо оповещать   static tTaskHandle tasksToNotifyFromButton[BUTTON_TASKS_NOTYFIED_NUM];   cLedsDirector *pLedsDirector = new cLedsDirector();   oRTOS.taskCreate(pLedsDirector, LEDSDIRECTOR_STACK_SIZE, LEDSDIRECTOR_PRIORITY, "Leds");    tasksToNotifyFromButton[LEDS_TASK_HANDLE_INDEX] = pLedsDirector->taskHandle;   cButtonsController *pButtonsController =  new cButtonsController(tasksToNotifyFromButton, BUTTON_TASKS_NOTYFIED_NUM);   oRTOS.taskCreate(pButtonsController, BUTTONSCONTROLLER_STACK_SIZE, BUTTONSCONTROLLER_PRIORITY, "Buttons");      oRTOS.startScheduler(); } 

Запускаем, проверяем и все работает — да просто удивительно 🙂 Вот что значит вначале порисовать.
Как обычно сохраняем проект:
Проект Кнопки и светодиоды для IAR 6.50
А проект стал выглдятеть вот так:
image

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

ссылка на оригинал статьи http://habrahabr.ru/post/261807/