Coroutines in C++20 on ARM Cortex-M

Coroutines in C++20 on ARM Cortex-M

Valentyn Korniienko

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

- MUST WATCH видео от автора предложения Coroutines в стандарт С++20. Раскрыты детали реализации сопрограмм, бенчмарки в сравнении с "callback hell". Рекомендуется к просмотру в первую очередь.

https://blog.panicsoftware.com/coroutines-introduction/ - тут хорошая начальная теория. Либо, взять за основу следующую статью:

https://luncliff.github.io/posts/Exploring-MSVC-Coroutine.html

- тут больше картинок, и примеры кода есть под конец статьи:

https://blog.panicsoftware.com/your-first-coroutine/

- следующая в цикле статей, для начального понимания происходящего:

https://lewissbaker.github.io/2017/09/25/coroutine-theory

- базовая теория по сопрограммам, для начала может быть сложной:

https://habr.com/ru/company/yandex/blog/420861/ - полезный пример "телепортации" между различными очередями задач;

https://habr.com/en/post/348602/ - как использовать сопрограммы из С++20 в связке с boost::asio;

https://habr.com/en/post/493808/ // coroutine code linearizing - Линеаризация кода с использованием сопрограмм. Показаны фрагменты реализации модели Акторов.

https://manybutfinite.com/post/anatomy-of-a-program-in-memory/

https://manybutfinite.com/post/journey-to-the-stack/

В свою очередь, было решено попробовать фишки из С++20 в домашнем проекте. Прежде чем собирать все в реальном проекте, был сделан отдельный репозиторий https://github.com/ValentiWorkLearning/CoroutineExperiments/ . В репозитории была реализована первая идея: использовать сопрограммы для переписывания драйвера для дисплея, работающего по SPI.

Пояснение демонстрационного примера из репозитория CoroutineExperiments

Для реализации "эмуляции" передачи данных по интерфейсу SPI с использованием DMA необходимо реализовать следующий набор функций:

  • сигнатуру HAL(Hardware Abstraction Layer) подобную доступной в API конкретного вендора;
  • пользовательскую сигнатуру для передачи данных ( например, передачи команды на дисплей);
  • функцию инициализации дисплея;

API для передачи данных, в большинстве своем, имеют подобный вид: сигнатура функции содержит одним из аргументов указатель на блок данных, размер передаваемого блока и void* на _pUserData, контекст, который может быть использован под нужды пользователя. Также, имеется функционал для инициализации интерфейса.

В случае Nordic, например, сигнатура функции для инициализации SPI имеет вид:

nrfx_err_t nrfx_spim_init(nrfx_spim_t const * const p_instance,
             nrfx_spim_config_t const * p_config,
             nrfx_spim_evt_handler_t  handler,
             void          * p_context)

где p_context- "контекст" который будет доступен в nrfx_spim_evt_handler_t handler callback-е.

Инициализация интерфейса SPI происходит вызовом nrfx_spim_init:

APP_ERROR_CHECK(
  nrfx_spim_init(&m_spiHandle, &spiConfig, spimEventHandler, this));

Аргументами которой являются:

  • m_spiHandle- структура-дескриптор, которая выглядит следующим образом:
typedef struct
{
  NRF_SPIM_Type * p_reg;    ///< Pointer to a structure with SPIM registers.
  uint8_t    drv_inst_idx; ///< Index of the driver instance. For internal use only.
} nrfx_spim_t;
  • spiConfig- структура с конфигурацией SPI( скорость, используемые порты, etc). Подробнее фрагмент можно посмотреть ссылка на листинг в репозитории
  • spimEventHandler — callback, вызываемый в момент обработки события на шине SPI по завершению приема/передачи.
  • this- контекст, который будет доступен в обработчике события, в текущей реализации — обрабатывает состояние транзакций на шине.

Callback реализован как:

static void spimEventHandler(nrfx_spim_evt_t const* _pEvent,
              void* _pContext) noexcept {
 Meta::UnuseVar(_pContext);
 if (_pEvent->type == NRFX_SPIM_EVENT_DONE) {
  auto pThis = reinterpret_cast<SpiBus::SpiBackendImpl*>(_pContext);
  pThis->m_pSpiBus->handleEvent(
    SpiBus::TCompletedEvent::TransactionCompleted);
 }
}

Передача данных по SPI формируется созданием дескриптора транзакции + инициации передачи данных:

nrfx_spim_xfer_desc_t xferDesc = NRFX_SPIM_XFER_TX(_pBuffer, _bufferSize);
nrfx_err_t transmissionError = nrfx_spim_xfer
    &m_spiHandle
 , &xferDesc
 , 0
);

В общем случае можно объединить вызов, таким образом:

void sendDataChunk(std::uint8_t* _pData,
         std::uint16_t _dataSize,
         void* _pUserContext);

При необходимости передачи данных блоком, ( например, передачи блока команд инициализации на дисплей в неблокирующем режиме), в традиционном варианте необходимо сформировать очередь на отправку, из которой по прерыванию(событию?) доставать новый блок данных и формировать пакет на отправку.

В коде выглядит примерно как:

sendCommand(DisplayReg::SLPOUT);
sendCommand(DisplayReg::COLMOD, 0x55);
sendCommand(DisplayReg::MADCTL, 0x08);

Реализацию sendCommand можно посмотреть по ссылке

Вкратце: помещаем в очередь выполнения SPI - транзакцию с beforeTransaction, transactionAction,afterTransaction.

CoroutineExperiments implementation

В предыдущем разделе был упомянут "объединенный" вызов для запуска передачи данных. Для эмуляции асинхронной передачи данных реализуем HAL-метод следующий образом:

void spiBackendImplTransmit(std::uint8_t* _pBuffer,
              std::uint16_t _bufferSize,
              void* _pUserData) {
 std::thread dmaThread = std::thread([_pUserData] {
  using namespace std::chrono_literals;
  std::this_thread::sleep_for(500ms);
​
  std::cout << "TRANSMIT SOME DATA" << std::endl;
  stdcoro::coroutine_handle<>::from_address(_pUserData).resume();
 });
 dmaThread.detach();
}

Пользовательский метод передачи данных реализуем следующим образом:

auto spiTrasnmitCommandBufferAsync(std::uint8_t* _pBuffer,
                 std::uint16_t _bufferSize) {
 std::cout << "Toggle GPIO ON" << std::endl;
 struct Awaiter {
  std::uint8_t* pBuffer;
  std::uint16_t bufferSize;
​
  bool await_ready() const noexcept { return false; }
  void await_resume() const noexcept {
   std::cout << "Toggle GPIO OFF" << std::endl;
 }
  void await_suspend(stdcoro::coroutine_handle<> thisCoroutine) const {
   spiBackendImplTransmit(pBuffer, bufferSize, thisCoroutine.address());
 }
 };
​
 return Awaiter{.pBuffer = _pBuffer, .bufferSize = _bufferSize};
}
  • bool await_ready() const noexcept - возвращает всегда false, т.к. нам гарантированно надо выполнять операцию для получения результата awaitable.
  • void await_resume() const noexcept - момент "восстановления" сопрограммы;
  • void await_suspend(stdcoro::coroutine_handle<> thisCoroutine) const - функция, вызываемая в момент приостановки сопрограммы.

И получим следующую реализацию функции инициализации дисплея:

auto commandBufferFirst = std::array{0x00u, 0x01u, 0x02u, 0x03u};
auto commandBufferSecond = std::array{0x04u, 0x05u, 0x06u, 0x07u, 0x08u};
​
void initDisplay() {
 co_await spiTrasnmitCommandBufferAsync(
   reinterpret_cast<std::uint8_t*>(commandBufferFirst.data()),
   commandBufferFirst.size());
 co_await spiTrasnmitCommandBufferAsync(
   reinterpret_cast<std::uint8_t*>(commandBufferSecond.data()),
   commandBufferSecond.size());
}

Подробный полный пример доступен тут: Ссылка на репозиторий

Мигаем светодиодом на NRF52832

Имея механизм сопрограмм, и исследовав пример Exploring MSVC Coroutine,

мы можем помигать светодиодом с весьма интересным синтаксисом:

​
void Board::ledToggle() {
 using namespace std::chrono_literals;
 while (true) {
  co_await 300ms;
​
  LOG_DEBUG_ENDL("LED TIMER EXPIRED");
  bsp_board_led_invert(0);
 }
}
​

Для примера, используя API FreeRTOS данная задача решилась бы следующим образом:

xTaskCreate(ledBlinkTask, "ledBlinkTask", 128, NULL, 2, NULL);
​
void ledBlinkTask(void* pvParameters) {
 while (true) {
  vTaskDelay(300 / portTICK_PERIOD_MS);
  bsp_board_led_invert(0);
 }
}

Реализация co_await 300ms схожа с вышеупомянутой статьей с Exploring MSVC Coroutine, а именно следующая:

auto operator co_await(std::chrono::milliseconds _duration) {
 class Awaitable {
 public:
  explicit Awaitable(std::chrono::milliseconds _duration)
   : m_duration{_duration} {}
​
  bool await_ready() const { return false; }
​
  void await_suspend(std::coroutine_handle<> _coroLedHandle) {
   ret_code_t errorCode{};
   errorCode =
     app_timer_start(m_ledDriverTimer, APP_TIMER_TICKS(m_duration.count()),
             _coroLedHandle.address());
   APP_ERROR_CHECK(errorCode);
 }
​
  void await_resume() { app_timer_stop(m_ledDriverTimer); }
​
 private:
  std::chrono::milliseconds m_duration;
 };
 return Awaitable{_duration};
}

Где на await_suspend сопрограммы делается запуск таймера на заданный период(300ms), в обработчике таймера вызывается coroutine_handle.resume() , после которого таймер останавливается в void await_resume().

Полная реализация доступна Ссылка на репозиторий,watchboard.cpp


Report Page