Wrapper C ++ untuk "semua" Sistem Operasi Real-Time untuk CortexM4

gambar

Saya sudah berbicara tentang bagaimana Anda dapat menggunakan FreeRtos untuk proyek yang ditulis dalam C ++ dalam artikel STM32, C ++ dan FreeRTOS. Pengembangan dari awal. Bagian 1 Sejak itu, sebanyak 3 tahun telah berlalu, saya benar-benar menua, kehilangan banyak koneksi saraf, jadi saya memutuskan untuk mengguncang masa lalu untuk memulihkan koneksi ini dan menyapu bungkusnya untuk RTOS populer "apa pun". Ini tentu saja sebuah lelucon, saya sengaja memberi tanda kutip pada "semua orang", tetapi ada beberapa kebenaran dalam setiap lelucon.

Jadi, apa tugasnya dan mengapa itu relevan? Saat ini, ada satu juta sistem operasi berbeda yang ditulis dalam C - Saya tidak ingin memilih untuk setiap selera, dibayar, gratis, kecil, besar ... Tetapi untuk proyek di mana saya berpartisipasi, semua chip dari sistem operasi yang berbeda ini tidak diperlukan, fungsionalitas dasar seperti tugas sudah cukup , acara, pemberitahuan tugas, bagian penting, mutex, dan semaphore (meskipun saya mencoba untuk tidak menggunakannya), antrian. Dan semua ini diperlukan dalam bentuk yang cukup sederhana, tanpa embel-embel.

Menurut pendapat saya, OSRV MAX domestik, ditulis dalam C ++, sangat cocok untuk proyek saya dan itu menyenangkan untuk menggunakannya.

Tetapi yang menarik adalah bahwa perangkat kami harus mematuhi standar IEC_61508, salah satu persyaratannya adalah Aplikasi E.29 dari perpustakaan target yang terbukti digunakan . Baik, atau dengan kata-kata sederhana, jika Anda membuat perangkat untuk memenuhi level SIL3 , maka silakan (Disarankan Lebih Tinggi) menggunakan perpustakaan yang sesuai dengan tingkat ini dan telah teruji oleh waktu.

Mengenai tugas kami, ini berarti dimungkinkan untuk menggunakan MAX MAX RTOS untuk perangkat semacam itu, tetapi poin keandalan tidak akan ditambahkan. Oleh karena itu, produsen RTOS membuat versi khusus dari sistem operasi mereka yang mematuhi standar IEC_61508, misalnya, FreeRTOS memiliki klon SafeRTOS , dan embo memiliki klon embos-Aman , tentu saja produsen menghasilkan uang yang sangat baik untuk ini, karena lisensi untuk OS ini berharga beberapa ribu, atau bahkan puluhan seribu dolar.

Ngomong-ngomong, contoh yang baik adalah kompiler IAR, lisensi yang harganya sekitar $ 1.500, tetapi versi Bersertifikat IAR sudah menelan biaya sekitar 10.000 dolar, meskipun saya memeriksa beberapa proyek - file keluaran dari versi tanpa sertifikat dan dengan sertifikat benar-benar identik. Nah, Anda mengerti bahwa Anda perlu membayar untuk perdamaian.

Jadi, pertama kami menggunakan satu sistem operasi , kemudian saya mulai menggunakan FreeRTOS untuk kebutuhan saya, kemudian kami beralih ke yang lain , secara umum, kami terus menerus harus menulis ulang kode yang sudah jadi. Selain itu, saya ingin terlihat cantik dan sederhana, sehingga siapa pun dapat memahami dengan kode apa yang terjadi, maka dukungan kode akan menjadi pekerjaan sederhana bagi siswa dan praktisi, dan guru akan dapat terus bekerja pada perangkat inovatif, daripada memahami tumpukan mie . Secara umum, saya ingin melihat fosil seperti ini:

gambar

Yah, atau semacamnya ...

gambar

Oleh karena itu, saya memutuskan untuk menulis pembungkus yang cocok dengan FreeRTOS dan mengatakan embos, well, untuk semua orang juga :) dan untuk memulainya, saya menentukan apa yang benar-benar saya butuhkan untuk kebahagiaan total:

  • Tugasnya
  • Bagian kritis
  • Pemberitahuan acara dan tugas
  • Semaphores dan Mutex
  • Antrian

Pembungkusnya harus ideologis SIL3 , dan level ini membebankan banyak hal yang Direkomendasikan Tinggi, dan jika Anda mengikutinya sepenuhnya, ternyata lebih baik tidak menulis kode sama sekali.

Tetapi kenyataan bahwa standar mengatur banyak aturan, atau lebih tepatnya rekomendasi, tidak berarti bahwa mereka tidak dapat dilanggar - Anda bisa, tetapi Anda harus mengikuti rekomendasi sebanyak mungkin untuk mendapatkan poin lebih banyak. Karena itu, saya memutuskan beberapa batasan penting:

  • tidak ada makro , well, kecuali untuk perlindungan terhadap pemasukan ganda file header. Makro itu jahat, jika Anda menghitung berapa banyak waktu yang dihabiskan untuk mencari kesalahan yang terkait dengan makro, ternyata alam semesta tidak begitu tua, dan seberapa banyak kebaikan yang dapat dilakukan selama waktu ini, mungkin lebih baik untuk melarang mereka di tingkat legislatif, karena torrents melarang atau ambil bonus untuk setiap makro yang Anda tulis
  • jangan gunakan pointer , tentu saja bila memungkinkan. Orang bisa mencoba untuk tidak menggunakannya sama sekali, tetapi masih ada tempat di mana tanpa mereka tidak ada cara. Bagaimanapun, pengguna bungkus, jika mungkin, seharusnya tidak tahu apa itu pointer, karena ia hanya mendengar tentang mereka dari kakeknya, karena sekarang ia bekerja secara eksklusif dengan tautan
  • tidak menggunakan alokasi memori dinamis - semuanya jelas, hanya menggunakan timbunan timbunan, pertama, kebutuhan cadangan RAM untuk timbunan ini, dan kedua, dengan seringnya menggunakan timbunan, itu didefragmentasi dan objek-objek baru dibuat lebih lama dan lebih lama lebih lama. Karena itu, pada kenyataannya, saya mengkonfigurasi FreeRTOS hanya pada memori yang dialokasikan secara statis dengan menetapkan configSUPPORT_STATIC_ALLOCATION 1 . Tetapi jika Anda ingin bekerja dalam mode default. Dan secara default, FreeRTOS menggunakan memori yang dialokasikan secara dinamis untuk membuat elemen OS, kemudian hanya mengatur configSUPPORT_STATIC_ALLOCATION 0 , dan
    configSUPPORT_DYNAMIC_ALLOCATION 1 dan jangan lupa untuk menghubungkan implementasi Mallocs dan Callocs Anda sendiri dari manajer memori, misalnya, file ini adalah FreeRtos / portable / MemMang / heap_1.c. Tetapi perlu diingat bahwa Anda harus mengalokasikan RAM dengan cadangan untuk banyak, karena Anda tidak akan dapat menghitung jumlah persis RAM yang dibutuhkan, dengan semua pengaturan (Idle aktif, tugas pengatur waktu program aktif, dua tugas saya, antrian, ukuran antrian untuk timer 10 dan seterusnya, katakanlah itu jelas bukan pengaturan yang paling optimal) yang berfungsi ketika saya mengalokasikan memori seperti ini:
    7 357 byte dari memori kode hanya baca
    535 byte memori data hanya baca
    6.053 byte memori data readwrite

    Alokasi memori statis "sedikit" lebih ringkas:
    7.329 byte dari memori kode hanya baca
    535 byte memori data hanya baca
    3.877 byte memori data readwrite

    Anda mungkin berpikir, "luar biasa ... sendiri," tetapi sekarang kami tidak tertarik dengan pertanyaan yang dirumuskan dalam artikel "Saya mengalokasikan sebanyak 3KB ke sistem operasi dan meluncurkan hanya 3 tugas dengan tumpukan 128B, dan untuk beberapa alasan sudah tidak cukup memori untuk keempat" , Dalam situasi ini, saya sengaja melakukannya, untuk kejelasan, untuk menunjukkan perbedaan antara alokasi memori dinamis dan statis dengan pengaturan yang sama.
  • jangan melemparkan tipe , jika memungkinkan. Jenis ghosting ke jenis lain itu sendiri berarti fakta bahwa ada sesuatu yang salah dalam desain, tetapi seperti biasa, kadang-kadang Anda masih harus melemparkannya untuk kenyamanan (misalnya, enum harus dilemparkan ke bilangan bulat), dan kadang-kadang Anda tidak dapat melakukannya tanpa ini, tetapi ini harus dihindari.
  • kesederhanaan dan kenyamanan . Untuk pengguna bungkus, semua kesulitan harus disembunyikan, jadi hidupnya bukan minyak, dan dia belum ingin menyulitkannya - dia menciptakan tugas, mengimplementasikan semua yang diperlukan di dalamnya, memulainya dan pergi untuk menikmati hidup.

Kami akan mulai dari ini, jadi kami menetapkan sendiri tugas untuk membuat tugas (ternyata langsung dari seri "dilarang untuk melarang").

Pembuatan tugas


Dengan penelitian panjang, para ilmuwan Inggris ( Seluruh kebenaran tentang RTOS dari Colin Walls. Artikel # 4. Tugas, pengalihan konteks dan interupsi ) (omong-omong, jika Anda tidak tahu, perakit untuk ARM juga ditemukan oleh ilmuwan Inggris, sesuatu yang saya tidak terkejut juga sekali :)), dan para ilmuwan Inggris menemukan bahwa untuk sebagian besar "semua" RTOS tugas memiliki nama , tumpukan , ukuran tumpukan , "unit kontrol" , pengidentifikasi atau penunjuk ke "unit kontrol" , prioritas , fungsi yang dilakukan dalam tugas . Itu saja, dan itu mungkin untuk menjejalkan semuanya ke dalam kelas, tapi itu benar jika kami menulis OS dengan Anda, tetapi kami melakukan pembungkus, jadi tidak ada gunanya menyimpan semua hal ini dalam pembungkus, semua ini akan dilakukan untuk Anda oleh ideologi SIL3 OS kami selesai. Bahkan, kita hanya perlu fungsi yang dieksekusi dalam tugas dan struktur yang menyimpan "unit kontrol" , yang diisi saat membuat tugas dan pengidentifikasi tugas . Oleh karena itu, kelas tugas, sebut saja Utas, dapat terlihat sangat sederhana:

class Thread { public: virtual void Execute() = 0 ; private: tTaskHandle taskHandle ; tTaskContext context ; } ; 

Saya hanya ingin mendeklarasikan kelas tugas saya di mana saya bisa mengimplementasikan semua yang diperlukan dan kemudian meneruskan pointer ke objek kelas ini ke pembungkus, yang akan membuat tugas menggunakan RTOS API di mana ia akan meluncurkan metode Execute () :

 class MyTask : public Thread { public: virtual void Execute() override { while(true) { //do something.. } } ; using tMyTaskStack = std::array<OsWrapper::tStack, static_cast<tU16>(OsWrapper::StackDepth::minimal)> ; inline static tMyTaskStack Stack; //!C++17 } ; MyTask myDesiredTask int main() { Rtos::CreateThread(myTask, MyTask::Stack.data(), "myTask") ; } 

Dalam "semua" RTOS, agar tugas yang akan dibuat, perlu untuk memberikan pointer ke fungsi yang akan diluncurkan oleh penjadwal. Dalam kasus kami, ini adalah fungsi Execute () , tapi saya tidak bisa melewatkan pointer ke metode ini, karena ini tidak statis. Oleh karena itu, kami melihat bagaimana tugas dibuat di API "semua" OS dan perhatikan bahwa kami dapat membuat tugas dengan meneruskan parameter ke fungsi tugas, misalnya, untuk embos ini:

 void OS_TASK_CreateEx( OS_TASK* pTask, const char* pName, OS_PRIO Priority, void (*pRoutine)(void * pVoid ), void OS_STACKPTR *pStack, OS_UINT StackSize, OS_UINT TimeSlice, void* pContext); 

void * pContext - ini adalah kunci solusinya. Mari kita memiliki metode statis, sebuah penunjuk yang akan kita lewati sebagai penunjuk ke metode yang disebut oleh penjadwal, dan sebagai parameter kita akan meneruskan penunjuk ke objek tipe Thread di mana kita dapat memanggil metode Execute () secara langsung. Inilah saat ketika tidak ada cara tanpa pointer dan dilemparkan ke tipe, tetapi kode ini akan disembunyikan dari pengguna:

 static void Run(void *pContext ) { static_cast<Thread*>(pContext)->Execute() ; } 

Yaitu seperti algoritma operasi, scheduler meluncurkan metode Run , pointer ke objek tipe Thread dilewatkan ke metode Run . Metode Jalankan secara langsung memanggil metode Execute () , objek spesifik dari kelas Thread , yang hanya merupakan implementasi tugas kami.

Masalahnya hampir selesai, sekarang kita perlu menerapkan metode. Semua OS memiliki API yang berbeda, jadi untuk mengimplementasikan, misalnya, fungsi pembuatan tugas untuk embos, Anda harus memanggil metode OS_TASK_CreateEx (..) yang kosong, dan untuk FreeRTOS dalam mode alokasi memori dinamis, ini adalah xTaskCreate (..) dan meskipun mereka memiliki satu dan esensi yang sama sama, tetapi sintaks dan parameternya berbeda. Tapi kami tidak ingin menjalankan melalui file dan menulis kode untuk setiap metode kelas setiap kali untuk OS baru, jadi kami perlu entah bagaimana memasukkan ini ke dalam satu file dan ... jalankan dalam bentuk makro. Hebat, tapi berhenti, saya melarang makro untuk diri saya sendiri - saya perlu pendekatan yang berbeda.

Hal paling sederhana yang terpikir oleh saya adalah membuat file terpisah untuk setiap OS dengan fungsi sebaris. Jika kita ingin menggunakan OS lain, kita hanya perlu mengimplementasikan masing-masing fungsi ini menggunakan API OS ini. File rtosFreeRtos.cpp berikut ini ternyata

 #include "rtos.hpp" //For FreeRTOS functions prototypes #include <FreeRTOS.h> //For xTaskCreate #include <task.h> namespace OsWrapper { void wCreateThread(Thread & thread, const char * pName, ThreadPriority prior,const tU16 stackDepth, tStack *pStack) { #if (configSUPPORT_STATIC_ALLOCATION == 1) if (pStack != nullptr) { thread.handle = xTaskCreateStatic(static_cast<TaskFunction_t>(Rtos::Run), pName, stackDepth, &thread, static_cast<uint32_t>(prior), pStack, &thread.taskControlBlock); } #else thread.handle = (xTaskCreate(static_cast<TaskFunction_t>(Rtos::Run), pName, stackDepth, &thread, static_cast<uint32_t>(prior), &thread.handle) == pdTRUE) ? thread.handle : nullptr ; #endif } 

File untuk embOS rtosEmbOS.cpp mungkin terlihat persis sama

 #include "rtos.hpp" //For embOS functions prototypes #include <rtos.h> namespace OsWrapper { void wCreateThread(Thread &thread, const char * pName, ThreadPriority prior,const tU16 stackDepth, tStack *pStack) { constexpr OS_UINT timeSliceNull = 0 ; if (pStack != nullptr) { OS_CreateTaskEx(&(thread.handle), pName, static_cast<OS_PRIO>(prior), Rtos::Run, pStack, ((stackSize == 0U) ? sizeof(pStack) : stackSize), timeSliceNull, &thread) ; } } 

Jenis sistem operasi yang berbeda juga berbeda, terutama struktur konteks tugas, jadi mari kita buat file rtosdefs.hpp dengan alias pembungkus kita sendiri.

 #include <FreeRTOS.h> //For TaskHandle_t namespace OsWrapper { using tTaskContext = StaticTask_t; using tTaskHandle = TaskHandle_t; using tStack = StackType_t ; } 

Untuk EmbOS, mungkin terlihat seperti ini:

 #include <rtos.h> //For OS_TASK namespace OsWrapper { using tTaskContext = OS_TASK; using tTaskHandle = OS_TASK; using tStack = tU16 //   void,      tU16 ; } 

Akibatnya, untuk perubahan di bawah RTOS lainnya, cukup membuat perubahan hanya di dua file ini rtosdefs.cpp dan rtos.cpp. Sekarang kelas Thread dan Rtos terlihat seperti gambar c

gambar

Meluncurkan OS dan menyelesaikan tugas


Untuk Cortex M4, "semua" OS menggunakan 3 interupsi, pengingat waktu Sistem , panggilan Layanan Sistem melalui instruksi SWI , Permintaan yang dapat ditunda untuk gangguan layanan sistem , yang terutama ditemukan untuk RTOS. Beberapa RTOS juga menggunakan interupsi sistem lain, tetapi ini akan cukup untuk sebagian besar "semua" OS. Dan jika tidak, maka dimungkinkan untuk menambahkan, jadi tentukan saja tiga penangan untuk interupsi ini dan untuk memulai RTOS kita perlu metode start yang lain:

 static void HandleSvcInterrupt() ; static void HandleSvInterrupt() ; static void HandleSysTickInterrupt() ; static void Start() ; 

Hal pertama yang saya butuhkan dan tanpanya saya tidak bisa hidup, apa yang saya impikan adalah mekanisme pemberitahuan untuk tugas. Saya umumnya menyukai pemrograman yang digerakkan oleh Acara, jadi saya harus segera mengimplementasikan pembungkus untuk memberi tahu tugas.

Semuanya ternyata cukup sederhana, OS apa pun bisa melakukannya, well, kecuali mungkin uc-OS-II dan III , meskipun mungkin saya tidak membacanya dengan baik, tetapi, menurut saya, mekanisme acara umumnya rumit, tetapi oh well, "semuanya" adalah sisanya mereka pasti bisa.

Untuk memberi tahu tugas, Anda hanya perlu mengirim acara bukan ke kekosongan, tetapi khusus untuk tugas tersebut, untuk ini, metode pemberitahuan harus memiliki penunjuk ke konteks tugas atau pengidentifikasi tugas. Saya hanya menyimpan ini di kelas Thread , yang berarti bahwa kelas Thread juga harus memiliki metode peringatan. Seharusnya ada metode untuk menunggu peringatan. Pada saat yang sama, kami menambahkan metode Tidur (..) , yang menjeda pelaksanaan tugas panggilan. Sekarang kedua kelas terlihat seperti ini:

gambar

rtos.hpp
 /******************************************************************************* * Filename : Rtos.hpp * * Details : Rtos class is used to create tasks, work with special Rtos * functions and also it contains a special static method Run. In this method * the pointer on Thread should be pass. This method is input point as * the task of Rtos. In the body of the method, the method of concrete Thread * will run. *******************************************************************************/ #ifndef __RTOS_HPP #define __RTOS_HPP #include "thread.hpp" // for Thread #include "../../Common/susudefs.hpp" #include "FreeRtos/rtosdefs.hpp" namespace OsWrapper { extern void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *) ; extern void wStart() ; extern void wHandleSvcInterrupt() ; extern void wHandleSvInterrupt() ; extern void wHandleSysTickInterrupt() ; extern void wEnterCriticalSection(); extern void wLeaveCriticalSection(); class Rtos { public: static void CreateThread(Thread &thread , tStack * pStack = nullptr, const char * pName = nullptr, ThreadPriority prior = ThreadPriority::normal, const tU16 stackDepth = static_cast<tU16>(StackDepth::minimal)) ; static void Start() ; static void HandleSvcInterrupt() ; static void HandleSvInterrupt() ; static void HandleSysTickInterrupt() ; friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *); private: //cstat !MISRAC++2008-7-1-2 To prevent reinterpet_cast in the CreateTask static void Run(void *pContext ) { static_cast<Thread*>(pContext)->Execute() ; } } ; } ; #endif // __RTOS_HPP 


thread.hpp
 /******************************************************************************* * Filename : thread.hpp * * Details : Base class for any Taskis which contains the pure virtual * method Execute(). Any active classes which will have a method for running as * a task of RTOS should inherit the Thread and override the Execute() method. * For example: * class MyTask : public OsWrapper::Thread * { * public: * virtual void Execute() override { * while(true) { * //do something.. * } * } ; * *******************************************************************************/ #ifndef __THREAD_HPP #define __THREAD_HPP #include "FreeRtos/rtosdefs.hpp" #include "../../Common/susudefs.hpp" namespace OsWrapper { extern void wSleep(const tTime) ; extern void wSleepUntil(tTime &, const tTime) ; extern tTime wGetTicks() ; extern void wSignal(tTaskHandle const &, const tTaskEventMask) ; extern tTaskEventMask wWaitForSignal(const tTaskEventMask, tTime) ; constexpr tTaskEventMask defaultTaskMaskBits = 0b010101010 ; enum class ThreadPriority { clear = 0, lowest = 10, belowNormal = 20, normal = 30, aboveNormal = 80, highest = 90, priorityMax = 255 } ; enum class StackDepth: tU16 { minimal = 128U, medium = 256U, big = 512U, biggest = 1024U }; class Thread { public: virtual void Execute() = 0 ; inline tTaskHandle GetTaskHanlde() const { return handle; } static void Sleep(const tTime timeOut = 1000ms) { wSleep(timeOut) ; }; inline void Signal(const tTaskEventMask mask = defaultTaskMaskBits) { wSignal(handle, mask); }; inline tTaskEventMask WaitForSignal(tTime timeOut = 1000ms, const tTaskEventMask mask = defaultTaskMaskBits) { return wWaitForSignal(mask, timeOut) ; } friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *); private: tTaskHandle handle ; tTaskContext context ; } ; } ; #endif // __THREAD_HPP 


Saya mulai mengimplementasikannya, dan di sini masalah pertama sedang menunggu saya, ternyata "ada" OS memanggil fungsinya dari interupsi dengan cara yang berbeda. Misalnya, FreeRTOS memiliki implementasi khusus fungsi untuk mengeksekusinya dari interupsi, misalnya, jika ada fungsi xTaskNotify (..) , maka Anda tidak dapat memanggilnya dari interrupt, tetapi Anda perlu memanggil xTaskNotifyFromISR (..) .
Untuk embOS, jika Anda memanggil fungsi apa pun dari interupsi, silakan gunakan OS_InInterrupt () saat memasukkan interupsi dan OS_LeaveInterrupt () saat keluar. Saya harus membuat kelas InterruptEntry , yang hanya memiliki konstruktor dan destruktor:

 namespace OsWrapper { extern void wEnterInterrupt() ; extern void wLeaveInterrupt() ; class InterruptEntry { public: inline InterruptEntry() { wEnterInterrupt() ; } inline ~InterruptEntry() { wLeaveInterrupt() ; } } ; } ; 

Anda bisa menggunakannya seperti ini:

 void Button::HandleInterrupt() { const OsWrapper::InterruptEntry ie; EXTI->PR = EXTI_PR_PR13 ; myDesiredTask.Signal(); } void myDesiredTask::Execute() { while(true) { if (WaitForSignal(100000ms) == defaultTaskMaskBits) { GPIOC->ODR ^= (1 << 5) ; } } } ; 

Jelas, untuk FreeRTOS, baik konstruktor maupun destruktor akan kosong. Dan untuk pemberitahuan, Anda dapat menggunakan fungsi xTaskNotifyFromISR (..) , yang dari mana pun namanya, sedikit overhead, tetapi apa yang tidak dapat Anda lakukan demi universalitas. Tentu saja Anda dapat membuat metode terpisah untuk panggilan dari interupsi, tetapi untuk sekarang saya memutuskan untuk melakukannya secara universal.
Trik yang sama dengan InterruptEntry dapat dilakukan dengan bagian kritis:

 namespace OsWrapper{ class CriticalSection { public: inline CriticalSection() { wEnterCriticalSection() ; } inline ~CriticalSection() { wLeaveCriticalSection() ; } } ; } ; 

Sekarang cukup tambahkan implementasi fungsi menggunakan FreeRtos API ke file dan jalankan cek, meskipun Anda tidak bisa menjalankannya, jadi jelas bahwa itu akan berhasil :)
rtosFreeRtos.cpp
 /******************************************************************************* * Filename : rtosFreeRtos.cpp * * Details : This file containce implementation of functions of concrete * FreeRTOS to support another RTOS create the same file with the * same functions but another name< for example rtosEmbOS.cpp and * implement these functions using EmbOS API. * *******************************************************************************/ #include "../thread.hpp" #include "../mutex.hpp" #include "../rtos.hpp" #include "../../../Common/susudefs.hpp" #include "rtosdefs.hpp" #include "../event.hpp" #include <limits> namespace OsWrapper { /***************************************************************************** * Function Name: wCreateThread * Description: Creates a new task and passes a parameter to the task. The * function should call appropriate RTOS API function to create a task. * * Assumptions: RTOS API create task function should get a parameter to pass the * paramete to task. * Some RTOS does not use pStack pointer so it should be set to nullptr * * Parameters: [in] thread - refernce on Thread object * [in] pName - name of task * [in] prior - task priority * [in] stackDepth - size of Stack * [in] pStack - pointer on task stack * Returns: No ****************************************************************************/ void wCreateThread(Thread & thread, const char * pName, ThreadPriority prior, const tU16 stackDepth, tStack *pStack) { #if (configSUPPORT_STATIC_ALLOCATION == 1) if (pStack != nullptr) { thread.handle = xTaskCreateStatic(static_cast<TaskFunction_t>(Rtos::Run), pName, stackDepth, &thread, static_cast<uint32_t>(prior), pStack, &thread.context); } #else thread.handle = (xTaskCreate(static_cast<TaskFunction_t>(Rtos::Run), pName, stackDepth, &thread, static_cast<uint32_t>(prior), &thread.handle) == pdTRUE) ? thread.handle : nullptr ; #endif } /***************************************************************************** * Function Name: wStart() * Description: Starts the RTOS scheduler * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wStart() { vTaskStartScheduler() ; } /***************************************************************************** * Function Name: wHandleSvcInterrupt() * Description: Handle of SVC Interrupt. The function should call appropriate * RTOS function to handle the interrupt * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wHandleSvcInterrupt() { vPortSVCHandler() ; } /***************************************************************************** * Function Name: wHandleSvInterrupt() * Description: Handle of SV Interrupt. The function should call appropriate * RTOS function to handle the interrupt * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wHandleSvInterrupt() { xPortPendSVHandler() ; } /***************************************************************************** * Function Name: wHandleSysTickInterrupt() * Description: Handle of System Timer Interrupt. The function should call * appropriate RTOS function to handle the interrupt * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wHandleSysTickInterrupt() { xPortSysTickHandler() ; } /***************************************************************************** * Function Name: wSleep() * Description: Suspends the calling task for a specified period of time, * or waits actively when called from main() * * Assumptions: No * Parameters: [in] timeOut - specifies the time interval in system ticks * Returns: No ****************************************************************************/ void wSleep(const tTime timeOut) { vTaskDelay(timeOut) ; } /***************************************************************************** * Function Name: wEnterCriticalSection() * Description: Basic critical section implementation that works by simply * disabling interrupts * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wEnterCriticalSection() { taskENTER_CRITICAL() ; } /***************************************************************************** * Function Name: wLeaveCriticalSection() * Description: Leave critical section implementation that works by simply * enabling interrupts * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wLeaveCriticalSection() { taskEXIT_CRITICAL() ; } /**************************************************************************** * Function Name: wEnterInterrupt() * Description: Some RTOS requires to inform the kernel that interrupt code * is executing * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wEnterInterrupt() { } /**************************************************************************** * Function Name: wLeaveInterrupt() * Description: Some RTOS requires to inform that the end of the interrupt r * outine has been reached; executes task switching within ISR * * Assumptions: No * Parameters: No * Returns: No ****************************************************************************/ void wLeaveInterrupt() { } /**************************************************************************** * Function Name: wSignal() * Description: Signals event(s) to a specified task * * Assumptions: No * Parameters: [in] taskHandle - Reference to the task structure * [in] mask - The event bit mask containing the event bits, * which shall be signaled. * Returns: No ****************************************************************************/ void wSignal(tTaskHandle const &taskHandle, const tTaskEventMask mask) { BaseType_t xHigherPriorityTaskWoken = pdFALSE ; xTaskNotifyFromISR(taskHandle, mask, eSetBits, &xHigherPriorityTaskWoken) ; portYIELD_FROM_ISR( xHigherPriorityTaskWoken ) ; } /**************************************************************************** * Function Name: wWaitForSignal() * Description: Waits for the specified events for a given time, and clears * the event memory when the function returns * * Assumptions: No * Parameters: [in] mask - The event bit mask containing the event bits, * which shall be waited for * [in] timeOut - Maximum time in system ticks waiting for events * to be signaled. * Returns: Set bits ****************************************************************************/ tTaskEventMask wWaitForSignal(const tTaskEventMask mask, tTime timeOut) { uint32_t ulNotifiedValue = 0U ; xTaskNotifyWait( 0U, std::numeric_limits<uint32_t>::max(), &ulNotifiedValue, timeOut); return (ulNotifiedValue & mask) ; } /**************************************************************************** * Function Name: wCreateEvent() * Description: Create an Event object * * Assumptions: No * Parameters: [in] event - reference on tEvent object * * Returns: Handle of created Event ****************************************************************************/ tEventHandle wCreateEvent(tEvent &event) { #if (configSUPPORT_STATIC_ALLOCATION == 1) return xEventGroupCreateStatic(&event); #else return xEventGroupCreate(); #endif } /**************************************************************************** * Function Name: wDeleteEvent() * Description: Create an Event object * * Assumptions: No * Parameters: [in] eventHandle - reference on tEventHandle object * * Returns: No ****************************************************************************/ void wDeleteEvent(tEventHandle &eventHandle) { vEventGroupDelete(eventHandle); } /**************************************************************************** * Function Name: wSignalEvent() * Description: Sets an resumes tasks which are waiting at the event object * * Assumptions: No * Parameters: [in] event - reference on eventHandle object * [in] mask - The event bit mask containing the event bits, * which shall be signaled * * Returns: No ****************************************************************************/ void wSignalEvent(tEventHandle const &eventHandle, const tEventBits mask) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; xEventGroupSetBitsFromISR(eventHandle, mask, &xHigherPriorityTaskWoken) ; portYIELD_FROM_ISR(xHigherPriorityTaskWoken) ; } /**************************************************************************** * Function Name: wWaitEvent() * Description: Waits for an event and suspends the task for a specified time * or until the event has been signaled. * * Assumptions: No * Parameters: [in] event - Reference on eventHandle object * [in] mask - The event bit mask containing the event bits, * which shall be signaled * [in] timeOut - Maximum time in RTOS system ticks until the * event must be signaled. * [in] mode - Indicate mask bit behaviour * * Returns: Set bits ****************************************************************************/ tEventBits wWaitEvent(tEventHandle const &eventHandle, const tEventBits mask, const tTime timeOut, OsWrapper::EventMode mode) { BaseType_t xWaitForAllBits = pdFALSE ; if (mode == OsWrapper::EventMode::waitAnyBits) { xWaitForAllBits = pdFALSE; } return xEventGroupWaitBits(eventHandle, mask, pdTRUE, xWaitForAllBits, timeOut) ; } /**************************************************************************** * Function Name: wCreateMutex() * Description: Create an mutex. Mutexes are used for managing resources by * avoiding conflicts caused by simultaneous use of a resource. The resource * managed can be of any kind: a part of the program that is not reentrant, a * piece of hardware like the display, a flash prom that can only be written to * by a single task at a time, a motor in a CNC control that can only be * controlled by one task at a time, and a lot more. * * Assumptions: No * Parameters: [in] mutex - Reference on tMutex structure * [in] mode - Indicate mask bit behaviour * * Returns: Mutex handle ****************************************************************************/ tMutexHandle wCreateMutex(tMutex &mutex) { #if (configSUPPORT_STATIC_ALLOCATION == 1) return xSemaphoreCreateMutexStatic(&mutex) ; #else return xSemaphoreCreateMutex(); #endif } /**************************************************************************** * Function Name: wDeleteMutex() * Description: Delete the mutex. * * Assumptions: No * Parameters: [in] mutex - handle of mutex * * Returns: Mutex handle ****************************************************************************/ void wDeleteMutex(tMutexHandle &handle) { vSemaphoreDelete(handle) ; } /**************************************************************************** * Function Name: wLockMutex() * Description: Claim the resource * * Assumptions: No * Parameters: [in] handle - handle of mutex * [in] timeOut - Maximum time until the mutex should be available * * Returns: true if resource has been claimed, false if timeout is expired ****************************************************************************/ bool wLockMutex(tMutexHandle const &handle, tTime timeOut) { return static_cast<bool>(xSemaphoreTake(handle, timeOut)) ; } /**************************************************************************** * Function Name: wUnLockMutex() * Description: Releases a mutex currently in use by a task * * Assumptions: No * Parameters: [in] handle - handle of mutex * * Returns: No ****************************************************************************/ void wUnLockMutex(tMutexHandle const &handle) { BaseType_t xHigherPriorityTaskWoken = pdFALSE ; xSemaphoreGiveFromISR(handle, &xHigherPriorityTaskWoken) ; portYIELD_FROM_ISR( xHigherPriorityTaskWoken ) ; } /**************************************************************************** * Function Name: wSleepUntil() * Description: Suspends the calling task until a specified time, or waits * actively when called from main() * * Assumptions: No * Parameters: [in] last - Refence to a variable that holds the time at which * the task was last unblocked. The variable must be initialised * with the current time prior to its first use * [in] timeOut - Time to delay until, the task will be unblocked * at time * * Returns: No ****************************************************************************/ void wSleepUntil(tTime & last, const tTime timeOut) { vTaskDelayUntil( &last, timeOut) ; } /**************************************************************************** * Function Name: wGetTicks() * Description: Returns the current system time in ticks as a native integer * value * * Assumptions: No * Parameters: No * * Returns: Current system time in ticks ****************************************************************************/ tTime wGetTicks() { return xTaskGetTickCount(); } } 


gambar

Kami terus memperbaiki tugas


Tugas sekarang memiliki hampir semua yang Anda butuhkan, kami menambahkan metode Sleep (). Metode ini menjeda tugas untuk waktu tertentu. Dalam kebanyakan kasus, ini sudah cukup, tetapi jika Anda membutuhkan waktu yang ditentukan dengan jelas, maka Sleep () dapat membawa Anda masalah. Misalnya, Anda ingin melakukan perhitungan dan mengedipkan LED dan melakukannya tepat setiap 100 ms

 void MyTask::Execute() { while(true) { DoCalculation(); //It takes about 10ms Led1.Toggle() ; Sleep(100ms) ; } } 

Kode ini akan berkedip LED setiap 110 ms. Tetapi Anda ingin sekali setiap 100 ms, Anda dapat secara kasar menghitung waktu perhitungan dan menempatkan Sleep (90 ms). Tetapi bagaimana jika waktu perhitungan tergantung pada parameter input, maka berkedip tidak akan menjadi deterministik sama sekali. Untuk kasus seperti itu di "semua" OS, ada metode khusus, seperti DelayUntil (). Ini bekerja pada prinsip ini - pertama Anda perlu mengingat nilai saat ini dari tick tick dari sistem operasi, kemudian tambahkan ke nilai ini jumlah ticks yang Anda butuhkan untuk menjeda tugas, segera setelah tick tick mencapai nilai ini, tugas dibuka kuncinya. Dengan demikian, tugas akan dikunci tepat ke nilai yang Anda tetapkan dan LED Anda akan berkedip persis setiap 100 ms, terlepas dari durasi perhitungan.
Mekanisme ini diimplementasikan secara berbeda di sistem operasi yang berbeda, tetapi memiliki satu algoritma. Akibatnya, mekanisme, katakanlah, diterapkan pada FreeRTOS, akan disederhanakan menjadi negara yang ditunjukkan pada gambar berikut:

gambar

Seperti yang Anda lihat pembacaan keadaan awal dari tick tick pada sistem operasi terjadi sebelum memasuki loop tak terbatas, dan kita perlu menemukan sesuatu untuk mengimplementasikannya. Templat desain tersedia untuk membantu. Metode templat .Diimplementasikan dengan sangat sederhana, kita hanya perlu menambahkan metode non-virtual, di mana pertama kita memanggil metode yang membaca dan mengingat penghitung tik dari sistem operasi, dan kemudian kita memanggil metode virtual Execute (), yang akan diimplementasikan dalam turunan, yaitu. dalam implementasi tugas Anda. Karena kami tidak memerlukan metode ini untuk pengguna (ini hanya tambahan), kami akan menyembunyikannya di bagian pribadi.

  class Thread { public: virtual void Execute() = 0 ; friend class Rtos ; private: void Run() { lastWakeTime = wGetTicks() ; Execute(); } ... tTime lastWakeTime = 0ms ; ... } 

Dengan demikian, dalam metode Run statis kelas Rtos, sekarang akan perlu untuk memanggil bukan Execute (), tetapi metode Run () dari objek Thread. Kami baru saja membuat kelas Rtos ramah untuk mengakses metode Run () pribadi di kelas Thread.

 static void Run(void *pContext ) { static_cast<Thread*>(pContext)->Run() ; } 

Satu-satunya batasan untuk metode SleepUntil () adalah tidak dapat diterapkan bersama dengan metode lain yang memblokir tugas. Atau, untuk menyelesaikan masalah bekerja bersama-sama dengan metode lain yang memblokir tugas, akan mungkin untuk menambahkan metode memperbarui penghitung centang yang diingat dari sistem, dan memanggilnya sebelum SleepUntil () , tetapi untuk saat ini kami hanya menjaga nuansa ini dalam pikiran. Versi ekstrem dari kelas terlihat seperti pada gambar berikut:
gambar

thread.hpp
 /******************************************************************************* * Filename : thread.hpp * * Details : Base class for any Taskis which contains the pure virtual * method Execute(). Any active classes which will have a method for running as * a task of RTOS should inherit the Thread and override the Execute() method. * For example: * class MyTask : public OsWrapper::Thread * { * public: * virtual void Execute() override { * while(true) { * //do something.. * } * } ; * * Author : Sergey Kolody *******************************************************************************/ #ifndef __THREAD_HPP #define __THREAD_HPP #include "FreeRtos/rtosdefs.hpp" #include "../../Common/susudefs.hpp" namespace OsWrapper { extern void wSleep(const tTime) ; extern void wSleepUntil(tTime &, const tTime) ; extern tTime wGetTicks() ; extern void wSignal(tTaskHandle const &, const tTaskEventMask) ; extern tTaskEventMask wWaitForSignal(const tTaskEventMask, tTime) ; constexpr tTaskEventMask defaultTaskMaskBits = 0b010101010 ; enum class ThreadPriority { clear = 0, lowest = 10, belowNormal = 20, normal = 30, aboveNormal = 80, highest = 90, priorityMax = 255 } ; enum class StackDepth: tU16 { minimal = 128U, medium = 256U, big = 512U, biggest = 1024U }; class Thread { public: virtual void Execute() = 0 ; inline tTaskHandle GetTaskHanlde() const { return handle; } static void Sleep(const tTime timeOut = 1000ms) { wSleep(timeOut) ; }; void SleepUntil(const tTime timeOut = 1000ms) { wSleepUntil(lastWakeTime, timeOut); }; inline void Signal(const tTaskEventMask mask = defaultTaskMaskBits) { wSignal(handle, mask); }; inline tTaskEventMask WaitForSignal(tTime timeOut = 1000ms, const tTaskEventMask mask = defaultTaskMaskBits) { return wWaitForSignal(mask, timeOut) ; } friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *); friend class Rtos ; private: tTaskHandle handle ; tTaskContext context ; tTime lastWakeTime = 0ms ; void Run() { lastWakeTime = wGetTicks() ; Execute(); } } ; } ; #endif // __THREAD_HPP 


rtos.hpp
 /******************************************************************************* * Filename : Rtos.hpp * * Details : Rtos class is used to create tasks, work with special Rtos * functions and also it contains a special static method Run. In this method * the pointer on Thread should be pass. This method is input point as * the task of Rtos. In the body of the method, the method of concrete Thread * will run. *******************************************************************************/ #ifndef __RTOS_HPP #define __RTOS_HPP #include "thread.hpp" // for Thread #include "../../Common/susudefs.hpp" #include "FreeRtos/rtosdefs.hpp" namespace OsWrapper { extern void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *) ; extern void wStart() ; extern void wHandleSvcInterrupt() ; extern void wHandleSvInterrupt() ; extern void wHandleSysTickInterrupt() ; extern void wEnterCriticalSection(); extern void wLeaveCriticalSection(); class Rtos { public: static void CreateThread(Thread &thread , tStack * pStack = nullptr, const char * pName = nullptr, ThreadPriority prior = ThreadPriority::normal, const tU16 stackDepth = static_cast<tU16>(StackDepth::minimal)) ; static void Start() ; static void HandleSvcInterrupt() ; static void HandleSvInterrupt() ; static void HandleSysTickInterrupt() ; friend void wCreateThread(Thread &, const char *, ThreadPriority, const tU16, tStack *); friend class Thread ; private: //cstat !MISRAC++2008-7-1-2 To prevent reinterpet_cast in the CreateTask static void Run(void *pContext ) { static_cast<Thread*>(pContext)->Run() ; } } ; } ; #endif // __RTOS_HPP 



Acara


Jadi tugas sedang dibuat, adalah mungkin untuk mengirim acara ke sana, tapi saya ingin mengimplementasikan acara yang dapat dikirim bukan ke tugas tertentu, tetapi untuk pelanggan yang memutuskan untuk menunggu acara ini. Secara kasar, kita perlu menerapkan pembungkus atas Acara.

Secara umum, mekanisme acara menyiratkan banyak pilihan, Anda dapat mengirim acara dengan mengatur bit, dan beberapa tugas dapat menunggu beberapa bit untuk diatur, yang lain untuk mengatur yang lain, Anda dapat mengharapkan semuanya sekaligus, Anda dapat menghapus bit setelah acara diterima, singkatnya ada banyak pilihan, tetapi dalam pekerjaan saya, saya perlu mengirim dan menerima acara dan membuang semua bit. Namun, Anda masih perlu menawarkan antarmuka sederhana untuk mendukung fungsionalitas tambahan. Dengan struktur acara, mereka mirip dengan tugas, mereka juga memiliki konteks tertentu yang perlu disimpan dan pengidentifikasi, saya juga ingin acara tersebut dikonfigurasi untuk menunggu waktu dan topeng, jadi saya menambahkan dua bidang pribadi lagi.



Anda bisa menggunakannya seperti ini:

 OsWrapper::Event event{10000ms, 3}; //  ,    10000ms,    0    1. void SomeTask::Execute() { while(true) { using OsWrapper::operator""ms ; Sleep(1000ms); event.Signal() ; //      0   1. Sleep(1000ms); event.SetMaskBits(4) //    2. event.Signal() ; //      2. } } ; void AnotherTask::Execute() { while(true) { using namespace::OsWrapper ; //,      ,    10000ms if ((event.Wait() & defaultTaskMaskBits) != 0) { GPIOC->ODR ^= (1 << 5) ; } } } ; 


Mutex, Semaphores dan Antrian


Awalnya, ketika saya menulis artikel ini, saya belum mengimplementasikannya, tetapi seperti yang dijanjikan saya mengembangkan mutexes dan kotak email, sumbernya ada di sini: GitHub OsWrapper . Contoh menggunakan kotak surat adalah sebagai berikut:
 OsWrapper::MailBox<tU32, 10> queue; //    10   int void ReceiveTask::Execute() { tU32 item; while(true) { using OsWrapper::operator""ms ; if (queue.Get(item, 10000ms)) { //    GPIOC->ODR ^= (1 << 9); } } } ; void SendTask::Execute() { tU32 item = 0U; while(true) { queue.Put(item); item ++; SleepUntil(1000ms); } } ; 


Cara menggunakan semua bisnis ini


Dasarnya dibuat untuk memahami bagaimana semua ini dapat digunakan, saya memberikan sepotong kecil kode yang melakukan hal berikut: Tugas LedTask berkedip LED tepat 2 detik, dan setiap 2 detik itu mengirim sinyal ke tugas myTask , yang menunggu 10 detik untuk acara segera setelah acara telah tiba, dia berkedip LED lain. Secara umum, sebagai hasilnya, dua LED berkedip setiap 2 detik sekali. Saya tidak langsung memberi tahu tugas itu, tetapi melakukannya melalui acara . Apakah ini solusi luar biasa untuk mengedipkan dua LED :)

 using OsWrapper::operator""ms ; OsWrapper::Event event{10000ms, 1}; class MyTask : public OsWrapper::Thread { public: virtual void Execute() override { while(true) { if (event.Wait() != 0) { GPIOC->ODR ^= (1 << 9); } } } using tMyTaskStack = std::array<OsWrapper::tStack, static_cast<tU16>(OsWrapper::StackDepth::minimal)> ; inline static tMyTaskStack Stack; //C++17   IAR 8.30 } ; class LedTask : public OsWrapper::Thread { public: virtual void Execute() override { while(true) { GPIOC->ODR ^= (1 << 5) ; using OsWrapper::operator""ms ; SleepUntil(2000ms); event.Signal() ; } } using tLedStack = std::array<OsWrapper::tStack, static_cast<tU16>(OsWrapper::StackDepth::minimal)> ; inline static tLedStack Stack; //C++17   IAR 8.30 } ; MyTask myTask; LedTask ledTask; int main() { using namespace OsWrapper ; Rtos::CreateThread(myTask, MyTask::Stack.data(), "myTask", ThreadPriority::lowest, MyTask::Stack.size()) ; Rtos::CreateThread(ledTask, LedTask::Stack.data()) ; Rtos::Start(); return 0; } 


Kesimpulan


Saya berani mengungkapkan pandangan subjektif saya tentang masa depan firmware untuk mikrokontroler. Saya percaya bahwa C ++ akan datang dan cepat atau lambat semakin banyak sistem operasi akan muncul yang menyediakan antarmuka C ++. Produsen sekarang perlu menulis ulang atau membungkus semuanya dalam C ++.
Dari sudut pandang ini, saya akan merekomendasikan menggunakan RTOS yang ditulis dalam C ++, misalnya, RTOS MAX yang ditunjukkan di atas , berapa banyak waktu yang dapat menyelamatkan Anda, Anda bahkan tidak bisa membayangkan, karena masih ada fitur unik seperti, misalnya, sarana interaksi tugas, berjalan pada mikrokontroler yang berbeda. Jika dia juga memiliki sertifikat keamanan, maka tidak akan ada solusi yang lebih baik.

Tetapi sementara itu, sebagian besar dari kita menggunakan OS Sishna tradisional, Anda dapat menggunakan pembungkus sebagai awal untuk transisi ke masa depan yang bahagia dengan C ++ :)

Saya mengumpulkan proyek uji kecil di Clion. Saya harus mengutak-atik pengaturannya, itu masih tidak cukup dimaksudkan untuk mengembangkan perangkat lunak untuk mikrokontroler, dan hampir tidak berteman dengan IAR toolchain, tetapi bagaimanapun, ternyata untuk mengkompilasi, tautan ke format elf, dikonversi ke format hex, flash, dan mulai debugging menggunakan Gdb. Dan itu sepadan - itu hanya lingkungan yang sangat baik, dan itu mengoreksi kesalahan saat bepergian, dan jika Anda perlu mengubah tanda tangan metode, kemudian refactoring dalam 2 detik, dan secara umum Anda tidak perlu berpikir lagi, dia akan mengatakan pada dirinya sendiri di mana seharusnya, apa yang lebih baik untuk membuat atau memberi nama parameter . Saya bahkan mendapat kesan bahwa pembungkus itu sendiri ditulis oleh Clion. Secara umum, ketika semua bug yang terkait dengan rantai alat IAR diperbaiki, Anda dapat mengambilnya.

Tetapi dengan cara kuno, saya membuat proyek untuk IAR untuk versi 8.30.1, di atasnya saya memeriksa cara kerjanya. Digunakan peralatan berikut:XNUCLEO-F411RE , ST-Link Debugger. Namun, sekali lagi, lihat bagaimana debugging terlihat di Clion - yah, bagus, tapi masih bermasalah :)

gambar

Proyek IAR dapat Anda ambil di sini: Proyek IAR 8.30.1 Sementara ini adalah versi yang tidak lengkap, tanpa antrian dan semafor, saya lebih lengkap Saya akan meletakkannya di github ketika tangan saya mencapai, waktu untuk belajar dimulai dan waktu akan sangat kecil, Tapi saya pikir ini sudah dapat digunakan untuk proyek-proyek kecil bersama dengan FreeRtos.

Z.Y.Seperti yang dijanjikan pada Proyek Wrapper GitHub

Source: https://habr.com/ru/post/id420467/


All Articles