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

Самые частые грабли при использовании printf в программах под микроконтроллеры

Время на прочтение8 мин
Количество просмотров23K
Всего голосов 42: ↑38 и ↓4+34
Комментарии26

Комментарии 26

Честно говоря не совсем понятно, зачем вообще нужно совмещать форматирование строки и отправку её в сеть.
Я бы отдельно занялся бы созданием форматированной строки, например старый добрый sprintf использовал, или другие с++ аналоги (std::stringstream).
А потом бы отдельно отправлял строчку в сеть.

По отдельности эти задачи выглядят в разы проще.
Честно говоря не совсем понятно, зачем вообще нужно совмещать форматирование строки и отправку её в сеть.

У меня не стояло задачи отправки строки в сеть. Возможность перепаковывать — просто дополнительный презент.
Я бы отдельно занялся бы созданием форматированной строки, например старый добрый sprintf использовал, или другие с++ аналоги (std::stringstream).

Согласен. Но уже стакнулся, что некоторые библиотеки (особенно бесит, когда данные производителем в бинарном виде), используют printf для логирования данных.
Что бы не выделять отдельный буфер для отформатированной строки. И не тратить флеш на стрингстрим, если он нужен только в одном месте. И в целом что бы сохранять больше контроля над тем что происоходит под капотом.
Что-то сомневаюсь, что на на отсутствии буфера можно что-то вразумительное выиграть.

Как правило в программах узкие места совершенно в других местах
Само собой, что это далеко не последняя проблема в проекте. Но, ИМХО, не стоит усугублять оставляя без внимания еще и это.
Что бы не выделять отдельный буфер для отформатированной строки.

void myPrintf() {
  char buf[размер]; // вот он здесь выделяется в стеке и потом освобождается.

Выделять же буфер размером в 2к наверное это перебор. Здесь требуется разумный подход к буферизации.
В принципе можно использользовать vsnprintf для написания своей myPrintf для вот такого использования:
Serial << "лала=" << myPrintf("%04d",myVar) << ", блабла" << endl;
НЛО прилетело и опубликовало эту надпись здесь
Так в статье так и происходит ведь. Мы набираем в буфер данные по байтам (подменяя как нам хочется \n\r. А то разные библиотеки любят по-разному каретку переводить) и затем вызываем метод объекта, который отправляет всю посылку за раз.

Спасибо за статью!
Интересно, а каков истинно-верный способ перенаправления printf?
Скажем, в Кейле очень часто используется переопределение fputc, а не _write.

Интересно, а каков истинно-верный способ перенаправления printf?

Думаю, тут все на вкус… Keil, на сколько я помню (могу ошибаться) использует свои библиотеки и свой компилятор (который отличается значительно от gcc). Если не прав, то прошу поправить. Так что тут сложно сказать. Давно не работал с keil.

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

Не совсем. В статье я упоминал, что "… если вы используете newlib-nano...". Интерфейсы верхнего уровня — да. Одинаковые (в большинстве своем). А вот «внизу», каждый как сам захочет (на сколько мне известно, это не регламентируется).

Просто и _write и fputc вроде как стандартные. Я правда не знаю, кто из них "ниже".

НЛО прилетело и опубликовало эту надпись здесь
Чувствуется опыт, но к подаче есть вопросы:
1) если это tutorial, то чему он меня учит?
2) есть введение, должно быть заключение

Если не нравятся готовые printf.c, xprint и прочие, то в чем сложность написать его самому, если не полный, то лайтовый. Или доработать указанные выше? Плавающая точка, в принципе, несложно делается.
1) если это tutorial, то чему он меня учит?

Как запустить то, что работает под «большой ОС» из коробки на МК без лишних затрат и проблем с надежностью.
есть введение, должно быть заключение

Что-то как-то не подумал об этом. Но оно было бы бессмысленно — уровня «я привел самые частые грабли и краткие методы их решения».
Если не нравятся готовые printf.c, xprint и прочие, то в чем сложность написать его самому, если не полный, то лайтовый. Или доработать указанные выше? Плавающая точка, в принципе, несложно делается.

Сами функции всем устраивают. Вообще не люблю велосипеды по возможности писать. Не так давно писал статью о том, как можно запихать как можно больше стандартных библиотек в МК чтобы не писать своего, пусть и в ущерб памяти. Сейчас отрабатываю вариант комфортной отладки без лишней перепрошивки, но удобнее чем в той статье. Опишу, как обнаружу основные изъяны (если таковые будут).
Во всяких Cortex M0 — велосипеды часто вещь необходимая. Кроме того, не научишься ходить — не научишься бегать. Как говорится, от велосипедов — к велозаводам.
Честно сказать, признаю велосипеды только на уровне аппаратных багов периферии. И то стараюсь это в обертки заворачивать. Подменяя стандартные методы. Чтобы было понятно, зачем я пишу «магическую константу» по «магическому адресу». Порой приходится городить целые объекты-агрегаторы, которые пишут «магические массивы» в «магические регистры».
Из vsprintf можно сделать любой свой printf.
Например, вот для вывода на дисплей LCD в заданных координатах:
#include <stdarg.h>
#define PRINTF_BUF 21
void printAt(int c, int r, const char *s, ...) {
  char buf[PRINTF_BUF];
  va_list ap;
  va_start(ap, s);
  lcd.setCursor(c, r); // Для аурта не нужно
  vsnprintf(buf, sizeof(buf), s, ap);
  lcd.print(buf); // Вот здесь Serial.print(buf) можно поставить.
  va_end(ap);
}

Соответственно, можно сделать SerPrintf(const char *s, ...), которая будет выводить в уарт и не переопределять вообще ничего.
Полезная штука, не знал. Спасибо. Однако ее можно применять только в своем коде, при условии, что никакая из библиотек в составе проекта не использует printf (иначе все равно придется переопределять и это теряет всякий смысл). А код библиотек я стараюсь не править, чтобы при любом обновлении без проблем обновить субмодуль с библиотекой.
Благодарю за статью! добавил в избранное. Рад на разжевывание со ссылками на примеры и туториалы. Продолжайте в том же духе, буду рад новым статьям.
Так всё такие не понятно? как именно лечить проблему хардфолта при использовании float в задаче FreerRtos?
Дано: stm32f103 + freertos (ide: atollic true studio);
Задача: выводить раз в 10 секунд в UART строку сгенерированную sprintf с float значением. Функция как таск freertos; размер таска стека было от 128 до 1024 (-u_printf_float в линкере прописан);
проблема: либо сразу либо после 5-10 выполнений hard fault МК
Гугление навело на этот пост, но конкретно «как решить» тут нет. Понимаю что проблема со стеком.
Так, как решить?
Тут есть несколько вариантов. Не так давно сталкивался с этим снова. На новом arm-none-eabi (9-я версия. Последняя на момент написания комментария).
Убедитесь, что:
  1. Стека задачи достаточно (тут я так понял вы используете snprintf. Пишете в буфер и потом буфер отсылаете. Ради интереса можно задать буфер сильно больше нужного.
  2. В прерываниях не происходит переполнения стека. Как стека задачи, в которой произошло прерывание, так и соседних задач. Может у вас есть задача, которая сбрасывает wdt, а в ней стек на 128 элементов. Появляется прерывание. Пишет 4 килобайна, вы возвращаетесь в задачу wdt, планировщик меняет задачу на другую и тут все падает.
  3. У вас достаточно места в «куче». Имею ввиду не freertos кучу, а та, что «не занятое место» между буферной зоной стека и bss областью. Ее пользует функция _sbrk. Она считает, что место от конца bss области до начала зарезервированного стека (физический конец оперативной памяти) принадлежит ей (функции _sbrk). Тут надо оставить хотя бы 1 кб. Реально используется около 500 байт, когда вы устанавливаете например единый буфер.
  4. Можно явно задать буфер, с которым будет работать printf/scanf. setvbuf функцией (помните, что когда вы их вызовите, случится еще дерганье sbrk функции, которая сохъст 500 байт в «куче» (та что не принадлежит freertos). Кстати. Устанавливать буферы надо до запуска планировщика.

Если это соблюдено, то можно попробовать:
Упростил себе задачу: разложил modf децимальную и фрактальную части (до и после точки). Децимальную привёл к инту, а фрактальную умножил на 100 (мне надо два знака) и тоже привёл к инту. и эти две переменные уже вывожу на экран(уарт и т.п).

Есть ли возможность добавить в текст некоторый ликбез по синтаксису и семантике языка *.ld файла?
Что значат эти точки, звёздочки, :, +=, функции AT() KEEP() ALIGN() > ,{,} и т.п. ?
Как отлаживать *.ld файлы? Как проверять, что *.ld скрипт правильно отработал?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории