Что такое fprintf в си
fprintf_s, _fprintf_s_l, fwprintf_s, _fwprintf_s_l
Печатает форматированные данные в поток. Это версии функций fprintf, _fprintf_l, fwprintf, _fwprintf_l с усовершенствованной безопасностью, как описано в разделе Функции безопасности в CRT.
Синтаксис
Параметры
вышестоящий
Указатель на структуру FILE.
format
Строка управления форматом.
argument_list
Необязательные аргументы для строки формата.
locale
Используемый языковой стандарт.
Возвращаемое значение
fprintf_s возвращает число записанных байтов. fwprintf_s возвращает число записанных расширенных символов. В случае ошибки вывода каждая из этих функций возвращает отрицательное значение.
Комментарии
fprintf_s форматирует и выводит последовательность символов и значений в потоквывода. Каждый аргумент в argument_list (при его наличии) преобразуется и выводится в соответствии с соответствующей спецификацией формата в формате. Аргумент Format использует синтаксис спецификации формата для функций printf и wprintf.
fwprintf_s — это версия fprintf_sдля расширенных символов; в fwprintf_sFormat — строка расширенных символов. Эти функции ведут себя одинаково, если поток открыт в режиме ANSI. fprintf_s в настоящее время не поддерживает вывод в поток Юникода.
Версии этих функций с суффиксом _l идентичны за исключением того, что они используют переданный параметр языкового стандарта вместо текущего языкового стандарта.
Убедитесь, что format не является строкой, определяемой пользователем.
Как и небезопасные версии (см. раздел fprintf, _fprintf_l, fwprintf, _fwprintf_l), эти функции проверяют свои параметры и вызывают обработчик недопустимых параметров, как описано в разделе Проверка параметров, если поток или Формат является пустым указателем. Также проверяется сама строка формата. При наличии любых неизвестных или неправильно сформированных описателей форматирования эти функции создают исключение недопустимого параметра. Во всех случаях, если выполнение может быть продолжено, функции возвращают значение-1 и задают значениееинвал. Дополнительные сведения об этих и других кодах ошибок см. в разделе _doserrno, errno, _sys_errlist и _sys_nerr.
Универсальное текстовое сопоставление функций
Подпрограмма TCHAR.H | &Не определено _UNICODE _MBCS | _MBCS определено | _UNICODE определено |
---|---|---|---|
_ftprintf_s | fprintf_s | fprintf_s | fwprintf_s |
_ftprintf_s_l | _fprintf_s_l | _fprintf_s_l | _fwprintf_s_l |
Дополнительные сведения см. в разделе Спецификации формата.
Требования
Функция | Обязательный заголовок |
---|---|
fprintf_s, _fprintf_s_l | |
fwprintf_s, _fwprintf_s_l | или |
Дополнительные сведения о совместимости см. в статье Compatibility.
Функция fprintf(), printf() в Си и подобные ей функции
Почти в каждом коде есть функция printf(). Эта и родственные ей функции пригодятся вам и при работе с файлами.
С помощью функций семейства printf() в Си можно записывать сформатированный текст прямо в файлы на диске.
Для этого откройте файл путем вызова функции fopen(), используя один из режимов записи. Затем вызовите функцию fprintf() аналогично тому, как вы вызывали функцию printf(), но в качестве первого аргумента задайте переменную типа FILE *.
Например, следующий оператор запишет в файл переменную d типа double, сформатированную в восьми позициях с двумя знаками после запятой:
Особенно полезна функция sprintf() — строковая версия printf() в Си — Объявите строковый буфер для хранения результата, а затем вызовите ее так:
Если d — переменная типа double, то этот оператор поместит в буфер строковое представление переменной d, оканчивающееся нулевым байтом.
Данный код демонстрирует практическое применение функции fprintf() — добавление номеров строк в текстовые файлы.
Скомпилируйте вместе модули. Затем запустите программу и введите имя текстового файла и имя выходного файла, в который будут записаны строки с номерами.
Программа — хорошая иллюстрация принципов обработки файлов, описанных ранее. Кроме того, программа демонстрирует несколько новых приемов.
В строке 13 объявляются две переменных типа char *: inpfname (имя входного файла) и outfname (имя выходного файла). Этим переменным присваивается адреса строк, которые вы введете в ответ на приглашение.
После создания имен файлов, а также открытия входного и выходного файлов (строки 25-30) программа выполняет цикл while (строки 34- 37). В цикле вызывается функция fgets() в Си. Для чтения строк из входного файла, а также функция fprintf(), которая добавляет номер в начало каждой строки и записывает их в выходной файл.
Строка 36 отображает на экране точку для каждой обработанной строки — хороший метод обратной связи при длительном выполнении задачи, который обеспечивает уверенность в том, что программа работает.
Функция perror() (строка 53) демонстрирует удобный способ отображения сообщений об ошибках для различных функций ввода-вывода. Большинство функций устанавливает внутреннюю переменную errno равной значению, представляющему одну или несколько ошибок. Вызов функции реггог() с любым строковым аргументом отобразит эту строку с описанием аварийной ситуации.
fprintf
Writes the C string pointed by format to the stream. If format includes format specifiers (subsequences beginning with %), the additional arguments following format are formatted and inserted in the resulting string replacing their respective specifiers.
After the format parameter, the function expects at least as many additional arguments as specified by format.
Parameters
stream Pointer to a FILE object that identifies an output stream. format C string that contains the text to be written to the stream.
It can optionally contain embedded format specifiers that are replaced by the values specified in subsequent additional arguments and formatted as requested.
A format specifier follows this prototype:
Where the specifier character at the end is the most significant component, since it defines the type and the interpretation of its corresponding argument:
specifier | Output | Example |
---|---|---|
d or i | Signed decimal integer | 392 |
u | Unsigned decimal integer | 7235 |
o | Unsigned octal | 610 |
x | Unsigned hexadecimal integer | 7fa |
X | Unsigned hexadecimal integer (uppercase) | 7FA |
f | Decimal floating point, lowercase | 392.65 |
F | Decimal floating point, uppercase | 392.65 |
e | Scientific notation (mantissa/exponent), lowercase | 3.9265e+2 |
E | Scientific notation (mantissa/exponent), uppercase | 3.9265E+2 |
g | Use the shortest representation: %e or %f | 392.65 |
G | Use the shortest representation: %E or %F | 392.65 |
a | Hexadecimal floating point, lowercase | -0xc.90fep-2 |
A | Hexadecimal floating point, uppercase | -0XC.90FEP-2 |
c | Character | a |
s | String of characters | sample |
p | Pointer address | b8000000 |
n | Nothing printed. The corresponding argument must be a pointer to a signed int. The number of characters written so far is stored in the pointed location. | |
% | A % followed by another % character will write a single % to the stream. | % |
The format specifier can also contain sub-specifiers: flags, width, .precision and modifiers (in that order), which are optional and follow these specifications:
flags | description |
---|---|
- | Left-justify within the given field width; Right justification is the default (see width sub-specifier). |
+ | Forces to preceed the result with a plus or minus sign (+ or -) even for positive numbers. By default, only negative numbers are preceded with a - sign. |
(space) | If no sign is going to be written, a blank space is inserted before the value. |
# | Used with o, x or X specifiers the value is preceeded with 0, 0x or 0X respectively for values different than zero. Used with a, A, e, E, f, F, g or G it forces the written output to contain a decimal point even if no more digits follow. By default, if no digits follow, no decimal point is written. |
0 | Left-pads the number with zeroes (0) instead of spaces when padding is specified (see width sub-specifier). |
width | description |
---|---|
(number) | Minimum number of characters to be printed. If the value to be printed is shorter than this number, the result is padded with blank spaces. The value is not truncated even if the result is larger. |
* | The width is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted. |
.precision | description |
---|---|
.number | For integer specifiers (d, i, o, u, x, X): precision specifies the minimum number of digits to be written. If the value to be written is shorter than this number, the result is padded with leading zeros. The value is not truncated even if the result is longer. A precision of 0 means that no character is written for the value 0. For a, A, e, E, f and F specifiers: this is the number of digits to be printed after the decimal point (by default, this is 6). For g and G specifiers: This is the maximum number of significant digits to be printed. For s: this is the maximum number of characters to be printed. By default all characters are printed until the ending null character is encountered. If the period is specified without an explicit value for precision, 0 is assumed. |
.* | The precision is not specified in the format string, but as an additional integer value argument preceding the argument that has to be formatted. |
The length sub-specifier modifies the length of the data type. This is a chart showing the types used to interpret the corresponding arguments with and without length specifier (if a different type is used, the proper type promotion or conversion is performed, if allowed):
specifiers | |||||||
---|---|---|---|---|---|---|---|
length | d i | u o x X | f F e E g G a A | c | s | p | n |
(none) | int | unsigned int | double | int | char* | void* | int* |
hh | signed char | unsigned char | signed char* | ||||
h | short int | unsigned short int | short int* | ||||
l | long int | unsigned long int | wint_t | wchar_t* | long int* | ||
ll | long long int | unsigned long long int | long long int* | ||||
j | intmax_t | uintmax_t | intmax_t* | ||||
z | size_t | size_t | size_t* | ||||
t | ptrdiff_t | ptrdiff_t | ptrdiff_t* | ||||
L | long double |
Note that the c specifier takes an int (or wint_t) as argument, but performs the proper conversion to a char value (or a wchar_t) before formatting it for output.
Note: Yellow rows indicate specifiers and sub-specifiers introduced by C99. See «> for the specifiers for extended types.
. (additional arguments) Depending on the format string, the function may expect a sequence of additional arguments, each containing a value to be used to replace a format specifier in the format string (or a pointer to a storage location, for n).
There should be at least as many of these arguments as the number of values specified in the format specifiers. Additional arguments are ignored by the function.
Return Value
On success, the total number of characters written is returned.
If a writing error occurs, the error indicator (ferror) is set and a negative number is returned.
If a multibyte character encoding error occurs while writing wide characters, errno is set to EILSEQ and a negative number is returned.
Форматный вывод на Си для микроконтроллеров.
Форматированный ввод-вывод применяется очень широко, в первую очередь это, конечно, взаимодействие с пользователем, а так-же отладочный вывод, логи, работа с различными текстовыми протоколами и многое другое. В этой статье рассматриваются особенности применения форматированного вывода (ввод оставим на потом) для микроконтроллеров.
Первая программа написанная для ПК традиционно называется «Hello, world» и естественно пишет в стандартный вывод эту знаменитую фразу:
Первая программа для микроконтроллера обычно зовётся «Blinky» и она просто мигает светодиодом. Дело в том, что заставить работать традиционный «Hello, world» на микроконтроллере не так уж и просто для начинающего. Во первых, нет стандартного устройства вывода и его функциональность ещё нужно реализовать. Во вторых, не всегда бывает очевидно как подружить стандартные функции вывода с целевым устройством. Ситуация усугубляется тем, что в каждом компиляторе (тулчейне) это делается каким-то своим способом.
Форматный вывод.
Что-же, в общем, за зверь такой форматный вывод? Упрощенно говоря, это — вывод значений различных типов в виде текстовых полей.
Текстовое поле состоит из собственно значения, преобразованного в строку символов, и заполняющих символов для получения нужной ширины поля. Заполняющие символы могут находится слева или справа от значения, с помощью этого получается выравнивание по правому или левому краю соответственно. Заполнение также может находится внутри значения между определёнными его элементами, например между знаком и числом (как на рисунке), или между базой шестнадцатеричного числа (0x) и числом.
В качестве заполняющего символа обычно используется пробел, однако иногда могут использоваться нули, например в качестве ведущих нулей, или ещё что-нибудь.
Таким образом, форматированный вывод это — преобразование значений различных типов в текстовую форму и вывод их с определённым выравниванием и определённым заполнением.
Требования и особенности ввода-вывода для МК.
1. Гибкость. В отличии от старших братьев, для МК нет и не может быть стандартного устройства ввода-вывода. В каждой системе будет что-то своё, с уникальными требованиями и особенностями. В одной системе будет вывод в USART, во второй — на ЖК дисплей, в третей — в файл на SD карточке, в четвёртой — всё сразу. Значит система форматированного ввода-вывода должна быть достаточно гибкой, настраиваемой и независимой от аппаратных особенностей целевой платформы.
2. Требования к ресурсам. В МК ресурсов бывает мало и очень мало. В идеале, в прошивку МК должна попасть только та функциональность, которая реально используется. Скорость и размер кода имеют значение, под час решающее. Если мы не используем вывод типов с плавающей точкой, то и в полученной прошивке не должно быть кода, который его осуществляет.
3. Стойкость к ошибкам кодирования. В идеале, хорошо было-бы если ошибка кодирования приводила бы сразу к ошибке компиляции, ну или по крайней мере к предупреждению. Чтоб не надо было заливать программу в железо и ходить отладчиком вылавливать место, где там, в функцию предался неправильный аргумент.
4. Доступность. Библиотека ввода-вывода должна быть в наличии для целевой платформы.
5. Функциональность часто ставится на последнее место в угоду скорости и компактности.
Стандартная библиотека Си.
Стандартным и единственно доступным средством форматированного вывода в Си является семейство функций: printf, sprintf, snprintf и fprintf.
Рассматривать будем функцию printf, как наиболее типичного и часто используемого представителя функций форматного вывода, при необходимости переходя к другим. Итак фунция printf имеет следующий синтаксис:
Это обычная функция с переменным числом параметров. Здесь первый аргумент fmt является форматной строкой, которая содержит, как обычный текст, так и специальные управляющие последовательности. Троеточие (. ) обозначает список дополнительных параметров, их может быть от нуля и до сколько поместится в стеке. Да, да все параметры в функцию printf передаются преимущественно в стеке (за исключением архитектуры ARM, где первые 4 параметра передаются в регистрах). Запихивает их в стек вызывающая функция, она-же инициализирует кадр стека перед вызовом и очищает после. Сама printf узнать сколько и каких параметров ей было передано может узнать только из форматной строки. При передачи параметров размером меньше (signed char, unsigned char, char и на некоторых платформах signed/unsigned short), чем int, они будут расширенны соответствующим расширением(знаковым или без-знаковым) до int-a. Это сделано для того, чтобы стек был всегда выровнен по границе машинного слова, а так-же уменьшает количество возможных ошибок при нестыковке размера фактического параметра и ожидаемого из форматной строки. Так-же параметры типа float при передаче в функции с переменным числом аргументов, приводятся к типу double.
Форматная строка содержит как символы непосредственно выводимые в поток, так и специальные управляющие последовательности, которые начинаются со знака «%». Управляющие последовательности имеют следующий формат:
Единственным обязательным элементом здесь является спецификатор, который определяет интерпретацию типа соответствующего параметра и может принимать следующие значения:
Флаги определяют дополнительные параметры форматирования (их может быть несколько):
Ширина — десятичное число, задаёт минимальное количество символов выводимых для соответствующего параметра. Если выводимое значение содержит меньше символов, чем указано, то оно будет дополнено пробелами (или нулями если есть флаг «0») до нужной ширины слева или справа, если указан флаг «-«. Например:
Если вместо десятичного числа указать «*», то значение ширины будет считанно из дополнительного целочисленного параметра. Это позволяет задавать значение ширины поля вывода из переменной:
Здесь первый раз i передаётся в качестве ширины поля, второй — значения.
Точность или длинна — десятичное число после знака точки «.». В случае вывода целых чисел этот элемент означает минимальное количество записанных знаков, если выводимое число короче указанной длинны, то оно дополняется ведущими нулями, если число длиннее, то оно не урезается.
Таким образом есть уже два способа вывести целое с нужным количеством ведущих нулей.
Для чисел с плавающей точкой в форматах «e», «E» и «f» этот элемент означает число знаков после десятичной точки. Результат округляется.
Для форматов «g» и «G» это — общее количество выведенных значимых цифр.
Для строк «s» этот элемент называется длинна и означает максимальное количество выведенных символов, обычно строки выводятся пока не встретится нулевой завершающий символ.
Также как и в элемента «ширина», в место точности можно поставить звёздочку и передать её значение в виде дополнительного целочисленного параметра:
Дополнительный модификатор служит для указания размерности типа:
h — применяется со спецификаторами i, d, o, u, x и X для знаковых и без-знаковых коротких целых short и unsigned short).
l — совместно со спецификаторами i, d, o, u, x и X означают длинные целые long и unsigned long).
l — совместно со спецификаторами s и c «широкие» многобайтные строку и символ соответственно.
L — обозначает тип long double, применяется со спецификаторами e, E, f, g и G.
В компиляторах поддерживающих тип long long, таких как GCC и IAR, часто есть для него нестандартный модификатор ll.
В стандарте С99 добавлены модификаторы «t» и «Z» для типов ptrdiff_t и size_t соответственно.
Работа над ошибками
Основным недостатком функций семейства printf считается вовсе не громоздкость и неторопливость — размер кода и накладные расходы на запихивание параметров в стек и разбор форматной строки обычно считаются приемлемыми, а подверженность ошибкам кодирования. Самое неприятное в этих ошибках то, что они могут быть неочевидными и проявляться не всегда, или не на всех платформах.
Большинство ошибок связано с несоответствием спецификаторов указанных в форматной строке с количеством и типами фактических аргументов. При этом можно выделить следующие ситуации:
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров совпадают с ожидаемыми. В этом случае просто выведутся параметры указанные в форматной строке, а лишние будут проигнорированы.
— занимаемый в стеке размер параметров ожидаемых из форматной строки меньше размера фактически переданных. Типы фактические параметров не совпадают с ожидаемыми. В этом случае параметры просто будут интерпретированы в соответствии с форматной строкой и в общем случае будет выведен мусор.
— размер фактических параметров меньше ожидаемых. Здесь поведение не определено и зависит от кучи разных факторов — от платформы, от компилятора, от содержимого стека на момент вызова printf. Поведение printf при этом может быть от внешне корректной работы и до чтения и даже записи произвольных участков памяти со всеми вытекающими последствиями.
Многие ошибки возникают при переносе кода с одной платформы на другую у которых отличаются размеры типов. Например:
На платформах, где int имеет 32 бита этот код работает правильно, а где int — 16 бит — будут выведены только 2 младших или старших (в зависимости от порядка следования байт) байта.
К счастью некоторые компиляторы, например GCC, знают printf «в лицо» и выдают предупреждения в случае несовпадения фактических параметров с форматной строкой. Однако это работает только если форматная строка задана литералом. А если она хранится в переменной (например extern указатель инициализируемый в другом модуле), то компилятор бессилен и проследить соответствеи параметров может быто очень не просто.
Особенности реализаций
AVR-GCC он-же WinAVR
В AVR-GCC, а точнее в avr-libc самая удачная, на мой взгляд, реализация стандартной библиотеки ввода-вывода Си. В ней имеется возможность выбирать необходимый функционал функций семейства printf. Они прекрасно работают со строками как RAM так и во Flash. Все функции семейства, включая snprintf и fprintf разделяют общий код и очень хорошо оптимизированы как по скорости, таки по объёму кода.
Для поддержки находящихся во Flash памяти строк введен новый спецификатор форматной строки %S — S — заглавная, строчная s по-прежнему означает строку в RAM. Но во Flash памяти может быть и сама форматная строка, для этого есть специальные модификации функций с суффиксом «_P» printf_P, fprintf_P, sprintf_P и т. д., которые ожидают форматную строку во Flash.
Для того чтобы printf заработала, нужно написать функцию вывода символа и определить файловый дескриптор стандартного вывода.
Помимо stdout есть стандартные дескрипторы stdin и stderr для стандартного ввода и стандартного вывода ошибок соответственно. Используя функцию fprintf можно явно указывать нужный файловый дескриптор и при необходимости можно определить сколько угодно устройств вывода:
В avr-libc имеется три уровня функциональности библиотеки ввода-вывода:
1. нормальный — поддерживается вся упомянутая выше функциональность, кроме вывода чисел с плавающей точкой. Этот режим включен по умолчанию и каких либо опций для него указывать не надо. Поддержка чисел с плавающей точкой занимает много места, а нужна сравнительно редко. Приведенный выше пример, скомпилированный с этим уровнем функциональности, занимает порядка 1946 байт Flash памяти.
2. минимальный — поддерживаются только самые базовые вещи: целые, строки, символы. Флаги, ширина поля и точность, если они есть в форматной строке, разбираются корректно, но игнорируются, поддерживается только флаг «#». Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash памяти. Включается указанием в командной строке компоновщика («Linker options» в AVRStudio, а не компилятора, как расплывчато указано в документации avr-libc) следующих опций:
3. максимальный — полная функциональность, включая поддержку чисел с плавающей точкой. Включается опциями
Скомпилированный пример занимает при этом 3488 байт.
Функции семейства printf из avr-libc не используют буферизацию, не считая буферов для конвертации чисел, и выводят символы в устройство по мере их обработки. Поэтому, если буферизация нужна, то ее можно реализовать самостоятельно в функции вывода символа, там можно реализовать и кольцевой буфер и все, что угодно. Также в этих функциях не используется динамическая память, что в нашем случае очень хорошо, зато активно используется стек. Попробуем определить максимальное использование стека в них. Для этого в приложенном архиве заходим в каталог AvrGccPrintf, компилируем проект посредством AVRStudio и запускаем симуляцию с помощью runSimul.cmd.
После открывает образовавшийся файл trace.log, находим значение указателя стека (SP) после входа в main (после пролога), находим минимальное значение SP (стек растёт вниз) и вычитаем из первого второе. У меня получилось 0x455 — 0x429 = 0x2c = 44 байта использует сама функция fprintf, плюс еще 8 байт в стеке занимают ее параметры, итого 52 байта. Еще 14 байт занимает один файловый дескриптор и ещё 6 байт три стандартных указателя на файловые дескрипторы (stdout, stdin, stderr). Итого 72 байта RAM только на вызов fprintf, без учета всего остального.
Также из файла trace.log можно узнать общее время выполнения функций и где процессор проводит его больше всего.
Подробнее о стандартной библиотеке ввода-вывода avr-libc можно здесь:
www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html
IAR for AVR
Здесь есть несколько версий Си-шных библиотек, с отличающейся функциональностью:
— CLIB — относительно маленькая, но ограниченная библиотека. Нет поддержки файловых дескрипторов, локалей и многобайтных символов. Считается устаревшей.
— Normal DLIB — более новая. Так-же нет поддержки файловых дескрипторов, локалей и многобайтных символов, но есть некоторые плюшки из стандарта С99.
— Full DLIB — полная библиотека Си. Поддерживает всё согласно стандарту С99, но при этом очень объёмная. Рассматривать этот вариант не буду, так, как размер функции printf отсюда превышает доступные 4 Кб кода для бесплатной версии IAR KickStart for AVR.
В IAR имеется возможность выбирать возможности для printf. Для этого заходим с меню Project->Options далее в диалоге General Options->Library Configuration. В списке «Printf formatter» можно выбрать необходимый уровень функционала. Для CLIB это Large, Small, Tiny, для DLIB добавляется еще Full. По возможностям эти уровни примерно соответствуют аналогичным в avr-gcc, поэтому расписывать их не буду.
Для того чтобы printf заработала, надо определить функцию вывода символа:
Разберём полный пример. Он также предназначен для запуска на симуляторе.
Тут выясняются две неприятные особенности. Во-первых функции printf и printf_P видимо не разделяют общий код и printf_P всегда использует максимальный форматтер, независимо от того, что выбрали в настройках для printf, занимая порядка 3500 байт кода. Поэтому приведенный пример не помещается в четыре бесплатных килобайта. Для проверки одну из функций надо закомментировать.
Во-вторых, ни printf, ни printf_P не умеют читать строки из flash памяти — для них нет спецификатора.
Размеры printf для различных уровней функциональности примерно соответствуют аналогичным у avr-gcc, где чуть меньше, где чуть больше — непринципиально. А вот использование стека в разы выше, минимальный размер стека данных, при котором printf заработала, составил 200 байт для DLIB и около 150 для CLIB. Так, что на контроллерах с 2 кб flash, 128 RAM использовать эти функции не получится.
Демо-проект находится в каталоге AvrIarPrintf. Для запуска симуляции, а точнее преобразования генерируемого IAR-ом hex-а в пригодный для потребления simulavr-ом elf, на машине должен быть установлен WinAVR (и прописан в переменной окружения PATH, естественно).
Mspgcc
В стандартной библиотеке mspgcc для форматного вывода реализованы только функции printf и sprintf, плюс еще нестандартная функция uprintf, которая принимает указатель на функцию вывода символа в качестве первого аргумента. Файловых дескрипторов там нет и в помине, выбирать уровень функциональности форматтеров тоже нельзя. При этом printf «весит» порядка двух килобайт так, что запустить её, скажем, на MSP43 Launchpad-е не получится.
Для работы printf нужно, вполне ожидаемо, определить функцию int putchar(int c):
IAR for MSP430, IAR for ARM и может еще какой IAR
Вообще в реализациях стандартной библиотеки от IAR Systems всё довольно однообразно для различных платформ, что не может не радовать. Как правило есть минимум две версии стандартной библиотеки — одна урезанная без поддержки файловых дескрипторов, вторая — полная, соответственно, с их поддержкой. Зовутся они как правило «Normal» и «Full» соответственно. Так-же в каждой из них можно выбирать необходимый функционал разбора форматной строки, поддерживаемый функциями семейства printf. Варианты уже уже описаны для IARfor AVR: Large, Small, Tiny и Full.
Если выбрать вариант библиотеки без файловых дескрипторов, то для работы функций вывода нужно определить лишь функцию int putchar(int outchar).
Если используем вариант с дескрипторами, то определить нужно функции __write, __lseek, __close и remove.
Минимальная их реализация, например, для STM32 может выглядеть так:
ARM + NewLib
Большинство сборок GCC под ARM используют NewLib в качестве стандартной библиотеки Си. Это достаточно взрослый проект и хорошо соответствует Си-шным стандартам, но при этом она относительно «тяжела» и требовательна к ресурсам. Для настройки библиотеки под свои нужды используется механизм системных вызовов. О нём уже немного писалось тут ispolzuem-libc-newlib-dlya-stm32, поэтому подробно на них останавливаться не буду, а упомяну об особенностях.
Первое это требования к памяти. Все stdio функции из NewLib используют хитрую буферизацию и динамически распределяют память для своих внутренних нужд. А значит им нужна куча и не маленькая, а в целых 3 Кбайт. Плюс примерно 1500 байт на статические структуры и плюс около 500 байт стека. Итого только чтоб напечатать «Hello, world!» нужно порядка 5 Кб оперативки. Что как-бы чуть больше, чем много для STM32-Discovery, на которой я запускал тестовый пример, с её 8 килобайтами. Также при использовании таких функций как printf по зависимостям тянется практически вся stdio плюс функции поддержки плавающей точки. В итоге тестовая прошивка занимает чуть меньше 30 Кб памяти программ. Если отказаться от использования чисел с плавающей точкой, то вместо printf можно использовать её облегченный аналог iprintf. В этом случае объём тестовой прошивки будет около 12 Кб.
Если какая либо из функций stdio не сможет выделить необходимую её память из кучи, то она тихонечко свалится в HardFault без объяснений причин.
Еще один момент это буферизация. Она может несколько запутать. Вызываем:
И… Ничего не происходит. Хотя системные вызовы правильно определены, куча настроена, места в ней хватает.
Наш вывод был спокойно положен в буфер и ждет там либо пока буфер не заполнится, либо не будет принудительно сброшен. Сбросить этот буфер можно с помощью функции fflush, или послав на вывод символ новой строки.
Режимом буферизации можно управлять с помощью функции setvbuf. Например, чтоб отключить буферизацию нужно сделать такой вызов до того, как в целевой поток был произведен какой либо вывод тыц:
При этом потребление памяти кучи уменьшится более чем на 1.5 Кб.
Xprintf
Это реализация функций похожих на printf от товарища Chan-а. Качаем отсюда:
elm-chan.org/fsw/strf/xprintf.html
Библиотека содержит следующие функции для форматированного вывода — аналоги функций стандартной библиотеки Си:
Все функции написаны целиком на Си и их можно использовать практически на любом микроконтроллере. Однако они не учитывают особенностей некоторых МК, например, нет никакой поддержки строк во Flash памяти для семейства AVR, что не добавляет удобства использования. Для использования xprintf необходимо инийиализировать указатель xfunc_out, присвоив ему адрес функции вывода символа. Рассмотрим пример. Компилятор avr-gcc, проект AvrStudio, рассчитан на запуск в симуляторе simulavr.
Здесь для вывода строк из Flash памяти их приходится предварительно скопировать в оперативку.
Из достоинств этой библиотеки можно выделить компактность кода (
1600 байт для приведённого примера) и лёгкость использования на различных платформах и модифицировать. На этом достоинства заканчиваются. Из недостатков стоит отметить
относительно медленную работу, примерно в полтора-два медленнее чем стандартная printf из avr-libc, и несоответствие стандартам — не поддерживаются некоторые флаги (‘пробел’, ‘ #’, ‘+’ ), спецификаторы (i, p n), не считая флагов для чисел с плавающей точкой и т.д.
Потребление стека порядка 38 байт не считая аргументов.
Описание примеров.
AvrGccPrintf
AvrIarPrintf
AvrXprintf
Msp430GccPrintf
Msp430IarPrintf
Stm32Format
IarArm
Работает на STM32 Discovery. Собирается с помощью IAR for ARM.
Итоги.
Преимущества использования стандартной библиотеки Си для форматного вывода:
— Стандартность. Этим всё сказано.
— Доступность. В каком-то виде есть практически в любом Си компиляторе.
— Разделение формата вывода и выводимых данных. Форматную строку легко вынести в отдельный файл/модуль, например для дальнейшей локализации.
— Хорошая функциональность.
Недостатки:
— Полностью стандартная реализация в большинстве случаев слишком «тяжела» для микроконтроллеров.
— Использование лишь одной функции, например printf, тянет за собой значительную часть библиотеки вывода, даже если реально используются только ограниченные возможности.
— Неаккуратное использование функций с форматной строкой может привести к трудно обнаруживаемым ошибкам кодирования.
— В каждом компиляторе используется какой-то свой способ для определения низкоуровневых функций вывода.
Комментарии ( 39 )
Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash
100 байт свободно 🙂
Сам форматный вывод в примерно 1000 с небольшим байт помещается (и для AVR и для MSP430).
Об этом следующая статья будет. С шаблонами и выносом мозга.
Отличная статья – видно, что проделана большая работа.
Позволю себе небольшое дополнение: если говорить о выводе отладочной информации то такая информация как правило нужна только на этапе разработки. В релизном коде часто отладку отключают. Для того чтобы упростить процесс включения/отключения отладки можно применить директивы условной компиляции. Например, такая реализация:
Соответственно в коде для вывода отладки вместо printf используем _DBG, например
Теперь отладка будет выводиться, только если в определен символ _DEBUG, в противном случае вся откатка будет вырезана препроцессором.