Как стать автором
Обновить

DevCore: программная часть проекта DevBoy

Время на прочтение35 мин
Количество просмотров7K
Привет, друзья!

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



Кому интересно — прошу под кат.

Как было сказано ранее, проект базируется на микроконтроллере STM32F415RG от STMicroelectronics на ядре ARM Cortex-M4. Для разработки под данные микроконтроллеры существует несколько различных IDE, однако для открытого проекта нужна как минимум бесплатная IDE, а лучше еще и Open Source. Кроме того, IDE должна еще поддерживаться в STM32CubeMX. На тот момент, когда я начал работать над этим проектом была только одна IDE удовлетворяющая всем этим требованиям — System Workbench for STM32.

На данный момент существует еще Atollic TrueStudio, ставшая бесплатной после того как STMicroelectronics их купила.

Следующая используемая программа — это STM32CubeMX. Данная программа представляет собой утилиту для конфигурирования периферии микроконтроллера с помощью графического интерфейса.

Результатом является код который включает в себя Hardware Abstraction Layer(HAL). Многие программисты не очень любят это "творение", оно не лишено багов, но, тем не менее, оно значительно упрощает разработку и улучшает переносимость программ между разными микроконтроллерами от STMicroelectronics.

Кроме того, при конфигурации можно задать использование некоторого стороннего открытого ПО такого как FreeRTOS, FatFS и некоторых других.

Описание используемого ПО закончили, теперь переходим к самому интересному — к DevCore. Название происходит от "Ядро Разработки" пойдем по порядку.

В первую очередь это С++ RTOS Wrapper(FreeRTOS в данном случае). Враппер нужен по двум причинам:

  • Гораздо приятнее создать объект, а потом звать к примеру mutex.Take(), чем создавать хэндл, звать функцию создания, а потом передавать этот хэндл во все функции работы с мьютексами
  • В случае необходимости замены RTOS достаточно заменить враппер, а не все вызовы функций RTOS из кода

Приводить код враппера тут не имеет смысла, кому интересно — смотрим на GitHub, а мы идем дальше.

Следующая часть — Application Framework. Это базовый класс для всех задач. Поскольку это всего два относительно не больших файла есть смысл привести их полностью:

Header
//******************************************************************************
//  @file AppTask.h
//  @author Nicolai Shlapunov
//
//  @details DevCore: Application Task Base Class, header
//
//  @section LICENSE
//
//   Software License Agreement (Modified BSD License)
//
//   Copyright (c) 2016, Devtronic & Nicolai Shlapunov
//   All rights reserved.
//
//   Redistribution and use in source and binary forms, with or without
//   modification, are permitted provided that the following conditions are met:
//
//   1. Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//   2. Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//   3. Neither the name of the Devtronic nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//   4. Redistribution and use of this software other than as permitted under
//      this license is void and will automatically terminate your rights under
//      this license.
//
//   THIS SOFTWARE IS PROVIDED BY DEVTRONIC ''AS IS'' AND ANY EXPRESS OR IMPLIED
//   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
//   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//   IN NO EVENT SHALL DEVTRONIC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
//   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//  @section SUPPORT
//
//   Devtronic invests time and resources providing this open source code,
//   please support Devtronic and open-source hardware/software by
//   donations and/or purchasing products from Devtronic.
//
//******************************************************************************

#ifndef AppTask_h
#define AppTask_h

// *****************************************************************************
// ***   Includes   ************************************************************
// *****************************************************************************
#include "DevCfg.h"

// *****************************************************************************
// * AppTask class. This class is wrapper for call C++ function from class. ****
// *****************************************************************************
class AppTask
{
  public:
    // *************************************************************************
    // ***   Init Task   *******************************************************
    // *************************************************************************
    virtual void InitTask(void) {CreateTask();}

  protected:
    // *************************************************************************
    // ***   Constructor   *****************************************************
    // *************************************************************************
    AppTask(uint16_t stk_size, uint8_t task_prio, const char name[],
            uint16_t queue_len = 0U, uint16_t queue_msg_size = 0U,
            void* task_msg_p = nullptr, uint32_t task_interval_ms = 0U) :
      ctrl_queue((queue_len + 2U), sizeof(CtrlQueueMsg)),
      task_queue(queue_len, queue_msg_size), task_msg_ptr(task_msg_p),
      timer(task_interval_ms, RtosTimer::REPEATING, TimerCallback, (void*)this),
      stack_size(stk_size), task_priority(task_prio), task_name(name) {};

    // *************************************************************************
    // ***   Virtual destructor - prevent warning   ****************************
    // *************************************************************************
    virtual ~AppTask() {};

    // *************************************************************************
    // ***   Create task function   ********************************************
    // *************************************************************************
    // * This function creates new task in FreeRTOS, provide pointer to function
    // * and pointer to class as parameter. When TaskFunctionCallback() called
    // * from FreeRTOS, it use pointer to class from parameter to call virtual
    // * functions.
    void CreateTask();

    // *************************************************************************
    // ***   Setup function   **************************************************
    // *************************************************************************
    // * * virtual function - some tasks may not have Setup() actions
    virtual Result Setup() {return Result::RESULT_OK;}

    // *************************************************************************
    // ***   IntervalTimerExpired function   ***********************************
    // *************************************************************************
    // * Empty virtual function - some tasks may not have TimerExpired() actions
    virtual Result TimerExpired() {return Result::RESULT_OK;}

    // *************************************************************************
    // ***   ProcessMessage function   *****************************************
    // *************************************************************************
    // * Empty virtual function - some tasks may not have ProcessMessage() actions
    virtual Result ProcessMessage() {return Result::RESULT_OK;}

    // *************************************************************************
    // ***   Loop function   ***************************************************
    // *************************************************************************
    // * Empty virtual function - some tasks may not have Loop() actions
    virtual Result Loop() {return Result::RESULT_OK;}

    // *************************************************************************
    // ***   SendTaskMessage function   ****************************************
    // *************************************************************************
    Result SendTaskMessage(const void* task_msg, bool is_priority = false);

  private:
    // Task control queue message types
    enum CtrlQueueMsgType
    {
       CTRL_TIMER_MSG,
       CTRL_TASK_QUEUE_MSG
    };
    // Task control queue message struct
    struct CtrlQueueMsg
    {
      CtrlQueueMsgType type;
    };
    // Task control queue
    RtosQueue ctrl_queue;

    // Task queue
    RtosQueue task_queue;
    // Pointer to receive message buffer
    void* task_msg_ptr;

    // Timer object
    RtosTimer timer;

    // Task stack size
    uint16_t stack_size;
    // Task priority
    uint8_t task_priority;
    // Pointer to the task name
    const char* task_name;

    // *************************************************************************
    // ***   IntLoop function   ************************************************
    // *************************************************************************
    Result IntLoop();

    // *************************************************************************
    // ***   TaskFunctionCallback   ********************************************
    // *************************************************************************
    static void TaskFunctionCallback(void* ptr);

    // *************************************************************************
    // ***   IntervalTimerCallback function   **********************************
    // *************************************************************************
    static void TimerCallback(void* ptr);

    // *************************************************************************
    // ***   SendControlMessage function   *************************************
    // *************************************************************************
    Result SendControlMessage(const CtrlQueueMsg& ctrl_msg, bool is_priority = false);

    // *************************************************************************
    // ***   Change counter   **************************************************
    // *************************************************************************
    static void ChangeCnt(bool is_up);

    // *************************************************************************
    // ***   Private constructor and assign operator - prevent copying   *******
    // *************************************************************************
    AppTask();
    AppTask(const AppTask&);
    AppTask& operator=(const AppTask&);
};

#endif
Code
//******************************************************************************
//  @file AppTask.cpp
//  @author Nicolai Shlapunov
//
//  @details DevCore: Application Task Base Class, implementation
//
//  @copyright Copyright (c) 2016, Devtronic & Nicolai Shlapunov
//             All rights reserved.
//
//  @section SUPPORT
//
//   Devtronic invests time and resources providing this open source code,
//   please support Devtronic and open-source hardware/software by
//   donations and/or purchasing products from Devtronic.
//
//******************************************************************************

// *****************************************************************************
// ***   Includes   ************************************************************
// *****************************************************************************
#include "AppTask.h"
#include "RtosMutex.h"

// *****************************************************************************
// ***   Static variables   ****************************************************
// *****************************************************************************
static RtosMutex startup_mutex;
static uint32_t startup_cnt = 0U;

// *****************************************************************************
// ***   Create task function   ************************************************
// *****************************************************************************
void AppTask::CreateTask()
{
  Result result = Result::RESULT_OK;

  // If interval timer period isn't zero or task queue present
  if((timer.GetTimerPeriod() != 0U) || (task_queue.GetQueueLen() != 0U))
  {
    // Set Control Queue name
    ctrl_queue.SetName(task_name, "Ctrl");
    // Create control queue
    result = ctrl_queue.Create();
  }
  // If task queue present
  if(task_queue.GetQueueLen() != 0U)
  {
    // Set Task Queue name
    task_queue.SetName(task_name, "Task");
    // Create task queue
    result |= task_queue.Create();
  }
  // If interval timer period isn't zero
  if(timer.GetTimerPeriod() != 0U)
  {
    // Create timer
    result |= timer.Create();
  }
  // Create task: function - TaskFunctionCallback(), parameter - pointer to "this"
  result |= Rtos::TaskCreate(TaskFunctionCallback, task_name, stack_size, this, task_priority);

  // Check result
  if(result.IsBad())
  {
    // TODO: implement error handling
    Break();
  }
}

// *****************************************************************************
// ***   SendTaskMessage function   ********************************************
// *****************************************************************************
Result AppTask::SendTaskMessage(const void* task_msg, bool is_priority)
{
  Result result = Result::RESULT_OK;

  // Send task message to front or back of task queue
  if(is_priority == true)
  {
    result = task_queue.SendToFront(task_msg);
  }
  else
  {
    result = task_queue.SendToBack(task_msg);
  }

  // If successful - send message to the control queue
  if(result.IsGood())
  {
    CtrlQueueMsg ctrl_msg;
    ctrl_msg.type = CTRL_TASK_QUEUE_MSG;
    result = SendControlMessage(ctrl_msg, is_priority);
  }

  return result;
}

// *****************************************************************************
// ***   IntLoop function   ****************************************************
// *****************************************************************************
Result AppTask::IntLoop()
{
  Result result = Result::RESULT_OK;

  while(result.IsGood())
  {
    // Buffer for control message
    CtrlQueueMsg ctrl_msg;
    // Read on the control queue
    result = ctrl_queue.Receive(&ctrl_msg, timer.GetTimerPeriod() * 2U);
    // If successful
    if(result.IsGood())
    {
      // Check message type
      switch(ctrl_msg.type)
      {
        case CTRL_TIMER_MSG:
          result = TimerExpired();
          break;

         case CTRL_TASK_QUEUE_MSG:
         {
           // Non blocking read from the task queue
           result = task_queue.Receive(task_msg_ptr, 0U);
           // If successful
           if(result.IsGood())
           {
             // Process it!
             result = ProcessMessage();
           }
           break;
         }

         default:
           result = Result::ERR_INVALID_ITEM;
           break;
      }
    }
  }

  return result;
}

// *****************************************************************************
// ***   TaskFunctionCallback   ************************************************
// *****************************************************************************
void AppTask::TaskFunctionCallback(void* ptr)
{
  Result result = Result::ERR_NULL_PTR;

  if(ptr != nullptr)
  {
    // Set good result
    result = Result::RESULT_OK;
    // Get reference to the task object
    AppTask& app_task = *(static_cast<AppTask*>(ptr));

    // Increment counter before call Setup()
    ChangeCnt(true);
    // Call virtual Setup() function from AppTask class
    app_task.Setup();
    // Decrement counter after call Setup()
    ChangeCnt(false);
    // Pause for give other tasks run Setup()
    RtosTick::DelayTicks(1U);
    // Pause while other tasks run Setup() before executing any Loop()
    while(startup_cnt) RtosTick::DelayTicks(1U);

    // If no timer or queue - just call Loop() function
    if((app_task.timer.GetTimerPeriod() == 0U) && (app_task.task_queue.GetQueueLen() == 0U))
    {
      // Call virtual Loop() function from AppTask class
      while(app_task.Loop() == Result::RESULT_OK);
    }
    else
    {
      // Start task timer if needed
      if(app_task.timer.GetTimerPeriod() != 0U)
      {
        result = app_task.timer.Start();
      }
      // Check result
      if(result.IsGood())
      {
        // Call internal AppTask function
        result = app_task.IntLoop();
      }
      // Stop task timer if needed
      if(app_task.timer.GetTimerPeriod() != 0U)
      {
        result |= app_task.timer.Stop();
      }
    }
  }

  // Check result
  if(result.IsBad())
  {
    // TODO: implement error handling
    Break();
  }

  // Delete task after exit
  Rtos::TaskDelete();
}

// *****************************************************************************
// ***   TimerCallback function   **********************************************
// *****************************************************************************
void AppTask::TimerCallback(void* ptr)
{
  Result result = Result::ERR_NULL_PTR;

  if(ptr != nullptr)
  {
    // Get reference to the task object
    AppTask& task = *((AppTask*)ptr);

    // Create control timer message
    CtrlQueueMsg timer_msg;
    timer_msg.type = CTRL_TIMER_MSG;

    // Send message to the control queue
    result = task.SendControlMessage(timer_msg);
  }

  // Check result
  if(result.IsBad())
  {
    // TODO: implement error handling
    Break();
  }
}

// *****************************************************************************
// ***   SendControlMessage function   *****************************************
// *****************************************************************************
Result AppTask::SendControlMessage(const CtrlQueueMsg& ctrl_msg, bool is_priority)
{
  Result result;

  if(is_priority == true)
  {
    result = ctrl_queue.SendToFront(&ctrl_msg);
  }
  else
  {
    result = ctrl_queue.SendToBack(&ctrl_msg);
  }

  return result;
}

// *****************************************************************************
// ***   Change counter   ******************************************************
// *****************************************************************************
void AppTask::ChangeCnt(bool is_up)
{
  // Take semaphore before change counter
  startup_mutex.Lock();
  // Check direction
  if(is_up == true)
  {
    // Increment counter
    startup_cnt++;
  }
  else
  {
    // Decrement counter
    startup_cnt--;
  }
  // Give semaphore after changes
  startup_mutex.Release();
}

Наследуемые классы могут переопределить 4 виртуальные функции:

  • Setup() — функция вызываемая перед запуском задачи. Гарантируется завершение кода во всех данных функциях всех задач перед началом исполнения основных циклов.
  • Loop() — основной цикл задачи, где задача сама организует что она хочет. Не может использоваться совместно с двумя следующими функциями.
  • TimerExpired() — функция вызываемая периодически с заданным интервалом. Удобно для реализации опроса датчика например.
  • ProcessMessage() — функция обработки сообщения от других задач.

Две первые функции реализуют "Arduino-Style" для задач.

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

Данный подход был подсмотрен когда я разрабатывал ПО для медицинского оборудования. Микроконтроллер имеет одну «сторожевую собаку»(watchdog) и в случае множества задач нужно отслеживать их всех. Для этого была отдельная задача которая обслуживала watchdog и получала сообщения от других задач высылаемых из функции TimerExpired(). Если в течении периода таймера задачи * n не приходили сообщения — задача умерла, тушим свет принимаем действия по отключению всех железок воздействующих на пациента.

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

// *****************************************************************************
// ***   Get Instance   ********************************************************
// *****************************************************************************
Application& Application::GetInstance(void)
{
   static Application application;
   return application;
}

Так же в состав входят задачи для вывода звука, для модулей ввода и для обслуживания экрана.

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

Header
//******************************************************************************
//  @file SoundDrv.h
//  @author Nicolai Shlapunov
//
//  @details DevCore: Sound Driver Class, header
//
//  @section LICENSE
//
//   Software License Agreement (Modified BSD License)
//
//   Copyright (c) 2016, Devtronic & Nicolai Shlapunov
//   All rights reserved.
//
//   Redistribution and use in source and binary forms, with or without
//   modification, are permitted provided that the following conditions are met:
//
//   1. Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//   2. Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//   3. Neither the name of the Devtronic nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//   4. Redistribution and use of this software other than as permitted under
//      this license is void and will automatically terminate your rights under
//      this license.
//
//   THIS SOFTWARE IS PROVIDED BY DEVTRONIC ''AS IS'' AND ANY EXPRESS OR IMPLIED
//   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
//   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//   IN NO EVENT SHALL DEVTRONIC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
//   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//  @section SUPPORT
//
//   Devtronic invests time and resources providing this open source code,
//   please support Devtronic and open-source hardware/software by
//   donations and/or purchasing products from Devtronic.
//
//******************************************************************************

#ifndef SoundDrv_h
#define SoundDrv_h

// *****************************************************************************
// ***   Includes   ************************************************************
// *****************************************************************************
#include "DevCfg.h"
#include "AppTask.h"
#include "RtosMutex.h"
#include "RtosSemaphore.h"

// *****************************************************************************
// ***   Sound Driver Class. This class implement work with sound.   ***********
// *****************************************************************************
class SoundDrv : public AppTask
{
  public:
    // *************************************************************************
    // ***   Get Instance   ****************************************************
    // *************************************************************************
    // * This class is singleton. For use this class you must call GetInstance()
    // * to receive reference to Sound Driver class
    static SoundDrv& GetInstance(void);

    // *************************************************************************
    // ***   Init Sound Driver Task   ******************************************
    // *************************************************************************
    virtual void InitTask(TIM_HandleTypeDef *htm);

    // *************************************************************************
    // ***   Sound Driver Setup   **********************************************
    // *************************************************************************
    virtual Result Setup();

    // *************************************************************************
    // ***   Sound Driver Loop   ***********************************************
    // *************************************************************************
    virtual Result Loop();

    // *************************************************************************
    // ***   Beep function   ***************************************************
    // *************************************************************************
    void Beep(uint16_t freq, uint16_t del, bool pause_after_play = false);

    // *************************************************************************
    // ***   Play sound function   *********************************************
    // *************************************************************************
    void PlaySound(const uint16_t* melody, uint16_t size, uint16_t temp_ms = 100U, bool rep = false);

    // *************************************************************************
    // ***   Stop sound function   *********************************************
    // *************************************************************************
    void StopSound(void);

    // *************************************************************************
    // ***   Mute sound function   *********************************************
    // *************************************************************************
    void Mute(bool mute_flag);

    // *************************************************************************
    // ***   Is sound played function   ****************************************
    // *************************************************************************
    bool IsSoundPlayed(void);

  private:
    // Timer handle
    TIM_HandleTypeDef* htim = SOUND_HTIM;
    // Timer channel
    uint32_t channel = SOUND_CHANNEL;
    
    // Ticks variable
    uint32_t last_wake_ticks = 0U;

    // Pointer to table contains melody
    const uint16_t* sound_table = nullptr;
    // Size of table
    uint16_t sound_table_size = 0U;
    // Current position
    uint16_t sound_table_position = 0U;
    // Current frequency delay
    uint16_t current_delay = 0U;
    // Time for one frequency in ms
    uint32_t delay_ms = 100U;
    // Repeat flag
    bool repeat = false;

    // Mute flag
    bool mute = false;

    // Mutex to synchronize when playing melody frames
    RtosMutex melody_mutex;

    // Semaphore for start play sound
    RtosSemaphore sound_update;

    // *************************************************************************
    // ***   Process Button Input function   ***********************************
    // *************************************************************************
    void Tone(uint16_t freq);

    // *************************************************************************
    // ** Private constructor. Only GetInstance() allow to access this class. **
    // *************************************************************************
    SoundDrv() : AppTask(SOUND_DRV_TASK_STACK_SIZE, SOUND_DRV_TASK_PRIORITY,
                         "SoundDrv") {};
};

#endif


Задача обслуживания модулей воода тоже достаточно проста. Из интересных моментов автоопределение модуля: вначале с помощью АЦП измеряем напряжение, если оно в пределах от 25% до 75% от питающего напряжения — вставлен аналоговый джойстик, иначе кнопки или энкодер. Если это не джойстик, проверяем четвертую линию I/O модуля: если на ней высокий уровень — это кнопки(все кнопки подтянуты к питанию и при нажатии кнопки замыкаются на землю), если на ней низкий уровень — это энкодер(маленькая кнопка «подтянута» к земле и при нажатии замыкается на питание).

Header
//******************************************************************************
//  @file InputDrv.h
//  @author Nicolai Shlapunov
//
//  @details DevCore: Input Driver Class, header
//
//  @section LICENSE
//
//   Software License Agreement (Modified BSD License)
//
//   Copyright (c) 2016, Devtronic & Nicolai Shlapunov
//   All rights reserved.
//
//   Redistribution and use in source and binary forms, with or without
//   modification, are permitted provided that the following conditions are met:
//
//   1. Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//   2. Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//   3. Neither the name of the Devtronic nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//   4. Redistribution and use of this software other than as permitted under
//      this license is void and will automatically terminate your rights under
//      this license.
//
//   THIS SOFTWARE IS PROVIDED BY DEVTRONIC ''AS IS'' AND ANY EXPRESS OR IMPLIED
//   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
//   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//   IN NO EVENT SHALL DEVTRONIC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
//   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//  @section SUPPORT
//
//   Devtronic invests time and resources providing this open source code,
//   please support Devtronic and open-source hardware/software by
//   donations and/or purchasing products from Devtronic.
//
//******************************************************************************

#ifndef InputDrv_h
#define InputDrv_h

// *****************************************************************************
// ***   Includes   ************************************************************
// *****************************************************************************
#include "DevCfg.h"
#include "AppTask.h"

// *****************************************************************************
// * Input Driver Class. This class implement work with user input elements like 
// * buttons and encoders.
class InputDrv : public AppTask
{
  public:
    // *************************************************************************
    // ***   Enum with all buttons   *******************************************
    // *************************************************************************
    typedef enum 
    {
      EXT_LEFT,  // Left ext port
      EXT_RIGHT, // Right ext port
      EXT_MAX    // Ext port count
    } PortType;

    // *************************************************************************
    // ***   Enum with all devices types   *************************************
    // *************************************************************************
    typedef enum 
    {
      EXT_DEV_NONE, // No device
      EXT_DEV_BTN,  // Buttons(cross)
      EXT_DEV_ENC,  // Encoder
      EXT_DEV_JOY,  // Joystick
      EXT_DEV_MAX   // Device types count
    } ExtDeviceType;
    
    // *************************************************************************
    // ***   Enum with all buttons   *******************************************
    // *************************************************************************
    typedef enum 
    {
      BTN_UP,    // Up button
      BTN_LEFT,  // Left button
      BTN_DOWN,  // Down button
      BTN_RIGHT, // Right button
      BTN_MAX    // Buttons count
    } ButtonType;

    // *************************************************************************
    // ***   Enum with all encoder buttons   ***********************************
    // *************************************************************************
    typedef enum
    {
      ENC_BTN_ENT,   // Press on the knob
      ENC_BTN_BACK,  // Small button
      ENC_BTN_MAX    // Buttons count
    } EncButtonType;

    // *************************************************************************
    // ***   Get Instance   ****************************************************
    // *************************************************************************
    // * This class is singleton. For use this class you must call GetInstance()
    // * to receive reference to Input Driver class
    static InputDrv& GetInstance(void);

    // *************************************************************************
    // ***   Init Input Driver Task   ******************************************
    // *************************************************************************
    // * This function initialize Input Driver class. If htim provided, this 
    // * timer will be used instead FreeRTOS task.
    virtual void InitTask(TIM_HandleTypeDef* htm, ADC_HandleTypeDef* had);

    // *************************************************************************
    // ***   Input Driver Setup   **********************************************
    // *************************************************************************
    virtual Result Setup();

    // *************************************************************************
    // ***   Input Driver Loop   ***********************************************
    // *************************************************************************
    // * If FreeRTOS task used, this function just call ProcessInput() with 1 ms
    // * period. If FreeRTOS tick is 1 ms - this task must have highest priority 
    virtual Result Loop();

    // *************************************************************************
    // ***   Process Input function   ******************************************
    // *************************************************************************
    // * Main class function - must call periodically for process user input.
    // * If timer used, this function must be called from interrupt handler.
    void ProcessInput(void);

    // *************************************************************************
    // ***   Process Encoders Input function   *********************************
    // *************************************************************************
    void ProcessEncodersInput(void);

    // *************************************************************************
    // ***   Get device type   *************************************************
    // *************************************************************************
    ExtDeviceType GetDeviceType(PortType port);

    // *************************************************************************
    // ***   Get button state   ************************************************
    // *************************************************************************
    // Return button state: true - pressed, false - unpressed
    bool GetButtonState(PortType port, ButtonType button);
    
    // *************************************************************************
    // ***   Get button state   ************************************************
    // *************************************************************************
    // Return button state change flag: true - changed, false - not changed
    bool GetButtonState(PortType port, ButtonType button, bool& btn_state);

    // *************************************************************************
    // ***   Get encoder counts from last call   *******************************
    // *************************************************************************
    // * Return state of encoder. Class counts encoder clicks and stored inside.
    // * This function substract from current encoder counter last_enc_val and
    // * return it to user. Before return last_enc_val will be assigned to
    // * current encoder counter.
    int32_t GetEncoderState(PortType port, int32_t& last_enc_val);

    // *************************************************************************
    // ***   Get button state   ************************************************
    // *************************************************************************
    // Return button state: true - pressed, false - unpressed
    bool GetEncoderButtonState(PortType port, EncButtonType button);

    // *************************************************************************
    // ***   Get encoder button state   ****************************************
    // *************************************************************************
    // Return button state: true - pressed, false - unpressed
    bool GetEncoderButtonState(PortType port, EncButtonType button, bool& btn_state);

    // *************************************************************************
    // ***   Get joystick counts from last call   ******************************
    // *************************************************************************
    void GetJoystickState(PortType port, int32_t& x, int32_t& y);

    // *************************************************************************
    // ***   SetJoystickCalibrationConsts   ************************************
    // *************************************************************************
    // * Set calibration constants. Must be call for calibration joystick.
    void SetJoystickCalibrationConsts(PortType port, int32_t x_mid,
                                      int32_t x_kmin, int32_t x_kmax,
                                      int32_t y_mid, int32_t y_kmin,
                                      int32_t y_kmax);

    // *************************************************************************
    // ***   Get joystick button state   ***************************************
    // *************************************************************************
    // Return button state: true - pressed, false - unpressed
    bool GetJoystickButtonState(PortType port);

    // *************************************************************************
    // ***   Get joystick button state   ***************************************
    // *************************************************************************
    // Return button state: true - pressed, false - unpressed
    bool GetJoystickButtonState(PortType port, bool& btn_state);

  private:
    // How many cycles button must change state before state will be changed in
    // result returned by GetButtonState() function. For reduce debouncing
    const static uint32_t BUTTON_READ_DELAY = 4U;
    // Coefficient for calibration
    const static int32_t COEF = 100;

    // ADC max value - 12 bit
    const static int32_t ADC_MAX_VAL = 0xFFF;
    // Joystich threshold
    const static int32_t JOY_THRESHOLD = 1000;

    // Ticks variable
    uint32_t last_wake_ticks = 0U;

    // *************************************************************************
    // ***   Structure to describe button   ************************************
    // *************************************************************************
    typedef struct
    {
      bool btn_state;           // Button state returned by GetButtonState() function
      bool btn_state_tmp;       // Temporary button state for reduce debouncing
      uint8_t btn_state_cnt;    // Counter for reduce debouncing
      GPIO_TypeDef* button_port;// Button port
      uint16_t button_pin;      // Button pin
      GPIO_PinState pin_state;  // High/low on input treated as pressed
    } ButtonProfile;

    // *************************************************************************
    // ***   Structure to describe encoder   ***********************************
    // *************************************************************************
    typedef struct
    {
      // Encoder rotation
      int32_t enc_cnt;            // Encoder counter
      uint8_t enc_state;          // Current state of encder clock & data pins
      GPIO_TypeDef* enc_clk_port; // Encoder clock port
      uint16_t enc_clk_pin;       // Encoder clock pin
      GPIO_TypeDef* enc_data_port;// Encoder data port
      uint16_t enc_data_pin;      // Encoder data pin
    } EncoderProfile;

    // *************************************************************************
    // ***   Structure to describe joysticks   *********************************
    // *************************************************************************
    typedef struct
    {
      int32_t x_ch_val;     // Joystick X axis value
      uint32_t x_channel;   // Joystick X axis ADC channel
      GPIO_TypeDef* x_port; // Joystick X axis port
      uint16_t x_pin;       // Joystick X axis pin 
      int32_t bx;           // Joystick X offset
      int32_t kxmin;        // Joystick X coefficient
      int32_t kxmax;        // Joystick X coefficient
      bool x_inverted;      // Joystick X inverted flag
      int32_t y_ch_val;     // Joystick Y axis value
      uint32_t y_channel;   // Joystick Y axis ADC channel
      GPIO_TypeDef* y_port; // Joystick Y axis port
      uint16_t y_pin;       // Joystick Y axis pin
      int32_t by;           // Joystick Y offset
      int32_t kymin;        // Joystick Y coefficient
      int32_t kymax;        // Joystick Y coefficient
      bool y_inverted;      // Joystick Y inverted flag
    } JoystickProfile;

    // *************************************************************************
    // ***   Structure to describe encoders   **********************************
    // *************************************************************************
    typedef struct
    {
      EncoderProfile enc;
      ButtonProfile  btn[ENC_BTN_MAX];
    } DevEncoders;

    // *************************************************************************
    // ***   Structure to describe encoders   **********************************
    // *************************************************************************
    typedef struct
    {
      JoystickProfile joy;
      ButtonProfile   btn;
    } DevJoysticks;

    // *************************************************************************
    // ***   Structure to describe buttons   ***********************************
    // *************************************************************************
    typedef struct
    {
      ButtonProfile button[BTN_MAX];
    } DevButtons;

    // ***   Array describes types of connected devices  ***********************
    ExtDeviceType devices[EXT_MAX];

    // ***   Structures array for describe buttons inputs  *********************
    DevButtons buttons[EXT_MAX] =
    {
      // Left device
      {{{false, false, 0, EXT_L1_GPIO_Port, EXT_L1_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_L2_GPIO_Port, EXT_L2_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_L3_GPIO_Port, EXT_L3_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_L4_GPIO_Port, EXT_L4_Pin, GPIO_PIN_RESET}}},
      // Right device
      {{{false, false, 0, EXT_R1_GPIO_Port, EXT_R1_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_R2_GPIO_Port, EXT_R2_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_R3_GPIO_Port, EXT_R3_Pin, GPIO_PIN_RESET},
        {false, false, 0, EXT_R4_GPIO_Port, EXT_R4_Pin, GPIO_PIN_RESET}}}
    };

    // ***   Structures array for describe encoders inputs  ********************
    DevEncoders encoders[EXT_MAX] =
    {
      // Left device
      {{0, 0, EXT_L1_GPIO_Port, EXT_L1_Pin, EXT_L2_GPIO_Port, EXT_L2_Pin}, // Encoder
       {{false, false, 0, EXT_L3_GPIO_Port, EXT_L3_Pin, GPIO_PIN_RESET},   // Button Enter
        {false, false, 0, EXT_L4_GPIO_Port, EXT_L4_Pin, GPIO_PIN_SET}}},   // Button Back
      // Right device
      {{0, 0, EXT_R1_GPIO_Port, EXT_R1_Pin, EXT_R2_GPIO_Port, EXT_R2_Pin}, // Encoder
       {{false, false, 0, EXT_R3_GPIO_Port, EXT_R3_Pin, GPIO_PIN_RESET},   // Button Enter
        {false, false, 0, EXT_R4_GPIO_Port, EXT_R4_Pin, GPIO_PIN_SET}}}    // Button Back
    };

    // ***   Structures array for describe encoders inputs  ********************
    DevJoysticks joysticks[EXT_MAX] =
    {
      // Left device
      {{0, ADC_CHANNEL_11, EXT_L2_GPIO_Port, EXT_L2_Pin, 0, COEF, COEF, false, // Joystick
        0, ADC_CHANNEL_10, EXT_L1_GPIO_Port, EXT_L1_Pin, 0, COEF, COEF, true},
       {false, false, 0, EXT_L3_GPIO_Port, EXT_L3_Pin, GPIO_PIN_RESET}},       // Button
      // Right device
      {{0, ADC_CHANNEL_13, EXT_R2_GPIO_Port, EXT_R2_Pin, 0, COEF, COEF, false, // Joystick
        0, ADC_CHANNEL_12, EXT_R1_GPIO_Port, EXT_R1_Pin, 0, COEF, COEF, true},
       {false, false, 0, EXT_R3_GPIO_Port, EXT_R3_Pin, GPIO_PIN_RESET}}        // Button
    };
 
    // Handle to timer used for process encoders input
    TIM_HandleTypeDef* htim = nullptr;
    // Handle to timer used for process encoders input
    ADC_HandleTypeDef* hadc = nullptr;

    // *************************************************************************
    // ***   Process Button Input function   ***********************************
    // *************************************************************************
    void ProcessButtonInput(ButtonProfile& button);

    // *************************************************************************
    // ***   Process Encoder Input function   **********************************
    // *************************************************************************
    void ProcessEncoderInput(EncoderProfile& encoder);

    // *************************************************************************
    // ***   Process Joystick Input function   *********************************
    // *************************************************************************
    void ProcessJoystickInput(JoystickProfile& joysticks, PortType port);

    // *************************************************************************
    // ***   Emulate buttons using joystick function   *************************
    // *************************************************************************
    void EmulateButtonsByJoystick(PortType port);

    // *************************************************************************
    // ***   Emulate encoders using buttons function   *************************
    // *************************************************************************
    void EmulateEncodersByButtons(PortType port);

    // *************************************************************************
    // ***   Configure inputs devices types   **********************************
    // *************************************************************************
    ExtDeviceType DetectDeviceType(PortType port);

    // *************************************************************************
    // ***   Configure ADC   ***************************************************
    // *************************************************************************
    void ConfigADC(ExtDeviceType dev_left, ExtDeviceType dev_right);

    // *************************************************************************
    // ***   Configure inputs for read digital/analog data   *******************
    // *************************************************************************
    void ConfigInputIO(bool is_digital, PortType port);

    // *************************************************************************
    // ** Private constructor. Only GetInstance() allow to access this class. **
    // *************************************************************************
    InputDrv() : AppTask(INPUT_DRV_TASK_STACK_SIZE, INPUT_DRV_TASK_PRIORITY,
                         "InputDrv") {};
};

#endif

Задача обслуживания экрана — самая интересная задача. Начнем с того, что экран 320x240x16bit, итого нужно 153600 байт для фреймбуфера. Это не просто много, это просто огромно — в данном микроконтроллере всего 192к ОЗУ, а в микроконтроллерах попроще может вообще не оказаться нужного размера. Как же быть? Ответ прост: рисовать экран по частям! Но рисовать-то можно по разному…

Решение которое я применил для этой задачи просто как и все гениальное. Она имеет буфер на две экранные строки. В одну строку отрисовываем все что должно быть, отправляем её на экран через SPI в DMA режиме, а сами в это время может подготавливать другую строку.

Откуда же задача знает что должно быть в строке и как её нарисовать? А она не знает! Но у нее есть список объектов, которые знают как нарисовать себя. Каждый такой объект наследуется от класса VisObject.

Header
//******************************************************************************
//  @file VisObject.h
//  @author Nicolai Shlapunov
//
//  @details DevCore: Visual Object Base Class, header
//
//  @section LICENSE
//
//   Software License Agreement (BSD License)
//
//   Copyright (c) 2016, Devtronic & Nicolai Shlapunov
//   All rights reserved.
//
//   Redistribution and use in source and binary forms, with or without
//   modification, are permitted provided that the following conditions are met:
//   1. Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//   2. Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//   3. Neither the name of the Devtronic nor the names of its contributors
//      may be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
//   THIS SOFTWARE IS PROVIDED BY DEVTRONIC ''AS IS'' AND ANY EXPRESS OR IMPLIED
//   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
//   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
//   IN NO EVENT SHALL DEVTRONIC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
//   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
//   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
//   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//******************************************************************************

#ifndef VisObject_h
#define VisObject_h

// *****************************************************************************
// ***   Includes   ************************************************************
// *****************************************************************************
#include "DevCfg.h"

// *****************************************************************************
// * VisObject class. This class implements base Visual Objects properties.
class VisObject
{
  public:
    // *************************************************************************
    // ***   Action   **********************************************************
    // ************************************************************************* 
    typedef enum
    {
      ACT_TOUCH,   // When object touched
      ACT_UNTOUCH, // When object detouched
      ACT_MOVE,    // When object moved on object
      ACT_MOVEIN,  // When object moved in to object
      ACT_MOVEOUT, // When object moved out of object
      ACT_MAX      // Total possible actions
    } ActionType;

    // *************************************************************************
    // ***   VisObject   *******************************************************
    // ************************************************************************* 
    VisObject() {};

    // *************************************************************************
    // ***   ~VisObject   ******************************************************
    // *************************************************************************
    // * Destructor. Call DelVisObjectFromList() from DisplayDrv class for
    // * remove from list before delete and delete semaphore.
    virtual ~VisObject();

    // *************************************************************************
    // ***   LockVisObject   ***************************************************
    // *************************************************************************
    void LockVisObject();

    // *************************************************************************
    // ***   UnlockVisObject   *************************************************
    // *************************************************************************
    void UnlockVisObject();

    // *************************************************************************
    // ***   Show   ************************************************************
    // *************************************************************************
    // * Show VisObject on screen. This function call AddVisObjectToList() from
    // * DisplayDrv class. When this function calls first time, user must
    // * provide Z level. In future user can call this function without
    // * parameters - previously set Z will be used.
    virtual void Show(uint32_t z_pos = 0);

    // *************************************************************************
    // ***   Hide   ************************************************************
    // *************************************************************************
    // * Hide VisObject from screen. This function call DelVisObjectFromList()
    // * from DisplayDrv class.
    virtual void Hide(void);
    
    // *************************************************************************
    // ***   IsShow   **********************************************************
    // *************************************************************************
    // * Check status of Show Visual Object. Return true if object in DisplayDrv list.
    virtual bool IsShow(void);

    // *************************************************************************
    // ***   Move   ************************************************************
    // *************************************************************************
    // * Move object on screen. Set new x and y coordinates. If flag is set - 
    // * move is relative, not absolute.
    virtual void Move(int32_t x, int32_t y, bool is_delta = false);

    // *************************************************************************
    // ***   DrawInBufH   ******************************************************
    // *************************************************************************
    // * Draw one horizontal line of object in specified buffer.
    // * Each derived class must implement this function.
    virtual void DrawInBufH(uint16_t* buf, int32_t n, int32_t row, int32_t start_y = 0) = 0;

    // *************************************************************************
    // ***   DrawInBufW   ******************************************************
    // *************************************************************************
    // * Draw one vertical line of object in specified buffer.
    // * Each derived class must implement this function.
    virtual void DrawInBufW(uint16_t* buf, int32_t n, int32_t line, int32_t start_x = 0) = 0;

    // *************************************************************************
    // ***   Action   **********************************************************
    // *************************************************************************
    virtual void Action(ActionType action, int32_t tx, int32_t ty);

    // *************************************************************************
    // ***   Return Start X coordinate   ***************************************
    // *************************************************************************
    virtual int32_t GetStartX(void) {return x_start;};

    // *************************************************************************
    // ***   Return Start Y coordinate   ***************************************
    // *************************************************************************
    virtual int32_t GetStartY(void) {return y_start;};

    // *************************************************************************
    // ***   Return End X coordinate   *****************************************
    // *************************************************************************
    virtual int32_t GetEndX(void) {return x_end;};

    // *************************************************************************
    // ***   Return End Y coordinate   *****************************************
    // *************************************************************************
    virtual int32_t GetEndY(void) {return y_end;};

    // *************************************************************************
    // ***   Return Width of object   ******************************************
    // *************************************************************************
    virtual int32_t GetWidth(void) {return width;};

    // *************************************************************************
    // ***   Return Height of object   *****************************************
    // *************************************************************************
    virtual int32_t GetHeight(void) {return height;};

  protected:
    // *************************************************************************
    // ***   Object parameters   ***********************************************
    // *************************************************************************

    // X and Y start coordinates of object
    int16_t x_start = 0, y_start = 0;
    // X and Y end coordinates of object
    int16_t x_end = 0, y_end = 0;
    // Width and Height of object
    int16_t width = 0, height = 0;
    // Rotation of object
    int8_t rotation = 0;
    // Object active
    bool active = false;

  private:
    // *************************************************************************
    // ***   Object parameters   ***********************************************
    // *************************************************************************
    // * Only base class and DisplayDrv have access to this parameters

    // Z position of object
    uint16_t z = 0;
    // Pointer to next object. This pointer need to maker object list. Object
    // can be added only to one list.
    VisObject* p_next = nullptr;
    // Pointer to next object. This pointer need to maker object list. Object
    // can be added only to one list.
    VisObject* p_prev = nullptr;

    // DisplayDrv is friend for access to pointers and Z
    friend class DisplayDrv;
};

#endif

Задача обслуживания экрана для каждой строки проходится по списку объектов и вызывает функцию DrawInBufW() передавая ей указатель на буфер, количество точек, рисуемую линию и начальную позицию(пока не используется была идея использовать режим контроллера экрана для обновления «окна»). Фактически каждый объект рисует себя сам поверх других уже нарисованных и легко расположить объекты в требуемом порядке просто размещая их в нужную позицию списка.

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

На данный момент имеются объекты для строк, примитивов(линия, квадрат, круг), картинок и карты плиток(для создания игр).

Так же в DevCore входит еще код для некоторых элементов UI(например меню), интерфейс для драйвера I2C, драйвер I2C и библиотеки для работы с датчиком BME280 и EEPROM 24С256, но это не так интересно и пока описывать не буду — и так получилось достаточно объемно.

Полный код доступен без регистрации и СМС на GitHub: https://github.com/nickshl/devboy

P.S. Судя по всему, компания идет к Epic Fail'у. За первую неделю всего три бакера, из них доллар от какого-то "Инновационного фонда", 180 долларов от человека, наверняка узнавшего об этом проекте из статьи на Хабре (Спасибо тебе, Андрей!) и остальное от моего коллеги из соседнего кюбикла.

Не собрать деньги — это не проблема. Проблема — отсутствие интереса к проекту…
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+13
Комментарии14

Публикации

Истории

Работа

QT разработчик
7 вакансий
Программист C++
129 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн