﻿# Отладочный вывод на микроконтроллерах: как Concepts и Ranges отправили мой printf на покой

Здравствуйте\! Меня зовут Александр, и я работаю программистом микроконтроллеров\.

![0944_DebugOutputOnMicrocontrollers_ru/image1.png](https://import.viva64.com/docx/blog/0944_DebugOutputOnMicrocontrollers_ru/image1.png)

Начиная на работе новый проект, я привычно набрасывал в project tree исходники всяческих полезных утилит\. И на хедере _app\_debug\.h_ несколько подзавис\.


> Мы опубликовали и перевели эту статью с разрешения правообладателя\\\. Автор статьи – Александр Сажин \\\(Ник \\\- \[Saalur\]\(https://habr\.com/ru/users/saalur/\), email \\\- a\\\.y\\\.sazhin@gmail\\\.com\\\)\\\. \[Оригинал\]\(https://habr\.com/ru/post/556144/\) опубликован на сайте Habr\\\.

Дело в том, что в декабре прошлого года у [GNU Arm Embedded Toolchain](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads) вышел релиз 10\-2020\-q4\-major, включающий все GCC 10\.2 features, а значит и поддержку Concepts, Ranges, Coroutines вкупе с другими, менее "громкими" новинками С\+\+20\.

Воодушевленное новым стандартом воображение рисовало мой будущий С\+\+ код ультрасовременным и лаконично\-поэтичным\. И старый, добрый _printf\("Debug message\\n"\)_ в это благостное видение не очень\-то вписывался\.

Хотелось бескомпромиссной плюсовой функциональности и стандартных удобств\!

```cpp
float raw[] = {3.1416, 2.7183, 1.618};
array<int, 3> arr{123, 456, 789};

cout << int{2021}       << '\n'
     << float{9.806}    << '\n'
     << raw             << '\n'
     << arr             << '\n'
     << "Hello, Habr!"  << '\n'
     << ("esreveR me!" | views::take(7) | views::reverse ) << '\n';
```

Ну а если хочется хорошего, зачем же себе отказывать?

Реализуем на С\+\+20 интерфейс потока для отладочного вывода МК, поддерживающий любой подходящий протокол, предусмотренный вендром камня\. Легковесный и быстрый, без бойлерплейта\. Поддерживающий как блокирующий посимвольный вывод \- для нечувствительных к времени выполнения участков кода, так и неблокирующий, для быстрых функций\.

Зададим для комфортного чтения кода несколько удобных алиасов:

```cpp
using base_t = std::uint32_t;
using fast_t = std::uint_fast32_t;
using index_t = std::size_t;
```

Как известно, в микроконтроллерах неблокирующие алгоритмы передачи данных реализуются на прерываниях и DMA\. Для идентификации режимов вывода заведем enum:

```cpp
enum class BusMode{
  BLOCKING,
  IT,
  DMA,
};
```

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

**\[НАЧАЛО БЛОКА SPOILER\]**

## class BusInterface

```cpp
template<typename T>
class BusInterface{

public:

  using derived_ptr = T*;
    
  static constexpr BusMode mode = T::mode;

  void send (const char arr[], index_t num) noexcept {

    if constexpr (BusMode::BLOCKING == mode){

      derived()->send_block(arr, num);

    } else if (BusMode::IT == mode){

      derived()->send_it(arr, num);

    } else if (BusMode::DMA == mode){

      derived()->send_dma(arr, num);
    }
  }

private:

  derived_ptr derived(void) noexcept{
    return static_cast<derived_ptr>(this);
  }

  void send_block (const char arr[], const index_t num) noexcept {}

  void send_it (const char arr[], const index_t num) noexcept {}

  void send_dma (const char arr[], const index_t num) noexcept {}
};
```

**\[КОНЕЦ БЛОКА SPOILER\]**

Класс реализован по паттерну [CRTP](https://ru.wikipedia.org/wiki/Curiously_recurring_template_pattern), что дает нам преимущества полиморфизма времени компиляции\. Класс содержит единственный публичный метод _send\(\)_, в котором на этапе компиляции, в зависимости от режима вывода, выбирается нужный метод\. В качестве аргументов метод принимает указатель на буфер с данными и его полезный размер\. На моей практике это самый распространенный формат аргументов в HAL\-функциях вендоров МК\.

И тогда, например, класс _Uart_, наследуемый от данного базового класса, будет выглядеть примерно так:

**\[НАЧАЛО БЛОКА SPOILER\]**

## class Uart

```cpp
template<BusMode Mode>
class Uart final : public BusInterface<Uart<Mode>> {

private:

  static constexpr BusMode mode = Mode;

  void send_block (const char arr[], const index_t num) noexcept{

    HAL_UART_Transmit(
        &huart,
        bit_cast<std::uint8_t*>(arr),
        std::uint16_t(num),
        base_t{5000}
    );
  }
  
  void send_it (const char arr[], const index_t num) noexcept {

    HAL_UART_Transmit_IT(
          &huart,
          bit_cast<std::uint8_t*>(arr),
          std::uint16_t(num)
    );
  }

  void send_dma (const char arr[], const index_t num) noexcept {

    HAL_UART_Transmit_DMA(
          &huart,
          bit_cast<std::uint8_t*>(arr),
          std::uint16_t(num)
    );
  }

  friend class BusInterface<Uart<BusMode::BLOCKING>>;
  friend class BusInterface<Uart<BusMode::IT>>;
  friend class BusInterface<Uart<BusMode::DMA>>;
};
```

**\[КОНЕЦ БЛОКА SPOILER\]**

По аналогии можно реализовать классы и других протоколов, поддерживаемых микроконтроллером, заменив в методах _send\_block\(\)_,_ send\_it\(\)_ и _send\_dma\(\)_ соответствующие функции HAL\. Если протокол передачи данных поддерживает не все режимы, тогда соответствующий метод просто не определяем\.

И в завершении этой части заведем короткие алиасы итогового класса Uart:

```cpp
using UartBlocking = BusInterface<Uart<BusMode::BLOCKING>>;
using UartIt = BusInterface<Uart<BusMode::IT>>;
using UartDma = BusInterface<Uart<BusMode::DMA>>;
```

Отлично, теперь разработаем класс потока вывода:

**\[НАЧАЛО БЛОКА SPOILER\]**

## class StreamBase

```cpp
template <class Bus, char Delim>
class StreamBase final: public StreamStorage
{

public:

  using bus_t = Bus;
  using stream_t = StreamBase<Bus, Delim>;

  static constexpr BusMode mode = bus_t::mode;

  StreamBase() = default;
  ~StreamBase(){ if constexpr (BusMode::BLOCKING != mode) flush(); }
  StreamBase(const StreamBase&) = delete;
  StreamBase& operator= (const StreamBase&) = delete;

  stream_t& operator << (const char_type auto c){

    if constexpr (BusMode::BLOCKING == mode){

      bus.send(&c, 1);

    } else {

      *it = c;
      it = std::next(it);
    }
    return *this;
  }

  stream_t& operator << (const std::floating_point auto f){

    if constexpr (BusMode::BLOCKING == mode){

      auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data());

      bus.send(ptr, cnt);

    } else {

      auto [ptr, cnt] = NumConvert::to_string_float(
        f, buffer.data() + std::distance(buffer.begin(), it));

      it = std::next(it, cnt);
    }
    return *this;
  }

  stream_t& operator << (const num_type auto n){

    auto [ptr, cnt] = NumConvert::to_string_integer( n, &buffer.back() );

    if constexpr (BusMode::BLOCKING == mode){

      bus.send(ptr, cnt);

    } else {

      auto src = std::prev(buffer.end(), cnt + 1);

      it = std::copy(src, buffer.end(), it);
    }
    return *this;
  }

  stream_t& operator << (const std::ranges::range auto& r){

        std::ranges::for_each(r, [this](const auto val) {
            
            if constexpr (char_type<decltype(val)>){
            
                *this << val;

            } else if (num_type<decltype(val)>
       || std::floating_point<decltype(val)>){

                *this << val << Delim;
            }
        });
    return *this;
  }

private:

  void flush (void) {

    bus.send(buffer.data(),
             std::distance(buffer.begin(), it));

    it = buffer.begin();
  }

  std::span<char> buffer{storage};
  std::span<char>::iterator it{buffer.begin()};

  bus_t bus;
};
```

**\[КОНЕЦ БЛОКА SPOILER\]**

Рассмотрим подробнее его значимые части\.

Шаблон класса параметризуется классом протокола, значением Delim типа _char_ и наследуется от класса _StreamStorage_\. Единственная задача последнего \- предоставить доступ к массиву _char_, в котором будут формироваться строки вывода в неблокирующем режиме\. Имплементацию здесь не привожу, она вторична к рассматриваемой теме; оставляю на ваше усмотрение или утяните из моего примера в конце статьи\. Для удобной и безопасной работы с этим массивом \(в примере \- storage\) мы заведем два приватных члена класса:

```cpp
std::span<char> buffer{storage};
std::span<char>::iterator it{buffer.begin()};
```

Delim \- разделитель между значениями чисел при выводе содержимого массивов/контейнеров\.

Публичные методы класса \- это четыре перегрузки _operator<<_\. Три из них \- для вывода базовых типов, с которыми наш интерфейс будет работать \(_char_, _float_ и _integral type_\), а четвертая \- для вывода содержимого массивов и стандартных контейнеров\.

Вот здесь начинается самая вкуснота\.

Каждая перегрузка оператора вывода \- фактически шаблонная функция, в которой шаблонный параметр ограничен требованиями указанного концепта\. Я использую собственные концепты _char\_type_, _num\_type_\.\.\.

```cpp
template <typename T>
concept char_type = std::same_as<T, char>;

template <typename T>
concept num_type = std::integral<T> && !char_type<T>;
```

\.\.\. и концепты из стандартной библиотеки \- _std::floating\_point_ и _std::ranges::range_\.

Концепты базовых типов защищают нас от неоднозначных перегрузок, и в комплексе с концептом range позволяет нам реализовать единый алгоритм вывода для любых стандартных контейнеров и массивов\.

Логика внутри каждого оператора вывода базового типа проста\. В зависимости от режима вывода \(блокирующий / не блокирующий\) мы или сразу отправляем символ на печать, либо формируем в буфере потока строку\. И в момент выхода из функции объект нашего потока разрушается, вызывается деструктор, где приватный метод _flush\(\)_ отправляет заготовленную строку на печать в режиме IT или DMA\.

При конвертации числового значения в массив char\-ов я отказался от известной идиомы с _snprintf\(\)_ в пользу наработок [neiver](http://we.easyelectronics.ru/blog/Soft/2400.html)\. Автор в своих публикациях показывает заметное превосходство предложенных им алгоритмов конвертации чисел в строку как в размере бинарника, так и в скорости преобразования\. Позаимствованный у него код я инкапсулировал в классе _NumConvert_, содержащем методы _to\_string\_integer\(\)_ и _to\_string\_float\(\)_\.

В перегрузке оператора вывода данных массива/контейнера мы с помощью стандартного алгоритма _std::ranges::for\_each\(\)_ пробегаемся по содержимому рэйнджа и если элемент удовлетворяет концепту _char\_type_, выводим строку слитно\. Если же удовлетворяет концептам _num\_type_ или _std::floating\_point_, разделяем значения с помощью заданного значения Delim\.

Ну хорошо, мы тут наворотили шаблонов, концептов и прочей плюсовой тяжелой артиллерии\. Это ж какой длины мы получим ассемблерную портянку на выходе? Посмотрим два примера:

```cpp
int main() {
  
  using StreamUartBlocking = StreamBase<UartBlocking, ' '>;
  
  StreamUartBlocking cout;
  
  cout << 'A'; // 1
  cout << ("esreveR me!" | std::views::take(7) | std::views::reverse); // 2
  
  return 0;
}
```

Выставим флаги компилятора: _\-std\=gnu\+\+20 \-Os \-fno\-exceptions \-fno\-rtti_\. Тогда на первом примере мы получим следующий ассемблерный листинг:

```cpp
main:
        push    {r3, lr}
        movs    r0, #65
        bl      putchar
        movs    r0, #0
        pop     {r3, pc}
```

На втором:

```cpp
.LC0:
        .ascii  "esreveR me!\000"
main:
        push    {r3, r4, r5, lr}
        ldr     r5, .L4
        movs    r4, #5
.L3:
        subs    r4, r4, #1
        bcc     .L2
        ldrb    r0, [r5, r4]    @ zero_extendqisi2
        bl      putchar
        b       .L3
.L2:
        movs    r0, #0
        pop     {r3, r4, r5, pc}
.L4:
        .word   .LC0
```

На мой взгляд, весьма неплохо\. Мы получили привычный плюсовой интерфейс потока, удобный вывод числовых значений, контейнеров/массивов, обработку рэнджей прямо в сигнатуре вывода и все это с фактически нулевым оверхедом\.

Конечно же, при выводе числовых значений, добавится еще код конвертации числа в строку\.

Потестировать онлайн можно [здесь](https://gcc.godbolt.org/z/eYbdd3hxq) \(hardware dependent код заменил для наглядности на _putchar\(\)_ \)\.

Рабочий код проекта смотрите/забирайте [отсюда](https://github.com/ALSCode/BareMetalStream/tree/master)\. Там реализован пример из начала статьи\.

Это стартовый вариант, для уверенного использования еще требуются некоторые доработки и тесты\. Например, нужно предусмотреть механизм синхронизации при неблокирующем выводе \- когда, скажем, вывод данных предыдущей функции еще не завершен, а мы в следующей функции уже переписываем буфер новой информацией\. Также нужно еще внимательно поэкспериментровать с алгоритмами _std::views_\. Например _std::views::drop\(\)_ при применении ее к строковому литералу или массиву char\-ов, взрывается ошибкой "inconsistent directions for distance and bound"\. Ну что ж, стандарт новый, со временем освоим\.

Как это работает можно посмотреть [здесь](https://www.youtube.com/watch?v=f6rMz3c4b4E)\. Проект поднят на двухядерном STM32H745; с одного ядра \(480МГц\) вывод идет в блокирующем режиме через отладочный интерфейс SWO, код примера выстреливается за 9,2 мкс, со второго\(240МГц\) \- через Uart в режиме DMA, примерно за 20 мкс\.

Как\-то так\.

Спасибо за внимание, буду рад отзывам и замечаниям, а также идеям и примерам, как это безобразие можно улучшить\.