С вами снова Николай, в прошлой статье "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. Это базовый класс для всех задач. Поскольку это всего два относительно не больших файла есть смысл привести их полностью:
//******************************************************************************
// @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
//******************************************************************************
// @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;
}
Так же в состав входят задачи для вывода звука, для модулей ввода и для обслуживания экрана.
Задача вывода звука достаточно проста — получает массив частот и длительностей и просто периодически меняет настройки таймера для генерации прямоугольных импульсов определенной частоты.
//******************************************************************************
// @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 модуля: если на ней высокий уровень — это кнопки(все кнопки подтянуты к питанию и при нажатии кнопки замыкаются на землю), если на ней низкий уровень — это энкодер(маленькая кнопка «подтянута» к земле и при нажатии замыкается на питание).
//******************************************************************************
// @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.
//******************************************************************************
// @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, но это не так интересно и пока описывать не буду — и так получилось достаточно объемно.
Полный код доступен
P.S. Судя по всему, компания идет к Epic Fail'у. За первую неделю всего три бакера, из них доллар от какого-то "Инновационного фонда", 180 долларов от человека, наверняка узнавшего об этом проекте из статьи на Хабре (Спасибо тебе, Андрей!) и остальное от моего коллеги из соседнего кюбикла.
Не собрать деньги — это не проблема. Проблема — отсутствие интереса к проекту…
Комментарии (13)
Javian
25.09.2018 09:58Возможно необходимо несколько видеообзоров «функционала». Прежде всего «комфортности» игр.
Konachan700
25.09.2018 10:14Получился, фактически, тот же дискавери, только обрезаный и в корпусе.
Позиционирование как игровой консоли — провал, ибо в 2018 году делать на такой железке игры без фреймбуфера и G2D это удел крошечной кучки олдфагов да демосцены. Остальным это не нужно и не интересно. Для управления роботом или дроном лучше всего подходит китайская копия PSP на андроиде, и на ней кстати, прекрасно идут все игры от старых приставок. Примитивный осциллограф такого же формфактора, и тоже на stm32, существует и стоит на али три копейки. Для работы с sdr не хватит проца, да и дрова на донглы написать непросто. Для чего еще можно приспособить этот проект? Не знаю. И бекеры не знают видимо, потому и нет у проекта популярности.
Sdima1357
25.09.2018 10:29Цена / функциональность заоблачная. Для тех у кого есть необходимые навыки программирования в среде stmcube она вроде ни к чему за такие деньги, есть намного более дешевые альтернативы. Для остальных слишком сложна и непривычна.
Малинка и ардуино пробились исключительно за счет цены и низкого порога вхождения. Им не было альтернатив в момент появления.
Фактически Ваш проект попадает где то между arduino и odroid-go по функциональности, но значительно дороже. При этом даже если Вы бы выставили цену порядка $40, вряд-ли бы смогли найти достаточно покупателей. Не хватает какой нибудь уникальной фишки.
sim-dev
25.09.2018 10:34+1Отсутствие интереса… а кого вы рассчитывали заинтересовать своим проектом? Я вот лет 15 занимаюсь в качестве хобби всякими микроконтроллерными поделками, и вашим проектом совершенно не заинтересовался — не вижу в нем никакого смысла.
NordicEnergy
25.09.2018 12:58Дык вам вроде еще в прошлой статье намекнули, что проект не представляет никакого интереса от слова совсем. Так что ничего удивительного тут нет.
Sdima1357
25.09.2018 13:29Ну все же не совсем так. Проект интересный в плане «как я провел лето», «помигал светодиодом » ," собрал эмулятор NES". Как нечто гиковское и прикольное именно несопоставимостью затраченных усилий и ценностью результата. Интересно сделать подобное самому плюс минус доступный каждому уровень.Но проект абсолютно не интересен в смысле покупки. (мо моему нескромному мнению, естественно)
NordicEnergy
25.09.2018 13:50Да, имелось ввиду как раз с точки зрения «экономического» успеха на кикстартере. В плане хоббийной интересности смысла оценивать нет, т.к. я лично не понимаю прикола проекта, а олдфаги может в восторге визжат при прочтении статьи. Тут как говорится на вкус и цвет фломастеры разные))
Sdima1357
25.09.2018 14:29Количество прочитавших и оценки за прошлую статью говорят о том, что тема интересна и олдфагов и гиков на
Geektimesхабре пока хватает. И это хорошо (наверное). Все таки это хорошая самостоятельная работа, а не перевод чужого текста с ошибками. Возможно это окажется кому то полезным как отрицательный опыт. То есть как статья ++, а как проект-забавно.NordicEnergy
27.09.2018 23:02Количество прочитавших явно не показатель, как и количество "+", ибо на хабре обычно чем трешовее статья, тем больше радости она приносит основная массе публики. Вспомните, что тут весь гиктаймс, а там 80% были не сильно обременены интеллектом.
Показатель вот, например, кикстартер и в какой-то меньшей степени отзывы людей под статьей.
Переводы бывают разные, например, технический хороший перевод коих тут много — весьма и весьма полезен, как минимум тем, что несет пользу практическую в отличии от…Sdima1357
28.09.2018 00:12Мне понятна Ваша точка зрения. Конкретно мне, просто интересны эмуляции старого железа (аккуратная эмуляция довольно сложна и занимает кучу времени, даже учитывая то, что она большей частью скопипастена). А хорошие технические статьи сейчас вообще редкость, очень много откровенного мусора, впрочем раньше и трава была зеленее :)
Смотрите, у Вас хорошие, дельные статьи профессионального уровня, но не все имеют возможность писать о том чем занимаются в основное время на работе. А уровень хобби, естественно будет уступать профессиональному.
Pafnutyi
25.09.2018 14:06Я бы купил эту штуку вместо дискавери при одинаковой цене. Удобный корпус для опытов — то за что можно заплатить. По программной части нужно приблизится по удобству к ардуино, всё что вы нового по напридумывали никому не интересно разбираться, нужно поддерживать скетчи.
dlinyj
25.09.2018 16:00Лично для меня, если бы всё это можно было заменить малиной или подобной платой, с выводом интерфейсов — было бы ценнее. А так очень малая гибкость
smart_alex
Наверное интерес определяется практической применимостью людьми для тех или иных своих нужд. Это к вопросу о популярности Ардуино — там и микроконтроллеры «похуже» и IDE «никакая» и, прости Господи, не к ночи будь упомянуты, практически не используются ОСРВ, а популярность бьёт все рекорды.
Отсюда вывод — нужна популярность — проект должен быть интересен и решать те или иные насущные проблемы людей. А сам проект мне нравится своим креативом — вы молодец.